Compare commits
1091 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c341d340e6 | ||
|
|
777e3ec008 | ||
|
|
6df135d547 | ||
|
|
a5109781fa | ||
|
|
2d809b4f06 | ||
|
|
ced1883b6f | ||
|
|
2e54a9a267 | ||
|
|
57722f25fd | ||
|
|
539a7e280c | ||
|
|
db4179df19 | ||
|
|
7ea3eb1e3c | ||
|
|
559868fede | ||
|
|
d74236d9ac | ||
|
|
30bc3653d7 | ||
|
|
95e06746bd | ||
|
|
1e492624cc | ||
|
|
6a524f9f7c | ||
|
|
84cc65b008 | ||
|
|
b8b3a3c8ab | ||
|
|
db0e555eae | ||
|
|
58a9facc6d | ||
|
|
11dac19c8c | ||
|
|
2d82f0400d | ||
|
|
94a6b99c57 | ||
|
|
2aa5eccc7c | ||
|
|
52f5e5640e | ||
|
|
d6c15bb1cb | ||
|
|
4f698f73dc | ||
|
|
e413325337 | ||
|
|
608a45408a | ||
|
|
323baa2d65 | ||
|
|
6235d18dab | ||
|
|
3d0f480d9e | ||
|
|
642d31432a | ||
|
|
421be6835e | ||
|
|
d58720252e | ||
|
|
0461539f4b | ||
|
|
78749ad3b4 | ||
|
|
8b4828f236 | ||
|
|
e5614b92a0 | ||
|
|
f5b725e32b | ||
|
|
006de60f66 | ||
|
|
14f4530de0 | ||
|
|
2022fa54d3 | ||
|
|
73ef090b9e | ||
|
|
6d7054c18c | ||
|
|
4549d826dd | ||
|
|
7eacb1dc3c | ||
|
|
5264dd111d | ||
|
|
a4f0b6a97e | ||
|
|
46af5e8ebf | ||
|
|
35fb15fe9f | ||
|
|
662259468d | ||
|
|
1e1b9a6919 | ||
|
|
5fa10dfdf1 | ||
|
|
a5556a0c63 | ||
|
|
0cefcf0e16 | ||
|
|
1959d4d8df | ||
|
|
1d445d9eb6 | ||
|
|
6cfcbccabc | ||
|
|
de39623378 | ||
|
|
96d0d4943b | ||
|
|
bfab6f42d0 | ||
|
|
b38fc96623 | ||
|
|
b7bade20ea | ||
|
|
283ab80fc7 | ||
|
|
d705287510 | ||
|
|
47d17af593 | ||
|
|
496a74f7a3 | ||
|
|
7eded6e126 | ||
|
|
4c9d3222d7 | ||
|
|
5a2b539003 | ||
|
|
a9252701c5 | ||
|
|
2fbbbe7260 | ||
|
|
1d1f5835da | ||
|
|
a04b72e866 | ||
|
|
3f4f63dc33 | ||
|
|
1763a2efff | ||
|
|
00906851b5 | ||
|
|
844a805de6 | ||
|
|
718e0b5c1e | ||
|
|
ad20fd1c64 | ||
|
|
0eb5c0a08a | ||
|
|
7c3a08057d | ||
|
|
c3a43fe677 | ||
|
|
1c613d0a52 | ||
|
|
303ec5be11 | ||
|
|
0e33da7760 | ||
|
|
64c752bea3 | ||
|
|
cb2938b5c2 | ||
|
|
1b8a26d36b | ||
|
|
afdcc88342 | ||
|
|
7238f404f7 | ||
|
|
1b8948a086 | ||
|
|
95ab9bda2d | ||
|
|
ed7f89cfe1 | ||
|
|
8d695d03df | ||
|
|
ad880b5b97 | ||
|
|
c0f3f31095 | ||
|
|
e2ac6a0b17 | ||
|
|
0dba83d8e9 | ||
|
|
245dd47082 | ||
|
|
dc40a05ebd | ||
|
|
0616506b31 | ||
|
|
196ca4f1cb | ||
|
|
2d9677eefa | ||
|
|
cd297150ea | ||
|
|
665f0d0ca7 | ||
|
|
bd1097f7b5 | ||
|
|
f30bb89597 | ||
|
|
9f1f7b989c | ||
|
|
aa8549b633 | ||
|
|
222056f973 | ||
|
|
e431a1ad9e | ||
|
|
452cc46d67 | ||
|
|
c956d075d9 | ||
|
|
cdf5a545eb | ||
|
|
a60df65062 | ||
|
|
f8cb8900d6 | ||
|
|
b46fdcefad | ||
|
|
3f827b3104 | ||
|
|
88bf534573 | ||
|
|
a93e78bd34 | ||
|
|
4eb1fffeb2 | ||
|
|
889e3c0d4a | ||
|
|
184026f5f1 | ||
|
|
3c14fee36c | ||
|
|
5fb326db4d | ||
|
|
d5f8655827 | ||
|
|
3e6100f368 | ||
|
|
3c71640547 | ||
|
|
913839727c | ||
|
|
9e2a295182 | ||
|
|
27c60d9fc4 | ||
|
|
f409ce5364 | ||
|
|
68cd879d1e | ||
|
|
c8098b2e00 | ||
|
|
100f8a5165 | ||
|
|
76899d2fdb | ||
|
|
41e8e196ec | ||
|
|
ef5977bcfe | ||
|
|
e94d47bf0f | ||
|
|
f8bbb3065c | ||
|
|
bc908b4534 | ||
|
|
a179719c3e | ||
|
|
2ca3a3f7db | ||
|
|
f2a8cf0930 | ||
|
|
966701f94e | ||
|
|
1a183a4825 | ||
|
|
3a7e3b2168 | ||
|
|
eb2664ac79 | ||
|
|
31743cf962 | ||
|
|
2c2ba2e32c | ||
|
|
4ff2153e15 | ||
|
|
3c6b5c6c1e | ||
|
|
6c1d79fe5b | ||
|
|
e5f1df8c70 | ||
|
|
d6b59f6799 | ||
|
|
4b069edd33 | ||
|
|
a8ac6b2eda | ||
|
|
5c925f0ca5 | ||
|
|
30c7782d6e | ||
|
|
f7480902b4 | ||
|
|
00b5a65859 | ||
|
|
317ac59f47 | ||
|
|
b08c156552 | ||
|
|
2ad7629c2a | ||
|
|
42963ead5c | ||
|
|
c0168ecc5b | ||
|
|
34649d809e | ||
|
|
ee0f6b0ccf | ||
|
|
c8b47d1033 | ||
|
|
3d3ae97af4 | ||
|
|
d7066a2bbc | ||
|
|
7f8545f1dc | ||
|
|
85a63c4b78 | ||
|
|
1852d71974 | ||
|
|
7da7468965 | ||
|
|
83a894823f | ||
|
|
4cb0ac37a6 | ||
|
|
7a6fab8624 | ||
|
|
6416b67c63 | ||
|
|
1e269a6f82 | ||
|
|
8694a3bfe4 | ||
|
|
9e320674c1 | ||
|
|
9047e9e785 | ||
|
|
b8d8f0c16d | ||
|
|
8b8e9b001e | ||
|
|
9c40bd98aa | ||
|
|
1a0608f49e | ||
|
|
3543e0a1b5 | ||
|
|
97c23f75dd | ||
|
|
49bb9980b8 | ||
|
|
fd4cbb43c1 | ||
|
|
40242b7dcc | ||
|
|
9731023014 | ||
|
|
0abbba7a56 | ||
|
|
cc809183ae | ||
|
|
f7b3f4ed76 | ||
|
|
04fc954769 | ||
|
|
3621b8c27c | ||
|
|
358959522d | ||
|
|
19cd5962aa | ||
|
|
961336f35c | ||
|
|
573fa9f7f3 | ||
|
|
22ce6b047a | ||
|
|
82525f382d | ||
|
|
ec9dd08970 | ||
|
|
9ec0280334 | ||
|
|
0861dbd75d | ||
|
|
f5949b6b9a | ||
|
|
99905497e1 | ||
|
|
4ae18b73d1 | ||
|
|
1da26e041d | ||
|
|
4e42911f3d | ||
|
|
b0045a910c | ||
|
|
9491f07ade | ||
|
|
d994339de5 | ||
|
|
0464054a2b | ||
|
|
b062164277 | ||
|
|
ff9acab0fc | ||
|
|
76e09447c8 | ||
|
|
9470699798 | ||
|
|
89bfa82c61 | ||
|
|
859131308a | ||
|
|
56d02673ed | ||
|
|
b28e9c120c | ||
|
|
c858edf5c9 | ||
|
|
99ebd1d503 | ||
|
|
f63a2c8ef6 | ||
|
|
95796ab256 | ||
|
|
a1992494d3 | ||
|
|
5896ae26c6 | ||
|
|
8978c181ee | ||
|
|
ae975d07db | ||
|
|
6b49535b52 | ||
|
|
8b0962e192 | ||
|
|
dce0578a2b | ||
|
|
a483184372 | ||
|
|
6e6c60b29a | ||
|
|
21dbfdb4f9 | ||
|
|
73ab26c7b7 | ||
|
|
3e61b60c4e | ||
|
|
bd2fe32d5a | ||
|
|
c8854b0775 | ||
|
|
042c119058 | ||
|
|
b92488e32f | ||
|
|
2bc76e7adc | ||
|
|
d4278996ae | ||
|
|
eb63dae405 | ||
|
|
1b35356f81 | ||
|
|
36c08b0dea | ||
|
|
1d32e4ecee | ||
|
|
40061e43bb | ||
|
|
7ed1998d04 | ||
|
|
fd2490c135 | ||
|
|
9ab098af86 | ||
|
|
64893e0a95 | ||
|
|
6fdc304313 | ||
|
|
fb265ed732 | ||
|
|
e2be5b401c | ||
|
|
8539938b8a | ||
|
|
bbf25823b8 | ||
|
|
6b066046c6 | ||
|
|
9226daa891 | ||
|
|
843cd8fb67 | ||
|
|
6d7d441d42 | ||
|
|
1c4da30b61 | ||
|
|
84e25851d3 | ||
|
|
3a52daedab | ||
|
|
b7cf9cd776 | ||
|
|
ccb184b17f | ||
|
|
162ff07bd5 | ||
|
|
7326a452c3 | ||
|
|
9453947309 | ||
|
|
d25653b6ed | ||
|
|
ea415e33f5 | ||
|
|
b6430d5622 | ||
|
|
6fcd50ecdf | ||
|
|
6857d92c4f | ||
|
|
4bb9edb2b5 | ||
|
|
dd881bb856 | ||
|
|
204e0bc179 | ||
|
|
d026460106 | ||
|
|
934a7855b9 | ||
|
|
54d7c4490f | ||
|
|
fa6af3d148 | ||
|
|
063753b569 | ||
|
|
baff2d78d9 | ||
|
|
8f832856a5 | ||
|
|
f2eec4f315 | ||
|
|
2536105ad0 | ||
|
|
98a3fc99a2 | ||
|
|
56524a9773 | ||
|
|
fae525c82f | ||
|
|
5023fa6189 | ||
|
|
f743503705 | ||
|
|
f6a82ba119 | ||
|
|
305d3ba6fe | ||
|
|
7873bd812e | ||
|
|
b31ded22b9 | ||
|
|
f80fe0e4d0 | ||
|
|
c063aed867 | ||
|
|
b2d3272151 | ||
|
|
e9986c92d6 | ||
|
|
3a7dba437c | ||
|
|
cefd747e91 | ||
|
|
21f785638c | ||
|
|
df62c8a428 | ||
|
|
37e02365c7 | ||
|
|
7352c9a394 | ||
|
|
5b8510be56 | ||
|
|
6d5244aa5d | ||
|
|
f1f60042d8 | ||
|
|
6558bcae1d | ||
|
|
828b9c8b4a | ||
|
|
7d052eba00 | ||
|
|
43a31efeac | ||
|
|
f12f1ae60d | ||
|
|
9dc94084c1 | ||
|
|
03d5d3c70c | ||
|
|
27cea996c0 | ||
|
|
7661d43327 | ||
|
|
a1f90ffaba | ||
|
|
2583fdf20e | ||
|
|
9ddb87a6fd | ||
|
|
cc6031bfde | ||
|
|
6d196879b4 | ||
|
|
3d1fba650b | ||
|
|
0d5e0b3e80 | ||
|
|
7460647d58 | ||
|
|
53e01177d9 | ||
|
|
6709ef4c4c | ||
|
|
0b519e9d08 | ||
|
|
7f20cb045c | ||
|
|
286b63e2c8 | ||
|
|
c1eb5ba90a | ||
|
|
4859ac8ecb | ||
|
|
df6bd30d03 | ||
|
|
70fd0ae269 | ||
|
|
dd83872339 | ||
|
|
f7aa3c1e13 | ||
|
|
a9da5f8476 | ||
|
|
06f3a1ecd4 | ||
|
|
bed3ade221 | ||
|
|
f2b32b211b | ||
|
|
f81f9ba492 | ||
|
|
f10aad927f | ||
|
|
4fcd97b142 | ||
|
|
dad1b8b772 | ||
|
|
22af82cb15 | ||
|
|
e4b176a4d5 | ||
|
|
bb94b11e37 | ||
|
|
67d23ec6b2 | ||
|
|
ff7a402bd4 | ||
|
|
305a2fd4f6 | ||
|
|
09c9a95fcf | ||
|
|
08a144ac31 | ||
|
|
7ddcde62dd | ||
|
|
b6462b406d | ||
|
|
0330f45c6a | ||
|
|
8664d94ea6 | ||
|
|
af9dea9226 | ||
|
|
2ce6e815ff | ||
|
|
cdad5913ca | ||
|
|
68bb01d064 | ||
|
|
f4dbdbb427 | ||
|
|
d84beb957c | ||
|
|
7029bde555 | ||
|
|
7427187647 | ||
|
|
f8a5ff7315 | ||
|
|
87d07ffaaf | ||
|
|
954ba7c937 | ||
|
|
16e8e8a5e5 | ||
|
|
75e62b800c | ||
|
|
e1c9af795c | ||
|
|
9a2fd9ba91 | ||
|
|
c0378b4f51 | ||
|
|
0a6ea6be42 | ||
|
|
ec0eca39ff | ||
|
|
3e9371bd6a | ||
|
|
db1344573f | ||
|
|
c34e9f576d | ||
|
|
0b7d551048 | ||
|
|
30ac5e4c24 | ||
|
|
70f7f35009 | ||
|
|
5583caa9e4 | ||
|
|
d3099aca39 | ||
|
|
765ea63aed | ||
|
|
adf8c09fd4 | ||
|
|
5a90b36734 | ||
|
|
2e7bfc822c | ||
|
|
607d09aefb | ||
|
|
9eb25c412e | ||
|
|
8d41fa20bd | ||
|
|
a034b3519e | ||
|
|
ebf0b4de51 | ||
|
|
c9ccdb5f77 | ||
|
|
88b4d3842f | ||
|
|
3cd7e01a2c | ||
|
|
bd39666759 | ||
|
|
67d5cba09f | ||
|
|
8d72d114c7 | ||
|
|
e2324b7d51 | ||
|
|
1eae149c75 | ||
|
|
2a986517f7 | ||
|
|
bf47bb1840 | ||
|
|
cb6924e6d6 | ||
|
|
1de6b5d385 | ||
|
|
4c73fc825c | ||
|
|
d0a0786cce | ||
|
|
b7390ece39 | ||
|
|
1aae4f4d0f | ||
|
|
244169d6d5 | ||
|
|
a5fdf81df5 | ||
|
|
c1a54abc58 | ||
|
|
7e649dff3b | ||
|
|
40eda01c22 | ||
|
|
74b3917e69 | ||
|
|
6ada0b649c | ||
|
|
e21fc656e4 | ||
|
|
f4f5c8fa88 | ||
|
|
64d1aecf16 | ||
|
|
26c2d5aa81 | ||
|
|
78d8212e57 | ||
|
|
7a3388646b | ||
|
|
c3fe8d1c39 | ||
|
|
fcd9974a7c | ||
|
|
53dc322fd1 | ||
|
|
4cab1d323c | ||
|
|
686d55de6e | ||
|
|
0d2d40791a | ||
|
|
4302ecd6ea | ||
|
|
fc12056ba2 | ||
|
|
72edce86ba | ||
|
|
288fc5dd08 | ||
|
|
7503899a9b | ||
|
|
49a4d1069a | ||
|
|
f916e0ce25 | ||
|
|
bd4efad22b | ||
|
|
66865b553e | ||
|
|
62bfe0af2b | ||
|
|
db8714a87b | ||
|
|
5868741fe3 | ||
|
|
08e284c51b | ||
|
|
1757ffd8ee | ||
|
|
2d3c46957f | ||
|
|
ae0a6ebfb5 | ||
|
|
aba06ba3f0 | ||
|
|
b012332d82 | ||
|
|
4a94525320 | ||
|
|
1444593bbd | ||
|
|
82e1d6b917 | ||
|
|
61c0cbc887 | ||
|
|
8c39b4197e | ||
|
|
e24c647b18 | ||
|
|
6db21d7b34 | ||
|
|
7ae55c1980 | ||
|
|
bd167b6088 | ||
|
|
b9b7bf845c | ||
|
|
fc5ebb619e | ||
|
|
abd31e3785 | ||
|
|
8a8f5cb6ce | ||
|
|
2ad8f8a4bb | ||
|
|
4533c994a3 | ||
|
|
6ed7ee76e7 | ||
|
|
167a826f50 | ||
|
|
032db11b48 | ||
|
|
996f05c73a | ||
|
|
0ce02b4f06 | ||
|
|
fb31319fcf | ||
|
|
46bf01d93e | ||
|
|
c2ec15aeb3 | ||
|
|
bb97611349 | ||
|
|
825e492891 | ||
|
|
0aa6baea2f | ||
|
|
cafd84cee4 | ||
|
|
0751c14907 | ||
|
|
de0a9f8303 | ||
|
|
145116f657 | ||
|
|
6a8c820e09 | ||
|
|
2b033710e2 | ||
|
|
23f484a14d | ||
|
|
4cfa7347e9 | ||
|
|
aa1ea98579 | ||
|
|
38249e6b5e | ||
|
|
c02a882eff | ||
|
|
9ef32e75c5 | ||
|
|
14b3de4300 | ||
|
|
02d54fd208 | ||
|
|
d1e9e3e43f | ||
|
|
f279302009 | ||
|
|
e2a742b5c1 | ||
|
|
217960de60 | ||
|
|
ed528eb19a | ||
|
|
b69ea0648c | ||
|
|
193d193352 | ||
|
|
8e801c6829 | ||
|
|
e7da2b408a | ||
|
|
14255d71f7 | ||
|
|
a258516322 | ||
|
|
d25bcfe29c | ||
|
|
8464cd925e | ||
|
|
c2c4f087e4 | ||
|
|
064d083db6 | ||
|
|
ede21fe46a | ||
|
|
5c79e68c77 | ||
|
|
e6d01f2a23 | ||
|
|
af4101c9fe | ||
|
|
b9ee811cbf | ||
|
|
7a1dada9dc | ||
|
|
1de76ba266 | ||
|
|
ec612d1046 | ||
|
|
15c3acfa0b | ||
|
|
6aac85bddb | ||
|
|
d8ae7635d1 | ||
|
|
fbb3dd89cf | ||
|
|
8d1e24ae90 | ||
|
|
367a901207 | ||
|
|
9597015413 | ||
|
|
08696651fd | ||
|
|
661c3897b0 | ||
|
|
4200208c7b | ||
|
|
aba7e05ee2 | ||
|
|
ed4b82f4b1 | ||
|
|
2d4cfbe821 | ||
|
|
34a1e07824 | ||
|
|
bb4233ddfe | ||
|
|
2449a1e8bf | ||
|
|
dc9bc8a8ca | ||
|
|
0914ebf737 | ||
|
|
dc162bf385 | ||
|
|
86add9b9f7 | ||
|
|
476e6ca150 | ||
|
|
6f8c0e3cb6 | ||
|
|
6bd2390acf | ||
|
|
599b18ac99 | ||
|
|
130e6ac057 | ||
|
|
ecea92942c | ||
|
|
12e348544d | ||
|
|
4b8f743fa6 | ||
|
|
aee063bb3c | ||
|
|
93a23f3fa3 | ||
|
|
7907b43ef7 | ||
|
|
c843dba29a | ||
|
|
5864c7e127 | ||
|
|
f57b314466 | ||
|
|
f2dd2331a5 | ||
|
|
164af7ae02 | ||
|
|
b97d9f692d | ||
|
|
32c1e3258e | ||
|
|
19e4ebf88c | ||
|
|
ea39d5c74d | ||
|
|
682681afa5 | ||
|
|
091740f407 | ||
|
|
e22830790b | ||
|
|
e566305426 | ||
|
|
8530a03449 | ||
|
|
1a24aabb6f | ||
|
|
381f46cdd8 | ||
|
|
470a6b30e0 | ||
|
|
ea4e930cbb | ||
|
|
a75bdc52f6 | ||
|
|
21d8e8bbb8 | ||
|
|
57b4473bc8 | ||
|
|
ae6beff5d8 | ||
|
|
da5563960b | ||
|
|
3b6df1cb84 | ||
|
|
91736e89cc | ||
|
|
e2a4998341 | ||
|
|
3c511df7f7 | ||
|
|
e61ffe4db4 | ||
|
|
9da46bbdcd | ||
|
|
e3749c8e8f | ||
|
|
4e1ffedc20 | ||
|
|
35f864256f | ||
|
|
ad2698dae2 | ||
|
|
46a7b13293 | ||
|
|
cbd7124cec | ||
|
|
0debc70ead | ||
|
|
f8331b74c7 | ||
|
|
12c4205785 | ||
|
|
8f5c4dc4ec | ||
|
|
711139ad4c | ||
|
|
cfe0724620 | ||
|
|
aeef1dae2e | ||
|
|
0f3fa8fdbe | ||
|
|
7b25e9acaf | ||
|
|
be00a519b3 | ||
|
|
4254c2590a | ||
|
|
57254bf175 | ||
|
|
fe8fecafeb | ||
|
|
0bc9e74853 | ||
|
|
1dfceeea99 | ||
|
|
68d703d1e3 | ||
|
|
82c713031e | ||
|
|
5579c3d04f | ||
|
|
5e7b0c7826 | ||
|
|
9e1e1a7a96 | ||
|
|
7becddc3a4 | ||
|
|
dfe949264f | ||
|
|
163fb1be4a | ||
|
|
33f76f426c | ||
|
|
1ca1b00d11 | ||
|
|
c89750fe94 | ||
|
|
fb2ce0d30a | ||
|
|
b8c5323ece | ||
|
|
7b13ea860a | ||
|
|
4b1cfa5840 | ||
|
|
6eeb01ad48 | ||
|
|
e6e6a1eeea | ||
|
|
dd64eb98f9 | ||
|
|
9cd111bae5 | ||
|
|
137dcbf7e1 | ||
|
|
fbdc668a22 | ||
|
|
7f931cbfd3 | ||
|
|
7b7a962e04 | ||
|
|
e4610af235 | ||
|
|
e7458e76e1 | ||
|
|
04a43378d8 | ||
|
|
809acb47bf | ||
|
|
ace2788b57 | ||
|
|
d60e738bd2 | ||
|
|
a50a59bbca | ||
|
|
76a6ea57a1 | ||
|
|
9907694ef6 | ||
|
|
20b3c31cbd | ||
|
|
25a74ffeee | ||
|
|
b8273c4441 | ||
|
|
8d3a305ec3 | ||
|
|
72b73bc9a6 | ||
|
|
ec31d206e2 | ||
|
|
bf11de82fc | ||
|
|
34d2228d5d | ||
|
|
0e507804b1 | ||
|
|
6c7477f1a9 | ||
|
|
886fc8d74e | ||
|
|
7b218c33f9 | ||
|
|
c67617262f | ||
|
|
057c824e9f | ||
|
|
1b4f4bba22 | ||
|
|
847798eec1 | ||
|
|
52c7437076 | ||
|
|
465d7166d4 | ||
|
|
733aa58de8 | ||
|
|
9620627ea6 | ||
|
|
e06314e802 | ||
|
|
986d922855 | ||
|
|
75dcae6fbe | ||
|
|
375691365e | ||
|
|
5c5af05408 | ||
|
|
69cf7015d8 | ||
|
|
6a533579b7 | ||
|
|
3ff46e06c0 | ||
|
|
9e2fccb105 | ||
|
|
f2c6d8aee1 | ||
|
|
836a530b86 | ||
|
|
a31e027c54 | ||
|
|
a1b554b3ac | ||
|
|
79c4c237fa | ||
|
|
85a35a3063 | ||
|
|
e0e391efbe | ||
|
|
9eff89963c | ||
|
|
352fac26f3 | ||
|
|
8c16bf126a | ||
|
|
42de968f8f | ||
|
|
ed8f024e42 | ||
|
|
e534369c5b | ||
|
|
fed797df93 | ||
|
|
6a44dbfe83 | ||
|
|
28c5357f11 | ||
|
|
a8824f92a1 | ||
|
|
e57b69973f | ||
|
|
d6798eed1e | ||
|
|
50383673ca | ||
|
|
022dbe32e5 | ||
|
|
baac4eb698 | ||
|
|
8aa8f452d5 | ||
|
|
54255cf923 | ||
|
|
bd3d82d224 | ||
|
|
b14ad9f3ab | ||
|
|
261b4646e4 | ||
|
|
d62e1c4468 | ||
|
|
5b0266c462 | ||
|
|
06118dcabb | ||
|
|
984c852ab6 | ||
|
|
47414cfd0e | ||
|
|
fdf4fdfb6a | ||
|
|
5ee0396a02 | ||
|
|
724eb9c658 | ||
|
|
936ab05882 | ||
|
|
817a43b178 | ||
|
|
0f16f42887 | ||
|
|
641c199c8c | ||
|
|
04dd6b900f | ||
|
|
536a05210e | ||
|
|
60d16016e8 | ||
|
|
9222075c7a | ||
|
|
641dcdff85 | ||
|
|
668649be7a | ||
|
|
0de936d474 | ||
|
|
47bd8b0ce7 | ||
|
|
2287c9951c | ||
|
|
94c28ebbc5 | ||
|
|
b4739f8f03 | ||
|
|
c32cd36861 | ||
|
|
945562b34d | ||
|
|
af0f89774e | ||
|
|
c6c4098759 | ||
|
|
fa1c79e15c | ||
|
|
0705c26513 | ||
|
|
140fa4b7c0 | ||
|
|
cd88901868 | ||
|
|
8eec97deb5 | ||
|
|
4331a7c9f4 | ||
|
|
f4fcfeba9d | ||
|
|
48aefb1862 | ||
|
|
bda9584346 | ||
|
|
ed1154af67 | ||
|
|
c368eacd87 | ||
|
|
949c59230f | ||
|
|
1114538f41 | ||
|
|
b0f83291ee | ||
|
|
e91e4ff430 | ||
|
|
4ad290d4bd | ||
|
|
37c6e2c0af | ||
|
|
b4a45afce9 | ||
|
|
330ff6eb47 | ||
|
|
c681eb1bdb | ||
|
|
de533e7031 | ||
|
|
ae6ed3f23f | ||
|
|
32aa209b75 | ||
|
|
e8c25d8ac5 | ||
|
|
718ad4b398 | ||
|
|
8d81a394ee | ||
|
|
d661c8b54e | ||
|
|
e45e782e7d | ||
|
|
5aa422dde1 | ||
|
|
24cbe3d281 | ||
|
|
13f5e76015 | ||
|
|
35c65ba890 | ||
|
|
8f2b82b832 | ||
|
|
7f1a206a52 | ||
|
|
8c503a226e | ||
|
|
cdcf9eaf67 | ||
|
|
a651e55dc9 | ||
|
|
d041a3f4f9 | ||
|
|
3ea05cb705 | ||
|
|
3f27ab1e44 | ||
|
|
e99b170d0a | ||
|
|
49e1a1c410 | ||
|
|
93ab3d5666 | ||
|
|
94f8159fdc | ||
|
|
8fb0b1445a | ||
|
|
15673555a2 | ||
|
|
ceeec7b8a6 | ||
|
|
46e5650882 | ||
|
|
405ea0076b | ||
|
|
8cd7f7de3f | ||
|
|
7cd17154fc | ||
|
|
299fcb091a | ||
|
|
025c6b0e7b | ||
|
|
388c3ec188 | ||
|
|
b6d9bb0d2a | ||
|
|
561ec3b1e9 | ||
|
|
9611646bc6 | ||
|
|
b5972bb680 | ||
|
|
455861ec69 | ||
|
|
5affa0de82 | ||
|
|
e11a2a6089 | ||
|
|
614a50ce0b | ||
|
|
66594b286a | ||
|
|
8e06f94bdd | ||
|
|
b0e03bb0b8 | ||
|
|
22e30b1871 | ||
|
|
8104a67e3e | ||
|
|
654ffda454 | ||
|
|
3f51437569 | ||
|
|
1bd784eef3 | ||
|
|
0d11aded40 | ||
|
|
00925d38aa | ||
|
|
12803b3957 | ||
|
|
7583498735 | ||
|
|
53eda31179 | ||
|
|
56e276939b | ||
|
|
39abccb35d | ||
|
|
dae8866053 | ||
|
|
a0e8aa3a43 | ||
|
|
e68b7da1f8 | ||
|
|
fcce7a531a | ||
|
|
f23dcbec17 | ||
|
|
8a1d28563d | ||
|
|
c33a87a07c | ||
|
|
b833e1ce27 | ||
|
|
6de1a55d73 | ||
|
|
cbf9086e10 | ||
|
|
eb4aa23577 | ||
|
|
bf9ae44775 | ||
|
|
f0822fa2b1 | ||
|
|
f966d34929 | ||
|
|
d51136f8f4 | ||
|
|
19843760e5 | ||
|
|
3457f93ed0 | ||
|
|
625ac2f1d7 | ||
|
|
061581c4ff | ||
|
|
a0116422c6 | ||
|
|
94e49ea86f | ||
|
|
040409eac7 | ||
|
|
baa2fca7ca | ||
|
|
1fd8139214 | ||
|
|
db5f9f639e | ||
|
|
90980f40f5 | ||
|
|
94d68c8b64 | ||
|
|
834a0a873f | ||
|
|
d8621f0b7a | ||
|
|
994c671fd8 | ||
|
|
7b95750dcf | ||
|
|
9fbbaf9b88 | ||
|
|
bb8572a364 | ||
|
|
bdd05a275d | ||
|
|
dcdbf23b6d | ||
|
|
ce6ad0bbb8 | ||
|
|
59d2946b91 | ||
|
|
92f3f4ee57 | ||
|
|
3311f17bbb | ||
|
|
e1f862507e | ||
|
|
7e040ac216 | ||
|
|
515c19422d | ||
|
|
5702d7afa8 | ||
|
|
833968a87d | ||
|
|
0c0e9c2f71 | ||
|
|
bcf1ebd778 | ||
|
|
4c82f9767f | ||
|
|
ab405f671e | ||
|
|
325d204332 | ||
|
|
97feed0be6 | ||
|
|
19636582ab | ||
|
|
393abe9e62 | ||
|
|
d9dca96a32 | ||
|
|
ebff4a765a | ||
|
|
d18019159b | ||
|
|
46905e9504 | ||
|
|
8b1c15ccb6 | ||
|
|
8873947a1b | ||
|
|
cdef22b4d1 | ||
|
|
29209105d3 | ||
|
|
fea3495098 | ||
|
|
871b2e52c7 | ||
|
|
ec174a38a9 | ||
|
|
46ff78a4c4 | ||
|
|
d2ad87d09c | ||
|
|
1903d6fb77 | ||
|
|
bbe1f9beda | ||
|
|
1f5710eaba | ||
|
|
392f8239f5 | ||
|
|
5c227888f2 | ||
|
|
e29e183e84 | ||
|
|
f31118292c | ||
|
|
5f4a5364b0 | ||
|
|
09da7cd242 | ||
|
|
e619b5fd31 | ||
|
|
b1855447b8 | ||
|
|
98f386dad6 | ||
|
|
80af3fb010 | ||
|
|
6e224c63ef | ||
|
|
aa113cc300 | ||
|
|
65df1ff909 | ||
|
|
e92c2d168e | ||
|
|
455d772095 | ||
|
|
e80973c4d2 | ||
|
|
721ecdbf3d | ||
|
|
e6b3d4349e | ||
|
|
b865add1b3 | ||
|
|
b35022d161 | ||
|
|
62b091c77b | ||
|
|
cdb112df05 | ||
|
|
6dc4fa0811 | ||
|
|
fb17af1afd | ||
|
|
b8811924c3 | ||
|
|
1b0b45649d | ||
|
|
e49bd05ae6 | ||
|
|
32f0d80729 | ||
|
|
f0f757cf75 | ||
|
|
ccf4e32523 | ||
|
|
56fe14c451 | ||
|
|
28801bf1ea | ||
|
|
460391136f | ||
|
|
215d4fc8bf | ||
|
|
46cb021260 | ||
|
|
f9c8028ea1 | ||
|
|
32b36f42f0 | ||
|
|
48f3ad842d | ||
|
|
3251b26d9e | ||
|
|
136df3724f | ||
|
|
0d6b8486c4 | ||
|
|
3a4aa5a001 | ||
|
|
5bea9915f5 | ||
|
|
26de367bc5 | ||
|
|
3c361c9414 | ||
|
|
0d4fdc38e4 | ||
|
|
6f7035e4cf | ||
|
|
38ae013e03 | ||
|
|
13d9f8f3f7 | ||
|
|
e990ae4800 | ||
|
|
6a68dc6e4f | ||
|
|
cfc19dca25 | ||
|
|
50318a8fc5 | ||
|
|
bde791fa55 | ||
|
|
2e6f3ed864 | ||
|
|
fb6bf4319f | ||
|
|
19bfff9899 | ||
|
|
f4d53e9006 | ||
|
|
46655ad462 | ||
|
|
1b6b957b23 | ||
|
|
0f6334314e | ||
|
|
3ac72a5c89 | ||
|
|
519be2d5a2 | ||
|
|
e8bc45f7dd | ||
|
|
76c3c5f2f6 | ||
|
|
54cd30c564 | ||
|
|
e260dd84ad | ||
|
|
bce85b04d6 | ||
|
|
642db480ce | ||
|
|
bea4be881b | ||
|
|
86832fb703 | ||
|
|
03aa4c9ddc | ||
|
|
36a158a805 | ||
|
|
32fbebd469 | ||
|
|
3d4b0c3fd6 | ||
|
|
1f7ec6769a | ||
|
|
239be527b8 | ||
|
|
fd4a87e3a6 | ||
|
|
3f535cc506 | ||
|
|
0b57bd51ca | ||
|
|
49c78ddd9d | ||
|
|
47b7046743 | ||
|
|
b4f1cae323 | ||
|
|
17856e0b96 | ||
|
|
8005679e60 | ||
|
|
ef33233624 | ||
|
|
2c6f57c7ce | ||
|
|
fd60d5bb17 | ||
|
|
742e6eb3a3 | ||
|
|
331ea586f1 | ||
|
|
afb0eabaa4 | ||
|
|
afa0aa5b3a | ||
|
|
1f99f69ecd | ||
|
|
d00596d297 | ||
|
|
8dcc18dde3 | ||
|
|
616d514d5a | ||
|
|
b2587c71b8 | ||
|
|
816866549c | ||
|
|
062735c9e3 | ||
|
|
aae9183de6 | ||
|
|
7f3e2d453f | ||
|
|
1b42aed34c | ||
|
|
974806d145 | ||
|
|
fe4c3c10d6 | ||
|
|
67dab35b57 | ||
|
|
55d8bc9ba5 | ||
|
|
572f01ed74 | ||
|
|
ee32d6f926 | ||
|
|
89abaa9dda | ||
|
|
a9ece209ae | ||
|
|
47ac50b4d0 | ||
|
|
5539f68a54 | ||
|
|
5b1ec1eb8f | ||
|
|
a609cbf84a | ||
|
|
1ad440f488 | ||
|
|
64330e6cc4 | ||
|
|
03421c099a | ||
|
|
b5c4084452 | ||
|
|
5d5b971f57 | ||
|
|
825d31af20 | ||
|
|
7880f980a8 | ||
|
|
137fd69f47 | ||
|
|
ed8efcf139 | ||
|
|
7fa3b67cc4 | ||
|
|
93c500fbf2 | ||
|
|
c58c12ea3d | ||
|
|
35305ad107 | ||
|
|
589d750e59 | ||
|
|
43b488f21f | ||
|
|
54d06462fc | ||
|
|
2d6109d587 | ||
|
|
8fddf627bf | ||
|
|
0334f6ce86 | ||
|
|
025558e58e | ||
|
|
f74547008d | ||
|
|
67dc1c7640 | ||
|
|
40322c6de4 | ||
|
|
417675b3df | ||
|
|
321ac74b6a | ||
|
|
b1c06eed7b | ||
|
|
8cb52e22b1 | ||
|
|
a073064900 | ||
|
|
07cceca67e | ||
|
|
540318fcd0 | ||
|
|
e9cbb872d0 | ||
|
|
f9dc42d1f9 | ||
|
|
1dabeedaa0 | ||
|
|
dc70ff128c | ||
|
|
b012b7170b | ||
|
|
0a1c2d8440 | ||
|
|
d72596f000 | ||
|
|
33029b2a11 | ||
|
|
bae24dd0ec | ||
|
|
db7de3aac1 | ||
|
|
c66dfbc653 | ||
|
|
51c207e025 | ||
|
|
e44e2442af | ||
|
|
86f40083ef | ||
|
|
f45fbbce60 | ||
|
|
c648866cb4 | ||
|
|
f8adafdabe | ||
|
|
ab4a1a0a1d | ||
|
|
34a3f4e275 | ||
|
|
55aedcd4a8 | ||
|
|
6633c550f5 | ||
|
|
0165658659 | ||
|
|
3075276a52 | ||
|
|
b46e752d0a | ||
|
|
ad171e7706 | ||
|
|
92c665436a | ||
|
|
2dbf433474 | ||
|
|
56ac642446 | ||
|
|
2f6c0d2208 | ||
|
|
a32614fff8 | ||
|
|
53053c127e | ||
|
|
a89ef41e81 | ||
|
|
eee7cb9bf4 | ||
|
|
98dc38ed99 | ||
|
|
7bcd6a6382 | ||
|
|
73e0a75585 | ||
|
|
71aa899f77 | ||
|
|
53f82a808b | ||
|
|
b035fc9ebd | ||
|
|
8cae5e8b3f | ||
|
|
6163f33abf | ||
|
|
a636faa4ab | ||
|
|
ee4df36735 | ||
|
|
4ede5463f9 | ||
|
|
4c277fe1b5 | ||
|
|
9f6079ad8a | ||
|
|
6fb6a16246 | ||
|
|
3af8070a57 | ||
|
|
ebe007eb78 | ||
|
|
55b7e192aa | ||
|
|
62f91f4d38 | ||
|
|
b1546c0568 | ||
|
|
af2d1c3a6e | ||
|
|
ca893b303a | ||
|
|
142369d651 | ||
|
|
3241c73e7c | ||
|
|
d6d9abd05e | ||
|
|
85ff2ff9de | ||
|
|
dbbb434723 | ||
|
|
7804ef83ce | ||
|
|
fb2d898e4c | ||
|
|
c6e99b35d5 | ||
|
|
3dcd38eca7 | ||
|
|
39ea44fdc3 | ||
|
|
2f84f3751a | ||
|
|
a7780dac9e | ||
|
|
e549a0824f | ||
|
|
8b8583d48b | ||
|
|
a8a8424ced | ||
|
|
0996eca3c5 | ||
|
|
e6f8bc8ae2 | ||
|
|
5ccc1c991b | ||
|
|
20a2d94996 | ||
|
|
f41eea2768 | ||
|
|
2cf7d976d3 | ||
|
|
194a1f204c | ||
|
|
75246e9db2 | ||
|
|
2d2525cff2 | ||
|
|
5160b640c4 | ||
|
|
d18ab3fe75 | ||
|
|
d15cf72474 | ||
|
|
d8f7a5dfa4 | ||
|
|
e68bef7ea7 | ||
|
|
7a0d690101 | ||
|
|
0eb8ae5117 | ||
|
|
b05d416e80 | ||
|
|
497a8f7a11 | ||
|
|
3bdf0dc6a6 | ||
|
|
b299302da4 | ||
|
|
2f0c64ae17 | ||
|
|
b58de9be9c | ||
|
|
d4a46cf257 | ||
|
|
1dd5f2b72f |
30
.codecov.yml
Normal file
30
.codecov.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
codecov:
|
||||
notify:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
precision: 2
|
||||
round: down
|
||||
range: "80...100"
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5%
|
||||
patch: off
|
||||
changes: no
|
||||
|
||||
parsers:
|
||||
gcov:
|
||||
branch_detection:
|
||||
conditional: yes
|
||||
loop: yes
|
||||
method: no
|
||||
macro: no
|
||||
|
||||
comment:
|
||||
layout: "diff, flags, files"
|
||||
behavior: default
|
||||
require_changes: yes
|
||||
require_base: no
|
||||
require_head: yes
|
||||
branches: null
|
||||
@@ -3,8 +3,17 @@ root = true
|
||||
|
||||
; Unix-style newlines
|
||||
[*]
|
||||
end_of_line = LF
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_size = 4
|
||||
|
||||
[*.yaml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
44
.env.dist
44
.env.dist
@@ -1,28 +1,16 @@
|
||||
# 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
|
||||
|
||||
# KIMAI DEFAULT ENV VARS
|
||||
DATABASE_PREFIX=kimai2_
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=c88c14fa70a424e7a12d459b9dd9df7f
|
||||
#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 ###
|
||||
# Configure your database connection and set the correct server version:
|
||||
# for MySQL "serverVersion=5.7" and for MariaDB "serverVersion=mariadb-10.5.8"
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=5.7
|
||||
# Email will be sent with this address as sender
|
||||
MAILER_FROM=kimai@example.com
|
||||
# Email connection (disabled by default) more info at https://www.kimai.org/documentation/emails.html
|
||||
MAILER_URL=null://null
|
||||
# do not change, unless you are developing for Kimai
|
||||
APP_ENV=prod
|
||||
# should be changed to a unique character sequence
|
||||
APP_SECRET=change_this_to_something_unique
|
||||
# unlikely, that you need to change this one
|
||||
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
||||
# Running behind reverse proxies? Use those:
|
||||
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||
# TRUSTED_HOSTS=localhost,example.com
|
||||
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [kevinpapst]
|
||||
custom: ["https://www.kimai.org/donate/", "https://www.paypal.me/kevinpapst"]
|
||||
22
.github/ISSUE_TEMPLATE.md
vendored
22
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
||||
## Description
|
||||
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
|
||||
## Possible Fix
|
||||
|
||||
|
||||
## Steps to Reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Your Environment
|
||||
* Version:
|
||||
* Browser Name and version:
|
||||
* Operating System and version (desktop or mobile):
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**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. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
87
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
87
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve Kimai
|
||||
labels: [ "bug" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for reporting an issue on Kimai! This form will guide you to create a useful issue report.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the issue
|
||||
description: A clear and concise description of the problem you are facing.
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: tried
|
||||
attributes:
|
||||
label: I already tried
|
||||
description: If you didn’t try already, try to search the documentation and existing issues what you wrote above.
|
||||
options:
|
||||
- label: I've read and searched [the documentation](https://www.kimai.org/documentation/).
|
||||
required: true
|
||||
- label: I've searched for similar issues in this repository.
|
||||
required: true
|
||||
- label: I've searched for similar issues in [the discussions](https://github.com/kimai/kimai/discussions).
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Kimai version
|
||||
description: Which version of Kimai are you running (check Doctor or About screen)
|
||||
placeholder: 1.xx.x
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: php
|
||||
attributes:
|
||||
label: Which PHP version are you using?
|
||||
options:
|
||||
- "7.3"
|
||||
- "7.4"
|
||||
- "8.0"
|
||||
- "8.1"
|
||||
- "8.2"
|
||||
- Other (please mention below)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: installation
|
||||
attributes:
|
||||
label: How do you run Kimai?
|
||||
options:
|
||||
- kimai.cloud service
|
||||
- Self-Hosted
|
||||
- Synology
|
||||
- Other (please mention below)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproducer
|
||||
attributes:
|
||||
label: Steps to reproduce the behavior
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Scroll down to '...'
|
||||
3. Click on '...'
|
||||
4. See error
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logfile
|
||||
description: Please paste the last lines from your logfile at "var/log/prod.log" or "Doctor > Logs", around the time when the problem happened.
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to better explain your problem.
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
value: |
|
||||
Device: [Ubuntu Laptop 16 inch, Windows Desktop 27 inch, iPhone 6s]
|
||||
Browser [e.g. Firefox 81, Chrome 85, Safari 14]
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Read our documentation
|
||||
url: https://www.kimai.org/documentation/
|
||||
about: Save your time! There is an instant solution for many issues in the documentation.
|
||||
- name: Docker image issue tracker
|
||||
url: https://github.com/tobybatch/kimai2/issues
|
||||
about: For issues specific for Kimai Docker deployment
|
||||
- name: Ask a Question
|
||||
url: https://github.com/kimai/kimai/discussions
|
||||
about: Want to discuss something with a community? Do it in discussions!
|
||||
- name: Get Professional support
|
||||
url: https://www.kimai.org/store/custom-plugins.html
|
||||
about: As a customer, you will always receive fast and helpful replies from the developer.
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
34
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
34
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea to make Kimai better
|
||||
labels: [ "feature request" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for reporting an issue on Kimai! This form will guide you to create a useful issue report.
|
||||
- type: textarea
|
||||
id: describe
|
||||
attributes:
|
||||
label: Describe the problem
|
||||
description: >
|
||||
Is your feature request related to a problem? If so, please provide
|
||||
a clear and concise description of what the issue is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what the new feature should do.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to better explain your problem.
|
||||
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,5 +1,5 @@
|
||||
## 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)
|
||||
@@ -7,8 +7,6 @@
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
|
||||
## Checklist
|
||||
- [ ] My code follows the code style
|
||||
- [ ] All files have a license header
|
||||
- [ ] All methods have a doc header with type declarations
|
||||
- [ ] I have updated the documentation accordingly
|
||||
- [ ] I have added tests to cover my changes
|
||||
- [ ] I verified that my code applies to the guidelines (`composer code-check`)
|
||||
- [ ] I updated the documentation (see [here](https://github.com/kimai/www.kimai.org/tree/master/_documentation))
|
||||
- [ ] I agree that this code is used in Kimai and will be published under the [MIT license](https://github.com/kimai/kimai/blob/master/LICENSE)
|
||||
|
||||
47
.github/release-drafter.yml
vendored
Normal file
47
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name-template: '$RESOLVED_VERSION'
|
||||
tag-template: '$RESOLVED_VERSION'
|
||||
categories:
|
||||
- title: 'Enhancements'
|
||||
labels:
|
||||
- 'feature request'
|
||||
- 'technical debt'
|
||||
- 'translation'
|
||||
- title: 'Fixed bugs'
|
||||
labels:
|
||||
- 'bug'
|
||||
- title: 'Infrastructure'
|
||||
labels:
|
||||
- 'infrastructure'
|
||||
exclude-labels:
|
||||
- 'duplicate'
|
||||
- 'invalid'
|
||||
- 'wontfix'
|
||||
- 'release'
|
||||
exclude-contributors:
|
||||
- 'dependabot'
|
||||
change-template: '- $TITLE (#$NUMBER)'
|
||||
change-title-escapes: '\<*_&`#@'
|
||||
version-resolver:
|
||||
minor:
|
||||
labels:
|
||||
- 'feature request'
|
||||
- 'release'
|
||||
patch:
|
||||
labels:
|
||||
- 'technical debt'
|
||||
- 'bug'
|
||||
- 'translation'
|
||||
default: patch
|
||||
template: |
|
||||
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://tobybatch.github.io/kimai2/)
|
||||
|
||||
**PHP Version compatibility:**
|
||||
- PHP 7.3 is [end-of-life](https://www.php.net/supported-versions.php)
|
||||
- PHP 7.4 is [end-of-life](https://www.php.net/supported-versions.php) in a few days!
|
||||
- PHP 8.0 and PHP 8.1 are supported
|
||||
|
||||
**Next release will be PHP 8 ONLY, as [announced one year ago](https://www.kimai.org/blog/2021/sunsetting-php-7/).**
|
||||
|
||||
$CHANGES
|
||||
|
||||
Involved in this release: $CONTRIBUTORS - thank you!
|
||||
69
.github/workflows/coverage.yaml
vendored
Normal file
69
.github/workflows/coverage.yaml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Coverage
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: false
|
||||
MYSQL_ROOT_PASSWORD: kimai
|
||||
MYSQL_DATABASE: kimai
|
||||
ports:
|
||||
- 3306/tcp
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.0']
|
||||
|
||||
name: Coverage (${{ matrix.php }})
|
||||
steps:
|
||||
|
||||
- name: Clone Kimai
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: pcov
|
||||
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
|
||||
|
||||
- name: Determine composer cache directory
|
||||
id: composer-cache
|
||||
run: "echo \"::set-output name=directory::$(composer config cache-dir)\""
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: "${{ steps.composer-cache.outputs.directory }}"
|
||||
key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install
|
||||
|
||||
- name: Install LDAP package
|
||||
run: composer require laminas/laminas-ldap
|
||||
|
||||
- name: Setup problem matchers for PHPUnit
|
||||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
|
||||
- name: Run tests
|
||||
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
|
||||
APP_ENV: dev
|
||||
MAILER_URL: null://localhost
|
||||
TEST_WITH_BUNDLES: 1
|
||||
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.xml
|
||||
fail_ci_if_error: true
|
||||
17
.github/workflows/docker.yaml
vendored
Normal file
17
.github/workflows/docker.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: 'Docker Build'
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Dispatch release event to tobybatch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Emit repository_dispatch
|
||||
uses: mvasigh/dispatch-action@1.1.6
|
||||
with:
|
||||
token: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
repo: kimai2
|
||||
owner: tobybatch
|
||||
event_type: kimai_release
|
||||
56
.github/workflows/lock.yaml
vendored
Normal file
56
.github/workflows/lock.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '180'
|
||||
exclude-issue-created-before: ''
|
||||
exclude-issue-created-after: ''
|
||||
exclude-issue-created-between: ''
|
||||
exclude-issue-closed-before: ''
|
||||
exclude-issue-closed-after: ''
|
||||
exclude-issue-closed-between: ''
|
||||
include-any-issue-labels: ''
|
||||
include-all-issue-labels: ''
|
||||
exclude-any-issue-labels: ''
|
||||
add-issue-labels: ''
|
||||
remove-issue-labels: ''
|
||||
issue-comment: >
|
||||
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.
|
||||
issue-lock-reason: 'resolved'
|
||||
pr-inactive-days: '180'
|
||||
exclude-pr-created-before: ''
|
||||
exclude-pr-created-after: ''
|
||||
exclude-pr-created-between: ''
|
||||
exclude-pr-closed-before: ''
|
||||
exclude-pr-closed-after: ''
|
||||
exclude-pr-closed-between: ''
|
||||
include-any-pr-labels: ''
|
||||
include-all-pr-labels: ''
|
||||
exclude-any-pr-labels: ''
|
||||
add-pr-labels: ''
|
||||
remove-pr-labels: ''
|
||||
pr-comment: ''
|
||||
pr-lock-reason: ''
|
||||
process-only: ''
|
||||
log-output: false
|
||||
21
.github/workflows/lockfiles.yaml
vendored
Normal file
21
.github/workflows/lockfiles.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Check .lock files
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
lockfiles:
|
||||
runs-on: ubuntu-latest
|
||||
name: Verify lock file integrity
|
||||
steps:
|
||||
|
||||
- name: Clone Kimai
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prevent file change
|
||||
uses: xalvarez/prevent-file-change-action@v1
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: .*.lock
|
||||
trustedAuthors: kevinpapst, dependabot
|
||||
30
.github/workflows/release-drafter.yaml
vendored
Normal file
30
.github/workflows/release-drafter.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
correct_repository:
|
||||
permissions:
|
||||
contents: none
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: fail on fork
|
||||
if: github.repository_owner != 'kimai'
|
||||
run: exit 1
|
||||
|
||||
update_release_draft:
|
||||
permissions:
|
||||
contents: write # for release-drafter/release-drafter to create a github release
|
||||
pull-requests: write # for release-drafter/release-drafter to add label to PR
|
||||
needs: correct_repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
101
.github/workflows/testing.yaml
vendored
Normal file
101
.github/workflows/testing.yaml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: false
|
||||
MYSQL_ROOT_PASSWORD: kimai
|
||||
MYSQL_DATABASE: kimai
|
||||
ports:
|
||||
- 3306/tcp
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.4', '8.0', '8.1']
|
||||
|
||||
name: Integration (${{ matrix.php }})
|
||||
steps:
|
||||
|
||||
- name: Clone Kimai
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: none
|
||||
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
|
||||
tools: cs2pr:1.1.0, symfony-cli
|
||||
|
||||
- name: Determine composer cache directory
|
||||
id: composer-cache
|
||||
run: "echo \"::set-output name=directory::$(composer config cache-dir)\""
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: "${{ steps.composer-cache.outputs.directory }}"
|
||||
key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install
|
||||
|
||||
- name: Validate Composer
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Warmup cache for PHPStan
|
||||
run: APP_ENV=dev bin/console kimai:reload -n
|
||||
|
||||
- name: Check codestyles
|
||||
run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none --format=checkstyle | cs2pr
|
||||
|
||||
- name: Run PHPStan on Codebase
|
||||
run: vendor/bin/phpstan analyse src -c phpstan.neon --level=5 --no-progress --error-format=checkstyle | cs2pr
|
||||
|
||||
- name: Run PHPStan on Tests
|
||||
run: vendor/bin/phpstan analyse tests -c tests/phpstan.neon --level=5 --no-progress --error-format=checkstyle | cs2pr
|
||||
|
||||
- name: Lint codebase
|
||||
run: composer linting
|
||||
|
||||
- name: Check for security issues in packages
|
||||
run: symfony security:check
|
||||
|
||||
- name: Install LDAP package
|
||||
run: composer require laminas/laminas-ldap
|
||||
|
||||
- name: Setup problem matchers for PHPUnit
|
||||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
|
||||
- name: Run unit tests
|
||||
run: composer kimai:tests-unit
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
|
||||
APP_ENV: dev
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Run integration tests
|
||||
run: composer kimai:tests-integration
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
|
||||
APP_ENV: dev
|
||||
MAILER_URL: null://localhost
|
||||
|
||||
- name: Run migrations on MySQL
|
||||
run: |
|
||||
bin/console doctrine:database:drop --if-exists --force -n
|
||||
bin/console doctrine:database:create --if-not-exists -n
|
||||
bin/console doctrine:migrations:migrate -n
|
||||
bin/console doctrine:migrations:migrate first -n
|
||||
env:
|
||||
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
|
||||
APP_ENV: dev
|
||||
MAILER_URL: null://localhost
|
||||
@@ -1,2 +1,5 @@
|
||||
unreleased=true
|
||||
future-release=0.2
|
||||
future-release=1.20
|
||||
exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback,documentation
|
||||
enhancement_labels=>feature request,translation,technical debt,documentation
|
||||
issues-wo-labels=false
|
||||
|
||||
51
.gitignore
vendored
51
.gitignore
vendored
@@ -1,22 +1,37 @@
|
||||
|
||||
# ========== KIMAI ==========
|
||||
# IDE
|
||||
# some hosters require a htaccess to change the PHP version
|
||||
.htaccess
|
||||
.env-*
|
||||
.idea/
|
||||
.DS_Store
|
||||
|
||||
# for symfony local webserver
|
||||
php.ini
|
||||
!.gitkeep
|
||||
|
||||
/bin/*
|
||||
!bin/console
|
||||
/config/packages/local.yaml
|
||||
!/bin/console
|
||||
*local.yaml
|
||||
/config/bundles-local.php
|
||||
|
||||
/var/data/kimai.sqlite
|
||||
/public/avatars/*.png
|
||||
|
||||
/templates/invoice/renderer/.~lock*
|
||||
/translations/branding.*.xlf
|
||||
|
||||
/var/data/*
|
||||
!/var/data/.gitkeep
|
||||
/var/coverage/
|
||||
/var/cache/*
|
||||
!var/cache/.gitkeep
|
||||
|
||||
/var/invoices/*
|
||||
/var/export/*
|
||||
/var/log/*
|
||||
!var/log/.gitkeep
|
||||
|
||||
/var/sessions/*
|
||||
!var/sessions/.gitkeep
|
||||
/var/packages/*
|
||||
/var/plugins/*
|
||||
*.disabled
|
||||
|
||||
# ========== KIMAI ==========
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
@@ -25,24 +40,24 @@
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/web-server-bundle ###
|
||||
.web-server-pid
|
||||
###< symfony/web-server-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> symfony/webpack-encore-pack ###
|
||||
/node_modules/
|
||||
###< symfony/webpack-encore-pack ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> symfony/webpack-encore-bundle ###
|
||||
/node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
###< symfony/webpack-encore-bundle ###
|
||||
/nbproject/*
|
||||
@@ -7,7 +7,8 @@ For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
COMMENT;
|
||||
|
||||
return PhpCsFixer\Config::create()
|
||||
$fixer = new PhpCsFixer\Config();
|
||||
$fixer
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'encoding' => true,
|
||||
@@ -19,7 +20,7 @@ return PhpCsFixer\Config::create()
|
||||
'function_declaration' => true,
|
||||
'indentation_type' => true,
|
||||
'line_ending' => true,
|
||||
'lowercase_constants' => true,
|
||||
'constant_case' => ['case' => 'lower'],
|
||||
'lowercase_keywords' => true,
|
||||
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||
'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'],
|
||||
@@ -46,7 +47,7 @@ return PhpCsFixer\Config::create()
|
||||
'statements' => ['return'],
|
||||
],
|
||||
'cast_spaces' => true,
|
||||
'class_attributes_separation' => ['elements' => ['method']],
|
||||
'class_attributes_separation' => ['elements' => ['method' => 'one']],
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'declare_equal_normalize' => true,
|
||||
'function_typehint_space' => true,
|
||||
@@ -100,10 +101,10 @@ return PhpCsFixer\Config::create()
|
||||
],
|
||||
'phpdoc_annotation_without_dot' => true,
|
||||
'phpdoc_indent' => true,
|
||||
'phpdoc_inline_tag' => true,
|
||||
'phpdoc_inline_tag_normalizer' => true,
|
||||
'phpdoc_no_access' => true,
|
||||
'phpdoc_no_alias_tag' => true,
|
||||
'phpdoc_no_empty_return' => true,
|
||||
'phpdoc_no_empty_return' => false,
|
||||
'phpdoc_no_package' => true,
|
||||
'phpdoc_no_useless_inheritdoc' => true,
|
||||
'phpdoc_return_self_reference' => true,
|
||||
@@ -130,7 +131,7 @@ return PhpCsFixer\Config::create()
|
||||
'standardize_increment' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'ternary_operator_spaces' => true,
|
||||
'trailing_comma_in_multiline_array' => false,
|
||||
'trailing_comma_in_multiline' => false,
|
||||
'trim_array_spaces' => true,
|
||||
'unary_operator_spaces' => true,
|
||||
'whitespace_after_comma_in_array' => true,
|
||||
@@ -141,6 +142,18 @@ return PhpCsFixer\Config::create()
|
||||
'method',
|
||||
'property',
|
||||
]],
|
||||
'native_function_invocation' => [
|
||||
'include' => [
|
||||
'@compiler_optimized'
|
||||
],
|
||||
'scope' => 'namespaced'
|
||||
],
|
||||
'native_function_type_declaration_casing' => true,
|
||||
'no_alias_functions' => [
|
||||
'sets' => [
|
||||
'@internal'
|
||||
]
|
||||
],
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
@@ -151,3 +164,5 @@ return PhpCsFixer\Config::create()
|
||||
)
|
||||
->setFormat('checkstyle')
|
||||
;
|
||||
|
||||
return $fixer;
|
||||
@@ -1,40 +0,0 @@
|
||||
build:
|
||||
environment:
|
||||
node: v8.11.2
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
override:
|
||||
- js-scrutinizer-run
|
||||
- php-scrutinizer-run
|
||||
tests:
|
||||
tests:
|
||||
override:
|
||||
-
|
||||
command: 'vendor/bin/phpunit --exclude-group integration --coverage-clover=unit tests/'
|
||||
coverage:
|
||||
file: 'unit'
|
||||
format: 'clover'
|
||||
-
|
||||
command: 'vendor/bin/phpunit --group integration --coverage-clover=integration tests/'
|
||||
coverage:
|
||||
file: 'integration'
|
||||
format: 'clover'
|
||||
-
|
||||
command: 'bin/console kimai:phpcs --checkstyle=checkstyle'
|
||||
analysis:
|
||||
file: 'checkstyle'
|
||||
format: 'php-cs-checkstyle'
|
||||
|
||||
filter:
|
||||
excluded_paths:
|
||||
- '.github/'
|
||||
- 'bin/'
|
||||
- 'config/'
|
||||
- 'public/'
|
||||
- 'templates/'
|
||||
- 'translations/'
|
||||
- 'var/'
|
||||
dependency_paths:
|
||||
- 'node_modules/'
|
||||
- 'vendor/'
|
||||
23
.travis.yml
23
.travis.yml
@@ -1,23 +0,0 @@
|
||||
language: php
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache/files
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- php: 7.1
|
||||
- php: 7.2
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi
|
||||
- composer self-update
|
||||
|
||||
install:
|
||||
- composer install
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit --exclude-group integration tests/
|
||||
- vendor/bin/phpunit --group integration tests/
|
||||
178
CHANGELOG.md
178
CHANGELOG.md
@@ -1,177 +1,3 @@
|
||||
# Change Log
|
||||
# Changelog
|
||||
|
||||
## [0.2](https://github.com/kevinpapst/kimai2/tree/0.2) (2018-06-24)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.1...0.2)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Prevent end time before start time [\#152](https://github.com/kevinpapst/kimai2/issues/152)
|
||||
- Importing old data fails [\#145](https://github.com/kevinpapst/kimai2/issues/145)
|
||||
- Translate LanguageType choices [\#83](https://github.com/kevinpapst/kimai2/issues/83)
|
||||
- Change to users language after login [\#7](https://github.com/kevinpapst/kimai2/issues/7)
|
||||
- Mobile alignments and toolbar \#151 [\#157](https://github.com/kevinpapst/kimai2/pull/157) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved mobile view \#151 [\#150](https://github.com/kevinpapst/kimai2/pull/150) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Hide notifications after some seconds [\#169](https://github.com/kevinpapst/kimai2/issues/169)
|
||||
- Setup Scrutinizer to run code sniffer [\#158](https://github.com/kevinpapst/kimai2/issues/158)
|
||||
- Local config to overwrite settings [\#153](https://github.com/kevinpapst/kimai2/issues/153)
|
||||
- Improve mobile view [\#151](https://github.com/kevinpapst/kimai2/issues/151)
|
||||
- Upgrade to Symfony 4.1 [\#149](https://github.com/kevinpapst/kimai2/issues/149)
|
||||
- Replace has\_role\(\) with is\_granted\(\) [\#148](https://github.com/kevinpapst/kimai2/issues/148)
|
||||
- Integrate FOSUserBundle \(for registration, password-reset, etc.\) [\#144](https://github.com/kevinpapst/kimai2/issues/144)
|
||||
- Add help text to form elements [\#138](https://github.com/kevinpapst/kimai2/issues/138)
|
||||
- Duration mode: creating new timesheet records prefills duration with some seconds [\#137](https://github.com/kevinpapst/kimai2/issues/137)
|
||||
- Add duration-only mode [\#131](https://github.com/kevinpapst/kimai2/issues/131)
|
||||
- Add help controller [\#129](https://github.com/kevinpapst/kimai2/issues/129)
|
||||
- \[RFC\] Encore: configureFilenames with hash after filename instead of in the filename [\#125](https://github.com/kevinpapst/kimai2/issues/125)
|
||||
- \[RFC\] Kimai license header [\#124](https://github.com/kevinpapst/kimai2/issues/124)
|
||||
- Secure create-user command [\#123](https://github.com/kevinpapst/kimai2/issues/123)
|
||||
- Setup travis to run phpunit tests [\#120](https://github.com/kevinpapst/kimai2/issues/120)
|
||||
- Add command for building a production release [\#115](https://github.com/kevinpapst/kimai2/issues/115)
|
||||
- Add webpack and webpack-encore for frontend assets [\#113](https://github.com/kevinpapst/kimai2/issues/113)
|
||||
- Add time rounding option [\#112](https://github.com/kevinpapst/kimai2/issues/112)
|
||||
- Add field order number in Projects [\#107](https://github.com/kevinpapst/kimai2/issues/107)
|
||||
- Create docu for invoice templates [\#106](https://github.com/kevinpapst/kimai2/issues/106)
|
||||
- Add invoice template: timesheet [\#104](https://github.com/kevinpapst/kimai2/issues/104)
|
||||
- Add importer from Kimai v1 [\#102](https://github.com/kevinpapst/kimai2/issues/102)
|
||||
- Setup error pages [\#100](https://github.com/kevinpapst/kimai2/issues/100)
|
||||
- Add basic HTML invoice rendering [\#97](https://github.com/kevinpapst/kimai2/issues/97)
|
||||
- Add russian translation [\#95](https://github.com/kevinpapst/kimai2/issues/95)
|
||||
- Filter user in timesheet admin [\#93](https://github.com/kevinpapst/kimai2/issues/93)
|
||||
- Support to switch theme settings [\#89](https://github.com/kevinpapst/kimai2/issues/89)
|
||||
- Roles should become a real table [\#86](https://github.com/kevinpapst/kimai2/issues/86)
|
||||
- Fix Doctrine extensions config loader [\#84](https://github.com/kevinpapst/kimai2/issues/84)
|
||||
- Upgrade to Symfony 4 and Flex [\#74](https://github.com/kevinpapst/kimai2/issues/74)
|
||||
- Add remember me login [\#53](https://github.com/kevinpapst/kimai2/issues/53)
|
||||
- Add preferences sidebar [\#10](https://github.com/kevinpapst/kimai2/issues/10)
|
||||
- Add user settings [\#9](https://github.com/kevinpapst/kimai2/issues/9)
|
||||
- Add english translation [\#8](https://github.com/kevinpapst/kimai2/issues/8)
|
||||
- Add configurable system configuration [\#6](https://github.com/kevinpapst/kimai2/issues/6)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- various documentation updates [\#176](https://github.com/kevinpapst/kimai2/pull/176) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code styles: header\_comment and ordered\_imports [\#175](https://github.com/kevinpapst/kimai2/pull/175) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Code sniffer rules \#158 [\#172](https://github.com/kevinpapst/kimai2/pull/172) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Set default locale for unit-tests [\#171](https://github.com/kevinpapst/kimai2/pull/171) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Hide success notification after 5 seconds [\#170](https://github.com/kevinpapst/kimai2/pull/170) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added validation for end date before start date [\#167](https://github.com/kevinpapst/kimai2/pull/167) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Security expression \#148 [\#166](https://github.com/kevinpapst/kimai2/pull/166) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Composer install [\#165](https://github.com/kevinpapst/kimai2/pull/165) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added scrutinizer config \#158 [\#161](https://github.com/kevinpapst/kimai2/pull/161) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update to Symfony 4.1 \#149 [\#160](https://github.com/kevinpapst/kimai2/pull/160) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated README [\#159](https://github.com/kevinpapst/kimai2/pull/159) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Local config \#153 [\#156](https://github.com/kevinpapst/kimai2/pull/156) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Filter in hidden toolbar - improved mobile \#151 [\#155](https://github.com/kevinpapst/kimai2/pull/155) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added russian translation \#95 [\#147](https://github.com/kevinpapst/kimai2/pull/147) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Bugfix importer \(\#145\) [\#146](https://github.com/kevinpapst/kimai2/pull/146) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- help text and link for form labels \#138 [\#143](https://github.com/kevinpapst/kimai2/pull/143) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- bugfix for duration\_only mode \#137 [\#142](https://github.com/kevinpapst/kimai2/pull/142) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Make the back link appear like a normal link instead of a button [\#139](https://github.com/kevinpapst/kimai2/pull/139) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Use short array syntax [\#135](https://github.com/kevinpapst/kimai2/pull/135) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Duration only mode \#131 [\#134](https://github.com/kevinpapst/kimai2/pull/134) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added controller to render markdown documentation \#129 [\#132](https://github.com/kevinpapst/kimai2/pull/132) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Rounding rules [\#128](https://github.com/kevinpapst/kimai2/pull/128) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Secure create-user command [\#127](https://github.com/kevinpapst/kimai2/pull/127) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- ConfigureFilenames with hash after filename [\#126](https://github.com/kevinpapst/kimai2/pull/126) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- added webpack-encore for managing frontend assets \#113 [\#122](https://github.com/kevinpapst/kimai2/pull/122) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Remove personal mentions in license header [\#119](https://github.com/kevinpapst/kimai2/pull/119) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Add github issue and pull request template [\#118](https://github.com/kevinpapst/kimai2/pull/118) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Improvements [\#114](https://github.com/kevinpapst/kimai2/pull/114) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added first docu on extending kimai with bundles \#106 [\#109](https://github.com/kevinpapst/kimai2/pull/109) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added order number to projects \#107 [\#108](https://github.com/kevinpapst/kimai2/pull/108) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added short invoice calculator and timesheet invoice template \#104 [\#105](https://github.com/kevinpapst/kimai2/pull/105) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added command to import data from kimai v1 \#102 [\#103](https://github.com/kevinpapst/kimai2/pull/103) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Error pages \#100 [\#101](https://github.com/kevinpapst/kimai2/pull/101) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added basic invoice rendering \#97 [\#99](https://github.com/kevinpapst/kimai2/pull/99) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added remember me feature \#53 [\#98](https://github.com/kevinpapst/kimai2/pull/98) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code-cleanup: removed unused classes, added role constants, phpcs fixes [\#96](https://github.com/kevinpapst/kimai2/pull/96) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- load doctrine extensions during kernel bootstrap \#84 [\#92](https://github.com/kevinpapst/kimai2/pull/92) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- support theme options via user preferences \#89 [\#91](https://github.com/kevinpapst/kimai2/pull/91) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added dynamic user preferences \#7 \#9 [\#90](https://github.com/kevinpapst/kimai2/pull/90) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added control sidebar \#10 [\#87](https://github.com/kevinpapst/kimai2/pull/87) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added english translation \#8 [\#82](https://github.com/kevinpapst/kimai2/pull/82) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- upgraded to symfony 4 \#74 [\#81](https://github.com/kevinpapst/kimai2/pull/81) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1) (2018-01-10)
|
||||
**Closed issues:**
|
||||
|
||||
- Hide inactive delete user button [\#78](https://github.com/kevinpapst/kimai2/issues/78)
|
||||
- Delete activity [\#71](https://github.com/kevinpapst/kimai2/issues/71)
|
||||
- Delete other users timesheet entries [\#70](https://github.com/kevinpapst/kimai2/issues/70)
|
||||
- Delete Project [\#69](https://github.com/kevinpapst/kimai2/issues/69)
|
||||
- Delete Customer [\#68](https://github.com/kevinpapst/kimai2/issues/68)
|
||||
- Delete timesheet entries [\#67](https://github.com/kevinpapst/kimai2/issues/67)
|
||||
- Add installation docu [\#64](https://github.com/kevinpapst/kimai2/issues/64)
|
||||
- Disallow login for inactive user [\#57](https://github.com/kevinpapst/kimai2/issues/57)
|
||||
- Add toolbar to filter user screen [\#56](https://github.com/kevinpapst/kimai2/issues/56)
|
||||
- Upgrade to latest Avanzu version [\#55](https://github.com/kevinpapst/kimai2/issues/55)
|
||||
- AdminLTE login screen [\#52](https://github.com/kevinpapst/kimai2/issues/52)
|
||||
- Add edit Timesheet entry for Teamleads [\#45](https://github.com/kevinpapst/kimai2/issues/45)
|
||||
- Add Timesheet Voter [\#44](https://github.com/kevinpapst/kimai2/issues/44)
|
||||
- Developer improvements [\#42](https://github.com/kevinpapst/kimai2/issues/42)
|
||||
- Base class for repository and query objects [\#40](https://github.com/kevinpapst/kimai2/issues/40)
|
||||
- Add console command to create user [\#36](https://github.com/kevinpapst/kimai2/issues/36)
|
||||
- Add basic security Voter [\#34](https://github.com/kevinpapst/kimai2/issues/34)
|
||||
- Do not show hidden Activities [\#33](https://github.com/kevinpapst/kimai2/issues/33)
|
||||
- Do not show hidden Projects [\#32](https://github.com/kevinpapst/kimai2/issues/32)
|
||||
- Add "create timesheet record" form [\#31](https://github.com/kevinpapst/kimai2/issues/31)
|
||||
- Add filter to Customer admin [\#28](https://github.com/kevinpapst/kimai2/issues/28)
|
||||
- Do not show hidden Customer [\#27](https://github.com/kevinpapst/kimai2/issues/27)
|
||||
- Rename Timesheet query class [\#25](https://github.com/kevinpapst/kimai2/issues/25)
|
||||
- Add "create activity" functionality [\#24](https://github.com/kevinpapst/kimai2/issues/24)
|
||||
- Add "create project" functionality [\#23](https://github.com/kevinpapst/kimai2/issues/23)
|
||||
- Add "create customer" functionality [\#22](https://github.com/kevinpapst/kimai2/issues/22)
|
||||
- Translate dashboard widgets [\#20](https://github.com/kevinpapst/kimai2/issues/20)
|
||||
- Display user avatar in navbar [\#16](https://github.com/kevinpapst/kimai2/issues/16)
|
||||
- Add role SUPER\_ADMIN [\#15](https://github.com/kevinpapst/kimai2/issues/15)
|
||||
- Teamlead can see all user times [\#13](https://github.com/kevinpapst/kimai2/issues/13)
|
||||
- Add filter to Timesheet \(user\) [\#11](https://github.com/kevinpapst/kimai2/issues/11)
|
||||
- Add "create user" functionality [\#5](https://github.com/kevinpapst/kimai2/issues/5)
|
||||
- Edit users role [\#4](https://github.com/kevinpapst/kimai2/issues/4)
|
||||
- Add filter to Timesheet admin [\#3](https://github.com/kevinpapst/kimai2/issues/3)
|
||||
- Add filter to Activity admin [\#2](https://github.com/kevinpapst/kimai2/issues/2)
|
||||
- Add filter to Project admin [\#1](https://github.com/kevinpapst/kimai2/issues/1)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- hide inactive delete user button \#78 [\#79](https://github.com/kevinpapst/kimai2/pull/79) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete customer action \#68 [\#77](https://github.com/kevinpapst/kimai2/pull/77) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete action for projects \#69 [\#76](https://github.com/kevinpapst/kimai2/pull/76) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Delete actions \#71 [\#75](https://github.com/kevinpapst/kimai2/pull/75) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete timesheet record for admins \#70 [\#73](https://github.com/kevinpapst/kimai2/pull/73) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete timesheet records action for users \#67 [\#72](https://github.com/kevinpapst/kimai2/pull/72) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added toolbar to user screen \#56 [\#66](https://github.com/kevinpapst/kimai2/pull/66) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- document installation options \#64 [\#65](https://github.com/kevinpapst/kimai2/pull/65) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- update avanzu admin bundle \#55 [\#63](https://github.com/kevinpapst/kimai2/pull/63) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- prevent inactive user from login \#57 [\#61](https://github.com/kevinpapst/kimai2/pull/61) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- handle hidden customer \#27 [\#60](https://github.com/kevinpapst/kimai2/pull/60) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- handle hidden projects \#32 [\#59](https://github.com/kevinpapst/kimai2/pull/59) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Handle hidden activities \#33 [\#58](https://github.com/kevinpapst/kimai2/pull/58) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- using the AdminLTE login screen \#52 [\#54](https://github.com/kevinpapst/kimai2/pull/54) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added toolbar for activity screen \#2 [\#51](https://github.com/kevinpapst/kimai2/pull/51) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added toolbar to project screen \#1 [\#50](https://github.com/kevinpapst/kimai2/pull/50) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added toolbar to customer screen \#28 [\#49](https://github.com/kevinpapst/kimai2/pull/49) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added form to edit timesheet entries for all user \#45 [\#48](https://github.com/kevinpapst/kimai2/pull/48) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Create timesheet for User \#31 [\#47](https://github.com/kevinpapst/kimai2/pull/47) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added timesheet voter \#44 [\#46](https://github.com/kevinpapst/kimai2/pull/46) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Developer cleanup \#42 [\#43](https://github.com/kevinpapst/kimai2/pull/43) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Repository cleanup [\#41](https://github.com/kevinpapst/kimai2/pull/41) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added voter for customer, project and activity [\#39](https://github.com/kevinpapst/kimai2/pull/39) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added create user console command [\#38](https://github.com/kevinpapst/kimai2/pull/38) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added "create user" functionality [\#37](https://github.com/kevinpapst/kimai2/pull/37) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added "create activity" form [\#35](https://github.com/kevinpapst/kimai2/pull/35) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added create project functionality [\#30](https://github.com/kevinpapst/kimai2/pull/30) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Rename TimesheetQuery class [\#29](https://github.com/kevinpapst/kimai2/pull/29) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Add customer functionality [\#26](https://github.com/kevinpapst/kimai2/pull/26) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Dashboard widgets [\#21](https://github.com/kevinpapst/kimai2/pull/21) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added form to edit user roles [\#18](https://github.com/kevinpapst/kimai2/pull/18) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added role ROLE\_SUPER\_ADMIN [\#17](https://github.com/kevinpapst/kimai2/pull/17) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow TEAMLEAD to see user times [\#14](https://github.com/kevinpapst/kimai2/pull/14) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Timesheet toolbar [\#12](https://github.com/kevinpapst/kimai2/pull/12) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
|
||||
|
||||
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
|
||||
See [GitHub release page](https://github.com/kimai/kimai/releases).
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
# Contributing
|
||||
|
||||
Kimai 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.
|
||||
Send your ideas, code reviews, pull requests and feature requests to help improving this project.
|
||||
|
||||
## Pull request rules
|
||||
|
||||
- We use PSR-2 with some additional code-style checks (see our [php-cs-fixer config](.php_cs.dist)). You can run `bin/console kimai:phpcs` to check and `bin/console kimai:phpcs --fix` to fix violations.
|
||||
- Add PHPUnit tests for your changes, verify everything still works and execute our test-suites `bin/console kimai:test-unit` and `bin/console kimai:test-integration`.
|
||||
- If you contribute new files, please add them with the file-header template from below (our chode-style fixer can do that for you).
|
||||
- With sending in a PR, you accept that your contributions/code will be published under MIT license (see the [LICENSE](LICENSE) file as well).
|
||||
- If one of the PR checks fails, please fix them before asking us for a review.
|
||||
- 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
|
||||
|
||||
Further documentation can be found in the [developer documentation](var/docs/developers.md).
|
||||
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
|
||||
|
||||
### File-header template
|
||||
```
|
||||
|
||||
12
LICENSE
12
LICENSE
@@ -1,11 +1,13 @@
|
||||
Copyright (c) 2017-2018 Kevin Papst
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2022 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.
|
||||
233
README.md
233
README.md
@@ -1,197 +1,90 @@
|
||||
# Kimai v2 - Time Tracking
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/kimai/images/master/repository-header.png" alt="Kimai logo">
|
||||
</p>
|
||||
|
||||
Kimai v2 - the open source time-tracking application with a mobile-first approach, read more at the [official website](http://v2.kimai.org).
|
||||
<p align="center">
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/workflows/CI/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/kevinpapst/kimai2"><img alt="Code Coverage" src="https://codecov.io/gh/kevinpapst/kimai2/branch/master/graph/badge.svg"></a>
|
||||
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="Latest stable version" src="https://poser.pugx.org/kevinpapst/kimai2/v/stable"></a>
|
||||
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="License" src="https://poser.pugx.org/kevinpapst/kimai2/license"></a>
|
||||
<a href="https://twitter.com/kimai_org" rel="me"><img alt="Twitter" src="https://img.shields.io/badge/follow-%40kimai__org-00acee"></a>
|
||||
<a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a>
|
||||
</p>
|
||||
|
||||
[](https://travis-ci.org/kevinpapst/kimai2)
|
||||
[](https://scrutinizer-ci.com/g/kevinpapst/kimai2/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/kevinpapst/kimai2/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/kevinpapst/kimai2/build-status/master)
|
||||
<h1 align="center">Kimai - time-tracker</h1>
|
||||
|
||||
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
||||
It is built with modern technologies such as [Symfony](https://github.com/symfony/symfony), [Bootstrap](https://github.com/twbs/bootstrap),
|
||||
[RESTful API](https://github.com/FriendsOfSymfony/FOSRestBundle), [Doctrine](https://github.com/doctrine/),
|
||||
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/), [Webpack](https://github.com/webpack/webpack), ES6 and [many](composer.json) [more](package.json).
|
||||
|
||||
## Introduction
|
||||
|
||||
This is the reloaded version of the open source timetracker Kimai.
|
||||
Right now its in an early development phase, its usable but some advanced features from Kimai v1 are missing by now.
|
||||
|
||||
Kimai v2 has nothing in common with its predecessor [Kimai v1](http://www.kimai.org) besides the basic ideas of time-tracking and the current development team.
|
||||
It is based on a lot of great frameworks. Special thanks to [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
|
||||
[AdminThemeBundle](https://github.com/avanzu/AdminThemeBundle/) (based on [AdminLTE](https://github.com/almasaeed2010/AdminLTE/)).
|
||||
- [Home](https://www.kimai.org) - Kimai project homepage
|
||||
- [Blog](https://www.kimai.org/blog/) - Read the latest news
|
||||
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use Kimai
|
||||
- [Translations](https://hosted.weblate.org/projects/kimai/#languages) - Kimai in your language
|
||||
- [Migration](https://www.kimai.org/documentation/migration-v1.html) - Import data from Kimai 1
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 7.1.3 or higher
|
||||
- One PHP extension of PDO-SQLite or PDO-MySQL enabled (it might work with PostgreSQL and Oracle as well, but that wasn't tested and is not officially supported)
|
||||
- the [usual Symfony application requirements](http://symfony.com/doc/current/reference/requirements.html)
|
||||
- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md)
|
||||
- PHP 7.4, 8.0 or 8.1
|
||||
- MariaDB or MySQL
|
||||
- A webserver and subdomain
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `xsl`, `zip`
|
||||
|
||||
## Documentation & Roadmap
|
||||
### About
|
||||
|
||||
Looking for more information about using Kimai? Check out our more detailed [documentation](var/docs/).
|
||||
The evolution of the most known(?) open source project time-tracker Kimai. It is stable, production ready and ships
|
||||
with many advanced features, including but not limited to:
|
||||
|
||||
You can see our development roadmap for the future in the [Milestones](milestones/) sections,
|
||||
current work is organized in the [Project](projects/) planning boards.
|
||||
Our roadmap is open for changes and input from the community, please [sent us](issues/) your ideas and questions.
|
||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
||||
authentication via SAML/LDAP/Database, customizable role and team permissions, responsive and ready for your mobile device,
|
||||
user/customer/project specific rates, advanced search & filtering, money and time budgets, reporting, support for plugins
|
||||
and so many more.
|
||||
|
||||
## Installation
|
||||
|
||||
First, install Git and [Composer](https://getcomposer.org/doc/00-intro.md) if you haven't already. Then clone this repo:
|
||||
- [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
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kevinpapst/kimai2.git
|
||||
cd kimai2/
|
||||
```
|
||||
### Updating Kimai
|
||||
|
||||
The next steps depend in which environment you want to use Kimai, you can choose between development or production mode.
|
||||
- [Update Kimai](https://www.kimai.org/documentation/updates.html) - get the latest version
|
||||
- [UPGRADING guide](UPGRADING.md) - version specific steps
|
||||
|
||||
### Installation (development)
|
||||
### Plugins
|
||||
|
||||
Lets prepare the environment by installing all dependencies:
|
||||
- [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
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
## Roadmap and releases
|
||||
|
||||
The default installation uses a SQLite database, so there is no need to create a database for your first tests.
|
||||
Our default settings will work out-of-the-box, but you might want to adjust the `.env` values to your needs.
|
||||
You can configure your database through your environment (e.g. Webserver, Cloud-Provider) or in your `.env` file:
|
||||
```
|
||||
DATABASE_PREFIX=kimai2_
|
||||
DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai.sqlite
|
||||
APP_ENV=dev
|
||||
APP_SECRET=some_random_secret_string_for_your_installation
|
||||
```
|
||||
You can see a rough development roadmap in the [Milestones](https://github.com/kimai/kimai/milestones) sections.
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kimai/kimai/issues) are welcome.
|
||||
|
||||
The next command will create the database and the schema:
|
||||
```bash
|
||||
bin/console doctrine:database:create
|
||||
bin/console doctrine:schema:create
|
||||
```
|
||||
> Kimai uses a rolling release concept for delivering updates.
|
||||
> You don't have to wait for the next official release, upgrade it at any time from the master branch,
|
||||
> which is always deployable - release tags are simple snapshots of the development version.
|
||||
|
||||
Lets bootstrap your environment by executing this commands (which is only available in dev environment):
|
||||
```bash
|
||||
bin/console kimai:reset-dev
|
||||
```
|
||||
Release versions will be created on a regular basis, every couple of weeks.
|
||||
Every code change, whether it's a new feature or a bugfix, will be done on the master branch.
|
||||
Kimai is actively developed in my spare time, I put my effort into the software instead of back-porting changes.
|
||||
|
||||
You just imported demo data, to test the application in its full beauty and with several different user accounts and permission sets.
|
||||
## Contributing
|
||||
|
||||
You can now login with these accounts:
|
||||
You want to contribute to this repository? This is so great!
|
||||
The best way to start is to [open a new issue](https://github.com/kimai/kimai/issues) for bugs or feature requests or a [discussion](https://github.com/kimai/kimai/discussions) for questions, support and such.
|
||||
|
||||
| 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 |
|
||||
In case you want to contribute, but you wouldn't know how, here are some suggestions:
|
||||
|
||||
Demo data can always be deleted by dropping the schema and re-creating it.
|
||||
ATTENTION - this will erase all your data:
|
||||
- Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on platforms like Slant, Product Hunt, Softpedia or AlternativeTo, you can tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every bit helps!
|
||||
- Answer questions: You know the answer to another user's problem? Share your knowledge!
|
||||
- Make a feature request: Something can be done better? Something essential missing? Let us know!
|
||||
- Report bugs
|
||||
- You don't have to be programmer to help. The documentation and translation could use some love as well.
|
||||
- Sponsor the project, free software still costs money
|
||||
|
||||
```bash
|
||||
bin/console doctrine:schema:drop --force
|
||||
bin/console doctrine:schema:create
|
||||
```
|
||||
|
||||
The `kimai:reset-dev` command can always be executed later on to reset your dev database and cache.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
To re-generate the frontend assets ([more information here](var/docs/developers.md)), execute:
|
||||
```bash
|
||||
yarn install
|
||||
npm run prod
|
||||
```
|
||||
|
||||
### Installation (production)
|
||||
|
||||
Lets prepare the environment by installing all dependencies:
|
||||
|
||||
```bash
|
||||
composer install --no-dev
|
||||
```
|
||||
|
||||
Make sure the [directories are read and writable by your webserver](https://symfony.com/doc/current/setup/file_permissions.html):
|
||||
```bash
|
||||
chown -R www-data var/
|
||||
chmod -R 777 var/
|
||||
```
|
||||
|
||||
The database to use is up to you, but we would not recommend using the default SQLite database for production usage.
|
||||
Please create your database and configure the connection string in your environment, e.g. with the `.env` file:
|
||||
```
|
||||
APP_ENV=prod
|
||||
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
|
||||
APP_SECRET=insert_a_random_secret_string_for_production
|
||||
```
|
||||
|
||||
Create the database schemas and warm up the cache (as webserver user):
|
||||
```bash
|
||||
bin/console doctrine:schema:create
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
```
|
||||
|
||||
Create your first user with the following command. You will be asked to enter a password afterwards.
|
||||
|
||||
```bash
|
||||
bin/console kimai:create-user username admin@example.com ROLE_SUPER_ADMIN
|
||||
```
|
||||
_Tip: You can skip the "create user" step, if you are going to import data from Kimai v1._
|
||||
|
||||
For available roles, please refer to the [user documentation](var/docs/users.md).
|
||||
|
||||
> **NOTE**
|
||||
>
|
||||
> If you want to use a fully-featured web server (like Nginx or Apache) to run
|
||||
> Kimai, configure it to point at the `public/` directory of the project.
|
||||
> For more details, see:
|
||||
> http://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html
|
||||
|
||||
That's it, you can start time-tracking :-)
|
||||
|
||||
### Importing data from Kimai v1
|
||||
|
||||
Before importing your data from a Kimai v1 installation, please read the following carefully:
|
||||
|
||||
- Data from the existing v1 installation is only read and will never be changed
|
||||
- Data can only be imported from a Kimai installation with at least `v1.0.1` and database revision `1388` (check your `configuration` table)
|
||||
- Kimai v1 has support for activities without project assignment, which Kimai v2 doesn't support. Unattached activities will be created for every project that has a linked activity in any of the imported timesheet records
|
||||
- Rates and fixed-rates are handled in a completely different way and for now only the timesheet record total amounts are imported
|
||||
- Customers cannot login and no user accounts will be created for them
|
||||
- The customers country has to be manually assigned afterwards, as there is no field in Kimai v1 for that
|
||||
- You have to supply the default password that is used for every imported user, as their password will be resetted
|
||||
- Data that was deleted in Kimai v1 (user, customer, projects, activities) will be imported and set to `invisible` (if you don't want that, you have to delete all entries that have the value `1` in the `trash` column before importing)
|
||||
|
||||
A possible full command for import:
|
||||
```bash
|
||||
bin/console kimai:import-v1 "mysql://user:password@127.0.0.1:3306/database?charset=utf8" "db_prefix" "password" "country"
|
||||
```
|
||||
|
||||
It is recommended to test the import in a fresh database. You can test your import as often as you like and fix possible problems in your installation.
|
||||
A sample command could look like that:
|
||||
```bash
|
||||
bin/console doctrine:schema:drop --force && bin/console doctrine:schema:create && bin/console kimai:import-v1 "mysql://kimai:test@127.0.0.1:3306/kimai?charset=latin1" "kimai_" "test123" "de"
|
||||
```
|
||||
That will drop the configured Kimai v2 database schema and re-create it, before importing the data from the `mysql` database at `127.0.0.1` on port `3306` authenticating the user `kimai` with the password `test` for import.
|
||||
The connection will use the charset `latin1` and the default table prefix `kimai_` for reading data. Imported users can login with the password `test123` and all customer will have the country `de` assigned.
|
||||
|
||||
## Extensions for Kimai 2
|
||||
|
||||
As Kimai 2 was built on top of Symfony, it can be extended like every other Symfony application.
|
||||
We call these extensions bundles, but you might also know them as add-ons, extensions or plugins.
|
||||
|
||||
All available Kimai 2 bundles can be found at the [Kimai recipes](https://github.com/kimai/recipes) repository.
|
||||
|
||||
## Developer
|
||||
|
||||
If you want to develop for Kimai 2 please read the following documentation:
|
||||
|
||||
- an example on how to extend Kimai 2 can be found in this [GitHub repository](https://github.com/kevinpapst/kimai2-invoice)
|
||||
- the [developer documentation](var/docs/developers.md) is available both on GitHub and your local installation
|
||||
There is one simple rule in our "Code of conduct": Don't be an ass!
|
||||
|
||||
24
SECURITY.md
Normal file
24
SECURITY.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As announced in the [README](README.md) I only support the latest available release and master.
|
||||
|
||||
| Version | Supported |
|
||||
|----------------------|--------------------|
|
||||
| main/master branch | :white_check_mark: |
|
||||
| latest minor release | :white_check_mark: |
|
||||
| older releases | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please read the [Bughunter](https://www.kimai.org/documentation/bughunter.html) documentation before posting.
|
||||
You can report any security related vulnerability in the [advisory section at GitHub](https://github.com/kimai/kimai/security/advisories) or via email to 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.
|
||||
|
||||
You will be mentioned in the release notes if your issue is valid.
|
||||
I am grateful for any (discrete) disclosure of vulnerabilities!
|
||||
366
UPGRADING.md
Normal file
366
UPGRADING.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# 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.16](https://github.com/kimai/kimai/releases/tag/1.16)
|
||||
|
||||
**DEVELOPER**
|
||||
|
||||
- Removed `formDateTime` field from API model `I18nConfig`
|
||||
|
||||
## [1.15](https://github.com/kimai/kimai/releases/tag/1.15)
|
||||
|
||||
**Many database changes: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
Updating the database might take quite a while, depending on the amount of timesheet entries and speed of your database server (~1 minute per 100k records).
|
||||
|
||||
**ATTENTION**
|
||||
|
||||
- This release bumps the minimum required [PHP version to 7.3](https://www.kimai.org/blog/2021/php8-support-php72-dropped/)
|
||||
- Self-registration is disabled by default
|
||||
- Self-registration now always requires email confirmation
|
||||
- All plugins that use own databases need to be updated as well
|
||||
- Removed the YearChart widget and the related configs named `userRecapThisYear`, `userRecapLastYear`, `userRecapTwoYears`, `userRecapThreeYears`
|
||||
|
||||
**LDAP & SAML**
|
||||
|
||||
Please verify your config with the [LDAP](https://www.kimai.org/documentation/ldap.html) and [SAML](https://www.kimai.org/documentation/saml.html) documentation, especially:
|
||||
|
||||
- SAML users: activate it by setting the `kimai.saml.activate: true` config key
|
||||
- LDAP users: activate it by setting the `kimai.ldap.activate: true` config key
|
||||
- LDAP and SAML users need to remove the complete `security` section from their `local.yaml`
|
||||
|
||||
**DEVELOPER**
|
||||
|
||||
PHP 8 compatibility forced to upgrade MANY libraries, including but not limited to:
|
||||
|
||||
- Removed FOSUserBundle and hslavich/oneloginsaml
|
||||
- Doctrine Migrations, whose new major version forces plugin updates
|
||||
- Gedmo v3 (which include BC breaks in definitions)
|
||||
- Doctrine DBAL and others, which required PHP 7.3 as well
|
||||
|
||||
**API BC break**: Due to team structure changes, it was impossible to keep the (writing) API structure. Please adjust your code accordingly!
|
||||
|
||||
## [1.14](https://github.com/kimai/kimai/releases/tag/1.14)
|
||||
|
||||
**CRITICAL BC break**: SQLite support was removed. If you are using SQLite, you have to [read this blog post](https://www.kimai.org/blog/2021/sqlite-and-ftp-support-removed/) and migrate to MySQL/MariaDB first!
|
||||
|
||||
**New database tables and fields: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
Permission changes:
|
||||
- `history_invoice` - removed permission entirely
|
||||
|
||||
## [1.13](https://github.com/kimai/kimai/releases/tag/1.13)
|
||||
|
||||
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
|
||||
- Changed invoice filename generation (check if you use cronjob for invoices)
|
||||
- **BC break**: duration entered as plain numbers will now be treated as decimal duration in hours instead of seconds
|
||||
|
||||
## [1.12](https://github.com/kimai/kimai/releases/tag/1.12)
|
||||
|
||||
- Export templates can now include items from plugins (eg. Expenses).
|
||||
|
||||
## [1.10](https://github.com/kimai/kimai/releases/tag/1.10)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- Invoice renderer `CSV` was removed
|
||||
- Sessions are now stored in the database (all users have to re-login after upgrade)
|
||||
- New permissions: `lockdown_grace_timesheet`, `lockdown_override_timesheet`, `view_all_data`
|
||||
- Fixed team permissions on user queries: depending on your previous team & permission setup your users might see less data (SUPER_ADMINS see all data, but new: ADMINS only see all data if they own the `view_all_data` permission)
|
||||
- Markdown does not support headings anymore, text like `# foo` is not converted to `<h1 id="foo">foo</h1>` anymore
|
||||
|
||||
### Developer
|
||||
|
||||
- **BC break**: removed registration of `.env` with `putenv()` - do not rely on `getenv()` as it is not thread-safe
|
||||
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf()` changed
|
||||
- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more
|
||||
- **BC Break**: removed `getVisible()` (deprecated since 1.4) method on Customer, Project and Activity (use `isVisible()` instead, templates are still working)
|
||||
- **BC Break**: API changes
|
||||
- some representation names changed (eg. from `ActivityMetaField` to `ActivityMeta`, `TimesheetSubCollection` vs `TimesheetCollectionExpanded`), you could use `class_alias()` if you use auto-generated code from Swagger-Gen or alike
|
||||
- new result types were introduced
|
||||
- result data changed in some areas to smooth out inconsistencies (eg. TeamEntity fields changed in nested results)
|
||||
|
||||
## [1.9](https://github.com/kimai/kimai/releases/tag/1.9)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- The directory `var/data/invoices/` will be used to store archived invoice files (check file permissions)
|
||||
- The default invoice number format changed. If you want to use the old one: configure `{date}` as format - see [invoice documentation](https://www.kimai.org/documentation/invoices.html)
|
||||
- HTML invoice templates are now treated like other files and offered as download. If you are using relative URLs for including assets (CSS, images) you need to either inline them (see the default templates) or use absolute URLs
|
||||
- Invoice templates that use the templates variables `${activity.X}` or `${project.X}` should be checked and possibly adapted, as multi-select is now possible for filtering
|
||||
- Invoice templates have access to all meta-fields as variables, not only the ones marked as visible
|
||||
- Rates configuration/structure changed for customer, project and activity
|
||||
- **Invoice templates**: rates variables were removed
|
||||
|
||||
Permission changes:
|
||||
- `history_invoice` - NEW: grants all features for the new invoice archive (by default for all admins)
|
||||
|
||||
### Developer
|
||||
|
||||
- **BC break**: `InvoiceItemInterface` has new methods `getType()` and `getCategory()`
|
||||
- **BC break**: API fields changed - see new `/rates` endpoints
|
||||
|
||||
## [1.8](https://github.com/kimai/kimai/releases/tag/1.8)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- New PHP requirement: `ext-xsl` - which should be pre-installed in most environments when `ext-xml` is loaded
|
||||
- New mailer library: check if emails are still working (eg. by using the "password forgotten" function) or if you need to adjust your configuration, [see docs at symfony.com](https://symfony.com/doc/current/components/mailer.html#transport)
|
||||
- Support for line breaks in multiline invoice fields for spreadsheets (check your invoice templates after the update)
|
||||
|
||||
Permission changes:
|
||||
|
||||
- `comments_create_customer` - NEW: permission that allows to add new comments for customers
|
||||
- `comments_create_team_customer` - NEW: permission that allows to add new comments for team members of the current customer
|
||||
- `comments_create_teamlead_customer` - NEW: permission that allows to add new comments for a teamlead of the current customer
|
||||
- `comments_create_project` - NEW: permission that allows to add new comments for project
|
||||
- `comments_create_team_project` - NEW: permission that allows to add new comments for team members of the current project
|
||||
- `comments_create_teamlead_project` - NEW: permission that allows to add new comments for a teamlead of the current project
|
||||
- `edit_teamlead_project` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
|
||||
- `edit_teamlead_customer` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
|
||||
- `upload_invoice_template` - NEW: permission that allows to upload invoice documents from the UI
|
||||
|
||||
## [1.7](https://github.com/kimai/kimai/releases/tag/1.7)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
New permissions:
|
||||
|
||||
- `comments_customer` - show comment list on customer detail page (new feature)
|
||||
- `details_customer` - show detail information for customers (customer number, vat, rates, meta-fields, assigned teams ...)
|
||||
- `comments_project` - show comment list on project detail page (new feature)
|
||||
- `details_project` - show detail information for projects (rates, meta-fields, assigned teams ...)
|
||||
|
||||
If you are using teams, please read on: The following list of permissions are now also available in the UI and they (can) replace the `X_project` and `X_customer` permissions.
|
||||
They are more strict, as they allow only access to team specific items, the older permissions without `_teamlead_`/`_team_` work on a global level instead.
|
||||
|
||||
- `view_teamlead_customer`, `edit_teamlead_customer`, `budget_teamlead_customer`, `permissions_teamlead_customer`, `comments_teamlead_customer`, `details_teamlead_customer` - allows access to customer data when user is teamlead of a team assigned to the customer (replaces more global permission like `view_customer` for teamleads)
|
||||
- `view_team_customer`, `edit_team_customer`, `budget_team_customer`, `comments_team_customer`, `details_team_customer` - allows access to customer data when user is member of a team assigned to the customer (replaces more global permission like `view_customer` for users)
|
||||
- `view_teamlead_project`, `edit_teamlead_project`, `budget_teamlead_project`, `permissions_teamlead_project`, `comments_teamlead_project`, `details_teamlead_project` - allows access to customer data when user is teamlead of a team assigned to the project (replaces more global permission like `view_project` for teamleads)
|
||||
- `view_team_project`, `edit_team_project`, `budget_team_project`, `comments_team_project`, `details_team_project` - allows access to customer data when user is member of a team assigned to the project (replaces more global permission like `view_project` for users)
|
||||
|
||||
### ExpenseBundle
|
||||
|
||||
**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step:
|
||||
You need to delete the bundle before updating: `rm -r var/plugins/ExpenseBundle`, otherwise you will run into errors during the update.
|
||||
After the Kimai update was successful, you have to re-install the latest bundle version, which is compatible with Kimai 1.7 only.
|
||||
|
||||
### Developer
|
||||
|
||||
- Projects now have a start and end date and the API will only return those, which are either unconfigured or currently active, you might want to reload the list of projects once the user entered begin and end datetime OR use the new `ignoreDates` parameter.
|
||||
- Doctrine bundle was updated to v2, check your code for [the usage of RegistryInterface and ObjectManager](https://github.com/doctrine/DoctrineBundle/blob/master/UPGRADE-2.0.md)
|
||||
- Removed the webserver bundle and the command `server:run` - see [docs](https://www.kimai.org/documentation/developers.html)
|
||||
|
||||
## [1.6](https://github.com/kimai/kimai/releases/tag/1.6), [1.6.1](https://github.com/kimai/kimai/releases/tag/1.6.1), [1.6.2](https://github.com/kimai/kimai/releases/tag/1.6.2)
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
- Invoice changes:
|
||||
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
|
||||
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
|
||||
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
|
||||
- Important permission change: regular users with the `view_other_timesheet` permission could see all timesheets. This was a legacy from the time before team permissions were introduced. If you rely on this behavior, you need to create a team with all users and the teamlead being the user who needs access to all timesheets.
|
||||
|
||||
### Developer
|
||||
|
||||
Please add default permissions to your [plugin](https://www.kimai.org/documentation/plugins.html).
|
||||
|
||||
## [1.5](https://github.com/kimai/kimai/releases/tag/1.5)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html)
|
||||
|
||||
## [1.4](https://github.com/kimai/kimai/releases/tag/1.4)
|
||||
|
||||
**There is a new directory, which needs to be writable by the webserver: `public/avatars/`.**
|
||||
|
||||
New permission (used in new dashboard widget):
|
||||
- `view_team_member` - display team assignments (names, teamleads and members) for the current user
|
||||
|
||||
Activated Javascript select component by default (check mobile devices).
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Dashboard widgets and rows need to define their `type` by FQCN
|
||||
- Switched to Symfony 4.3 event types, this could fail in plugins, but only if they didn't use the official constants for event names
|
||||
|
||||
## [1.3](https://github.com/kimai/kimai/releases/tag/1.3)
|
||||
|
||||
Added `manage_tag` permission for new tag features
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Refactored toolbars and search, plugins needs to be checked
|
||||
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
|
||||
|
||||
## [1.2](https://github.com/kimai/kimai/releases/tag/1.2)
|
||||
|
||||
**If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.**
|
||||
|
||||
- Deleted timezone conversion command.
|
||||
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
|
||||
- Maximum customer name length lowered to 150 character
|
||||
- Maximum project name length lowered to 150 character
|
||||
- Maximum activity name length lowered to 150 character
|
||||
- Added new permission: `manage_invoice_template`
|
||||
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
|
||||
- Removed permission: `view_export` (using `create_export` only)
|
||||
|
||||
### Developer: BC breaks
|
||||
|
||||
- Custom export renderer need to check for usage of `Timesheet::getEnd()` as running entries can now be exported as well
|
||||
|
||||
## [1.1](https://github.com/kimai/kimai/releases/tag/1.1)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0 / 1.0.1.
|
||||
|
||||
## [1.0](https://github.com/kimai/kimai/releases/tag/1.0)
|
||||
|
||||
This release contains several changes, as I still have the goal to stabilize the code base to prevent
|
||||
such "challenges" after 1.0 for a while.
|
||||
|
||||
### Changes for your local.yaml
|
||||
|
||||
New permissions are available. You have to add them to your `local.yaml` ONLY if you use a custom permission structure,
|
||||
otherwise you can't use the new features:
|
||||
- `view_tag` - view all tags
|
||||
- `delete_tag` - delete tags
|
||||
- `edit_exported_timesheet` - allows to edit records which were exported
|
||||
- `role_permissions` - view calculated permissions for user roles
|
||||
- `budget_activity` - view and edit budgets for activities
|
||||
- `budget_project` - view and edit budgets for projects
|
||||
- `budget_customer` - view and edit budgets for customers
|
||||
|
||||
Removed permission:
|
||||
- `system_actions` - removed experimental feature to flush app cache from the about screen
|
||||
|
||||
### BC BREAKS
|
||||
|
||||
- API: Format for queries including a datetime object fixed to use HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`)
|
||||
- **Permission config**: the `permissions` definition in your `local.yaml` needs to be verified/changed, as the internal structure was highly optimized to simplify the definition.
|
||||
Thanks to the new structure, you should be able to remove almost everything from your `local.yaml` (tip: start over from scratch!). Please read [the updated permission docu](https://www.kimai.org/documentation/permissions.html).
|
||||
- default widgets were removed from `kimai.yaml`, that shouldn't cause any issues ... but if something is odd: [look here for help](https://www.kimai.org/documentation/dashboard.html)
|
||||
|
||||
## [0.9](https://github.com/kimai/kimai/releases/tag/0.9)
|
||||
|
||||
Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
### BC BREAKS
|
||||
|
||||
This release contains some BC breaks which were necessary before 1.0 will be released (_now or never_), to prevent those BC breaks after 1.0.
|
||||
|
||||
- **Kimai requires PHP 7.2 now => [PHP 7.1 expired 4 month ago](https://www.php.net/supported-versions.php)**
|
||||
- The `.env` variable `DATABASE_PREFIX` was removed and the table prefix is now hardcoded to `kimai2_`. If you used another prefix,
|
||||
you have to rename your tables manually before starting the update process. You can delete the row `DATABASE_PREFIX` from your `.env` file.
|
||||
- API: Format for DateTime objects changed, now including timezone identifier (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00+00:00`), see [#718](https://github.com/kimai/kimai/pull/718)
|
||||
- API: changed from snake_case to camelCase (affected fields: hourlyRate vs hourly_rate / fixedRate vs fixed_rate / orderNumber vs order_number / i18n config object)
|
||||
- Plugin mechanism changed: existing Plugins have to be deleted or updated
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
New permissions are available:
|
||||
- `system_configuration` - for accessing the new system configuration screen
|
||||
- `system_actions` - for the experimental feature to flush your cache from the about screen
|
||||
- `plugins` - for accessing the new plugins screen
|
||||
|
||||
The setting `kimai.timesheet.mode` replaces the setting `kimai.timesheet.duration_only`. If you used the duration_only mode, you need to change your config:
|
||||
```yaml
|
||||
# Before
|
||||
kimai:
|
||||
timesheet:
|
||||
duration_only: true
|
||||
|
||||
# After
|
||||
kimai:
|
||||
timesheet:
|
||||
mode: duration_only
|
||||
```
|
||||
Or switch the mode directly in the new System configuration screen within Kimai.
|
||||
|
||||
## [0.8.1](https://github.com/kimai/kimai/releases/tag/0.8.1)
|
||||
|
||||
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
## [0.8](https://github.com/kimai/kimai/releases/tag/0.8)
|
||||
|
||||
After you followed the normal update and database migration process (see above), you need to execute a bash command to convert your timesheet data for timezone support:
|
||||
|
||||
- Read this [pull request](https://github.com/kimai/kimai/pull/372) BEFORE you follow the instructions to convert the
|
||||
timezones in your existing time records with `bin/console kimai:convert-timezone`. Without that, you will end up with wrong times in your database.
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
- A new boolean setting `kimai.timesheet.rules.allow_future_times` was introduced
|
||||
- New permissions are available:
|
||||
- `view_export` - for the new export feature
|
||||
- `create_export` - for the new export feature
|
||||
- `edit_export_own_timesheet` - for the new export feature
|
||||
- `edit_export_other_timesheet` - for the new export feature
|
||||
- `system_information` - to see the new about screen
|
||||
|
||||
## [0.7](https://github.com/kimai/kimai/releases/tag/0.7)
|
||||
|
||||
The configuration `kimai.theme.active_warning` was deprecated and should be replaced in your local.yaml,
|
||||
[read config docs for more information](https://www.kimai.org/documentation/timesheet.html#limit-active-entries).
|
||||
|
||||
## [0.6.1](https://github.com/kimai/kimai/releases/tag/0.6.1)
|
||||
|
||||
A bugfix release to address database compatibility issues with older MySQL/MariaDB versions.
|
||||
|
||||
## [0.6](https://github.com/kimai/kimai/releases/tag/0.6)
|
||||
|
||||
The API has some minor BC breaks: some fields were renamed and entities have a larger attribute set than collections.
|
||||
Be aware that the API is still is development mode and shouldn't be considered stable for now.
|
||||
|
||||
## [0.5](https://github.com/kimai/kimai/releases/tag/0.5)
|
||||
|
||||
Some configuration nodes were removed, if you have one of them in your `local.yaml` you need to delete them before you start the update:
|
||||
- `kimai.invoice.calculator`
|
||||
- `kimai.invoice.renderer`
|
||||
- `kimai.invoice.number_generator`
|
||||
|
||||
The new config `kimai.invoice.documents` was introduced, holding a list of directories ([read more](https://www.kimai.org/documentation/invoices.html)).
|
||||
|
||||
**BC break:** InvoiceTemplate name was changed from 255 characters to 60. If you used longer invoice-template names, they will be truncated when upgrading the database.
|
||||
Please make sure that they are unique in the first 60 character before you upgrade your database with `doctrine:migrations:migrate`.
|
||||
|
||||
## [0.4](https://github.com/kimai/kimai/releases/tag/0.4)
|
||||
|
||||
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
|
||||
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
|
||||
|
||||
```
|
||||
[ErrorException]
|
||||
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
|
||||
```
|
||||
|
||||
This can be fixed by updating Composer and Flex before executing the Kimai update:
|
||||
```
|
||||
sudo composer self-update
|
||||
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
|
||||
```
|
||||
|
||||
## [0.3](https://github.com/kimai/kimai/releases/tag/0.3)
|
||||
|
||||
You need to adjust your `.env` file and add your `from` address for [all emails](https://www.kimai.org/documentation/emails.html) generated by Kimai 2:
|
||||
```
|
||||
MAILER_FROM=kimai@example.com
|
||||
```
|
||||
|
||||
Create a file and database backup before executing the following steps:
|
||||
|
||||
```bash
|
||||
git pull origin master
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
bin/console doctrine:migrations:version --add 20180701120000
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
107
assets/app.js
Normal file
107
assets/app.js
Normal file
@@ -0,0 +1,107 @@
|
||||
// ------------------- 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/el');
|
||||
require('select2/dist/js/i18n/es');
|
||||
require('select2/dist/js/i18n/eu');
|
||||
require('select2/dist/js/i18n/fa');
|
||||
require('select2/dist/js/i18n/fi');
|
||||
require('select2/dist/js/i18n/fr');
|
||||
require('select2/dist/js/i18n/he');
|
||||
require('select2/dist/js/i18n/hr');
|
||||
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/nb');
|
||||
require('select2/dist/js/i18n/nl');
|
||||
require('select2/dist/js/i18n/pl');
|
||||
require('select2/dist/js/i18n/pt');
|
||||
require('select2/dist/js/i18n/pt-BR');
|
||||
require('select2/dist/js/i18n/ro');
|
||||
require('select2/dist/js/i18n/ru');
|
||||
require('select2/dist/js/i18n/sk');
|
||||
require('select2/dist/js/i18n/sv');
|
||||
require('select2/dist/js/i18n/tr');
|
||||
require('select2/dist/js/i18n/uk');
|
||||
require('select2/dist/js/i18n/vi');
|
||||
require('select2/dist/js/i18n/zh-CN');
|
||||
|
||||
const Moment = require('moment');
|
||||
global.moment = Moment;
|
||||
require('moment/locale/ar');
|
||||
require('moment/locale/cs');
|
||||
require('moment/locale/da');
|
||||
require('moment/locale/de');
|
||||
require('moment/locale/de-at');
|
||||
require('moment/locale/de-ch');
|
||||
require('moment/locale/el');
|
||||
require('moment/locale/eo');
|
||||
require('moment/locale/es');
|
||||
require('moment/locale/eu');
|
||||
require('moment/locale/fa');
|
||||
require('moment/locale/fi');
|
||||
require('moment/locale/fo');
|
||||
require('moment/locale/fr');
|
||||
require('moment/locale/he');
|
||||
require('moment/locale/hr');
|
||||
require('moment/locale/hu');
|
||||
require('moment/locale/it');
|
||||
require('moment/locale/ja');
|
||||
require('moment/locale/ko');
|
||||
require('moment/locale/nb');
|
||||
require('moment/locale/nl');
|
||||
require('moment/locale/pl');
|
||||
require('moment/locale/pt');
|
||||
require('moment/locale/pt-br');
|
||||
require('moment/locale/ro');
|
||||
require('moment/locale/ru');
|
||||
require('moment/locale/sk');
|
||||
require('moment/locale/sv');
|
||||
require('moment/locale/tr');
|
||||
require('moment/locale/uk');
|
||||
require('moment/locale/vi');
|
||||
require('moment/locale/zh-cn');
|
||||
|
||||
require('daterangepicker');
|
||||
|
||||
// ------ AdminLTE framework ------
|
||||
require('./sass/bootstrap.scss');
|
||||
require('./sass/fontawesome.scss');
|
||||
require('admin-lte/dist/css/AdminLTE.min.css');
|
||||
require('admin-lte/dist/css/skins/_all-skins.css');
|
||||
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte-extensions.scss');
|
||||
|
||||
global.$.AdminLTE = {};
|
||||
global.$.AdminLTE.options = {};
|
||||
require('admin-lte/dist/js/adminlte.min');
|
||||
// ------------------- INLINED ADMIN-LTE DEFINITIONS -------------------
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
require('./sass/app.scss');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./js/KimaiWebLoader.js');
|
||||
global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget').default;
|
||||
global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default;
|
||||
global.KimaiCookies = require('./js/widgets/KimaiCookies').default;
|
||||
global.KimaiStorage = require('./js/widgets/KimaiStorage').default;
|
||||
|
||||
// ------ Autocomplete for tags only ------
|
||||
require('jquery-ui/ui/widgets/autocomplete');
|
||||
41
assets/calendar.js
Normal file
41
assets/calendar.js
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
// can be removed, once fullcalendar was updated and works without draggable jquery ui objects
|
||||
require('jquery-ui/ui/widgets/draggable');
|
||||
|
||||
require('fullcalendar');
|
||||
require('fullcalendar/dist/gcal.min');
|
||||
|
||||
require('fullcalendar/dist/locale/ar');
|
||||
require('fullcalendar/dist/locale/cs');
|
||||
require('fullcalendar/dist/locale/da');
|
||||
require('fullcalendar/dist/locale/de');
|
||||
require('fullcalendar/dist/locale/de-at');
|
||||
require('fullcalendar/dist/locale/de-ch');
|
||||
require('fullcalendar/dist/locale/el');
|
||||
require('fullcalendar/dist/locale/es');
|
||||
require('fullcalendar/dist/locale/eu');
|
||||
require('fullcalendar/dist/locale/fa');
|
||||
require('fullcalendar/dist/locale/fi');
|
||||
require('fullcalendar/dist/locale/fr');
|
||||
require('fullcalendar/dist/locale/he');
|
||||
require('fullcalendar/dist/locale/hr');
|
||||
require('fullcalendar/dist/locale/hu');
|
||||
require('fullcalendar/dist/locale/it');
|
||||
require('fullcalendar/dist/locale/ja');
|
||||
require('fullcalendar/dist/locale/ko');
|
||||
require('fullcalendar/dist/locale/nb');
|
||||
require('fullcalendar/dist/locale/nl');
|
||||
require('fullcalendar/dist/locale/pl');
|
||||
require('fullcalendar/dist/locale/pt');
|
||||
require('fullcalendar/dist/locale/pt-br');
|
||||
require('fullcalendar/dist/locale/ro');
|
||||
require('fullcalendar/dist/locale/ru');
|
||||
require('fullcalendar/dist/locale/sk');
|
||||
require('fullcalendar/dist/locale/sv');
|
||||
require('fullcalendar/dist/locale/tr');
|
||||
require('fullcalendar/dist/locale/uk');
|
||||
require('fullcalendar/dist/locale/zh-cn');
|
||||
require('fullcalendar/dist/locale/vi');
|
||||
require('fullcalendar/dist/locale/en-gb'); // the last imported file is used as fallback for locales that do not exist (like EO)
|
||||
|
||||
require('fullcalendar/dist/fullcalendar.min.css');
|
||||
2
assets/chart.js
Normal file
2
assets/chart.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
require('chart.js/dist/Chart.min');
|
||||
1348
assets/css/emails.css
Normal file
1348
assets/css/emails.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
|
||||
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
|
||||
@import '~bootstrap-sass/assets/stylesheets/bootstrap';
|
||||
@import '~font-awesome/scss/font-awesome';
|
||||
@import '~daterangepicker/daterangepicker.css';
|
||||
@import '~bootstrap-select/dist/css/bootstrap-select.min.css';
|
||||
BIN
assets/images/signature.png
Normal file
BIN
assets/images/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
2
assets/invoice-pdf.js
Normal file
2
assets/invoice-pdf.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
require('./sass/invoice-pdf.scss');
|
||||
2
assets/invoice.js
Normal file
2
assets/invoice.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
require('./sass/invoice.scss');
|
||||
26
assets/js/KimaiConfiguration.js
Normal file
26
assets/js/KimaiConfiguration.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
91
assets/js/KimaiContainer.js
Normal file
91
assets/js/KimaiContainer.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
94
assets/js/KimaiLoader.js
Normal file
94
assets/js/KimaiLoader.js
Normal 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] 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";
|
||||
import KimaiDateUtils from "./plugins/KimaiDateUtils";
|
||||
import KimaiEscape from "./plugins/KimaiEscape";
|
||||
|
||||
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 KimaiEscape());
|
||||
kimai.registerPlugin(new KimaiEvent());
|
||||
kimai.registerPlugin(new KimaiAPI());
|
||||
kimai.registerPlugin(new KimaiAlert());
|
||||
kimai.registerPlugin(new KimaiDateUtils());
|
||||
kimai.registerPlugin(new KimaiFormSelect('.selectpicker'));
|
||||
kimai.registerPlugin(new KimaiConfirmationLink('confirmation-link'));
|
||||
kimai.registerPlugin(new KimaiActiveRecordsDuration());
|
||||
kimai.registerPlugin(new KimaiDatatableColumnView('data-column-visibility'));
|
||||
kimai.registerPlugin(new KimaiDateRangePicker('input[data-daterangepickerenable="on"]'));
|
||||
kimai.registerPlugin(new KimaiDateTimePicker('input[data-datetimepicker="on"]'));
|
||||
kimai.registerPlugin(new KimaiDatePicker('input[data-datepickerenable="on"]'));
|
||||
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
|
||||
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
|
||||
kimai.registerPlugin(new KimaiSelectDataAPI('select[data-related-select]'));
|
||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||
kimai.registerPlugin(new KimaiRecentActivities('li.notifications-menu'));
|
||||
kimai.registerPlugin(new KimaiActiveRecords('li.messages-menu', 'li.messages-menu-empty'));
|
||||
kimai.registerPlugin(new 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;
|
||||
}
|
||||
|
||||
}
|
||||
76
assets/js/KimaiPlugin.js
Normal file
76
assets/js/KimaiPlugin.js
Normal 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] 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 you call it BEFORE init().
|
||||
*
|
||||
* @returns {KimaiContainer}
|
||||
*/
|
||||
getContainer() {
|
||||
return this._core;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {(string|number|boolean)}
|
||||
*/
|
||||
getConfiguration(name) {
|
||||
return this.getContainer().getConfiguration().get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {KimaiPlugin}
|
||||
*/
|
||||
getPlugin(name) {
|
||||
return this.getContainer().getPlugin(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} title
|
||||
* @returns {string}
|
||||
*/
|
||||
escape(title) {
|
||||
return this.getPlugin('escape').escapeForHtml(title);
|
||||
};
|
||||
}
|
||||
37
assets/js/KimaiTranslation.js
Normal file
37
assets/js/KimaiTranslation.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* @param {Array<string, string>} translations
|
||||
*/
|
||||
constructor(translations) {
|
||||
this._translations = translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
get(name) {
|
||||
return this._translations[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(name) {
|
||||
return name in this._translations;
|
||||
}
|
||||
|
||||
}
|
||||
33
assets/js/KimaiWebLoader.js
Normal file
33
assets/js/KimaiWebLoader.js
Normal 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;
|
||||
|
||||
}));
|
||||
@@ -1,36 +0,0 @@
|
||||
// ------ jquery and bootstrap basics ------
|
||||
// create global $ and jQuery variables
|
||||
const $ = require('jquery');
|
||||
global.$ = global.jQuery = $;
|
||||
|
||||
require('jquery-ui');
|
||||
require('bootstrap-sass');
|
||||
require('jquery-slimscroll');
|
||||
require('bootstrap-select');
|
||||
|
||||
const Moment = require('moment');
|
||||
global.moment = Moment;
|
||||
require('daterangepicker');
|
||||
|
||||
// ------ for charts ------
|
||||
const Chart = require('../../vendor/almasaeed2010/adminlte/plugins/chartjs/Chart.min.js');
|
||||
global.Chart = Chart;
|
||||
|
||||
// ------ AdminLTE framework ------
|
||||
require('../css/vendor.scss');
|
||||
require('../../vendor/almasaeed2010/adminlte/dist/css/AdminLTE.min.css');
|
||||
require('../../vendor/almasaeed2010/adminlte/dist/css/skins/_all-skins.css');
|
||||
require('../css/kimai.scss');
|
||||
|
||||
global.$.AdminLTE = {};
|
||||
global.$.AdminLTE.options = {};
|
||||
require('../../vendor/almasaeed2010/adminlte/dist/js/app.js');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./kimai.js');
|
||||
require('./toolbar.js');
|
||||
require('../images/default_avatar.png');
|
||||
|
||||
// ------ icheck for enhanced radio buttins and checkboxes ------
|
||||
require('icheck');
|
||||
require('icheck/skins/square/blue.css');
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
if (typeof jQuery === 'undefined') {
|
||||
throw new Error('Kimai requires jQuery');
|
||||
}
|
||||
|
||||
/* kimai
|
||||
*
|
||||
* @type Object
|
||||
* @description $.kimai is the main object for the template's app.
|
||||
* It's used for implementing functions and options related
|
||||
* to the template. Keeping everything wrapped in an object
|
||||
* prevents conflict with other plugins and is a better
|
||||
* way to organize our code.
|
||||
*/
|
||||
$.kimai = {};
|
||||
|
||||
$(function() {
|
||||
"use strict";
|
||||
|
||||
$.kimai = {
|
||||
init: function(options) {
|
||||
if (typeof options !== 'undefined') {
|
||||
$.kimai.settings = $.extend({}, $.kimai.defaults, options);
|
||||
}
|
||||
|
||||
// ask before a delete call is executed
|
||||
$('a.btn-trash').click(function (event) {
|
||||
return confirm($.kimai.settings['confirmDelete']);
|
||||
});
|
||||
|
||||
// activate the dropdown functionality
|
||||
$('.dropdown-toggle').dropdown();
|
||||
|
||||
// auto hide success message after x seconds, as they are just meant as quick feedback and
|
||||
// not as a permanent source of information
|
||||
if ($.kimai.settings['alertSuccessAutoHide'] > 0) {
|
||||
setTimeout(
|
||||
function() {
|
||||
$('div.alert-success').alert('close');
|
||||
},
|
||||
$.kimai.settings['alertSuccessAutoHide']
|
||||
);
|
||||
}
|
||||
|
||||
$('input[data-datepicker="on"]').daterangepicker({
|
||||
singleDatePicker: true,
|
||||
showDropdowns: true,
|
||||
locale: {
|
||||
format: "YYYY-MM-DD",
|
||||
firstDay: 1
|
||||
}
|
||||
});
|
||||
|
||||
$('input[data-datetimepicker="on"]').daterangepicker({
|
||||
singleDatePicker: true,
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
cancelLabel: 'Clear',
|
||||
format: "YYYY-MM-DD HH:mm",
|
||||
firstDay: 1
|
||||
}
|
||||
});
|
||||
$('input[data-datetimepicker="on"]').on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format('YYYY-MM-DD HH:mm'));
|
||||
});
|
||||
|
||||
/*
|
||||
$('input').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
radioClass: 'iradio_square-blue',
|
||||
increaseArea: '20%' // optional
|
||||
});
|
||||
*/
|
||||
|
||||
// $.AdminLTE.pushMenu.expandOnHover();
|
||||
},
|
||||
pauseRecord: function(selector) {
|
||||
$(selector + ' .pull-left i').hover(function () {
|
||||
var link = $(this).parents('a');
|
||||
link.attr('href', link.attr('href').replace('/stop', '/pause'));
|
||||
$(this).removeClass('fa-stop-circle').addClass('fa-pause-circle').addClass('text-orange');
|
||||
},function () {
|
||||
var link = $(this).parents('a');
|
||||
link.attr('href', link.attr('href').replace('/pause', '/stop'));
|
||||
$(this).removeClass('fa-pause-circle').removeClass('text-orange').addClass('fa-stop-circle');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// default values
|
||||
$.kimai.defaults = {
|
||||
baseUrl: '/',
|
||||
imagePath: '/images',
|
||||
confirmDelete: 'Really delete?',
|
||||
alertSuccessAutoHide: 5000
|
||||
};
|
||||
|
||||
// once initialized, here are all values
|
||||
$.kimai.settings = {};
|
||||
|
||||
});
|
||||
|
||||
152
assets/js/plugins/KimaiAPI.js
Normal file
152
assets/js/plugins/KimaiAPI.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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
|
||||
});
|
||||
}
|
||||
|
||||
post(url, data, callbackSuccess, callbackError) {
|
||||
if (callbackError === null || callbackError === undefined) {
|
||||
callbackError = this.getPostErrorHandler();
|
||||
}
|
||||
|
||||
jQuery.ajax({
|
||||
url: url,
|
||||
headers: {
|
||||
'X-AUTH-SESSION': true,
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
success: callbackSuccess,
|
||||
error: callbackError
|
||||
});
|
||||
}
|
||||
|
||||
patch(url, data, callbackSuccess, callbackError) {
|
||||
if (callbackError === null || callbackError === undefined) {
|
||||
callbackError = this.getPatchErrorHandler();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (callbackError === null || callbackError === undefined) {
|
||||
callbackError = this.getDeleteErrorHandler();
|
||||
}
|
||||
|
||||
jQuery.ajax({
|
||||
url: url,
|
||||
headers: {
|
||||
'X-AUTH-SESSION': true,
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
success: callbackSuccess,
|
||||
error: callbackError
|
||||
});
|
||||
}
|
||||
|
||||
getDeleteErrorHandler() {
|
||||
const self = this;
|
||||
return function(xhr, err) {
|
||||
self.handleError('action.delete.error', xhr, err);
|
||||
};
|
||||
}
|
||||
|
||||
getPatchErrorHandler() {
|
||||
const self = this;
|
||||
return function(xhr, err) {
|
||||
self.handleError('action.update.error', xhr, err);
|
||||
};
|
||||
}
|
||||
|
||||
getPostErrorHandler() {
|
||||
const self = this;
|
||||
return function(xhr, err) {
|
||||
self.handleError('action.update.error', xhr, err);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {jqXHR} xhr
|
||||
* @param {string} err
|
||||
*/
|
||||
handleError(message, xhr, err) {
|
||||
let resultError = err;
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
resultError = xhr.responseJSON.message;
|
||||
// find validation errors
|
||||
if (xhr.status === 400 && xhr.responseJSON.errors) {
|
||||
let collected = ['<u>' + resultError + '</u>'];
|
||||
// form errors that are not attached to a field (like extra fields)
|
||||
if (xhr.responseJSON.errors.errors) {
|
||||
for (let error of xhr.responseJSON.errors.errors) {
|
||||
collected.push(error);
|
||||
}
|
||||
}
|
||||
if (xhr.responseJSON.errors.children) {
|
||||
for (let field in xhr.responseJSON.errors.children) {
|
||||
let tmpField = xhr.responseJSON.errors.children[field];
|
||||
if (tmpField.hasOwnProperty('errors') && tmpField.errors.length > 0) {
|
||||
for (let error of tmpField.errors) {
|
||||
collected.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collected.length > 0) {
|
||||
resultError = collected;
|
||||
}
|
||||
}
|
||||
} else if (xhr.status && xhr.statusText) {
|
||||
resultError = '[' + xhr.status + '] ' + xhr.statusText;
|
||||
}
|
||||
|
||||
this.getPlugin('alert').error(message, resultError);
|
||||
}
|
||||
|
||||
}
|
||||
98
assets/js/plugins/KimaiAPILink.js
Normal file
98
assets/js/plugins/KimaiAPILink.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import 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;
|
||||
}
|
||||
API.handleError(message, xhr, err);
|
||||
};
|
||||
|
||||
if (method === 'PATCH') {
|
||||
let data = {};
|
||||
if (attributes.payload) {
|
||||
data = attributes.payload;
|
||||
}
|
||||
API.patch(url, data, successHandle, errorHandle);
|
||||
} else if (method === 'POST') {
|
||||
let data = {};
|
||||
if (attributes.payload) {
|
||||
data = attributes.payload;
|
||||
}
|
||||
API.post(url, data, successHandle, errorHandle);
|
||||
} else if (method === 'DELETE') {
|
||||
API.delete(url, successHandle, errorHandle);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
125
assets/js/plugins/KimaiActiveRecords.js
Normal file
125
assets/js/plugins/KimaiActiveRecords.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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() {
|
||||
this.menu = document.querySelector(this.selector);
|
||||
|
||||
// the menu can be hidden if user has no permissions to see it
|
||||
if (this.menu === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attributes = this.menu.dataset;
|
||||
|
||||
const self = this;
|
||||
const handle = function() { self.reloadActiveRecords(); };
|
||||
|
||||
document.addEventListener('kimai.timesheetUpdate', handle);
|
||||
document.addEventListener('kimai.timesheetDelete', handle);
|
||||
document.addEventListener('kimai.activityUpdate', handle);
|
||||
document.addEventListener('kimai.activityDelete', handle);
|
||||
document.addEventListener('kimai.projectUpdate', handle);
|
||||
document.addEventListener('kimai.projectDelete', handle);
|
||||
document.addEventListener('kimai.customerUpdate', handle);
|
||||
document.addEventListener('kimai.customerDelete', handle);
|
||||
}
|
||||
|
||||
_toggleMenu(hasEntries) {
|
||||
this.menu.style.display = hasEntries ? 'inline-block' : 'none';
|
||||
if (!hasEntries) {
|
||||
// make sure that template entries in the menu are removed, otherwise they
|
||||
// might still be shown in the browsers title
|
||||
for (let record of this.menu.querySelectorAll('[data-since]')) {
|
||||
record.dataset['since'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
const menuEmpty = document.querySelector(this.selectorEmpty);
|
||||
if (menuEmpty !== null) {
|
||||
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
setEntries(entries) {
|
||||
this._toggleMenu(entries.length > 0);
|
||||
|
||||
const template = this.menu.querySelector('[data-template="active-record"]');
|
||||
|
||||
const label = this.menu.querySelector('a > span.label');
|
||||
if (label !== null) {
|
||||
label.innerText = entries.length === 0 ? '' : entries.length;
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (template === null) {
|
||||
this._replaceInNode(this.menu, entries[0]);
|
||||
} else {
|
||||
const container = template.parentElement;
|
||||
container.innerHTML = '';
|
||||
|
||||
for (let timesheet of entries) {
|
||||
const newNode = template.cloneNode(true);
|
||||
container.appendChild(this._replaceInNode(newNode, timesheet));
|
||||
}
|
||||
}
|
||||
|
||||
this.getContainer().getPlugin('timesheet-duration').updateRecords();
|
||||
}
|
||||
|
||||
_replaceInNode(node, timesheet) {
|
||||
const date = this.getContainer().getPlugin('date');
|
||||
const allReplacer = node.querySelectorAll('[data-replacer]');
|
||||
for (let node of allReplacer) {
|
||||
const replacerName = node.dataset['replacer'];
|
||||
if (replacerName === 'url') {
|
||||
node.href = this.attributes['href'].replace('000', timesheet.id);
|
||||
} else if (replacerName === 'activity') {
|
||||
node.innerText = timesheet.activity.name;
|
||||
} else if (replacerName === 'project') {
|
||||
node.innerText = timesheet.project.name;
|
||||
} else if (replacerName === 'customer') {
|
||||
node.innerText = timesheet.project.customer.name;
|
||||
} else if (replacerName === 'duration') {
|
||||
node.dataset['since'] = timesheet.begin;
|
||||
node.innerText = date.formatDuration(timesheet.duration);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
reloadActiveRecords() {
|
||||
const self = this;
|
||||
const API= this.getContainer().getPlugin('api');
|
||||
|
||||
API.get(this.attributes['api'], {}, function(result) {
|
||||
self.setEntries(result);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
78
assets/js/plugins/KimaiActiveRecordsDuration.js
Normal file
78
assets/js/plugins/KimaiActiveRecordsDuration.js
Normal 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] KimaiActiveRecordsDuration: activate the updates for all active timesheet records on this page
|
||||
*/
|
||||
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiActiveRecordsDuration extends KimaiPlugin {
|
||||
|
||||
getId() {
|
||||
return 'timesheet-duration';
|
||||
}
|
||||
|
||||
init() {
|
||||
this.updateBrowserTitle = !!this.getConfiguration('updateBrowserTitle');
|
||||
this.updateRecords();
|
||||
const self = this;
|
||||
const handle = function() { self.updateRecords(); };
|
||||
this._updatesHandler = setInterval(handle, 10000);
|
||||
// this will probably not work as expected, as other event-handler might need longer to update the DOM
|
||||
document.addEventListener('kimai.timesheetUpdate', handle);
|
||||
}
|
||||
|
||||
unregisterUpdates() {
|
||||
clearInterval(this._updatesHandler);
|
||||
}
|
||||
|
||||
updateRecords() {
|
||||
let durations = [];
|
||||
const activeRecords = document.querySelectorAll('[data-since]:not([data-since=""])');
|
||||
|
||||
if (activeRecords.length === 0) {
|
||||
if (this.updateBrowserTitle) {
|
||||
if (document.body.dataset['title'] === undefined) {
|
||||
this.updateBrowserTitle = false;
|
||||
} else {
|
||||
document.title = document.body.dataset['title'];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const DATE = this.getPlugin('date');
|
||||
|
||||
for (let record of activeRecords) {
|
||||
const since = record.dataset['since'];
|
||||
const duration = DATE.formatDuration(since);
|
||||
// only use the ones from the menu for the title
|
||||
if (record.dataset['replacer'] !== undefined && record.dataset['title'] !== null && duration !== '?') {
|
||||
durations.push(duration);
|
||||
}
|
||||
// but update all on the page (running entries in list pages)
|
||||
record.textContent = duration;
|
||||
}
|
||||
|
||||
if (durations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.updateBrowserTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
let title = durations.shift();
|
||||
let prefix = ' | ';
|
||||
|
||||
for (let duration of durations.slice(0, 2)) {
|
||||
title += prefix + duration;
|
||||
}
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
241
assets/js/plugins/KimaiAjaxModalForm.js
Normal file
241
assets/js/plugins/KimaiAjaxModalForm.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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 KimaiReducedClickHandler from "./KimaiReducedClickHandler";
|
||||
|
||||
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
|
||||
constructor(selector) {
|
||||
super();
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'modal';
|
||||
}
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
this.isDirty = false;
|
||||
|
||||
this.modal = jQuery('#remote_form_modal');
|
||||
this.modal
|
||||
.on('hide.bs.modal', function (e) {
|
||||
if (self.isDirty) {
|
||||
if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) {
|
||||
const msg = self.getContainer().getTranslation().get('modal.dirty');
|
||||
jQuery('#remote_form_modal .modal-body').prepend('<p class="'+(self.modal.hasClass('modal-danger') ? 'well well-sm ' : '') + 'text-danger small remote_modal_is_dirty_warning">' + msg + '</p>');
|
||||
}
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler);
|
||||
self.isDirty = false;
|
||||
self.getContainer().getPlugin('event').trigger('modal-hide');
|
||||
})
|
||||
.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('');
|
||||
})
|
||||
.on('show.bs.modal', function () {
|
||||
self.getContainer().getPlugin('event').trigger('modal-show');
|
||||
});
|
||||
|
||||
this._addClickHandler(this.selector, function(href) {
|
||||
self.openUrlInModal(href);
|
||||
});
|
||||
}
|
||||
|
||||
openUrlInModal(url, errorHandler) {
|
||||
const self = this;
|
||||
|
||||
if (errorHandler === undefined) {
|
||||
errorHandler = function(xhr, err) {
|
||||
if (xhr.status === undefined || xhr.status !== 403) {
|
||||
window.location = url;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
// Support changing modal importance/types
|
||||
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');
|
||||
}
|
||||
|
||||
// Support changing modal sizes
|
||||
let modalDialog = remoteModal.find('.modal-dialog');
|
||||
let largeModal = jQuery(html).find('.modal-dialog').hasClass('modal-lg');
|
||||
if (largeModal && !modalDialog.hasClass('modal-lg')) {
|
||||
modalDialog.addClass('modal-lg');
|
||||
}
|
||||
if (!largeModal && modalDialog.hasClass('modal-lg')) {
|
||||
modalDialog.removeClass('modal-lg');
|
||||
}
|
||||
|
||||
jQuery('#remote_form_modal .modal-content').replaceWith(
|
||||
jQuery(html).find('#form_modal .modal-content')
|
||||
);
|
||||
|
||||
jQuery('#remote_form_modal [data-dismiss=modal]').on('click', function() {
|
||||
self.isDirty = false;
|
||||
});
|
||||
|
||||
// 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/kimai/kimai/issues/618
|
||||
let enforceModalFocusFn = jQuery.fn.modal.Constructor.prototype.enforceFocus;
|
||||
jQuery.fn.modal.Constructor.prototype.enforceFocus = function() {};
|
||||
remoteModal.on('hidden.bs.modal', function () {
|
||||
jQuery.fn.modal.Constructor.prototype.enforceFocus = enforceModalFocusFn;
|
||||
});
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
remoteModal.modal('show');
|
||||
|
||||
// the new form that was loaded via ajax
|
||||
form = jQuery(formIdentifier);
|
||||
|
||||
this._isDirtyHandler = function(e) {
|
||||
self.isDirty = true;
|
||||
}
|
||||
form.on('change', this._isDirtyHandler);
|
||||
|
||||
// click handler for modal save button, to send forms via ajax
|
||||
form.on('submit', function(event) {
|
||||
// if the form has a target, we let the normal HTML flow happen
|
||||
if (form.attr('target') !== undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise we do some AJAX magic to process the form in the background
|
||||
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';
|
||||
}
|
||||
self.isDirty = false;
|
||||
remoteModal.modal('hide');
|
||||
alert.success(msg);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
let message = form.attr('data-msg-error');
|
||||
if (message === null || message === undefined) {
|
||||
message = 'action.update.error';
|
||||
}
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
err = xhr.responseJSON.message;
|
||||
} else if (xhr.status && xhr.statusText) {
|
||||
err = '[' + xhr.status +'] ' + xhr.statusText;
|
||||
}
|
||||
alert.error(message, err);
|
||||
// this is useful for changing form fields and retrying to save (and in development to test form changes)
|
||||
setTimeout(function() {
|
||||
btn.button('reset');
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
116
assets/js/plugins/KimaiAlert.js
Normal file
116
assets/js/plugins/KimaiAlert.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} title
|
||||
* @param {string|array} message
|
||||
*/
|
||||
error(title, message) {
|
||||
const translation = this.getContainer().getTranslation();
|
||||
if (translation.has(title)) {
|
||||
title = translation.get(title);
|
||||
}
|
||||
if (translation.has(message)) {
|
||||
message = translation.get(message);
|
||||
}
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: title.replace('%reason%', ''),
|
||||
html: message.join('<br>'),
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: title.replace('%reason%', ''),
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
this._show('warning', message);
|
||||
}
|
||||
|
||||
success(message) {
|
||||
this._toast('success', message);
|
||||
}
|
||||
|
||||
info(message) {
|
||||
this._show('info', message);
|
||||
}
|
||||
|
||||
_show(type, message) {
|
||||
const translation = this.getContainer().getTranslation();
|
||||
|
||||
if (translation.has(message)) {
|
||||
message = translation.get(message);
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
icon: type,
|
||||
title: message,
|
||||
});
|
||||
}
|
||||
|
||||
_toast(type, message) {
|
||||
const translation = this.getContainer().getTranslation();
|
||||
|
||||
if (translation.has(message)) {
|
||||
message = translation.get(message);
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
timer: 2000,
|
||||
timerProgressBar: true,
|
||||
toast: true,
|
||||
position: 'top',
|
||||
showConfirmButton: false,
|
||||
icon: type,
|
||||
title: message,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
31
assets/js/plugins/KimaiAlternativeLinks.js
Normal file
31
assets/js/plugins/KimaiAlternativeLinks.js
Normal 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 KimaiReducedClickHandler from "./KimaiReducedClickHandler";
|
||||
|
||||
export default class KimaiAlternativeLinks extends KimaiReducedClickHandler {
|
||||
|
||||
constructor(selector) {
|
||||
super();
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
init() {
|
||||
this._addClickHandler(this.selector, function(href) {
|
||||
window.location = href;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
106
assets/js/plugins/KimaiAutocomplete.js
Normal file
106
assets/js/plugins/KimaiAutocomplete.js
Normal 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.getConfiguration('autoComplete');
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'autocomplete';
|
||||
}
|
||||
|
||||
splitTagList(val) {
|
||||
return val.split(/,\s*/);
|
||||
}
|
||||
|
||||
extractLastTag(term) {
|
||||
return this.splitTagList(term).pop();
|
||||
}
|
||||
|
||||
activateAutocomplete(selector) {
|
||||
const self = this;
|
||||
|
||||
jQuery(selector + ' ' + this.selector).each(function(index) {
|
||||
const currentField = jQuery(this);
|
||||
const apiUrl = currentField.attr('data-autocomplete-url');
|
||||
const API = self.getContainer().getPlugin('api');
|
||||
|
||||
currentField
|
||||
// don't navigate away from the field on tab when selecting an item
|
||||
.on("keydown", function (event) {
|
||||
if (event.keyCode === jQuery.ui.keyCode.TAB &&
|
||||
jQuery(this).autocomplete("instance").menu.active) {
|
||||
event.preventDefault();
|
||||
}
|
||||
})
|
||||
.autocomplete({
|
||||
source: function (request, response) {
|
||||
const lastEntry = self.extractLastTag(request.term);
|
||||
API.get(apiUrl, {'name': lastEntry}, function(data){
|
||||
response(data);
|
||||
});
|
||||
},
|
||||
search: function () {
|
||||
// custom minLength
|
||||
var term = self.extractLastTag(this.value);
|
||||
if (term.length < self.minChars) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
focus: function () {
|
||||
// prevent value inserted on focus
|
||||
return false;
|
||||
},
|
||||
select: function (event, ui) {
|
||||
var terms = self.splitTagList(this.value);
|
||||
|
||||
// remove the current input
|
||||
terms.pop();
|
||||
|
||||
// check if selected tag is already in list
|
||||
if (!terms.includes(ui.item.value)) {
|
||||
// add the selected item
|
||||
terms.push(ui.item.value);
|
||||
}
|
||||
// add placeholder to get the comma-and-space at the end
|
||||
terms.push("");
|
||||
|
||||
this.value = terms.join(", ");
|
||||
|
||||
$(this).trigger('change');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
)
|
||||
;
|
||||
});
|
||||
}
|
||||
|
||||
destroyAutocomplete(selector) {
|
||||
jQuery(selector + ' ' + this.selector).each(function(index) {
|
||||
const currentField = jQuery(this);
|
||||
currentField.autocomplete("destroy");
|
||||
currentField.removeData('autocomplete');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
70
assets/js/plugins/KimaiConfirmationLink.js
Normal file
70
assets/js/plugins/KimaiConfirmationLink.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
106
assets/js/plugins/KimaiDatatable.js
Normal file
106
assets/js/plugins/KimaiDatatable.js
Normal 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.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [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'];
|
||||
|
||||
this.fixDropdowns();
|
||||
|
||||
if (events === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const handle = function() { self.reloadDatatable(); };
|
||||
|
||||
for (let eventName of events.split(' ')) {
|
||||
document.addEventListener(eventName, handle);
|
||||
}
|
||||
|
||||
document.addEventListener('pagination-change', handle);
|
||||
document.addEventListener('filter-change', handle);
|
||||
}
|
||||
|
||||
reloadDatatable() {
|
||||
const self = this;
|
||||
const contentArea = this.contentArea;
|
||||
const durations = this.getContainer().getPlugin('timesheet-duration');
|
||||
const toolbarSelector = this.getContainer().getPlugin('toolbar').getSelector();
|
||||
|
||||
const form = jQuery(toolbarSelector);
|
||||
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
|
||||
jQuery(contentArea).append(loading);
|
||||
|
||||
// remove the empty fields to prevent errors
|
||||
let formData = jQuery(toolbarSelector + ' :input')
|
||||
.filter(function(index, element) {
|
||||
return jQuery(element).val() !== '';
|
||||
})
|
||||
.serialize();
|
||||
|
||||
jQuery.ajax({
|
||||
url: form.attr('action'),
|
||||
type: form.attr('method'),
|
||||
data: formData,
|
||||
success: function(html) {
|
||||
jQuery(contentArea).replaceWith(
|
||||
jQuery(html).find(contentArea)
|
||||
);
|
||||
durations.updateRecords();
|
||||
self.fixDropdowns();
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* show dropdown menu upwards, if it is outside the visible viewport
|
||||
*/
|
||||
fixDropdowns() {
|
||||
const docHeight = jQuery(document).height();
|
||||
jQuery(this.selector + ' [data-toggle=dropdown]').each(function() {
|
||||
const parent = jQuery(this).parent();
|
||||
const menu = parent.find('.dropdown-menu');
|
||||
|
||||
if (parent && menu) {
|
||||
if ((parent.offset().top + parent.outerHeight() + menu.outerHeight()) > docHeight) {
|
||||
parent.addClass('dropup').removeClass('dropdown');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
110
assets/js/plugins/KimaiDatatableColumnView.js
Normal file
110
assets/js/plugins/KimaiDatatableColumnView.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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'), checkbox.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
saveVisibility() {
|
||||
const form = this.modal.getElementsByTagName('form')[0];
|
||||
let settings = {};
|
||||
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
|
||||
settings[checkbox.getAttribute('name')] = checkbox.checked;
|
||||
}
|
||||
Cookies.set(form.getAttribute('name'), JSON.stringify(settings), {expires: 365, SameSite: 'Strict'});
|
||||
jQuery(this.modal).modal('toggle');
|
||||
}
|
||||
|
||||
resetVisibility() {
|
||||
const form = this.modal.getElementsByTagName('form')[0];
|
||||
Cookies.remove(form.getAttribute('name'));
|
||||
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
|
||||
if (!checkbox.checked) {
|
||||
checkbox.click();
|
||||
}
|
||||
}
|
||||
jQuery(this.modal).modal('toggle');
|
||||
}
|
||||
|
||||
changeVisibility(columnName, checked) {
|
||||
const tables = document.getElementsByClassName('datatable_' + this.id);
|
||||
for (let tableBox of tables) {
|
||||
let column = 0;
|
||||
let foundColumn = false;
|
||||
let table = tableBox.getElementsByClassName('dataTable')[0];
|
||||
for (let columnElement of table.getElementsByTagName('th')) {
|
||||
if (columnElement.getAttribute('data-field') === columnName) {
|
||||
foundColumn = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (columnElement.getAttribute('colspan') !== null) {
|
||||
console.log('Tables with colspans are not supported!');
|
||||
}
|
||||
|
||||
column++;
|
||||
}
|
||||
|
||||
if (!foundColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let rowElement of table.getElementsByTagName('tr')) {
|
||||
if (rowElement.children[column] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
rowElement.children[column].classList.remove('hidden');
|
||||
} else {
|
||||
rowElement.children[column].classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
72
assets/js/plugins/KimaiDatePicker.js
Normal file
72
assets/js/plugins/KimaiDatePicker.js
Normal 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] KimaiDatePicker: single date selects (currently unused)
|
||||
*/
|
||||
|
||||
import jQuery from 'jquery';
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiDatePicker extends KimaiPlugin {
|
||||
|
||||
constructor(selector) {
|
||||
super();
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'date-picker';
|
||||
}
|
||||
|
||||
activateDatePicker(selector) {
|
||||
const TRANSLATE = this.getContainer().getTranslation();
|
||||
const DATE_UTILS = this.getContainer().getPlugin('date');
|
||||
const firstDow = this.getConfiguration('first_dow_iso') % 7;
|
||||
|
||||
jQuery(selector + ' ' + this.selector).each(function(index) {
|
||||
let localeFormat = jQuery(this).data('format');
|
||||
jQuery(this).daterangepicker({
|
||||
singleDatePicker: true,
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
drops: 'down',
|
||||
locale: {
|
||||
format: localeFormat,
|
||||
firstDay: firstDow,
|
||||
applyLabel: TRANSLATE.get('confirm'),
|
||||
cancelLabel: TRANSLATE.get('cancel'),
|
||||
customRangeLabel: TRANSLATE.get('customRange'),
|
||||
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
|
||||
monthNames: DATE_UTILS.getMonthNames(),
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(this).on('show.daterangepicker', function (ev, picker) {
|
||||
if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) {
|
||||
// "up" is not possible here, because the code is triggered on many mobile phones and the picker then appears out of window
|
||||
picker.drops = 'auto';
|
||||
picker.move();
|
||||
}
|
||||
});
|
||||
|
||||
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).data('daterangepicker').remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
95
assets/js/plugins/KimaiDateRangePicker.js
Normal file
95
assets/js/plugins/KimaiDateRangePicker.js
Normal 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.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiDateRangePicker: activate the (daterange picker) compound field in toolbar
|
||||
*/
|
||||
|
||||
import jQuery from 'jquery';
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class KimaiDateRangePicker extends KimaiPlugin {
|
||||
|
||||
constructor(selector) {
|
||||
super();
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'date-range-picker';
|
||||
}
|
||||
|
||||
activateDateRangePicker(selector) {
|
||||
const TRANSLATE = this.getContainer().getTranslation();
|
||||
const DATE_UTILS = this.getContainer().getPlugin('date');
|
||||
const firstDow = this.getConfiguration('first_dow_iso') % 7;
|
||||
|
||||
jQuery(selector + ' ' + this.selector).each(function(index) {
|
||||
let localeFormat = jQuery(this).data('format');
|
||||
let separator = jQuery(this).data('separator');
|
||||
let rangesList = {};
|
||||
|
||||
rangesList[TRANSLATE.get('today')] = [moment(), moment()];
|
||||
rangesList[TRANSLATE.get('yesterday')] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
|
||||
rangesList[TRANSLATE.get('thisWeek')] = [moment().startOf('isoWeek'), moment().endOf('isoWeek')];
|
||||
rangesList[TRANSLATE.get('lastWeek')] = [moment().subtract(1, 'week').startOf('isoWeek'), moment().subtract(1, 'week').endOf('isoWeek')];
|
||||
if (firstDow === 0) { // sunday = 0
|
||||
rangesList[TRANSLATE.get('thisWeek')] = [moment().startOf('isoWeek').subtract(1, 'day'), moment().endOf('isoWeek').subtract(1, 'day')];
|
||||
rangesList[TRANSLATE.get('lastWeek')] = [moment().subtract(1, 'week').startOf('isoWeek').subtract(1, 'day'), moment().subtract(1, 'week').endOf('isoWeek').subtract(1, 'day')];
|
||||
}
|
||||
rangesList[TRANSLATE.get('thisMonth')] = [moment().startOf('month'), moment().endOf('month')];
|
||||
rangesList[TRANSLATE.get('lastMonth')] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')];
|
||||
rangesList[TRANSLATE.get('thisYear')] = [moment().startOf('year'), moment().endOf('year')];
|
||||
rangesList[TRANSLATE.get('lastYear')] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')];
|
||||
|
||||
jQuery(this).daterangepicker({
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
autoApply: false,
|
||||
linkedCalendars: true,
|
||||
drops: 'down',
|
||||
locale: {
|
||||
separator: separator,
|
||||
format: localeFormat,
|
||||
firstDay: firstDow,
|
||||
applyLabel: TRANSLATE.get('confirm'),
|
||||
cancelLabel: TRANSLATE.get('cancel'),
|
||||
customRangeLabel: TRANSLATE.get('customRange'),
|
||||
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
|
||||
monthNames: DATE_UTILS.getMonthNames(),
|
||||
},
|
||||
ranges: rangesList,
|
||||
alwaysShowCalendars: true
|
||||
});
|
||||
|
||||
jQuery(this).on('show.daterangepicker', function (ev, picker) {
|
||||
if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) {
|
||||
// "up" is not possible here, because the code is triggered on many mobile phones and the picker then appears out of window
|
||||
picker.drops = 'auto';
|
||||
picker.move();
|
||||
}
|
||||
});
|
||||
|
||||
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).data('daterangepicker').remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
75
assets/js/plugins/KimaiDateTimePicker.js
Normal file
75
assets/js/plugins/KimaiDateTimePicker.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiDateTimePicker: activate the (datetime picker) field in timesheet edit dialog
|
||||
*/
|
||||
|
||||
import jQuery from 'jquery';
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiDateTimePicker extends KimaiPlugin {
|
||||
|
||||
constructor(selector) {
|
||||
super();
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'date-time-picker';
|
||||
}
|
||||
|
||||
activateDateTimePicker(selector) {
|
||||
const TRANSLATE = this.getContainer().getTranslation();
|
||||
const DATE_UTILS = this.getContainer().getPlugin('date');
|
||||
const firstDow = this.getConfiguration('first_dow_iso') % 7;
|
||||
const is24hours = this.getConfiguration('twentyFourHours');
|
||||
|
||||
jQuery(selector + ' ' + this.selector).each(function(index) {
|
||||
let localeFormat = jQuery(this).data('format');
|
||||
jQuery(this).daterangepicker({
|
||||
singleDatePicker: true,
|
||||
timePicker: true,
|
||||
timePicker24Hour: is24hours,
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
drops: 'down',
|
||||
locale: {
|
||||
format: localeFormat,
|
||||
firstDay: firstDow,
|
||||
applyLabel: TRANSLATE.get('confirm'),
|
||||
cancelLabel: TRANSLATE.get('cancel'),
|
||||
customRangeLabel: TRANSLATE.get('customRange'),
|
||||
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
|
||||
monthNames: DATE_UTILS.getMonthNames(),
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(this).on('show.daterangepicker', function (ev, picker) {
|
||||
if (picker.element.offset().top - jQuery(window).scrollTop() + picker.container.outerHeight() + 30 > jQuery(window).height()) {
|
||||
// "up" is not possible here, because the code is triggered on many mobile phones and the picker then appears out of window
|
||||
picker.drops = 'auto';
|
||||
picker.move();
|
||||
}
|
||||
});
|
||||
|
||||
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).data('daterangepicker').remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
114
assets/js/plugins/KimaiDateUtils.js
Normal file
114
assets/js/plugins/KimaiDateUtils.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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] KimaiDateUtils: responsible for handling date specific tasks
|
||||
*/
|
||||
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class KimaiDateUtils extends KimaiPlugin {
|
||||
|
||||
getId() {
|
||||
return 'date';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateTime
|
||||
* @returns {string}
|
||||
*/
|
||||
getFormattedDate(dateTime) {
|
||||
return moment(dateTime).format(this.getConfiguration('formatDate'));
|
||||
}
|
||||
|
||||
getWeekDaysShort() {
|
||||
return moment.localeData().weekdaysShort();
|
||||
}
|
||||
|
||||
getMonthNames() {
|
||||
return moment.localeData().months();
|
||||
}
|
||||
|
||||
formatDuration(since) {
|
||||
const duration = moment.duration(moment(new Date()).diff(moment(since)));
|
||||
|
||||
return this.formatMomentDuration(duration);
|
||||
}
|
||||
|
||||
formatSeconds(seconds) {
|
||||
const duration = moment.duration('PT' + seconds + 'S');
|
||||
|
||||
return this.formatMomentDuration(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {moment.Duration} duration
|
||||
* @returns {string|*}
|
||||
*/
|
||||
formatMomentDuration(duration) {
|
||||
const hours = parseInt(duration.asHours());
|
||||
const minutes = duration.minutes();
|
||||
|
||||
return this.formatTime(hours, minutes);
|
||||
}
|
||||
|
||||
formatTime(hours, minutes) {
|
||||
let format = this.getConfiguration('formatDuration');
|
||||
|
||||
if (hours < 0 || minutes < 0) {
|
||||
hours = Math.abs(hours);
|
||||
minutes = Math.abs(minutes);
|
||||
if (minutes > 0 || hours > 0) {
|
||||
format = '-' + format;
|
||||
}
|
||||
}
|
||||
|
||||
// special case for hours, as they can overflow the 24h barrier - Kimai does not support days as duration unit
|
||||
if (hours < 10) {
|
||||
hours = '0' + hours;
|
||||
}
|
||||
|
||||
|
||||
return format.replace('%h', hours).replace('%m', ('0' + minutes).substr(-2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} duration
|
||||
* @returns {int}
|
||||
*/
|
||||
getSecondsFromDurationString(duration)
|
||||
{
|
||||
duration = duration.trim().toUpperCase();
|
||||
let momentDuration = moment.duration(NaN);
|
||||
|
||||
if (duration.indexOf(':') !== -1) {
|
||||
momentDuration = moment.duration(duration);
|
||||
} else if (duration.indexOf('.') !== -1 || duration.indexOf(',') !== -1) {
|
||||
duration = duration.replace(/,/, '.');
|
||||
duration = (parseFloat(duration) * 3600).toString();
|
||||
momentDuration = moment.duration('PT' + duration + 'S');
|
||||
} else if (duration.indexOf('H') !== -1 || duration.indexOf('M') !== -1 || duration.indexOf('S') !== -1) {
|
||||
/* D for days does not work, because 'PT1H' but with days 'P1D' is used */
|
||||
momentDuration = moment.duration('PT' + duration);
|
||||
} else {
|
||||
let c = parseInt(duration);
|
||||
let d = parseInt(duration).toFixed();
|
||||
if (!isNaN(c) && duration === d) {
|
||||
duration = (c * 3600).toString();
|
||||
momentDuration = moment.duration('PT' + duration + 'S');
|
||||
}
|
||||
}
|
||||
|
||||
if (!momentDuration.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return momentDuration.asSeconds();
|
||||
}
|
||||
|
||||
}
|
||||
35
assets/js/plugins/KimaiEscape.js
Normal file
35
assets/js/plugins/KimaiEscape.js
Normal 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] KimaiEscape: sanitize strings
|
||||
*/
|
||||
|
||||
import KimaiPlugin from "../KimaiPlugin";
|
||||
|
||||
export default class KimaiEscape extends KimaiPlugin {
|
||||
|
||||
getId() {
|
||||
return 'escape';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} title
|
||||
* @returns {string}
|
||||
*/
|
||||
escapeForHtml(title) {
|
||||
const tagsToReplace = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
|
||||
return title.replace(/[&<>]/g, function(tag) {
|
||||
return tagsToReplace[tag] || tag;
|
||||
});
|
||||
};
|
||||
}
|
||||
33
assets/js/plugins/KimaiEvent.js
Normal file
33
assets/js/plugins/KimaiEvent.js
Normal 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(event, {detail: details});
|
||||
}
|
||||
document.dispatchEvent(triggerEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
assets/js/plugins/KimaiForm.js
Normal file
56
assets/js/plugins/KimaiForm.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
* @param {Object} overwrites
|
||||
* @returns {string}
|
||||
*/
|
||||
convertFormDataToQueryString(form, overwrites = {})
|
||||
{
|
||||
let serialized = [];
|
||||
let data = new FormData(form);
|
||||
|
||||
for (const key in overwrites) {
|
||||
data.set(key, overwrites[key]);
|
||||
}
|
||||
|
||||
for (let row of data) {
|
||||
serialized.push(encodeURIComponent(row[0]) + "=" + encodeURIComponent(row[1]));
|
||||
}
|
||||
|
||||
return serialized.join('&');
|
||||
}
|
||||
}
|
||||
298
assets/js/plugins/KimaiFormSelect.js
Normal file
298
assets/js/plugins/KimaiFormSelect.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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';
|
||||
}
|
||||
|
||||
init() {
|
||||
// selects the original value inside select2 dropdowns, as the "reset" event (the updated option)
|
||||
// is not automatically catched by select2
|
||||
jQuery('body').on('reset', 'form', function(event) {
|
||||
setTimeout(function() {
|
||||
jQuery(event.target).find(this.selector).trigger('change');
|
||||
}, 10);
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
||||
// 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
|
||||
this.matcher = function (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 = self.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;
|
||||
}
|
||||
}
|
||||
|
||||
activateSelectPickerByElement(node, container) {
|
||||
let options = {};
|
||||
if (container !== undefined) {
|
||||
options = {
|
||||
dropdownParent: jQuery(container),
|
||||
};
|
||||
}
|
||||
|
||||
options = {...options, ...{
|
||||
language: this.getConfiguration('locale').replace('_', '-'),
|
||||
theme: "bootstrap",
|
||||
matcher: this.matcher,
|
||||
dropdownAutoWidth: true,
|
||||
width: "resolve"
|
||||
}};
|
||||
|
||||
const element = jQuery(node);
|
||||
|
||||
if (node.dataset['renderer'] !== undefined && node.dataset['renderer'] === 'color') {
|
||||
const templateResultFunc = function (state) {
|
||||
return jQuery('<span><span style="background-color:'+state.id+'; width: 20px; height: 20px; display: inline-block; margin-right: 10px;"> </span>' + state.text + '</span>');
|
||||
};
|
||||
|
||||
const colorOptions = {...options, ...{
|
||||
templateSelection: templateResultFunc,
|
||||
templateResult: templateResultFunc
|
||||
}};
|
||||
|
||||
element.select2(colorOptions);
|
||||
} else {
|
||||
element.select2(options);
|
||||
}
|
||||
|
||||
// this is a bugfix for safari, which does render the dropdown only with correct width upon the second opening
|
||||
// see https://github.com/select2/select2/issues/4678
|
||||
element.on('select2:open', function (ev) {
|
||||
if (element.data('performing-reopen') === undefined || element.data('performing-reopen') === null) {
|
||||
element.data('performing-reopen', true);
|
||||
element.select2('close');
|
||||
element.select2('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
activateSelectPicker(selector, container) {
|
||||
const self = this;
|
||||
jQuery(selector + ' ' + this.selector).each(function(i, el) {
|
||||
self.activateSelectPickerByElement(el, container);
|
||||
});
|
||||
}
|
||||
|
||||
destroySelectPicker(selector) {
|
||||
jQuery(selector + ' ' + this.selector).select2('destroy');
|
||||
}
|
||||
|
||||
updateOptions(selectIdentifier, data) {
|
||||
let select = jQuery(selectIdentifier);
|
||||
let emptyOption = jQuery(selectIdentifier + ' option[value=""]');
|
||||
const selectedValue = select.val();
|
||||
|
||||
select.find('option').remove().end().find('optgroup').remove().end();
|
||||
|
||||
if (emptyOption.length !== 0) {
|
||||
select.append(this._createOption(emptyOption.text(), ''));
|
||||
}
|
||||
|
||||
let emptyOpts = [];
|
||||
let options = [];
|
||||
let titlePattern = null;
|
||||
if (select[0] !== undefined && select[0].dataset !== undefined && select[0].dataset['optionPattern'] !== undefined) {
|
||||
titlePattern = select[0].dataset['optionPattern'];
|
||||
}
|
||||
if (titlePattern === null || titlePattern === '') {
|
||||
titlePattern = '{name}';
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (key === '__empty__') {
|
||||
for (const entity of value) {
|
||||
emptyOpts.push(this._createOption(this._getTitleFromPattern(titlePattern, entity), entity.id));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let optGroup = this._createOptgroup(key);
|
||||
for (const entity of value) {
|
||||
optGroup.appendChild(this._createOption(this._getTitleFromPattern(titlePattern, entity), entity.id));
|
||||
}
|
||||
options.push(optGroup);
|
||||
}
|
||||
|
||||
select.append(options);
|
||||
select.append(emptyOpts);
|
||||
|
||||
// if available, re-select the previous selected option (mostly usable for global activities)
|
||||
select.val(selectedValue);
|
||||
|
||||
// pre-select an option if it is the only available one
|
||||
if (select.val() === '' || select.val() === null) {
|
||||
const allOptions = select.find('option');
|
||||
const optionLength = allOptions.length;
|
||||
let selectOption = '';
|
||||
|
||||
if (optionLength === 1) {
|
||||
selectOption = allOptions[0].value;
|
||||
} else if (optionLength === 2 && emptyOption.length === 1) {
|
||||
selectOption = allOptions[1].value;
|
||||
}
|
||||
|
||||
if (selectOption !== '') {
|
||||
select.val(selectOption);
|
||||
}
|
||||
}
|
||||
|
||||
// if we don't trigger the change, the other selects won't reset
|
||||
select.trigger('change');
|
||||
|
||||
// if select2 is active, this will tell the select to refresh
|
||||
if (select.hasClass('selectpicker')) {
|
||||
select.trigger('change.select2');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pattern
|
||||
* @param {array} entity
|
||||
* @private
|
||||
*/
|
||||
_getTitleFromPattern(pattern, entity) {
|
||||
const DATE_UTILS = this.getPlugin('date');
|
||||
const regexp = new RegExp('{[^}]*?}','g');
|
||||
let title = pattern;
|
||||
let match = null;
|
||||
|
||||
while ((match = regexp.exec(pattern)) !== null) {
|
||||
const field = match[0].substr(1, match[0].length - 2);
|
||||
let value = entity[field] === undefined ? null : entity[field];
|
||||
if ((field === 'start' || field === 'end')) {
|
||||
if (value === null) {
|
||||
value = '?';
|
||||
} else {
|
||||
value = DATE_UTILS.getFormattedDate(value);
|
||||
}
|
||||
}
|
||||
|
||||
title = title.replace(new RegExp('{' + field + '}', 'g'), value ?? '');
|
||||
}
|
||||
title = title.replace(/- \?-\?/, '');
|
||||
title = title.replace(/\r\n|\r|\n/g, ' ');
|
||||
title = title.substr(0, 110);
|
||||
|
||||
const chars = '- ';
|
||||
let start = 0, end = title.length;
|
||||
|
||||
while (start < end && chars.indexOf(title[start]) >= 0) {
|
||||
++start;
|
||||
}
|
||||
|
||||
while (end > start && chars.indexOf(title[end - 1]) >= 0) {
|
||||
--end;
|
||||
}
|
||||
|
||||
return (start > 0 || end < title.length) ? title.substring(start, end) : title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
* @param {string} value
|
||||
* @returns {HTMLElement}
|
||||
* @private
|
||||
*/
|
||||
_createOption(label, value) {
|
||||
let option = document.createElement('option');
|
||||
option.innerText = label;
|
||||
option.value = value;
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
* @returns {HTMLElement}
|
||||
* @private
|
||||
*/
|
||||
_createOptgroup(label) {
|
||||
let optGroup = document.createElement('optgroup');
|
||||
optGroup.label = label;
|
||||
return optGroup;
|
||||
}
|
||||
}
|
||||
73
assets/js/plugins/KimaiMultiUpdateTable.js
Normal file
73
assets/js/plugins/KimaiMultiUpdateTable.js
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
41
assets/js/plugins/KimaiPauseRecord.js
Normal file
41
assets/js/plugins/KimaiPauseRecord.js
Normal 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');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
93
assets/js/plugins/KimaiRecentActivities.js
Normal file
93
assets/js/plugins/KimaiRecentActivities.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiRecentActivities: responsible to reload the users recent activities
|
||||
*/
|
||||
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiRecentActivities extends KimaiPlugin {
|
||||
|
||||
constructor(selector) {
|
||||
super();
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'recent-activities';
|
||||
}
|
||||
|
||||
init() {
|
||||
const menu = document.querySelector(this.selector);
|
||||
// the menu can be hidden if user has no permissions to see it
|
||||
if (menu === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdown = menu.querySelector('ul.dropdown-menu');
|
||||
|
||||
this.attributes = dropdown.dataset;
|
||||
this.itemList = dropdown.querySelector('li > ul.menu');
|
||||
|
||||
const self = this;
|
||||
const handle = function() { self.reloadRecentActivities(); };
|
||||
|
||||
// don't block initial browser rendering
|
||||
setTimeout(handle, 500);
|
||||
|
||||
document.addEventListener('kimai.recentActivities', handle);
|
||||
document.addEventListener('kimai.timesheetUpdate', handle);
|
||||
document.addEventListener('kimai.timesheetDelete', handle);
|
||||
document.addEventListener('kimai.activityUpdate', handle);
|
||||
document.addEventListener('kimai.activityDelete', handle);
|
||||
document.addEventListener('kimai.projectUpdate', handle);
|
||||
document.addEventListener('kimai.projectDelete', handle);
|
||||
document.addEventListener('kimai.customerUpdate', handle);
|
||||
document.addEventListener('kimai.customerDelete', handle);
|
||||
}
|
||||
|
||||
emptyList() {
|
||||
this.itemList.innerHTML = '';
|
||||
}
|
||||
|
||||
setEntries(entries) {
|
||||
if (entries.length === 0) {
|
||||
this.emptyList();
|
||||
return;
|
||||
}
|
||||
|
||||
let htmlToInsert = '';
|
||||
|
||||
for (let timesheet of entries) {
|
||||
let label = this.attributes['template']
|
||||
.replace('%customer%', this.escape(timesheet.project.customer.name))
|
||||
.replace('%project%', this.escape(timesheet.project.name))
|
||||
.replace('%activity%', this.escape(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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
53
assets/js/plugins/KimaiReducedClickHandler.js
Normal file
53
assets/js/plugins/KimaiReducedClickHandler.js
Normal 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] KimaiReducedClickHandler: abstract class
|
||||
*/
|
||||
|
||||
import jQuery from 'jquery';
|
||||
import KimaiPlugin from "../KimaiPlugin";
|
||||
|
||||
export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
|
||||
_addClickHandler(selector, callback) {
|
||||
jQuery('body').on('click', selector, function(event) {
|
||||
// just in case an inner element is editable, than this should not be triggered
|
||||
if (event.target.parentNode.isContentEditable || event.target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// handles the "click" on table rows or list elements
|
||||
let target = event.target;
|
||||
if (event.currentTarget.matches('tr') || event.currentTarget.matches('li')) {
|
||||
while (target !== null && !target.matches('body')) {
|
||||
// when an element within the row is clicked, that can trigger stuff itself, we don't want the event to be processed
|
||||
// don't act if a link, button or form element was clicked
|
||||
if (target.matches('a') || target.matches ('button') || target.matches ('input')) {
|
||||
return;
|
||||
}
|
||||
target = target.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
let href = jQuery(this).attr('data-href');
|
||||
if (!href) {
|
||||
href = jQuery(this).attr('href');
|
||||
}
|
||||
|
||||
if (href === undefined || href === null || href === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(href);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
150
assets/js/plugins/KimaiSelectDataAPI.js
Normal file
150
assets/js/plugins/KimaiSelectDataAPI.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 = this.dataset['formPrefix'];
|
||||
if (formPrefix === undefined || formPrefix === null) {
|
||||
formPrefix = '';
|
||||
} else if (formPrefix.length > 0) {
|
||||
formPrefix += '_';
|
||||
}
|
||||
|
||||
let newApiUrl = self._buildUrlWithFormFields(this.dataset['apiUrl'], formPrefix);
|
||||
|
||||
const selectValue = jQuery(this).val();
|
||||
|
||||
// Problem: select a project with activities and then select a customer that has no project
|
||||
// results in a wrong URL, it triggers "activities?project=" instead of using the "emptyUrl"
|
||||
if (selectValue === undefined || selectValue === null || selectValue === '' || (Array.isArray(selectValue) && selectValue.length === 0)) {
|
||||
if (this.dataset['emptyUrl'] === undefined) {
|
||||
self._updateSelect(targetSelect, {});
|
||||
jQuery(targetSelect).attr('disabled', 'disabled');
|
||||
return;
|
||||
}
|
||||
newApiUrl = self._buildUrlWithFormFields(this.dataset['emptyUrl'], formPrefix);
|
||||
}
|
||||
|
||||
jQuery(targetSelect).removeAttr('disabled');
|
||||
|
||||
API.get(newApiUrl, {}, function(data){
|
||||
self._updateSelect(targetSelect, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_buildUrlWithFormFields(apiUrl, formPrefix) {
|
||||
let newApiUrl = apiUrl;
|
||||
|
||||
apiUrl.split('?')[1].split('&').forEach(item => {
|
||||
const [key, value] = item.split('=');
|
||||
const decoded = decodeURIComponent(value);
|
||||
const test = decoded.match(/%(.*)%/);
|
||||
if (test !== null) {
|
||||
const targetField = jQuery('#' + formPrefix + test[1]);
|
||||
let newValue = '';
|
||||
if (targetField.length === 0) {
|
||||
// happens for example:
|
||||
// - in duration only mode, when the end field is not found
|
||||
// console.log('ERROR: Cannot find field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
|
||||
} else {
|
||||
if (targetField.val() !== null) {
|
||||
newValue = targetField.val();
|
||||
|
||||
if (newValue !== '') {
|
||||
// having that special case here is far from being perfect... but for now it works ;-)
|
||||
if (targetField.data('daterangepicker') !== undefined) {
|
||||
if (key === 'begin' || key === 'start' || targetField.data('daterangepicker').singleDatePicker) {
|
||||
newValue = targetField.data('daterangepicker').startDate.format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
|
||||
} else if (key === 'end') {
|
||||
newValue = targetField.data('daterangepicker').endDate.format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
|
||||
}
|
||||
} else if (targetField.data('format') !== undefined) {
|
||||
if (moment(newValue, targetField.data('format')).isValid()) {
|
||||
newValue = moment(newValue, targetField.data('format')).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// happens for example:
|
||||
// - when the end date is not set on a timesheet record and the project list is loaded (as the URL contains the %end% replacer)
|
||||
// console.log('Empty value found for field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
|
||||
}
|
||||
} else {
|
||||
// happens for example:
|
||||
// - when a customer without projects is selected
|
||||
// console.log('ERROR: Empty field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(newValue)) {
|
||||
newValue = newValue.join(',');
|
||||
}
|
||||
|
||||
newApiUrl = newApiUrl.replace(value, newValue);
|
||||
}
|
||||
});
|
||||
|
||||
return newApiUrl;
|
||||
}
|
||||
|
||||
_updateSelect(selectName, data) {
|
||||
const options = {};
|
||||
for (const apiData of data) {
|
||||
let title = '__empty__';
|
||||
if (apiData.hasOwnProperty('parentTitle') && apiData.parentTitle !== null) {
|
||||
title = apiData.parentTitle;
|
||||
}
|
||||
if (!options.hasOwnProperty(title)) {
|
||||
options[title] = [];
|
||||
}
|
||||
options[title].push(apiData);
|
||||
}
|
||||
|
||||
const ordered = {};
|
||||
Object.keys(options).sort().forEach(function(key) {
|
||||
ordered[key] = options[key];
|
||||
});
|
||||
|
||||
/** @var {KimaiFormSelect} select */
|
||||
const select = this.getContainer().getPlugin('form-select');
|
||||
select.updateOptions(selectName, ordered);
|
||||
}
|
||||
|
||||
}
|
||||
101
assets/js/plugins/KimaiThemeInitializer.js
Normal file
101
assets/js/plugins/KimaiThemeInitializer.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] 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');
|
||||
this.getContainer().getPlugin('form').activateForm('form.searchform', 'body');
|
||||
|
||||
this.registerModalAutofocus('#modal_search');
|
||||
this.registerModalAutofocus('#remote_form_modal');
|
||||
}
|
||||
|
||||
/**
|
||||
* workaround for autofocus attribute, as the modal "steals" it
|
||||
*
|
||||
* @param {string} selector
|
||||
*/
|
||||
registerModalAutofocus(selector) {
|
||||
let modal = jQuery(selector);
|
||||
if (modal.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
modal.on('shown.bs.modal', function () {
|
||||
let form = modal.find('form');
|
||||
let formAutofocus = form.find('[autofocus]');
|
||||
if (formAutofocus.length < 1) {
|
||||
formAutofocus = form.find('input[type=text],textarea,select');
|
||||
}
|
||||
formAutofocus.filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* redirect access denied / session timeouts to login page
|
||||
*/
|
||||
registerGlobalAjaxErrorHandler() {
|
||||
const loginUrl = this.getConfiguration('login');
|
||||
const alert = this.getContainer().getPlugin('alert');
|
||||
const translation = this.getContainer().getTranslation().get('login.required');
|
||||
jQuery(document).ajaxError(function(event, jqxhr, settings, thrownError) {
|
||||
if (jqxhr.status !== undefined && jqxhr.status === 403) {
|
||||
const loginRequired = jqxhr.getResponseHeader('login-required');
|
||||
if (loginRequired !== null) {
|
||||
alert.question(translation, function (result) {
|
||||
if (result === true) {
|
||||
window.location.replace(loginUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
}
|
||||
173
assets/js/plugins/KimaiToolbar.js
Normal file
173
assets/js/plugins/KimaiToolbar.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 + ' #orderBy').val(orderBy);
|
||||
jQuery(formSelector + ' #order').val(order);
|
||||
|
||||
// re-render the selectboxes
|
||||
jQuery(formSelector + ' #orderBy').trigger('change');
|
||||
jQuery(formSelector + ' #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;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
$(document).ready(function () {
|
||||
|
||||
//$('.navbar-form select').selectpicker({});
|
||||
|
||||
// DateRange - TODO improve me, so users can type without reloading in between
|
||||
$('.toolbar form input').change(function (event) {
|
||||
$('.toolbar form').submit();
|
||||
});
|
||||
|
||||
$('.toolbar form select').change(function (event) {
|
||||
switch (event.target.id) {
|
||||
case 'customer':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#project').parent().remove();
|
||||
} else {
|
||||
$('.toolbar form select#project').val('');
|
||||
}
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
break;
|
||||
case 'project':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
} else {
|
||||
$('.toolbar form select#activity').val('');
|
||||
}
|
||||
break;
|
||||
}
|
||||
$('.toolbar form').submit();
|
||||
});
|
||||
|
||||
});
|
||||
28
assets/js/widgets/KimaiCookies.js
Normal file
28
assets/js/widgets/KimaiCookies.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiCookies: simple wrapper to handle cookies
|
||||
*/
|
||||
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export default class KimaiCookies {
|
||||
|
||||
static set(name, values, options) {
|
||||
Cookies.set(name, values, options);
|
||||
}
|
||||
|
||||
static get(name) {
|
||||
return Cookies.getJSON(name);
|
||||
}
|
||||
|
||||
static remove(name) {
|
||||
Cookies.remove(name);
|
||||
}
|
||||
|
||||
}
|
||||
97
assets/js/widgets/KimaiPaginatedBoxWidget.js
Normal file
97
assets/js/widgets/KimaiPaginatedBoxWidget.js
Normal 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 ul.pagination a', function (event) {
|
||||
event.preventDefault();
|
||||
self.loadPage(jQuery(event.currentTarget).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);
|
||||
}
|
||||
|
||||
}
|
||||
73
assets/js/widgets/KimaiReloadPageWidget.js
Normal file
73
assets/js/widgets/KimaiReloadPageWidget.js
Normal 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] 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')
|
||||
);
|
||||
document.dispatchEvent(new Event('kimai.reloadPage'));
|
||||
self._hideOverlay();
|
||||
},
|
||||
dataType: 'html',
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
self._hideOverlay();
|
||||
document.location = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
30
assets/js/widgets/KimaiStorage.js
Normal file
30
assets/js/widgets/KimaiStorage.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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] KimaiStorage: simple wrapper to handle localStorage access
|
||||
*/
|
||||
|
||||
export default class KimaiStorage {
|
||||
|
||||
static set(name, values) {
|
||||
window.localStorage.setItem(name, JSON.stringify(values));
|
||||
}
|
||||
|
||||
static get(name) {
|
||||
let value = window.localStorage.getItem(name);
|
||||
if (value === undefined || value === null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
static remove(name) {
|
||||
window.localStorage.removeItem(name);
|
||||
}
|
||||
|
||||
}
|
||||
34
assets/sass/app.scss
Normal file
34
assets/sass/app.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import 'variables';
|
||||
@import 'layout';
|
||||
@import 'error-page';
|
||||
@import 'print';
|
||||
@import 'content';
|
||||
@import 'box';
|
||||
@import 'content-header';
|
||||
@import 'toolbar';
|
||||
@import 'sidebar';
|
||||
@import 'footer';
|
||||
@import 'datatable';
|
||||
@import 'datatable-extensions';
|
||||
@import 'calendar';
|
||||
@import 'dashboard';
|
||||
@import 'navbar';
|
||||
@import 'daterangepicker';
|
||||
@import 'skins';
|
||||
@import 'sweetalert';
|
||||
@import 'autocomplete';
|
||||
@import 'selectpicker';
|
||||
@import 'reporting';
|
||||
@import 'forms';
|
||||
@import 'modal';
|
||||
@import 'progressbar';
|
||||
@import 'avatar';
|
||||
@import 'security';
|
||||
@import 'tabs';
|
||||
76
assets/sass/autocomplete.scss
Normal file
76
assets/sass/autocomplete.scss
Normal 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;
|
||||
}
|
||||
60
assets/sass/avatar.scss
Normal file
60
assets/sass/avatar.scss
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
$avatar-base-size: 30;
|
||||
|
||||
.avatar {
|
||||
text-align: center;
|
||||
background-color: #d2d6de;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: ($avatar-base-size)+px;
|
||||
width: ($avatar-base-size)+px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.initials {
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
font-size: ceil($avatar-base-size / 2 - 2)+px;
|
||||
top: floor($avatar-base-size / 4 - 1)+px;
|
||||
}
|
||||
|
||||
$avatarSizes: "xs" .75, "sm" 1.25, "md" 1.5, "lg" 2;
|
||||
|
||||
@each $avatarSizeName, $avatarBaseSize in $avatarSizes {
|
||||
.avatar-#{$avatarSizeName} {
|
||||
width: ceil($avatar-base-size * $avatarBaseSize)+px;
|
||||
height: ceil($avatar-base-size * $avatarBaseSize)+px;
|
||||
|
||||
.initials {
|
||||
font-size: ceil($avatar-base-size * $avatarBaseSize / 2 - 2)+px;
|
||||
top: ceil($avatar-base-size * $avatarBaseSize / 4 - 1)+px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget-user-image {
|
||||
border-radius: 50%;
|
||||
border: 3px solid #fff;
|
||||
.avatar {
|
||||
width: ceil($avatar-base-size * 2.75)+px;
|
||||
height: ceil($avatar-base-size * 2.75)+px;
|
||||
.initials {
|
||||
font-size: ceil($avatar-base-size * 2.75 / 2 - 2)+px;
|
||||
top: ceil($avatar-base-size * 2.75 / 4 - 1)+px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Detail tables like my teams (Dashboard) or */
|
||||
table.dataTable.table > tbody > tr > td {
|
||||
&.avatars {
|
||||
.avatar {
|
||||
margin: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar.teamlead {
|
||||
box-shadow: 0px 0px 5px 1px rgba(0,0,0,.4),0 0px 3px 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
9
assets/sass/bootstrap.scss
vendored
Normal file
9
assets/sass/bootstrap.scss
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
|
||||
@import '~bootstrap-sass/assets/stylesheets/bootstrap';
|
||||
22
assets/sass/box.scss
Normal file
22
assets/sass/box.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.box {
|
||||
/* used on detail pages for: customer, project and activity */
|
||||
.box-body.no-padding div.comment,
|
||||
.box-body.no-padding .box-padding {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.box-tools {
|
||||
/* a button that is directly connected with a pagination element inside the box tools (eg. project list on customer detail page) */
|
||||
.btn-pager {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
#customer_details_box,
|
||||
#project_details_box,
|
||||
#activity_details_box {
|
||||
th {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
51
assets/sass/calendar.scss
Normal file
51
assets/sass/calendar.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
.popover-content {
|
||||
.calendar-entry {
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Drag and Drop events with styling */
|
||||
.external-events {
|
||||
.external-event {
|
||||
vertical-align: middle;
|
||||
padding: 0 !important;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
padding: 0 7px 4px 7px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 3px 7px 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.fc-bootstrap3 {
|
||||
.table-bordered > tbody > tr > td,
|
||||
.table-bordered > thead > tr > td,
|
||||
.table-bordered > thead > tr > th {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.fc-head {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
td.fc-today {
|
||||
background-color: #fcf8e3!important;
|
||||
}
|
||||
}
|
||||
65
assets/sass/content-header.scss
Normal file
65
assets/sass/content-header.scss
Normal 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.
|
||||
*/
|
||||
|
||||
.content-header {
|
||||
height: 50px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
|
||||
background-color: #fff !important;
|
||||
padding: 10px 0 0 10px;
|
||||
|
||||
h1 {
|
||||
padding-top: 3px;
|
||||
float: left;
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Page based action buttons in the upper right corner of the content area */
|
||||
.content-header>.breadcrumb {
|
||||
position: absolute;
|
||||
float: right;
|
||||
background: transparent;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding-left: 10px;
|
||||
.btn {
|
||||
span.label {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-md-max) {
|
||||
.content-header h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.content-header>.breadcrumb {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header>.breadcrumb {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
assets/sass/content.scss
Normal file
82
assets/sass/content.scss
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-lg-min) {
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&.timesheet-description {
|
||||
ul {
|
||||
padding-left: 15px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ABOUT SCREEN */
|
||||
.box-body .menu-icon {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 35px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.label-gray {
|
||||
background-color: $gray-lte;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Delete link in action dropdowns */
|
||||
.dropdown-menu > li.delete > a {
|
||||
color: #dd4b39;
|
||||
}
|
||||
|
||||
.open-edit {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* allow minimized table columns, eg. customer team & visibility */
|
||||
th.w-min,
|
||||
td.w-min {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable thead > tr > th.hw-min {
|
||||
width: 1%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-weight: normal;
|
||||
color: #000;
|
||||
background-color: $kimai-default;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a.link-black {
|
||||
color: #000;
|
||||
}
|
||||
20
assets/sass/dashboard.scss
Normal file
20
assets/sass/dashboard.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
section {
|
||||
&.dashboard {
|
||||
.info-box {
|
||||
.info-box-text {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
.box-body-scrollable {
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
135
assets/sass/datatable-extensions.scss
Normal file
135
assets/sass/datatable-extensions.scss
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
table.dataTable.table > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
.img-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 0;
|
||||
&.teamlead {
|
||||
box-shadow: 0 0 5px 3px $gray-lte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span.label-color,
|
||||
span.label-meta,
|
||||
span.label-tag,
|
||||
span.label-user,
|
||||
span.label-activity,
|
||||
span.label-project,
|
||||
span.label-customer {
|
||||
display: inline-block;
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
.name-inner {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
.dot {
|
||||
font-size: 9px;
|
||||
margin-right: 3px;
|
||||
color: $gray-lte;
|
||||
}
|
||||
}
|
||||
|
||||
table.dataTable {
|
||||
/* action column */
|
||||
.actions {
|
||||
width: 40px;
|
||||
}
|
||||
tr {
|
||||
/* summary row - provided in the users own timesheet table */
|
||||
&.summary td {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
th.multiCheckbox {
|
||||
width: 15px;
|
||||
}
|
||||
&.overlapping {
|
||||
border-top: 2px solid rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
&.exported {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
td {
|
||||
/*
|
||||
Make sure that the action buttons do not line-break if another column takes all available space
|
||||
See https://github.com/twbs/bootstrap/issues/9939#issuecomment-37682952
|
||||
*/
|
||||
.btn-group {
|
||||
display: flex;
|
||||
}
|
||||
.label {
|
||||
font-size: 85%;
|
||||
padding: .3em .6em .3em;
|
||||
line-height: 24px;
|
||||
}
|
||||
span.label-activity,
|
||||
span.label-project,
|
||||
span.label-customer {
|
||||
a {
|
||||
color: unset;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px dotted $gray-lte;
|
||||
&:hover {
|
||||
border-bottom: 1px dotted darken($gray-lte, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
span.label-invisible {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
tfoot {
|
||||
tr {
|
||||
/* overwritten if used in tfoot, to switch the border from below to top */
|
||||
&.summary td {
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
th.weekend,
|
||||
td.weekend {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
/* order is important, "today” should overwrite "weekend" therefor later in the file */
|
||||
th.today,
|
||||
td.today {
|
||||
background-color: $highlight-today;
|
||||
}
|
||||
th.total,
|
||||
td.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* Quick entry form */
|
||||
.form-dataTable {
|
||||
table.dataTable {
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.table-hover {
|
||||
tr {
|
||||
&.exported {
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,162 +5,43 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
|
||||
|
||||
.error-page {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* ================================ CONTENT ================================ */
|
||||
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================ NAVBAR ================================ */
|
||||
|
||||
.navbar-nav>li>a.ddt-large {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
li.messages-menu ul.menu li .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
|
||||
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.navbar-nav .start_record {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
.ticktac:hover i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
li.open .ticktac i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
.ticktac i.stopped{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ticktac:hover i.stopped{
|
||||
color: #4ff131;
|
||||
}
|
||||
.user-panel>.info {
|
||||
left: 40px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* ================================ TOOLBAR ================================ */
|
||||
|
||||
/* Page based action buttons in the upper right corner of the content area */
|
||||
.content-header > .breadcrumb {
|
||||
/* Loading indicator: overwritten from AdminLTE for positioning and supporting fas classes */
|
||||
.box .overlay > .fas,
|
||||
section.content .overlay > .fas,
|
||||
.overlay-wrapper .overlay > .fas {
|
||||
position: absolute;
|
||||
float: right;
|
||||
background: transparent;
|
||||
}
|
||||
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
|
||||
.control-sidebar {
|
||||
select {
|
||||
color: #000;
|
||||
}
|
||||
div.image {
|
||||
img.img-circle {
|
||||
max-width: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The filter form available on most pages above the datatable */
|
||||
.toolbar form {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
/* ================================ SIDEBAR ================================ */
|
||||
|
||||
/* Used for the Sidebar UserPanel (which is deactivated right now) */
|
||||
.user-panel>.info {
|
||||
left: 80px;
|
||||
top: 50vh;
|
||||
left: 50vw;
|
||||
margin-left: -15px;
|
||||
margin-top: -15px;
|
||||
color: #000;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
/* ================================ FOOTER ================================ */
|
||||
|
||||
footer.main-footer {
|
||||
padding: 5px;
|
||||
font-size: 80%;
|
||||
.box > .overlay,
|
||||
.overlay-wrapper > .overlay,
|
||||
section.content > .overlay,
|
||||
.box > .loading-img,
|
||||
.overlay-wrapper > .loading-img {
|
||||
position: fixed; /* see #1330 */
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ================================ LOGIN-PAGE ================================ */
|
||||
|
||||
.login-page form label {
|
||||
padding-left: 5px;
|
||||
.box .overlay,
|
||||
section.content .overlay,
|
||||
.overlay-wrapper .overlay {
|
||||
z-index: 50;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: $box-border-radius;
|
||||
}
|
||||
/* ================================ PRINT ================================ */
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {display: none !important;}
|
||||
.box .overlay.dark,
|
||||
section.content .overlay.dark,
|
||||
.overlay-wrapper .overlay.dark {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* ================================ INVOICES ================================ */
|
||||
|
||||
.invoice {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 10px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-small{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table.invoice-records {}
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ================================ DATA-TABLES ================================ */
|
||||
|
||||
/*
|
||||
dataTables.bootstrap.css
|
||||
*/
|
||||
@@ -244,39 +125,43 @@ table.dataTable thead .sorting_desc_disabled {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
table.dataTable thead .sorting_asc,
|
||||
table.dataTable thead .sorting_desc {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table.dataTable thead .sorting:after,
|
||||
table.dataTable thead .sorting_asc:after,
|
||||
table.dataTable thead .sorting_desc:after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: block;
|
||||
font-family: 'Glyphicons Halflings';
|
||||
padding-left: 5px;
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
}
|
||||
table.dataTable thead .sorting:after {
|
||||
opacity: 0.2;
|
||||
content: "\e150"; /* sort */
|
||||
content: "\f0dc"; /* sort */
|
||||
}
|
||||
table.dataTable thead .sorting_asc:after {
|
||||
content: "\e155"; /* sort-by-attributes */
|
||||
content: "\f062";
|
||||
}
|
||||
table.dataTable thead .sorting_desc:after {
|
||||
content: "\e156"; /* sort-by-attributes-alt */
|
||||
content: "\f063";
|
||||
}
|
||||
div.dataTables_scrollBody table.dataTable thead .sorting:after,
|
||||
div.dataTables_scrollBody table.dataTable thead .sorting_asc:after,
|
||||
div.dataTables_scrollBody table.dataTable thead .sorting_desc:after {
|
||||
div.dataTables_scrollBody table.dataTable thead .sorting:before,
|
||||
div.dataTables_scrollBody table.dataTable thead .sorting_asc:before,
|
||||
div.dataTables_scrollBody table.dataTable thead .sorting_desc:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table.dataTable thead .sorting_asc_disabled:after,
|
||||
table.dataTable thead .sorting_desc_disabled:after {
|
||||
table.dataTable thead .sorting_asc_disabled:before,
|
||||
table.dataTable thead .sorting_desc_disabled:before {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
table.dataTable thead > tr > th {
|
||||
padding-right: 30px;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.dataTable th:active {
|
||||
@@ -289,9 +174,9 @@ table.dataTable.table-condensed thead > tr > th {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
table.dataTable.table-condensed thead .sorting:after,
|
||||
table.dataTable.table-condensed thead .sorting_asc:after,
|
||||
table.dataTable.table-condensed thead .sorting_desc:after {
|
||||
table.dataTable.table-condensed thead .sorting:before,
|
||||
table.dataTable.table-condensed thead .sorting_asc:before,
|
||||
table.dataTable.table-condensed thead .sorting_desc:before {
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
13
assets/sass/daterangepicker.scss
Normal file
13
assets/sass/daterangepicker.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
@import '~daterangepicker/daterangepicker.css';
|
||||
|
||||
.drp-calendar {
|
||||
.calendar-table {
|
||||
td.today:not(.active) {
|
||||
background-color: lighten(#357ebd, 40);
|
||||
color: #357ebd;
|
||||
}
|
||||
select.yearselect{
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
assets/sass/error-page.scss
Normal file
10
assets/sass/error-page.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.error-page {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
12
assets/sass/fontawesome.scss
vendored
Normal file
12
assets/sass/fontawesome.scss
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
|
||||
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
@import '~@fortawesome/fontawesome-free/scss/regular';
|
||||
@import '~@fortawesome/fontawesome-free/scss/solid';
|
||||
@import '~@fortawesome/fontawesome-free/scss/brands';
|
||||
11
assets/sass/footer.scss
Normal file
11
assets/sass/footer.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
footer.main-footer {
|
||||
padding: 5px;
|
||||
font-size: 80%;
|
||||
}
|
||||
67
assets/sass/forms.scss
Normal file
67
assets/sass/forms.scss
Normal file
@@ -0,0 +1,67 @@
|
||||
/* Currently only used in batch-update form for timesheets, to highlight sections of combined fields */
|
||||
form .box-body fieldset {
|
||||
border-radius: $box-border-radius;
|
||||
padding: 10px;
|
||||
&:hover {
|
||||
background-color: $highlight-bg;
|
||||
}
|
||||
}
|
||||
|
||||
/* To be used with .form-horizontal - to reduce the space between rows, eg. used in the search modals */
|
||||
form.form-narrow {
|
||||
.form-group {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark as exported checkbox in export and invoice create screen */
|
||||
.form-horizontal {
|
||||
.checkbox {
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* bootstrap3 hack because the filter look plain ugly without it (see report: project month) */
|
||||
.checkbox-menu li label {
|
||||
display: block;
|
||||
padding: 5px 15px 5px 10px !important;
|
||||
clear: both;
|
||||
font-weight: normal;
|
||||
line-height: 1.42857143;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
margin:0;
|
||||
transition: background-color .4s ease;
|
||||
}
|
||||
.checkbox-menu li div.checkbox {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.checkbox-menu li div.radio {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.checkbox-menu li input {
|
||||
margin: 0 5px !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.checkbox-menu li.active label {
|
||||
background-color: #cbcbff;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.checkbox-menu li label:hover,
|
||||
.checkbox-menu li label:focus {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.checkbox-menu li.active label:hover,
|
||||
.checkbox-menu li.active label:focus {
|
||||
background-color: #b8b8ff;
|
||||
}
|
||||
|
||||
.duration-input {
|
||||
min-width:50px;
|
||||
padding: 6px;
|
||||
}
|
||||
119
assets/sass/invoice-pdf.scss
Normal file
119
assets/sass/invoice-pdf.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 10pt;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
table, tr, td, th, p, h1, h2, h3, h4, h5 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
th {
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.text-small {
|
||||
font-size: 7pt;
|
||||
}
|
||||
.padding-left {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.date {
|
||||
color: #666;
|
||||
font-size: 80%;
|
||||
text-align: right;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.wrapper {
|
||||
margin: 4px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
}
|
||||
table.addresses, p {
|
||||
line-height: 14pt;
|
||||
}
|
||||
/* The address table */
|
||||
table.addresses {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
/* The PDF header */
|
||||
table.header {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
table.header .title {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
}
|
||||
/* The PDF footer */
|
||||
table.footer {
|
||||
border-top: 1px solid #999;
|
||||
font-size: 80%;
|
||||
padding-top: 8px;
|
||||
line-height: 14px;
|
||||
}
|
||||
table.footer td {
|
||||
vertical-align: top;
|
||||
}
|
||||
/* The invoice items list */
|
||||
table.items {
|
||||
border-collapse: collapse;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
table.items thead th,
|
||||
table.items tbody td {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
table.items thead th.first,
|
||||
table.items tbody td.first {
|
||||
padding-left: 0;
|
||||
}
|
||||
table.items thead th.last,
|
||||
table.items tbody td.last,
|
||||
table.items tfoot td.last {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
table.items td,
|
||||
table.items th {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
table.items tfoot td,
|
||||
table.items tfoot th {
|
||||
padding: 20px 0 0 0;
|
||||
line-height: 1px;
|
||||
}
|
||||
table.items tfoot td.last {
|
||||
padding-left: 10px;
|
||||
}
|
||||
table.items tr.odd {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
305
assets/sass/invoice.scss
Normal file
305
assets/sass/invoice.scss
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* 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 "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/mixins";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/normalize";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/print";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/scaffolding";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/type";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/grid";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/tables";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/list-group";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/panels";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/wells";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/utilities";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities";
|
||||
|
||||
body {
|
||||
font-family: $font-family-sans-serif;
|
||||
|
||||
&.invoice_print {
|
||||
background-color: #eee;
|
||||
|
||||
.table.no-border, .table.no-border td, .table.no-border th {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
margin: 105px auto 30px auto;
|
||||
padding: 50px 65px;
|
||||
min-height: 297mm;
|
||||
position: relative;
|
||||
max-width: 210mm;
|
||||
box-shadow: 0 0 20px rgba(80, 80, 80, 0.7);
|
||||
background: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin: 10px 0 20px 0;
|
||||
font-size: 22px;
|
||||
|
||||
> small {
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
border-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice-items {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 3em;
|
||||
|
||||
.table {
|
||||
thead th {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 15px
|
||||
}
|
||||
|
||||
tfoot {
|
||||
border-top: 1px solid #ddd;
|
||||
|
||||
tr:first-child th,
|
||||
tr:first-child td {
|
||||
padding-top: 15px
|
||||
}
|
||||
|
||||
tr {
|
||||
th, td {
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid #ccc;
|
||||
margin-top: 50px;
|
||||
font-size: 0.8em;
|
||||
padding-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ================================ FREELANCER INVOICE ================================ */
|
||||
|
||||
#freelancer-invoice {
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
h1 {
|
||||
font: bold 100% sans-serif;
|
||||
letter-spacing: 0.5em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font: bold 1.5em sans-serif;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* table */
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* header */
|
||||
|
||||
header {
|
||||
&:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
address {
|
||||
float: left;
|
||||
font-style: normal;
|
||||
line-height: 1.25;
|
||||
margin: 0 1em 1em 0;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
|
||||
article address {
|
||||
font-size: 1em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
article.address {
|
||||
margin: 1em 0 1.5em 0;
|
||||
}
|
||||
|
||||
/* article */
|
||||
|
||||
article:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
clip: rect(0 0 0 0);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
article p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* table meta & balance */
|
||||
|
||||
table.meta:after, table.balance:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* table meta */
|
||||
|
||||
table.meta {
|
||||
float: right;
|
||||
width: 50%;
|
||||
font-size: 85%;
|
||||
|
||||
th {
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
/* table balance */
|
||||
|
||||
table.balance {
|
||||
float: right;
|
||||
margin-top: 1em;
|
||||
|
||||
th {
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* table items */
|
||||
|
||||
table.inventory {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
margin-top: 2em;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
table.inventory th,
|
||||
table.inventory td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table.inventory thead th {
|
||||
font-weight: bold;
|
||||
border-width: 0 0 1px 0;
|
||||
border-bottom: 1px solid #000;
|
||||
padding-bottom: 15px
|
||||
}
|
||||
|
||||
table.inventory tbody tr:first-child td {
|
||||
padding-top: 15px
|
||||
}
|
||||
|
||||
table.inventory tbody tr:last-child td {
|
||||
padding-bottom: 15px
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================ PRINTING RULES ================================ */
|
||||
|
||||
@media print {
|
||||
* {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
body, .wrapper {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: white;
|
||||
}
|
||||
#freelancer-invoice {
|
||||
span:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add, .cut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.invoice {
|
||||
margin: 1em 2em;
|
||||
font-size: 90%;
|
||||
width: unset;
|
||||
padding: 0;
|
||||
min-height: unset;
|
||||
position: unset;
|
||||
max-width: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
assets/sass/layout.scss
Normal file
8
assets/sass/layout.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
@media (max-width: $screen-sm-max) {
|
||||
.main-header .logo {
|
||||
display: none;
|
||||
}
|
||||
.fixed .content-wrapper, .fixed .right-side, .control-sidebar, .main-sidebar {
|
||||
padding-top: 50px;
|
||||
}
|
||||
}
|
||||
9
assets/sass/modal.scss
Normal file
9
assets/sass/modal.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.modal-header,
|
||||
.modal-footer {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.modal-content {
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 10px 80px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
153
assets/sass/navbar.scss
Normal file
153
assets/sass/navbar.scss
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.navbar-custom-menu {
|
||||
.navbar-nav {
|
||||
li>.ddt-small {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
.avatar {
|
||||
max-height: 30px;
|
||||
max-width: 30px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin-top: 0;
|
||||
.initials {
|
||||
top: 5px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
li>.ddt-small {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
li.messages-menu ul.menu li {
|
||||
a:hover .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
a .pull-left i {
|
||||
color: #444;
|
||||
}
|
||||
}
|
||||
.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.messages-menu>.dropdown-menu>li .menu>li>a>h4>span {
|
||||
margin-right: 55px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
max-width: 90vw;
|
||||
width: unset !important;
|
||||
}
|
||||
.user-menu {
|
||||
ul {
|
||||
li {
|
||||
a {
|
||||
color: #444;
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
i {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes ticktac-blink {
|
||||
0% { opacity: 1; }
|
||||
5% { opacity: 0.95; }
|
||||
10% { opacity: 0.9; }
|
||||
15% { opacity: 0.85; }
|
||||
20% { opacity: 0.8; }
|
||||
25% { opacity: 0.75; }
|
||||
30% { opacity: 0.7; }
|
||||
35% { opacity: 0.65; }
|
||||
40% { opacity: 0.6; }
|
||||
45% { opacity: 0.65; }
|
||||
50% { opacity: 0.7; }
|
||||
55% { opacity: 0.75; }
|
||||
60% { opacity: 0.8; }
|
||||
65% { opacity: 0.85; }
|
||||
70% { opacity: 0.9; }
|
||||
75% { opacity: 0.95; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
.ticktac-single {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
i {
|
||||
animation: ticktac-blink 2s step-end infinite;
|
||||
}
|
||||
span {
|
||||
font-size: 1.8em;
|
||||
line-height: 1em;
|
||||
padding-left: 5px;
|
||||
padding-right: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(0,0,0,.3);
|
||||
}
|
||||
}
|
||||
body.fixed {
|
||||
.navbar-nav {
|
||||
.user-menu {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
.navbar-custom-menu>.navbar-nav>li>.dropdown-menu {
|
||||
right: 0;
|
||||
}
|
||||
body.fixed {
|
||||
.navbar-nav {
|
||||
.user-menu {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.navbar-nav .user-menu.open .dropdown-menu > li > a {
|
||||
padding: 5px 25px 5px 25px
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
.ticktac-single {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.navbar-custom-menu .navbar-nav li>.ddt-small {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-md-max) {
|
||||
.ticktac-single {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.navbar-custom-menu .navbar-nav li>.ddt-small {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
26
assets/sass/print.scss
Normal file
26
assets/sass/print.scss
Normal 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.
|
||||
*/
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
.col-print-12{
|
||||
width:100%;
|
||||
}
|
||||
.sidebar-mini.sidebar-collapse .content-wrapper, .sidebar-mini.sidebar-collapse .main-footer, .sidebar-mini.sidebar-collapse .right-side {
|
||||
margin: 0!important;
|
||||
}
|
||||
section.content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
span i.dot {
|
||||
display: none;
|
||||
margin-right: 0!important;
|
||||
}
|
||||
}
|
||||
20
assets/sass/progressbar.scss
Normal file
20
assets/sass/progressbar.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
make sure that the progressbar title does not wrap and does not need to be embedded in a container with white-space: nowrap
|
||||
used for example in the project-overview report
|
||||
*/
|
||||
.progress-group {
|
||||
.progress-title {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
.progress-text {
|
||||
display: inline-block;
|
||||
width: 1%;
|
||||
}
|
||||
.progress-number {
|
||||
display: inline-block;
|
||||
width: 98%;
|
||||
text-align: right;
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
assets/sass/reporting.scss
Normal file
7
assets/sass/reporting.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
.form-reporting .box-header .form-group {
|
||||
margin-right: 10px;
|
||||
label {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user