Compare commits

...

485 Commits
0.1 ... 1.7

Author SHA1 Message Date
Kevin Papst
85a35a3063 prepare release 1.7 (#1388) 2020-01-19 21:50:29 +01:00
Kevin Papst
e0e391efbe prepare release 1.7 (#1388) 2020-01-19 21:49:41 +01:00
Kevin Papst
9eff89963c prepare release 1.7 (#1388) 2020-01-19 21:49:04 +01:00
Lukas
352fac26f3 added missing raw filter (#1393) 2020-01-19 21:47:40 +01:00
Lukas
8c16bf126a added missing raw filter (#1393) 2020-01-19 21:47:17 +01:00
Lukas
42de968f8f added missing raw filter (#1393) 2020-01-19 21:46:47 +01:00
ikusimakusi
ed8f024e42 added Basque translations (#1382) 2020-01-17 15:28:12 +01:00
Kevin Papst
e534369c5b fix widgets imports (#1387) 2020-01-17 00:02:02 +01:00
Kevin Papst
fed797df93 order user roles and permissions (#1372) 2020-01-16 16:54:42 +01:00
Kevin Papst
6a44dbfe83 detail pages for customers and projects (#1371) 2020-01-16 16:25:28 +01:00
Kevin Papst
28c5357f11 allow to hide rate or duration in html export summary (#1370) 2020-01-14 13:46:07 +01:00
Kevin Papst
a8824f92a1 added support on PHP 7.4 (#1355) 2020-01-07 21:43:33 +01:00
Kevin Papst
e57b69973f re-written exporter for simpler extendability (#1349) 2020-01-07 21:20:45 +01:00
Kevin Papst
d6798eed1e added project start and end date (#1303)
* added sortable js library
* activity in invoice is optional
* added javascript widget for paginated boxes
* fix activity dropdown for globals only
* added timesheet service to reduce code duplication
* use repository to query for teams in dropdowns
* added project validator
* validate project start and end against timesheet
* include begin and end in dynamic form requests for projects
* added timezone and language option to import flag, improve timesheet import speed
* deactivate cross-timezone filter
* add virtual fields to field order list
* composer update
* added param to ignore dates
* position loader icon fixed - fixes #1330
* permission problem when creating a new project - fixes #1340
* remove dev dependencies webserver and thanks bundle
* stop information leak (begin and end date) in duration mode - fixes #1307
* unify timesheet edit dialog for user and admins
* fix security issue, own rates exposed to unauthorized users in multi-update dialog
2020-01-05 02:49:01 +01:00
hmr-it-jr
50383673ca added api endpoint to return instance specific timesheet config (#1335) 2020-01-04 01:44:11 +01:00
Kevin Papst
022dbe32e5 allow empty date in export (#1338) 2019-12-31 20:24:47 +01:00
Kevin Papst
baac4eb698 set rates via batch update (#1326) 2019-12-29 16:52:43 +01:00
Kevin Papst
8aa8f452d5 added exported filter in timesheet view (#1327) 2019-12-29 13:55:15 +01:00
Kevin Papst
54255cf923 validate tag name (#1325) 2019-12-16 16:55:34 +01:00
Kevin Papst
bd3d82d224 fix daterange filter for multi-timezone setups (#1321) 2019-12-16 16:23:03 +01:00
Kevin Papst
b14ad9f3ab renamed translation files from .xliff to .xlf (#1314) 2019-12-09 13:25:31 +01:00
Kevin Papst
261b4646e4 added chinese translations (#1310) 2019-12-09 12:56:38 +01:00
Kevin Papst
d62e1c4468 simplify to add widgets via plugin (#1308) 2019-12-06 23:36:03 +01:00
benzel
5b0266c462 clarified translation (#1309) 2019-12-06 22:05:33 +01:00
Kevin Papst
06118dcabb loose email validation on customer (#1224) 2019-12-02 15:06:06 +01:00
Kevin Papst
984c852ab6 Release 1.6.2 (#1289)
* include user teams in user entity
* prevent unauthorized access via API
* improve teamlead permission handling in team timesheets
* add team data to user entity
* add security tests
* highlight menu for invoice template copy
* unified handling of invoice data across all templates
* access to the current users data in invoice templates
* permission improvement in invoice form
* allow to skip record rows
* allow to add new invoice locations without overwriting the global ones
* allow to order user preferences
* change permission for normal users with access to view_other_timesheets
* properly validate invoice template field length
* allow to replace multiple variables in cell values text
* upgraded office invoice template
* doctrine deprecation fix
* upgrade phpoffice/phpword
* fix future begin check for default rounding rules
* dashboard widget counter: respect visibility and teams - fixes #1161
* fix future begin check for default rounding rules
* added new events for pre and post invoice rendering
* fix permission issue for users without team seeing all records
* prevent error in spreadsheet renderer for empty invoices
2019-12-02 10:57:03 +01:00
Badgie
47414cfd0e Danish translations (#1296) 2019-12-02 00:25:28 +01:00
Kevin Papst
fdf4fdfb6a fix sqlite installer (#1283) 2019-11-26 19:32:16 +01:00
Kevin Papst
5ee0396a02 Release 1.6.1 (#1282) 2019-11-26 17:22:31 +01:00
Kevin Papst
724eb9c658 release 1.6 (#1277) 2019-11-24 15:29:01 +01:00
Kevin Papst
936ab05882 prepare release 1.6 (#1270) 2019-11-24 00:02:43 +01:00
Kevin Papst
817a43b178 added csv importer for timesheet and project data (#1216) 2019-11-23 22:02:55 +01:00
Kevin Papst
0f16f42887 Select2 search enhancement (#1276) 2019-11-23 14:26:35 +01:00
Kevin Papst
641c199c8c API: allow to set user for timesheet (#1259) 2019-11-23 14:21:46 +01:00
Aptevo
04dd6b900f added de_CH language (#1272) 2019-11-23 11:29:29 +01:00
Kevin Papst
536a05210e enable interactive features of Swagger UI (#1273) 2019-11-22 21:59:25 +01:00
Kevin Papst
60d16016e8 allow to configure calendar slot duration (#1269) 2019-11-22 01:30:25 +01:00
Kevin Papst
9222075c7a Added update command (#1268) 2019-11-21 23:15:41 +01:00
1willem
641dcdff85 Dutch translation files (#1226) 2019-11-18 21:48:16 +01:00
Kevin Papst
668649be7a use homepage link to redirect from error page (#1260) 2019-11-18 12:29:21 +01:00
Kevin Papst
0de936d474 fix the password reset page layout (#1261) 2019-11-18 12:28:59 +01:00
Kevin Papst
47bd8b0ce7 added new invoice fields, improved invoice templates (#1258) 2019-11-18 12:01:26 +01:00
Kevin Papst
2287c9951c invoice: money fields without currency, title cleanup (#1254) 2019-11-15 18:54:08 +01:00
Kevin Papst
94c28ebbc5 Update and delete multi timesheets and tags (#1240) 2019-11-15 01:25:31 +01:00
Kevin Papst
b4739f8f03 allow to include bundle configuration in system configuration screen (#1235) 2019-11-14 19:25:38 +01:00
Kevin Papst
c32cd36861 fix reset input for forms (#1251) 2019-11-14 19:09:55 +01:00
Kevin Papst
945562b34d fixes wrong foreign keys on timesheet_tags join table (#1242) 2019-11-14 01:57:58 +01:00
Kevin Papst
af0f89774e User roles and permission management via Admin UI (#1231) 2019-11-10 18:53:56 +01:00
Kevin Papst
c6c4098759 rounding rules via admin screen, round begin when starting record (#1229) 2019-11-10 13:57:05 +01:00
Kevin Papst
fa1c79e15c support different formats in user timesheet exports (#1222) 2019-11-08 16:10:38 +01:00
Kevin Papst
0705c26513 highlight menus on sub-pages (#1220) 2019-11-07 11:19:10 +01:00
Kevin Papst
140fa4b7c0 prepare release 1.5 (#1214) 2019-11-03 17:31:33 +01:00
Kevin Papst
cd88901868 re-style error pages (#1210) 2019-10-31 14:20:53 +01:00
Kevin Papst
8eec97deb5 respect users timezone in statistic widgets (#1207) 2019-10-31 00:17:31 +01:00
Kevin Papst
4331a7c9f4 fix error pages (#1209) 2019-10-31 00:16:25 +01:00
Kevin Papst
f4fcfeba9d dynamic branding options (#1205) 2019-10-30 14:22:34 +01:00
Kevin Papst
48aefb1862 fix avatar path for subdirectory installations (#1201) 2019-10-30 14:00:48 +01:00
Kevin Papst
bda9584346 reformat edit forms (#1192) 2019-10-30 13:59:53 +01:00
Kevin Papst
ed1154af67 fix absolute url (#1200) 2019-10-30 13:45:59 +01:00
Kevin Papst
c368eacd87 fixed renamed filter name, added that disabled users cannot be filtered (#1193) 2019-10-27 13:56:45 +01:00
Kevin Papst
949c59230f improve list views (#1191) 2019-10-25 16:21:30 +02:00
Kevin Papst
1114538f41 duration field in timesheet edit dialog (#1180) 2019-10-24 23:46:22 +02:00
Kevin Papst
b0f83291ee upgraded to phpunit 8 (#1155) 2019-10-24 17:35:05 +02:00
Kevin Papst
e91e4ff430 support search term in API collection requests (#1185) 2019-10-24 17:31:15 +02:00
Ahmed Şeref
4ad290d4bd added turkish translations (#1160) 2019-10-24 17:30:34 +02:00
Kevin Papst
37c6e2c0af added order-date field to project (#1186) 2019-10-24 17:04:21 +02:00
Kevin Papst
b4a45afce9 fix API login in subdirectory installations (#1181) 2019-10-24 11:06:12 +02:00
yayitazale
330ff6eb47 updated spanish translations (#1183) 2019-10-24 10:53:23 +02:00
Adrian Rudnik
c681eb1bdb docker fix to hide details for 500 errors (#1173) 2019-10-21 16:22:29 +02:00
Kevin Papst
de533e7031 Kimai 1.4.2 (#1167) 2019-10-14 12:30:11 +02:00
Kevin Papst
ae6ed3f23f Bugfixes for Kimai 1.4.1 (#1158)
* fix missing translation for preset datatable headers
* better dependency check for avatar service
* show only own teams to teamleads
2019-10-07 16:04:19 +02:00
Kevin Papst
32aa209b75 prepare release 1.4 (#1154) 2019-10-04 07:36:59 +02:00
Kevin Papst
e8c25d8ac5 added avatars, show user teams in list, new team dashboard widgets (#1150) 2019-10-03 13:44:16 +02:00
Kevin Papst
718ad4b398 fix migrations for older database versions (#1148) 2019-09-27 14:52:22 +02:00
Kevin Papst
8d81a394ee updated theme, fix deprecations in events dispatching (#1145) 2019-09-25 19:12:12 +02:00
Kevin Papst
d661c8b54e fix closing search dropdown (#1142)
* using a different library for javascript selects, fixing the closing search dropdown
* fix closing dropdown for daterangepicker
* added missing search button for mobile on customer page
* fix meta fields with same names than existing columns
2019-09-25 18:34:25 +02:00
Kevin Papst
e45e782e7d fix invoice preview (#1129) 2019-09-20 16:00:03 +02:00
Kevin Papst
5aa422dde1 Display meta-fields in datatables (#1116) 2019-09-19 20:28:31 +02:00
Kevin Papst
24cbe3d281 post 1.3 fixes (#1106) 2019-09-17 22:20:36 +02:00
Kevin Papst
13f5e76015 added czech translations (#1075) 2019-09-16 23:19:46 +02:00
Kevin Papst
35c65ba890 composer update (#1104) 2019-09-13 17:16:53 +02:00
Kevin Papst
8f2b82b832 bump version to 1.3 (#1102) 2019-09-12 00:36:48 +02:00
Kevin Papst
7f1a206a52 support installation specific translations (#1099) 2019-09-11 23:18:35 +02:00
Kevin Papst
8c503a226e fix API version string (#1096) 2019-09-11 21:44:14 +02:00
Kevin Papst
cdcf9eaf67 handles session timeouts in modals (#1092) 2019-09-10 17:07:34 +02:00
Kevin Papst
a651e55dc9 added table-column ordering (#1086) 2019-09-09 23:47:42 +02:00
Kevin Papst
d041a3f4f9 improved tag auto-completion and added tag management (#1083) 2019-09-04 23:47:33 +02:00
Kevin Papst
3ea05cb705 support multiple invoice repositories (#1084) 2019-09-04 22:50:53 +02:00
hmr-it-jr
3f27ab1e44 added mail icons (#1077) 2019-09-04 19:49:03 +02:00
Kevin Papst
e99b170d0a refactored search with free search term support (#1064) 2019-09-04 18:54:06 +02:00
Kevin Papst
49e1a1c410 use timesheet description in invoices (#1079) 2019-09-04 10:54:21 +02:00
Kevin Papst
93ab3d5666 post release 1.2 fixes (#1068)
* fix release command
* fix permission name in UPGRADING
* added missing korean calendar translations
* fixed entry.total for invoice with fixed rates
2019-09-03 11:59:34 +02:00
Kevin Papst
94f8159fdc prepare release 1.2 (#1059) 2019-08-27 18:37:42 +02:00
Kevin Papst
8fb0b1445a added korean translations (#1039) 2019-08-26 15:00:59 +02:00
Kevin Papst
15673555a2 added API functions to save meta fields (#1063) 2019-08-26 14:30:39 +02:00
Kevin Papst
ceeec7b8a6 improved invoice handling for fixed rates (#1058) 2019-08-24 00:02:16 +02:00
Kevin Papst
46e5650882 refactored invoice and export screens (#1046) 2019-08-22 18:56:21 +02:00
Kevin Papst
405ea0076b fix empty user language (#1054) 2019-08-21 11:16:44 +02:00
Kevin Papst
8cd7f7de3f do not delete team if linked entity is deleted (#1053) 2019-08-21 01:04:58 +02:00
Kevin Papst
7cd17154fc support creating new roles (#1050) 2019-08-20 14:02:15 +02:00
Kevin Papst
299fcb091a added documentation links to all screens (#1044) 2019-08-19 12:53:36 +02:00
Kevin Papst
025c6b0e7b added css class to target running records in timesheet tables (#1042) 2019-08-16 22:34:41 +02:00
Kevin Papst
388c3ec188 reduce field size to 150 chars to prevent index out of length (#1031) 2019-08-16 16:57:09 +02:00
Kevin Papst
b6d9bb0d2a fix team opens in modal (#1041) 2019-08-16 16:55:56 +02:00
Kevin Papst
561ec3b1e9 added team permissions (#996) 2019-08-16 01:22:33 +02:00
Kevin Papst
9611646bc6 added decimal format variable for invoice duration (#1037) 2019-08-15 21:31:13 +02:00
Kevin Papst
b5972bb680 added invoice variable to display month numerical (#1033) 2019-08-14 23:30:45 +02:00
Kevin Papst
455861ec69 copy timesheet meta fields on restart record (#1030) 2019-08-13 18:41:20 +02:00
trentks
5affa0de82 fix various minor english grammar translations (#1002) 2019-08-08 23:04:02 +02:00
Kevin Papst
e11a2a6089 upgraded FosUserBundle and Symfony 4.3 (#1005) 2019-08-02 16:52:30 +02:00
Kevin Papst
614a50ce0b added command to show plugin versions (#994) 2019-08-02 08:53:51 +02:00
Kevin Papst
66594b286a remove default role from user filter and role select (#993) 2019-08-02 08:31:45 +02:00
Kevin Papst
8e06f94bdd composer prod dependencies and commands (#986) 2019-08-01 23:21:43 +02:00
trentks
b0e03bb0b8 Fix english translation for 'info' (#995) 2019-08-01 23:01:02 +02:00
Kevin Papst
22e30b1871 fix activity import Kimai v1 (#1001) 2019-08-01 21:38:28 +02:00
Kevin Papst
8104a67e3e travis builds with mysql (#924) 2019-08-01 19:25:38 +02:00
Kevin Papst
654ffda454 respect users timezone when populating working-time dashboard widget (#988) 2019-07-30 23:57:20 +02:00
cdahinten
3f51437569 optimize DE translations (#950) 2019-07-29 13:31:00 +02:00
Kevin Papst
1bd784eef3 added calendar slot_duration config (#989) 2019-07-26 23:51:29 +02:00
yayitazale
0d11aded40 spanish update - more natural translations (#945) 2019-07-25 18:26:39 +02:00
Kevin Papst
00925d38aa fix migration for sqlite users (#985) 2019-07-25 18:00:53 +02:00
Kevin Papst
12803b3957 prepare release 1.1 (#983) 2019-07-24 19:38:46 +02:00
Kevin Papst
7583498735 added invoice calculator to sum by day (#982) 2019-07-24 18:36:06 +02:00
Kevin Papst
53eda31179 project grouped invoice calculator (#892) 2019-07-24 17:45:51 +02:00
Kevin Papst
56e276939b trigger template events in authentication layout as well (#981) 2019-07-24 15:32:46 +02:00
Kevin Papst
39abccb35d dashboard is missing in initial view (#974) 2019-07-24 14:47:07 +02:00
Toby Batch
dae8866053 Fixes regexp for search timesheets by tag (#978) 2019-07-22 15:34:40 +02:00
Kevin Papst
a0e8aa3a43 fixed xss in timesheet description (#962) 2019-07-14 13:58:10 +02:00
Kevin Papst
e68b7da1f8 added slovakian translations (#952) 2019-07-12 15:52:30 +02:00
Marcos Ellys Rocha Honorato
fcce7a531a updated Portuguese (Brazilian) translations (#942) 2019-07-11 00:18:42 +02:00
Kevin Papst
f23dcbec17 run migrations always in kimai:install command (#944) 2019-07-10 19:19:03 +02:00
Kevin Papst
8a1d28563d fix changing project for existing entries (#940) 2019-07-10 19:17:34 +02:00
Kevin Papst
c33a87a07c toolbar dropdown and visibility improvements (#933) 2019-07-09 16:34:11 +02:00
Toby Batch
b833e1ce27 pre-populate tags for new timesheet records via request params (#930) 2019-07-09 16:32:40 +02:00
Kevin Papst
6de1a55d73 updated swedish translations (#934) 2019-07-09 14:02:11 +02:00
Kevin Papst
cbf9086e10 performance improvements (#927) 2019-07-09 03:40:48 +02:00
Kevin Papst
eb4aa23577 allow non-interactive installation (#932) 2019-07-09 03:03:27 +02:00
Kevin Papst
bf9ae44775 fix LDAP edge-case initialization error (#931) 2019-07-08 20:35:41 +02:00
Kevin Papst
f0822fa2b1 prepare release 1.0.1 (#922) 2019-07-05 19:25:06 +02:00
Kevin Papst
f966d34929 updated all composer packages (#920) 2019-07-05 15:07:03 +02:00
Kevin Papst
d51136f8f4 cleanup prod log config (#921) 2019-07-05 13:48:24 +02:00
Kevin Papst
19843760e5 allow to configure default values for users (#919) 2019-07-05 02:42:50 +02:00
Kevin Papst
3457f93ed0 fix 500 happening in error pages (#914) 2019-07-04 17:00:55 +02:00
Kevin Papst
625ac2f1d7 fix default calendar config (#918) 2019-07-04 16:59:57 +02:00
Kevin Papst
061581c4ff use pre-defined icons in menu as well (#913) 2019-07-04 14:59:03 +02:00
Kevin Papst
a0116422c6 remove scrutinizer (#915) 2019-07-04 14:57:07 +02:00
yayitazale
94e49ea86f updated Spanish translations (#906) 2019-07-02 12:10:38 +02:00
heddi.nabbisen
040409eac7 Update japanese translation (#905) 2019-07-02 12:07:01 +02:00
Kevin Papst
baa2fca7ca Release 1.0 (#885) 2019-07-02 02:36:46 +02:00
infeeeee
1fd8139214 Update Hungarian translation (#902) 2019-07-02 00:14:31 +02:00
Kevin Papst
db5f9f639e fix selectbox is not resettable (#901) 2019-07-02 00:14:06 +02:00
Kevin Papst
90980f40f5 allow to disable user preferences by permission (#895) 2019-07-01 19:37:20 +02:00
Kevin Papst
94d68c8b64 fixed short invoice (#891) 2019-06-30 18:38:02 +02:00
Kevin Papst
834a0a873f fix migration for sqlite (#881) 2019-06-30 15:16:44 +02:00
Kevin Papst
d8621f0b7a support custom fields for timesheets, customers, projects and activities (#871) 2019-06-26 00:39:05 +02:00
Kevin Papst
994c671fd8 order users by username (#880) 2019-06-26 00:35:54 +02:00
Kevin Papst
7b95750dcf added LDAP config to set filter for finding user attributes (#877) 2019-06-23 12:38:11 +02:00
Kevin Papst
9fbbaf9b88 performance tuning on timesheets (#874) 2019-06-20 16:05:31 +02:00
Jacob Blanco
bb8572a364 fix LDAP hydration with multi-value email field (#862) 2019-06-20 15:35:22 +02:00
Kevin Papst
bdd05a275d use default_begin in duration only mode (#873) 2019-06-20 01:38:28 +02:00
Toby Batch
dcdbf23b6d load plugins in test env (#864) 2019-06-19 23:10:06 +02:00
Kevin Papst
ce6ad0bbb8 export tags in spreadsheets (#860) 2019-06-16 15:35:22 +02:00
Kevin Papst
59d2946b91 add "duration + fixed start time" tracking mode (#859) 2019-06-15 23:04:53 +02:00
Kevin Papst
92f3f4ee57 added config to brand Kimai with logo and company name (#856) 2019-06-13 19:06:02 +02:00
Kevin Papst
3311f17bbb users weekly stats as bar-chart in dashboard (#847) 2019-06-13 18:38:49 +02:00
yayitazale
e1f862507e Spanish translate updated (#852) 2019-06-13 13:41:26 +02:00
Thierry Pasquier
7e040ac216 Update of the French translation (#850) 2019-06-13 13:02:12 +02:00
Kevin Papst
515c19422d improved fixture data for demo (#848) 2019-06-11 14:55:25 +02:00
Kevin Papst
5702d7afa8 added hourly and money budgets to activity, project and customer (#843) 2019-06-11 13:18:57 +02:00
Kevin Papst
833968a87d fix LDAP install for systems without ldap extension (#846) 2019-06-11 01:25:49 +02:00
Kevin Papst
0c0e9c2f71 LDAP authentication support (#815) 2019-06-07 22:48:39 +02:00
Kevin Papst
bcf1ebd778 fixed pdf temp directory (#845) 2019-06-07 18:17:45 +02:00
heddi.nabbisen
4c82f9767f updated japanese translations (#837) 2019-06-05 13:57:08 +02:00
Kevin Papst
ab405f671e updated dockerfile (#840) 2019-06-05 13:56:31 +02:00
Kevin Papst
325d204332 added installation command (#838) 2019-06-04 23:51:37 +02:00
Kevin Papst
97feed0be6 updated frontend stack to latest versions (#729) 2019-06-03 01:43:49 +02:00
Kevin Papst
19636582ab use codecov for code coverage (#833) 2019-06-01 11:53:27 +02:00
Kevin Papst
393abe9e62 phpstan level 3, fixed deprecations, code cleanup (#811) 2019-05-28 12:24:21 +02:00
Kevin Papst
d9dca96a32 added punch-in punch-out / time-clock mode (#812) 2019-05-27 01:36:43 +02:00
Kevin Papst
ebff4a765a refactored permissions for simpler customizations (#808) 2019-05-25 17:23:15 +02:00
Kevin Papst
d18019159b fix tag query to show all tags of an item, even if not included in query (#810) 2019-05-25 16:27:01 +02:00
Kevin Papst
46905e9504 allow markdown for customer, project and activity comments (#807) 2019-05-25 00:29:23 +02:00
Kevin Papst
8b1c15ccb6 added github funding config (#804) 2019-05-24 13:16:25 +02:00
Kevin Papst
8873947a1b added phpstan for code analyzes (#806) 2019-05-24 01:11:03 +02:00
Kevin Papst
cdef22b4d1 added SECUTRITY.md for reporting vulnerabilities (#805) 2019-05-23 21:42:52 +02:00
Kevin Papst
29209105d3 improved html export (#802) 2019-05-23 18:19:56 +02:00
Kevin Papst
fea3495098 lock exported timesheets (#798) 2019-05-22 22:28:29 +02:00
Kevin Papst
871b2e52c7 use dropdown for entity actions on detail pages (#801) 2019-05-21 23:21:26 +02:00
Kevin Papst
ec174a38a9 timesheet controller refactoring (#796) 2019-05-19 23:52:43 +02:00
Kevin Papst
46ff78a4c4 fix phpunit annotations (#794) 2019-05-19 17:05:07 +02:00
Kevin Papst
d2ad87d09c improved calendar (#784) 2019-05-19 16:16:20 +02:00
Kevin Papst
1903d6fb77 fix column and max key length (#789) 2019-05-14 23:09:33 +02:00
Kevin Papst
bbe1f9beda restart via API allows to copy description (#782) 2019-05-13 17:32:01 +02:00
heddi.nabbisen
1f5710eaba updated japanese translations (#779) 2019-05-13 13:16:27 +02:00
Kevin Papst
392f8239f5 bugfix for API selects (#780) 2019-05-12 17:22:24 +02:00
Kevin Papst
5c227888f2 delete timesheets via api (#776) 2019-05-12 03:26:31 +02:00
Mathias
e29e183e84 added tags for timesheets (#604) 2019-05-12 01:40:04 +02:00
Kevin Papst
f31118292c translate date picker and charts (#775) 2019-05-12 00:56:00 +02:00
heddi.nabbisen
5f4a5364b0 add japanese translation (#774) 2019-05-11 22:37:36 +02:00
Kevin Papst
09da7cd242 javascript and api to stop and display active records (#772) 2019-05-10 13:45:09 +02:00
Björn Weinbrenner
e619b5fd31 added more entity actions (#757) 2019-05-10 02:10:37 +02:00
Kevin Papst
b1855447b8 recent activities via API (#761) 2019-05-07 22:24:28 +02:00
Kevin Papst
98f386dad6 refactored javascript to ES6 classes (#759) 2019-05-06 15:22:52 +02:00
Kevin Papst
80af3fb010 fix toolbar requests for empty choice (#758) 2019-05-05 08:31:13 +02:00
Kevin Papst
6e224c63ef improve cross database engine compatibility (#756) 2019-05-05 03:09:02 +02:00
Kevin Papst
aa113cc300 dynamically update title and visible durations for running records (#752) 2019-05-05 03:01:25 +02:00
Kevin Papst
65df1ff909 colors for visual grouping of customer, projects and activities (#751) 2019-05-04 01:45:52 +02:00
Kevin Papst
e92c2d168e entity action buttons re-designed (#746) 2019-05-03 21:10:16 +02:00
Kevin Papst
455d772095 moved shared entity fields and functions to trait (#750) 2019-05-02 18:03:40 +02:00
Kevin Papst
e80973c4d2 refactored column visibility modal to ES6 (#733) 2019-05-01 22:18:57 +02:00
Kevin Papst
721ecdbf3d prevent jumping icons in navbar between collapsed and open state (#739) 2019-04-30 18:50:31 +02:00
Kevin Papst
e6b3d4349e navbar block cleanup (#742) 2019-04-30 18:10:25 +02:00
Kevin Papst
b865add1b3 translate time-tracking in login screen and browser title (#738) 2019-04-30 17:52:19 +02:00
Kevin Papst
b35022d161 use bootstrap tooltips instead of own solution (#741) 2019-04-30 17:00:10 +02:00
Kevin Papst
62b091c77b always query timesheets by begin date (#740) 2019-04-30 14:31:10 +02:00
Kevin Papst
cdb112df05 theme update for swedish auth screens (#737) 2019-04-30 14:29:52 +02:00
Kevin Papst
6dc4fa0811 fix tests that failed around midnight (#735) 2019-04-29 02:29:40 +02:00
Kevin Papst
fb17af1afd prepare release 0.9 (#730) 2019-04-29 01:48:27 +02:00
Kevin Papst
b8811924c3 reduce config complexity by converting php format to js format (#734) 2019-04-29 00:24:23 +02:00
Kevin Papst
1b0b45649d added swedish (#732) 2019-04-28 23:55:27 +02:00
Kevin Papst
e49bd05ae6 updated authorization screen styles (#727) 2019-04-27 19:58:32 +02:00
Kevin Papst
32f0d80729 configs: overwrite via prepend, mode instead of duration_only (#715) 2019-04-26 16:14:28 +02:00
Simone Gasparini
f0f757cf75 allow dynamic export types (#703) 2019-04-25 19:33:06 +02:00
Kevin Papst
ccf4e32523 added delete timesheet API endpoint (#726) 2019-04-25 18:27:58 +02:00
Kevin Papst
56fe14c451 UI updates - new user menu, updated about and profile screen and more (#723) 2019-04-25 17:50:28 +02:00
Kevin Papst
28801bf1ea remove invalid cache header (#722) 2019-04-24 18:14:08 +02:00
Kevin Papst
460391136f API: changed date-format, camelCase instead of snake_case, null values, update and create for customer and project (#718) 2019-04-24 18:13:33 +02:00
Kevin Papst
215d4fc8bf apply users timezone to toolbar queries (#716) 2019-04-22 16:21:22 +02:00
horlabs
46cb021260 API: added activities create and update (#717) 2019-04-22 16:16:06 +02:00
Kevin Papst
f9c8028ea1 updated to Symfony 4.2 (#710) 2019-04-21 01:41:08 +02:00
Kevin Papst
32b36f42f0 remove unused role ROLE_CUSTOMER (#712) 2019-04-19 18:42:25 +02:00
Kevin Papst
48f3ad842d fix validation for create user command (#709) 2019-04-17 00:20:38 +02:00
Simone Gasparini
3251b26d9e added floor and ceil rounding modes (#695) 2019-04-16 17:36:47 +02:00
Simone Gasparini
136df3724f removed active flag in user preferences form for own profile (#696) 2019-04-16 17:30:51 +02:00
Kevin Papst
0d6b8486c4 remove the total counter from all admin pages (#708) 2019-04-16 17:20:53 +02:00
Kevin Papst
3a4aa5a001 added plugin screen (#671) 2019-04-08 18:38:56 +02:00
Kevin Papst
5bea9915f5 improved activity-project assignment in importer (#679) 2019-04-05 23:29:16 +02:00
hmr-it-jr
26de367bc5 added content-type to allowed cors header for api (#672) 2019-04-01 21:51:46 +02:00
infeeeee
3c361c9414 Updated hungarian translation (#661) 2019-03-26 15:37:28 +01:00
Kevin Papst
0d4fdc38e4 remove mailer url from about debug info (#657) 2019-03-23 13:31:52 +01:00
Kevin Papst
6f7035e4cf fixing default env var for cors header (#656) 2019-03-23 13:24:15 +01:00
Kevin Papst
38ae013e03 added cors bundle to allow external api access (#655) 2019-03-23 11:04:55 +01:00
Kevin Papst
13d9f8f3f7 added database driven system configurations with admin screen (#647) 2019-03-22 20:58:38 +01:00
Kevin Papst
e990ae4800 fix modal autofocus problem, datepicker not shown on begin date (#653) 2019-03-22 15:00:59 +01:00
Kevin Papst
6a68dc6e4f added route to redirect locale to configured homepage (#654) 2019-03-22 14:37:54 +01:00
Kevin Papst
cfc19dca25 fix pdf export cache directory (#650) 2019-03-22 14:37:25 +01:00
Kevin Papst
50318a8fc5 added command to create release packages (for FTP user) (#642) 2019-03-18 11:57:26 +01:00
Kevin Papst
bde791fa55 added begin, end and export filter for API timesheets (#639) 2019-03-14 03:34:54 +01:00
Kevin Papst
2e6f3ed864 delete directory with invalid case (#643) 2019-03-13 18:50:34 +01:00
Kevin Papst
fb6bf4319f removed dynamic database prefix (#641) 2019-03-13 15:17:18 +01:00
Kevin Papst
19bfff9899 set autofocus attribute on form fields for better keyboard support (#640) 2019-03-13 00:09:02 +01:00
Kevin Papst
f4d53e9006 replaced delete confirm dialog with modal (#638) 2019-03-12 23:24:22 +01:00
Kevin Papst
46655ad462 support subdirectories and subdomains for assets (#636) 2019-03-12 01:02:42 +01:00
blueowl04
1b6b957b23 allow non-interactive timezone conversion (#637) 2019-03-11 18:44:54 +01:00
Kevin Papst
0f6334314e removed inline documentation (#624) 2019-03-11 15:10:51 +01:00
Kevin Papst
3ac72a5c89 enhanced plugin support (#634)
- refactored admin controller and templates
- plugin support for entity actions
- changed column mail to email in customer table
- refactored theme events
2019-03-11 14:46:30 +01:00
Kevin Papst
519be2d5a2 remove invisible entities from toolbars (#628) 2019-03-08 20:22:58 +01:00
Kevin Papst
e8bc45f7dd fix random order in recent activities (#631) 2019-03-08 16:09:37 +01:00
Kevin Papst
76c3c5f2f6 fix for firefox datepicker focus problem in modals (#630) 2019-03-08 01:16:10 +01:00
Kevin Papst
54cd30c564 added support for Ante meridiem and Post meridiem format (#615) 2019-03-06 10:59:31 +01:00
Kevin Papst
e260dd84ad Improve create and start permission handling (#613) 2019-03-06 10:24:27 +01:00
Kevin Papst
bce85b04d6 added filter for global activities (#614) 2019-03-05 01:42:06 +01:00
Kevin Papst
642db480ce added new rounding mode: closest (#611) 2019-03-03 20:33:35 +01:00
Kevin Papst
bea4be881b Edit and create timesheet records in modal (#603)
- added loading indicator for datatables
- fix multiple reloads of datatable content
2019-03-03 19:43:12 +01:00
Kevin Papst
86832fb703 code cleanup (#602) 2019-02-26 02:26:37 +01:00
Kevin Papst
03aa4c9ddc Plugins support (#592) 2019-02-24 15:54:38 +01:00
Kevin Papst
36a158a805 simplified theme settings and standard layout (#586) 2019-02-23 13:51:44 +01:00
Kevin Papst
32fbebd469 order datatable items by name (#594) 2019-02-23 02:27:56 +01:00
Kevin Papst
3d4b0c3fd6 UI improvements for datatables (#593) 2019-02-22 21:25:43 +01:00
Kevin Papst
1f7ec6769a release 0.8.1 (#595) 2019-02-22 18:05:45 +01:00
Kevin Papst
239be527b8 post release fixes (#584) 2019-02-22 17:20:41 +01:00
Kevin Papst
fd4a87e3a6 Support foreign keys with SQLite (#590) 2019-02-20 20:06:19 +01:00
Kevin Papst
3f535cc506 stop navbar from breaking on mobile views (#583) 2019-02-20 00:35:04 +01:00
Kevin Papst
0b57bd51ca fix sql for only_full_group_by (#581) 2019-02-19 13:39:08 +01:00
Kevin Papst
49c78ddd9d updated CHANGELOG (#580) 2019-02-19 12:49:02 +01:00
Kevin Papst
47b7046743 release 0.8 (#567) 2019-02-19 11:03:11 +01:00
Kevin Papst
b4f1cae323 fix pagination in combination with daterange picker (#568) 2019-02-14 01:09:23 +01:00
Kevin Papst
17856e0b96 handle deleted user during import from v1 (#569) 2019-02-14 00:26:54 +01:00
Kevin Papst
8005679e60 allow to query other users timesheets via api (#563) 2019-02-14 00:04:27 +01:00
Kevin Papst
ef33233624 fix timezone problems in timesheet forms (#555) 2019-02-13 21:37:54 +01:00
Kevin Papst
2c6f57c7ce daily stats in timesheet (#552) 2019-02-13 15:36:59 +01:00
Kevin Papst
fd60d5bb17 Updated HTML invoice templates (#560) 2019-02-12 15:35:33 +01:00
Kevin Papst
742e6eb3a3 updated composer packages (#559) 2019-02-11 20:38:40 +01:00
infeeeee
331ea586f1 updated php requirements in dev-documentation (#551) 2019-02-11 16:28:27 +01:00
infeeeee
afb0eabaa4 styles: year selector width, recent activities menu width (#549) 2019-02-09 23:13:47 +01:00
Kevin Papst
afa0aa5b3a improve recent activities (#550) 2019-02-09 23:01:35 +01:00
Kevin Papst
1f99f69ecd fix daterange-picker for fr, hu and ar (#547) 2019-02-08 20:02:23 +01:00
Kevin Papst
d00596d297 convert timesheets to UTC with support for user timezone (#372) 2019-02-08 18:24:33 +01:00
Kevin Papst
8dcc18dde3 improved installation docs (#548) 2019-02-08 18:16:53 +01:00
Sanjit Patel
616d514d5a Use Symfony formatter for currency symbol placement (#542) 2019-02-08 16:56:47 +01:00
Kevin Papst
b2587c71b8 do not limit timesheet-export page size (#545) 2019-02-07 23:43:20 +01:00
Kevin Papst
816866549c added export module (#538) 2019-02-05 21:37:33 +01:00
Kevin Papst
062735c9e3 added daterange-picker and localized date-inputs (#540) 2019-02-03 21:24:30 +01:00
Kevin Papst
aae9183de6 highlight current day in date-picker (#527) 2019-02-02 15:27:02 +01:00
Kevin Papst
7f3e2d453f updated documentation (#536) 2019-02-01 16:33:55 +01:00
Kevin Papst
1b42aed34c support remote data in beta-test selectpicker (#529) 2019-01-31 12:59:25 +01:00
Kevin Papst
974806d145 Improve allowed running records (#528) 2019-01-29 20:45:04 +01:00
Kevin Papst
fe4c3c10d6 added configurable view after login (#523) 2019-01-29 02:15:12 +01:00
Kevin Papst
67dab35b57 prepare release 0.7 (#514) 2019-01-28 01:40:42 +01:00
Kevin Papst
55d8bc9ba5 pagination without reload while keeping filters applied (#521) 2019-01-23 18:48:17 +01:00
Kevin Papst
572f01ed74 go back to calendar after editing and creation of time-records (#519) 2019-01-23 17:30:45 +01:00
Kevin Papst
ee32d6f926 fix wrong include filename in user registration (#520) 2019-01-23 14:16:37 +01:00
Kevin Papst
89abaa9dda fetch toolbar results without page reload (#518) 2019-01-23 02:45:33 +01:00
Kevin Papst
a9ece209ae Form and theme improvements (#513) 2019-01-21 13:10:53 +01:00
Lukas
47ac50b4d0 Only one running timesheet: automatically stop others (#386) 2019-01-21 12:49:22 +01:00
Kevin Papst
5539f68a54 validation for future and negative times (#512) 2019-01-21 01:51:57 +01:00
Kevin Papst
5b1ec1eb8f alphabetical order for selectboxes (#510) 2019-01-19 19:24:29 +01:00
Kevin Papst
a609cbf84a export team timesheets (#508) 2019-01-19 00:25:00 +01:00
Kevin Papst
1ad440f488 support template variable entry.row for docx if description is not used (#504) 2019-01-18 16:19:02 +01:00
Kevin Papst
64330e6cc4 display invoice entries in ascending order (#505) 2019-01-18 14:32:08 +01:00
Kevin Papst
03421c099a fix broken sql in activity admin (#506)
* fix broken sql in activity admin
* pre-select customer for existing activities
2019-01-18 14:30:20 +01:00
Lukas
b5c4084452 support anchor links for inline documentation (#482) 2019-01-17 23:30:02 +01:00
Kevin Papst
5d5b971f57 do not display admin menu if it has no children (#500) 2019-01-17 01:25:00 +01:00
Lukas
825d31af20 improve login screen and simplify customizations (#493) 2019-01-17 01:01:30 +01:00
Toby Batch
7880f980a8 Better composer install in docker (#481) 2018-12-21 14:32:04 +01:00
Steve Colson
137fd69f47 Update dockerfile for https in composer installer (#478) 2018-12-20 13:01:09 +01:00
Kevin Papst
ed8efcf139 support line breaks in docx (#466) 2018-12-07 11:24:39 +01:00
Kevin Papst
7fa3b67cc4 updated README and installation docu (#465) 2018-12-05 23:47:59 +01:00
Kevin Papst
93c500fbf2 fix segmentation fault - rollback composer dependencies (#463) 2018-12-05 00:01:48 +01:00
Kevin Papst
c58c12ea3d fixed null project for advanced invoice calculator (#462) 2018-12-03 22:36:38 +01:00
Kevin Papst
35305ad107 updated all composer packages (#459) 2018-12-03 20:24:22 +01:00
Kevin Papst
589d750e59 allow decimals in users hourly rate (#460) 2018-12-03 20:23:52 +01:00
Kevin Papst
43b488f21f moved some packages to dev requirements (#456) 2018-12-01 19:53:44 +01:00
Matheus
54d06462fc added portuguese translations (#446) 2018-11-28 18:22:05 +01:00
Kevin Papst
2d6109d587 simplify timesheet edit form if only one customer is existing (#443) 2018-11-28 15:05:12 +01:00
Kevin Papst
8fddf627bf added configurable permission system (#424) 2018-11-26 13:20:32 +01:00
Kevin Papst
0334f6ce86 added project variables for invoice templates (#439) 2018-11-23 15:56:06 +01:00
Kevin Papst
025558e58e fix restart timesheet button (#436) 2018-11-22 21:48:05 +01:00
Kevin Papst
f74547008d prepare release 0.6.1 (#433) 2018-11-19 16:17:28 +01:00
Kevin Papst
67dc1c7640 fix migration for MySQL (#430) 2018-11-19 15:24:00 +01:00
Kevin Papst
40322c6de4 fix importer for MySQL #428 (#429) 2018-11-19 00:32:12 +01:00
Kevin Papst
417675b3df prepare release 0.6 (#412) 2018-11-18 20:45:00 +01:00
Lukas
321ac74b6a added parsedown extension for header ids (#388) (#399) 2018-11-18 20:31:37 +01:00
Kevin Papst
b1c06eed7b API endpoint for Timesheet entries (#332) 2018-11-18 19:43:58 +01:00
Kevin Papst
8cb52e22b1 fix column visibility if unconfigured / no cookie existing (#423) 2018-11-18 02:13:44 +01:00
Kevin Papst
a073064900 redirect to user language after profile update (#421) 2018-11-15 12:06:57 +01:00
Kevin Papst
07cceca67e fix duration and rate for html invoices (#414) 2018-11-12 23:32:05 +01:00
Kevin Papst
540318fcd0 toolbar - always allow to choose global activities (#410) 2018-11-12 15:00:25 +01:00
Kevin Papst
e9cbb872d0 added further timesheet related invoice fields (#411) 2018-11-12 12:19:57 +01:00
Kevin Papst
f9dc42d1f9 option to move entries from one entity to another upon deletion (#409) 2018-11-12 00:01:49 +01:00
Kevin Papst
1dabeedaa0 show toolbar filter after submission (#407) 2018-11-11 15:32:41 +01:00
Kevin Papst
dc70ff128c full configurable data columns in all screen sizes (#404) 2018-11-10 22:22:08 +01:00
Kevin Papst
b012b7170b added global activity support in importer (#402) 2018-11-10 03:05:33 +01:00
Toby Batch
0a1c2d8440 updated docker documentation #397 (#398) 2018-11-09 14:07:49 +01:00
Toby Batch
d72596f000 added links to docker hub and production docker repo (#396) 2018-11-08 23:07:33 +01:00
Lukas
33029b2a11 fixed docu: issues- & milestone links (#395) 2018-11-07 00:45:58 +01:00
Kevin Papst
bae24dd0ec beta test: added quick search box for customers, projects and activities (#392) 2018-11-06 23:35:09 +01:00
Kevin Papst
db7de3aac1 added global activities (#259) 2018-11-06 23:10:34 +01:00
Kevin Papst
c66dfbc653 added links to further docker images by the community (#394) 2018-11-06 22:30:44 +01:00
Kevin Papst
51c207e025 fixed broken toolbar form with empty date (#391) 2018-11-05 22:26:57 +01:00
Lukas
e44e2442af updated invoice documentation (#387) 2018-11-04 15:55:34 +01:00
infeeeee
86f40083ef Added Hungarian translation (#385) 2018-11-02 23:38:12 +01:00
Kevin Papst
f45fbbce60 improved docs and cross-linked docker (#383) 2018-11-02 16:11:40 +01:00
srdco
c648866cb4 Update installation instructions for non-server (#382) 2018-11-01 12:28:39 +01:00
Kevin Papst
f8adafdabe invoice calculator to group entries by activity (#380) 2018-10-31 15:10:06 +01:00
Toby Batch
ab4a1a0a1d added dockerfile for development (#377) 2018-10-31 13:19:48 +01:00
Kevin Papst
34a3f4e275 about screen with debug information for super-admin (#378) 2018-10-29 15:28:52 +01:00
Kevin Papst
55aedcd4a8 bump version number for development (#375) 2018-10-25 22:27:43 +02:00
Kevin Papst
6633c550f5 user-profile - sidebar links and language settings (#369) 2018-10-23 23:59:38 +02:00
Kevin Papst
0165658659 added config for initial calendar view (#363) 2018-10-21 15:43:16 +02:00
Toby Batch
3075276a52 added default settings for new customer form (#358) 2018-10-21 15:27:43 +02:00
Kevin Papst
b46e752d0a decrease minimum username length to 3 character (#362) 2018-10-19 16:50:03 +02:00
Kevin Papst
ad171e7706 removed deprecated symfony/lts and updated all other packages (#354) 2018-10-12 17:51:38 +02:00
Kevin Papst
92c665436a updated admin lte theme bundle (#350) 2018-10-08 20:58:21 +02:00
Kevin Papst
2dbf433474 fixed redirect route after password reset (#345) 2018-10-07 19:12:59 +02:00
Kevin Papst
56ac642446 arabic translations and locale support in datepicker (#337) 2018-10-07 18:29:12 +02:00
Kevin Papst
2f6c0d2208 updated timesheet and email documentation (#349) 2018-10-07 16:50:20 +02:00
Kevin Papst
a32614fff8 updated required dependencies (#346) 2018-10-07 16:43:54 +02:00
Kevin Papst
53053c127e prepare release 0.5 (#326) 2018-09-27 13:35:03 +02:00
Kevin Papst
a89ef41e81 added copy invoice template action (#331) 2018-09-26 17:50:46 +02:00
Kevin Papst
eee7cb9bf4 fix spreadsheet renderer for invoices with one entry (#328) 2018-09-25 21:16:41 +02:00
Kevin Papst
98dc38ed99 timesheet export (#317)
* remove autocomplete for begin and end
* fix minute display in timesheet-edit form
* toolbar form label in separate row
* added export action
2018-09-24 16:46:10 +02:00
Kevin Papst
7bcd6a6382 added version command and api endpoint (#321) 2018-09-24 15:56:29 +02:00
Kevin Papst
73e0a75585 invoice VAT as float and database key length (#323) 2018-09-24 15:03:51 +02:00
Kevin Papst
71aa899f77 added invoice voter (#316) 2018-09-22 22:38:51 +02:00
Kevin Papst
53f82a808b improved invoices with new renderer (#306)
* added docx renderer and demo template
* added csv renderer and demo template
* added xlsx renderer and demo template
* added ods renderer and demo template
* added user calculator
* added invoice documentation
2018-09-21 01:27:56 +02:00
Kevin Papst
b035fc9ebd add stable controller tests (#305) 2018-09-07 00:44:57 +02:00
Kevin Papst
8cae5e8b3f added fixed-rate and hourly-rate for customer, project and activity (#304) 2018-09-06 23:37:25 +02:00
Kevin Papst
6163f33abf add hourly_rate and fixed_rate to timesheet entries (#302) 2018-09-05 15:27:39 +02:00
Kevin Papst
a636faa4ab added freelancer invoice template (#254) 2018-09-02 21:29:49 +02:00
Kevin Papst
ee4df36735 allow to switch language on any page (#299) 2018-09-02 20:58:19 +02:00
Thierry Pasquier
4ede5463f9 Fix duration_short format for french (#297) 2018-09-02 17:03:38 +02:00
Kevin Papst
4c277fe1b5 allow markdown in timesheet descriptions (#296) 2018-09-02 12:10:49 +02:00
Kevin Papst
9f6079ad8a added option to delete invoice template (#294) 2018-09-02 11:36:04 +02:00
Kevin Papst
6fb6a16246 prepare release 0.4 (#292) 2018-09-01 21:05:29 +02:00
Kevin Papst
3af8070a57 fixed money display in dashboard widgets (#291) 2018-09-01 19:47:03 +02:00
Kevin Papst
ebe007eb78 added configurable formats for duration (#287) 2018-09-01 17:54:17 +02:00
Kevin Papst
55b7e192aa Symfony update 4.1.4 (#290) 2018-09-01 14:23:38 +02:00
Kevin Papst
62f91f4d38 do not allow to stop already stopped timesheets (#289) 2018-09-01 13:00:30 +02:00
Kevin Papst
b1546c0568 Updated russian translations (#286) 2018-09-01 11:20:54 +02:00
Thierry Pasquier
af2d1c3a6e fix french date format and update translations (#279) 2018-08-23 20:46:30 +02:00
Thierry Pasquier
ca893b303a Fix standard french date format 2018-08-19 10:15:24 +02:00
Kevin Papst
142369d651 activated spanish, french and italian 2018-08-18 23:39:40 +02:00
Thierry Pasquier
3241c73e7c Added French translations (#275) 2018-08-18 23:39:40 +02:00
Ivan Castro
d6d9abd05e added Spanish translations (#266)
Spanish translation for kimai
2018-08-18 23:39:40 +02:00
Kevin Papst
85ff2ff9de updated to AdminLTE 2.4.8 (#274) 2018-08-17 23:54:26 +02:00
Kevin Papst
dbbb434723 dashboard widgets are configurable via config (#269) 2018-08-17 23:17:02 +02:00
Ingo Ebel
7804ef83ce fixing migration_v1.md url (#270) 2018-08-13 10:15:34 +02:00
Kevin Papst
fb2d898e4c fixed calendar for empty timesheet descriptions (#265) 2018-08-10 08:19:30 +02:00
Kevin Papst
c6e99b35d5 moved twig globals to dynamic compiler pass (#264) 2018-08-10 00:11:48 +02:00
Kevin Papst
3dcd38eca7 Added config setting to disable password-reset and user-registration (#261) 2018-08-09 20:44:49 +02:00
Kevin Papst
39ea44fdc3 fixed .htaccess for apache users (#260) 2018-08-08 23:35:02 +02:00
Kevin Papst
2f84f3751a Added basic API endpoints (#258) 2018-08-08 23:10:45 +02:00
Kevin Papst
a7780dac9e added italian translation (#253) 2018-08-08 00:00:24 +02:00
Kevin Papst
e549a0824f Hotfix for Symfony Flex incompatibility with latest composer changes (#257) 2018-08-07 20:17:56 +02:00
Kevin Papst
8b8583d48b Merge branch 'translations' into master 2018-07-31 17:37:59 +02:00
Kevin Papst
a8a8424ced Added delete user feature #225 (#249) 2018-07-31 13:16:40 +02:00
Kevin Papst
0996eca3c5 dashboard widgets from event (#246) 2018-07-28 23:18:28 +02:00
Kevin Papst
e6f8bc8ae2 Added timesheet-calendar view (#236) 2018-07-27 20:19:56 +02:00
Kevin Papst
5ccc1c991b Improved data fixtures with Faker (#234) 2018-07-24 19:24:58 +02:00
Kevin Papst
20a2d94996 prepare release 0.3 (#231) 2018-07-23 00:43:30 +02:00
Kevin Papst
f41eea2768 Easier creation of Tasks/Projects/User (#230) 2018-07-22 23:18:47 +02:00
Kevin Papst
2cf7d976d3 added email configuration docs (#228) 2018-07-22 18:46:07 +02:00
Kevin Papst
194a1f204c Show revenue only for ROLE_ADMIN (#227) 2018-07-22 15:31:59 +02:00
Kevin Papst
75246e9db2 Integrated FOSUserBundle (#216) 2018-07-21 22:36:36 +02:00
Kevin Papst
2d2525cff2 added favicons and webapp metadata (#211) 2018-07-15 21:15:24 +02:00
Kevin Papst
5160b640c4 fixed responsiveness (#220) 2018-07-15 16:07:35 +02:00
Kevin Papst
d18ab3fe75 added yunohost installation link #194 (#212) 2018-07-13 15:15:26 +02:00
Kevin Papst
d15cf72474 added auto-script to install assets (#214) 2018-07-13 15:14:39 +02:00
Kevin Papst
d8f7a5dfa4 added FAQ and database requirement docu #196 (#210)
* added FAQ and database requirement docu - fixes #196
* added FTP installation docu - see #199
* moved migration guide from Kimai v1 to its own docu chapter
2018-07-12 22:34:34 +02:00
Kevin Papst
e68bef7ea7 Updated to AdminLTEBundle 1.0 #201 (#206) 2018-07-12 20:37:08 +02:00
Kevin Papst
7a0d690101 replaced AvanzuAdminTheme with AdminLTE bundle #201 (#202) 2018-07-09 18:25:08 +02:00
Kevin Papst
0eb8ae5117 allow empty invoice template terms #188 (#189) 2018-06-29 19:49:01 +02:00
Kevin Papst
b05d416e80 improved install docs and composer (#187) 2018-06-29 18:08:54 +02:00
Kevin Papst
497a8f7a11 added lock-bot config #183 (#186) 2018-06-29 16:41:14 +02:00
Kevin Papst
3bdf0dc6a6 composer package name (#185) 2018-06-28 23:44:02 +02:00
Kevin Papst
b299302da4 added dynamic column filter #174 (#184) 2018-06-28 22:56:04 +02:00
Kevin Papst
2f0c64ae17 added browser info to readme (#177) 2018-06-26 20:18:08 +02:00
Kevin Papst
b58de9be9c updated to font-awesome 5 (#181) 2018-06-26 19:58:20 +02:00
Kevin Papst
d4a46cf257 language specific money and date display (#180)
* improved date translation
* language specific date and money display
2018-06-25 16:41:12 +02:00
Kevin Papst
f563e0703d prepare v0.2 release #177 (#178)
* added CHANGELOG.md and support for github-changelog-generator
2018-06-24 01:18:55 +02:00
Kevin Papst
c3251ee435 various documentation updates (#176)
* updated README
* improved CONTRIBUTING guidelines
* improved wording on developer docu
2018-06-23 21:20:20 +02:00
Kevin Papst
e9e61afc76 code styles: header_comment and ordered_imports (#175) 2018-06-23 11:41:58 +02:00
Simon Schaufelberger
1dd5f2b72f Translations 2018-06-23 00:48:11 +02:00
Kevin Papst
cd7cbeae88 improved code styles #158 (#172)
* use php-cs-fixer instead of phpcodesniffer
* updated scrutinizer config
* adjusted code yules to all files
* updated CONTRIBUTING guidelines
2018-06-22 21:09:10 +02:00
Kevin Papst
773d27bb0e set default locale for tests (#171) 2018-06-21 22:44:32 +02:00
Kevin Papst
0eaa81578a hide success notification after 5 seconds #169 (#170) 2018-06-21 18:47:13 +02:00
Kevin Papst
3dc054b839 help text and link for form labels #138 (#143)
* added help for timesheet duration form field - fixes #138
* added label option to link to manual chapter
* created docu chapter "timesheet" and added duration mode docu
* allow field and form option "documentation" to link to chapters in the documentation
2018-06-21 18:25:11 +02:00
Kevin Papst
8c686b0714 Added validation for end date before start date (#167)
* added validation for end date before start date
* added unit test for new validations
2018-06-20 23:27:56 +02:00
Kevin Papst
071abc8fc8 Update issue templates 2018-06-20 21:37:57 +02:00
Kevin Papst
75a6bf3ef6 Updated security expressions #148 (#166)
* replace has_role() with is_granted()
* added basic integration test classes for main controller
* added basic integration test classes for admin controller
2018-06-19 22:08:44 +02:00
Kevin Papst
856e7ccaeb Composer updates (#165)
* updated composer dependencies
* updated README about composer install
2018-06-18 23:13:29 +02:00
Kevin Papst
bfa32a90db Update to Symfony 4.1 #149 (#160)
* updated to Symfony 4.1 #149
* updated to phpunit 7 #149
* downgrade ocramius/proxy-manager for PHP 7.1 compatibility #149
* fixed symfony deprecations #149
* replaced AdvancedUserInterface with custom user checker
* code sniffer fixes
2018-06-18 22:26:00 +02:00
Kevin Papst
4e629fcaf8 some wording changes 2018-06-18 21:37:39 +02:00
Kevin Papst
01bce05da4 added scrutinizer config #158 (#161) 2018-06-08 00:16:12 +02:00
Kevin Papst
8a19af4efc Travis integration #120 (#159)
* fixed php versions used in travis #120
* fixed broken test #120
* updated developer docu
2018-06-06 21:36:24 +02:00
Kevin Papst
6eef9f6f37 Mobile alignments and toolbar #151 (#157)
* improved toolbar position #151
* fixed alignments on mobile #151
* fixed moment js #151
2018-06-05 23:15:25 +02:00
Kevin Papst
0445a255d0 added local config to overwrite settings #153 (#156) 2018-06-05 23:06:47 +02:00
Kevin Papst
451cf4cd1d Filter in hidden toolbar - improved mobile #151 (#155)
* improved toolbar: hide data table form in collapsible + filter button
* added collapsible area for toolbar form #151
2018-06-05 21:49:13 +02:00
Kevin Papst
7206d064a5 Improved mobile view #151 (#150)
- fixed group by for mysql mode
- only user can edit own prefs
- updated installation README
- pagerfanta shows less links
- improved manual tab
- moved user profile link to pref tabs
- added menu links to top navbar
- moved page actions to breadcrumb position
- integrated daterangepicker and improved toolbar forms
- bugfix for time rounding
2018-06-05 21:28:25 +02:00
Kevin Papst
db5b6f8e2e added russian translation #95 (#147)
* added russian translation #95
* added config for duration_only mode and adjusted timesheet views #131
2018-06-02 19:57:12 +02:00
Kevin Papst
9d9860e495 bugfix for duration_only mode #137 (#142)
* replaced durationForEntry with duration filter
* fixed duration problem - fixes #137
2018-06-02 19:39:03 +02:00
Kevin Papst
1c0f8cdad1 Bugfix importer (#145) (#146)
* Fix importer #145
2018-06-01 15:06:29 +02:00
Simon Schaufelberger
4af0e277b4 Make the back link appear like a normal link instead of a button (#139) 2018-02-18 12:12:24 +01:00
Kevin Papst
5a4bb0d081 Duration only mode #131 (#134) 2018-02-10 22:19:49 +01:00
Simon Schaufelberger
93e1cd5095 Secure create-user command #123 (#127)
* ask the user for the password #123
* added integration test #123
2018-02-10 19:11:30 +01:00
Simon Schaufelberger
5527f29caf Use short array syntax (#135) 2018-02-10 19:05:19 +01:00
Simon Schaufelberger
4c6158c1ea Merge pull request #132 from kevinpapst/inline-docu-129
added controller to render markdown documentation #129
2018-02-07 21:41:42 +01:00
Kevin Papst
6431fbecf6 code review fixes #129 2018-02-07 21:29:35 +01:00
Kevin Papst
29699572bd added controller to render markdown documentation #129 2018-02-07 15:07:53 +01:00
Kevin Papst
2e60e14132 added rounding rules and hourly rates factor #112 (#128)
* added unit tests and documentation #112
* fixed DashboardController
* added CONTRIBUTION guidelines
* fixed license year
2018-02-07 12:51:00 +01:00
Simon Schaufelberger
65790f1fb7 using new license header #124 (#119) 2018-02-03 18:23:47 +01:00
Simon Schaufelberger
76d4cba596 Add github issue and pull request template (#118) 2018-02-03 18:19:51 +01:00
Simon Schaufelberger
974cee0b6e compile assets with hash after filename #125 (#126) 2018-02-03 17:52:26 +01:00
Kevin Papst
7deaba8368 added webpack-encore for managing frontend assets #113 (#122)
* updated installation docu #113
* rewritten login page #113
* added icheck plugin #113
* changed to YesNoType in edit forms #113
* updated CONTRIBUTING.md #113
* added start of developer docu #113
* added compile scripts and compiled assets #113
2018-02-01 22:26:19 +01:00
Kevin Papst
e35d73eab5 fixed short invoice calculator (#114)
* fixed money display and address field for InvoiceTemplate entity
* added quick start button for users without active records
* rearranged columns on project and customer admin page
* improved extensions docu
2018-01-28 12:08:44 +01:00
Kevin Papst
9bbaf0ec71 added first docu on extending kimai with bundles #106 (#109) 2018-01-27 11:21:52 +01:00
Kevin Papst
1958bb9d09 added order number to projects #107 (#108) 2018-01-25 21:42:18 +01:00
Kevin Papst
7f5b5ac254 added short invoice calculator and timesheet invoice template #104 (#105) 2018-01-23 22:30:41 +01:00
Kevin Papst
50b5e93a10 added command to import data from kimai v1 #102 (#103) 2018-01-23 00:23:41 +01:00
Kevin Papst
6ae9bc4df5 added layout and translation to error pages #100 (#101) 2018-01-21 22:53:10 +01:00
Kevin Papst
4cbc5391c0 added basic invoice rendering #97 #93 (#99) 2018-01-21 22:50:46 +01:00
Kevin Papst
9a161b8fd9 added remember me feature #53 (#98)
cleanup event subscriber
2018-01-17 19:55:26 +01:00
Kevin Papst
7df21f3f5a code-cleanup: removed unused classes, added role constants, phpcs fixes (#96) 2018-01-16 23:56:55 +01:00
Kevin Papst
0a65965187 load doctrine extensions during kernel bootstrap #84 (#92) 2018-01-16 21:35:23 +01:00
Kevin Papst
3f221fcdb8 support theme options via user preferences #89 (#91) 2018-01-15 22:58:07 +01:00
Kevin Papst
827b1ae9ad fixed dynamic user preferences #9
added missing Chart.min.js
2018-01-14 23:02:50 +01:00
Kevin Papst
49d8796f6b added dynamic user preferences #7 #9 (#90)
- fixed user settings database #9
- added dynamic user preferences #9
- added form to edit user preferences #9
- removed user language #7
2018-01-14 22:47:20 +01:00
Kevin Papst
023fef6a70 added control sidebar #10 (#87)
- added configurable sidebar #10
- added language switch in sidebar preferences tab #10
- added sidebar tab with kimai beta-info message #10
- use trait for repositories
- improved README
2018-01-14 01:36:07 +01:00
Kevin Papst
d5e689e307 code cleanup 2018-01-13 00:20:41 +01:00
Kevin Papst
fc9ce52d69 removed unused files #74 2018-01-13 00:19:23 +01:00
Kevin Papst
c7935da16a added english translation #8 (#82)
* reformatted translation file #8
* set default language to en #8
* added english translations #8
2018-01-12 22:21:15 +01:00
Kevin Papst
1833a6632a skip broken tests, added browserkit for integration tests 2018-01-12 21:21:43 +01:00
Kevin Papst
4d7fe3d595 fixes for symfony4 #74 #81 2018-01-12 21:07:20 +01:00
Kevin Papst
a87355695e upgraded to symfony 4 #74 (#81) 2018-01-12 20:39:07 +01:00
1472 changed files with 142426 additions and 19752 deletions

33
.codecov.yml Normal file
View File

@@ -0,0 +1,33 @@
codecov:
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
parsers:
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

19
.editorconfig Normal file
View File

@@ -0,0 +1,19 @@
; top-most EditorConfig file
root = true
; Unix-style newlines
[*]
end_of_line = lf
charset = utf-8
[*.php]
indent_style = space
indent_size = 4
[*.yaml]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 4

31
.env.dist Normal file
View File

@@ -0,0 +1,31 @@
# This file is a "template" of which env vars need to be defined for your application
# Copy this file to .env file for development, create environment variables when deploying to production
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
MAILER_FROM=kimai@example.com
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=change_this_to_something_unique
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
#TRUSTED_HOSTS=localhost,example.com
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data/kimai.sqlite"
# Configure your db driver and server_version in config/packages/doctrine.yaml
# DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai.sqlite
###< doctrine/doctrine-bundle ###
###> symfony/swiftmailer-bundle ###
# For Gmail as a transport, use: "gmail://username:password@localhost"
# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode="
# Delivery is disabled by default via "null://localhost"
MAILER_URL=null://localhost
###< symfony/swiftmailer-bundle ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
###< nelmio/cors-bundle ###

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
#github: [kevinpapst]
custom: https://www.kimai.org/donate/

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve Kimai
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. See error
**Logfile**
```
Add the last lines from your logfile at var/log/prod.log, around the time when the problem happened.
```
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop/Smartphone**
Please complete the following information if this seems to be a frontend issue:
- Device: [e.g. iPhone6s]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
- Kimai version
- PHP version

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea to make Kimai better
---
**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.
**Additional context**
Add any other context or screenshots about the feature request here.

12
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,12 @@
## Description
A clear and concise description of what this pull request adds or changes.
## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Checklist
- [ ] I verified that my code applies to the guidelines (`composer kimai: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)

24
.github/lock.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
# 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

View File

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

101
.gitignore vendored
View File

@@ -1,57 +1,62 @@
# Cache and logs (Symfony2)
/app/cache/*
/app/logs/*
!app/cache/.gitkeep
!app/logs/.gitkeep
# ========== KIMAI ==========
.idea/
.DS_Store
# Email spool folder
/app/spool/*
# Cache, session files and logs (Symfony3)
/var/cache/*
/var/logs/*
/var/sessions/*
!var/cache/.gitkeep
!var/logs/.gitkeep
!var/sessions/.gitkeep
# Parameters
/app/config/parameters.yml
/app/config/parameters.ini
# Managed by Composer
/app/bootstrap.php.cache
/var/bootstrap.php.cache
/bin/*
!bin/console
!bin/symfony_requirements
/config/packages/local.yaml
/config/packages/*-local.yaml
/config/packages/*/local.yaml
/config/packages/*/*-local.yaml
public/avatars/*.png
templates/invoice/renderer/.~lock*
/var/data/*
!/var/data/.gitkeep
!/var/data/kimai_test.sqlite
/var/coverage/
/var/cache/*
!var/cache/.gitkeep
/var/invoices/*
/var/packages/*
!var/packages/.gitkeep
/var/plugins/*
!var/plugins/.gitkeep
/var/log/*
!var/log/.gitkeep
/var/sessions/*
!var/sessions/.gitkeep
# ========== KIMAI ==========
###> symfony/framework-bundle ###
.env
/public/bundles/
/vendor/
###< symfony/framework-bundle ###
# Assets and user uploads
/web/bundles/
/web/uploads/
# Assets managed by Bower
/web/assets/vendor/
# PHPUnit
/app/phpunit.xml
###> phpunit/phpunit ###
/phpunit.xml
/.phpunit.result.cache
###< phpunit/phpunit ###
# Build data
/build/
###> friendsofphp/php-cs-fixer ###
.php_cs
.php_cs.cache
###< friendsofphp/php-cs-fixer ###
# Composer PHAR
/composer.phar
###> symfony/phpunit-bridge ###
.phpunit
/phpunit.xml
###< symfony/phpunit-bridge ###
# Backup entities generated with doctrine:generate:entities command
*/Entity/*~
# IDE
.idea/
# SQLite files
/app/data/timesheet.sqlite
# generated install and setup files
/web/theme/
###> symfony/webpack-encore-bundle ###
/node_modules/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

153
.php_cs.dist Normal file
View File

@@ -0,0 +1,153 @@
<?php
$fileHeaderComment = <<<COMMENT
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.
COMMENT;
return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'encoding' => true,
'full_opening_tag' => true,
'blank_line_after_namespace' => true,
'braces' => true,
'class_definition' => true,
'elseif' => true,
'function_declaration' => true,
'indentation_type' => true,
'line_ending' => true,
'lowercase_constants' => true,
'lowercase_keywords' => true,
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'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,
'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true,
'single_blank_line_at_eof' => true,
'single_class_element_per_statement' => ['elements' => ['property']],
'single_import_per_statement' => true,
'single_line_after_imports' => true,
'switch_case_semicolon_to_colon' => true,
'switch_case_space' => true,
'array_syntax' => [
'syntax' => 'short'
],
'binary_operator_spaces' => true,
'blank_line_after_opening_tag' => true,
'blank_line_before_statement' => [
'statements' => ['return'],
],
'cast_spaces' => true,
'class_attributes_separation' => ['elements' => ['method']],
'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => true,
'function_typehint_space' => true,
'include' => true,
'lowercase_cast' => true,
'lowercase_static_reference' => true,
'magic_constant_casing' => true,
'native_function_casing' => true,
'new_with_braces' => true,
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => ['tokens' => [
'curly_brace_block',
'extra',
'parenthesis_brace_block',
'square_brace_block',
'throw',
'use',
]],
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_mixed_echo_print' => ['use' => 'echo'],
'no_multiline_whitespace_around_double_arrow' => true,
'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_unneeded_final_method' => true,
'no_unused_imports' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
'normalize_index_brace' => true,
'object_operator_without_whitespace' => true,
'php_unit_fqcn_annotation' => true,
'phpdoc_align' => [
'align' => 'left',
'tags' => [
'method',
'param',
'property',
'return',
'throws',
'type',
'var',
],
],
'phpdoc_annotation_without_dot' => true,
'phpdoc_indent' => true,
'phpdoc_inline_tag' => true,
'phpdoc_no_access' => true,
'phpdoc_no_alias_tag' => true,
'phpdoc_no_empty_return' => true,
'phpdoc_no_package' => true,
'phpdoc_no_useless_inheritdoc' => true,
'phpdoc_return_self_reference' => true,
'phpdoc_scalar' => true,
'phpdoc_separation' => false,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_summary' => false,
'phpdoc_to_comment' => true,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'phpdoc_var_without_name' => true,
'protected_to_private' => true,
'return_type_declaration' => true,
'semicolon_after_instruction' => true,
'short_scalar_cast' => true,
'single_blank_line_before_namespace' => true,
'single_line_comment_style' => [
'comment_types' => ['hash'],
],
'single_quote' => true,
'space_after_semicolon' => [
'remove_in_empty_for_expressions' => true,
],
'standardize_increment' => true,
'standardize_not_equals' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline_array' => 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' => [
'const',
'method',
'property',
]],
])
->setFinder(
PhpCsFixer\Finder::create()
->in([
__DIR__ . '/src/',
__DIR__ . '/tests/',
])
)
->setFormat('checkstyle')
;

View File

@@ -1,26 +1,96 @@
dist: xenial
language: php
sudo: false
cache:
directories:
- $HOME/.composer/cache/files
matrix:
fast_finish: true
include:
- php: 5.5
- php: 5.6
- php: 7.0
- php: hhvm
allow_failures:
- php: hhvm
- stage: Code quality
env: CODECOVERAGE=0 DB=sqlite
php: '7.3'
script:
- composer validate --no-check-all --strict
- composer kimai:codestyle
- composer kimai:phpstan
- composer kimai:code-lint
- stage: Test
php: '7.2'
env: CODECOVERAGE=0 DB=mysql
sudo: required
services:
- mysql
- stage: Test
php: '7.3'
env: CODECOVERAGE=0 DB=mysql
sudo: required
services:
- mysql
- stage: Test
php: '7.4'
env: CODECOVERAGE=0 DB=mysql
sudo: required
services:
- mysql
- stage: Test
php: '7.2'
env: CODECOVERAGE=0 DB=sqlite
- stage: Test
php: '7.3'
env: CODECOVERAGE=0 DB=sqlite
- stage: Test
php: '7.4'
env: CODECOVERAGE=0 DB=sqlite
- stage: Test
php: '7.2'
env: CODECOVERAGE=0 DB=mariadb
addons:
mariadb: '10.2'
services:
- mysql
before_script:
- mysql -u root -e 'CREATE USER IF NOT EXISTS travis@localhost; GRANT ALL ON *.* TO travis@localhost;'
- stage: Test
php: '7.3'
env: CODECOVERAGE=0 DB=mariadb
addons:
mariadb: '10.2'
services:
- mysql
before_script:
- mysql -u root -e 'CREATE USER IF NOT EXISTS travis@localhost; GRANT ALL ON *.* TO travis@localhost;'
- stage: Test
php: '7.4'
env: CODECOVERAGE=0 DB=mariadb
addons:
mariadb: '10.2'
services:
- mysql
before_script:
- mysql -u root -e 'CREATE USER IF NOT EXISTS travis@localhost; GRANT ALL ON *.* TO travis@localhost;'
- stage: Code coverage
php: '7.3'
env: CODECOVERAGE=1 DB=sqlite
after_success:
- if [[ $CODECOVERAGE == 1 ]]; then bash <(curl -s https://codecov.io/bash); fi
before_install:
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi
- if [[ $CODECOVERAGE == 0 ]]; then phpenv config-rm xdebug.ini; fi;
- composer self-update
- php -i
- php -m
install:
- composer install
- composer require zendframework/zend-ldap
script:
- phpunit
- if [[ $CODECOVERAGE == 1 ]]; then TEST_WITH_BUNDLES=1 vendor/bin/phpunit tests/ --coverage-clover=coverage.xml; fi;
- if [[ $CODECOVERAGE == 0 ]]; then vendor/bin/phpunit tests/; fi;
- if [[ $DB == 'sqlite' ]]; then cp tests/.env.dist.sqlite .env; fi;
- if [[ $DB == "mysql" || $DB == "mariadb" ]]; then cp tests/.env.dist.mysql .env; fi;
- bin/console doctrine:database:create -n
- bin/console doctrine:migrations:migrate -n
- bin/console doctrine:migrations:migrate first -n

120
CHANGELOG.md Normal file
View File

@@ -0,0 +1,120 @@
# Change Log
## [1.7](https://github.com/kevinpapst/kimai2/tree/1.7) (2020-01-19)
[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) (2019-12-02)
[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) (2019-11-26)
[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) (2019-11-24)
[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) (2019-11-03)
[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) (2019-10-14)
[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) (2019-10-07)
[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) (2019-10-03)
[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) (2019-09-11)
[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) (2019-08-28)
[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) (2019-07-24)
[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) (2019-07-05)
[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) (2019-07-02)
[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) (2019-04-29)
[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) (2019-02-22)
[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) (2019-02-19)
[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) (2019-01-28)
[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) (2018-11-19)
[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) (2018-11-18)
[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) (2018-09-27)
[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) (2018-09-01)
[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) (2018-07-22)
[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) (2018-06-23)
[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) (2018-01-10)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.1)

View File

@@ -1,20 +1,26 @@
Contributing
============
# Contributing
The Kimai application is an open source project. Contributions made by
the community are welcome. Send us your ideas, code reviews, pull requests and
feature requests to help us improve this project.
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.
Web Assets Management
---------------------
## Pull request rules
This project manages its web assets in a special way to allow them to work
without configuring any option, installing any tool or executing any command.
If your contribution changes CSS styles or JavaScript code in any way, make
sure to regenerate the `web/css/app.css` and `web/js/app.js` files. To do so,
uncomment the Assetic blocks in the `app/Resources/views/base.html.twig` and
execute the following command:
- 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
```bash
$ php bin/console assetic:dump --no-debug
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.
*/
```

55
Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
# 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.
FROM php:7.2.9-apache-stretch AS tmp_kimai2_base
RUN apt update && \
apt install -y --allow-unauthenticated \
git \
haveged \
libicu-dev \
libjpeg-dev \
libldap2-dev \
libldb-dev \
libpng-dev \
mysql-client \
unzip \
wget \
zip \
&& \
EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)" && \
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" && \
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]; then >&2 echo 'ERROR: Invalid installer signature'; rm composer-setup.php; exit 1; fi && \
php composer-setup.php --quiet && \
rm composer-setup.php && \
mv /var/www/html/composer.phar /usr/bin/composer && \
docker-php-ext-install \
gd \
intl \
ldap \
pdo_mysql \
zip && \
apt remove -y wget && \
apt -y autoremove && \
apt clean && \
cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
FROM tmp_kimai2_base
RUN git clone https://github.com/kevinpapst/kimai2.git /opt/kimai && \
sed "s/prod/dev/g" /opt/kimai/.env.dist > /opt/kimai/.env && \
composer install --working-dir=/opt/kimai --dev --optimize-autoloader && \
/opt/kimai/bin/console doctrine:database:create && \
/opt/kimai/bin/console doctrine:schema:create && \
/opt/kimai/bin/console doctrine:migrations:version --add --all && \
/opt/kimai/bin/console cache:warmup && \
chown -R www-data:www-data /opt/kimai/var && \
chown www-data:www-data /opt/kimai/vendor/mpdf/mpdf/tmp
WORKDIR /opt/kimai
EXPOSE 8001
USER www-data
CMD /opt/kimai/bin/console server:run 0.0.0.0:8001

12
LICENSE
View File

@@ -1,11 +1,13 @@
Copyright (c) 2016 Kevin Papst
MIT License
Copyright (c) 2017-2019 Kevin Papst @ https://www.kevinpapst.de
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:
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
@@ -15,5 +17,5 @@ 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.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

144
README.md
View File

@@ -1,114 +1,74 @@
# Kimai v2 - Time Tracking
# Kimai 2 - online time-tracker
Kimai v2 - the reloaded open source Time-Tracking application.
[![Latest Stable Version](https://poser.pugx.org/kevinpapst/kimai2/v/stable)](https://packagist.org/packages/kevinpapst/kimai2)
[![License](https://poser.pugx.org/kevinpapst/kimai2/license)](https://packagist.org/packages/kevinpapst/kimai2)
[![Travis Status](https://travis-ci.org/kevinpapst/kimai2.svg?branch=master)](https://travis-ci.org/kevinpapst/kimai2)
[![Code Coverage](https://codecov.io/gh/kevinpapst/kimai2/branch/master/graph/badge.svg)](https://codecov.io/gh/kevinpapst/kimai2)
[![Gitter](https://badges.gitter.im/kimai2/support.svg)](https://gitter.im/kimai2/support)
[![Bountysource](https://img.shields.io/bountysource/team/kimai2/activity)](https://www.bountysource.com/teams/kimai2)
[![Build Status](https://travis-ci.org/kevinpapst/kimai2.svg?branch=master)](https://travis-ci.org/kevinpapst/kimai2)
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.
## Introduction
This is (or will be in the future, currently a lot of features are still missing) the reloaded version of the open source time-tracking application [Kimai](http://www.kimai.org).
- [Home](https://www.kimai.org) - The house of Kimai
- [Blog](https://www.kimai.org/blog/) - Get the latest news
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use
- [Translations](https://www.kimai.org/documentation/translations.html) - Kimai in your language
- [Migration](https://www.kimai.org/documentation/migration-v1.html) - Import data from v1
It is based on the following PHP components:
- [Symfony Framework 3.4](https://github.com/symfony/symfony)
- [AdminThemeBundle](https://github.com/avanzu/AdminThemeBundle/) (based on [AdminLTE](https://github.com/almasaeed2010/AdminLTE/))
- [Doctrine](https://github.com/doctrine/)
- Bower
### Requirements
## Requirements
- PHP 7.2 or higher
- Database (MySQL/MariaDB with timezone data, SQLite for development)
- Webserver (nginx, Apache)
- A modern browser
- [Other libraries](https://www.kimai.org/download/)
- PHP 7 or higher
- One PHP extension of PDO-SQLite and/or PDO-MySQL enabled
- and the [usual Symfony application requirements](http://symfony.com/doc/current/reference/requirements.html)
### About
If unsure about meeting these requirements, download the demo application and
browse to the <http://localhost:8000/config.php> script to get more detailed information.
This is the new version of the open source timetracker Kimai. It is stable and production ready, ships
with most advanced features from Kimai 1 and many new ones, including but not limited to:
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
LDAP and built-in authentication, customizable role permissions, responsive and ready for your mobile device,
hourly and fixed rates, advanced filtering, money and time budgets with report, support for plugins and many more.
## Installation
First, install Git and [Composer](https://getcomposer.org/doc/00-intro.md)
if you haven't already. Then, clone this repo and execute this command in the cloned directory:
- [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
- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation) - unfortunately still widely used ;-)
```bash
$ git clone https://github.com/kevinpapst/kimai2.git
$ cd kimai2/
```
### Updating Kimai
Lets prepare the environment by installing all dependencies. You will be asked for your application parameter,
like the database connection afterwards (if you don't have a [app/config/parameters.yml](blob/master/app/config/parameters.yml.dist) yet):
- [Update Kimai](https://www.kimai.org/documentation/updates.html) - the documentation
- [UPGRADING guide](UPGRADING.md) - version specific steps
```bash
$ composer install
```
### Plugins
The next command will create the database, the schema and install all web assets:
```bash
$ bin/console kimai:install --relative
```
- [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
### Installation (live)
## Roadmap and releases
All thats left to do is to create your first user:
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.
```bash
$ bin/console kimai:create-user admin admin@example.com password en ROLE_SUPER_ADMIN
```
> 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.
For available roles, please refer to [the user documentation](app/Resources/docs/users.md).
Release versions will be created on a regular base (approx. one release per month) and you can should use these tags if you are not familiar with git.
Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
I have to do it this way, as I develop Kimai in my free time and want to put my effort into the software instead of backporting changes for old versions.
### Installation (development / demo)
Lets boostrap your environment by executing this commands (which is only available in dev environment):
```bash
$ bin/console kimai:dev:reset
```
You just imported demo data, to test the application in its full beauty and with several different user accounts and permission sets.
You can now login with these accounts:
| Username | Password | Role |
|---|:---:|---|
| clara_customer | kitten | Customer |
| john_user | kitten | User |
| chris_user | kitten | User (deactivated) |
| tony_teamlead | kitten | Teamlead |
| anna_admin | kitten | Administrator |
| susan_super | kitten | Super-Administrator |
Demo data can always be deleted by dropping the schema and re-creating it.
ATTENTION - this will erase all your data:
```bash
$ bin/console doctrine:schema:drop --force
$ bin/console doctrine:schema:create
```
The `kimai:dev:reset` command can always be executed later on to reset your dev database and cache. I use it very frequently.
## Usage
There is no need to configure a virtual host in your web server to access the application for testing.
Just use the built-in web server for your first tests:
```bash
$ APP_FRONT_CONTROLLER=app.php bin/console server:run
```
This command will start a web server for Kimai. Now you can access the application in your browser at <http://127.0.0.1:8000/>.
You can stop the built-in web server by pressing `Ctrl + C` while you're in the terminal.
> **NOTE**
>
> If you want to use a fully-featured web server (like Nginx or Apache) to run
> Kimai, configure it to point at the `web/` directory of the project.
> For more details, see:
> http://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html
## Troubleshooting
Cannot see any assets (like images) and/or missing styles? Try executing:
```bash
$ php bin/console assets:install --symlink
```
## Credits
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).

23
SECURITY.md Normal file
View File

@@ -0,0 +1,23 @@
# Security Policy
## Supported Versions
As announced in the [README](README.md) I only support the latest available release and master.
| Version | Supported |
| ------- | ------------------ |
| master | :white_check_mark: |
| 1.7 | :white_check_mark: |
| < 1.7 | :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 Europe/Berlin.
If your issue is valid and I verified and fixed it, you will be mentioned in the release notes within the Security section at the top of the release.
I am grateful for any (discrete) disclosure of vulnerabilities!

248
UPGRADING.md Normal file
View File

@@ -0,0 +1,248 @@
# Upgrading Kimai 2
_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.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
```

View File

@@ -1,26 +0,0 @@
{
"name": "Kimai v2",
"description": "The reloaded Kimai Time-tracking application",
"keywords": [
"php",
"kimai"
],
"website": "https://www.kimai.org.com/",
"repository": "https://github.com/kevinpapst/kimai2",
"logo": "",
"success_url": "/",
"scripts": {
"postdeploy": "php bin/console doctrine:schema:create && php bin/console doctrine:fixtures:load -n"
},
"env": {
"SYMFONY_ENV": "prod",
"SYMFONY_SECRET": {
"description": "Extra entropy for %kernel.secret%; used for CSRF tokens, cookies and signed URLs.",
"generator": "secret"
}
},
"addons": [
"heroku-postgresql"
],
"image": "heroku/php"
}

View File

@@ -1,7 +0,0 @@
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>

View File

@@ -1,21 +0,0 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
/**
* Class AppCache
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class AppCache extends HttpCache
{
}

View File

@@ -1,71 +0,0 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
/**
* Class AppKernel
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
new Avanzu\AdminThemeBundle\AvanzuAdminThemeBundle(),
new AppBundle\AppBundle(),
new TimesheetBundle\TimesheetBundle(),
];
// Bundles only used for development or unit and functional tests
if (in_array($this->getEnvironment(), ['dev', 'test'])) {
//$bundles[] = new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle();
$bundles[] = new Symfony\Bundle\AsseticBundle\AsseticBundle();
$bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
$bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle();
}
return $bundles;
}
public function getRootDir()
{
return __DIR__;
}
public function getCacheDir()
{
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
}
public function getLogDir()
{
return dirname(__DIR__).'/var/logs';
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}

View File

@@ -1,38 +0,0 @@
{# User Account: style can be found in dropdown.less #}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
<li class="dropdown user user-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
{{ macro.avatar(user.avatar, user.username, 'user-image') }}
<span class="hidden-xs">{{ user.name }}</span>
</a>
<ul class="dropdown-menu">
<!-- User image -->
<li class="user-header">
{{ macro.avatar(user.avatar, user.username) }}
<p>
{{user.name}}
<small>{{user.title}}</small>
</p>
</li>
{#
<!-- Menu Body -->
<li class="user-body">
<div class="col-xs-4 text-center">
</div>
<div class="col-xs-4 text-center">
</div>
<div class="col-xs-4 text-center">
</div>
</li>
#}
<!-- Menu Footer-->
<li class="user-footer">
<div class="pull-left">
<a href="{{ path('user_profile', {'username' : user.username}) }}" class="btn btn-default btn-flat">{{ 'label.user_profile'|trans }}</a>
</div>
<div class="pull-right">
<a href="{{ logout_path() }}" class="btn btn-default btn-flat">{{ 'label.logout'|trans }}</a>
</div>
</li>
</ul>
</li>

View File

@@ -1,12 +0,0 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<link rel="stylesheet" href="{{ asset('theme/bootstrap/css/bootstrap.min.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
{#<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.min.css">#}
<link rel="stylesheet" href="{{ asset('theme/dist/css/AdminLTE.min.css') }}">
<link rel="stylesheet" href="{{ asset("theme/dist/css/skins/" ~ avanzu_admin_context.skin|default('skin-blue-light') ~ ".min.css") }}">
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->

View File

@@ -1,28 +0,0 @@
{#
This template is used to render any error different from 403, 404 and 500.
This is the simplest way to customize error pages in Symfony applications.
In case you need it, you can also hook into the internal exception handling
made by Symfony. This allows you to perform advanced tasks and even recover
your application from some errors.
See http://symfony.com/doc/current/cookbook/controller/error_pages.html
#}
{% extends 'base.html.twig' %}
{% block body_id 'error' %}
{% block main %}
<h1 class="text-danger">{{ 'http_error.name'|trans({ '%status_code%': status_code }) }}</h1>
<p class="lead">
{{ 'http_error.description'|trans({ '%status_code%': status_code }) }}
</p>
<p>
{{ 'http_error.suggestion'|trans({ '%url%': path('homepage') })|raw }}
</p>
{% endblock %}
{% block sidebar %}
{{ parent() }}
{% endblock %}

View File

@@ -1,28 +0,0 @@
{#
This template is used to render errors of type HTTP 403 (Forbidden)
This is the simplest way to customize error pages in Symfony applications.
In case you need it, you can also hook into the internal exception handling
made by Symfony. This allows you to perform advanced tasks and even recover
your application from some errors.
See http://symfony.com/doc/current/cookbook/controller/error_pages.html
#}
{% extends 'base.html.twig' %}
{% block body_id 'error' %}
{% block main %}
<h1 class="text-danger"><i class="fa fa-unlock-alt"></i> {{ 'http_error.name'|trans({ '%status_code%': 403 }) }}</h1>
<p class="lead">
{{ 'http_error_403.description'|trans }}
</p>
<p>
{{ 'http_error_403.suggestion'|trans }}
</p>
{% endblock %}
{% block sidebar %}
{{ parent() }}
{% endblock %}

View File

@@ -1,28 +0,0 @@
{#
This template is used to render errors of type HTTP 404 (Not Found)
This is the simplest way to customize error pages in Symfony applications.
In case you need it, you can also hook into the internal exception handling
made by Symfony. This allows you to perform advanced tasks and even recover
your application from some errors.
See http://symfony.com/doc/current/cookbook/controller/error_pages.html
#}
{% extends 'base.html.twig' %}
{% block body_id 'error' %}
{% block main %}
<h1 class="text-danger">{{ 'http_error.name'|trans({ '%status_code%': 404 }) }}</h1>
<p class="lead">
{{ 'http_error_404.description'|trans }}
</p>
<p>
{{ 'http_error_404.suggestion'|trans({ '%url%': path('homepage') })|raw }}
</p>
{% endblock %}
{% block sidebar %}
{{ parent() }}
{% endblock %}

View File

@@ -1,28 +0,0 @@
{#
This template is used to render errors of type HTTP 500 (Internal Server Error)
This is the simplest way to customize error pages in Symfony applications.
In case you need it, you can also hook into the internal exception handling
made by Symfony. This allows you to perform advanced tasks and even recover
your application from some errors.
See http://symfony.com/doc/current/cookbook/controller/error_pages.html
#}
{% extends 'base.html.twig' %}
{% block body_id 'error' %}
{% block main %}
<h1 class="text-danger">{{ 'http_error.name'|trans({ '%status_code%': 500 }) }}</h1>
<p class="lead">
{{ 'http_error_500.description'|trans }}
</p>
<p>
{{ 'http_error_500.suggestion'|trans({ '%url%': path('homepage') })|raw }}
</p>
{% endblock %}
{% block sidebar %}
{{ parent() }}
{% endblock %}

View File

@@ -1,14 +0,0 @@
# Users
There are multiple pre-defined roles in Kimai, which define the ACLs. A user can only inherit one role, where the roles extend each user.
## Roles
| Role name | extends | Gives permission for |
|---|---|---|
| ROLE_CUSTOMER | - | Currently has no permissions, but was reserved for future functionality |
| ROLE_USER | ROLE_CUSTOMER | Time-tracking |
| ROLE_TEAMLEAD | ROLE_USER | All of the above, plus: editing other users timesheets |
| ROLE_ADMIN | ROLE_TEAMLEAD | All of the above, plus: editing customers, editing projects, editing activities |
| ROLE_SUPER_ADMIN | ROLE_ADMIN | All of the above, plus: editing users |

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2016-10-19T21:46:45Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
<body>
<trans-unit id="Toggle navigation">
<source>Toggle navigation</source>
<target>Ansicht der Seitenleiste umschalten</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2016-10-19T21:46:45Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
<body>
<trans-unit id="access.denied">
<source>access.denied</source>
<target>Der Zugriff wurde verweigert</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2016-10-19T21:46:45Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
<body>
<trans-unit id="timesheet.stop.success">
<source>timesheet.stop.success</source>
<target>Zeitmessung wurde gestoppt</target>
</trans-unit>
<trans-unit id="timesheet.stop.error">
<source>timesheet.stop.error</source>
<target>Zeitmessung konnte nicht gestoppt werden: %reason%</target>
</trans-unit>
<trans-unit id="timesheet.start.success">
<source>timesheet.start.success</source>
<target>Zeitmessung wurde gestartet</target>
</trans-unit>
<trans-unit id="timesheet.start.error">
<source>timesheet.start.error</source>
<target>Zeitmessung konnte nicht gestartet werden: %reason%</target>
</trans-unit>
<trans-unit id="action.updated_successfully">
<source>action.updated_successfully</source>
<target>Änderungen erfolgreich gespeichert</target>
</trans-unit>
<trans-unit id="action.deleted_successfully">
<source>action.deleted_successfully</source>
<target>Eintrag wurde erfolgreich gelöscht</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,712 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2016-10-19T21:46:45Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
<body>
<!--
Global template keys
-->
<trans-unit id="browser.title">
<source>browser.title</source>
<target>Kimai - Time Tracking</target>
</trans-unit>
<trans-unit id="yes">
<source>yes</source>
<target>Ja</target>
</trans-unit>
<trans-unit id="no">
<source>no</source>
<target>Nein</target>
</trans-unit>
<trans-unit id="subtitle.amount">
<source>subtitle.amount</source>
<target> (insgesamt %count%)</target>
</trans-unit>
<trans-unit id="This is a mandatory field">
<source>This is a mandatory field</source>
<target>Pflichtfeld</target>
</trans-unit>
<trans-unit id="create">
<source>create</source>
<target>Erstellen</target>
</trans-unit>
<trans-unit id="confirm.delete">
<source>confirm.delete</source>
<target>Wollen Sie das wirklich löschen?</target>
</trans-unit>
<trans-unit id="confirm.delete_message">
<source>confirm.delete_message</source>
<target>Sie müssen das Löschen erneut bestätigen, da verknüpfte Objekte existieren welche ebenfalls mit gelöscht werden.</target>
</trans-unit>
<!--
Login / Security
-->
<trans-unit id="security.title.login">
<source>security.title.login</source>
<target>Anmelden</target>
</trans-unit>
<trans-unit id="security.label.username">
<source>security.label.username</source>
<target>Benutzername</target>
</trans-unit>
<trans-unit id="security.label.password">
<source>security.label.password</source>
<target>Passwort</target>
</trans-unit>
<trans-unit id="security.label.password_repeat">
<source>security.label.password_repeat</source>
<target>Passwort wiederholen</target>
</trans-unit>
<trans-unit id="security.action.sign_in">
<source>security.action.sign_in</source>
<target>Los geht's</target>
</trans-unit>
<trans-unit id="label.logout">
<source>label.logout</source>
<target>Abmelden</target>
</trans-unit>
<trans-unit id="label.user_profile">
<source>label.user_profile</source>
<target>Mein Profil</target>
</trans-unit>
<!--
Menu / Navbar items
-->
<trans-unit id="menu.homepage">
<source>menu.homepage</source>
<target>Dashboard</target>
</trans-unit>
<trans-unit id="menu.admin">
<source>menu.admin</source>
<target>Administration</target>
</trans-unit>
<trans-unit id="menu.logout">
<source>menu.logout</source>
<target>Abmelden</target>
</trans-unit>
<trans-unit id="menu.timesheet">
<source>menu.timesheet</source>
<target>Zeiterfassung</target>
</trans-unit>
<trans-unit id="menu.admin_timesheet">
<source>menu.admin_timesheet</source>
<target>Mitarbeiter Zeiten</target>
</trans-unit>
<trans-unit id="menu.admin_customer">
<source>menu.admin_customer</source>
<target>Kunden</target>
</trans-unit>
<trans-unit id="menu.admin_project">
<source>menu.admin_project</source>
<target>Projekte</target>
</trans-unit>
<trans-unit id="menu.admin_activity">
<source>menu.admin_activity</source>
<target>Aktivitäten</target>
</trans-unit>
<trans-unit id="menu.admin_user">
<source>menu.admin_user</source>
<target>Benutzer</target>
</trans-unit>
<!--
Footer
-->
<trans-unit id="footer.license">
<source>footer.license</source>
<target>Alle Rechte vorbehalten.</target>
</trans-unit>
<!--
Error templates
-->
<trans-unit id="http_error.name">
<source>http_error.name</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error.description">
<source>http_error.description</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error.suggestion">
<source>http_error.suggestion</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error_403.description">
<source>http_error_403.description</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error_403.suggestion">
<source>http_error_403.suggestion</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error_404.description">
<source>http_error_404.description</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error_404.suggestion">
<source>http_error_404.suggestion</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error_405.description">
<source>http_error_405.description</source>
<target>Error</target>
</trans-unit>
<trans-unit id="http_error_405.suggestion">
<source>http_error_405.suggestion</source>
<target>Error</target>
</trans-unit>
<trans-unit id="error.no_entries_found">
<source>error.no_entries_found</source>
<target>Keine Einträge vorhanden</target>
</trans-unit>
<!--
General labels
-->
<trans-unit id="label.date">
<source>label.date</source>
<target>Datum</target>
</trans-unit>
<trans-unit id="label.starttime">
<source>label.starttime</source>
<target>Begin</target>
</trans-unit>
<trans-unit id="label.endtime">
<source>label.endtime</source>
<target>Ende</target>
</trans-unit>
<trans-unit id="label.duration">
<source>label.duration</source>
<target>Dauer</target>
</trans-unit>
<trans-unit id="label.username">
<source>label.username</source>
<target>Benutzer</target>
</trans-unit>
<trans-unit id="label.description">
<source>label.description</source>
<target>Beschreibung</target>
</trans-unit>
<trans-unit id="label.name">
<source>label.name</source>
<target>Name</target>
</trans-unit>
<trans-unit id="label.comment">
<source>label.comment</source>
<target>Kommentar</target>
</trans-unit>
<trans-unit id="label.id">
<source>label.id</source>
<target>ID</target>
</trans-unit>
<trans-unit id="label.visible">
<source>label.visible</source>
<target>Sichtbar</target>
</trans-unit>
<trans-unit id="label.budget">
<source>label.budget</source>
<target>Budget</target>
</trans-unit>
<trans-unit id="label.activity">
<source>label.activity</source>
<target>Aktivität</target>
</trans-unit>
<trans-unit id="label.project">
<source>label.project</source>
<target>Projekt</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Stundenlohn</target>
</trans-unit>
<trans-unit id="label.hours">
<source>label.hours</source>
<target>Stunden</target>
</trans-unit>
<trans-unit id="label.rate">
<source>label.rate</source>
<target>Lohn</target>
</trans-unit>
<trans-unit id="label.language">
<source>label.language</source>
<target>Sprache</target>
</trans-unit>
<trans-unit id="label.customer">
<source>label.customer</source>
<target>Kunde</target>
</trans-unit>
<trans-unit id="label.email">
<source>label.email</source>
<target>Email</target>
</trans-unit>
<!--
Buttons & Actions
-->
<trans-unit id="label.actions">
<source>label.actions</source>
<target>Aktionen</target>
</trans-unit>
<trans-unit id="action.edit">
<source>action.edit</source>
<target>Bearbeiten</target>
</trans-unit>
<trans-unit id="action.delete">
<source>action.delete</source>
<target>Löschen</target>
</trans-unit>
<trans-unit id="action.save">
<source>action.save</source>
<target>Speichern</target>
</trans-unit>
<trans-unit id="action.back">
<source>action.back</source>
<target>Zurück</target>
</trans-unit>
<!--
Dashboard
-->
<trans-unit id="dashboard.title">
<source>dashboard.title</source>
<target>Dashboard</target>
</trans-unit>
<trans-unit id="dashboard.subtitle">
<source>dashboard.subtitle</source>
<target>Willkommen!</target>
</trans-unit>
<trans-unit id="dashboard.you">
<source>dashboard.you</source>
<target>Deine Statistiken</target>
</trans-unit>
<trans-unit id="dashboard.all">
<source>dashboard.all</source>
<target>Alle Benutzer</target>
</trans-unit>
<trans-unit id="dashboard.admin">
<source>dashboard.admin</source>
<target>Administrator</target>
</trans-unit>
<!--
Widgets
-->
<trans-unit id="more.info.link">
<source>more.info.link</source>
<target>Mehr Infos</target>
</trans-unit>
<trans-unit id="badge.visible">
<source>badge.visible</source>
<target>Ja</target>
</trans-unit>
<trans-unit id="badge.invisible">
<source>badge.invisible</source>
<target>Nein</target>
</trans-unit>
<trans-unit id="label.toggle_dropdown">
<source>label.toggle_dropdown</source>
<target>Menü anzeigen</target>
</trans-unit>
<!--
Timesheet
-->
<trans-unit id="timesheet.title">
<source>timesheet.title</source>
<target>Zeiterfassung</target>
</trans-unit>
<trans-unit id="timesheet.subtitle">
<source>timesheet.subtitle</source>
<target>Verwalten Sie Ihre Zeiteinträge</target>
</trans-unit>
<trans-unit id="timesheet.edit">
<source>timesheet.edit</source>
<target>Eintrag bearbeiten</target>
</trans-unit>
<trans-unit id="label.begin">
<source>label.begin</source>
<target>Von</target>
</trans-unit>
<trans-unit id="label.end">
<source>label.end</source>
<target>Bis</target>
</trans-unit>
<!--
User profile
-->
<trans-unit id="profile.title">
<source>profile.title</source>
<target>Benutzerprofil</target>
</trans-unit>
<trans-unit id="profile.subtitle">
<source>profile.subtitle</source>
<target>Verwalte Deine Einstellungen</target>
</trans-unit>
<trans-unit id="profile.about_me">
<source>profile.about_me</source>
<target>Über mich</target>
</trans-unit>
<trans-unit id="profile.first_entry">
<source>profile.first_entry</source>
<target>Arbeitet seit</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Jahresstatistik</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Einstellungen</target>
</trans-unit>
<trans-unit id="profile.password">
<source>profile.password</source>
<target>Passwort</target>
</trans-unit>
<trans-unit id="profile.roles">
<source>profile.roles</source>
<target>Rollen</target>
</trans-unit>
<!--
Admin: Timesheet
-->
<trans-unit id="admin_timesheet.title">
<source>admin_timesheet.title</source>
<target>Mitarbeiter Zeiterfassung</target>
</trans-unit>
<trans-unit id="admin_timesheet.subtitle">
<source>admin_timesheet.subtitle</source>
<target>Zeiten aller Benutzer</target>
</trans-unit>
<!--
Admin: Projects
-->
<trans-unit id="admin_project.title">
<source>admin_project.title</source>
<target>Projekte</target>
</trans-unit>
<trans-unit id="admin_project.subtitle">
<source>admin_project.subtitle</source>
<target>Ein Projekt fasst Tätigkeiten für jeweils einen Kunden in einer Gruppe zusammen</target>
</trans-unit>
<trans-unit id="admin_project.delete_confirm">
<source>admin_project.delete_confirm</source>
<target>Momentan existieren für das Projekt %project% des Kunden %customer% insgesamt %activities% Aktivitäten und %records% Zeiteinträge, welche sich auf eine Gesamtdauer von %duration% belaufen. Diese Aktivitäten und Zeiteinträge werden ebenfalls mit gelöscht!</target>
</trans-unit>
<!--
Admin: Activity
-->
<trans-unit id="admin_activity.title">
<source>admin_activity.title</source>
<target>Aktivitäten</target>
</trans-unit>
<trans-unit id="admin_activity.subtitle">
<source>admin_activity.subtitle</source>
<target>Aufgaben die zur Zeiterfassung genutzt werden können</target>
</trans-unit>
<trans-unit id="admin_activity.delete_confirm">
<source>admin_activity.delete_confirm</source>
<target>Momentan existieren für die Aktivität %activity% im Projekt %project% für den Kunden %customer% insgesamt %records% Zeiteinträge, welche sich auf eine Gesamtdauer von %duration% belaufen. Diese Zeiteinträge werden ebenfalls mit gelöscht!</target>
</trans-unit>
<!--
Admin: Customer
-->
<trans-unit id="admin_customer.title">
<source>admin_customer.title</source>
<target>Kunden</target>
</trans-unit>
<trans-unit id="admin_customer.subtitle">
<source>admin_customer.subtitle</source>
<target>Ihre Kunden stellen die Basis dar, auf der Sie Projekte und Tätigkeiten anlegen können</target>
</trans-unit>
<trans-unit id="label.company">
<source>label.company</source>
<target>Unternehmensbezeichnung</target>
</trans-unit>
<trans-unit id="label.vat">
<source>label.vat</source>
<target>Umsatzsteuer</target>
</trans-unit>
<trans-unit id="label.contact">
<source>label.contact</source>
<target>Kontakt</target>
</trans-unit>
<trans-unit id="label.address">
<source>label.address</source>
<target>Adresse</target>
</trans-unit>
<trans-unit id="label.country">
<source>label.country</source>
<target>Land</target>
</trans-unit>
<trans-unit id="label.phone">
<source>label.phone</source>
<target>Telefon</target>
</trans-unit>
<trans-unit id="label.fax">
<source>label.fax</source>
<target>Fax</target>
</trans-unit>
<trans-unit id="label.mobile">
<source>label.mobile</source>
<target>Handy</target>
</trans-unit>
<trans-unit id="label.homepage">
<source>label.homepage</source>
<target>Homepage</target>
</trans-unit>
<trans-unit id="label.timezone">
<source>label.timezone</source>
<target>Zeitzone</target>
</trans-unit>
<trans-unit id="label.currency">
<source>label.currency</source>
<target>Währung</target>
</trans-unit>
<trans-unit id="admin_customer.delete_confirm">
<source>admin_customer.delete_confirm</source>
<target>Momentan existieren für den Kunden %customer% insgesamt %project% Projekte mit %activity% Aktivitäten, die sich in %records% Zeiteinträgen auf eine Gesamtdauer von %duration% belaufen. Diese Projekte, Aktivitäten und Zeiteinträge werden ebenfalls mit gelöscht!</target>
</trans-unit>
<!--
Admin: User
-->
<trans-unit id="admin_user.title">
<source>admin_user.title</source>
<target>Benutzer</target>
</trans-unit>
<trans-unit id="admin_user.subtitle">
<source>admin_user.subtitle</source>
<target>Alle registrierten Benutzer der Zeitverwaltung</target>
</trans-unit>
<trans-unit id="label.alias">
<source>label.alias</source>
<target>Name</target>
</trans-unit>
<trans-unit id="label.title">
<source>label.title</source>
<target>Titel</target>
</trans-unit>
<trans-unit id="label.avatar">
<source>label.avatar</source>
<target>Profilbild</target>
</trans-unit>
<trans-unit id="label.active">
<source>label.active</source>
<target>Aktiv</target>
</trans-unit>
<trans-unit id="label.roles">
<source>label.roles</source>
<target>Rolle</target>
</trans-unit>
<!--
ROLES
-->
<trans-unit id="ROLE_SUPER_ADMIN">
<source>ROLE_SUPER_ADMIN</source>
<target>System-Admin</target>
</trans-unit>
<trans-unit id="ROLE_ADMIN">
<source>ROLE_ADMIN</source>
<target>Administrator</target>
</trans-unit>
<trans-unit id="ROLE_TEAMLEAD">
<source>ROLE_TEAMLEAD</source>
<target>Teamleiter</target>
</trans-unit>
<trans-unit id="ROLE_USER">
<source>ROLE_USER</source>
<target>Benutzer</target>
</trans-unit>
<trans-unit id="ROLE_CUSTOMER">
<source>ROLE_CUSTOMER</source>
<target>Kunde</target>
</trans-unit>
<!--
Statistics
-->
<trans-unit id="stats.durationThisMonth">
<source>stats.durationThisMonth</source>
<target>Arbeitszeit diesen Monat</target>
</trans-unit>
<trans-unit id="stats.amountThisMonth">
<source>stats.amountThisMonth</source>
<target>Umsatz diesen Monat</target>
</trans-unit>
<trans-unit id="stats.durationTotal">
<source>stats.durationTotal</source>
<target>Arbeitszeit total</target>
</trans-unit>
<trans-unit id="stats.amountTotal">
<source>stats.amountTotal</source>
<target>Umsatz total</target>
</trans-unit>
<!--
Dashboard
-->
<trans-unit id="stats.userTotal">
<source>stats.userTotal</source>
<target>Anzahl Benutzer</target>
</trans-unit>
<trans-unit id="stats.userActiveThisMoth">
<source>stats.userActiveThisMoth</source>
<target>Aktive Benutzer diesen Monat</target>
</trans-unit>
<trans-unit id="stats.userActiveEver">
<source>stats.userActiveEver</source>
<target>Aktive Benutzer jemals</target>
</trans-unit>
<trans-unit id="stats.activeRecordings">
<source>stats.activeRecordings</source>
<target>Momentan aktive Zeitmessungen</target>
</trans-unit>
<trans-unit id="stats.activitiesTotal">
<source>stats.activitiesTotal</source>
<target>Anzahl Aktivitäten</target>
</trans-unit>
<trans-unit id="stats.projectsTotal">
<source>stats.projectsTotal</source>
<target>Anzahl Projekte</target>
</trans-unit>
<trans-unit id="stats.customerTotal">
<source>stats.customerTotal</source>
<target>Anzahl Kunden</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Januar</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Februar</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>März</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>April</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Mai</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Juni</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Juli</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>August</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Oktober</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Dezember</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->
<trans-unit id="active.entries">
<source>active.entries</source>
<target>Sie haben %count% aktiven Eintrag|Sie haben %count% aktive Einträge</target>
</trans-unit>
<trans-unit id="timesheet.all">
<source>timesheet.all</source>
<target>Alle Einträge anzeigen</target>
</trans-unit>
<trans-unit id="recent.activities">
<source>recent.activities</source>
<target>Ihr %count% zuletzt bearbeitete Aktivität|Eine ihrer %count% letzten Aktivitäten neustarten</target>
</trans-unit>
<trans-unit id="recent.activities.format">
<source>recent.activities.format</source>
<target>%activity% in %project% für %customer%</target>
</trans-unit>
<trans-unit id="timesheet.start">
<source>timesheet.start</source>
<target>Neue Zeitmessung starten</target>
</trans-unit>
<!--
TOOLBARS
-->
<trans-unit id="label.pageSize">
<source>label.pageSize</source>
<target>Anzahl Einträge</target>
</trans-unit>
<trans-unit id="label.entryState">
<source>label.entryState</source>
<target>Zeiten</target>
</trans-unit>
<trans-unit id="entryState.all">
<source>entryState.all</source>
<target>Alle</target>
</trans-unit>
<trans-unit id="entryState.running">
<source>entryState.running</source>
<target>Laufende</target>
</trans-unit>
<trans-unit id="entryState.stopped">
<source>entryState.stopped</source>
<target>Beendete</target>
</trans-unit>
<trans-unit id="0">
<source>0</source>
<target>0</target>
</trans-unit>
<trans-unit id="10">
<source>10</source>
<target>10</target>
</trans-unit>
<trans-unit id="25">
<source>25</source>
<target>25</target>
</trans-unit>
<trans-unit id="50">
<source>50</source>
<target>50</target>
</trans-unit>
<trans-unit id="75">
<source>75</source>
<target>75</target>
</trans-unit>
<trans-unit id="100">
<source>100</source>
<target>100</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file date="2016-10-19T21:46:45Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
<body>
<trans-unit id="previous">
<source>previous</source>
<target>Vorherige</target>
</trans-unit>
<trans-unit id="next">
<source>next</source>
<target>Nächste</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="de" datatype="plaintext" original="not.available">
<body>
<trans-unit id="This value is not a valid role.">
<source>This value is not a valid role.</source>
<target>Dieser Wert ist keine gültige Rolle.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,52 +0,0 @@
{% extends 'base.html.twig' %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'admin_user.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
{% block javascript_imports %}<script src="{{ asset('js/toolbar.js') }}"></script>{% endblock %}
{% block main %}
{% if entries.count == 0 %}
{{ widgets.callout('warning', 'error.no_entries_found') }}
{% endif %}
{{ tables.data_table_header({
'label.id': 'hidden-xs',
'label.alias': '',
'label.username': 'hidden-xs',
'label.email': 'hidden-xs hidden-sm',
'label.title': 'hidden-xs',
'label.active': '',
'label.roles': '',
'label.actions': '',
}, toolbarForm, {'plus-square': path('admin_user_create')}) }}
{% for entry in entries %}
<tr>
<td class="hidden-xs">{{ entry.id }}</td>
<td>{{ widgets.username(entry) }}</td>
<td class="hidden-xs">{{ entry.username }}</td>
<td class="hidden-xs hidden-sm">{{ entry.email }}</td>
<td class="hidden-xs">{{ entry.title }}</td>
<td>{{ widgets.label_visible(entry.active) }}</td>
<td>
{% for role in entry.roles %}
{{ widgets.label_role(role) }}
{% endfor %}
</td>
<td>
{% set actionButtons = {} %}
{% if is_granted('edit', entry) %}
{% set actionButtons = {'edit': path('user_profile', {'username' : entry.username})}|merge(actionButtons) %}
{% endif %}
{% if is_granted('delete', entry) %}
{% set actionButtons = actionButtons|merge({'trash': '#'}) %}
{% endif %}
{{ widgets.button_group(actionButtons) }}
</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(entries, 'admin_user_paginated') }}
{% endblock %}

View File

@@ -1,100 +0,0 @@
{% extends 'AvanzuAdminThemeBundle:layout:default-layout.html.twig' %}
{% block avanzu_page_content %}
{{ include('default/_flash_messages.html.twig') }}
{% block main %}{% endblock %}
{% endblock %}
{% block avanzu_document_title %}
{% block title %}{{ 'browser.title'|trans }}{% endblock %}
{% endblock %}
{% block avanzu_page_title %}
{% block page_title %}{% endblock %}
{% endblock %}
{% block avanzu_page_subtitle %}
{% block page_subtitle %}{% endblock %}
{% endblock %}
{% block avanzu_logo_path %}
{{ path('dashboard') }}
{% endblock %}
{% block avanzu_logo_mini %}
<b>K</b>TT
{% endblock %}
{% block avanzu_logo_lg %}
<b>Kimai</b> - Time Tracking
{% endblock %}
{% block avanzu_head %}
<link rel="stylesheet" href="{{ asset('css/kimai.css') }}">
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
{% endblock %}
{% block avanzu_footer %}
{% block footer %}
<footer class="main-footer">
<div class="pull-right hidden-xs">
<b>Version</b> 2.0
</div>
<strong>Copyright &copy; {{ 'now'|date('Y') }} - <a href="http://www.kevinpapst.de">Kevin Papst</a></strong>.
{{ 'footer.license'|trans }}
</footer>
<!-- Page rendered on {{ 'now'|localizeddate('long', 'long', null, 'UTC') }} -->
{% endblock %}
{% endblock %}
{% block avanzu_navbar_messages %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_FULLY') %}
{{ render(controller('TimesheetBundle:Timesheet:activeEntries')) }}
{% endif %}
{% endblock %}
{% block avanzu_navbar_notifications %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_FULLY') %}
{{ render(controller('TimesheetBundle:Activity:recentActivities')) }}
{% endif %}
{#
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-globe"></i> <span class="caret"></span></a>
<ul class="dropdown-menu locales" role="menu">
{% for locale in locales() %}
<li {% if app.request.locale == locale.code %}class="active"{% endif %}><a href="{{ path(app.request.get('_route', 'homepage'), app.request.get('_route_params', [])|merge({ _locale: locale.code })) }}">{{ locale.name|capitalize }}</a></li>
{% endfor %}
</ul>
</li>
#}
{% endblock %}
{% block avanzu_navbar_toggle %}{{ parent() }}{% endblock %}
{% block avanzu_navbar_tasks %}{{ parent() }}{% endblock %}
{% block avanzu_navbar_user %}{{ parent() }}{% endblock %}
{% block avanzu_navbar_control_sidebar_toggle %}{{ parent() }}{% endblock %}
{% block avanzu_sidebar_user %}{# right now we simply deactivate that box #}{% endblock %}
{% block avanzu_sidebar_search %}{# right now we simply deactivate that box #}{% endblock %}
{% block avanzu_sidebar_nav %}{{ parent() }}{% endblock %}
{% block avanzu_breadcrumb %}{{ parent() }}{% endblock %}
{% block avanzu_control_sidebar %}{{ parent() }}{% endblock %}
{% block avanzu_javascripts %}
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<script src="{{ asset('theme/bootstrap/js/bootstrap.min.js') }}"></script>
<script src="{{ asset('js/kimai.js') }}"></script>
<script src="{{ asset('js/Chart.min.js') }}"></script>
{% block javascript_imports %}{% endblock %}
{% endblock %}
{% block avanzu_javascripts_inline %}
<script type="text/javascript">
$(document).ready(function () {
$('.dropdown-toggle').dropdown();
$(document).kimai({imagePath: '{{ asset('images') }}', confirmDelete: '{{ 'confirm.delete'|trans }}'});
//$(document).kimai('pauseRecord', 'li.messages-menu ul.menu li');
});
</script>
{% endblock %}

View File

@@ -1,27 +0,0 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'dashboard.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'dashboard.subtitle'|trans }}{% endblock %}
{% block main %}
{% for settings in dashboard_widgets %}
{% if settings.header %}
{{ widgets.page_header(settings.header) }}
{% endif %}
{% set width = settings.widgets|length %}
{% set rawWidth = 12 / width %}
{% set columnWidth = rawWidth|round(0, 'floor') %}
<div class="row">
{% for widgetTemplate in settings.widgets %}
<div class="col-md-{{ columnWidth }} col-sm-{{ columnWidth * 2 }} col-xs-{{ columnWidth * 4 }}">
{% set widgetString = '{% import "macros/widgets.html.twig" as widgets %}' ~ widgetTemplate %}
{{ include(template_from_string(widgetString)) }}
</div>
{% endfor %}
</div>
{% endfor %}
{% endblock %}

View File

@@ -1,9 +0,0 @@
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
{% if app.session.started and app.session.flashbag.peekAll|length > 0 %}
<div class="row">
<div class="col-sm-12">
{{ macro.session_flash(avanzu_adminlte_close_alert|default(true), false, '', 'flashmessages') }}
</div>
</div>
{% endif %}

View File

@@ -1,16 +0,0 @@
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ title }}</h3>
</div>
{{ form_start(form) }}
<div class="box-body">
{{ form_widget(form) }}
</div>
<div class="box-footer">
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{% if back %}
<a href="{{ back }}" class="btn btn-warning">{{ 'action.back'|trans }}</a>
{% endif %}
</div>
{{ form_end(form) }}
</div>

View File

@@ -1,20 +0,0 @@
<div class="box box-solid box-danger">
<div class="box-header">
<h3 class="box-title">{{ title|default('confirm.delete'|trans) }}</h3>
</div>
<div class="box-body">
{{ message|default('confirm.delete_message'|trans)|raw }}
</div>
{{ form_start(form) }}
<div class="box-body">
{{ form_widget(form) }}
</div>
<div class="box-footer">
<input type="submit" value="{{ 'action.delete'|trans }}" class="btn btn-danger" />
{% if back %}
<a href="{{ back }}" class="btn btn-primary">{{ 'action.back'|trans }}</a>
{% endif %}
</div>
{{ form_end(form) }}
</div>

View File

@@ -1,19 +0,0 @@
{#
Each field type is rendered by a template fragment, which is determined
by the name of your form type class (DateTimePickerType -> date_time_picker)
and the suffix "_widget". This can be controlled by overriding getBlockPrefix()
in DateTimePickerType.
See http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#creating-a-template-for-the-field
#}
{% block date_time_picker_widget %}
{% spaceless %}
<div class="input-group date" data-toggle="datetimepicker">
{{ block('datetime_widget') }}
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
</div>
{% endspaceless %}
{% endblock %}

View File

@@ -1,78 +0,0 @@
{% macro data_table_header(entries, form, tools, toolsRight) %}
<div class="box data_table">
{% if form or tools or toolsRight %}
<div class="box-header with-border">
{% if tools %}
<div class="btn-group tools-left">
{% for icon,url in tools %}
<a class="btn btn-default" href="{{ url }}"><i class="fa fa-{{ icon }}"></i></a>
{% endfor %}
</div>
{% endif %}
{% if form %}
<div class="box-title toolbar">
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
</div>
{% endif %}
{% if toolsRight %}
<div class="box-tools">
<div class="btn-group">
{% for icon,url in tools %}
<a class="btn btn-default" href="{{ url }}"><i class="fa fa-{{ icon }}"></i></a>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
<div class="box-body no-padding">
<div class="dataTables_wrapper form-inline dt-bootstrap">
<div class="row">
<div class="col-sm-6"></div>
<div class="col-sm-6"></div>
</div>
<div class="row">
<div class="col-sm-12">
<table class="table table-striped table-hover dataTable" role="grid">
<thead>
<tr>
{% for title, class in entries %}
<th{% if class %} class="{{ class }}"{% endif %}>
{{ title|trans }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% endmacro %}
{% macro data_table_footer(entries, route) %}
</tbody>
</table>
</div>
</div>
{#<div class="row">
<div class="col-sm-5">
{#<div class="dataTables_info" id="example2_info" role="status" aria-live="polite">
{{ 'datatables.entry_counter'|trans({'%from%': '1', '%to%': '10', '%total%': entries.count}) }}
</div># }
</div>
<div class="col-sm-7">
<div class="dataTables_paginate paging_simple_numbers">
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { routeName: route }) }}
</div>
</div>
</div>#}
</div>
</div>
</div>
<div class="navigation text-center">
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { routeName: route }) }}
</div>
{% endmacro %}

View File

@@ -1,173 +0,0 @@
{% macro page_header(title) %}
<h2 class="page-header">{{ title|trans }}</h2>
{% endmacro %}
{% macro label_visible(visible) %}
{% import _self as macro %}
{% if visible %}
{{ macro.label('badge.visible', 'success') }}
{% else %}
{{ macro.label('badge.invisible', 'warning') }}
{% endif %}
{% endmacro %}
{% macro label_role(role) %}
{% import _self as macro %}
{% if role == 'ROLE_SUPER_ADMIN' %}
{{ macro.label(role, 'danger') }}
{% else %}
{{ macro.label(role, 'primary') }}
{% endif %}
{% endmacro %}
{% macro username(user) %}
{{ user.alias|default(user.username) }}
{% endmacro %}
{% macro label_user(user) %}
{% import _self as macro %}
{{ macro.label(macro.username(user), 'primary') }}
{% endmacro %}
{% macro label_activity(activity) %}
{% import _self as macro %}
{% if activity.visible and activity.project.visible and activity.project.customer.visible %}
{{ macro.label(activity.name, 'primary', activity.project.customer.name ~ ': ' ~ activity.project.name) }}
{% else %}
{{ macro.label(activity.name, 'warning', activity.project.customer.name ~ ': ' ~ activity.project.name) }}
{% endif %}
{% endmacro %}
{% macro label_project(project) %}
{% import _self as macro %}
{% if project.visible and project.customer.visible %}
{{ macro.label(project.name, 'primary', project.customer.name) }}
{% else %}
{{ macro.label(project.name, 'warning', project.customer.name) }}
{% endif %}
{% endmacro %}
{% macro label_customer(customer) %}
{% import _self as macro %}
{% if customer.visible %}
{{ macro.label(customer.name, 'primary') }}
{% else %}
{{ macro.label(customer.name, 'warning') }}
{% endif %}
{% endmacro %}
{% macro badge_counter(count, url) %}
{% if url %}
<a href="{{ url }}"><span class="badge bg-blue">{{ count }}</span></a>
{% else %}
<span class="badge bg-blue">{{ count }}</span>
{% endif %}
{% endmacro %}
{% macro label(title, type, tooltip) %}
{# success, warning, danger, primary #}
<span {% if tooltip %}title="{{ tooltip }}" {% endif %}class="label label-{{ type|default('success') }}">{{ title|trans }}</span>
{% endmacro %}
{% macro badge(title, color) %}
{# black, green, blue, yellow #}
<span class="badge bg-{{ color|default('red') }}">{{ title|trans }}</span>
{% endmacro %}
{% macro alert(type, description, title, icon) %}
<div class="alert alert-{{ type|default('danger') }} alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
{% if title %}<h4><i class="icon fa fa-{{ icon|default('ban') }}"></i> {{ title|trans }}</h4>{% endif %}
{{ description|trans }}
</div>
{% endmacro %}
{% macro callout(type, description, title) %}
<div class="callout callout-{{ type|default('danger') }} lead">
{% if title %}<h4>{{ title|trans }}</h4>{% endif %}
<p>{{ description|trans }}</p>
</div>
{% endmacro %}
{% macro info_box_counter(title, amount, icon, color, url) %}
<div class="info-box">
<span class="info-box-icon bg-{{ color|default(kimai_context.box_color) }}"><i class="fa fa-{{ icon|default('flag-o') }}"></i></span>
<div class="info-box-content">
{# this is a ugly hack, make me look nicely (dashboard widget with link) #}
{% if url %}
<a href="{{ url }}" class="small-box-footer">
{% endif %}
<span class="info-box-text">{{ title|trans }}</span>
<span class="info-box-number">{{ amount }}</span>
{% if url %}
</a>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro info_box_progress(title, description, amount, percentage, icon, color) %}
<div class="info-box bg-{{ color|default(kimai_context.box_color) }}">
<span class="info-box-icon"><i class="fa fa-{{ icon|default('thumbs-o-up') }}"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ title|trans }}</span>
<span class="info-box-number">{{ amount }}</span>
<div class="progress">
<div class="progress-bar" style="width: {{ percentage }}%"></div>
</div>
<span class="progress-description">
{{ description }}
</span>
</div>
<!-- /.info-box-content -->
</div>
{% endmacro %}
{% macro info_box_more(title, amount, unit, url, icon, color) %}
<div class="small-box bg-{{ color|default(kimai_context.box_color) }}">
<div class="inner">
<h3>{{ amount }}<sup style="font-size: 20px">{{ unit|default('') }}</sup></h3>
<p>{{ title|trans }}</p>
</div>
<div class="icon">
<i class="fa fa-{{ icon|default('bar-chart') }}"></i>
</div>
<a href="{{ url }}" class="small-box-footer">
{{ 'more.info.link'|trans }} <i class="fa fa-arrow-circle-right"></i>
</a>
</div>
{% endmacro %}
{% macro button_group_dropdown(title, actions) %}
<div class="btn-group">
<button type="button" class="btn btn-default">{{ title|trans }}</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for url, entry in actions %}
<li><a href="{{ url }}">{{ entry|trans }}</a></li>
{% endfor %}
</ul>
</div>
{% endmacro %}
{% macro button_group(actions) %}
{% import _self as macro %}
<div class="btn-group">
{% for icon, url in actions %}
{{ macro.button_action(icon, url) }}
{% endfor %}
</div>
{% endmacro %}
{% macro button_action(icon, url) %}
<a href="{{ url }}" class="btn btn-default btn-{{ icon }}">
<i class="fa fa-{{ icon }}"></i>
</a>
{% endmacro %}

View File

@@ -1,40 +0,0 @@
{% extends 'AvanzuAdminThemeBundle:layout:login-layout.html.twig' %}
{% block avanzu_login_title %}{{ 'browser.title'|trans }}{% endblock %}
{% block avanzu_logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block avanzu_login_box_msg %}
{{ 'security.title.login'|trans }}
{% if error|default(false) %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
{% endblock %}
{% block avanzu_login_form %}
<form action="{{ path('security_login') }}" method="post">
<div class="form-group has-feedback">
<input type="text" name="_username" class="form-control" placeholder="{{ 'security.label.username'|trans }}" value="{{ last_username }}">
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input name="_password" type="password" class="form-control" placeholder="{{ 'security.label.password'|trans }}">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-8">
{#
<div class="checkbox icheck">
<label>
<input type="checkbox"> {{ 'Remember Me'|trans({}, 'AvanzuAdminTheme') }}
</label>
</div>
#}
</div>
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">{{ 'security.action.sign_in'|trans }}</button>
</div>
</div>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/>
</form>
{% endblock %}
{% block avanzu_login_social_auth %}{% endblock %}
{% block avanzu_login_actions %}{% endblock %}

View File

@@ -1,254 +0,0 @@
{% extends 'base.html.twig' %}
{% block page_title %}{{ 'profile.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'profile.subtitle'|trans }}{% endblock %}
{% block main %}
{% import _self as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="row">
<div class="col-md-3">
{{ macro.profile_box(user, stats) }}
{{ macro.profile_infos(user, stats) }}
</div>
<div class="col-md-9">
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<li {% if tab == "charts" %}class="active"{% endif %}><a href="#charts" data-toggle="tab" aria-expanded="true">{{ 'profile.tab_monthly'|trans }}</a></li>
{% if form %}
<li {% if tab == "profile" %}class="active"{% endif %}><a href="#profile" data-toggle="tab" aria-expanded="false">{{ 'profile.settings'|trans }}</a></li>
{% endif %}
{% if form_password %}
<li {% if tab == "password" %}class="active"{% endif %}><a href="#password" data-toggle="tab" aria-expanded="false">{{ 'profile.password'|trans }}</a></li>
{% endif %}
{% if form_roles %}
<li {% if tab == "roles" %}class="active"{% endif %}><a href="#roles" data-toggle="tab" aria-expanded="false">{{ 'profile.roles'|trans }}</a></li>
{% endif %}
</ul>
<div class="tab-content">
<div class="tab-pane {% if tab == "charts" %}active{% endif %}" id="charts">
{% if years is empty %}
{{ widgets.callout('warning', 'error.no_entries_found') }}
{% endif %}
<script type="text/javascript">
var barChartOptions = {
scaleBeginAtZero: true,
scaleShowGridLines: true,
scaleGridLineColor: "rgba(0,0,0,.05)",
scaleGridLineWidth: 1,
scaleShowHorizontalLines: true,
scaleShowVerticalLines: true,
barShowStroke: true,
barStrokeWidth: 2,
barValueSpacing: 5,
barDatasetSpacing: 1,
responsive: true,
maintainAspectRatio: true,
tooltipTemplate: "<%= value %> {{ 'label.hours'|trans }}",
};
var barChartLabels = [
{% for i in 1..11 %}
"{{ ('month.' ~i)|trans }}",
{% endfor %}
"{{ 'month.12'|trans }}"
];
</script>
{% for year,yearStat in years %}
<h2>{{ year }}</h2>
<div class="chart">
<canvas id="barChart{{ year }}" style="height: 230px;"></canvas>
</div>
<script type="text/javascript">
var barChartData{{ year }} = {
labels: barChartLabels,
datasets: [
{
{# label: '# hours',#}
fillColor: "#00a65a",
strokeColor: "#00a65a",
pointColor: "#00a65a",
pointStrokeColor: "#c1c7d1",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [
{% for i in 1..11 %}
{{ (yearStat.month(i).totalDuration / 3600)|round }},
{% endfor %}
{{ (yearStat.month(12).totalDuration / 3600)|round }}
]
}
]
};
{% if tab == "charts" %}
$(function () {
var barChartCanvas{{ year }} = $("#barChart{{ year }}").get(0).getContext("2d");
var barChart = new Chart(barChartCanvas{{ year }});
barChart.Bar(barChartData{{ year }}, barChartOptions);
});
{% endif %}
$('a[href="#charts"]').on('shown.bs.tab', function(e){
var barChartCanvas{{ year }} = $("#barChart{{ year }}").get(0).getContext("2d");
var barChart = new Chart(barChartCanvas{{ year }});
barChart.Bar(barChartData{{ year }}, barChartOptions);
});
</script>
{% endfor %}
</div>
{% if form %}
<div class="tab-pane {% if tab == "profile" %}active{% endif %}" id="profile">
{{ form_start(form) }}
{{ form_widget(form) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_end(form) }}
</div>
{% endif %}
{% if form_password %}
<div class="tab-pane {% if tab == "password" %}active{% endif %}" id="password">
{{ form_start(form_password) }}
{{ form_widget(form_password) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_end(form_password) }}
</div>
{% endif %}
{% if form_roles %}
<div class="tab-pane {% if tab == "roles" %}active{% endif %}" id="roles">
{{ form_start(form_roles) }}
{{ form_widget(form_roles) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_end(form_roles) }}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% macro profile_infos(user, stats) %}
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'profile.about_me'|trans }}</h3>
</div>
<div class="box-body">
<ul class="nav nav-stacked">
{# colors = purple, blue, aqua, red, green #}
{# FIXME hourly rate must be dynamic <li><a href="#">{{ 'label.hourly_rate'|trans }} <span class="pull-right badge bg-blue">70</span></a></li> #}
<li><a href="#">{{ 'label.id'|trans }} <span class="pull-right badge bg-aqua">{{ user.id }}</span></a></li>
<li><a href="#">{{ 'label.username'|trans }} <span class="pull-right badge bg-blue">{{ user.username }}</span></a></li>
<li><a href="#">{{ 'label.language'|trans }} <span class="pull-right badge bg-green">{{ user.language }}</span></a></li>
<li><a href="#">{{ 'profile.first_entry'|trans }} <span class="pull-right badge bg-purple">{{ stats.firstEntry|date }}</span></a></li>
</ul>
</div>
</div>
{% endmacro %}
{% macro profile_box(user, stats) %}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-primary">
<div class="box-body box-profile">
{{ macro.avatar(user.avatar, user.username, 'profile-user-img img-responsive img-circle') }}
<h3 class="profile-username text-center">{{ widgets.username(user) }}</h3>
<p class="text-muted text-center">{{ user.title }}</p>
<ul class="list-group list-group-unbordered">
<li class="list-group-item">
<b>{{ 'stats.durationThisMonth'|trans }}</b> <a class="pull-right">{{ stats.durationThisMonth|duration }}</a>
</li>
<li class="list-group-item">
<b>{{ 'stats.amountThisMonth'|trans }}</b> <a class="pull-right">{{ stats.amountThisMonth|money }}</a>
</li>
<li class="list-group-item">
<b>{{ 'stats.durationTotal'|trans }}</b> <a class="pull-right">{{ stats.durationTotal|duration }}</a>
</li>
<li class="list-group-item">
<b>{{ 'stats.amountTotal'|trans }}</b> <a class="pull-right">{{ stats.amountTotal|money }}</a>
</li>
</ul>
{# <a href="#" class="btn btn-primary btn-block"><b>Follow</b></a> #}
</div>
<!-- /.box-body -->
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_list_unused(user, items, color) %}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user-2">
<!-- Add the bg color to the header using any of the bg-* classes -->
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<!-- /.widget-user-image -->
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="box-footer no-padding">
<ul class="nav nav-stacked">
{% for entry in items %}
<li><a href="{{ entry.url }}">{{ entry.title|trans }} <span class="pull-right badge bg-{{ entry.color }}">{{ entry.value }}</span></a></li>
{% endfor %}
</ul>
</div>
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_box_unused(user, stats, color) %}
{% import "AvanzuAdminThemeBundle:layout:macros.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user">
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<div class="box-footer">
<div class="row">
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationTotal|duration }}</h5>
<span class="description-text">{{ 'stats.durationTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountTotal|money }}</h5>
<span class="description-text">{{ 'stats.amountTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationThisMonth|duration }}</h5>
<span class="description-text">{{ 'stats.durationThisMonth'|trans }}</span>
</div>
</div>
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountThisMonth|money }}</h5>
<span class="description-text">{{ 'stats.amountThisMonth'|trans }}</span>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -1,13 +0,0 @@
<?php
use Doctrine\Common\Annotations\AnnotationRegistry;
use Composer\Autoload\ClassLoader;
/**
* @var ClassLoader $loader
*/
$loader = require __DIR__.'/../vendor/autoload.php';
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
return $loader;

View File

@@ -1,105 +0,0 @@
# This is the main configuration file of your application. It stores all the
# common options for every execution environment ('prod', 'dev', 'test').
# To avoid creating configuration files that are too long, we first import some
# files that define the values for important parts of the application,
# such as the security component and the dependency injection services
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
# These are the configuration parameters that define the application's behavior
# and which are independent from the underlying technical infrastructure
# See http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
# This parameter defines the codes of the locales (languages) enabled in the application
app_locales: en|de
app.notifications.email_sender: "%mailer_from%"
# Basic configuration for the Symfony framework features
framework:
# Uncomment the 'ide' option to turn all of the file paths in an exception
# page into clickable links that open the given file using your favorite IDE.
# Supported values are 'textmate', 'macvim', 'emacs' and 'sublime' shortcuts
# and any custom configuration string, such as: "phpstorm://open?file=%%f&line=%%l"
# See http://symfony.com/doc/current/reference/configuration/framework.html#ide
#ide: sublime
# When using the HTTP Cache, ESI allows to render page fragments separately
# and with different cache configurations for each fragment
# http://symfony.com/doc/current/book/http_cache.html#edge-side-includes
esi: { enabled: true }
translator: { fallback: "%locale%" }
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: ~
form: ~
csrf_protection: ~
validation: { enable_annotations: true }
templating:
engines: ['twig']
default_locale: "%locale%"
trusted_hosts: ~
session:
handler_id: session.handler.native_file
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
fragments: ~
http_method_override: true
assets: ~
# Twig Configuration (used for rendering application templates)
twig:
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"
# form_themes:
# - "bootstrap_3_layout.html.twig"
# - "form/fields.html.twig"
globals:
kimai_context:
date_1: "d.m.Y" # used for display in timesheets
box_color: "green" # a color for ???
active_warning: 3 # display a warning color if the user has at least X active recordings
# Assetic Configuration (used for managing web assets: CSS, JavaScript, Sass, etc.)
#assetic:
# debug: "%kernel.debug%"
# use_controller: false
# bundles: [ ]
# filters:
# cssrewrite: ~
# jsqueeze: ~
# scssphp:
# # the formatter must be the FQCN (don't use the 'compressed' value)
# formatter: "Leafo\\ScssPhp\\Formatter\\Compressed"
# Doctrine Configuration (used to access databases and manipulate their information)
doctrine:
dbal:
url: "%database_url%"
orm:
auto_generate_proxy_classes: "%kernel.debug%"
auto_mapping: true
# Swiftmailer Configuration (used to send emails)
#swiftmailer:
# transport: "%mailer_transport%"
# host: "%mailer_host%"
# username: "%mailer_user%"
# password: "%mailer_password%"
# spool: { type: memory }
avanzu_admin_theme:
use_twig: true
use_assetic: true
bower_bin: "/usr/local/bin/bower"
options:
default_avatar: avatars/default.png
skin: skin-green-light
fixed_layout: false
boxed_layout: false
collapsed_sidebar: true
mini_sidebar: true
control_sidebar: false

View File

@@ -1,47 +0,0 @@
imports:
- { resource: config.yml }
- { resource: services_dev.yml }
framework:
router:
resource: "%kernel.root_dir%/config/routing_dev.yml"
strict_requirements: true
profiler: { only_exceptions: false }
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: info
console:
type: console
bubble: false
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
#assetic:
# use_controller: true
#swiftmailer:
# delivery_address: me@example.com
services:
twig.text_extension:
class: Twig_Extensions_Extension_Text
tags:
- name: twig.extension
web_profiler:
toolbar: '%kernel.debug%'
intercept_redirects: false
# See routing_dev.yml
#avanzu_admin_theme:
# enable_demo: false

View File

@@ -1,25 +0,0 @@
imports:
- { resource: config.yml }
#framework:
# cache:
# system: cache.adapter.apcu
#doctrine:
# orm:
# metadata_cache_driver: apc
# result_cache_driver: apc
# query_cache_driver: apc
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
console:
type: console

View File

@@ -1,31 +0,0 @@
imports:
- { resource: config_dev.yml }
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
web_profiler:
toolbar: false
intercept_redirects: false
swiftmailer:
disable_delivery: true
# It's recommended to use a separate database for tests. This allows to have a
# fixed and known set of data fixtures, it simplifies the code of tests and it
# makes them more robust.
# In this case we just need to define a different path for the application database.
doctrine:
dbal:
database_url: 'sqlite:///%kernel.root_dir%/data/timesheet_test.sqlite'
# this configuration simplifies testing URLs protected by the security mechanism
# See http://symfony.com/doc/current/cookbook/testing/http_authentication.html
security:
firewalls:
secured_area:
http_basic: ~

View File

@@ -1,37 +0,0 @@
# This file defines the canonical configuration parameters of the application.
# Symfony uses this file as a template to generate the real app/config/parameters.yml used by the application.
# See http://symfony.com/doc/current/best_practices/configuration.html#canonical-parameters
parameters:
# used to load additional doctrine functions (possible values: mysql, sqlite)
database_engine: sqlite
# Define the database connection string
database_url: 'sqlite:///%kernel.root_dir%/data/timesheet.sqlite'
# database_url: 'mysql://root:pass@127.0.0.1:3306/timesheet'
# You can prefix all your tables to pevent collision with other software
database_prefix: kimai2_
# You can create the database and load the sample data from the command line:
#
# php bin/console doctrine:database:create
# php bin/console doctrine:schema:create
# php bin/console doctrine:fixtures:load
# If you don't use a real mail server, you can send emails via your Gmail account.
# see http://symfony.com/doc/current/cookbook/email/gmail.html
mailer_transport: smtp
mailer_host: 127.0.0.1
mailer_user: ~
mailer_password: ~
mailer_from: anonymous@example.com
# The code of the default language used by the application ('en' = English, 'de' = German)
locale: de
# The 'secret' value is a random string of characters, numbers and symbols
# used internally by Symfony in several places (CSRF tokens, URI signing,
# 'Remember Me' functionality, etc.)
# see: http://symfony.com/doc/current/reference/configuration/framework.html#secret
secret: 'please_change_this_secret_to_something_unique'

View File

@@ -1,39 +0,0 @@
# These first lines load the all routes defined as @Route() annotations in any
# controller defined inside the src/AppBundle/Controller/ directory. Controllers
# are loaded recursively, so you can separate them into subdirectories.
# If you don't like to add annotations in your applications, you can also define
# the routes in YAML, XML or PHP files.
# See http://symfony.com/doc/current/book/routing.html
app:
resource: '@AppBundle/Controller/'
type: annotation
prefix: /{_locale}
requirements:
_locale: '%app_locales%'
defaults:
_locale: '%locale%'
timesheet:
resource: '@TimesheetBundle/Controller/'
type: annotation
prefix: /{_locale}
requirements:
_locale: '%app_locales%'
defaults:
_locale: '%locale%'
# These lines define a route using YAML configuration. The controller used by
# the route (FrameworkBundle:Template:template) is a convenient shortcut when
# the template can be rendered without executing any logic in your own controller.
# See http://symfony.com/doc/current/cookbook/templating/render_without_controller.html
homepage:
path: /{_locale}
defaults:
_controller: FrameworkBundle:Redirect:redirect
route: timesheet
permanent: true
avanzu_admin_profile:
path: /{_locale}/profile/{username}
options:
avanzu_admin_route: profile

View File

@@ -1,35 +0,0 @@
# this imports the routes used to display the web debug toolbar at the bottom of each page
_wdt:
resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
prefix: /_wdt
# this imports the routes needed to display the Symfony Profiler information
_profiler:
resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
prefix: /_profiler
# this imports the route used to test error pages. Just browse the following URL:
# /{_locale}/_error/{status_code}.{format}
# (e.g. /en/_error/404, /en/_error/403.json, /fr/_error/500.xml)
# See http://symfony.com/doc/current/cookbook/controller/error_pages.html#testing-error-pages-during-development
_errors:
resource: "@TwigBundle/Resources/config/routing/errors.xml"
prefix: /{_locale}/_error
requirements:
_locale: '%app_locales%'
defaults:
_locale: '%locale%'
# this loads the main routing file, which usually defines the routes available
# in any environment (production, development, etc.)
_main:
resource: routing.yml
# ========================================================================
# Demo routes, only work with "avanzu_admin_theme/enable_demo: true"
# ========================================================================
#avanzu_admin:
# prefix: /admin
# resource: "@AvanzuAdminThemeBundle/Resources/config/routes.yml"

View File

@@ -1,55 +0,0 @@
security:
encoders:
# Our user class and the algorithm we'll use to encode passwords
# http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
AppBundle\Entity\User: bcrypt
providers:
# in this example, users are stored via Doctrine in the database
# demo users can be found at src/AppBundle/DataFixtures/ORM/LoadFixtures.php
database_users:
entity: { class: AppBundle:User, property: username }
# http://symfony.com/doc/current/book/security.html#firewalls-authentication
firewalls:
secured_area:
# does what it says
logout_on_user_change: true
# this firewall applies to all URLs
pattern: ^/
# but the firewall does not require login on every page
# denying access is done in access_control or in your controllers
anonymous: ~
# This allows the user to login by submitting a username and password
# Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
form_login:
# The route name that the login form submits to
check_path: security_login
# The name of the route where the login form lives
# When the user tries to access a protected page, they are redirected here
login_path: security_login
# Secure the login form against CSRF
# Reference: http://symfony.com/doc/current/cookbook/security/csrf_in_login_form.html
csrf_token_generator: security.csrf.token_manager
logout:
# The route name the user can go to in order to logout
path: security_logout
# The name of the route to redirect to after logging out
target: homepage
role_hierarchy:
ROLE_USER: ROLE_CUSTOMER
ROLE_TEAMLEAD: ROLE_USER
ROLE_ADMIN: ROLE_TEAMLEAD
ROLE_SUPER_ADMIN: ROLE_ADMIN
access_control:
# this is a catch-all for the admin area
# additional security lives in the controllers
- { path: '^/(%app_locales%)$', role: IS_AUTHENTICATED_FULLY }
- { path: '^/(%app_locales%)/login', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }

View File

@@ -1,177 +0,0 @@
services:
# ================================================================================
# TWIG
# ================================================================================
markdown:
class: AppBundle\Utils\Markdown
app.twig.app_extension:
public: false
class: AppBundle\Twig\Extensions
arguments: ['@markdown', '%app_locales%']
tags:
- { name: twig.extension }
# twig function: template_from_string()
app.twig.loader_extension:
class: Twig_Extension_StringLoader
tags:
- { name: 'twig.extension' }
app.twig.intl_extension:
public: false
class: Twig_Extensions_Extension_Intl
tags:
- { name: twig.extension }
# ================================================================================
# SECURITY
# ================================================================================
# security voter to check user-profile access
app.voter.user:
class: AppBundle\Voter\UserVoter
arguments: ["@security.access.decision_manager"]
tags:
- { name: security.voter }
# security voter to check customer access
timesheet.voter.customer:
class: TimesheetBundle\Voter\CustomerVoter
arguments: ["@security.access.decision_manager"]
tags:
- { name: security.voter }
# security voter to check project access
timesheet.voter.project:
class: TimesheetBundle\Voter\ProjectVoter
arguments: ["@security.access.decision_manager"]
tags:
- { name: security.voter }
# security voter to check activity access
timesheet.voter.activity:
class: TimesheetBundle\Voter\ActivityVoter
arguments: ["@security.access.decision_manager"]
tags:
- { name: security.voter }
# security voter to check activity access
timesheet.voter.timesheet:
class: TimesheetBundle\Voter\TimesheetVoter
arguments: ["@security.access.decision_manager"]
tags:
- { name: security.voter }
# ================================================================================
# FORMS
# ================================================================================
# form to edit user roles
app.admin.user_role:
class: AppBundle\Form\Type\UserRoleType
arguments: ["%security.role_hierarchy.roles%"]
tags:
- { name: form.type }
# event-listener to populate the navigation
app.menu_builder:
class: AppBundle\EventListener\MenuBuilder
arguments: ["@event_dispatcher", "@security.authorization_checker"]
tags:
- { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupNavbar }
# ================================================================================
# DATABASE
# ================================================================================
# service that prefixes every database table
app.tableprefix_subscriber:
class: AppBundle\Doctrine\TablePrefixSubscriber
arguments: ["%database_prefix%"]
tags:
- { name: doctrine.event_subscriber }
app.database_listener.timesheet:
class: TimesheetBundle\EventListener\TimesheetListener
tags:
- { name: doctrine.event_listener, event: prePersist, lazy: true }
- { name: doctrine.event_listener, event: preUpdate, lazy: true }
# Uncomment the following lines to define a service for the Post Doctrine repository.
# It's not mandatory to create these services, but if you use repositories a lot,
# these services simplify your code:
#
# app.post_repository:
# class: Doctrine\ORM\EntityRepository
# factory: ['@doctrine.orm.entity_manager', getRepository]
# arguments: [AppBundle\Entity\User]
#
# // traditional code inside a controller
# $entityManager = $this->getDoctrine()->getManager();
# $posts = $entityManager->getRepository('AppBundle:User')->findAll();
#
# // same code using repository services
# $posts = $this->get('app.user_repository')->findAll();
# ================================================================================
# THEME
# ================================================================================
avanzu_admin_theme.navbar_user_listener:
class: AppBundle\EventListener\NavbarShowUserListener
arguments: ["@security.token_storage"]
tags:
- { name: kernel.event_listener, event: theme.navbar_user, method: onShowUser }
- { name: kernel.event_listener, event: theme.sidebar_user, method: onShowUser }
# avanzu_admin_theme.navbar_task_listener:
# class: "%avanzu_admin_theme.navbar_task_listener.class%"
# tags:
# - { name: kernel.event_listener, event: theme.tasks, method: onListTasks }
#
# avanzu_admin_theme.navbar_notify_listener:
# class: "%avanzu_admin_theme.navbar_notify_listener.class%"
# tags:
# - { name: kernel.event_listener, event: theme.notifications, method: onListNotifications }
#
# avanzu_admin_theme.navbar_msg_listener:
# class: "%avanzu_admin_theme.navbar_msg_listener.class%"
# tags:
# - { name: kernel.event_listener, event: theme.messages, method: onListMessages }
#
# avanzu_admin_theme.setup_menu_listener:
# class: "%avanzu_admin_theme.setup_menu_listener.class%"
# tags:
# - { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupMenu }
# - { name: kernel.event_listener, event: theme.breadcrumb, method: onSetupMenu }
# ================================================================================
# APPLICATION CORE
# ================================================================================
# a route listener, that injects the locale through a URL directory
app.redirect_to_preferred_locale_listener:
class: AppBundle\EventListener\RedirectToPreferredLocaleListener
arguments: ['@router', '%app_locales%', '%locale%']
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
# additional menu entries for the timesheet bundle
timesheet.configure_menu_listener:
class: TimesheetBundle\EventListener\Menu
tags:
- { name: kernel.event_listener, event: app.main_menu_configure, method: onMainMenuConfigure }
- { name: kernel.event_listener, event: app.admin_menu_configure, method: onAdminMenuConfigure }
# ================================================================================
# CONSOLE COMMANDS
# ================================================================================
app.command.create_user:
class: AppBundle\Command\CreateUserCommand
arguments: ["@security.password_encoder", "@doctrine", "@validator"]
tags:
- { name: console.command, command: kimai:create-user }

View File

@@ -1,32 +0,0 @@
imports:
- { resource: services.yml }
services:
# ================================================================================
# CONSOLE COMMANDS
# ================================================================================
app.command.run_phpcs:
class: AppBundle\Command\RunCodeSnifferCommand
arguments: ["%kernel.root_dir%"]
tags:
- { name: console.command, command: kimai:dev:phpcs }
app.command.run_unit_tests:
class: AppBundle\Command\RunUnitTestsCommand
arguments: ["%kernel.root_dir%"]
tags:
- { name: console.command, command: kimai:dev:test-unit }
app.command.run_integration_tests:
class: AppBundle\Command\RunIntegrationTestsCommand
arguments: ["%kernel.root_dir%"]
tags:
- { name: console.command, command: kimai:dev:test-integration }
app.command.dev_reset:
class: AppBundle\Command\DevResetCommand
arguments: ["%kernel.root_dir%"]
tags:
- { name: console.command, command: kimai:dev:reset }

82
assets/app.js Normal file
View File

@@ -0,0 +1,82 @@
// ------------------- 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/fr');
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/pt-BR');
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/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-ch');
require('moment/locale/es');
require('moment/locale/eu');
require('moment/locale/fr');
require('moment/locale/hu');
require('moment/locale/it');
require('moment/locale/ja');
require('moment/locale/ko');
require('moment/locale/nl');
require('moment/locale/pt-br');
require('moment/locale/ru');
require('moment/locale/sk');
require('moment/locale/sv');
require('moment/locale/tr');
require('moment/locale/zh-cn');
require('daterangepicker');
const Sortable = require('sortablejs/Sortable.min');
global.Sortable = Sortable;
// ------ AdminLTE framework ------
require('./sass/admin-lte.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');
// ------ Kimai itself ------
require('./js/KimaiWebLoader.js');
global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget').default;
global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default;
// ------ Autocomplete for tags only ------
require('jquery-ui/ui/widgets/autocomplete');

25
assets/calendar.js Normal file
View File

@@ -0,0 +1,25 @@
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-ch');
require('fullcalendar/dist/locale/es');
require('fullcalendar/dist/locale/eu');
require('fullcalendar/dist/locale/fr');
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/pt-br');
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/fullcalendar.min.css');

2
assets/chart.js Normal file
View File

@@ -0,0 +1,2 @@
require('chart.js/dist/Chart.min');

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/images/signature.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,26 @@
/*
* 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] KimaiConfiguration: handling all configuration and runtime settings
*/
export default class KimaiConfiguration {
constructor(configurations) {
this._configurations = configurations;
}
get(name) {
return this._configurations[name];
}
has(name) {
return name in this._configurations;
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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] KimaiContainer
*
* ServiceContainer for Kimai
*/
import KimaiConfiguration from './KimaiConfiguration';
import KimaiTranslation from './KimaiTranslation';
import KimaiPlugin from './KimaiPlugin';
export default class KimaiContainer {
/**
* Create a new Container with the given configurations and translations.
*
* @param {Object} configuration
* @param {Object} translation
*/
constructor(configuration, translation) {
if (!(configuration instanceof KimaiConfiguration)) {
throw new Error('Configuration needs to a KimaiConfiguration instance');
}
this._configuration = configuration;
if (!(translation instanceof KimaiTranslation)) {
throw new Error('Configuration needs to a KimaiTranslation instance');
}
this._translation = translation;
this._plugins = [];
}
/**
* Register a new Plugin.
*
* @param {KimaiPlugin} plugin
* @returns {KimaiPlugin}
*/
registerPlugin(plugin) {
if (!(plugin instanceof KimaiPlugin)) {
throw new Error('Invalid plugin given, needs to be a KimaiPlugin instance');
}
plugin.setContainer(this);
this._plugins.push(plugin);
return plugin;
}
/**
* @param {string} name
* @returns {KimaiPlugin}
*/
getPlugin(name) {
for (let plugin of this._plugins) {
if (plugin.getId() !== null && plugin.getId() === name) {
return plugin;
}
}
throw new Error('Unknown plugin: ' + name);
}
/**
* @returns {Array<KimaiPlugin>}
*/
getPlugins() {
return this._plugins;
}
/**
* @returns {KimaiTranslation}
*/
getTranslation() {
return this._translation;
}
/**
* @returns {KimaiConfiguration}
*/
getConfiguration() {
return this._configuration;
}
}

90
assets/js/KimaiLoader.js Normal file
View File

@@ -0,0 +1,90 @@
/*
* 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] KimaiLoader: bootstrap the application and all plugins
*/
import moment from 'moment';
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 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 KimaiForm from "./plugins/KimaiForm";
import KimaiDatePicker from "./plugins/KimaiDatePicker";
import KimaiConfirmationLink from "./plugins/KimaiConfirmationLink";
import KimaiMultiUpdateTable from "./plugins/KimaiMultiUpdateTable";
export default class KimaiLoader {
constructor(configurations, translations) {
// set the current locale for all javascript components
moment.locale(configurations['locale'].replace('_', '-').toLowerCase());
const kimai = new KimaiContainer(
new KimaiConfiguration(configurations),
new KimaiTranslation(translations)
);
kimai.registerPlugin(new KimaiEvent());
kimai.registerPlugin(new KimaiAPI());
kimai.registerPlugin(new KimaiAlert());
kimai.registerPlugin(new KimaiFormSelect('.selectpicker'));
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.header-search', '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 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'));
// notify all listeners that Kimai plugins can now be registered
kimai.getPlugin('event').trigger('kimai.pluginRegister', {'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});
this.kimai = kimai;
}
getKimai() {
return this.kimai;
}
}

53
assets/js/KimaiPlugin.js Normal file
View File

@@ -0,0 +1,53 @@
/*
* 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] KimaiPlugin: base class for all plugins
*/
import KimaiContainer from "./KimaiContainer";
export default class KimaiPlugin {
/**
* Overwrite this method to initialize your plugin.
*
* It is called AFTER setContainer() and AFTER DOMContentLoaded was fired.
* You don't have access to the container before this method!
*/
init() {
}
/**
* If you return an ID, you indicate that your plugin can be used by other plugins.
*
* @returns {string|null}
*/
getId() {
return null;
}
/**
* @param {KimaiContainer} core
*/
setContainer(core) {
if (!(core instanceof KimaiContainer)) {
throw new Error('Plugin was given an invalid KimaiContainer');
}
this._core = core;
}
/**
* This function returns null, if xou call it BEFORE init().
*
* @returns {KimaiContainer}
*/
getContainer() {
return this._core;
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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] KimaiTranslation: handling translation strings
*/
export default class KimaiTranslation {
constructor(translations) {
this._translations = translations;
}
get(name) {
return this._translations[name];
}
has(name) {
return name in this._translations;
}
}

View File

@@ -0,0 +1,33 @@
/*
* This file is part of the Kimai time-tracking app.
*
* Main JS application file for Kimai 2. This file should be included in all pages.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] Wrapper class for loading Kimai app in browser script scope
*/
import KimaiLoader from "./KimaiLoader";
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], function () {
return (root.KimaiWebLoader = factory());
});
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.KimaiWebLoader = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
class KimaiWebLoader extends KimaiLoader {
}
return KimaiWebLoader;
}));

View File

@@ -0,0 +1,65 @@
/*
* 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] KimaiAPI: easy access to API methods
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiAPI extends KimaiPlugin {
getId() {
return 'api';
}
get(url, data, callbackSuccess, callbackError) {
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
method: 'GET',
data: data,
dataType: 'json',
success: callbackSuccess,
error: callbackError
});
}
patch(url, data, callbackSuccess, callbackError) {
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
method: 'PATCH',
data: data,
dataType: 'json',
success: callbackSuccess,
error: callbackError
});
}
delete(url, callbackSuccess, callbackError) {
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
method: 'DELETE',
dataType: 'json',
success: callbackSuccess,
error: callbackError
});
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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";
/**
* Needs to be initialized with a class name.
*
* A link like <a href=# class=remoteLink> can be activated with:
* new KimaiAPILink('remoteLink')
*
* Allows to assign the given selector to any element, which then is used as click-handler
* calling an API method and trigger the event from data-event attribute afterwards.
*
* @param selector
*/
export default class KimaiAPILink extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
const self = this;
document.addEventListener('click', function(event) {
let target = event.target;
while (target !== null && !target.matches('body')) {
if (target.classList.contains(self.selector)) {
const attributes = target.dataset;
let url = attributes['href'];
if (!url) {
url = target.getAttribute('href');
}
if (attributes.question !== undefined) {
self.getContainer().getPlugin('alert').question(attributes.question, function(value) {
if (value) {
self._callApi(url, attributes);
}
});
} else {
self._callApi(url, attributes);
}
event.preventDefault();
event.stopPropagation();
}
target = target.parentNode;
}
});
}
_callApi(url, attributes)
{
const method = attributes['method'];
const eventName = attributes['event'];
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);
}
};
const errorHandle = function(xhr, err) {
let message = 'action.update.error';
if (attributes.msgError) {
message = attributes.msgError;
}
if (xhr.responseJSON && xhr.responseJSON.message) {
err = xhr.responseJSON.message;
}
alert.error(message, err);
};
if (method === 'PATCH') {
let data = {};
if (attributes.payload) {
data = attributes.payload;
}
API.patch(url, data, successHandle, errorHandle);
} else if (method === 'DELETE') {
API.delete(url, successHandle, errorHandle);
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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] KimaiActiveRecords: responsible to display the users active records
*/
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiActiveRecords extends KimaiPlugin {
constructor(selector, selectorEmpty) {
super();
this.selector = selector;
this.selectorEmpty = selectorEmpty;
}
getId() {
return 'active-records';
}
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');
this.label = menu.querySelector('a > span.label');
const self = this;
const handle = function() { self.reloadActiveRecords(); };
document.addEventListener('kimai.timesheetUpdate', handle);
document.addEventListener('kimai.activityUpdate', handle);
document.addEventListener('kimai.projectUpdate', handle);
document.addEventListener('kimai.customerUpdate', handle);
}
emptyList() {
this.itemList.innerHTML = '';
}
_toggleMenu(hasEntries) {
const menu = document.querySelector(this.selector);
const menuEmpty = document.querySelector(this.selectorEmpty);
menu.style.display = hasEntries ? 'inline-block' : 'none';
if (menuEmpty !== null) {
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
}
}
setEntries(entries) {
this._toggleMenu(entries.length > 0);
if (entries.length === 0) {
this.label.innerText = '';
this.emptyList();
return;
}
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 }" data-format="${ this.attributes['format'] }">${ durations.formatDuration(timesheet.duration, this.attributes['format']) }</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');
API.get(this.attributes['api'], {}, function(result) {
self.setEntries(result);
});
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.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) {
document.title = document.querySelector('body').dataset['title'];
return;
}
for(let record of activeRecords) {
const since = record.getAttribute('data-since');
const format = record.getAttribute('data-format');
const duration = this.formatDuration(since, format);
if (record.getAttribute('data-title') !== null) {
durations.push(duration);
}
record.textContent = duration;
}
if (durations.length === 0) {
return;
}
let title = durations.shift();
let prefix = ' | ';
for (let duration of durations.slice(0, 2)) {
title += prefix + duration;
}
document.title = title;
}
formatDuration(since, format) {
const duration = moment.duration(moment(new Date()).diff(moment(since)));
let hours = parseInt(duration.asHours()).toString();
let minutes = duration.minutes();
let seconds = duration.seconds();
// 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;
}
return format.replace('%h', hours).replace('%m', ('0'+minutes).substr(-2)).replace('%s', ('0'+seconds).substr(-2));
}
}

View File

@@ -0,0 +1,209 @@
/*
* 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] KimaiAjaxModalForm
*
* allows to assign the given selector to any element, which then is used as click-handler:
* opening a modal with the content from the URL given in the elements 'data-href' or 'href' attribute
*/
import jQuery from 'jquery';
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableRow {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'modal';
}
init() {
const self = this;
this.modal = jQuery('#remote_form_modal');
this.modal.on('hide.bs.modal', function () {
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
jQuery(self._getFormIdentifier()).find('input[type=text],textarea,select').filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
this._addClickHandlerReducedInTableRow(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;
}
};
}
jQuery.ajax({
url: url,
success: function(html) {
self._openFormInModal(html);
},
error: errorHandler
});
}
/**
* Returns the CSS selector for the modal form.
*
* @returns {string}
* @private
*/
_getFormIdentifier() {
return '#remote_form_modal .modal-content form';
}
_openFormInModal(html) {
const self = this;
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;
// will be (re-)activated later
form.off('submit');
// 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 (jQuery(html).find('#form_modal').hasClass('modal-danger')) {
remoteModal.addClass('modal-danger');
}
jQuery('#remote_form_modal .modal-content').replaceWith(
jQuery(html).find('#form_modal .modal-content')
);
// activate new loaded widgets
self.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);
}
// -----------------------------------------------------------------------
// 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;
});
// -----------------------------------------------------------------------
this.getContainer().getPlugin('toolbar').hide();
remoteModal.modal('show');
// the new form that was loaded via ajax
form = jQuery(formIdentifier);
// 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');
event.preventDefault();
event.stopPropagation();
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;
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';
}
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);
}
});
});
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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] KimaiAlert: notifications for Kimai
*/
import Swal from 'sweetalert2'
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiAlert extends KimaiPlugin {
getId() {
return 'alert';
}
error(title, message) {
const translation = this.getContainer().getTranslation();
if (translation.has(title)) {
title = translation.get(title);
}
if (translation.has(message)) {
message = translation.get(message);
}
Swal.fire({
icon: 'error',
title: title.replace('%reason%', ''),
text: message,
});
}
success(message) {
const translation = this.getContainer().getTranslation();
if (translation.has(message)) {
message = translation.get(message);
}
Swal.fire({
timer: 2000,
timerProgressBar: true,
toast: true,
position: 'top-end',
showConfirmButton: false,
icon: 'success',
title: message,
});
}
/**
* Callback receives a value and needs to decide what should happen with it
*
* @param message
* @param callback
*/
question(message, callback) {
const translation = this.getContainer().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);
});
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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] KimaiAlternativeLinks
*
* allows to assign the given selector to any element, which then is used as click-handler
* redirecting to the URL given in the elements 'data-href' or 'href' attribute
*/
import jQuery from 'jquery';
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
export default class KimaiAlternativeLinks extends KimaiClickHandlerReducedInTableRow {
constructor(selector) {
super();
this.selector = selector;
}
init() {
this._addClickHandlerReducedInTableRow(this.selector, function(href) {
window.location = href;
});
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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.getContainer().getConfiguration().get('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

@@ -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] KimaiClickHandlerReducedInTableRow: abstract class
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiClickHandlerReducedInTableRow extends KimaiPlugin {
_addClickHandlerReducedInTableRow(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) {
return;
}
// handles the "click" on table rows to open an entry for editing
let target = event.target;
if (event.currentTarget.matches('tr')) {
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;
}
}
event.preventDefault();
event.stopPropagation();
let href = jQuery(this).attr('data-href');
if (!href) {
href = jQuery(this).attr('href');
}
callback(href);
});
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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";
/**
* Needs to be initialized with a class name.
*
* Allows to assign the given selector to any element, which then is used as click-handler
* calling an API method and trigger the event from data-event attribute afterwards.
*
* @param selector
*/
export default class KimaiConfirmationLink extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
const self = this;
document.addEventListener('click', function(event) {
let target = event.target;
while (target !== null && !target.matches('body')) {
if (target.classList.contains(self.selector)) {
const attributes = target.dataset;
// is this a link?
let url = attributes['href'];
// or another HTML element with a custom href
if (!url) {
url = target.getAttribute('href');
}
// or is this a button?
let form = null;
if (target.type === 'submit' && target.form !== undefined) {
form = target.form;
}
if (attributes.question !== undefined) {
self.getContainer().getPlugin('alert').question(attributes.question, function(value) {
if (value) {
if (form === null) {
document.location = url;
} else {
if (url !== null) {
form.action = url;
}
form.submit();
}
}
});
}
event.preventDefault();
event.stopPropagation();
}
target = target.parentNode;
}
});
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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] KimaiDatatable: handles functionality for the datatable
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiDatatable extends KimaiPlugin {
constructor(contentAreaSelector, tableSelector) {
super();
this.contentArea = contentAreaSelector;
this.selector = tableSelector;
}
getId() {
return 'datatable';
}
init() {
const dataTable = document.querySelector(this.selector);
// not every page contains a dataTable
if (dataTable === null) {
return;
}
const attributes = dataTable.dataset;
const events = attributes['reloadEvent'];
if (events === undefined) {
return;
}
const self = this;
const handle = function() { self.reloadDatatable(); };
for (let eventName of events.split(' ')) {
document.addEventListener(eventName, handle);
}
if (this.getContainer().getConfiguration().get('autoReloadDatatable')) {
document.addEventListener('toolbar-change', handle);
} else {
document.addEventListener('pagination-change', handle);
document.addEventListener('filter-change', handle);
}
}
reloadDatatable() {
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();
},
error: function(xhr, err) {
form.submit();
}
});
}
}

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] 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 {
constructor(dataAttribute) {
super();
this.dataAttribute = dataAttribute;
}
getId() {
return 'datatable-column-visibility';
}
init() {
let dataTable = document.querySelector('[' + this.dataAttribute + ']');
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.modal.querySelector('button[data-type=reset]').addEventListener('click', function() {
self.resetVisibility();
});
for (let checkbox of this.modal.querySelectorAll('form input[type=checkbox]')) {
checkbox.addEventListener('click', function () {
self.changeVisibility(checkbox.getAttribute('name'));
});
}
}
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;
}
Cookies.set(form.getAttribute('name'), JSON.stringify(settings), {expires: 365});
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');
}
changeVisibility(columnName) {
const table = document.getElementById('datatable_' + this.id).getElementsByClassName('dataTable')[0];
let column = 0;
let foundColumn = false;
for (let columnElement of table.getElementsByTagName('th')) {
if (columnElement.getAttribute('data-field') === columnName) {
foundColumn = true;
break;
}
column++;
}
if (!foundColumn) {
return;
}
for (let rowElement of table.getElementsByTagName('tr')) {
rowElement.children[column].classList.toggle('hidden');
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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();
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: 1,
applyLabel: TRANSLATE.get('confirm'),
cancelLabel: TRANSLATE.get('cancel'),
customRangeLabel: TRANSLATE.get('customRange'),
daysOfWeek: moment.weekdaysShort(),
monthNames: moment.months(),
}
});
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

@@ -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] 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) {
let translator = this.getContainer().getTranslation();
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
let separator = jQuery(this).data('separator');
let rangesList = {};
rangesList[translator.get('today')] = [moment(), moment()];
rangesList[translator.get('yesterday')] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
rangesList[translator.get('thisWeek')] = [moment().startOf('week'), moment().endOf('week')];
rangesList[translator.get('lastWeek')] = [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')];
rangesList[translator.get('thisMonth')] = [moment().startOf('month'), moment().endOf('month')];
rangesList[translator.get('lastMonth')] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')];
rangesList[translator.get('thisYear')] = [moment().startOf('year'), moment().endOf('year')];
rangesList[translator.get('lastYear')] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')];
jQuery(this).daterangepicker({
showDropdowns: true,
autoUpdateInput: false,
autoApply: false,
linkedCalendars: false,
locale: {
separator: separator,
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('confirm'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange'),
daysOfWeek: moment.weekdaysShort(),
monthNames: moment.months(),
},
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

@@ -0,0 +1,66 @@
/*
* 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';
import moment from 'moment';
export default class KimaiDateTimePicker extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'date-time-picker';
}
activateDateTimePicker(selector) {
let translator = this.getContainer().getTranslation();
let configuration = this.getContainer().getConfiguration();
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
jQuery(this).daterangepicker({
singleDatePicker: true,
timePicker: true,
timePicker24Hour: configuration.get('twentyFourHours'),
showDropdowns: true,
autoUpdateInput: false,
locale: {
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('confirm'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange'),
daysOfWeek: moment.weekdaysShort(),
monthNames: moment.months(),
}
});
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

@@ -0,0 +1,33 @@
/*
* 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] KimaiEvent: helper to trigger events
*/
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiEvent extends KimaiPlugin {
getId() {
return 'event';
}
trigger(name, details) {
if (name === null || name === undefined) {
return;
}
for(let event of name.split(' ')) {
let triggerEvent = new Event(event);
if (details !== undefined) {
triggerEvent = new CustomEvent(name, {detail: details});
}
document.dispatchEvent(triggerEvent);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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] KimaiForm: basic functions for all forms
*/
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiForm extends KimaiPlugin {
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);
}
}

View File

@@ -0,0 +1,168 @@
/*
* 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.getContainer().getConfiguration().get('locale').replace('_', '-'),
theme: "bootstrap",
matcher: matcher
}};
jQuery(selector + ' ' + elementSelector).select2(options);
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('<option value="">' + emptyOption.text() + '</option>');
}
let htmlOptions = '';
let emptyOptions = '';
for (const [key, value] of Object.entries(data)) {
if (key === '__empty__') {
for (const entity of value) {
emptyOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
continue;
}
htmlOptions += '<optgroup label="' + key + '">';
for (const entity of value) {
htmlOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
htmlOptions += '</optgroup>';
}
select.append(htmlOptions);
select.append(emptyOptions);
// 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 be resetted
select.trigger('change');
// if the beta test kimai.theme.select_type is active, this will tell the selects to refresh
if (select.hasClass('selectpicker')) {
select.trigger('change.select2');
}
}
}

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] KimaiMultiUpdateForm: handle the multi update checkbox list and form
*/
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()
{
let ids = [];
jQuery('.multi_update_single:checked').each(function(i){
ids[i] = $(this).val();
});
return ids;
}
toggleForm()
{
const ids = this.getSelectedIds();
jQuery('#multi_update_table_entities').val(ids.join(','));
if (ids.length > 0) {
jQuery('#multi_update_form').show();
} else {
jQuery('#multi_update_form').hide();
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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

@@ -0,0 +1,87 @@
/*
* 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.timesheetStop', handle);
document.addEventListener('kimai.activityUpdate', handle);
document.addEventListener('kimai.projectUpdate', handle);
document.addEventListener('kimai.customerUpdate', 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

@@ -0,0 +1,130 @@
/*
* 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);
if (jQuery(this).val() === '') {
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) {
// debug: this case for example happens 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);
}
}
}
}
}
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

@@ -0,0 +1,76 @@
/*
* 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] KimaiThemeInitializer: initialize theme functionality
*/
import jQuery from 'jquery';
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');
}
/**
* redirect access denied / session timeouts to login page
*/
registerGlobalAjaxErrorHandler() {
const loginUrl = this.getContainer().getConfiguration().get('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);
}
});
}
}
});
}
/**
* auto hide success messages, as they are just meant as user feedback and not as a permanent information
*
* @param {string} selector
* @param {integer} interval
*/
registerAutomaticAlertRemove(selector, interval) {
const self = this;
this._alertRemoveHandler = setInterval(
function() {
self.hideAlert(selector);
},
interval
);
}
unregisterAutomaticAlertRemove() {
clearInterval(this._alertRemoveHandler);
}
/**
* @param {string} selector
*/
hideAlert(selector) {
jQuery(selector).alert('close');
}
}

View File

@@ -0,0 +1,232 @@
/*
* 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] 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;
}
getId() {
return 'toolbar';
}
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._registerSearchButtons(formSelector);
jQuery('body')
// prevent that the dropdown closes, when a form input is changed - eg. a select option was clicked
.on('click', formSelector + ' .dropdown-menu', function (event) {
event.stopPropagation();
})
// prevent that a click into the search field will close the dropdown
.on('click', '.select2-search__field', function (event) {
event.stopPropagation();
})
// prevent that the dropdown closes when a optgroup header is clicked
.on('click', '.select2-results__group', function (event) {
event.stopPropagation();
})
// prevent that a click into a daterangepicker will close the dropdown
.on('click', '.daterangepicker', function (event) {
event.stopPropagation();
})
// prevent that clicks in the dropdown elements, but outside of elements will close the dropdown (eg border besides the search field)
.on('click', '.select2-container', function (event) {
event.stopPropagation();
})
;
// 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();
});
// 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;
case 'project':
if (jQuery(formSelector + ' select#activity').length > 0) {
reload = false;
}
break;
}
jQuery(formSelector + ' input#page').val(1);
if (reload) {
self.triggerChange();
}
});
// close all open selectpicker upon choosing any dropdown option
jQuery(formSelector + ' select.selectpicker').on('change', function(event) {
jQuery('.bootstrap-select.open').removeClass('open');
});
}
/**
* The search toggle button is not part of this component, but it is directly connected to it.
* @private
*/
_registerSearchButtons(formSelector) {
jQuery('body')
// only for mobile experience currently: show the search form field
.on('click', '.btn-search.search-toggle', function (event) {
event.preventDefault();
event.stopPropagation();
jQuery(formSelector).parent('section').toggleClass('search-open');
jQuery(formSelector).toggleClass('hidden-xs');
jQuery(formSelector + ' input#searchTerm').dropdown('toggle');
jQuery(formSelector + ' input#searchTerm').focus();
})
// hide the search form field
.on('click', formSelector + ' a.search-cancel', function (event) {
event.preventDefault();
event.stopPropagation();
jQuery(formSelector).parent('section').toggleClass('search-open');
jQuery(formSelector + ' input#searchTerm').dropdown('toggle');
jQuery(formSelector).toggleClass('hidden-xs');
})
;
}
/**
* Some actions utilize the filter from the search form and submit it to another URL.
* @private
*/
_registerAlternativeSubmitActions(toolbarSelector, actionBtnClass) {
document.addEventListener('click', function(event) {
let target = event.target;
while (target !== null && !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';
form.action = target.href;
if (target.dataset.method !== undefined) {
form.method = target.dataset.method;
}
form.submit();
form.target = '';
form.action = prevAction;
form.method = prevMethod;
event.preventDefault();
event.stopPropagation();
}
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')) {
order = 'ASC';
}
jQuery(formSelector + ' input#orderBy').val(orderBy);
jQuery(formSelector + ' input#order').val(order);
// triggers the page reset - see below
jQuery(formSelector + ' input#order').trigger('change');
// triggers the datatable reload - search for the event name
EVENT.trigger('filter-change');
});
}
/**
* 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) {
return;
}
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');
return false;
});
}
hide() {
const toggle = jQuery(this.getSelector() + ' #searchTerm.dropdown-toggle');
if (toggle.parent().hasClass('open')) {
toggle.dropdown('toggle');
}
}
/**
* Triggers an event, that everyone can listen for.
*/
triggerChange() {
this.getContainer().getPlugin('event').trigger('toolbar-change');
}
/**
* Returns the CSS selector to target the toolbar form.
*
* @returns {string}
*/
getSelector() {
return this.formSelector;
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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] KimaiPaginatedBoxWidget: handles box widgets that have a pagination
*/
import jQuery from "jquery";
export default class KimaiPaginatedBoxWidget {
constructor(boxId) {
this.selector = boxId;
this.overlay = jQuery('<div class="overlay"><div class="fas fa-sync fa-spin"></div></div>');
this.widget = jQuery(this.selector);
this.href = this.widget.data('href');
this.events = this.widget.data('reload').split(' ');
const self = this;
const reloadPage = function (event) {
const page = jQuery(self.selector + ' .box-tools').data('page');
const url = self._buildUrl(page);
self.loadPage(url);
};
for (const eventName of this.events) {
document.addEventListener(eventName, reloadPage);
}
this.widget.on('click', '.box-tools a', function (event) {
event.preventDefault();
self.loadPage(jQuery(event.target).attr('href'));
});
}
static create(elementId) {
return new KimaiPaginatedBoxWidget(elementId);
}
_showOverlay() {
this.widget.append(this.overlay);
}
_hideOverlay() {
jQuery(this.overlay).remove();
}
loadPage(url) {
const self = this;
const selector = this.selector;
self._showOverlay();
jQuery.ajax({
url: url,
data: {},
success: function (response) {
const html = jQuery(response);
jQuery(selector + ' .box-tools').replaceWith(html.find('.box-tools'));
jQuery(selector + ' .box-body').replaceWith(html.find('.box-body'));
jQuery(selector + ' .box-title').replaceWith(html.find('.box-title'));
if (jQuery(selector + ' .box-footer').length > 0) {
jQuery(selector + ' .box-footer').replaceWith(html.find('.box-footer'));
}
jQuery(selector + ' .box-body [data-toggle="tooltip"]').tooltip();
self.widget.removeData('error-retry');
self._hideOverlay();
},
dataType: 'html',
error: function(jqXHR, textStatus, errorThrown) {
if (jqXHR.status !== undefined && jqXHR.status === 500) {
if (self.widget.data('error-retry') !== undefined) {
// TODO show error message ?
return;
}
const page = jQuery(selector + ' .box-tools').data('page');
if (page > 1) {
self.widget.data('error-retry', 1);
var url = self._buildUrl(page - 1);
self.loadPage(url);
}
}
self._hideOverlay();
}
});
}
_buildUrl(page) {
return this.href.replace('1', page);
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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] KimaiReloadPageWidget: a simple helper to reload the page on events
*/
import jQuery from "jquery";
export default class KimaiReloadPageWidget {
constructor(events, fullReload) {
this.overlay = jQuery('<div class="overlay-wrapper"><div class="overlay"><div class="fas fa-sync fa-spin"></div></div></div>');
this.widget = jQuery('div.content-wrapper');
const self = this;
const reloadPage = function (event) {
if (fullReload) {
document.location.reload(true);
} else {
self.loadPage(document.location);
}
};
for (const eventName of events.split(' ')) {
document.addEventListener(eventName, reloadPage);
}
}
static create(events, fullReload) {
if (fullReload === undefined || fullReload === null) {
fullReload = false;
}
return new KimaiReloadPageWidget(events, fullReload);
}
_showOverlay() {
this.widget.append(this.overlay);
}
_hideOverlay() {
jQuery(this.overlay).remove();
}
loadPage(url) {
const self = this;
self._showOverlay();
jQuery.ajax({
url: url,
data: {},
success: function (response) {
jQuery('section.content').replaceWith(
jQuery(response).find('section.content')
);
self._hideOverlay();
},
dataType: 'html',
error: function(jqXHR, textStatus, errorThrown) {
self._hideOverlay();
document.location = url;
}
});
}
}

View File

@@ -0,0 +1,10 @@
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
@import '~bootstrap-sass/assets/stylesheets/bootstrap';
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
@import '~@fortawesome/fontawesome-free/scss/regular';
@import '~@fortawesome/fontawesome-free/scss/solid';
@import '~@fortawesome/fontawesome-free/scss/brands';
@import '~daterangepicker/daterangepicker.css';

26
assets/sass/app.scss Normal file
View File

@@ -0,0 +1,26 @@
/*
* 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 'variables';
@import 'error-page';
@import 'print';
@import 'content';
@import 'content-header';
@import 'toolbar';
@import 'sidebar';
@import 'footer';
@import 'invoice';
@import 'datatable';
@import 'datatable-extensions';
@import 'calendar';
@import 'dashboard';
@import 'navbar';
@import 'daterangepicker';
@import 'skins';
@import 'sweetalert';
@import 'autocomplete';
@import 'selectpicker';

View File

@@ -0,0 +1,76 @@
/*
* 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.
*/
.ui-widget {
font-size: 1em/*{fsDefault}*/;
}
.ui-widget.ui-widget-content {
border: 1px solid #c5c5c5/*{borderColorDefault}*/;
}
.ui-widget-content {
border: 1px solid #dddddd/*{borderColorContent}*/;
background: #ffffff/*{bgColorContent}*/ /*{bgImgUrlContent}*/ /*{bgContentXPos}*/ /*{bgContentYPos}*/ /*{bgContentRepeat}*/;
color: #333333/*{fcContent}*/;
}
.ui-menu {
list-style: none;
padding: 0;
margin: 0;
display: block;
outline: 0;
}
.ui-menu .ui-menu-item {
margin: 0;
cursor: pointer;
}
.ui-autocomplete {
position: absolute;
top: 0;
left: 0;
cursor: default;
}
.ui-front {
z-index: 1200;
}
.ui-menu .ui-menu-item-wrapper {
position: relative;
padding: 3px 1em 3px .4em;
}
.ui-menu .ui-state-focus,
.ui-menu .ui-state-active {
margin: -1px;
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active,
a.ui-button:active,
.ui-button:active,
.ui-button.ui-state-active:hover {
border: 1px solid #003eff/*{borderColorActive}*/;
background: #007fff/*{bgColorActive}*/ /*{bgImgUrlActive}*/ /*{bgActiveXPos}*/ /*{bgActiveYPos}*/ /*{bgActiveRepeat}*/;
font-weight: normal/*{fwDefault}*/;
color: #ffffff/*{fcActive}*/;
}
.ui-icon-background,
.ui-state-active .ui-icon-background {
border: #003eff/*{borderColorActive}*/;
background-color: #ffffff/*{fcActive}*/;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
color: #ffffff/*{fcActive}*/;
text-decoration: none;
}

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