prepare intranet for first Dokploy deployment

This commit is contained in:
2026-08-26 12:59:51 +02:00
parent 5cfe688bdc
commit 29dfb85fef
1321 changed files with 227234 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg"
x="0px"
y="0px"
width="5058.3882"
viewBox="0 0 5058.3882 766.70679"
enable-background="new 0 0 5200 800"
xml:space="preserve"
sodipodi:docname="intranet.svg"
height="766.70679"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" />
<sodipodi:namedview
id="namedview25"
pagecolor="#f1f2f4"
bordercolor="#000000"
borderopacity="1"
showgrid="false"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.052980769"
inkscape:cx="-764.42831"
inkscape:cy="-934.30127"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1358"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg" />
<style
id="style1">path { fill: #f1f2f4; opacity=&quot;1&quot; stroke=&quot;none&quot; style=&quot;display:inline&quot;}</style>
<path
id="1_i"
d="m 55.846921,280.65323 c -0.785523,160.7849 1.212471,321.0364 6.316604,482.0026 -10.618347,0 -20.714839,0 -30.811332,0 -9.993119,0 -19.986239,0 -31.226396,0 C 11.316134,508.41933 15.377435,254.38003 0,0 21.078011,0 41.14803,0 60.747296,0 59.11366,93.408 57.480641,186.7806 55.846921,280.65323 Z" />
<path
id="2_n"
d="m 263.94938,455.03133 c -0.094,102.7806 -2.31018,205.0743 2.11413,307.6643 -17.28564,0 -34.04425,0 -51.74792,0 11.72375,-253.883 14.63104,-507.8318 -0.0664,-762.2574 1.96695,-0.1725 3.37747,-0.4015 4.78839,-0.4039 15.4993,-0.026 30.99961,-0.095 46.49674,0.09 1.79605,0.021 4.29101,1.0033 5.25723,2.3834 29.33283,41.8961 58.50794,83.9026 87.72083,125.8828 88.01974,126.4882 176.68716,252.5439 260.47421,381.9064 30.3267,46.8228 58.979,94.7306 88.3378,142.1788 2.9187,4.7172 5.4344,9.6837 8.1371,14.5346 0.4771,-0.084 0.9542,-0.1676 1.4313,-0.2513 0,-1.8538 0.051,-3.7093 -0.01,-5.5612 -1.3987,-43.9697 -3.1534,-87.9316 -4.1195,-131.9107 -0.9443,-42.9836 -1.3427,-85.9837 -1.5278,-128.9785 -0.4152,-96.4939 -0.363,-192.99 -0.8986,-289.483 -0.1895,-34.1557 -1.5371,-68.305 -2.3427,-102.4575 -0.059,-2.4917 -0.01,-4.9859 -0.01,-7.9146 16.9793,0 33.8638,0 50.7475,0 -8.1759,253.8841 -9.1391,507.8516 0.09,762.4987 -10.439,0 -21.2036,0.1765 -31.9487,-0.2031 -1.4963,-0.053 -3.172,-2.6637 -4.2991,-4.3938 -21.73,-33.3585 -43.6975,-66.5681 -65.0128,-100.1909 -84.3111,-132.9982 -173.89784,-262.4539 -262.97652,-392.2553 -39.96744,-58.2388 -79.65716,-116.6683 -119.39981,-175.0609 -3.8371,-5.6378 -7.14658,-11.634 -10.72168,-17.4512 -0.73645,-1.1983 -1.56519,-2.3399 -3.27585,-3.3485 3.66208,128.2398 4.84033,256.3529 2.76215,384.9832 z" />
<path
id="3_t"
d="m 1383.2471,45.7185 c -50.642,-0.8569 -100.7924,-1.7344 -150.9444,-2.5139 -9.1775,-0.1426 -8.562,-1.4584 -8.9499,8.4 -4.6272,117.5814 -6.1254,235.21883 -5.1343,352.87233 0.8647,102.6436 2.7607,205.2785 4.1234,307.9183 0.2186,16.473 0.031,32.9515 0.031,49.8292 -18.988,0 -39.025,0 -58.7251,0 0,-9.6737 -0.2366,-19.2759 0.037,-28.8634 2.1898,-76.6167 5.2717,-153.2178 6.513,-229.8489 1.2874,-79.468 1.6011,-158.9717 0.8603,-238.4459 -0.5403,-57.96713 -3.4699,-115.91223 -5.3622,-173.86623 -0.4622,-14.1552 -1.1482,-28.3041 -1.4823,-42.4618 -0.098,-4.1707 -1.2757,-5.7989 -5.7866,-5.6855 -43.9765,1.1051 -87.9588,1.9644 -131.9378,2.9721 -37.31584,0.855 -74.62925,1.8169 -111.94445,2.7077 -1.1154,0.027 -2.2374,-0.2138 -3.6374,-0.3589 0,-16.1464 0,-32.0675 0,-48.3736 188.52605,0 376.89005,0 565.92395,0 -0.7413,15.745 -1.4809,31.4546 -2.2509,47.8118 -30.601,-0.7008 -60.7217,-1.3905 -91.3332,-2.0933 z" />
<path
id="4_r"
d="m 1693.066,762.82672 c -0.7055,1.5411 -1.4462,3.6166 -2.205,3.6232 -20.2795,0.1767 -40.561,0.1407 -61.944,0.1407 15.0303,-254.4632 17.6845,-508.3694 2.6879,-762.98801 2.3573,0 4.1279,0.028 5.8975,0 65.3069,-1.1859 130.6113,-2.5171 195.921,-3.5202 45.6857,-0.7017 90.89,2.9055 134.6044,17.4884 30.7028,10.2424 58.0708,25.8477 79.4777,50.6849 19.125,22.1897 30.1187,48.1807 34.6248,76.878 7.3323,46.6937 3.1809,92.11211 -17.7588,135.03271 -19.3518,39.6654 -49.6182,69.4095 -87.0791,92.004 -11.0918,6.6899 -22.9963,12.0273 -34.4651,18.1044 -1.2696,0.6729 -2.1201,2.1372 -3.1651,3.2344 0.9088,0.7843 1.6939,1.9721 2.7445,2.2944 36.1333,11.0854 63.6491,34.041 86.894,62.9085 19.6672,24.4246 34.4917,51.813 48.4719,79.6813 35.5154,70.7976 64.3426,144.4186 91.9143,218.5624 1.0901,2.9319 2.1233,5.8851 3.481,9.6554 -4.3962,0 -8.1399,0 -11.8838,0 -11.9995,0 -24.0019,-0.1566 -35.9963,0.092 -3.7735,0.078 -5.4329,-1.1069 -6.7889,-4.6875 -23.8288,-62.9311 -49.8635,-124.9229 -79.7014,-185.2701 -15.07,-30.4794 -31.0022,-60.4575 -50.9167,-88.1612 -17.3465,-24.1305 -40.2879,-41.3755 -66.3296,-54.9509 -16.0514,-8.3675 -33.5768,-10.739 -51.2111,-11.9236 -60.4316,-4.0596 -120.9238,-2.3967 -181.4035,-1.2929 -0.818,0.015 -1.6336,0.18 -3.2119,0.3637 0,1.7754 -0.019,3.5351 0,5.2943 1.0211,82.6516 1.7776,165.3077 3.2138,247.9521 0.5122,29.4727 2.6728,58.9167 4.1247,88.8036 m -8.2547,-715.70671 c -0.3651,2.015 -1.081,4.0366 -1.0428,6.0439 0.5898,30.9884 1.8343,61.975 1.8316,92.9618 -0.01,73.99691 -0.5904,147.99381 -0.9398,221.99061 -0.01,2.0834 0,4.1669 0,7.4907 8.4679,0 16.1119,0 23.7561,7e-4 31.4994,0.02 63.0038,0.3861 94.4965,-0.065 18.6283,-0.2666 37.338,-0.928 55.8349,-2.9912 34.8654,-3.8887 67.622,-14.6499 97.1828,-33.9769 31.875,-20.84 55.9978,-48.1322 68.4575,-84.6419 10.281,-30.1245 11.4106,-61.01651 6.5383,-92.26621 -7.3345,-47.0424 -32.3579,-80.0179 -77.2124,-97.3932 -32.8736,-12.7342 -67.2872,-17.6183 -102.1238,-19.4808 -55.3125,-2.9572 -110.6172,-1.0149 -166.7777,2.3271 z" />
<path
id="5_a"
d="m 2327.7505,752.85213 c 19.2041,-45.4364 37.4165,-90.8864 57.5408,-135.4733 54.7898,-121.3885 110.5935,-242.3189 165.5835,-363.6176 30.3787,-67.01033 60.198,-134.28313 86.1362,-203.18563 3.1062,-8.2519 6.0257,-16.5901 8.6145,-25.0164 2.5359,-8.2529 4.5389,-16.6696 6.9214,-25.5592 21.1829,0 43.064,0 64.2639,0 17.3108,61.5161 43.5442,119.3058 69.3809,177.1244 53.384,119.46383 108.0369,238.35983 161.8311,357.64143 28.3308,62.8204 56.5305,125.719 80.7556,190.2853 3.8511,10.2639 6.7737,20.8787 10.0288,31.3614 0.53,1.7069 0.5525,3.5715 0.8721,5.8006 -20.2281,0 -40.1001,0 -59.3702,0 -13.7429,-62.2083 -39.8989,-119.74 -63.768,-178.0492 -13.052,-31.8849 -27.1612,-63.336 -40.6372,-95.0493 -1.4331,-3.3725 -3.0796,-4.7511 -6.9056,-4.7478 -124.3234,0.1053 -248.6472,0.09 -372.9707,0.07 -2.7417,-5e-4 -4.9179,-0.01 -6.3181,3.3023 -29.9043,70.669 -60.5156,141.0623 -85.6504,213.6276 -5.9753,17.2508 -10.1294,35.1353 -15.0029,52.761 -0.6965,2.5188 -0.7229,5.2229 -1.0989,8.147 -20.7207,0 -41.302,0 -62.7038,0 0.8281,-3.1485 1.603,-6.0952 2.497,-9.4221 m 276.5979,-525.8677 c -32.832,72.7061 -65.664,145.4123 -98.6044,218.3583 116.7929,0 232.8803,0 349.3957,0 -0.6238,-1.7314 -1.0427,-3.139 -1.6248,-4.4756 -39.3452,-90.357 -78.6225,-180.7436 -118.0979,-271.04363 -17.6528,-40.3805 -37.3188,-79.9036 -51.6972,-121.8717 -2.7461,-1.0229 -4.2273,0.01 -5.0967,3.1074 -1.3469,4.7975 -2.8616,9.592 -4.821,14.1669 -23.0066,53.7071 -46.1097,107.3729 -69.4537,161.75833 z" />
<path
id="6_n"
d="m 3241.2241,675.02045 c 0.6673,29.4491 1.3304,58.3981 2.0013,87.6769 -16.9424,0 -33.701,0 -51.4034,0 11.7352,-253.8879 14.6209,-507.8365 -0.057,-762.26152 1.9609,-0.1712 3.372,-0.3992 4.7834,-0.4015 15.4993,-0.026 30.9998,-0.095 46.4968,0.09 1.7954,0.021 4.2896,1.005 5.2561,2.3853 29.333,41.896 58.5081,83.9025 87.7207,125.8825 88.0191,126.48822 176.6868,252.54332 260.4734,381.90562 30.3267,46.8226 58.979,94.7303 88.3377,142.1783 2.9184,4.717 5.4338,9.6833 8.1365,14.5339 0.477,-0.084 0.9543,-0.16751 1.4313,-0.2513 0,-1.8539 0.052,-3.7095 -0.01,-5.5616 -1.3989,-43.9696 -3.1499,-87.9314 -4.1208,-131.9104 -0.9419,-42.6501 -1.3448,-85.3168 -1.5264,-127.9782 -0.4116,-96.6604 -0.3533,-193.32302 -0.8872,-289.98242 -0.1897,-34.3223 -1.5447,-68.6381 -2.354,-102.9572 -0.059,-2.4919 -0.01,-4.9864 -0.01,-7.9151 16.9785,0 33.863,0 50.7471,0 -8.1768,253.88292 -9.1368,507.84992 0.089,762.49881 -10.436,0 -21.2007,0.17651 -31.9455,-0.203 -1.4964,-0.053 -3.1731,-2.6624 -4.2999,-4.3922 -21.7307,-33.35829 -43.698,-66.56799 -65.0134,-100.19069 -84.3152,-132.9974 -173.8948,-262.4575 -262.9819,-392.2526 -39.5003,-57.54982 -78.7505,-115.27102 -118.032,-172.97042 -4.3061,-6.3253 -8.0596,-13.0268 -12.0869,-19.5428 -0.7402,-1.1976 -1.5667,-2.342 -3.5991,-3.2771 -0.089,1.8293 -0.2928,3.6614 -0.2495,5.4876 1.0673,44.9812 2.7861,89.9574 3.1115,134.94372 0.5664,78.3216 0.3504,156.6495 0.325,234.9747 -0.025,76.3304 -0.2212,152.6607 -0.3382,229.49099 z" />
<path
id="7_e"
d="m 4126.4746,718.67753 c 71.4199,-2.2664 142.3417,-4.5288 214.061,-6.8168 -0.7451,16.4174 -1.5098,33.2761 -2.2896,50.4655 -150.0732,0 -299.7773,0 -449.3708,0 0,-9.0219 -0.2398,-17.8086 0.043,-26.5785 1.0991,-34.1419 2.4472,-68.2758 3.5473,-102.4177 1.9158,-59.4573 4.5193,-118.9072 5.3506,-178.381 0.9199,-65.8076 0.7197,-131.6446 -0.034,-197.457 -0.5263,-45.9768 -2.6347,-91.93993 -4.3493,-137.89753 -1.4288,-38.2954 -3.3152,-76.5737 -4.9873,-114.8601 -0.064,-1.4648 -0.01,-2.9348 -0.01,-4.7344 138.3159,0 276.373,0 415.1914,0 -0.7642,15.7183 -1.521,31.2789 -2.314,47.5817 -2.6108,0 -5.0278,0.027 -7.4439,0 -114.8071,-1.4635 -229.6137,-2.946 -344.4211,-4.3742 -9.5456,-0.1187 -8.282,-0.8195 -8.2141,8.2124 0.7046,93.8221 1.4219,187.64413 2.1331,281.46623 0.018,2.3219 0,4.6441 0,7.8306 96.0235,-0.3875 191.7266,-1.1139 287.7359,-7.079 0,17.5543 0,34.787 0,51.832 -95.5908,-1.947 -191.2661,-3.8957 -287.7454,-5.8608 0.2673,114.2965 -1.0813,227.1922 2.9629,341.1996 60.1511,-0.7109 119.9011,-1.4171 180.1502,-2.1271 z" />
<path
id="8_t"
d="m 4643.8493,0.005 c 138.2856,0 276.0713,0 414.5386,0 -0.7608,16.1367 -1.501,31.8451 -2.2613,47.9687 -2.6269,0 -4.7666,0.035 -6.9052,-0.01 -78.6382,-1.466 -157.2764,-2.9459 -235.9146,-4.4006 -8.1318,-0.1504 -8.1011,-0.082 -8.4204,7.9567 -5.8862,148.0727 -6.2427,296.21243 -4.646,444.35493 0.9448,87.642 3.2334,175.2695 4.9151,262.9035 0.022,1.1504 0,2.3017 0,3.8394 -20.2554,0 -40.292,0 -59.9736,0 0,-9.7037 -0.2476,-19.3086 0.039,-28.8974 1.8487,-61.7897 3.9976,-123.5708 5.6973,-185.3643 2.5986,-94.4601 3.6372,-188.9431 1.8257,-283.4266 -0.9419,-49.1413 -2.9234,-98.26313 -4.52,-147.39093 -0.7364,-22.648 -1.7842,-45.2863 -2.4356,-67.9363 -0.1338,-4.647 -1.436,-6.2972 -6.3467,-6.1719 -57.6347,1.4701 -115.2749,2.7291 -172.9135,4.0528 -23.4888,0.5394 -46.9776,1.1033 -70.4664,1.6419 -0.9853,0.023 -1.9736,-0.099 -3.4404,-0.1796 0,-16.1404 0,-32.1629 0,-48.9453 50.2334,0 100.4795,0 151.2256,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,865 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" version="1.1" viewBox="0 0 566.929 566.929">
<!-- Generator: Adobe Illustrator 29.8.2, SVG Export Plug-In . SVG Version: 2.1.1 Build 3) -->
<rect x="0" y="-.006" width="566.93" height="566.94" fill="#2786cf"/>
<path d="M198.546,432.37c-.147,0-.308-.03-.469-.049l-71.276-10.782c-1.709-.253-2.867-1.855-2.618-3.554.254-1.68,1.783-2.832,3.55-2.608l71.281,10.791c1.7.254,2.862,1.846,2.608,3.545-.235,1.534-1.568,2.657-3.076,2.657h0ZM372.855,432.155c-1.523,0-2.851-1.114-3.066-2.657-.254-1.699.918-3.281,2.597-3.535l71.306-10.576c1.758-.215,3.281.928,3.516,2.617.253,1.7-.918,3.291-2.598,3.545l-71.306,10.567c-.156.029-.293.039-.449.039h0ZM202.804,405.339c-.459,0-.928-.107-1.373-.322l-84.855-41.609c-1.538-.742-2.178-2.617-1.421-4.16.742-1.543,2.632-2.188,4.16-1.426l84.856,41.609c1.538.752,2.182,2.608,1.421,4.17-.528,1.094-1.646,1.738-2.788,1.738h0ZM368.637,405.134c-1.162,0-2.256-.644-2.802-1.738-.743-1.563-.098-3.418,1.445-4.17l84.841-41.404c1.543-.742,3.398-.108,4.17,1.426.722,1.543.097,3.418-1.455,4.16l-84.831,41.404c-.43.215-.899.322-1.368.322h0ZM215.05,380.995c-.747,0-1.504-.264-2.1-.811l-84.894-77.8c-1.255-1.162-1.348-3.134-.186-4.394,1.167-1.26,3.135-1.338,4.395-.196l84.884,77.79c1.27,1.172,1.358,3.155.196,4.405-.611.674-1.456,1.006-2.295,1.006h0ZM356.372,380.926c-.84,0-1.68-.332-2.285-1.006-1.172-1.269-1.094-3.222.195-4.394l84.899-77.722c1.25-1.152,3.223-1.074,4.375.196,1.172,1.259,1.094,3.232-.195,4.394l-84.899,77.722c-.586.547-1.348.81-2.09.81h0ZM234.15,361.748c-1.045,0-2.075-.547-2.661-1.495l-68.63-112.583c-.888-1.485-.429-3.399,1.026-4.278,1.469-.898,3.398-.439,4.287,1.035l68.634,112.565c.899,1.465.43,3.398-1.035,4.297-.508.293-1.064.459-1.621.459h0ZM337.174,361.601c-.567,0-1.094-.156-1.622-.459-1.464-.889-1.914-2.812-1.035-4.287l68.767-112.418c.898-1.474,2.832-1.924,4.277-1.025,1.465.879,1.934,2.802,1.035,4.267l-68.766,112.428c-.586.967-1.621,1.494-2.656,1.494h0ZM313.083,349.111c-.274,0-.547-.029-.84-.117-1.641-.469-2.617-2.178-2.149-3.819l38.299-137.314c.469-1.646,2.158-2.627,3.877-2.153,1.621.464,2.598,2.173,2.129,3.828l-38.299,137.299c-.39,1.368-1.64,2.276-3.017,2.276h0ZM258.514,349.101c-1.357,0-2.607-.898-2.998-2.266l-38.504-137.309c-.458-1.655.503-3.364,2.159-3.828,1.689-.484,3.369.503,3.838,2.158l38.503,137.3c.459,1.65-.507,3.359-2.158,3.828-.283.088-.576.117-.84.117h0Z" fill="#fff"/>
<path d="M297.624,445.094h-23.837c-2.344,0-4.248-1.904-4.248-4.248V138.959h-73.664c-2.334,0-4.238-1.924-4.238-4.283v-24.085c0-2.344,1.904-4.253,4.238-4.253h73.644l.01-55.14c0-2.344,1.904-4.253,4.258-4.253h23.856c2.364,0,4.239,1.909,4.239,4.253v55.14h73.649c2.353,0,4.267,1.909,4.267,4.253v24.085c0,2.359-1.914,4.283-4.267,4.283l-73.649-.015v301.902c0,2.344-1.895,4.248-4.258,4.248h0ZM275.75,438.893h19.93V136.971c0-2.343,1.924-4.248,4.327-4.248l73.571.015v-20.18h-73.63c-2.363,0-4.268-1.904-4.268-4.248v-55.144h-19.94l-.01,55.144c0,2.344-1.894,4.248-4.219,4.248h-73.668v20.18h73.649c2.363,0,4.258,1.89,4.258,4.219v301.936h0Z" fill="#fff"/>
<path d="M396.009,475.873l-41.003.127c-1.875,2.754-3.946,5.381-6.163,7.911h-126.206c-2.275-2.618-4.438-5.352-6.264-8.047l-40.706.009c-3.936,0-7.124,3.184-7.124,7.12s3.188,7.119,7.124,7.119l36.395-.049c.142.019.288.049.43.049h183.518c3.926,0,7.139-3.184,7.139-7.1,0-3.955-3.213-7.139-7.139-7.139Z" fill="#fff"/>
<path d="M249.921,504.603h-29.926c-3.936,0-7.144,3.164-7.144,7.109,0,1.905.738,3.692,2.08,5.039,1.353,1.338,3.145,2.11,5.064,2.11h131.513c1.915,0,3.711-.772,5.059-2.11,1.328-1.347,2.07-3.134,2.07-5.039,0-3.945-3.183-7.109-7.129-7.109h-29.959c-10.85,5.137-22.988,8.008-35.82,8.008s-24.94-2.871-35.809-8.008Z" fill="#fff"/>
<path d="M420.501,447.106h-53.045c-.606,2.676-1.348,5.371-2.188,8.027h-158.978c-.933-2.676-1.758-5.4-2.344-8.027h-52.908c-3.931,0-7.119,3.203-7.119,7.129s3.188,7.129,7.119,7.129l54.651-.02h157.314v.01l57.498.01c3.935,0,7.138-3.194,7.138-7.129s-3.203-7.129-7.138-7.129Z" fill="#fff"/>
<metadata>
<i:aipgfRef id="adobe_illustrator_pgf"/>
<i:aipgf id="adobe_illustrator_pgf" i:pgfEncoding="zstd/base64" i:pgfVersion="24">
<![CDATA[
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
]]>
</i:aipgf>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 68 KiB

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg id="plus-add-more-detail" style="enable-background:new 0 0 15 15;" version="1.1" viewBox="0 0 15 15" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M7.5,0C3.364,0,0,3.364,0,7.5S3.364,15,7.5,15S15,11.636,15,7.5S11.636,0,7.5,0z M7.5,14C3.916,14,1,11.084,1,7.5 S3.916,1,7.5,1S14,3.916,14,7.5S11.084,14,7.5,14z"/><polygon points="8,3.5 7,3.5 7,7 3.5,7 3.5,8 7,8 7,11.5 8,11.5 8,8 11.5,8 11.5,7 8,7 "/></svg>

After

Width:  |  Height:  |  Size: 492 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.2893 5.70708C13.8988 5.31655 13.2657 5.31655 12.8751 5.70708L7.98768 10.5993C7.20729 11.3805 7.2076 12.6463 7.98837 13.427L12.8787 18.3174C13.2693 18.7079 13.9024 18.7079 14.293 18.3174C14.6835 17.9269 14.6835 17.2937 14.293 16.9032L10.1073 12.7175C9.71678 12.327 9.71678 11.6939 10.1073 11.3033L14.2893 7.12129C14.6799 6.73077 14.6799 6.0976 14.2893 5.70708Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.71069 18.2929C10.1012 18.6834 10.7344 18.6834 11.1249 18.2929L16.0123 13.4006C16.7927 12.6195 16.7924 11.3537 16.0117 10.5729L11.1213 5.68254C10.7308 5.29202 10.0976 5.29202 9.70708 5.68254C9.31655 6.07307 9.31655 6.70623 9.70708 7.09676L13.8927 11.2824C14.2833 11.6729 14.2833 12.3061 13.8927 12.6966L9.71069 16.8787C9.32016 17.2692 9.32016 17.9023 9.71069 18.2929Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="#FEFEFE" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16.06 13.79C15.91 13.94 15.72 14.01 15.53 14.01C15.34 14.01 15.15 13.94 15 13.79L12 10.79L9 13.79C8.71 14.08 8.23 14.08 7.94 13.79C7.65 13.5 7.65 13.02 7.94 12.73L11.47 9.2C11.76 8.91 12.24 8.91 12.53 9.2L16.06 12.73C16.35 13.03 16.35 13.5 16.06 13.79Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3C4.55228 3 5 3.44772 5 4V5H7V4C7 3.44772 7.44772 3 8 3C8.55228 3 9 3.44772 9 4V5H11V4C11 3.44772 11.4477 3 12 3C12.5523 3 13 3.44772 13 4V5H15V4C15 3.44772 15.4477 3 16 3C16.5523 3 17 3.44772 17 4V5H19V4C19 3.44772 19.4477 3 20 3C20.5523 3 21 3.44772 21 4V6V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V6V4C3 3.44772 3.44772 3 4 3ZM19 7H16H12H8H5V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V7ZM13 10C13 10.5523 12.5523 11 12 11C11.4477 11 11 10.5523 11 10C11 9.44771 11.4477 9 12 9C12.5523 9 13 9.44771 13 10ZM16 11C16.5523 11 17 10.5523 17 10C17 9.44771 16.5523 9 16 9C15.4477 9 15 9.44771 15 10C15 10.5523 15.4477 11 16 11ZM9 13C9 13.5523 8.55228 14 8 14C7.44772 14 7 13.5523 7 13C7 12.4477 7.44772 12 8 12C8.55228 12 9 12.4477 9 13ZM12 14C12.5523 14 13 13.5523 13 13C13 12.4477 12.5523 12 12 12C11.4477 12 11 12.4477 11 13C11 13.5523 11.4477 14 12 14ZM17 13C17 13.5523 16.5523 14 16 14C15.4477 14 15 13.5523 15 13C15 12.4477 15.4477 12 16 12C16.5523 12 17 12.4477 17 13ZM8 17C8.55228 17 9 16.5523 9 16C9 15.4477 8.55228 15 8 15C7.44772 15 7 15.4477 7 16C7 16.5523 7.44772 17 8 17ZM13 16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16C11 15.4477 11.4477 15 12 15C12.5523 15 13 15.4477 13 16ZM16 17C16.5523 17 17 16.5523 17 16C17 15.4477 16.5523 15 16 15C15.4477 15 15 15.4477 15 16C15 16.5523 15.4477 17 16 17Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><title/><g id="cross"><line class="cls-1" x1="7" x2="25" y1="7" y2="25"/><line class="cls-1" x1="7" x2="25" y1="25" y2="7"/></g></svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M1.293,11.293l4-4A1,1,0,1,1,6.707,8.707L3.414,12l3.293,3.293a1,1,0,1,1-1.414,1.414l-4-4A1,1,0,0,1,1.293,11.293Zm17.414-4a1,1,0,1,0-1.414,1.414L20.586,12l-3.293,3.293a1,1,0,1,0,1.414,1.414l4-4a1,1,0,0,0,0-1.414ZM13.039,4.726l-4,14a1,1,0,0,0,.686,1.236A1.053,1.053,0,0,0,10,20a1,1,0,0,0,.961-.726l4-14a1,1,0,1,0-1.922-.548Z"/></svg>

After

Width:  |  Height:  |  Size: 482 B

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Icons" x="-1024" y="0" width="1280" height="800" style="fill:none;"/><g id="Icons1" serif:id="Icons"><g id="Strike"></g><g id="H1"></g><g id="H2"></g><g id="H3"></g><g id="list-ul"></g><g id="hamburger-1"></g><g id="hamburger-2"></g><g id="list-ol"></g><g id="list-task"></g><g id="trash"></g><g id="vertical-menu"></g><g id="horizontal-menu"></g><g id="sidebar-2"></g><g id="Pen"></g><g id="Pen1" serif:id="Pen"><path d="M55.944,51.712l0,4.201l-33.652,-0.027l4.71,-4.174l28.942,0Zm-7.555,-43.712c1.649,0 2.505,0.128 4.752,2.011c2.294,1.921 2.707,3.419 2.803,5.087c0.102,1.795 -0.504,3.976 -2.188,5.681l-31.961,31.961c-0.52,0.475 -0.629,0.45 -0.977,0.553l-10.226,2.557c-1.472,0.299 -2.854,-1.049 -2.55,-2.55l2.557,-10.226c0.1,-0.334 0.133,-0.517 0.553,-0.976c10.696,-10.697 21.195,-21.594 32.09,-32.087c1.421,-1.335 3.497,-2.011 5.147,-2.011Zm-32.375,35.182l-1.477,1.477l-1.566,6.262l6.262,-1.566c0.493,-0.492 0.985,-0.983 1.479,-1.474l-4.698,-4.699Zm30.176,-20.573l-4.802,-4.801l-22.493,22.493l4.712,4.713c7.549,-7.448 15.196,-14.801 22.583,-22.405Zm2.826,-2.936c0.618,-0.648 1.234,-1.298 1.848,-1.951c1.673,-1.826 0.443,-5.454 -2.307,-5.578c-0.056,-0.002 -0.112,-0.002 -0.168,-0.002c-0.861,0.016 -1.699,0.372 -2.312,0.977l-1.807,1.808l4.746,4.746Z" style="fill-rule:nonzero;"/></g><g id="clock"></g><g id="external-link"></g><g id="hr"></g><g id="info"></g><g id="warning"></g><g id="plus-circle"></g><g id="minus-circle"></g><g id="vue"></g><g id="cog"></g><g id="logo"></g><g id="eye-slash"></g><g id="eye"></g><g id="toggle-off"></g><g id="shredder"></g><g id="spinner--loading--dots-" serif:id="spinner [loading, dots]"></g><g id="react"></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" version="1">
<rect style="opacity:0.2" width="25" height="13" x="3" y="15" rx="1.25" ry="1.25"/>
<path style="fill:#60924b" d="M 3,21.75 C 3,22.442 3.5575,23 4.25,23 H 26.75 C 27.442,23 28,22.442 28,21.75 V 8.25 C 28,7.5575 27.442,7 26.75,7 H 14 V 6.25 C 14,5.5575 13.443,5 12.75,5 H 4.25 C 3.5575,5 3,5.5575 3,6.25"/>
<rect style="opacity:0.2" width="25" height="13" x="3" y="13" rx="1.25" ry="1.25"/>
<rect style="fill:#e4e4e4" width="21" height="10" x="5" y="9" rx="1.25" ry="1.25"/>
<rect style="fill:#87b158" width="25" height="13" x="3" y="14" rx="1.25" ry="1.25"/>
<path style="opacity:0.1;fill:#ffffff" d="M 4.25,5 C 3.557,5 3,5.5575 3,6.25 V 7.25 C 3,6.5575 3.557,6 4.25,6 H 12.75 C 13.443,6 14,6.5575 14,7.25 V 8 H 26.75 C 27.442,8 28,8.5575 28,9.25 V 8.25 C 28,7.5575 27.442,7 26.75,7 H 14 V 6.25 C 14,5.5575 13.443,5 12.75,5 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 918 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" version="1">
<rect style="opacity:0.2" width="25" height="13" x="3" y="15" rx="1.25" ry="1.25"/>
<path style="fill:#727272" d="M 3,21.75 C 3,22.442 3.5575,23 4.25,23 H 26.75 C 27.442,23 28,22.442 28,21.75 V 8.25 C 28,7.5575 27.442,7 26.75,7 H 14 V 6.25 C 14,5.5575 13.443,5 12.75,5 H 4.25 C 3.5575,5 3,5.5575 3,6.25"/>
<rect style="opacity:0.2" width="25" height="13" x="3" y="13" rx="1.25" ry="1.25"/>
<rect style="fill:#e4e4e4" width="21" height="10" x="5" y="9" rx="1.25" ry="1.25"/>
<rect style="fill:#8e8e8e" width="25" height="13" x="3" y="14" rx="1.25" ry="1.25"/>
<path style="opacity:0.1;fill:#ffffff" d="M 4.25,5 C 3.557,5 3,5.5575 3,6.25 V 7.25 C 3,6.5575 3.557,6 4.25,6 H 12.75 C 13.443,6 14,6.5575 14,7.25 V 8 H 26.75 C 27.442,8 28,8.5575 28,9.25 V 8.25 C 28,7.5575 27.442,7 26.75,7 H 14 V 6.25 C 14,5.5575 13.443,5 12.75,5 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 918 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" version="1">
<rect style="opacity:0.2" width="25" height="16" x="3" y="12" rx="1.25" ry="1.25"/>
<path style="fill:#dd772f" d="M 3,21.75 C 3,22.442 3.5575,23 4.25,23 H 26.75 C 27.442,23 28,22.442 28,21.75 V 8.25 C 28,7.5575 27.442,7 26.75,7 H 14 V 6.25 C 14,5.5575 13.443,5 12.75,5 H 4.25 C 3.5575,5 3,5.5575 3,6.25"/>
<rect style="opacity:0.2" width="25" height="16" x="3" y="10" rx="1.25" ry="1.25"/>
<rect style="fill:#e4e4e4" width="21" height="10" x="5" y="9" rx="1.25" ry="1.25"/>
<rect style="fill:#ee923a" width="25" height="16" x="3" y="11" rx="1.25" ry="1.25"/>
<path style="opacity:0.1;fill:#ffffff" d="M 4.25 5 C 3.5575 5 3 5.5575 3 6.25 L 3 7.25 C 3 6.5575 3.5575 6 4.25 6 L 12.75 6 C 13.443 6 14 6.5575 14 7.25 L 14 8 L 26.75 8 C 27.442 8 28 8.5575 28 9.25 L 28 8.25 C 28 7.5575 27.442 7 26.75 7 L 14 7 L 14 6.25 C 14 5.5575 13.443 5 12.75 5 L 4.25 5 z"/>
</svg>

After

Width:  |  Height:  |  Size: 946 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" version="1">
<rect style="opacity:0.2" width="25" height="16" x="3" y="12" rx="1.25" ry="1.25"/>
<path style="fill:#bf4b4b" d="M 3,21.75 C 3,22.442 3.5575,23 4.25,23 H 26.75 C 27.442,23 28,22.442 28,21.75 V 8.25 C 28,7.5575 27.442,7 26.75,7 H 14 V 6.25 C 14,5.5575 13.443,5 12.75,5 H 4.25 C 3.5575,5 3,5.5575 3,6.25"/>
<rect style="opacity:0.2" width="25" height="16" x="3" y="10" rx="1.25" ry="1.25"/>
<rect style="fill:#e4e4e4" width="21" height="10" x="5" y="9" rx="1.25" ry="1.25"/>
<rect style="fill:#e25252" width="25" height="16" x="3" y="11" rx="1.25" ry="1.25"/>
<path style="opacity:0.1;fill:#ffffff" d="M 4.25 5 C 3.5575 5 3 5.5575 3 6.25 L 3 7.25 C 3 6.5575 3.5575 6 4.25 6 L 12.75 6 C 13.443 6 14 6.5575 14 7.25 L 14 8 L 26.75 8 C 27.442 8 28 8.5575 28 9.25 L 28 8.25 C 28 7.5575 27.442 7 26.75 7 L 14 7 L 14 6.25 C 14 5.5575 13.443 5 12.75 5 L 4.25 5 z"/>
</svg>

After

Width:  |  Height:  |  Size: 946 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" version="1">
<rect style="opacity:0.2" width="25" height="16" x="3" y="12" rx="1.25" ry="1.25"/>
<path style="fill:#b58f1b" d="M 3,21.75 C 3,22.442 3.5575,23 4.25,23 H 26.75 C 27.442,23 28,22.442 28,21.75 V 8.25 C 28,7.5575 27.442,7 26.75,7 H 14 V 6.25 C 14,5.5575 13.443,5 12.75,5 H 4.25 C 3.5575,5 3,5.5575 3,6.25"/>
<rect style="opacity:0.2" width="25" height="16" x="3" y="10" rx="1.25" ry="1.25"/>
<rect style="fill:#e4e4e4" width="21" height="10" x="5" y="9" rx="1.25" ry="1.25"/>
<rect style="fill:#e2b322" width="25" height="16" x="3" y="11" rx="1.25" ry="1.25"/>
<path style="opacity:0.1;fill:#ffffff" d="M 4.25 5 C 3.5575 5 3 5.5575 3 6.25 L 3 7.25 C 3 6.5575 3.5575 6 4.25 6 L 12.75 6 C 13.443 6 14 6.5575 14 7.25 L 14 8 L 26.75 8 C 27.442 8 28 8.5575 28 9.25 L 28 8.25 C 28 7.5575 27.442 7 26.75 7 L 14 7 L 14 6.25 C 14 5.5575 13.443 5 12.75 5 L 4.25 5 z"/>
</svg>

After

Width:  |  Height:  |  Size: 946 B

@@ -0,0 +1,3 @@
<?xml version="1.0" ?><svg data-name="Livello 1" id="Livello_1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><title/>
<path d="M64,39A25,25,0,1,0,89,64,25,25,0,0,0,64,39Zm0,44A19,19,0,1,1,83,64,19,19,0,0,1,64,83Z"/>
<path d="M121,48h-8.93a1,1,0,0,1-.94-.68,49.9,49.9,0,0,0-2-4.85,1,1,0,0,1,.18-1.15L115.62,35a7,7,0,0,0,0-9.9L102.89,12.38a7,7,0,0,0-9.9,0l-6.31,6.31a1,1,0,0,1-1.15.18,49.76,49.76,0,0,0-4.85-2,1,1,0,0,1-.68-.94V7a7,7,0,0,0-7-7H55a7,7,0,0,0-7,7v8.93a1,1,0,0,1-.68.94,49.9,49.9,0,0,0-4.85,2,1,1,0,0,1-1.15-.18L35,12.38a7,7,0,0,0-9.9,0L12.38,25.11a7,7,0,0,0,0,9.9l6.31,6.31a1,1,0,0,1,.18,1.15,49.76,49.76,0,0,0-2,4.85,1,1,0,0,1-.94.68H7a7,7,0,0,0-7,7V73a7,7,0,0,0,7,7h8.93a1,1,0,0,1,.94.68,49.9,49.9,0,0,0,2,4.85,1,1,0,0,1-.18,1.15L12.38,93a7,7,0,0,0,0,9.9l12.73,12.73a7,7,0,0,0,9.9,0l6.31-6.31a1,1,0,0,1,1.15-.18,49.76,49.76,0,0,0,4.85,2,1,1,0,0,1,.68.94V121a7,7,0,0,0,7,7H73a7,7,0,0,0,7-7v-8.93a1,1,0,0,1,.68-.94,49.9,49.9,0,0,0,4.85-2,1,1,0,0,1,1.15.18L93,115.62a7,7,0,0,0,9.9,0l12.73-12.73a7,7,0,0,0,0-9.9l-6.31-6.31a1,1,0,0,1-.18-1.15,49.76,49.76,0,0,0,2-4.85,1,1,0,0,1,.94-.68H121a7,7,0,0,0,7-7V55A7,7,0,0,0,121,48Zm1,25a1,1,0,0,1-1,1h-8.93a7,7,0,0,0-6.6,4.69,43.9,43.9,0,0,1-1.76,4.26,7,7,0,0,0,1.35,8l6.31,6.31a1,1,0,0,1,0,1.41L98.65,111.38a1,1,0,0,1-1.41,0l-6.31-6.31a7,7,0,0,0-8-1.35,43.88,43.88,0,0,1-4.27,1.76,7,7,0,0,0-4.68,6.6V121a1,1,0,0,1-1,1H55a1,1,0,0,1-1-1v-8.93a7,7,0,0,0-4.69-6.6,43.9,43.9,0,0,1-4.26-1.76,7,7,0,0,0-8,1.35l-6.31,6.31a1,1,0,0,1-1.41,0L16.62,98.65a1,1,0,0,1,0-1.41l6.31-6.31a7,7,0,0,0,1.35-8,43.88,43.88,0,0,1-1.76-4.27A7,7,0,0,0,15.93,74H7a1,1,0,0,1-1-1V55a1,1,0,0,1,1-1h8.93a7,7,0,0,0,6.6-4.69,43.9,43.9,0,0,1,1.76-4.26,7,7,0,0,0-1.35-8l-6.31-6.31a1,1,0,0,1,0-1.41L29.35,16.62a1,1,0,0,1,1.41,0l6.31,6.31a7,7,0,0,0,8,1.35,43.88,43.88,0,0,1,4.27-1.76A7,7,0,0,0,54,15.93V7a1,1,0,0,1,1-1H73a1,1,0,0,1,1,1v8.93a7,7,0,0,0,4.69,6.6,43.9,43.9,0,0,1,4.26,1.76,7,7,0,0,0,8-1.35l6.31-6.31a1,1,0,0,1,1.41,0l12.73,12.73a1,1,0,0,1,0,1.41l-6.31,6.31a7,7,0,0,0-1.35,8,43.88,43.88,0,0,1,1.76,4.27,7,7,0,0,0,6.6,4.68H121a1,1,0,0,1,1,1Z"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,3 @@
<svg viewBox="0 0 512 512">
<path d="M377.9 105.9L500.7 228.7c7.2 7.2 11.3 17.1 11.3 27.3s-4.1 20.1-11.3 27.3L377.9 406.1c-6.4 6.4-15 9.9-24 9.9c-18.7 0-33.9-15.2-33.9-33.9l0-62.1-128 0c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l128 0 0-62.1c0-18.7 15.2-33.9 33.9-33.9c9 0 17.6 3.6 24 9.9zM160 96L96 96c-17.7 0-32 14.3-32 32l0 256c0 17.7 14.3 32 32 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-64 0c-53 0-96-43-96-96L0 128C0 75 43 32 96 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32z"></path>
</svg>

After

Width:  |  Height:  |  Size: 502 B

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
␍<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
␍<g>
␍<path d="m28.6 11.4h5.1a1.11 1.11 0 0 0 1.1-1.1 1.32 1.32 0 0 0 -.3-.8l-7.2-7.1a.89.89 0 0 0 -.7-.3 1.11 1.11 0 0 0 -1.1 1.1v5.1a3.12 3.12 0 0 0 3.1 3.1z"/>
␍<path d="m49.83 32.45a1.43 1.43 0 0 0 -1.39-1.45h-12a1.43 1.43 0 0 0 -1.44 1.44v1.44a1.43 1.43 0 0 0 1.4 1.44h6.14l-11 11a1.42 1.42 0 0 0 0 2l1 1a1.54 1.54 0 0 0 1.09.45 1.32 1.32 0 0 0 .94-.38l11-11v6a1.43 1.43 0 0 0 1.43 1.44h1.53a1.43 1.43 0 0 0 1.47-1.4z"/>
␍<path d="m18.35 20.68h-1.46v4.93h1.46a1.56 1.56 0 0 0 1.56-1.1 3.93 3.93 0 0 0 .24-1.44 3.61 3.61 0 0 0 -.36-1.77 1.53 1.53 0 0 0 -1.44-.62z"/>
␍<path d="m11.29 20.93a1.27 1.27 0 0 0 -.84-.25h-1.45v2.23h1.46a1.18 1.18 0 0 0 .84-.28 1.09 1.09 0 0 0 .3-.86 1 1 0 0 0 -.33-.84z"/>
␍<path d="m33.24 16h-7.84a4.67 4.67 0 0 1 -4.7-4.6v-7.8a1.42 1.42 0 0 0 -1.34-1.5h-12.56a4.67 4.67 0 0 0 -4.7 4.6v29.4a4.6 4.6 0 0 0 4.62 4.6h17.88v-.1a15.92 15.92 0 0 1 10.2-14.9v-8.1a1.58 1.58 0 0 0 -1.56-1.6zm-20.75 7.65a2.93 2.93 0 0 1 -1.89.56h-1.6v2.71h-1.51v-7.55h3.24a2.64 2.64 0 0 1 1.76.63 2.3 2.3 0 0 1 .66 1.8 2.35 2.35 0 0 1 -.66 1.85zm8.66 1.9a2.73 2.73 0 0 1 -2.53 1.45h-3.26v-7.63h3.26a3.85 3.85 0 0 1 1.17.17 2.49 2.49 0 0 1 1.28.95 3.43 3.43 0 0 1 .54 1.22 5.6 5.6 0 0 1 .14 1.29 5 5 0 0 1 -.6 2.55zm7.92-4.84h-3.81v1.74h3.34v1.31h-3.34v3.16h-1.57v-7.54h5.38z"/>
␍</g>
␍</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Icons" x="-896" y="-128" width="1280" height="800" style="fill:none;"/><g id="Icons1" serif:id="Icons"><g id="Strike"></g><g id="H1"></g><g id="H2"></g><g id="H3"></g><g id="list-ul"></g><g id="hamburger-1"></g><g id="hamburger-2"></g><g id="list-ol"></g><g id="list-task"></g><g id="trash"></g><g id="vertical-menu"></g><g id="horizontal-menu"></g><g id="sidebar-2"></g><g id="Pen"></g><g id="Pen1" serif:id="Pen"></g><g id="clock"></g><g id="external-link"></g><g id="hr"></g><g id="info"></g><g id="warning"></g><g id="plus-circle"></g><g id="minus-circle"></g><g><path id="caret-down" d="M45.274,29.772c-1.094,-8.15 -8.075,-14.435 -16.525,-14.435c-9.203,0 -16.674,7.471 -16.674,16.674c0,9.203 7.471,16.675 16.674,16.675c4.423,0 8.664,-1.757 11.791,-4.884l2.862,2.863c-3.886,3.886 -9.157,6.069 -14.653,6.069c-11.437,0 -20.723,-9.285 -20.723,-20.723c0,-11.437 9.286,-20.723 20.723,-20.723c10.623,0 19.379,7.994 20.582,18.294l3.551,-3.551l3.118,3.117l-8.792,8.792l-8.796,-8.796l3.118,-3.117l3.744,3.745Z" style="fill-rule:nonzero;"/></g><g id="vue"></g><g id="cog"></g><g id="logo"></g><g id="eye-slash"></g><g id="eye"></g><g id="toggle-off"></g><g id="shredder"></g><g id="spinner--loading--dots-" serif:id="spinner [loading, dots]"></g><g id="react"></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M1.75 4.5a.25.25 0 00-.25.25v14.5c0 .138.112.25.25.25h20.5a.25.25 0 00.25-.25V4.75a.25.25 0 00-.25-.25H1.75zM0 4.75C0 3.784.784 3 1.75 3h20.5c.966 0 1.75.784 1.75 1.75v14.5A1.75 1.75 0 0122.25 21H1.75A1.75 1.75 0 010 19.25V4.75z"/>
<path d="M9 15.584V8.416a.5.5 0 01.77-.42l5.576 3.583a.5.5 0 010 .842L9.77 16.005a.5.5 0 01-.77-.42z"/>
</svg>

After

Width:  |  Height:  |  Size: 515 B

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
<footer id="footer">
<div class="legal-links">
<a href="https://nak-reinickendorf.de/impressum.php">Impressum</a>
<a href="#" onclick="openContactModal(); return false;">Admin kontaktieren</a>
<a href="https://nak-reinickendorf.de/datenschutz.php">Datenschutz</a>
</div>
</footer>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/snippets/contact_modal.php'; ?>
@@ -0,0 +1,47 @@
<?php /* Name: header.php
Beschreibung: Ausgabe des Seitenkopfes inkl. Intranet-Menü
- Profil-Leiste */
?>
<header>
<div class="header_menue unselectable" id="header_menue">
<a class="nak-logo-link" href="<?php echo $g_httpHomePath; ?>">
<img src="/intranet_repo/projects/_central/img/nak-logo.svg" alt="NAK" class="nak-logo-icon">
<div class="nak-logo-text">
<span class="nak-name">Neuapostolische Kirche</span>
<span class="nak-gemeinde">Berlin-Reinickendorf</span>
</div>
</a>
<div class="header_profile" id="header_profile">
<?php // Profilbild
// Generiere den Dateinamen basierend auf Benutzer-ID und Datum/Uhrzeit
$profileImg = glob($g_profilePicturePath."{$_SESSION['user_id']}-*.jpg");
// Überprüfe, ob eine passende Datei existiert
if (!empty($profileImg)) {
// Sortiere die Dateien nach Datum/Uhrzeit und nimm die neueste
usort($profileImg, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$latestProfileImg = $profileImg[0];
// Entferne den absoluten Pfad
$headerProfileImg = str_replace($_SERVER['DOCUMENT_ROOT'], '', $latestProfileImg);
} else {
// Standardbild, wenn keine passende Datei gefunden wurde
$headerProfileImg = "/intranet_repo/general_files/profile_pictures/default.jpg";
}
// Bild anzeigen
echo "<img src='$headerProfileImg' alt='Profil' id='header_profile_img' onclick='openCloseHeaderProfile();'>";
?>
<div class="header_profile_bar" onclick="window.location.href='<?php echo $g_httpLogoutPath; ?>';">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
<span>Abmelden</span>
</div>
</div>
</div>
</header>
@@ -0,0 +1,18 @@
<?php /* Name: html_head.php
Beschreibung: Zentraler HTML Head für das Intranet und Kalli
Anforderung: Umschliessendes Tag für diese Datei erforderlich: <head>...</head> */
?>
<meta charset="UTF-8">
<meta http-equiv="Content-Language" content="de">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="/intranet_repo/projects/_central/img/nak-logo.svg">
<link rel="shortcut icon" href="/intranet_repo/projects/_central/img/favicon.ico">
<link rel="stylesheet" href="/intranet_repo/projects/_central/layouts/nak-design.css">
<script src="/intranet_repo/projects/_central/scripts/intranet.js"></script>
<title>NAK Berlin-Reinickendorf</title>
@@ -0,0 +1,26 @@
<?php /* Name: button_edit.php
Beschreibung: Button-Ausgabe für die Umschaltung auf den Bearbeitungsmodus
GET-Parameter: action=edit
id=[$edit_id] */
?>
<div class="right">
<?php
if (isset($_GET['action']) && $_GET['action'] == 'edit') {
display_button('edit','Bearbeiten','right');
} else {
if (isset($edit_id)) {
echo "<a href='".$g_httpCurrentCleanUrl."?action=edit&id=$edit_id' style='display:table-cell;'>";
} else {
echo "<a href='".$g_httpCurrentCleanUrl."?action=edit' style='display:table-cell;'>";
}
display_button('edit','Bearbeiten','right');
echo "</a>";
}
?>
</div>
@@ -0,0 +1,119 @@
<?php /* Name: contact_modal.php
Beschreibung: Globales Kontaktformular-Modal
- Wird überall eingebunden wo technik@-Link war
- Sendet per AJAX an process_contact.php */
?>
<!-- Kontaktformular Modal -->
<div class="standard_overlay" id="contactModal" onclick="closeContactModalOnBackdrop(event)">
<div style="position:relative;width:min(520px,calc(100vw - 24px));max-height:min(700px,calc(100vh - 24px));overflow:auto;background:#FFF;border-radius:0;box-shadow:0 24px 70px rgba(0,0,0,0.36);">
<button type="button" onclick="closeContactModal()" style="position:absolute;top:14px;right:14px;width:34px;height:34px;border-radius:0;border:1px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.16);color:#FFF;font-size:1.35em;cursor:pointer;z-index:1;">×</button>
<div style="padding:24px;color:#FFF;background:radial-gradient(circle at 10% 0%,rgba(255,255,255,0.24),transparent 32%),linear-gradient(135deg,var(--theme-color),var(--theme-hover));border-radius:0;">
<div style="font-size:1.3em;font-weight:850;">Admin kontaktieren</div>
<div style="margin-top:6px;color:rgba(255,255,255,0.8);font-weight:600;">Ihr Anliegen wird an den Administrator weitergeleitet.</div>
</div>
<div style="padding:22px 24px 24px 24px;">
<form id="contactForm" onsubmit="submitContactForm(event)">
<?php
$isLoggedIn = isset($user) && !empty($user['first_name']) && !empty($user['mail']);
$prefillName = $isLoggedIn ? htmlspecialchars($user['first_name'] . ' ' . $user['last_name']) : '';
$prefillMail = $isLoggedIn ? htmlspecialchars($user['mail']) : '';
$readonlyAttr = $isLoggedIn ? 'readonly' : '';
$readonlyStyle = $isLoggedIn ? 'cursor:not-allowed;opacity:0.7;' : '';
?>
<label style="display:block;margin-bottom:4px;font-weight:750;font-size:0.88em;color:hsl(210,18%,36%);text-transform:uppercase;letter-spacing:0.04em;">Name</label>
<input type="text" name="contact_name" id="contact_name" required <?= $readonlyAttr ?>
value="<?= $prefillName ?>"
style="width:100%;padding:12px 14px;border:1px solid hsl(210,18%,84%);border-radius:0;background:hsl(210,12%,98%);font-size:1em;font-weight:650;outline:none;margin-bottom:12px;box-sizing:border-box;<?= $readonlyStyle ?>">
<label style="display:block;margin-bottom:4px;font-weight:750;font-size:0.88em;color:hsl(210,18%,36%);text-transform:uppercase;letter-spacing:0.04em;">E-Mail</label>
<input type="email" name="contact_email" id="contact_email" required <?= $readonlyAttr ?>
value="<?= $prefillMail ?>"
style="width:100%;padding:12px 14px;border:1px solid hsl(210,18%,84%);border-radius:0;background:hsl(210,12%,98%);font-size:1em;font-weight:650;outline:none;margin-bottom:12px;box-sizing:border-box;<?= $readonlyStyle ?>">
<?php if ($isLoggedIn): ?>
<div style="margin:-8px 0 12px 0;font-size:0.82em;color:hsl(210,14%,50%);font-weight:600;">Ihre Kontaktdaten werden automatisch aus Ihrem Profil übernommen.</div>
<?php endif; ?>
<label style="display:block;margin-bottom:4px;font-weight:750;font-size:0.88em;color:hsl(210,18%,36%);text-transform:uppercase;letter-spacing:0.04em;">Betreff</label>
<input type="text" name="contact_subject" id="contact_subject" required
style="width:100%;padding:12px 14px;border:1px solid hsl(210,18%,84%);border-radius:0;background:hsl(210,12%,98%);font-size:1em;font-weight:650;outline:none;margin-bottom:12px;box-sizing:border-box;">
<label style="display:block;margin-bottom:4px;font-weight:750;font-size:0.88em;color:hsl(210,18%,36%);text-transform:uppercase;letter-spacing:0.04em;">Nachricht</label>
<textarea name="contact_message" id="contact_message" rows="5" required
style="width:100%;padding:12px 14px;border:1px solid hsl(210,18%,84%);border-radius:0;background:hsl(210,12%,98%);font-size:1em;font-weight:650;outline:none;margin-bottom:16px;resize:vertical;box-sizing:border-box;font-family:inherit;"></textarea>
<div style="display:flex;justify-content:flex-end;gap:8px;">
<button type="button" onclick="closeContactModal()" style="padding:8px 16px;border:1px solid hsl(210,18%,84%);border-radius:0;background:#FFF;font-weight:850;font-size:0.86em;cursor:pointer;">Abbrechen</button>
<button type="submit" id="contactSubmitBtn" style="padding:8px 16px;border:1px solid var(--theme-color);border-radius:0;background:linear-gradient(135deg,var(--theme-color),var(--theme-hover));color:#FFF;font-weight:850;font-size:0.86em;cursor:pointer;">Nachricht senden</button>
</div>
</form>
<div id="contactFormResult" style="display:none;margin-top:12px;padding:10px 14px;border-radius:0;font-weight:700;text-align:center;"></div>
</div>
</div>
</div>
<script>
function openContactModal() {
document.getElementById('contactModal').style.display = 'flex';
}
function closeContactModal() {
document.getElementById('contactModal').style.display = 'none';
document.getElementById('contactFormResult').style.display = 'none';
document.getElementById('contactSubmitBtn').disabled = false;
document.getElementById('contactSubmitBtn').textContent = 'Nachricht senden';
}
function closeContactModalOnBackdrop(event) {
if (event.target.id === 'contactModal') closeContactModal();
}
function submitContactForm(event) {
event.preventDefault();
const form = document.getElementById('contactForm');
const resultDiv = document.getElementById('contactFormResult');
const submitBtn = document.getElementById('contactSubmitBtn');
submitBtn.disabled = true;
submitBtn.textContent = 'Wird gesendet...';
const formData = new FormData(form);
fetch('/intranet_repo/projects/adminpanel/process_contact.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.style.background = 'hsl(145, 50%, 93%)';
resultDiv.style.color = 'hsl(145, 50%, 28%)';
resultDiv.textContent = 'Nachricht erfolgreich gesendet!';
form.reset();
setTimeout(() => closeContactModal(), 2000);
} else {
resultDiv.style.background = 'hsl(0, 78%, 95%)';
resultDiv.style.color = 'hsl(0, 78%, 40%)';
resultDiv.textContent = data.error || 'Ein Fehler ist aufgetreten.';
submitBtn.disabled = false;
submitBtn.textContent = 'Nachricht senden';
}
})
.catch(() => {
resultDiv.style.display = 'block';
resultDiv.style.background = 'hsl(0, 78%, 95%)';
resultDiv.style.color = 'hsl(0, 78%, 40%)';
resultDiv.textContent = 'Verbindungsfehler. Bitte erneut versuchen.';
submitBtn.disabled = false;
submitBtn.textContent = 'Nachricht senden';
});
}
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && document.getElementById('contactModal').style.display === 'flex') {
closeContactModal();
}
});
</script>
@@ -0,0 +1,30 @@
<?php
/*
Name: countdown.php
Beschreibung: Reload der aktuellen Seite mit Countdown
*/
?>
<div class="countdown"><span id="countdown">120</span></div>
<script>
var countdown = document.getElementById("countdown").textContent;
let pauseCountdown = false; //Von außen kann der Countdown angehalten werden: pauseCountdown = true
var countdownTimer = setInterval(function() {
if (!pauseCountdown) {
countdown--;
document.getElementById('countdown').textContent = countdown;
if ((countdown <= 0)) {
clearInterval(countdownTimer);
window.location.reload();
}
}
}, 1000);
</script>
@@ -0,0 +1,21 @@
<?php /* Name: overlay_addNewUser.php
Beschreibung: Neuen Nutzer anlegen in einem Overlay
- wird in admin.php aufgerufen */
// Neuen Nutzer anlegen im Overlay (Formular) ?>
<div class="standard_overlay" id="addNewUser">
<div class="bg_blur"></div>
<form method="post" action="">
<div class="eventbox_mini center">
<div class="exit" onclick="openCloseOverlay('addNewUser')">
<?php display_button('cancel','','center'); ?>
</div>
<h3 style="margin-top:15px;margin-left:5px;margin-bottom:15px">Neuen Nutzer anlegen</h3>
<div class="item">
<input class="field" placeholder="E-Mailadresse" type="text" name="newMail"required>
<input class="button" type="submit" name="addRecord" value="Hinzufügen">
</div>
</div>
</form>
</div>
@@ -0,0 +1,73 @@
<?php /* Name: overlay_message.php
Beschreibung: Ausgabe einer Message als Toast-Notification
Anforderung: Ausgabe möglichst zum <body>-Beginn. */
// Mapping aller Intranet-Messageboxen mit Typ und Ausgabetext
function getMessageInfo($message) {
return match ($message) {
1 => ['type' => 'success', 'text' => "Passwort zurückgesetzt."],
2 => ['type' => 'success', 'text' => "Nutzer entfernt."],
3 => ['type' => 'success', 'text' => "Nutzer angelegt."],
4 => ['type' => 'error', 'text' => "Nutzer existiert bereits."],
5 => ['type' => 'success', 'text' => "Speicherung erfolgt."],
6 => ['type' => 'success', 'text' => "Einladungsmail gesendet."],
7 => ['type' => 'error', 'text' => "Bitte Name und E-Mail ausfüllen."],
8 => ['type' => 'success', 'text' => "Brandschutzschulung abgeschlossen. Vielen Dank für deine Teilnahme!"],
default => ['type' => 'error', 'text' => "Unbekannter Fehler."],
};
}
// Laden der Message
if (isset($_GET["message"])) {
$message = intval($_GET["message"]);
$messageInfo = getMessageInfo($message);
$toastType = $messageInfo['type'];
$toastText = $messageInfo['text'];
?>
<div class="nak-toast nak-toast--<?= $toastType ?>" id="nakToast">
<div class="nak-toast__icon">
<?php if ($toastType === 'success'): ?>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg>
<?php else: ?>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<?php endif; ?>
</div>
<span class="nak-toast__text"><?= htmlspecialchars($toastText) ?></span>
<button class="nak-toast__close" onclick="dismissNakToast()" aria-label="Schließen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<script>
(function() {
var toast = document.getElementById('nakToast');
if (!toast) return;
// Slide in
requestAnimationFrame(function() {
toast.classList.add('nak-toast--visible');
});
// Auto-dismiss after 4s
var timer = setTimeout(function() { dismissNakToast(); }, 4000);
toast._timer = timer;
// Clean URL
if (window.history && window.history.replaceState) {
var url = new URL(window.location);
url.searchParams.delete('message');
window.history.replaceState({}, '', url);
}
})();
function dismissNakToast() {
var toast = document.getElementById('nakToast');
if (!toast || toast.classList.contains('nak-toast--hiding')) return;
clearTimeout(toast._timer);
toast.classList.add('nak-toast--hiding');
toast.classList.remove('nak-toast--visible');
setTimeout(function() { toast.remove(); }, 350);
}
</script>
<?php } ?>
@@ -0,0 +1,81 @@
<?php /* Name: overlay_profile.php
Beschreibung: Profil eines ausgewählten Nutzers im Overlay */
?>
<?php
if (isset($_SESSION["profile_id"])) {
$pdo = require($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
$sql = "SELECT * FROM user_general_information
WHERE user_id = {$_SESSION["profile_id"]}";
$result = $pdo->query($sql);
$partner = $result->fetch(PDO::FETCH_ASSOC);
if (isset($partner["mail"])) {
$sql_groups = "SELECT * FROM user_permissions WHERE user_id = ?";
$stmt = $pdo->prepare($sql_groups);
$stmt->execute([$_SESSION["profile_id"]]);
$groups = $stmt->fetch(PDO::FETCH_ASSOC);
}
}
else {
header("Location:{$g_httpLoginPath}");
exit();
}
// Profile image
$profileImg = glob($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/profile_pictures/{$_SESSION["profile_id"]}-*.jpg");
if (!empty($profileImg)) {
usort($profileImg, function($a, $b) { return filemtime($b) - filemtime($a); });
$profileImgUrl = str_replace($_SERVER['DOCUMENT_ROOT'], '', $profileImg[0]);
} else {
$profileImgUrl = "/intranet_repo/general_files/profile_pictures/default.jpg";
}
$fullName = trim(($partner["first_name"] ?? '') . ' ' . ($partner["last_name"] ?? ''));
$isRegistered = !empty($partner["first_name"]);
?>
<div class="profile-modal-card">
<button type="button" class="exit" aria-label="Schließen" onclick="openCloseProfile(0, '<?= $profile_container_name ?>', '<?= $profile_overlay_id ?>');">×</button>
<div class="profile-modal-hero">
<img class="profile-modal-img" src="<?= htmlspecialchars($profileImgUrl) ?>" alt="Profilbild">
<div>
<?php if ($isRegistered): ?>
<div class="profile-modal-title"><?= htmlspecialchars($fullName) ?></div>
<?php else: ?>
<div class="profile-modal-title" style="color:rgba(255,255,255,0.7);">Nicht registriert</div>
<?php endif; ?>
<div class="profile-modal-subtitle">
<span><?= htmlspecialchars($partner["mail"]) ?></span>
</div>
</div>
</div>
<div class="profile-modal-body">
<?php if ($partner["phone"] != null): ?>
<div class="profile-modal-field">
<div class="profile-modal-field-label">Telefon</div>
<div class="profile-modal-field-value">
<a href="tel:0<?= htmlspecialchars($partner["phone"]) ?>">0<?= htmlspecialchars($partner["phone"]) ?></a>
</div>
</div>
<?php endif; ?>
<?php if (isset($groups)): ?>
<div class="profile-modal-field">
<div class="profile-modal-field-label">Gruppen</div>
<div class="profile-modal-chips">
<?php
foreach ($groups as $key => $value) {
if ($value == 1) {
echo '<span class="profile-modal-chip">' . htmlspecialchars($key) . '</span>';
}
}
?>
</div>
</div>
<?php endif; ?>
</div>
</div>
@@ -0,0 +1,199 @@
////==========================================//
//// GLOBALER BEREICH //
////////////////////////////////////////////////
//// Globale Konstanten
const url = new URL(window.location.href);
const basisUrl = url.origin + url.pathname;
////
////////////////////////////////////////////////
////==========================================//
////==========================================//
//// FUNKTIONEN und LISTENER //
////////////////////////////////////////////////
////
// Position der Elemente – min-height wird jetzt vollständig per CSS gelöst (nak-design.css)
// Die alte JS-Berechnung wurde entfernt, da sie nach dem Laden die Höhe reduzierte.
// JavaScript zum Erkennen der Scroll-Richtung und zum Umschalten der Klasse
// Funktioniert sowohl bei Body-Scroll als auch bei internem Container-Scroll (z.B. eventbox_table)
var lastScrollTop = 0;
var lastContainerScrollTop = 0;
function handleHeaderScroll(scrollTop, lastScroll) {
const elemHeaderMenue = document.getElementById('header_menue');
const elemHeaderTitle = document.getElementById('header_title');
// Dynamische Höhe aus CSS-Variable lesen
const headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
// Kein Einfahren, wenn der Content auf die Seite passt (kein Scrollen nötig)
if (document.documentElement.scrollHeight <= window.innerHeight) {
return;
}
// Am unteren Ende der Seite: Header wieder einblenden
const atBottom = (window.innerHeight + window.scrollY) >= (document.documentElement.scrollHeight - 5);
if (scrollTop > lastScroll && !atBottom) {
// Menü raus bei Scroll nach unten
if (elemHeaderMenue) {
elemHeaderMenue.style.top = `-${headerHeight}px`;
elemHeaderMenue.style.transition = "top 0.3s ease-in-out";
}
if (elemHeaderTitle) {
elemHeaderTitle.style.top = "0";
elemHeaderTitle.style.transition = "top 0.3s ease-in-out";
}
} else {
// Menü wieder rein bei Scroll nach oben
if (elemHeaderMenue) {
elemHeaderMenue.style.top = "0";
elemHeaderMenue.style.transition = "top 0.3s ease-in-out";
}
if (elemHeaderTitle) {
elemHeaderTitle.style.top = `${headerHeight}px`;
elemHeaderTitle.style.transition = "top 0.3s ease-in-out";
}
}
}
// Body/Document Scroll
document.addEventListener("scroll", function() {
var st = window.scrollY || document.documentElement.scrollTop;
handleHeaderScroll(st, lastScrollTop);
lastScrollTop = st <= 0 ? 0 : st;
}, false);
// Auch bei internem Container-Scroll (eventbox_table, display_within_dev)
document.addEventListener("DOMContentLoaded", function() {
const scrollContainers = document.querySelectorAll('.eventbox_table, form.display_within_dev');
scrollContainers.forEach(function(container) {
var lastCST = 0;
container.addEventListener("scroll", function() {
var st = container.scrollTop;
handleHeaderScroll(st, lastCST);
lastCST = st <= 0 ? 0 : st;
}, false);
});
});
function openCloseHeaderProfile() {
if (window.innerWidth >= 769) {
// Desktop: click on profile image goes to profile page
window.location.href = '/intranet_repo/projects/intranet/profile.php';
} else {
// Mobile: toggle expand/collapse
const hpElem = document.getElementById('header_profile');
hpElem.classList.toggle('header_profile_slideLeft');
}
}
function openCloseOverlay(id) {
//Overlay ein-/ausblenden - beim Ausblenden zurück auf eigene Seite
const elem = document.getElementById(id);
if (elem.style.display === "flex") {
elem.style.display = "none";
} else {
elem.style.display = "flex";
}
}
function openCloseProfile(id, name, overlay) {
// Profil per Klick öffnen/schließen, alle anderen immer schließen
const profiles = document.getElementsByName(name); //Ausgewählter Profilbereich (visit, calendar, ...)
const oProfile = document.getElementById(overlay); //Ausgewähltes Overlay
const aProfile = document.getElementById(id); //Ausgewähltes Profil
const aId = (id > 0) ? aProfile.id : 0; //Ausweichen, wenn kein Profil gewählt
console.log('id:' + id);
console.log('overlay:' + overlay);
console.log('name:' + name);
if (oProfile.style.display === "flex") { //Overlay ein-/ausblenden
oProfile.style.display = "none";
} else {
oProfile.style.display = "flex";
//TODO: folgender ScrollTop greift nicht, WARUM?
//oProfile.scrollTop = 0; //Hinscrollen zum Beginn des Overlays, vor allem bei @Media
}
for (let i = 0; i < profiles.length; i++) {
const cProfile = profiles[i]; //Aktuelles Profil (Style: profile_container)
if (cProfile.id === aId && cProfile.style.display !== "block") {
profiles[i].style.display = "block";
} else {
profiles[i].style.display = "none";
}
}
document.activeElement.blur(); //Fokus entfernen
// TEST console.log(id);
}
function mix(content) { //wird aktuell nicht eingesetzt, Daten werden per SQL randomized
const list = document.getElementById(content);
const items = Array.from(list.children);
console.log('Vor dem Mischen:', items.map(item => item.textContent || item.innerText));
for (let i = items.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[items[i], items[j]] = [items[j], items[i]];
}
console.log('Nach dem Mischen:', items.map(item => item.textContent || item.innerText));
items.forEach(item => list.appendChild(item));
}
function showHideText(element) {
element.classList.toggle("open");
}
// Per Taste Overlay verlassen
document.addEventListener('keydown', function(event) {
const profileVisitOverlay = document.getElementById('profile_visit_overlay');
const profileCalendarOverlay = document.getElementById('profile_calendar_overlay');
const addNewUser = document.getElementById('addNewUser');
const showMessage = document.getElementById('showMessage');
if (event.key === 'Escape') {
if (profileVisitOverlay && profileVisitOverlay.style.display === 'flex') {
openCloseProfile(0, 'profile_visit', 'profile_visit_overlay');
}
if (profileCalendarOverlay && profileCalendarOverlay.style.display === 'flex') {
openCloseProfile(0, 'profile_calendar', 'profile_calendar_overlay');
}
if (addNewUser && addNewUser.style.display === 'flex') {
openCloseOverlay('addNewUser');
}
} else if (event.key === 'Enter' || event.key === 'Return' || event.key === " ") {
if (showMessage && showMessage.style.display === 'flex') {
openCloseOverlay('showMessage'); // ausblenden
window.location.href=basisUrl; // zur aktuellen Seite zurückkehren
}
}
});
// Fokus auf passendes Element beim Laden der Intranet-Seiten
document.addEventListener("DOMContentLoaded", function() {
document.querySelector("#admintable tr[tabindex='0']")?.focus(); // Adminpanel
document.querySelector("#visittable tr[tabindex='0']")?.focus(); // Besuchsstellen
});
@@ -0,0 +1,247 @@
////==========================================//
////GLOBALER BEREICH//
////////////////////////////////////////////////
////Globale Konstanten
////
////////////////////////////////////////////////
////==========================================//
////==========================================//
////FUNKTIONEN und LISTENER//
////////////////////////////////////////////////
////
// Warten, bis das DOM vollstaendig geladen ist
document.addEventListener('DOMContentLoaded', function() {
var menuItems = document.querySelectorAll('.content-tab-bar__item[data-target]');
// Event-Listener fuer Klickereignisse auf Menueintraege
menuItems.forEach(function(item) {
item.addEventListener('click', function() {
// Entferne die aktive Klasse von allen Menueintraegen
menuItems.forEach(function(menuItem) {
menuItem.classList.remove('active');
});
// Fuege die aktive Klasse zum geklickten Menuelement hinzu
item.classList.add('active');
// Verstecke alle Inhaltsbereiche
document.querySelectorAll('.content_seelsorge').forEach(function(section) {
section.classList.remove('active');
});
// Zeige den entsprechenden Inhalt an
var contentId = item.getAttribute('data-target');
document.getElementById(contentId).classList.add('active');
});
});
// ESC-Taste schliesst offene Modals
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCalendarHelp();
closeDayModal();
}
});
});
// Wechsel der Kalenderblaetter Monat 1 und 2
function switchCalendar(type) {
const calendar_header_1 = document.querySelector('#calendar_header_1');
const calendar_header_2 = document.querySelector('#calendar_header_2');
const calendar_1 = document.querySelectorAll('[name="calendar_1"]');
const calendar_2 = document.querySelectorAll('[name="calendar_2"]');
const isLeft = type === "left";
calendar_header_1.style.display = isLeft ? "flex" : "none";
calendar_header_2.style.display = isLeft ? "none" : "flex";
calendar_1.forEach(cal => cal.style.visibility = isLeft ? "visible" : "collapse");
calendar_2.forEach(cal => cal.style.visibility = isLeft ? "collapse" : "visible");
}
// Tages-Modal oeffnen
async function openDayModal(calendarDate) {
if (!calendarDate) return;
const modal = document.getElementById('seelsorge-day-modal');
const body = document.getElementById('modal-day-body');
const title = document.getElementById('modal-day-title');
const subtitle = document.getElementById('modal-day-subtitle');
if (!modal || !body || !title || !subtitle) {
console.error('openDayModal: Modal-Elemente nicht gefunden.');
return;
}
// Modal sofort anzeigen mit Ladezustand
title.textContent = 'Laden...';
subtitle.innerHTML = '';
body.innerHTML = '<div class="seelsorge-modal-loading"><div class="loading-spinner"></div></div>';
modal.style.display = 'flex';
try {
const response = await fetch('content/calendar_day.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'calendarDate=' + encodeURIComponent(calendarDate)
});
const html = await response.text();
if (response.ok) {
body.innerHTML = html;
// Metadaten aus dem eingebetteten JSON lesen
const metaEl = body.querySelector('#modal-meta');
if (metaEl) {
const meta = JSON.parse(metaEl.textContent);
title.textContent = meta.title;
let chips = '<span class="seelsorge-modal-chip">KW ' + meta.kw + '</span>';
if (meta.feiertag) {
chips += '<span class="seelsorge-modal-chip seelsorge-chip-holiday">' + meta.feiertag + '</span>';
}
subtitle.innerHTML = chips;
metaEl.remove();
}
} else {
// PHP-Fehler direkt im Modal anzeigen
title.textContent = 'Fehler ' + response.status;
body.innerHTML = '<div style="padding:16px;font-family:monospace;font-size:0.85em;color:#d21400;white-space:pre-wrap;word-break:break-word;background:#fff5f5;border:1px solid #fcc;">'
+ (html || 'Keine Antwort vom Server.') + '</div>';
}
} catch (error) {
body.innerHTML = '<div class="seelsorge-modal-empty">Fehler beim Laden der Daten: ' + error.message + '</div>';
console.error('Fehler beim Laden der Tagesdetails:', error);
}
}
// Modal schliessen
function closeDayModal() {
const modal = document.getElementById('seelsorge-day-modal');
if (modal) modal.style.display = 'none';
}
// Backdrop-Klick schliesst Modal
function closeDayModalOnBackdrop(event) {
if (event.target === event.currentTarget) {
closeDayModal();
}
}
// Verfuegbarkeit im Modal togglen (Modal bleibt offen)
async function toggleAvailabilityModal(date) {
const btn = document.getElementById('modal-toggle-btn');
if (!btn) return;
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Wird gespeichert\u2026';
try {
const response = await fetch("toggle_availability.php", {
method: "POST",
headers: { "Content-type": "application/x-www-form-urlencoded" },
body: "date=" + encodeURIComponent(date)
});
const data = await response.json();
if (data.ok) {
// Modal-Inhalt neu laden um aktuellen Stand zu zeigen
await openDayModal(date);
// Kalender-Tageszelle visuell aktualisieren
updateCalendarCell(date, data.nowAvailable, data.availCount);
} else {
btn.textContent = originalText;
btn.disabled = false;
console.error("Fehler:", data.error);
}
} catch (error) {
btn.textContent = originalText;
btn.disabled = false;
console.error("Fehler beim Aktualisieren der Verfuegbarkeit:", error);
}
}
// Kalender-Tageszelle visuell aktualisieren ohne Page-Reload
function updateCalendarCell(date, nowAvailable, availCount) {
var cells = document.querySelectorAll('#calendar_day_item');
cells.forEach(function(cell) {
var onclick = cell.getAttribute('onclick') || '';
if (onclick.indexOf(date) === -1) return;
// Alle Verfuegbarkeits-Klassen entfernen
cell.classList.remove('available_single', 'available_multiple', 'normalday');
if (availCount >= 2) {
cell.classList.add('available_multiple');
} else if (availCount === 1) {
cell.classList.add('available_single');
} else {
cell.classList.add('normalday');
}
});
}
// Profil eines Diakons aus dem Kalender-Modal oeffnen
var _lastCalendarDate = null;
function openCalendarProfile(userId) {
// Datum merken, damit wir beim Schliessen zurueckkehren koennen
var metaEl = document.getElementById('modal-meta');
var bodyEl = document.getElementById('modal-day-body');
if (!_lastCalendarDate && bodyEl) {
// Datum aus dem Toggle-Button extrahieren
var toggleBtn = bodyEl.querySelector('#modal-toggle-btn');
if (toggleBtn) {
var onclickAttr = toggleBtn.getAttribute('onclick') || '';
var match = onclickAttr.match(/'(\d{4}-\d{2}-\d{2})'/);
if (match) _lastCalendarDate = match[1];
}
}
closeDayModal();
openCloseProfile(userId, 'profile_calendar', 'profile_calendar_overlay');
}
// Globalen Listener fuer Kalender-Profil-Overlay: bei Schliessen Tagesmodal wieder oeffnen
document.addEventListener('DOMContentLoaded', function() {
// MutationObserver auf das Kalender-Profil-Overlay
var calOverlay = document.getElementById('profile_calendar_overlay');
if (calOverlay) {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.attributeName === 'style' && calOverlay.style.display === 'none' && _lastCalendarDate) {
var date = _lastCalendarDate;
_lastCalendarDate = null;
openDayModal(date);
}
});
});
observer.observe(calOverlay, { attributes: true, attributeFilter: ['style'] });
}
});
// Hilfe-Modal oeffnen/schliessen
function openCalendarHelp() {
var modal = document.getElementById('calendar-help-modal');
if (modal) modal.style.display = 'flex';
}
function closeCalendarHelp() {
var modal = document.getElementById('calendar-help-modal');
if (modal) modal.style.display = 'none';
}
@@ -0,0 +1,32 @@
<?php
/* Zentrale Reiternavigation des Adminpanels.
* Erwartet: $pdo (PDO) und optional $adminActiveTab.
*/
$adminActiveTab = $adminActiveTab ?? '';
$adminInboxBadgeCount = 0;
try {
$adminInboxBadgeCount = (int)$pdo->query("SELECT COUNT(*) FROM admin_messages WHERE status = 'pending'")->fetchColumn();
} catch (Throwable $e) {
// Die Navigation soll auch dann benutzbar bleiben, wenn das Postfach temporär nicht verfügbar ist.
$adminInboxBadgeCount = 0;
}
$adminTabs = [
'users' => ['label' => 'Nutzerverwaltung', 'href' => 'admin.php'],
'inbox' => ['label' => 'Postfach', 'href' => 'admin_inbox.php', 'badge' => $adminInboxBadgeCount],
'kalli' => ['label' => 'Kalli', 'href' => 'admin_kalli.php'],
'brandschutz' => ['label' => 'Brandschutz', 'href' => 'admin_brandschutz.php'],
'notices' => ['label' => 'Meldungen', 'href' => 'admin_notices.php'],
];
?>
<div class="content-tab-bar">
<?php foreach ($adminTabs as $tabKey => $tab): ?>
<a class="content-tab-bar__item<?= $adminActiveTab === $tabKey ? ' active' : '' ?>" href="<?= htmlspecialchars($tab['href'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($tab['label'], ENT_QUOTES, 'UTF-8') ?>
<?php if (!empty($tab['badge'])): ?>
<span class="content-tab-bar__badge"><?= (int)$tab['badge'] ?></span>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,677 @@
<?php /* Name: admin_brandschutz.php
Beschreibung: Brandschutz-Schulungsverwaltung im Adminpanel
- Übersicht aller Nutzer mit Brandschutz-Rolle
- Fälligkeitsstatus und Details
- Manuelle Anpassung des Schulungsdatums */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
// Hilfsfunktionen
function getProfileImageUrl_bs(array $row, string $g_profilePicturePath): string {
$profileImg = glob($g_profilePicturePath."{$row['user_id']}-*.jpg");
if (!empty($profileImg)) {
usort($profileImg, function($a, $b) {
return filemtime($b) - filemtime($a);
});
return str_replace($_SERVER['DOCUMENT_ROOT'], '', $profileImg[0]);
}
return '/intranet_repo/general_files/profile_pictures/default.jpg';
}
function getDisplayName_bs(array $row): string {
$fullName = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? ''));
return $fullName !== '' ? $fullName : ($row['mail'] ?? 'Unbekannt');
}
// Schulungsdatum manuell anpassen (POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bs_update_date'])) {
$editUserId = (int)($_POST['bs_user_id'] ?? 0);
$newDate = $_POST['bs_new_date'] ?? '';
if ($editUserId > 0 && preg_match('/^\d{4}-\d{2}-\d{2}$/', $newDate)) {
// Prüfen ob das neue Datum innerhalb der Frist liegt (365 Tage ab Datum > heute)
$newDueDate = date('Y-m-d', strtotime($newDate . ' +365 days'));
$isWithinDeadline = (strtotime($newDueDate) >= strtotime(date('Y-m-d')));
if ($isWithinDeadline) {
// Datum in der Frist: Zähler zurücksetzen
$stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date, reminder_count = 0, admin_reminder_count = 0");
} else {
// Datum außerhalb der Frist: Nur Datum aktualisieren, Zähler beibehalten
$stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date");
}
$stmtUpsert->execute([$editUserId, $newDate]);
// Systemnachricht in erledigte Admin-Nachrichten
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';
$stmtUser = $pdo->prepare("SELECT first_name, last_name, mail FROM user_general_information WHERE user_id = ?");
$stmtUser->execute([$editUserId]);
$editedUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
$editedName = trim(($editedUser['first_name'] ?? '') . ' ' . ($editedUser['last_name'] ?? ''));
if (empty($editedName)) $editedName = $editedUser['mail'] ?? 'Unbekannt';
$formattedDate = date('d.m.Y', strtotime($newDate));
$sysSubject = "Brandschutz-Schulungsdatum angepasst: {$editedName}";
$sysMessage = "{$adminName} hat das Schulungsdatum von {$editedName} manuell auf den {$formattedDate} gesetzt.";
$stmtMsg = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$senderMail = $editedUser['mail'] ?? '';
$stmtMsg->execute([$adminName, $senderMail, $sysSubject, $sysMessage]);
header("Location: admin_brandschutz.php?message=5");
exit();
}
}
// Brandschutz-Nutzer laden
$sql = "SELECT ugi.user_id, ugi.first_name, ugi.last_name, ugi.mail,
bt.last_training_date, bt.reminder_count, bt.admin_reminder_count,
CASE WHEN ul.user_id IS NOT NULL THEN 1 ELSE 0 END AS IstRegistriert
FROM user_permissions up
INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id
LEFT JOIN brandschutz_training bt ON ugi.user_id = bt.user_id
LEFT JOIN user_login ul ON ugi.user_id = ul.user_id
WHERE up.\"Brandschutz\" = 1
ORDER BY bt.last_training_date ASC, ugi.last_name ASC, ugi.first_name ASC";
$bsUsers = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.admin-users-shell {
padding: 18px;
background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
min-height: 100%;
}
.admin-table-card {
overflow: hidden;
border: 1px solid rgba(120, 140, 160, 0.22);
background: rgba(255,255,255,0.86);
box-shadow: 0 20px 45px rgba(31, 38, 46, 0.10), 0 1px 0 rgba(255,255,255,0.9) inset;
}
.admin-list-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 12px 18px;
border-bottom: 1px solid rgba(120, 140, 160, 0.16);
background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.72));
}
.admin-list-title {
font-size: 1.05em;
font-weight: 800;
color: hsl(210, 20%, 15%);
}
.admin-list-hint {
font-size: 0.86em;
color: hsl(210, 15%, 46%);
}
table.admin-user-table {
display: table !important;
width: calc(100% - 24px);
margin: 8px 12px 14px 12px;
border-collapse: separate;
border-spacing: 0 8px;
padding: 0;
background: transparent;
overflow: visible;
}
.admin-user-table thead th {
position: sticky;
top: 0;
padding: 10px 14px;
color: hsl(210, 16%, 46%);
font-size: 0.78em;
font-weight: 800;
text-align: left;
letter-spacing: 0.04em;
text-transform: uppercase;
background: transparent !important;
border: none !important;
}
.admin-user-table tbody tr.admin-user-row {
position: relative;
cursor: pointer;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 6px 18px rgba(31, 38, 46, 0.06);
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.admin-user-table tbody tr.admin-user-row:hover {
transform: translateY(-1px);
background: #FFFFFF;
box-shadow: 0 12px 26px rgba(31, 38, 46, 0.13);
}
.admin-user-table td {
padding: 12px 14px !important;
vertical-align: middle;
border: none !important;
background: transparent !important;
}
.admin-user-table tbody tr.admin-user-row td:first-child {
width: 82px;
min-width: 82px;
max-width: 82px;
}
.admin-profile-button {
display: block;
width: 58px;
height: 58px;
border-radius: 12px;
overflow: hidden;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
box-shadow: 0 8px 20px rgba(31, 38, 46, 0.18), 0 0 0 3px rgba(255,255,255,0.95);
}
.admin-profile-button img {
display: block;
width: 58px !important;
height: 58px !important;
object-fit: cover;
border-radius: 12px !important;
transition: transform 0.2s ease;
}
.admin-user-table tbody tr.admin-user-row:hover .admin-profile-button img {
transform: scale(1.08);
}
.admin-user-name {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 1em;
font-weight: 800;
color: hsl(210, 20%, 15%);
}
.bs-status-badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
font-size: 0.78em;
font-weight: 800;
}
.bs-status-ok {
color: hsl(145, 50%, 28%);
background: hsl(145, 50%, 93%);
border: 1px solid hsl(145, 50%, 82%);
}
.bs-status-warning {
color: hsl(35, 78%, 30%);
background: hsl(42, 90%, 93%);
border: 1px solid hsl(42, 90%, 78%);
}
.bs-status-overdue {
color: hsl(0, 65%, 38%);
background: hsl(0, 65%, 95%);
border: 1px solid hsl(0, 65%, 82%);
}
.bs-status-critical {
color: #FFFFFF;
background: hsl(0, 65%, 45%);
border: 1px solid hsl(0, 65%, 38%);
}
.bs-due-date {
font-size: 0.9em;
font-weight: 650;
color: hsl(210, 14%, 35%);
}
/* Modal */
.admin-modal-card {
position: relative;
width: min(720px, calc(100vw - 24px));
max-height: min(760px, calc(100vh - 24px));
overflow: auto;
padding: 0;
background: #FFFFFF;
box-shadow: 0 24px 70px rgba(0,0,0,0.36);
}
.admin-modal-hero {
position: relative;
display: grid;
grid-template-columns: 112px 1fr;
gap: 18px;
align-items: center;
padding: 24px;
color: #FFFFFF;
background:
radial-gradient(circle at 10% 0%, rgba(255,255,255,0.24), transparent 32%),
linear-gradient(135deg, hsl(25, 90%, 45%), hsl(25, 95%, 35%));
}
.admin-modal-profile-img {
width: 104px;
height: 104px;
object-fit: cover;
border-radius: 16px;
box-shadow: 0 12px 28px rgba(0,0,0,0.32), 0 0 0 4px rgba(255,255,255,0.18);
}
.admin-modal-title {
margin: 0 42px 6px 0;
font-size: 1.45em;
font-weight: 850;
color: #FFFFFF;
}
.admin-modal-subtitle {
display: flex;
flex-flow: row wrap;
gap: 8px;
align-items: center;
margin-bottom: 10px;
color: rgba(255,255,255,0.86);
font-weight: 600;
}
.admin-modal-chip {
display: inline-flex;
align-items: center;
padding: 4px 9px;
border-radius: 9999px;
background: rgba(255,255,255,0.14);
border: 1px solid rgba(255,255,255,0.18);
font-size: 0.82em;
}
.admin-modal-body {
padding: 22px 24px 24px 24px;
}
.admin-section-title {
margin: 0 0 10px 0;
font-size: 0.9em;
font-weight: 850;
letter-spacing: 0.04em;
text-transform: uppercase;
color: hsl(210, 18%, 36%);
}
.admin-modal-card .exit {
position: absolute;
top: 14px;
right: 14px;
width: 34px;
height: 34px;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.16);
color: #FFFFFF;
font-size: 1.35em;
line-height: 1;
cursor: pointer;
}
.bs-detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.bs-detail-card {
padding: 14px 16px;
border: 1px solid hsl(210, 18%, 88%);
background: linear-gradient(180deg, #FFFFFF, hsl(210, 12%, 98%));
}
.bs-detail-label {
margin-bottom: 3px;
color: hsl(210, 14%, 46%);
font-size: 0.74em;
font-weight: 850;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.bs-detail-value {
color: hsl(210, 20%, 18%);
font-size: 1.02em;
font-weight: 850;
}
.bs-detail-value.overdue {
color: hsl(0, 65%, 42%);
}
.bs-detail-value.ok {
color: hsl(145, 50%, 32%);
}
.admin-modal-actions {
display: flex;
justify-content: space-between;
gap: 10px;
border-top: 1px solid hsl(210, 18%, 90%);
padding-top: 16px;
}
.admin-modal-actions button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 8px 13px;
border: 1px solid hsl(210, 18%, 84%);
background: #FFFFFF;
color: hsl(210, 20%, 22%);
font-size: 0.86em;
font-weight: 850;
cursor: pointer;
box-shadow: 0 4px 12px rgba(31,38,46,0.06);
}
.admin-modal-actions button:hover {
background: hsl(210, 57%, 96%);
}
.admin-save-button {
color: #FFFFFF !important;
background: linear-gradient(135deg, var(--nak-orange), hsl(25, 95%, 40%)) !important;
border-color: hsl(25, 95%, 40%) !important;
}
.bs-date-input {
padding: 10px 12px;
font-size: var(--font-size-sm);
font-family: var(--font-primary);
border: 1px solid var(--gray-300);
background: var(--gray-50);
width: 100%;
box-sizing: border-box;
font-weight: 650;
}
.bs-date-input:focus {
border-color: hsl(25, 80%, 55%);
background: #FFFFFF;
outline: none;
box-shadow: 0 0 0 4px hsla(25, 80%, 55%, 0.14);
}
@media (max-width: 780px) {
.admin-users-shell { padding: 10px; }
.admin-list-header {
flex-direction: column;
align-items: flex-start;
padding: 14px;
gap: 8px;
}
table.admin-user-table {
display: block !important;
width: auto;
margin: 8px;
}
.admin-user-table thead { display: none; }
.admin-user-table tbody {
display: grid;
gap: 10px;
}
.admin-user-table tbody tr.admin-user-row {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 0 12px;
padding: 12px;
border: 1px solid rgba(120, 140, 160, 0.16);
box-shadow: 0 8px 22px rgba(31, 38, 46, 0.08);
}
.admin-user-table tbody tr.admin-user-row td {
display: block;
padding: 0 !important;
}
.admin-user-table tbody tr.admin-user-row td:first-child {
grid-row: 1 / span 3;
width: 72px;
min-width: 72px;
max-width: 72px;
}
.admin-profile-button,
.admin-profile-button img {
width: 64px !important;
height: 64px !important;
border-radius: 12px !important;
}
.admin-modal-card {
width: calc(100vw - 12px);
max-height: calc(100dvh - 12px);
}
.admin-modal-hero {
grid-template-columns: 78px minmax(0, 1fr);
gap: 14px;
padding: 18px;
}
.admin-modal-profile-img {
width: 72px;
height: 72px;
border-radius: 14px;
}
.admin-modal-title {
font-size: 1.15em;
margin-right: 40px;
}
.admin-modal-body { padding: 18px; }
.bs-detail-grid { grid-template-columns: 1fr; }
.admin-modal-actions { flex-direction: column-reverse; }
.admin-modal-actions button { width: 100%; }
}
</style>
</head>
<body class="theme-admin">
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<div class="header_title ht_admin text_white unselectable" id="header_title">
<span class="header_title_text">Adminpanel</span>
</div>
<div class="content" id="fullsize">
<?php $adminActiveTab = 'brandschutz'; require __DIR__.'/_tabs.php'; ?>
<div class="admin-users-shell">
<div class="admin-table-card">
<div class="admin-list-header">
<div>
<span class="admin-list-title">Brandschutz-Schulungen</span>
<div class="admin-list-hint"><?= count($bsUsers) ?> Nutzer mit Brandschutz-Rolle</div>
</div>
</div>
<table class="admin-user-table" id="bsTable">
<thead>
<tr>
<th>Profil</th>
<th>Name</th>
<th>Nächste Fälligkeit</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($bsUsers as $bsUser): ?>
<?php
$nameOutput = getDisplayName_bs($bsUser);
$profileUrl = getProfileImageUrl_bs($bsUser, $g_profilePicturePath);
$lastTraining = $bsUser['last_training_date'];
$reminderCount = (int)($bsUser['reminder_count'] ?? 0);
$adminReminderCount = (int)($bsUser['admin_reminder_count'] ?? 0);
// Berechne Fälligkeit
if (empty($lastTraining)) {
$dueDate = null;
$daysOverdue = null;
$statusText = 'Nie geschult';
$statusClass = 'bs-status-overdue';
$dueDateDisplay = 'Sofort fällig';
} else {
$dueDate = date('Y-m-d', strtotime($lastTraining . ' +365 days'));
$today = date('Y-m-d');
$diff = (strtotime($dueDate) - strtotime($today)) / 86400;
if ($diff > 30) {
$statusText = 'Aktuell';
$statusClass = 'bs-status-ok';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
} elseif ($diff > 0) {
$statusText = 'Bald fällig';
$statusClass = 'bs-status-warning';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
} elseif ($diff > -120) {
$daysOverdue = abs((int)$diff);
$statusText = "{$daysOverdue} Tage überfällig";
$statusClass = 'bs-status-overdue';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
} else {
$daysOverdue = abs((int)$diff);
$statusText = "{$daysOverdue} Tage überfällig";
$statusClass = 'bs-status-critical';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
}
}
?>
<tr class="admin-user-row" tabindex="0" role="button" aria-label="<?= htmlspecialchars($nameOutput) ?> Details" onclick="openBsModal(<?= (int)$bsUser['user_id'] ?>)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openBsModal(<?= (int)$bsUser['user_id'] ?>)}">
<td>
<button class="admin-profile-button" type="button" tabindex="-1" aria-hidden="true">
<img src="<?= htmlspecialchars($profileUrl) ?>" alt="Profilbild von <?= htmlspecialchars($nameOutput) ?>">
</button>
</td>
<td>
<span class="admin-user-name"><?= htmlspecialchars($nameOutput) ?></span>
</td>
<td>
<span class="bs-due-date"><?= $dueDateDisplay ?></span>
</td>
<td>
<span class="bs-status-badge <?= $statusClass ?>"><?= $statusText ?></span>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($bsUsers)): ?>
<tr><td colspan="4" style="padding:24px !important;text-align:center;color:hsl(210,20%,45%);background:#FFF !important;">Keine Nutzer mit der Rolle "Brandschutz" gefunden.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Modals für jeden Brandschutz-Nutzer -->
<?php foreach ($bsUsers as $bsUser): ?>
<?php
$nameOutput = getDisplayName_bs($bsUser);
$profileUrl = getProfileImageUrl_bs($bsUser, $g_profilePicturePath);
$lastTraining = $bsUser['last_training_date'];
$reminderCount = (int)($bsUser['reminder_count'] ?? 0);
$adminReminderCount = (int)($bsUser['admin_reminder_count'] ?? 0);
if (empty($lastTraining)) {
$dueDate = null;
$dueDateDisplay = 'Sofort fällig';
$lastTrainingDisplay = 'Nie';
$overdueText = 'Keine Schulung hinterlegt – sofort fällig';
$overdueClass = 'overdue';
} else {
$dueDate = date('Y-m-d', strtotime($lastTraining . ' +365 days'));
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
$lastTrainingDisplay = date('d.m.Y', strtotime($lastTraining));
$today = date('Y-m-d');
$diff = (strtotime($dueDate) - strtotime($today)) / 86400;
if ($diff > 0) {
$overdueText = 'Nicht überfällig (' . (int)$diff . ' Tage verbleibend)';
$overdueClass = 'ok';
} else {
$daysOverdue = abs((int)$diff);
$overdueText = "{$daysOverdue} Tage überfällig";
$overdueClass = 'overdue';
}
}
?>
<div class="standard_overlay" id="bsModal-<?= (int)$bsUser['user_id'] ?>" onclick="if(event.target.id==='bsModal-<?= (int)$bsUser['user_id'] ?>')closeBsModal(<?= (int)$bsUser['user_id'] ?>)">
<div class="admin-modal-card">
<button type="button" class="exit" aria-label="Schließen" onclick="closeBsModal(<?= (int)$bsUser['user_id'] ?>)">×</button>
<div class="admin-modal-hero">
<img class="admin-modal-profile-img" src="<?= htmlspecialchars($profileUrl) ?>" alt="Profilbild von <?= htmlspecialchars($nameOutput) ?>">
<div>
<div class="admin-modal-title"><?= htmlspecialchars($nameOutput) ?></div>
<div class="admin-modal-subtitle">
<span><?= htmlspecialchars($bsUser['mail'] ?? '') ?></span>
<span class="admin-modal-chip">Brandschutz</span>
</div>
<div class="admin-modal-subtitle" style="margin-bottom:0;">Schulungsdetails &amp; Fälligkeiten</div>
</div>
</div>
<div class="admin-modal-body">
<div class="admin-section-title">Schulungsstatus</div>
<div class="bs-detail-grid">
<div class="bs-detail-card">
<div class="bs-detail-label">Letzte Schulung</div>
<div class="bs-detail-value"><?= $lastTrainingDisplay ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Nächste Fälligkeit</div>
<div class="bs-detail-value"><?= $dueDateDisplay ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Überfälligkeit</div>
<div class="bs-detail-value <?= $overdueClass ?>"><?= $overdueText ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Erinnerungsmails gesendet</div>
<div class="bs-detail-value"><?= $reminderCount ?></div>
</div>
<?php if ($adminReminderCount > 0): ?>
<div class="bs-detail-card">
<div class="bs-detail-label">Admin-Eskalationen</div>
<div class="bs-detail-value overdue"><?= $adminReminderCount ?></div>
</div>
<?php endif; ?>
</div>
<div class="admin-section-title">Schulungsdatum anpassen</div>
<form method="post" action="">
<input type="hidden" name="bs_user_id" value="<?= (int)$bsUser['user_id'] ?>">
<div style="display:flex;gap:10px;align-items:flex-end;margin-bottom:8px;">
<div style="flex:1;">
<label style="display:block;font-size:var(--font-size-xs);font-weight:600;color:var(--gray-600);margin-bottom:4px;">Datum der letzten Schulung</label>
<input type="date" name="bs_new_date" class="bs-date-input" value="<?= htmlspecialchars($lastTraining ?? '') ?>" required>
</div>
</div>
<p style="margin:0 0 16px 0;font-size:0.82em;color:hsl(210,14%,46%);font-weight:600;">Setzt das Schulungsdatum und alle Erinnerungszähler zurück.</p>
<div class="admin-modal-actions">
<div>
<button type="button" onclick="closeBsModal(<?= (int)$bsUser['user_id'] ?>)">Schließen</button>
</div>
<div>
<button type="submit" name="bs_update_date" value="1" class="admin-save-button">Datum speichern</button>
</div>
</div>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div><!-- content -->
<script>
function openBsModal(userId) {
var modal = document.getElementById('bsModal-' + userId);
if (modal) modal.style.display = 'flex';
}
function closeBsModal(userId) {
var modal = document.getElementById('bsModal-' + userId);
if (modal) modal.style.display = 'none';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.standard_overlay[id^="bsModal-"]').forEach(function(m) {
m.style.display = 'none';
});
}
});
</script>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,564 @@
<?php /* Name: admin_inbox.php
Beschreibung: Admin-Nachrichteninbox
- Registrierungsanfragen
- Kontaktformular-Anfragen
- Systemnachrichten
- Seelsorge-Nachrichten
- Info-Nachrichten */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
// Typ-Konfiguration für Inbox
$typeConfig = [
'registration' => ['label' => 'Registrierung', 'class' => 'inbox-type-registration'],
'contact' => ['label' => 'Kontakt', 'class' => 'inbox-type-contact'],
'system' => ['label' => 'System', 'class' => 'inbox-type-system'],
];
// Filter
$activeFilter = $_GET['filter'] ?? 'all';
$validFilters = array_merge(['all'], array_keys($typeConfig));
if (!in_array($activeFilter, $validFilters, true)) $activeFilter = 'all';
function getTypeLabel($type, $typeConfig) {
return $typeConfig[$type]['label'] ?? ucfirst($type);
}
function getTypeClass($type, $typeConfig) {
return $typeConfig[$type]['class'] ?? 'inbox-type-system';
}
// Kontaktnachricht als erledigt markieren
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["mark_done"])) {
$msgId = (int)$_POST["mark_done"];
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$msgId]);
header("Location: admin_inbox.php?inbox_status=done");
exit();
}
// Brandschutz-Eskalation: Admin bestätigt persönliche Erinnerung
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["bs_escalation_confirm"])) {
$msgId = (int)$_POST["bs_escalation_msg_id"];
$bsUserId = (int)$_POST["bs_escalation_user_id"];
// Nachricht als erledigt markieren
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$msgId]);
// Log-Eintrag erstellen
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';
$stmtUser = $pdo->prepare("SELECT first_name, last_name, mail FROM user_general_information WHERE user_id = ?");
$stmtUser->execute([$bsUserId]);
$bsUserData = $stmtUser->fetch(PDO::FETCH_ASSOC);
$bsUserName = trim(($bsUserData['first_name'] ?? '') . ' ' . ($bsUserData['last_name'] ?? ''));
if (empty($bsUserName)) $bsUserName = $bsUserData['mail'] ?? 'Unbekannt';
$logSubject = "Persönliche Brandschutz-Erinnerung bestätigt: {$bsUserName}";
$logMessage = "{$adminName} hat bestätigt, {$bsUserName} persönlich an die ausstehende Brandschutzunterweisung erinnert zu haben.";
$stmtLog = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$senderMail = $bsUserData['mail'] ?? '';
$stmtLog->execute([$adminName, $senderMail, $logSubject, $logMessage]);
header("Location: admin_inbox.php?inbox_status=done");
exit();
}
// Nachrichten laden (mit optionalem Filter)
$filterClause = '';
if ($activeFilter !== 'all') {
$filterClause = " AND type = '" . $activeFilter . "'";
}
$pendingMessages = $pdo->query("SELECT * FROM admin_messages WHERE status = 'pending'{$filterClause} ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC);
$doneMessages = $pdo->query("SELECT * FROM admin_messages WHERE status = 'done'{$filterClause} ORDER BY processed_at DESC LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
// Rollen laden für Registrierungs-Modal
$permissionRoles = [];
$rolesResult = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name = 'user_permissions' AND table_schema = 'public'");
while ($col = $rolesResult->fetch(PDO::FETCH_ASSOC)) {
if (!in_array($col['column_name'], ['p_id', 'user_id'], true)) {
$permissionRoles[] = $col['column_name'];
}
}
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.inbox-shell {
padding: 18px;
background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
min-height: 100%;
}
.inbox-card {
overflow: hidden;
border: 1px solid rgba(120, 140, 160, 0.22);
background: rgba(255,255,255,0.86);
box-shadow: 0 20px 45px rgba(31, 38, 46, 0.10), 0 1px 0 rgba(255,255,255,0.9) inset;
margin-bottom: 18px;
}
.inbox-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 18px 12px 18px;
border-bottom: 1px solid rgba(120, 140, 160, 0.16);
background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.72));
}
.inbox-title {
font-size: 1.05em;
font-weight: 800;
color: hsl(210, 20%, 15%);
}
.inbox-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 7px;
background: hsl(0, 78%, 47%);
color: #FFFFFF;
font-size: 0.78em;
font-weight: 850;
}
.inbox-item {
padding: 16px 18px;
border-bottom: 1px solid rgba(120, 140, 160, 0.1);
transition: background 0.18s ease;
}
.inbox-item:last-child { border-bottom: none; }
.inbox-item:hover { background: hsl(210, 57%, 97%); }
.inbox-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.inbox-item-type {
display: inline-flex;
align-items: center;
padding: 3px 8px;
font-size: 0.72em;
font-weight: 800;
}
.inbox-type-registration {
color: hsl(210, 57%, 34%);
background: hsl(210, 57%, 94%);
border: 1px solid hsl(210, 57%, 88%);
}
.inbox-type-contact {
color: hsl(145, 50%, 32%);
background: hsl(145, 50%, 93%);
border: 1px solid hsl(145, 50%, 85%);
}
.inbox-type-system {
color: hsl(30, 70%, 36%);
background: hsl(30, 70%, 94%);
border: 1px solid hsl(30, 70%, 85%);
}
.inbox-item-date {
font-size: 0.82em;
color: hsl(210, 14%, 50%);
font-weight: 600;
}
.inbox-item-subject {
font-weight: 800;
color: hsl(210, 20%, 15%);
margin-bottom: 4px;
}
.inbox-item-sender {
font-size: 0.9em;
color: hsl(210, 14%, 40%);
margin-bottom: 8px;
}
.inbox-item-message {
font-size: 0.9em;
color: hsl(210, 12%, 35%);
white-space: pre-wrap;
background: hsl(210, 12%, 98%);
padding: 10px 12px;
border: 1px solid hsl(210, 12%, 92%);
margin-bottom: 10px;
}
.inbox-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.inbox-actions form { display: inline; }
.inbox-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 6px 12px;
border: 1px solid hsl(210, 18%, 84%);
background: #FFFFFF;
color: hsl(210, 20%, 22%);
font-size: 0.82em;
font-weight: 850;
cursor: pointer;
text-decoration: none;
box-shadow: 0 4px 12px rgba(31,38,46,0.06);
transition: background 0.18s ease;
}
.inbox-btn:hover { background: hsl(210, 57%, 96%); }
.inbox-btn-approve {
color: #FFFFFF !important;
background: linear-gradient(135deg, var(--nak-darkgreen), #005a10) !important;
border-color: #005a10 !important;
}
.inbox-btn-reject {
color: var(--nak-red) !important;
border-color: hsl(352, 72%, 88%) !important;
background: hsl(352, 72%, 97%) !important;
}
.inbox-btn-done {
color: hsl(210, 14%, 46%) !important;
background: hsl(210, 12%, 95%) !important;
border-color: hsl(210, 12%, 88%) !important;
}
.inbox-empty {
padding: 24px;
text-align: center;
color: hsl(210, 14%, 50%);
font-weight: 600;
}
.inbox-section-title {
margin: 18px 0 10px 0;
font-size: 0.88em;
font-weight: 850;
text-transform: uppercase;
letter-spacing: 0.04em;
color: hsl(210, 14%, 46%);
}
.inbox-done-item {
opacity: 0.6;
}
@media (max-width: 780px) {
.inbox-shell { padding: 10px; }
.inbox-item-header { flex-direction: column; align-items: flex-start; gap: 4px; }
.inbox-filter-bar { gap: 6px; }
.inbox-filter-btn { font-size: 0.72em; padding: 5px 10px; }
}
.inbox-filter-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.inbox-filter-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 14px;
border: 1px solid hsl(210, 18%, 84%);
background: #FFF;
color: hsl(210, 18%, 42%);
font-size: 0.82em;
font-weight: 750;
cursor: pointer;
transition: all 0.18s ease;
text-decoration: none;
}
.inbox-filter-btn:hover { background: hsl(210, 57%, 96%); }
.inbox-filter-btn.active {
background: hsl(210, 57%, 94%);
border-color: hsl(210, 57%, 72%);
color: hsl(210, 57%, 30%);
font-weight: 850;
}
</style>
</head>
<body class="theme-admin">
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<?php if (isset($_GET["inbox_status"])): ?>
<?php
$inboxStatusMap = [
'done' => ['type' => 'success', 'text' => 'Nachricht als erledigt markiert.'],
'approved' => ['type' => 'success', 'text' => 'Registrierung freigegeben.'],
'rejected' => ['type' => 'success', 'text' => 'Registrierung abgelehnt.'],
'error' => ['type' => 'error', 'text' => 'Ein Fehler ist aufgetreten.'],
];
$statusKey = $_GET["inbox_status"];
$statusInfo = $inboxStatusMap[$statusKey] ?? ['type' => 'success', 'text' => 'Aktion ausgeführt.'];
?>
<div class="nak-toast nak-toast--<?= $statusInfo['type'] ?>" id="nakToast">
<div class="nak-toast__icon">
<?php if ($statusInfo['type'] === 'success'): ?>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg>
<?php else: ?>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<?php endif; ?>
</div>
<span class="nak-toast__text"><?= htmlspecialchars($statusInfo['text']) ?></span>
<button class="nak-toast__close" onclick="dismissNakToast()" aria-label="Schließen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<script>
(function() {
var toast = document.getElementById('nakToast');
if (!toast) return;
requestAnimationFrame(function() { toast.classList.add('nak-toast--visible'); });
var timer = setTimeout(function() { dismissNakToast(); }, 4000);
toast._timer = timer;
if (window.history && window.history.replaceState) {
window.history.replaceState({}, '', 'admin_inbox.php');
}
})();
function dismissNakToast() {
var toast = document.getElementById('nakToast');
if (!toast || toast.classList.contains('nak-toast--hiding')) return;
clearTimeout(toast._timer);
toast.classList.add('nak-toast--hiding');
toast.classList.remove('nak-toast--visible');
setTimeout(function() { toast.remove(); }, 350);
}
</script>
<?php endif; ?>
<div class="header_title ht_admin text_white" id="header_title">
<span class="header_title_text">Adminpanel</span>
</div>
<div class="content" id="fullsize">
<?php $adminActiveTab = 'inbox'; require __DIR__.'/_tabs.php'; ?>
<div class="inbox-shell">
<!-- Filter-Leiste -->
<div class="inbox-filter-bar">
<a href="?filter=all" class="inbox-filter-btn <?= $activeFilter === 'all' ? 'active' : '' ?>">Alle</a>
<?php foreach ($typeConfig as $typeKey => $typeCfg): ?>
<a href="?filter=<?= $typeKey ?>" class="inbox-filter-btn <?= $activeFilter === $typeKey ? 'active' : '' ?>"><?= $typeCfg['label'] ?></a>
<?php endforeach; ?>
</div>
<!-- Offene Nachrichten -->
<div class="inbox-card">
<div class="inbox-header">
<div class="inbox-title">Offene Anfragen</div>
<?php if (count($pendingMessages) > 0): ?>
<span class="inbox-count"><?= count($pendingMessages) ?></span>
<?php endif; ?>
</div>
<?php if (count($pendingMessages) === 0): ?>
<div class="inbox-empty">Keine offenen Anfragen vorhanden.</div>
<?php else: ?>
<?php foreach ($pendingMessages as $msg): ?>
<div class="inbox-item">
<div class="inbox-item-header">
<div>
<span class="inbox-item-type <?= getTypeClass($msg['type'], $typeConfig) ?>">
<?= getTypeLabel($msg['type'], $typeConfig) ?>
</span>
</div>
<span class="inbox-item-date"><?= date('d.m.Y, H:i', strtotime($msg['created_at'])) ?> Uhr</span>
</div>
<div class="inbox-item-subject"><?= htmlspecialchars($msg['subject'] ?? '') ?></div>
<div class="inbox-item-sender">
Von: <?= htmlspecialchars($msg['sender_name'] ?? 'Unbekannt') ?>
(<?= htmlspecialchars($msg['sender_email'] ?? '') ?>)
</div>
<div class="inbox-item-message"><?= htmlspecialchars($msg['message'] ?? '') ?></div>
<div class="inbox-actions">
<?php if ($msg['type'] === 'registration'): ?>
<?php $data = json_decode($msg['data_json'], true); ?>
<button type="button" class="inbox-btn inbox-btn-approve" onclick="openRegistrationModal(<?= (int)($data['pending_id'] ?? 0) ?>, <?= (int)$msg['id'] ?>, '<?= htmlspecialchars($msg['sender_name'] ?? '', ENT_QUOTES) ?>', '<?= htmlspecialchars($msg['sender_email'] ?? '', ENT_QUOTES) ?>')">Anfrage bearbeiten</button>
<?php elseif ($msg['type'] === 'system' && !empty($msg['data_json'])): ?>
<?php
$data = json_decode($msg['data_json'], true);
if (isset($data['action']) && $data['action'] === 'brandschutz_escalation'):
?>
<button type="button" class="inbox-btn inbox-btn-approve" style="background:linear-gradient(135deg,var(--nak-orange),hsl(25,95%,40%)) !important;border-color:hsl(25,95%,40%) !important;" onclick="openBsEscalationModal(<?= (int)$msg['id'] ?>, <?= (int)$data['user_id'] ?>, '<?= htmlspecialchars($data['user_name'] ?? '', ENT_QUOTES) ?>', <?= (int)($data['days_overdue'] ?? 0) ?>)">Persönliche Erinnerung bestätigen</button>
<?php else: ?>
<form method="post" action="">
<input type="hidden" name="mark_done" value="<?= (int)$msg['id'] ?>">
<button type="submit" class="inbox-btn inbox-btn-done">Als erledigt markieren</button>
</form>
<?php endif; ?>
<?php else: ?>
<form method="post" action="">
<input type="hidden" name="mark_done" value="<?= (int)$msg['id'] ?>">
<button type="submit" class="inbox-btn inbox-btn-done">Als erledigt markieren</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Erledigte Nachrichten -->
<?php if (count($doneMessages) > 0): ?>
<div class="inbox-section-title">Erledigte Anfragen (letzte 20)</div>
<div class="inbox-card">
<?php foreach ($doneMessages as $msg): ?>
<div class="inbox-item inbox-done-item">
<div class="inbox-item-header">
<div>
<span class="inbox-item-type <?= getTypeClass($msg['type'], $typeConfig) ?>">
<?= getTypeLabel($msg['type'], $typeConfig) ?>
</span>
</div>
<span class="inbox-item-date">Erledigt: <?= date('d.m.Y, H:i', strtotime($msg['processed_at'])) ?> Uhr</span>
</div>
<div class="inbox-item-subject"><?= htmlspecialchars($msg['subject'] ?? '') ?></div>
<div class="inbox-item-sender">
Von: <?= htmlspecialchars($msg['sender_name'] ?? 'Unbekannt') ?>
(<?= htmlspecialchars($msg['sender_email'] ?? '') ?>)
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div><!-- inbox-shell -->
</div><!-- content -->
<!-- Registrierungs-Modal -->
<div class="standard_overlay" id="registrationModal" onclick="if(event.target.id==='registrationModal')closeRegistrationModal()">
<div style="position:relative;width:min(560px,calc(100vw - 24px));max-height:min(700px,calc(100vh - 24px));overflow:auto;background:#FFF;box-shadow:0 24px 70px rgba(0,0,0,0.36);">
<button type="button" aria-label="Schließen" onclick="closeRegistrationModal()" style="position:absolute;top:14px;right:14px;width:34px;height:34px;border:1px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.16);color:#FFF;font-size:1.35em;cursor:pointer;z-index:1;">×</button>
<div style="padding:24px;color:#FFF;background:radial-gradient(circle at 10% 0%,rgba(255,255,255,0.24),transparent 32%),linear-gradient(135deg,var(--gray-800),var(--gray-900));">
<div style="font-size:1.3em;font-weight:850;">Registrierung bearbeiten</div>
<div id="regModalUserInfo" style="margin-top:6px;color:rgba(255,255,255,0.8);font-weight:600;"></div>
</div>
<div style="padding:22px 24px 24px 24px;">
<form method="post" action="approve-registration.php" id="regModalForm">
<input type="hidden" name="pending_id" id="regModalPendingId">
<input type="hidden" name="message_id" id="regModalMessageId">
<input type="hidden" name="action" id="regModalAction">
<div style="margin-bottom:16px;">
<label style="display:block;margin-bottom:8px;font-weight:750;font-size:0.92em;color:hsl(210,18%,36%);">Rollen zuweisen:</label>
<div style="display:flex;flex-wrap:wrap;gap:8px;">
<?php foreach ($permissionRoles as $role): ?>
<label style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid hsl(210,18%,84%);background:hsl(210,12%,98%);cursor:pointer;font-weight:650;font-size:0.88em;hyphens:auto;-webkit-hyphens:auto;overflow-wrap:break-word;">
<input type="checkbox" name="roles[]" value="<?= htmlspecialchars($role) ?>" style="accent-color:hsl(210,57%,45%);flex-shrink:0;min-width:16px;min-height:16px;">
<?= htmlspecialchars(genderRole($role)) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;">
<button type="button" onclick="submitRegistrationAction('reject')" class="inbox-btn inbox-btn-reject" style="padding:10px 18px;font-size:0.9em;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" style="vertical-align:middle;margin-right:4px;"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>Ablehnen</button>
<button type="button" onclick="submitRegistrationAction('approve')" class="inbox-btn inbox-btn-approve" style="padding:10px 18px;font-size:0.9em;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" style="vertical-align:middle;margin-right:4px;"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>Freischalten</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openRegistrationModal(pendingId, messageId, name, email) {
document.getElementById('regModalPendingId').value = pendingId;
document.getElementById('regModalMessageId').value = messageId;
document.getElementById('regModalUserInfo').textContent = name + ' (' + email + ')';
document.getElementById('registrationModal').style.display = 'flex';
}
function closeRegistrationModal() {
document.getElementById('registrationModal').style.display = 'none';
}
function submitRegistrationAction(action) {
var confirmMsg = action === 'approve' ? 'Nutzer mit den gewählten Rollen freischalten?' : 'Registrierung ablehnen?';
if (!confirm(confirmMsg)) return;
document.getElementById('regModalAction').value = action;
document.getElementById('regModalForm').submit();
}
// Brandschutz Eskalation Modal
function openBsEscalationModal(msgId, userId, userName, daysOverdue) {
document.getElementById('bsEscMsgId').value = msgId;
document.getElementById('bsEscUserId').value = userId;
document.getElementById('bsEscUserName').textContent = userName;
document.getElementById('bsEscDaysOverdue').textContent = daysOverdue + ' Tage';
document.getElementById('bsEscalationModal').style.display = 'flex';
}
function closeBsEscalationModal() {
document.getElementById('bsEscalationModal').style.display = 'none';
}
function submitBsEscalation() {
document.getElementById('bsEscForm').submit();
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (document.getElementById('registrationModal').style.display === 'flex') {
closeRegistrationModal();
}
if (document.getElementById('bsEscalationModal').style.display === 'flex') {
closeBsEscalationModal();
}
}
});
</script>
<!-- Brandschutz Eskalation Modal -->
<div class="standard_overlay" id="bsEscalationModal" onclick="if(event.target.id==='bsEscalationModal')closeBsEscalationModal()">
<div style="position:relative;width:min(520px,calc(100vw - 24px));max-height:min(500px,calc(100vh - 24px));overflow:auto;background:#FFF;box-shadow:0 24px 70px rgba(0,0,0,0.36);">
<button type="button" aria-label="Schließen" onclick="closeBsEscalationModal()" style="position:absolute;top:14px;right:14px;width:34px;height:34px;border:1px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.16);color:#FFF;font-size:1.35em;cursor:pointer;z-index:1;">×</button>
<div style="padding:24px;color:#FFF;background:radial-gradient(circle at 10% 0%,rgba(255,255,255,0.24),transparent 32%),linear-gradient(135deg,hsl(25,90%,45%),hsl(25,95%,35%));">
<div style="font-size:1.3em;font-weight:850;">Persönliche Erinnerung bestätigen</div>
<div style="margin-top:6px;color:rgba(255,255,255,0.8);font-weight:600;">Brandschutzunterweisung – Eskalation</div>
</div>
<div style="padding:22px 24px 24px 24px;">
<p style="margin:0 0 14px 0;font-size:0.95em;color:hsl(210,20%,25%);line-height:1.6;">
Bitte bestätigen Sie, dass Sie <strong id="bsEscUserName"></strong> persönlich an die ausstehende Brandschutzunterweisung erinnert haben.
</p>
<div style="padding:12px 14px;background:hsl(0,65%,95%);border:1px solid hsl(0,65%,85%);margin-bottom:18px;">
<div style="font-size:0.78em;font-weight:850;text-transform:uppercase;letter-spacing:0.04em;color:hsl(0,65%,38%);margin-bottom:4px;">Überfälligkeit</div>
<div style="font-size:1.1em;font-weight:850;color:hsl(0,65%,35%);" id="bsEscDaysOverdue"></div>
</div>
<form method="post" action="" id="bsEscForm">
<input type="hidden" name="bs_escalation_msg_id" id="bsEscMsgId">
<input type="hidden" name="bs_escalation_user_id" id="bsEscUserId">
<input type="hidden" name="bs_escalation_confirm" value="1">
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px;">
<button type="button" onclick="closeBsEscalationModal()" class="inbox-btn" style="padding:10px 18px;font-size:0.9em;">Abbrechen</button>
<button type="submit" class="inbox-btn inbox-btn-approve" style="padding:10px 18px;font-size:0.9em;background:linear-gradient(135deg,var(--nak-orange),hsl(25,95%,40%)) !important;border-color:hsl(25,95%,40%) !important;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" style="vertical-align:middle;margin-right:4px;"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
Bestätigen
</button>
</div>
</form>
</div>
</div>
</div>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,471 @@
<?php
/* Name: admin_notices.php
Beschreibung: Verwaltung der Meldungen auf dem Intranet-Hauptmenü */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
// Admin-Check
$stmt = $pdo->prepare('SELECT "Admin" FROM user_permissions WHERE user_id = ?');
$stmt->execute([$_SESSION['user_id']]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
const NOTICE_MAX_TITLE_LENGTH = 180;
const NOTICE_MAX_MESSAGE_LENGTH = 4000;
$noticeTypes = [
'info' => 'Information',
'warning' => 'Warnung',
'success' => 'Erfolg',
'error' => 'Wichtig',
];
if (empty($_SESSION['admin_notices_csrf'])) {
$_SESSION['admin_notices_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['admin_notices_csrf'];
function noticeRedirect(string $status): void {
header('Location: admin_notices.php?status=' . rawurlencode($status));
exit();
}
function noticeVerifyCsrf(): void {
$postedToken = $_POST['csrf_token'] ?? '';
$sessionToken = $_SESSION['admin_notices_csrf'] ?? '';
if (!is_string($postedToken) || !is_string($sessionToken) || $sessionToken === '' || !hash_equals($sessionToken, $postedToken)) {
http_response_code(400);
exit('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
}
}
function noticeParseLocalDateTime(?string $value): ?string {
$value = trim((string)$value);
if ($value === '') {
return null;
}
$tz = new DateTimeZone('Europe/Berlin');
$date = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $value, $tz);
$errors = DateTimeImmutable::getLastErrors();
if (!$date || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))) {
throw new InvalidArgumentException('Ungültiges Datum.');
}
// Mit explizitem Offset an PostgreSQL senden, damit TIMESTAMPTZ eindeutig ist.
return $date->format('Y-m-d H:i:sP');
}
function noticeFormatLocalDateTime(?string $value): string {
if (empty($value)) {
return '';
}
try {
return (new DateTimeImmutable($value))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('Y-m-d\TH:i');
} catch (Throwable $e) {
return '';
}
}
function noticeDisplayDate(?string $value): string {
if (empty($value)) {
return 'unbegrenzt';
}
try {
return (new DateTimeImmutable($value))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('d.m.Y, H:i') . ' Uhr';
} catch (Throwable $e) {
return '–';
}
}
function noticeDbBool($value): bool {
return $value === true || $value === 1 || $value === '1' || $value === 't' || $value === 'true';
}
function noticeTextLength(string $value): int {
return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
}
function noticeValidateInput(array $noticeTypes): array {
$title = trim((string)($_POST['title'] ?? ''));
$message = trim((string)($_POST['message'] ?? ''));
$type = (string)($_POST['type'] ?? 'info');
$sortOrder = filter_var($_POST['sort_order'] ?? 0, FILTER_VALIDATE_INT);
$isActive = isset($_POST['is_active']);
if ($title === '' || noticeTextLength($title) > NOTICE_MAX_TITLE_LENGTH) {
throw new InvalidArgumentException('Titel fehlt oder ist zu lang.');
}
if ($message === '' || noticeTextLength($message) > NOTICE_MAX_MESSAGE_LENGTH) {
throw new InvalidArgumentException('Meldung fehlt oder ist zu lang.');
}
if (!array_key_exists($type, $noticeTypes)) {
throw new InvalidArgumentException('Ungültiger Meldungstyp.');
}
if ($sortOrder === false || $sortOrder < -9999 || $sortOrder > 9999) {
throw new InvalidArgumentException('Ungültige Sortierung.');
}
$startsAt = noticeParseLocalDateTime($_POST['starts_at'] ?? null);
$endsAt = noticeParseLocalDateTime($_POST['ends_at'] ?? null);
if ($startsAt !== null && $endsAt !== null) {
$start = new DateTimeImmutable($startsAt);
$end = new DateTimeImmutable($endsAt);
if ($end <= $start) {
throw new InvalidArgumentException('Das Ende muss nach dem Beginn liegen.');
}
}
return [
'title' => $title,
'message' => $message,
'type' => $type,
'sort_order' => (int)$sortOrder,
'is_active' => $isActive,
'starts_at' => $startsAt,
'ends_at' => $endsAt,
];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
noticeVerifyCsrf();
$action = (string)($_POST['action'] ?? '');
try {
if ($action === 'create') {
$data = noticeValidateInput($noticeTypes);
$stmt = $pdo->prepare(
'INSERT INTO mainpage_notices
(title, message, type, is_active, starts_at, ends_at, sort_order, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())'
);
$stmt->execute([
$data['title'], $data['message'], $data['type'], $data['is_active'] ? 'true' : 'false',
$data['starts_at'], $data['ends_at'], $data['sort_order'], (int)$_SESSION['user_id'],
]);
noticeRedirect('created');
}
if ($action === 'update') {
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
throw new InvalidArgumentException('Ungültige Meldungs-ID.');
}
$data = noticeValidateInput($noticeTypes);
$stmt = $pdo->prepare(
'UPDATE mainpage_notices
SET title = ?, message = ?, type = ?, is_active = ?, starts_at = ?, ends_at = ?, sort_order = ?, updated_at = NOW()
WHERE id = ?'
);
$stmt->execute([
$data['title'], $data['message'], $data['type'], $data['is_active'] ? 'true' : 'false',
$data['starts_at'], $data['ends_at'], $data['sort_order'], (int)$id,
]);
noticeRedirect('updated');
}
if ($action === 'toggle') {
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
throw new InvalidArgumentException('Ungültige Meldungs-ID.');
}
$stmt = $pdo->prepare('UPDATE mainpage_notices SET is_active = NOT is_active, updated_at = NOW() WHERE id = ?');
$stmt->execute([(int)$id]);
noticeRedirect('toggled');
}
if ($action === 'delete') {
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
throw new InvalidArgumentException('Ungültige Meldungs-ID.');
}
$stmt = $pdo->prepare('DELETE FROM mainpage_notices WHERE id = ?');
$stmt->execute([(int)$id]);
noticeRedirect('deleted');
}
throw new InvalidArgumentException('Unbekannte Aktion.');
} catch (InvalidArgumentException $e) {
$_SESSION['admin_notices_error'] = $e->getMessage();
noticeRedirect('error');
} catch (PDOException $e) {
$_SESSION['admin_notices_error'] = 'Datenbankfehler. Prüfen Sie, ob die Tabelle mainpage_notices angelegt wurde.';
noticeRedirect('error');
}
}
$noticeError = $_SESSION['admin_notices_error'] ?? '';
unset($_SESSION['admin_notices_error']);
$status = (string)($_GET['status'] ?? '');
$statusMessages = [
'created' => 'Meldung wurde angelegt.',
'updated' => 'Meldung wurde gespeichert.',
'toggled' => 'Status der Meldung wurde geändert.',
'deleted' => 'Meldung wurde gelöscht.',
];
$tableAvailable = true;
$notices = [];
try {
$notices = $pdo->query(
'SELECT id, title, message, type, is_active, starts_at, ends_at, sort_order, created_at, updated_at
FROM mainpage_notices
ORDER BY sort_order ASC, created_at DESC, id DESC'
)->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$tableAvailable = false;
$noticeError = 'Die Tabelle mainpage_notices fehlt. Führen Sie zuerst general_files/ini/mainpage_notices.sql aus.';
}
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.notice-admin-shell { padding: 18px; min-height: 100%; }
.notice-admin-card { background:#fff; border:1px solid var(--gray-200); box-shadow:var(--shadow-md); margin-bottom:18px; }
.notice-admin-header { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:16px 18px; border-bottom:1px solid var(--gray-200); }
.notice-admin-title { font-size:1.05em; font-weight:800; color:var(--gray-900); }
.notice-admin-hint { margin-top:3px; font-size:.86em; color:var(--gray-500); }
.notice-admin-button { display:inline-flex; align-items:center; justify-content:center; min-height:36px; padding:7px 14px; border:1px solid var(--gray-800); background:var(--gray-900); color:#fff; font:inherit; font-size:.86em; font-weight:750; cursor:pointer; text-decoration:none; }
.notice-admin-button:hover { color:#fff; background:#111; text-decoration:none; }
.notice-admin-button--secondary { color:var(--gray-800); background:#fff; border-color:var(--gray-300); }
.notice-admin-button--secondary:hover { color:var(--gray-900); background:var(--gray-50); }
.notice-admin-button--danger { color:var(--nak-red); background:#fff; border-color:hsl(352,72%,84%); }
.notice-admin-button--danger:hover { color:#fff; background:var(--nak-red); }
.notice-admin-grid { display:grid; gap:12px; padding:16px 18px 18px; }
.notice-admin-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:16px; padding:16px; border:1px solid var(--gray-200); background:var(--gray-50); }
.notice-admin-item.is-disabled { opacity:.62; }
.notice-admin-item-title { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:6px; font-weight:800; color:var(--gray-900); }
.notice-admin-chip { display:inline-flex; align-items:center; padding:3px 8px; font-size:.72em; font-weight:800; border-left:3px solid currentColor; background:#fff; }
.notice-admin-chip--info { color:var(--nak-blue); }
.notice-admin-chip--warning { color:var(--nak-orange); }
.notice-admin-chip--success { color:var(--nak-darkgreen); }
.notice-admin-chip--error { color:var(--nak-red); }
.notice-admin-message { white-space:pre-wrap; line-height:1.45; color:var(--gray-700); font-size:.92em; }
.notice-admin-meta { display:flex; gap:14px; flex-wrap:wrap; margin-top:10px; color:var(--gray-500); font-size:.8em; }
.notice-admin-actions { display:flex; align-items:flex-start; gap:7px; flex-wrap:wrap; justify-content:flex-end; }
.notice-admin-actions form { margin:0; }
.notice-admin-empty { padding:28px 18px; text-align:center; color:var(--gray-500); }
.notice-admin-alert { margin:16px 18px 0; padding:11px 13px; border-left:4px solid var(--nak-darkgreen); background:hsl(145,50%,94%); color:hsl(145,50%,28%); font-size:.9em; }
.notice-admin-alert--error { border-color:var(--nak-red); background:hsl(352,72%,96%); color:hsl(352,62%,35%); }
.notice-modal { position:fixed; inset:0; z-index:5000; display:none; align-items:center; justify-content:center; padding:20px; background:rgba(16,24,32,.62); }
.notice-modal.open { display:flex; }
.notice-modal-card { width:min(680px,100%); max-height:calc(100dvh - 40px); overflow:auto; background:#fff; box-shadow:var(--shadow-xl); }
.notice-modal-header { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:18px 20px; border-bottom:1px solid var(--gray-200); }
.notice-modal-title { font-size:1.08em; font-weight:800; color:var(--gray-900); }
.notice-modal-close { border:0; background:transparent; font-size:1.5em; line-height:1; cursor:pointer; color:var(--gray-500); }
.notice-modal-body { padding:20px; }
.notice-form-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.notice-field { display:flex; flex-direction:column; gap:6px; }
.notice-field--full { grid-column:1 / -1; }
.notice-field label { font-size:.83em; font-weight:750; color:var(--gray-700); }
.notice-field input, .notice-field select, .notice-field textarea { width:100%; box-sizing:border-box; padding:10px 11px; border:1px solid var(--gray-300); background:#fff; color:var(--gray-900); font:inherit; font-size:.9em; }
.notice-field textarea { min-height:120px; resize:vertical; }
.notice-checkbox { display:flex; align-items:center; gap:8px; min-height:40px; }
.notice-checkbox input { width:auto; }
.notice-form-note { margin-top:5px; font-size:.78em; color:var(--gray-500); }
.notice-modal-actions { display:flex; justify-content:flex-end; gap:8px; padding:0 20px 20px; }
@media (max-width:760px) {
.notice-admin-shell { padding:10px; }
.notice-admin-header, .notice-admin-item { grid-template-columns:1fr; display:grid; }
.notice-admin-actions { justify-content:flex-start; }
.notice-form-grid { grid-template-columns:1fr; }
.notice-field--full { grid-column:auto; }
.content-tab-bar { overflow-x:auto; }
}
</style>
</head>
<body class="theme-admin">
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<div class="header_title ht_admin text_white unselectable" id="header_title">
<span class="header_title_text">Adminpanel</span>
</div>
<div class="content" id="fullsize">
<?php $adminActiveTab = 'notices'; require __DIR__.'/_tabs.php'; ?>
<div class="notice-admin-shell">
<div class="notice-admin-card">
<div class="notice-admin-header">
<div>
<div class="notice-admin-title">Meldungen im Hauptmenü</div>
<div class="notice-admin-hint">Meldungen erscheinen ausschließlich oberhalb der Kacheln auf der Intranet-Hauptseite.</div>
</div>
<?php if ($tableAvailable): ?>
<button type="button" class="notice-admin-button" onclick="openNoticeModal()">+ Neue Meldung</button>
<?php endif; ?>
</div>
<?php if ($status === 'error' && $noticeError !== ''): ?>
<div class="notice-admin-alert notice-admin-alert--error"><?= htmlspecialchars($noticeError, ENT_QUOTES, 'UTF-8') ?></div>
<?php elseif (!$tableAvailable && $noticeError !== ''): ?>
<div class="notice-admin-alert notice-admin-alert--error"><?= htmlspecialchars($noticeError, ENT_QUOTES, 'UTF-8') ?></div>
<?php elseif (isset($statusMessages[$status])): ?>
<div class="notice-admin-alert"><?= htmlspecialchars($statusMessages[$status], ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?php if ($tableAvailable): ?>
<?php if (empty($notices)): ?>
<div class="notice-admin-empty">Noch keine Meldungen vorhanden.</div>
<?php else: ?>
<div class="notice-admin-grid">
<?php foreach ($notices as $notice): ?>
<?php
$noticeId = (int)$notice['id'];
$noticeType = array_key_exists($notice['type'], $noticeTypes) ? $notice['type'] : 'info';
$editPayload = [
'id' => $noticeId,
'title' => $notice['title'],
'message' => $notice['message'],
'type' => $noticeType,
'is_active' => noticeDbBool($notice['is_active']),
'starts_at' => noticeFormatLocalDateTime($notice['starts_at']),
'ends_at' => noticeFormatLocalDateTime($notice['ends_at']),
'sort_order' => (int)$notice['sort_order'],
];
?>
<div class="notice-admin-item<?= noticeDbBool($notice['is_active']) ? '' : ' is-disabled' ?>">
<div>
<div class="notice-admin-item-title">
<?= htmlspecialchars($notice['title'], ENT_QUOTES, 'UTF-8') ?>
<span class="notice-admin-chip notice-admin-chip--<?= htmlspecialchars($noticeType, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($noticeTypes[$noticeType], ENT_QUOTES, 'UTF-8') ?></span>
<?php if (!noticeDbBool($notice['is_active'])): ?><span class="notice-admin-chip">Deaktiviert</span><?php endif; ?>
</div>
<div class="notice-admin-message"><?= htmlspecialchars($notice['message'], ENT_QUOTES, 'UTF-8') ?></div>
<div class="notice-admin-meta">
<span>Von: <?= htmlspecialchars(noticeDisplayDate($notice['starts_at']), ENT_QUOTES, 'UTF-8') ?></span>
<span>Bis: <?= htmlspecialchars(noticeDisplayDate($notice['ends_at']), ENT_QUOTES, 'UTF-8') ?></span>
<span>Sortierung: <?= (int)$notice['sort_order'] ?></span>
</div>
</div>
<div class="notice-admin-actions">
<button type="button" class="notice-admin-button notice-admin-button--secondary" onclick='openNoticeModal(<?= json_encode($editPayload, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>)'>Bearbeiten</button>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="action" value="toggle">
<input type="hidden" name="id" value="<?= $noticeId ?>">
<button type="submit" class="notice-admin-button notice-admin-button--secondary"><?= noticeDbBool($notice['is_active']) ? 'Deaktivieren' : 'Aktivieren' ?></button>
</form>
<form method="post" onsubmit="return confirm('Meldung wirklich dauerhaft löschen?');">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $noticeId ?>">
<button type="submit" class="notice-admin-button notice-admin-button--danger">Löschen</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="notice-modal" id="noticeModal" aria-hidden="true">
<div class="notice-modal-card" role="dialog" aria-modal="true" aria-labelledby="noticeModalTitle">
<form method="post" id="noticeForm">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="action" id="noticeAction" value="create">
<input type="hidden" name="id" id="noticeId" value="">
<div class="notice-modal-header">
<div class="notice-modal-title" id="noticeModalTitle">Neue Meldung</div>
<button type="button" class="notice-modal-close" onclick="closeNoticeModal()" aria-label="Schließen">×</button>
</div>
<div class="notice-modal-body">
<div class="notice-form-grid">
<div class="notice-field notice-field--full">
<label for="noticeTitle">Titel</label>
<input id="noticeTitle" name="title" type="text" maxlength="180" required>
</div>
<div class="notice-field notice-field--full">
<label for="noticeMessage">Meldung</label>
<textarea id="noticeMessage" name="message" maxlength="4000" required></textarea>
</div>
<div class="notice-field">
<label for="noticeType">Typ</label>
<select id="noticeType" name="type">
<?php foreach ($noticeTypes as $typeKey => $typeLabel): ?>
<option value="<?= htmlspecialchars($typeKey, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($typeLabel, ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="notice-field">
<label for="noticeSortOrder">Sortierung</label>
<input id="noticeSortOrder" name="sort_order" type="number" min="-9999" max="9999" value="0" required>
<div class="notice-form-note">Kleinere Zahlen erscheinen zuerst.</div>
</div>
<div class="notice-field">
<label for="noticeStartsAt">Anzeigen ab</label>
<input id="noticeStartsAt" name="starts_at" type="datetime-local">
<div class="notice-form-note">Leer = sofort.</div>
</div>
<div class="notice-field">
<label for="noticeEndsAt">Anzeigen bis</label>
<input id="noticeEndsAt" name="ends_at" type="datetime-local">
<div class="notice-form-note">Leer = ohne Ablaufdatum.</div>
</div>
<div class="notice-field notice-field--full">
<label class="notice-checkbox">
<input id="noticeIsActive" name="is_active" type="checkbox" value="1" checked>
Meldung aktiv
</label>
</div>
</div>
</div>
<div class="notice-modal-actions">
<button type="button" class="notice-admin-button notice-admin-button--secondary" onclick="closeNoticeModal()">Abbrechen</button>
<button type="submit" class="notice-admin-button">Speichern</button>
</div>
</form>
</div>
</div>
<script>
function openNoticeModal(data) {
data = data || null;
var modal = document.getElementById('noticeModal');
document.getElementById('noticeAction').value = data ? 'update' : 'create';
document.getElementById('noticeId').value = data ? data.id : '';
document.getElementById('noticeTitle').value = data ? data.title : '';
document.getElementById('noticeMessage').value = data ? data.message : '';
document.getElementById('noticeType').value = data ? data.type : 'warning';
document.getElementById('noticeSortOrder').value = data ? data.sort_order : 0;
document.getElementById('noticeStartsAt').value = data ? data.starts_at : '';
document.getElementById('noticeEndsAt').value = data ? data.ends_at : '';
document.getElementById('noticeIsActive').checked = data ? !!data.is_active : true;
document.getElementById('noticeModalTitle').textContent = data ? 'Meldung bearbeiten' : 'Neue Meldung';
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
document.getElementById('noticeTitle').focus();
}
function closeNoticeModal() {
var modal = document.getElementById('noticeModal');
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
}
document.getElementById('noticeModal').addEventListener('click', function(event) {
if (event.target === this) closeNoticeModal();
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') closeNoticeModal();
});
</script>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,53 @@
<?php /* Name: admin_profiles.php
Beschreibung: Administratoren-Seite zur Nutzerverwaltung */
// Initialisierung
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
</head>
<body class="theme-admin">
<?php /* Besondere Admin-Rechte checken */
// Fetch user details
$stmt = $pdo->prepare("SELECT * FROM user_permissions up INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id WHERE up.user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetch(PDO::FETCH_ASSOC);
// Check if the user has admin access (Admin = 1)
if ($isAdmin["Admin"] == 1) {
$sql_groups = "SELECT * FROM user_permissions";
$stmt = $pdo->prepare($sql_groups);
$stmt->execute([]);
$groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
header("Location: {$g_httpHomePath}");
exit();
}
?>
<?php // Es folgen ab hier die HTML-Ausgaben beginnend mit dem HTML-Header: ?>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php // Header (einfach): Titel der Seite ?>
<div class="header_title ht_admin text_white" id="header_title">
<div class="flexrow fullwidth" style="justify-content:space-between !important;">
<div class="header_title_text">Nutzerverwaltung</div>
</div>
</div>
<?php // Profilanzeige
require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/projects/intranet/show_profile.php"); ?>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,182 @@
<?php /* Name: approve-registration.php
Beschreibung: Admin genehmigt oder lehnt eine Registrierungsanfrage ab.
- Setzt Rollen bei Freischaltung
- Sendet Mails an Nutzer UND Admins
POST-Parameter: action (approve/reject), pending_id, message_id, roles[] */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: admin_inbox.php");
exit();
}
// Admin-Name ermitteln
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';
$action = $_POST["action"] ?? '';
$pending_id = (int)($_POST["pending_id"] ?? 0);
$message_id = (int)($_POST["message_id"] ?? 0);
$selectedRoles = $_POST["roles"] ?? [];
if (!is_array($selectedRoles)) $selectedRoles = [];
if ($pending_id === 0) {
header("Location: admin_inbox.php?inbox_status=error");
exit();
}
// Pending Registration laden
$stmt = $pdo->prepare("SELECT * FROM pending_registrations WHERE id = ? AND status = 'pending'");
$stmt->execute([$pending_id]);
$pending = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$pending) {
header("Location: admin_inbox.php?inbox_status=error");
exit();
}
// Rollen-Spalten laden
$permissionRoles = [];
$rolesResult = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name = 'user_permissions' AND table_schema = 'public'");
while ($col = $rolesResult->fetch(PDO::FETCH_ASSOC)) {
if (!in_array($col['column_name'], ['p_id', 'user_id'], true)) {
$permissionRoles[] = $col['column_name'];
}
}
$loginUrl = "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/authorization/login.php";
if ($action === 'approve') {
$pdo->beginTransaction();
try {
// Nutzer in user_general_information anlegen
$stmt = $pdo->prepare("INSERT INTO user_general_information (mail, first_name, last_name, phone) VALUES (?, ?, ?, ?)");
$stmt->execute([$pending['email'], $pending['first_name'], $pending['last_name'], $pending['phone']]);
$newUserId = $pdo->lastInsertId();
if (!$newUserId) {
$checkStmt = $pdo->prepare("SELECT user_id FROM user_general_information WHERE mail = ?");
$checkStmt->execute([$pending['email']]);
$newUserId = $checkStmt->fetchColumn();
}
// Login-Daten anlegen
$stmt = $pdo->prepare("INSERT INTO user_login (user_id, password_hash) VALUES (?, ?)");
$stmt->execute([$newUserId, $pending['password_hash']]);
// Berechtigungen mit gewählten Rollen anlegen
$roleColumns = array_map(function($r) { return '"' . str_replace('"', '""', $r) . '"'; }, $permissionRoles);
$columns = array_merge(['"user_id"'], $roleColumns);
$placeholders = array_fill(0, count($columns), '?');
$roleValues = [];
foreach ($permissionRoles as $role) {
$roleValues[] = in_array($role, $selectedRoles, true) ? 1 : 0;
}
$params = array_merge([(int)$newUserId], $roleValues);
$insertSql = "INSERT INTO user_permissions (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $pdo->prepare($insertSql);
$stmt->execute($params);
// Pending-Status aktualisieren
$stmt = $pdo->prepare("UPDATE pending_registrations SET status = 'approved' WHERE id = ?");
$stmt->execute([$pending_id]);
// Inbox-Nachricht als erledigt markieren
if ($message_id > 0) {
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$message_id]);
}
$pdo->commit();
// Zugewiesene Rollen als Text
$assignedRoles = !empty($selectedRoles) ? implode(', ', $selectedRoles) : 'Keine besonderen Rollen';
// Mail an den Nutzer
$userBetreff = "Registrierung freigeschaltet";
$userNachricht = "Hallo {$pending['first_name']},\n\n";
$userNachricht .= "Ihr Account für das Intranet der NAK Reinickendorf wurde freigeschaltet!\n\n";
$userNachricht .= "Sie können sich ab sofort mit Ihrer E-Mail-Adresse und dem von Ihnen gewählten Passwort anmelden.";
send_intranet_mail($pending['email'], $userBetreff, $userNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt anmelden", $loginUrl);
// Mail an Admin-Team
$adminBetreff = "Registrierung freigegeben: {$pending['first_name']} {$pending['last_name']}";
$adminNachricht = "{$adminName} hat die Registrierung bearbeitet:\n\n";
$adminNachricht .= "Nutzer: {$pending['first_name']} {$pending['last_name']}\n";
$adminNachricht .= "E-Mail: {$pending['email']}\n";
$adminNachricht .= "Aktion: Freigeschaltet\n";
$adminNachricht .= "Zugewiesene Rollen: {$assignedRoles}";
send_admin_team_mail($pdo, $adminBetreff, $adminNachricht);
// Systemnachricht im Posteingang
$sysSubject = "Nutzer freigeschaltet: {$pending['first_name']} {$pending['last_name']}";
$sysMessage = "{$adminName} hat {$pending['first_name']} {$pending['last_name']} ({$pending['email']}) freigeschaltet.\nZugewiesene Rollen: {$assignedRoles}";
$stmtSys = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$stmtSys->execute([$adminName, $pending['email'], $sysSubject, $sysMessage]);
header("Location: admin_inbox.php?inbox_status=approved");
exit();
} catch (Throwable $e) {
$pdo->rollBack();
error_log("approve-registration error: " . $e->getMessage());
header("Location: admin_inbox.php?inbox_status=error");
exit();
}
} elseif ($action === 'reject') {
// Ablehnen
$stmt = $pdo->prepare("UPDATE pending_registrations SET status = 'rejected' WHERE id = ?");
$stmt->execute([$pending_id]);
// Inbox-Nachricht als erledigt markieren
if ($message_id > 0) {
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$message_id]);
}
// Mail an den Nutzer
$userBetreff = "Registrierung abgelehnt";
$userNachricht = "Hallo {$pending['first_name']},\n\n";
$userNachricht .= "Ihre Registrierungsanfrage für das Intranet der NAK Reinickendorf wurde leider abgelehnt.\n\n";
$userNachricht .= "Bei Fragen wenden Sie sich bitte an den Administrator über das Kontaktformular auf der Anmeldeseite.";
send_intranet_mail($pending['email'], $userBetreff, $userNachricht);
// Mail an Admin-Team
$adminBetreff = "Registrierung abgelehnt: {$pending['first_name']} {$pending['last_name']}";
$adminNachricht = "{$adminName} hat die Registrierung bearbeitet:\n\n";
$adminNachricht .= "Nutzer: {$pending['first_name']} {$pending['last_name']}\n";
$adminNachricht .= "E-Mail: {$pending['email']}\n";
$adminNachricht .= "Aktion: Abgelehnt";
send_admin_team_mail($pdo, $adminBetreff, $adminNachricht);
// Systemnachricht im Posteingang
$sysSubject = "Registrierung abgelehnt: {$pending['first_name']} {$pending['last_name']}";
$sysMessage = "{$adminName} hat die Registrierung von {$pending['first_name']} {$pending['last_name']} ({$pending['email']}) abgelehnt.";
$stmtSys = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$stmtSys->execute([$adminName, $pending['email'], $sysSubject, $sysMessage]);
header("Location: admin_inbox.php?inbox_status=rejected");
exit();
}
header("Location: admin_inbox.php");
exit();
?>
@@ -0,0 +1,56 @@
<?php /* Name: process_contact.php
Beschreibung: Verarbeitung von Kontaktformular-Anfragen
- Speichert in admin_messages
- Sendet Mail an technik@nak-reinickendorf.de */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Methode nicht erlaubt']);
exit;
}
// Felder auslesen
$name = trim($_POST["contact_name"] ?? '');
$email = trim($_POST["contact_email"] ?? '');
$subject = trim($_POST["contact_subject"] ?? 'Kontaktanfrage');
$message = trim($_POST["contact_message"] ?? '');
// Validierung
if (empty($name) || empty($email) || empty($message)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Bitte alle Felder ausfüllen.']);
exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Ungültige E-Mail-Adresse.']);
exit;
}
// In admin_messages speichern
$stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message) VALUES ('contact', ?, ?, ?, ?)");
$stmt->execute([$name, $email, $subject, $message]);
// Mail an Admin-Team (Admin + Technik)
$mail_betreff = "Kontaktanfrage: {$subject}";
$mail_nachricht = "Neue Kontaktanfrage über das Intranet:\n\n";
$mail_nachricht .= "Name: {$name}\n";
$mail_nachricht .= "E-Mail: {$email}\n";
$mail_nachricht .= "Betreff: {$subject}\n\n";
$mail_nachricht .= "Nachricht:\n{$message}";
send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht);
// JSON-Antwort für AJAX
header('Content-Type: application/json');
echo json_encode(['success' => true, 'message' => 'Nachricht wurde gesendet.']);
exit;
?>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,106 @@
<?php /* Name: login.php
Beschreibung: Login-Seite (Authorisierung) */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
$sql = "SELECT
u.*,
l.password_hash AS password_hash
FROM
user_general_information AS u
INNER JOIN
user_login AS l ON u.user_id = l.user_id
WHERE
u.mail = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST["email"]]);
$user = $stmt->fetch();
if ($user) {
if (isset($user['password_hash']) && password_verify($_POST["password"], $user["password_hash"])) {
session_start();
session_regenerate_id();
$_SESSION["user_id"] = $user["user_id"];
header("Location: {$g_httpHomePath}");
exit;
}
}
$is_invalid = true;
}
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<script src="https://unpkg.com/just-validate@latest/dist/just-validate.production.min.js" defer></script>
<script src="validation.js" defer></script>
</head>
<body>
<div class="login_panel">
<?php if (isset($is_invalid) && $is_invalid): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastLogin">
<div class="nak-toast__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
</div>
<span class="nak-toast__text">Ungültiger Login!</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastLogin').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php if (isset($_GET["message"]) && $_GET["message"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastPw">
<div class="nak-toast__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg>
</div>
<span class="nak-toast__text">Passwort erfolgreich geändert. Sie können sich jetzt anmelden.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastPw').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<div class="login_content">
<div class="nak-logo-link auth-logo">
<img class="auth-logo__icon" src="/intranet_repo/projects/_central/img/nak-logo.svg" alt="NAK Logo">
<div class="auth-logo__text">
<div class="auth-logo__name">Neuapostolische Kirche</div>
<div class="auth-logo__gemeinde">Berlin-Reinickendorf</div>
</div>
</div>
<h2>Anmelden</h2>
<form method="post">
<div class="flexcolumn_center">
<input class="login_field" placeholder="E-Mailadresse" type="email" name="email" id="email"
value="<?= htmlspecialchars($_POST["email"] ?? "") ?>">
<input class="login_field" placeholder="Passwort" type="password" name="password" id="password">
<input class="login_button" type="submit" name="submit" value="Anmelden">
</div>
</form>
<div class="small_text" style="text-align:center;margin-top:24px;color:var(--gray-600);">
Sie haben noch keinen Account für das Intranet?<br>
<a href="signup.php">Registrieren</a><br>
<br>
Passwort vergessen?<br>
<a href="reset-password.php">Passwort zurücksetzen</a><br>
<br>
Probleme bei der Anmeldung?<br>
<a href="#" onclick="openContactModal(); return false;">Admin kontaktieren</a>
</div>
</div><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,13 @@
<?php /* Name: loginout.php
Beschreibung: Logout-Seite */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
session_destroy();
header("Location: {$g_httpLoginPath}");
exit();
@@ -0,0 +1,223 @@
<?php /* Name: reset-password-process.php
Beschreibung: Verarbeitung des Passwort-Resets (3 Schritte)
- Schritt 1: OTP generieren und per SMS + Mail senden
- Schritt 2: OTP verifizieren → Reset-Token erzeugen
- Schritt 3: Neues Passwort setzen mit Reset-Token */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// PostgreSQL-Zeitzone auf PHP-Zeitzone setzen
$pdo->exec("SET timezone = 'Europe/Berlin'");
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: reset-password.php");
exit;
}
$action = $_POST["action"] ?? '';
// =============================================
// SCHRITT 1: OTP anfordern
// =============================================
if ($action === "request_otp") {
$email = trim($_POST["email"] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
header("Location: reset-password.php?error=1");
exit;
}
// Nutzer suchen (muss registriert sein = user_login existiert)
$stmt = $pdo->prepare("SELECT ugi.user_id, ugi.phone, ugi.first_name FROM user_general_information ugi INNER JOIN user_login ul ON ugi.user_id = ul.user_id WHERE ugi.mail = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if (!$user) {
header("Location: reset-password.php?error=1");
exit;
}
if (empty($user['phone'])) {
header("Location: reset-password.php?error=2");
exit;
}
// OTP generieren (6-stellig)
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
// Ablaufzeit als UNIX-Timestamp speichern (vermeidet Timezone-Probleme)
$expires_timestamp = time() + 600; // 10 Minuten
$expires_at = date('Y-m-d H:i:s', $expires_timestamp);
// Alte OTPs für diesen Nutzer invalidieren
$stmt = $pdo->prepare("UPDATE password_reset_otp SET used = 1 WHERE user_id = ? AND used = 0");
$stmt->execute([$user['user_id']]);
// Neues OTP speichern
$stmt = $pdo->prepare("INSERT INTO password_reset_otp (user_id, otp_code, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$user['user_id'], $otp, $expires_at]);
// SMS senden (wie im Seelsorgeplaner)
$phone_full = "+49" . $user['phone'];
$messageText = "Intranet NAK Reinickendorf\n\nIhr Bestaetigungscode: {$otp}\n\nDer Code ist 10 Minuten gueltig.";
$smsData = array(
"phone" => $phone_full,
"message" => $messageText
);
$options = array(
'http' => array(
'header' => "Content-Type: application/json; charset=utf-8",
'method' => 'POST',
'content' => json_encode($smsData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'timeout' => 10,
),
);
$context = stream_context_create($options);
$smsResult = @file_get_contents(rtrim(intranet_env('SMS_GATEWAY_URL'), '/') . '/send_sms', false, $context);
if ($smsResult === FALSE) {
header("Location: reset-password.php?error=4");
exit;
}
// OTP zusätzlich per Mail senden
$greeting = !empty($user['first_name']) ? "Hallo {$user['first_name']}" : "Hallo";
$mailBetreff = "Ihr Bestätigungscode";
$mailNachricht = "{$greeting},\n\n";
$mailNachricht .= "Sie haben das Zurücksetzen Ihres Passworts angefordert.\n\n";
$mailNachricht .= "Ihr Bestätigungscode lautet: {$otp}\n\n";
$mailNachricht .= "Der Code ist 10 Minuten gültig.\n\n";
$mailNachricht .= "Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.";
send_intranet_mail($email, $mailBetreff, $mailNachricht);
// Telefonnummer maskieren für Anzeige (nur letzte 3 Ziffern zeigen)
$phone_masked = str_repeat('*', max(0, strlen($user['phone']) - 3)) . substr($user['phone'], -3);
header("Location: reset-password.php?step=verify&uid={$user['user_id']}&phone=" . urlencode($phone_masked));
exit;
}
// =============================================
// SCHRITT 2: OTP verifizieren → Reset-Token erzeugen
// =============================================
if ($action === "verify_otp") {
$user_id = (int)($_POST["user_id"] ?? 0);
$otp_code = trim($_POST["otp_code"] ?? '');
if ($user_id === 0 || strlen($otp_code) !== 6) {
header("Location: reset-password.php?error=3");
exit;
}
// OTP prüfen – Vergleich mit PHP-Zeit statt DB NOW()
$now = date('Y-m-d H:i:s');
$stmt = $pdo->prepare("SELECT id FROM password_reset_otp WHERE user_id = ? AND otp_code = ? AND used = 0 AND expires_at > ?");
$stmt->execute([$user_id, $otp_code, $now]);
$otpRecord = $stmt->fetch();
if (!$otpRecord) {
header("Location: reset-password.php?step=verify&uid={$user_id}&error=3");
exit;
}
// OTP als verwendet markieren
$stmt = $pdo->prepare("UPDATE password_reset_otp SET used = 1 WHERE id = ?");
$stmt->execute([$otpRecord['id']]);
// Einmal-Token für Passwort-Änderung generieren (in Session speichern)
$reset_token = bin2hex(random_bytes(32));
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$_SESSION['reset_token'] = $reset_token;
$_SESSION['reset_user_id'] = $user_id;
$_SESSION['reset_token_expires'] = time() + 600; // 10 Minuten
header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token));
exit;
}
// =============================================
// SCHRITT 3: Neues Passwort setzen (nach OTP-Verifizierung)
// =============================================
if ($action === "set_password") {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$reset_token = $_POST["reset_token"] ?? '';
$new_password = $_POST["new_password"] ?? '';
$new_password_confirm = $_POST["new_password_confirm"] ?? '';
// Token validieren
if (empty($reset_token)
|| !isset($_SESSION['reset_token'])
|| $reset_token !== $_SESSION['reset_token']
|| !isset($_SESSION['reset_token_expires'])
|| $_SESSION['reset_token_expires'] < time()) {
// Session-Daten aufräumen
unset($_SESSION['reset_token'], $_SESSION['reset_user_id'], $_SESSION['reset_token_expires']);
header("Location: reset-password.php?error=7");
exit;
}
$user_id = (int)$_SESSION['reset_user_id'];
// Passwort-Validierung
if (strlen($new_password) < 8 || !preg_match("/[a-z]/i", $new_password) || !preg_match("/[0-9]/", $new_password)) {
header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token) . "&error=5");
exit;
}
if ($new_password !== $new_password_confirm) {
header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token) . "&error=6");
exit;
}
// Neues Passwort setzen
$password_hash = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE user_login SET password_hash = ? WHERE user_id = ?");
$stmt->execute([$password_hash, $user_id]);
// User-Daten laden für Mail und Vermerk
$stmt = $pdo->prepare("SELECT mail, first_name, last_name FROM user_general_information WHERE user_id = ?");
$stmt->execute([$user_id]);
$resetUserData = $stmt->fetch();
if ($resetUserData) {
$loginUrl = "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/authorization/login.php";
$greeting = !empty($resetUserData['first_name']) ? "Hallo {$resetUserData['first_name']}" : "Hallo";
$mailBetreff = "Passwort erfolgreich geändert";
$mailNachricht = "{$greeting},\n\n";
$mailNachricht .= "Ihr Passwort für das Intranet der NAK Reinickendorf wurde erfolgreich geändert.\n\n";
$mailNachricht .= "Sie können sich ab sofort mit Ihrem neuen Passwort anmelden.";
send_intranet_mail($resetUserData['mail'], $mailBetreff, $mailNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt anmelden", $loginUrl);
// Vermerk in erledigte Tasks
$userName = trim(($resetUserData['first_name'] ?? '') . ' ' . ($resetUserData['last_name'] ?? ''));
if (empty($userName)) $userName = $resetUserData['mail'];
$taskSubject = "Passwort selbst zurückgesetzt: {$userName}";
$taskMessage = "{$userName} ({$resetUserData['mail']}) hat das eigene Passwort erfolgreich zurückgesetzt.";
$stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$stmt->execute([$userName, $resetUserData['mail'], $taskSubject, $taskMessage]);
}
// Session-Daten aufräumen
unset($_SESSION['reset_token'], $_SESSION['reset_user_id'], $_SESSION['reset_token_expires']);
header("Location: login.php?message=1");
exit;
}
header("Location: reset-password.php");
exit;
?>
@@ -0,0 +1,159 @@
<?php /* Name: reset-password.php
Beschreibung: Passwort-Reset per SMS OTP (3 Schritte)
- Schritt 1: E-Mail eingeben → OTP per SMS
- Schritt 2: OTP-Code verifizieren
- Schritt 3: Neues Passwort setzen */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
$step = $_GET["step"] ?? "email";
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.login_button { position: relative; }
.login_button.is-loading {
color: transparent !important;
pointer-events: none;
}
.login_button.is-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 22px;
margin: -11px 0 0 -11px;
border: 3px solid rgba(255,255,255,0.35);
border-top-color: #FFFFFF;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="login_panel">
<?php if (isset($_GET["error"])): ?>
<?php
$errorMessages = [
1 => "E-Mail-Adresse nicht gefunden oder nicht registriert.",
2 => "Keine Telefonnummer hinterlegt. Bitte wenden Sie sich an den Administrator.",
3 => "Ungültiger oder abgelaufener Code.",
4 => "SMS konnte nicht gesendet werden. Bitte versuchen Sie es erneut.",
5 => "Passwort muss mindestens 8 Zeichen, einen Buchstaben und eine Zahl enthalten.",
6 => "Passwörter stimmen nicht überein.",
7 => "Sitzung abgelaufen. Bitte starten Sie den Vorgang erneut."
];
$errorNum = (int)$_GET["error"];
$errorText = $errorMessages[$errorNum] ?? "Unbekannter Fehler.";
?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<span class="nak-toast__text"><?= htmlspecialchars($errorText) ?></span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php if (isset($_GET["success"]) && $_GET["success"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastOk">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg></div>
<span class="nak-toast__text">Passwort erfolgreich geändert. Sie können sich jetzt anmelden.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastOk').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<div class="login_content">
<div class="nak-logo-link auth-logo">
<img class="auth-logo__icon" src="/intranet_repo/projects/_central/img/nak-logo.svg" alt="NAK Logo">
<div class="auth-logo__text">
<div class="auth-logo__name">Neuapostolische Kirche</div>
<div class="auth-logo__gemeinde">Berlin-Reinickendorf</div>
</div>
</div>
<h2>Passwort zurücksetzen</h2>
<?php if ($step === "email"): ?>
<!-- SCHRITT 1: E-Mail eingeben -->
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Geben Sie Ihre E-Mail-Adresse ein. Ein Bestätigungscode wird an Ihre hinterlegte Telefonnummer und per Mail gesendet.</p>
<form action="reset-password-process.php" method="post" id="otpRequestForm">
<input type="hidden" name="action" value="request_otp">
<label for="email">E-Mailadresse</label>
<div class="item">
<input class="login_field" type="email" id="email" name="email" required>
</div>
<button class="login_button" type="submit" id="requestBtn">Code anfordern</button>
</form>
<script>
document.getElementById('otpRequestForm').addEventListener('submit', function() {
var btn = document.getElementById('requestBtn');
btn.classList.add('is-loading');
btn.disabled = true;
});
</script>
<?php elseif ($step === "verify"): ?>
<!-- SCHRITT 2: Nur OTP-Code eingeben -->
<?php $phone_masked = htmlspecialchars($_GET["phone"] ?? "***"); ?>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Ein 6-stelliger Code wurde an die Nummer +49 <?= $phone_masked ?> und an Ihre E-Mail-Adresse gesendet.</p>
<form action="reset-password-process.php" method="post">
<input type="hidden" name="action" value="verify_otp">
<input type="hidden" name="user_id" value="<?= (int)($_GET["uid"] ?? 0) ?>">
<label for="otp_code">Bestätigungscode</label>
<div class="item">
<input class="login_field" type="text" id="otp_code" name="otp_code" maxlength="6" inputmode="numeric" pattern="[0-9]{6}" placeholder="000000" required autocomplete="one-time-code" style="text-align:center;font-size:1.4em;letter-spacing:8px;">
</div>
<input class="login_button" type="submit" value="Code bestätigen">
</form>
<?php elseif ($step === "newpassword"): ?>
<!-- SCHRITT 3: Neues Passwort setzen (nur nach verifiziertem OTP) -->
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Ihr Code wurde bestätigt. Geben Sie nun Ihr neues Passwort ein.</p>
<form action="reset-password-process.php" method="post">
<input type="hidden" name="action" value="set_password">
<input type="hidden" name="reset_token" value="<?= htmlspecialchars($_GET["token"] ?? '') ?>">
<label for="new_password">Neues Passwort</label>
<div class="item">
<input class="login_field" type="password" id="new_password" name="new_password" required>
</div>
<label for="new_password_confirm">Neues Passwort wiederholen</label>
<div class="item">
<input class="login_field" type="password" id="new_password_confirm" name="new_password_confirm" required>
</div>
<input class="login_button" type="submit" value="Passwort ändern">
</form>
<?php endif; ?>
<div class="small_text" align="center">
<br>
<a href="login.php">Zurück zur Anmeldung</a><br>
<br>
Probleme beim Zurücksetzen?<br>
<a href="#" onclick="openContactModal(); return false;">Admin kontaktieren</a>
</div>
</div><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,97 @@
<?php /* Name: self-signup-process.php
Beschreibung: Verarbeitung der Selbstregistrierung
- Validierung der Eingaben
- Speicherung in pending_registrations
- Mail an Admin (samuel.bias@aol.de)
- Eintrag in admin_messages */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// Validierung
if (empty($_POST["first_name"])) {
die("Vorname ist erforderlich.");
}
if (empty($_POST["last_name"])) {
die("Nachname ist erforderlich.");
}
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
die("Gültige E-Mail-Adresse erforderlich.");
}
if (strlen($_POST["password"]) < 8) {
die("Passwort muss mindestens 8 Zeichen lang sein.");
}
if (!preg_match("/[a-z]/i", $_POST["password"])) {
die("Passwort muss mindestens einen Buchstaben enthalten.");
}
if (!preg_match("/[0-9]/", $_POST["password"])) {
die("Passwort muss mindestens eine Zahl enthalten.");
}
if ($_POST["password"] !== $_POST["password_confirmation"]) {
die("Passwörter stimmen nicht überein.");
}
$email = trim($_POST["email"]);
$first_name = trim($_POST["first_name"]);
$last_name = trim($_POST["last_name"]);
// Prüfen ob E-Mail bereits existiert (in user_general_information oder pending_registrations)
$checkSql = "SELECT COUNT(*) FROM user_general_information WHERE mail = ?";
$stmt = $pdo->prepare($checkSql);
$stmt->execute([$email]);
$count1 = $stmt->fetchColumn();
$checkSql2 = "SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'";
$stmt2 = $pdo->prepare($checkSql2);
$stmt2->execute([$email]);
$count2 = $stmt2->fetchColumn();
if ($count1 > 0 || $count2 > 0) {
header("Location: self-signup.php?error=2");
exit;
}
// Passwort hashen und Token generieren
$password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
$token = bin2hex(random_bytes(32));
// In pending_registrations speichern
$insertSql = "INSERT INTO pending_registrations (first_name, last_name, email, password_hash, token) VALUES (?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($insertSql);
if (!$stmt) {
header("Location: self-signup.php?error=1");
exit;
}
if (!$stmt->execute([$first_name, $last_name, $email, $password_hash, $token])) {
header("Location: self-signup.php?error=1");
exit;
}
$pending_id = $pdo->lastInsertId();
// Admin-Nachricht in Inbox speichern
$subject = "Neue Registrierungsanfrage: {$first_name} {$last_name}";
$message_text = "Neue Selbstregistrierung:\n\nName: {$first_name} {$last_name}\nE-Mail: {$email}\n\nBitte im Adminpanel freischalten.";
$data_json = json_encode(['pending_id' => $pending_id, 'token' => $token]);
$inboxSql = "INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, data_json) VALUES ('registration', ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($inboxSql);
$sender_name = "{$first_name} {$last_name}";
$stmt->execute([$sender_name, $email, $subject, $message_text, $data_json]);
// Mail an Admin-Team (Admin + Technik)
$admin_email = ADMIN_EMAIL;
$mail_betreff = "Intranet: Neue Registrierungsanfrage von {$first_name} {$last_name}";
$mail_nachricht = "Hallo Admin,\n\n";
$mail_nachricht .= "Es liegt eine neue Registrierungsanfrage vor:\n\n";
$mail_nachricht .= "Name: {$first_name} {$last_name}\n";
$mail_nachricht .= "E-Mail: {$email}\n\n";
$mail_nachricht .= "Bitte melden Sie sich im Adminpanel an, um die Anfrage zu bearbeiten.\n\n";
$mail_nachricht .= "-- Intranet NAK Reinickendorf";
send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht);
// Erfolg
header("Location: self-signup.php?success=1");
exit;
?>
@@ -0,0 +1,101 @@
<?php /* Name: self-signup.php
Beschreibung: Selbstregistrierung - Nutzer kann sich eigenständig registrieren.
Admin erhält Mail + Inbox-Nachricht zur Freischaltung. */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<script src="just-validate.js" defer></script>
<script src="validation.js" defer></script>
</head>
<body>
<div class="login_panel">
<?php if (isset($_GET["error"])): ?>
<?php if ($_GET["error"] == 1): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<span class="nak-toast__text">Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php elseif ($_GET["error"] == 2): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<span class="nak-toast__text">Diese E-Mailadresse ist bereits registriert oder es liegt bereits eine Anfrage vor.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php endif; ?>
<?php if (isset($_GET["success"]) && $_GET["success"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastOk">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg></div>
<span class="nak-toast__text">Ihre Registrierungsanfrage wurde gesendet. Sie erhalten eine Benachrichtigung, sobald Ihr Konto freigeschaltet wurde.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastOk').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<div class="login_content">
<div class="nak-logo-link" style="display:flex;align-items:flex-end;gap:12px;text-decoration:none;margin-bottom:24px;">
<img src="/intranet_repo/projects/_central/img/nak-logo.svg" alt="NAK Logo" style="width:52px;height:52px;flex-shrink:0;">
<div style="display:flex;flex-direction:column;justify-content:flex-end;line-height:1.2;">
<div style="font-family:var(--font-primary);font-weight:500;font-size:0.875rem;color:var(--gray-900);white-space:nowrap;">Neuapostolische Kirche</div>
<div style="font-family:var(--font-primary);font-weight:300;font-size:0.75rem;color:var(--gray-600);white-space:nowrap;">Berlin-Reinickendorf</div>
</div>
</div>
<h2>Registrierung beantragen</h2>
<h3 class="no_margins" align="center">Ihr Antrag wird von einem Administrator geprüft und freigeschaltet.</h3>
<form action="self-signup-process.php" method="post" id="signup" novalidate>
<label for="first_name"><br>Vorname</label>
<div class="item">
<input class="login_field" type="text" id="first_name" name="first_name" required>
</div>
<label for="last_name">Nachname</label>
<div class="item">
<input class="login_field" type="text" id="last_name" name="last_name" required>
</div>
<label for="email">E-Mailadresse</label>
<div class="item">
<input class="login_field" type="email" id="email" name="email" required>
</div>
<label for="password">Passwort</label>
<div class="item">
<input class="login_field" type="password" id="password" name="password" required>
</div>
<label for="password_confirmation">Passwort wiederholen</label>
<div class="item">
<input class="login_field" type="password" id="password_confirmation" name="password_confirmation" required>
</div>
<input class="login_button" type="submit" value="Registrierung beantragen">
</form>
<div class="small_text" align="center">
Sie haben bereits einen Account?
<br>
<a href="login.php">Anmelden</a>
<br><br>
Sie wurden bereits vom Admin vorregistriert?
<br>
<a href="signup.php">Direkt registrieren</a>
</div>
</div><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,118 @@
<?php /* Name: signup-process.php
Beschreibung: Vereinte Registrierungs-Verarbeitung
- Prüft ob Nutzer vorregistriert ist → direkte Registrierung
- Wenn nicht → automatisch Antrag in pending_registrations + Mail an Admin */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// === Validierung ===
if (empty($_POST["first_name"])) {
die("Vorname ist erforderlich.");
}
if (empty($_POST["last_name"])) {
die("Nachname ist erforderlich.");
}
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
die("Gültige E-Mail-Adresse erforderlich.");
}
if (empty($_POST["phone"])) {
die("Telefonnummer ist erforderlich.");
}
if (strlen($_POST["password"]) < 8) {
die("Passwort muss mindestens 8 Zeichen lang sein.");
}
if (!preg_match("/[a-z]/i", $_POST["password"])) {
die("Passwort muss mindestens einen Buchstaben enthalten.");
}
if (!preg_match("/[0-9]/", $_POST["password"])) {
die("Passwort muss mindestens eine Zahl enthalten.");
}
if ($_POST["password"] !== $_POST["password_confirmation"]) {
die("Passwörter stimmen nicht überein.");
}
$email = trim($_POST["email"]);
$first_name = trim($_POST["first_name"]);
$last_name = trim($_POST["last_name"]);
$phone = preg_replace('/[^0-9]/', '', trim($_POST["phone"]));
$password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
// === Prüfung 1: Ist der Nutzer vom Admin vorregistriert? ===
$stmt = $pdo->prepare("SELECT user_id FROM user_general_information WHERE mail = ?");
$stmt->execute([$email]);
$user_id = $stmt->fetchColumn();
if ($user_id) {
// Nutzer ist vorregistriert – prüfen ob bereits Login existiert
$stmt = $pdo->prepare("SELECT COUNT(*) FROM user_login WHERE user_id = ?");
$stmt->execute([$user_id]);
$loginExists = $stmt->fetchColumn();
if ($loginExists > 0) {
// Bereits vollständig registriert
header("Location: signup.php?error=2");
exit;
}
// Direkte Registrierung durchführen
$stmt = $pdo->prepare("INSERT INTO user_login (user_id, password_hash) VALUES (?, ?)");
if ($stmt->execute([$user_id, $password_hash])) {
// Namen und Telefonnummer aktualisieren
$stmt = $pdo->prepare("UPDATE user_general_information SET first_name=?, last_name=?, phone=? WHERE user_id=?");
$stmt->execute([$first_name, $last_name, $phone, $user_id]);
header("Location: login.php");
exit;
} else {
header("Location: signup.php?error=1");
exit;
}
}
// === Prüfung 2: Liegt bereits ein Antrag vor? ===
$stmt = $pdo->prepare("SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'");
$stmt->execute([$email]);
$pendingCount = $stmt->fetchColumn();
if ($pendingCount > 0) {
header("Location: signup.php?error=2");
exit;
}
// === Antrag stellen (Nutzer nicht vorregistriert) ===
$token = bin2hex(random_bytes(32));
$stmt = $pdo->prepare("INSERT INTO pending_registrations (first_name, last_name, email, phone, password_hash, token) VALUES (?, ?, ?, ?, ?, ?)");
if (!$stmt->execute([$first_name, $last_name, $email, $phone, $password_hash, $token])) {
header("Location: signup.php?error=1");
exit;
}
$pending_id = $pdo->lastInsertId();
// Admin-Nachricht in Inbox speichern
$subject = "Neue Registrierungsanfrage: {$first_name} {$last_name}";
$message_text = "Neue Registrierungsanfrage:\n\nName: {$first_name} {$last_name}\nE-Mail: {$email}\n\nBitte im Adminpanel freischalten.";
$data_json = json_encode(['pending_id' => $pending_id, 'token' => $token]);
$stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, data_json) VALUES ('registration', ?, ?, ?, ?, ?)");
$sender_name = "{$first_name} {$last_name}";
$stmt->execute([$sender_name, $email, $subject, $message_text, $data_json]);
// Mail an Admin-Team (Admin + Technik)
$mail_betreff = "Neue Registrierungsanfrage von {$first_name} {$last_name}";
$mail_nachricht = "Es liegt eine neue Registrierungsanfrage vor:\n\n";
$mail_nachricht .= "Name: {$first_name} {$last_name}\n";
$mail_nachricht .= "E-Mail: {$email}\n";
$mail_nachricht .= "Telefon: {$phone}\n\n";
$mail_nachricht .= "Bitte melden Sie sich im Adminpanel an, um die Anfrage zu bearbeiten.";
send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht, "Im Adminpanel bearbeiten", "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/adminpanel/admin_inbox.php");
// Erfolg – gleiche Meldung wie bei direkter Registrierung (Nutzer merkt keinen Unterschied)
header("Location: signup.php?success=1");
exit;
?>
@@ -0,0 +1,111 @@
<?php /* Name: signup.php
Beschreibung: Registrierungsformular (vereint)
- Wenn Nutzer vorregistriert: direkte Registrierung
- Wenn nicht: Antrag wird automatisch gestellt
- Verwendet signup-process.php */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<!-- Quelle für just-validate.js: https://unpkg.com/just-validate@latest/dist/just-validate.production.min.js -->
<script src="just-validate.js" defer></script>
<script src="validation.js" defer></script>
</head>
<body>
<div class="login_panel">
<?php if (isset($_GET["error"])): ?>
<?php if ($_GET["error"] == 1): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<span class="nak-toast__text">Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php elseif ($_GET["error"] == 2): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<span class="nak-toast__text">Diese E-Mailadresse ist bereits registriert oder es liegt bereits eine Anfrage vor.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php endif; ?>
<?php if (isset($_GET["success"]) && $_GET["success"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastOk">
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg></div>
<span class="nak-toast__text">Ihre Registrierung wurde beantragt. Sie erhalten eine Benachrichtigung, sobald Ihr Konto freigeschaltet wurde.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastOk').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<div class="login_content">
<div class="nak-logo-link auth-logo">
<img class="auth-logo__icon" src="/intranet_repo/projects/_central/img/nak-logo.svg" alt="NAK Logo">
<div class="auth-logo__text">
<div class="auth-logo__name">Neuapostolische Kirche</div>
<div class="auth-logo__gemeinde">Berlin-Reinickendorf</div>
</div>
</div>
<h2>Registrieren</h2>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Erstellen Sie Ihren persönlichen Zugang zum Intranet.</p>
<form action="signup-process.php" method="post" id="signup" novalidate>
<label for="first_name">Vorname</label>
<div class="item">
<input class="login_field" type="text" id="first_name" name="first_name">
</div>
<label for="last_name">Nachname</label>
<div class="item">
<input class="login_field" type="text" id="last_name" name="last_name">
</div>
<label for="email">E-Mailadresse</label>
<div class="item">
<input class="login_field" type="email" id="email" name="email">
</div>
<label for="phone">Telefonnummer</label>
<div class="item">
<div style="display:flex;align-items:stretch;width:100%;max-width:100%;margin-bottom:10px;box-sizing:border-box;">
<div class="input-prefix" style="border-radius:0;border:1px solid var(--gray-300);border-right:none;margin:0;padding:14px 12px;font-weight:500;box-shadow:none;width:auto;background:var(--gray-200);color:var(--gray-700);display:flex;align-items:center;flex-shrink:0;">+49</div>
<input class="login_field" type="tel" id="phone" name="phone" style="border-radius:0;border-left:0;margin:0;flex:1;width:auto;min-width:0;">
</div>
</div>
<label for="password">Passwort</label>
<div class="item">
<input class="login_field" type="password" id="password" name="password">
</div>
<label for="password_confirmation">Passwort wiederholen</label>
<div class="item">
<input class="login_field" type="password" id="password_confirmation" name="password_confirmation">
</div>
<input class="login_button" type="submit" value="Registrieren">
</form>
<div class="small_text" align="center">
Sie haben bereits einen Account?
<br>
<a href="login.php">Anmelden</a><br>
<br>
Probleme bei der Registrierung?<br>
<a href="#" onclick="openContactModal(); return false;">Admin kontaktieren</a>
</div>
</div><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,40 @@
<?php /* Name: validate-email.php
Beschreibung: Validierung der E-Mailadresse
- Wird innerhalb von validation.js aufgerufen
- Prüft ob E-Mail bereits vollständig registriert oder
ein offener Antrag vorliegt */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
header("Content-Type: application/json");
if (!isset($_GET["email"]) || empty(trim($_GET["email"]))) {
echo json_encode(["available" => false]);
exit;
}
$email = trim($_GET["email"]);
$is_available = true;
// Prüfung 1: Gibt es einen vollständigen Login für diese E-Mail?
$stmt = $pdo->prepare("SELECT u.user_id FROM user_general_information u INNER JOIN user_login l ON u.user_id = l.user_id WHERE u.mail = ?");
$stmt->execute([$email]);
if ($stmt->fetch()) {
$is_available = false;
}
// Prüfung 2: Liegt bereits ein offener Antrag vor?
if ($is_available) {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'");
$stmt->execute([$email]);
$pendingCount = $stmt->fetchColumn();
if ($pendingCount > 0) {
$is_available = false;
}
}
echo json_encode(["available" => $is_available]);
exit;
?>
@@ -0,0 +1,74 @@
const validation = new JustValidate("#signup");
validation
.addField("#first_name", [
{
rule: "required",
errorMessage: "Pflichtfeld"
}
])
.addField("#last_name", [
{
rule: "required",
errorMessage: "Pflichtfeld"
}
])
.addField("#email", [
{
rule: "required",
errorMessage: "Pflichtfeld"
},
{
rule: "email",
errorMessage: "Ungültig"
},
{
validator: (value) => () => {
return fetch("validate-email.php?email=" + encodeURIComponent(value))
.then(function(response) {
return response.json();
})
.then(function(json) {
return json.available;
});
},
errorMessage: "Bereits registriert"
}
])
.addField("#phone", [
{
rule: "required",
errorMessage: "Pflichtfeld"
},
{
rule: "minLength",
value: 6,
errorMessage: "Ungültige Telefonnummer"
},
{
validator: (value) => {
return /^[1-9][0-9\s\-\/()]{4,}$/.test(value);
},
errorMessage: "Ohne führende 0, nur Ziffern"
}
])
.addField("#password", [
{
rule: "required",
errorMessage: "Pflichtfeld"
},
{
rule: "password",
errorMessage: "Zu leicht"
}
])
.addField("#password_confirmation", [
{
validator: (value, fields) => {
return value === fields["#password"].elem.value;
},
errorMessage: "Passwörter müssen übereinstimmen"
}
])
.onSuccess((event) => {
document.getElementById("signup").submit();
});
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

@@ -0,0 +1,162 @@
<?php
/* Name: brandschutz_cron.php
Beschreibung: Monatlicher Cron-Job (am 1. des Monats) für Brandschutz-Schulungserinnerungen
Aufgaben:
1. Einen Monat vor Ablauf: Erste Erinnerungsmail an den Nutzer
2. Nach Ablauf der Frist: Monatliche Erinnerungsmail an den Nutzer
3. 4+ Monate nach Ablauf: Eskalation an Admins (aktive Systemnachricht + Mail)
Aufruf: php brandschutz_cron.php (via Cron: 0 8 1 * *)
*/
// Initialisierung
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit('Forbidden');
}
$_SERVER['DOCUMENT_ROOT'] = '/var/www/html';
$_SERVER['HTTP_HOST'] = getenv('INTRANET_FQDN') ?: 'localhost';
$pdo = require $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/database.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/send_mail.php';
$today = date('Y-m-d');
$log = [];
// Alle Nutzer mit Brandschutz-Rolle laden
$sql = "SELECT ugi.user_id, ugi.first_name, ugi.last_name, ugi.mail,
bt.last_training_date, bt.reminder_count, bt.admin_reminder_count
FROM user_permissions up
INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id
LEFT JOIN brandschutz_training bt ON ugi.user_id = bt.user_id
WHERE up.\"Brandschutz\" = 1";
$result = $pdo->query($sql);
$users = $result->fetchAll(PDO::FETCH_ASSOC);
foreach ($users as $bsUser) {
$userId = (int)$bsUser['user_id'];
$firstName = $bsUser['first_name'] ?? '';
$lastName = $bsUser['last_name'] ?? '';
$mail = $bsUser['mail'] ?? '';
$fullName = trim("{$firstName} {$lastName}");
if (empty($fullName)) $fullName = $mail;
$greeting = !empty($firstName) ? "Hallo {$firstName}" : "Hallo";
$lastTraining = $bsUser['last_training_date'];
$reminderCount = (int)($bsUser['reminder_count'] ?? 0);
$adminReminderCount = (int)($bsUser['admin_reminder_count'] ?? 0);
// Sicherstellen dass ein Tracking-Eintrag existiert
$stmtEnsure = $pdo->prepare("INSERT INTO brandschutz_training (user_id) VALUES (?) ON CONFLICT (user_id) DO NOTHING");
$stmtEnsure->execute([$userId]);
// Fälligkeit berechnen
if (empty($lastTraining)) {
// Nie geschult = sofort fällig
$dueDate = $today;
$daysUntilDue = -1; // Bereits fällig
$daysOverdue = 1;
} else {
$dueDate = date('Y-m-d', strtotime($lastTraining . ' +365 days'));
$daysUntilDue = (int)((strtotime($dueDate) - strtotime($today)) / 86400);
$daysOverdue = $daysUntilDue < 0 ? abs($daysUntilDue) : 0;
}
// Szenario 1: Einen Monat vor Ablauf (0-30 Tage verbleibend) - Erste Erinnerung
if ($daysUntilDue >= 0 && $daysUntilDue <= 30 && $reminderCount === 0) {
$dueDateFormatted = date('d.m.Y', strtotime($dueDate));
$subject = "Erinnerung: Brandschutzunterweisung bis {$dueDateFormatted} fällig";
$message = "{$greeting},\n\nIhre Brandschutzunterweisung läuft am {$dueDateFormatted} ab.\n\n";
$message .= "Die Schulung dauert nur etwa 20 Minuten und kann bequem vom Handy oder PC aus durchgeführt werden.\n\n";
$message .= "Bitte absolvieren Sie die Unterweisung innerhalb der nächsten zwei Monate über das Intranet.\n\n";
$message .= "Vielen Dank!";
$brandschutzUrl = "https://" . $_SERVER['HTTP_HOST'] . "/intranet_repo/projects/brandschutz/brandschutz.php";
send_intranet_mail($mail, $subject, $message, null, "intranet@nak-reinickendorf.de", "Zur Brandschutzunterweisung", $brandschutzUrl);
// Zähler erhöhen
$stmtUpdate = $pdo->prepare("UPDATE brandschutz_training SET reminder_count = reminder_count + 1 WHERE user_id = ?");
$stmtUpdate->execute([$userId]);
// Log in erledigte Nachrichten
$sysSubject = "Brandschutz-Erinnerung gesendet: {$fullName}";
$sysMessage = "Automatische Erinnerungsmail an {$fullName} ({$mail}) gesendet. Schulung fällig am {$dueDateFormatted}.";
$stmtLog = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', 'System', ?, ?, ?, 'done', NOW(), NOW())");
$stmtLog->execute([$mail, $sysSubject, $sysMessage]);
$log[] = "Erste Erinnerung an {$fullName} gesendet (fällig am {$dueDateFormatted}).";
}
// Szenario 2: Frist überschritten - Monatliche Erinnerung an Nutzer
if ($daysOverdue > 0) {
$dueDateFormatted = !empty($lastTraining) ? date('d.m.Y', strtotime($dueDate)) : 'sofort';
$monthsOverdue = (int)ceil($daysOverdue / 30);
$subject = "Überfällig: Brandschutzunterweisung ({$daysOverdue} Tage)";
$message = "{$greeting},\n\nIhre Brandschutzunterweisung ist seit {$daysOverdue} Tagen überfällig";
if ($dueDateFormatted !== 'sofort') {
$message .= " (Frist war der {$dueDateFormatted})";
}
$message .= ".\n\nDie Schulung dauert nur etwa 20 Minuten und kann bequem vom Handy oder PC aus durchgeführt werden.\n\n";
$message .= "Bitte absolvieren Sie die Unterweisung umgehend über das Intranet.\n\n";
$message .= "Dies ist die {$reminderCount}. Erinnerung zu dieser Schulung.";
$brandschutzUrl = "https://" . $_SERVER['HTTP_HOST'] . "/intranet_repo/projects/brandschutz/brandschutz.php";
send_intranet_mail($mail, $subject, $message, null, "intranet@nak-reinickendorf.de", "Zur Brandschutzunterweisung", $brandschutzUrl);
// Zähler erhöhen
$stmtUpdate = $pdo->prepare("UPDATE brandschutz_training SET reminder_count = reminder_count + 1 WHERE user_id = ?");
$stmtUpdate->execute([$userId]);
// Log in erledigte Nachrichten
$sysSubject = "Brandschutz-Erinnerung gesendet: {$fullName} ({$daysOverdue} Tage überfällig)";
$sysMessage = "Automatische Erinnerungsmail (Nr. " . ($reminderCount + 1) . ") an {$fullName} ({$mail}) gesendet. Schulung seit {$daysOverdue} Tagen überfällig.";
$stmtLog = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', 'System', ?, ?, ?, 'done', NOW(), NOW())");
$stmtLog->execute([$mail, $sysSubject, $sysMessage]);
$log[] = "Erinnerung Nr." . ($reminderCount + 1) . " an {$fullName} ({$daysOverdue} Tage überfällig).";
// Szenario 3: 4+ Monate überfällig - Eskalation an Brandschutzbeauftragte + Admins
if ($daysOverdue >= 120) {
$adminSubject = "ESKALATION: Brandschutzunterweisung überfällig – {$fullName}";
$adminMessage = "Die Brandschutzunterweisung von {$fullName} ({$mail}) ist seit {$daysOverdue} Tagen überfällig (mehr als 4 Monate).\n\n";
$adminMessage .= "Bitte erinnern Sie die Person persönlich an die ausstehende Schulung.\n\n";
$adminMessage .= "Bisherige Erinnerungsmails an den Nutzer: " . ($reminderCount + 1) . "\n";
$adminMessage .= "Bisherige Admin-Eskalationen: " . ($adminReminderCount + 1);
// Mail an Brandschutzbeauftragte
send_brandschutzbeauftragter_mail($pdo, $adminSubject, $adminMessage);
// Aktive (pending) Systemnachricht im Postfach
$dataJson = json_encode([
'action' => 'brandschutz_escalation',
'user_id' => $userId,
'user_name' => $fullName,
'days_overdue' => $daysOverdue
]);
$stmtAdmin = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, created_at, data_json) VALUES ('system', 'System', ?, ?, ?, 'pending', NOW(), ?)");
$stmtAdmin->execute([$mail, $adminSubject, $adminMessage, $dataJson]);
// Admin-Zähler erhöhen
$stmtAdminCount = $pdo->prepare("UPDATE brandschutz_training SET admin_reminder_count = admin_reminder_count + 1 WHERE user_id = ?");
$stmtAdminCount->execute([$userId]);
$log[] = "ESKALATION an Brandschutzbeauftragte für {$fullName} ({$daysOverdue} Tage überfällig).";
}
}
}
$pdo = null;
// Ausgabe/Logging
$output = "Brandschutz-Cron ausgeführt am " . date('d.m.Y H:i:s') . "\n";
$output .= count($users) . " Nutzer mit Brandschutz-Rolle geprüft.\n";
if (!empty($log)) {
$output .= "\nAktionen:\n" . implode("\n", $log);
} else {
$output .= "\nKeine Aktionen erforderlich.";
}
echo $output . "\n";
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

@@ -0,0 +1,133 @@
<?php
// Prüft, ob ein Basisname dem von der Anwendung erzeugten Format entspricht.
function isValidNewsFileBase(string $fileBase): bool
{
return preg_match(
'/^\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+$/D',
$fileBase
) === 1;
}
/**
* Löscht eine News vollständig:
*
* - Original-PDF
* - alle daraus erzeugten PNG-Seiten
*/
function deleteNewsFiles(string $fileBase): bool
{
if (!isValidNewsFileBase($fileBase)) {
error_log('Ungültiger News-Dateiname beim Löschen: ' . $fileBase);
return false;
}
$newsDirectory = __DIR__ . '/news';
$pngDirectory = $newsDirectory . '/png';
$success = true;
// Alle erzeugten PNG-Seiten löschen
$pngFiles = glob(
$pngDirectory . '/' . $fileBase . '_page_*.png'
) ?: [];
foreach ($pngFiles as $pngFile) {
if (is_file($pngFile) && !unlink($pngFile)) {
error_log('News-PNG konnte nicht gelöscht werden: ' . $pngFile);
$success = false;
}
}
// Original-PDF löschen
$pdfFile = $newsDirectory . '/' . $fileBase . '.pdf';
if (is_file($pdfFile) && !unlink($pdfFile)) {
error_log('News-PDF konnte nicht gelöscht werden: ' . $pdfFile);
$success = false;
}
return $success;
}
/**
* Löscht alle abgelaufenen News.
*
* Wichtig:
* Es werden sowohl vorhandene PDFs als auch page_1-PNGs betrachtet.
* Dadurch werden auch verwaiste PDFs ohne PNG zuverlässig gefunden.
*/
function cleanupExpiredNews(): void
{
$newsDirectory = __DIR__ . '/news';
$pngDirectory = $newsDirectory . '/png';
$fileBases = [];
// PDFs erfassen
foreach (glob($newsDirectory . '/*.pdf') ?: [] as $pdfFile) {
$fileBase = pathinfo($pdfFile, PATHINFO_FILENAME);
if (isValidNewsFileBase($fileBase)) {
$fileBases[$fileBase] = true;
}
}
// Zusätzlich PNGs erfassen.
// Damit funktioniert die Bereinigung auch, falls die PDF bereits fehlt.
foreach (glob($pngDirectory . '/*_page_1.png') ?: [] as $pngFile) {
$fileName = basename($pngFile);
$suffix = '_page_1.png';
if (!str_ends_with($fileName, $suffix)) {
continue;
}
$fileBase = substr($fileName, 0, -strlen($suffix));
if (isValidNewsFileBase($fileBase)) {
$fileBases[$fileBase] = true;
}
}
$now = new DateTimeImmutable();
foreach (array_keys($fileBases) as $fileBase) {
// YYYY-MM-DD aus dem Dateinamen holen
$expiryDateString = substr($fileBase, 0, 10);
$expiryDate = DateTimeImmutable::createFromFormat(
'!Y-m-d',
$expiryDateString
);
// Zusätzlich prüfen, ob wirklich ein gültiges Datum entstanden ist
if (
$expiryDate === false ||
$expiryDate->format('Y-m-d') !== $expiryDateString
) {
error_log(
'Ungültiges Ablaufdatum bei News-Datei: ' . $fileBase
);
continue;
}
/*
* Aushang bleibt den gesamten angegebenen Tag sichtbar.
* Beispiel Ablaufdatum 2026-09-05:
* Löschung ab 2026-09-06 00:00 Uhr.
*/
$deleteFrom = $expiryDate->modify('+1 day');
if ($now >= $deleteFrom) {
if (!deleteNewsFiles($fileBase)) {
error_log(
'Abgelaufene News konnte nicht vollständig gelöscht werden: '
. $fileBase
);
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

@@ -0,0 +1,4 @@
<?php
require_once __DIR__ . '/../../general_files/ini/runtime_env.php';
define('HA_BASE_URL', rtrim(intranet_env('HA_BASE_URL'), '/'));
define('HA_TOKEN', intranet_env('HA_TOKEN'));
@@ -0,0 +1,512 @@
<?php
date_default_timezone_set('Europe/Berlin');
require_once __DIR__ . '/../contentpanel/insertion/news_cleanup.php';
// DB-Verbindung zentral laden (liefert PDO-Objekt zurück)
$pdo = require __DIR__ . '/../../general_files/ini/database.php';
$openWeatherApiKey = intranet_env('OPENWEATHER_API_KEY');
$heute = date('Y-m-d');
// --- Gottesdienst (Bibelwort & Eingangslied) ---
$sql = "SELECT bibelwort, bibelvers, eingang_nummer, eingang_titel
FROM gottesdienst
WHERE bibeldatum = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$heute]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$bibelwort = $row['bibelwort'];
$bibelvers = $row['bibelvers'];
$eingangNummer = $row['eingang_nummer'];
$eingangTitel = $row['eingang_titel'];
} else {
$bibelwort = "Bibelwort nicht eingetragen";
$bibelvers = "Bibelvers nicht gefunden.";
$eingangNummer = "N/A";
$eingangTitel = "Eingangslied nicht gefunden";
}
// --- Ansagen ---
$sqlAnsagen = "SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
FROM ansagen
WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
ORDER BY ansage_datum_von ASC";
$stmtAnsagen = $pdo->prepare($sqlAnsagen);
$stmtAnsagen->execute([$heute, $heute]);
$ansagenResult = $stmtAnsagen->fetchAll(PDO::FETCH_ASSOC);
// Verbindung schließen (optional, aber sauber)
$pdo = null;
// --- Aushänge ---
$pngDirectoryUrl = '../contentpanel/insertion/news/png/';
// Abgelaufene Aushänge einschließlich Original-PDF bereinigen
cleanupExpiredNews();
$pngDirectoryFs = realpath(
__DIR__ . '/../contentpanel/insertion/news/png'
);
$pngFiles = ($pngDirectoryFs && is_dir($pngDirectoryFs))
? scandir($pngDirectoryFs)
: [];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NAK Reinickendorf</title>
<link rel="icon" type="image/svg+xml" href="/intranet_repo/projects/_central/img/nak-logo.svg">
<link rel="shortcut icon" href="/intranet_repo/projects/_central/img/favicon.ico">
<link rel="stylesheet" href="stylo.css?v=<?php echo time(); ?>">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=SUSE:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Anton&display=swap" rel="stylesheet">
</head>
<body>
<div class="content-board">
<div class="content-column-right">
<div class="content-column-right-upper-box">
<div class="content-column-right-upper-box-right">
<div class="content-item-time">
<div class="time">
<div id="uhrzeit"></div>
</div>
<div class="date">
<center><?php
$months = ["UNDEFINED", "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
echo date("d. ") . $months[intval(date("m"))];
?></center>
</div>
</div>
<div class="content-item-daily">
<div class="header-sticky-note">
<div>Heutige Ansagen</div>
</div>
<div class="news-container">
<div class="news-scroll">
<?php foreach ($ansagenResult as $ansage) : ?>
<div class="news-box">
<div class="news-date">
<?php
$wochentage = [
'Mon' => 'Mo','Tue' => 'Di','Wed' => 'Mi','Thu' => 'Do',
'Fri' => 'Fr','Sat' => 'Sa','Sun' => 'So'
];
$datum_von_timestamp = strtotime($ansage['ansage_datum_von']);
$heute = date("Y-m-d");
$morgen = date("Y-m-d", strtotime('+1 day'));
$datum_von_format = date("Y-m-d", $datum_von_timestamp);
if ($datum_von_format === $heute) {
echo "Heute";
} elseif ($datum_von_format === $morgen) {
echo "Morgen";
} else {
$datum_von = date("d.m.", $datum_von_timestamp);
$wochentag_englisch = date("D", $datum_von_timestamp);
$wochentag = $wochentage[$wochentag_englisch] ?? $wochentag_englisch;
echo $wochentag . '&nbsp;' . $datum_von;
}
if (!empty($ansage['ansage_datum_bis'])) {
$datum_bis = date("d.m.", strtotime($ansage['ansage_datum_bis']));
echo "<br> bis " . $datum_bis;
}
?>
</div>
<div>
<?php echo htmlspecialchars($ansage['ansage_titel']); ?>
<?php if (!empty($ansage['ansage_untertitel'])) : ?>
<div class="news-subtitle"><?php echo htmlspecialchars($ansage['ansage_untertitel']); ?></div>
<?php endif; ?>
<?php if (!empty($ansage['ansage_uhrzeit']) && !empty($ansage['ansage_ort'])) : ?>
<div class="news-subtitle"><i><?php echo date("H:i", strtotime($ansage['ansage_uhrzeit'])) . ' Uhr'; ?>, <?php echo htmlspecialchars($ansage['ansage_ort']); ?></i></div>
<?php elseif (!empty($ansage['ansage_uhrzeit'])) : ?>
<div class="news-subtitle"><i><?php echo date("H:i", strtotime($ansage['ansage_uhrzeit'])) . ' Uhr'; ?></i></div>
<?php elseif (!empty($ansage['ansage_ort'])) : ?>
<div class="news-subtitle"><i><?php echo htmlspecialchars($ansage['ansage_ort']); ?></i></div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="content-column-right-upper-box-left">
<div class="content-item-middle-top">
<div class="header-sticky-note">
<div>Bibelwort</div>
</div>
<div class="bible"><?php echo $bibelwort; ?></div>
<div class="verse"><?php echo $bibelvers; ?></div>
</div>
<div class="content-column-double-item">
<div class="content-item">
<div class="header-sticky-note">
<div>Eingangslied</div>
</div>
<div class="GB"><?php echo $eingangNummer; ?></div>
<div class="GB-title"><?php echo $eingangTitel; ?></div>
</div>
<div class="content-item">
<div class="header-sticky-note">
<div>Temperatur</div>
</div>
<div class="temperature">
<div class="container">
<div id="wetter"></div>
</div>
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 60 60" width="64px" height="64px" baseProfile="basic">
<polygon fill="#66e1ff" points="46,22.11 46,24.42 43,26.15 43,34.24 25,44.63 14,38.28 14,35.97 12,34.81 10,35.97 7,34.24 7,20.38 4,20.96 4,18.65 22,8.26 34,5.95"/>
<polygon fill="#4292a6" points="25,30.774 28,34.815 43,26.155 43,34.238 25,44.63"/>
<polygon fill="#ff5757" points="16,18.649 4,20.959 4,18.649 16,16.34 28,32.506 28,34.815"/>
<polygon fill="#4292a6" points="34,8.257 22,10.566 22,8.257 34,5.947 46,22.113 46,24.423"/>
<polygon fill="#e64e4e" points="4,18.649 22,8.257 34,5.947 16,16.34"/>
<polygon fill="#cc4545" points="28,32.506 46,22.113 34,5.947 16,16.34"/>
<polygon fill="#b33d3d" points="28,32.506 46,22.113 46,24.423 28,34.815"/>
<polygon fill="#4292a6" points="17,34.238 22,37.124 22,31.351 17,28.464"/>
<polygon fill="#4292a6" points="10,35.97 12,34.815 12,27.887 10,26.732"/>
<polygon fill="#4ad8ff" points="19,33.083 22,34.815 22,31.351 19,29.619"/>
<polygon fill="#52b1cc" points="17,34.238 22,37.124 22,34.815 19,33.083"/>
<polygon fill="#ff5757" points="12,34.815 14,35.97 14,29.041 12,27.887"/>
</svg>
<div class="text-indoor" id='temperature_txt'></div>
</div>
</div>
</div>
</div>
</div> <!-- /left upper box -->
</div> <!-- /right upper -->
<div class="content-item-bottom-box">
<div class="header-sticky-note">
<div>Aushänge</div>
</div>
<div class="aushang-flex">
<?php
$hasPng = false;
foreach ($pngFiles as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'png') {
$hasPng = true;
echo '<img src="' . $pngDirectoryUrl . $file . '" alt="Aushang" height="95%">';
}
}
if (!$hasPng) {
echo '<p style="text-align: left;">Keine Aushänge vorgeplant.</p>';
}
?>
</div>
</div>
</div> <!-- /right column -->
<div class="content-column-left">
<div class="content-item-video">
<!-- leerer Container, kein Video/HLS -->
<div class="video-container" aria-label="Leer"></div>
</div>
<div class="content-item-month">
<div class="header-sticky-note">
<div>Monatsplan</div>
</div>
<div class="content-item-month-cell">
<img src="month/terminuebersicht-1.png?v=<?php echo time(); ?>" alt="image">
<img src="month/terminuebersicht-2.png?v=<?php echo time(); ?>" alt="image">
</div>
</div>
</div> <!-- /left column -->
</div> <!-- /content-board -->
<script>
function updateClock() {
var now = new Date();
var uhrzeitElement = document.getElementById("uhrzeit");
var stunden = now.getHours();
var minuten = now.getMinutes();
uhrzeitElement.innerHTML = + padZero(stunden) + ':' + padZero(minuten);
}
function padZero(number) { return number < 10 ? '0' + number : number; }
updateClock();
setInterval(updateClock, 1000);
// OpenWeather (Außentemperatur+Icon)
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=52.571291&lon=13.310422&appid=<?= rawurlencode($openWeatherApiKey) ?>&units=metric&language=german`;
// Wetter-Icons
const cloudy = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<defs>
<filter id="blur" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2.5"/>
<feOffset dx="0" dy="2" result="off"/>
<feComponentTransfer><feFuncA type="linear" slope="0.15"/></feComponentTransfer>
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<g filter="url(#blur)">
<path d="M22 46h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 20 29a9 9 0 0 0 2 17z" fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<path d="M18 46h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 17 31a8 8 0 0 0 1 15z" fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/>
</g>
</svg>`;
const sun = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round">
<circle cx="32" cy="32" r="10" fill="#FFD34D" stroke="#fff"/>
<path d="M32 6v8M32 50v8M6 32h8M50 32h8M13.5 13.5l5.7 5.7M44.8 44.8l5.7 5.7M50.5 13.5l-5.7 5.7M19.2 44.8l-5.7 5.7"/>
</g>
</svg>`;
const moon = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<path d="M41 8a22 22 0 1 0 15 35A18 18 0 0 1 41 8z" fill="#DDE7FF" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
</svg>`;
const cloudy_day = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g>
<g transform="translate(4,0)">
<circle cx="22" cy="22" r="9" fill="#FFD34D" stroke="#fff" stroke-width="2"/>
<g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round">
<path d="M22 6v6M22 32v6M6 22h6M32 22h6M10.5 10.5l4.2 4.2M29.3 29.3l4.2 4.2M33.5 10.5l-4.2 4.2M14.7 29.3l-4.2 4.2"/>
</g>
</g>
<g transform="translate(8,10)">
<path d="M18 36h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 16 19a9 9 0 0 0 2 17z"
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<path d="M22 36h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 21 21a8 8 0 0 0 1 15z"
fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/>
</g>
</g>
</svg>`;
const cloudy_night = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g>
<path d="M38 10a16 16 0 1 0 11 26A13 13 0 0 1 38 10z" fill="#DDE7FF" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<g transform="translate(10,18)">
<path d="M14 28h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 12 11a9 9 0 0 0 2 17z"
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<path d="M18 28h22a9 9 0 0 0 0-18c-.7 0-1.4.1-2 .2A11 11 0 0 0 17 13a8 8 0 0 0 1 15z"
fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round" opacity="0.95"/>
</g>
</g>
</svg>`;
const rain_shower = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g>
<path d="M20 36h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 19a9 9 0 0 0 2 17z"
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<g stroke="#fff" stroke-width="2" stroke-linecap="round">
<path d="M24 42l-3 6" />
<path d="M34 42l-3 6" />
<path d="M44 42l-3 6" />
</g>
</g>
</svg>`;
const rain = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g>
<path d="M20 34h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 17a9 9 0 0 0 2 17z"
fill="#57A0EE" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<g stroke="#fff" stroke-width="2" stroke-linecap="round">
<path d="M22 40l-3 8" />
<path d="M32 40l-3 8" />
<path d="M42 40l-3 8" />
<path d="M52 40l-3 8" />
</g>
</g>
</svg>`;
const thunder = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g>
<path d="M18 34h28a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 16 17a9 9 0 0 0 2 17z"
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<path d="M32 36l-6 12h7l-3 10 12-16h-7l3-6z"
fill="#FFD34D" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
</g>
</svg>`;
const snowy = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g>
<path d="M20 34h26a10 10 0 0 0 0-20c-.8 0-1.6.1-2.4.3A13 13 0 0 0 18 17a9 9 0 0 0 2 17z"
fill="#91C0F8" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
<g fill="#fff">
<circle cx="24" cy="44" r="2"/>
<circle cx="34" cy="44" r="2"/>
<circle cx="44" cy="44" r="2"/>
<circle cx="29" cy="52" r="2"/>
<circle cx="39" cy="52" r="2"/>
</g>
</g>
</svg>`;
const mist = `<svg xmlns="http://www.w3.org/2000/svg" width="49" height="49" viewBox="6 0 50 50" aria-hidden="true">
<g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.95">
<path d="M14 24h36"/>
<path d="M10 32h44"/>
<path d="M14 40h36"/>
<path d="M18 48h28"/>
</g>
</svg>`;
async function getWeather() {
try {
var response = await fetch(apiUrl);
var data = await response.json();
if (data.cod === 200) {
var weatherIcon = data.weather[0].icon;
var temperature = data.main.temp;
var weatherElement = document.getElementById('wetter');
if (["04d","04n","03d","03n"].includes(weatherIcon)) {
weatherElement.innerHTML = '<div>' + cloudy + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (weatherIcon === "02d") {
weatherElement.innerHTML = '<div>' + cloudy_day + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (weatherIcon === "02n") {
weatherElement.innerHTML = '<div>' + cloudy_night + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (weatherIcon === "01d") {
weatherElement.innerHTML = '<div>' + sun + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (weatherIcon === "01n") {
weatherElement.innerHTML = '<div>' + moon + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (["09d","09n","10n"].includes(weatherIcon)) {
weatherElement.innerHTML = '<div>' + rain_shower + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (weatherIcon === "10d") {
weatherElement.innerHTML = '<div>' + rain + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (["11d","11n"].includes(weatherIcon)) {
weatherElement.innerHTML = '<div>' + thunder + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (["13d","13n"].includes(weatherIcon)) {
weatherElement.innerHTML = '<div>' + snowy + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
} else if (["50d","50n"].includes(weatherIcon)) {
weatherElement.innerHTML = '<div>' + mist + `</div><div class="text-outdoor"><center>${Math.round(temperature)}°C</center></div>`;
}
} else {
console.error('Fehler beim Abrufen des Wetters:', data.message);
}
} catch (error) {
console.error('Fehler beim Abrufen des Wetters:', error.message);
}
}
getWeather();
setInterval(getWeather, 30000);
// INNENTEMPERATUR: jetzt über Home Assistant (Server-Proxy)
function updateTemperature() {
const entityId = 'sensor.temp_temperatur';
fetch('ha_temperature.php?entity_id=' + encodeURIComponent(entityId))
.then(r => r.json())
.then(data => {
if (data.error) {
console.error('HA Fehler:', data);
return;
}
document.getElementById('temperature_txt').innerText = `${data.value}${data.unit || '°C'}`;
})
.catch(err => console.error('Fetch Fehler:', err));
}
updateTemperature();
setInterval(updateTemperature, 10000);
function updateContent() {
fetch('update_content.php')
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Fehler:', data.error);
return;
}
document.querySelector('.bible').innerText = data.bibelwort;
document.querySelector('.verse').innerText = data.bibelvers;
document.querySelector('.GB').innerText = data.eingang_nummer;
document.querySelector('.GB-title').innerText = data.eingang_titel;
setupScrolling();
})
.catch(error => {
console.error('Fehler beim Abrufen der Daten:', error);
});
}
updateContent();
setInterval(updateContent, 30000);
function setupScrolling() {
const SPEED_V = 7; // News vertikal
const SPEED_H = 10; // Aushänge horizontal
const containerFlex = document.querySelector('.aushang-flex');
if (containerFlex) {
const images = containerFlex.querySelectorAll('img');
const measureAndApplyH = () => {
let totalWidth = 0;
images.forEach(img => {
const styles = window.getComputedStyle(img);
const mr = parseFloat(styles.marginRight) || 0;
totalWidth += (img.offsetWidth || 0) + mr;
});
const containerWidth = containerFlex.offsetWidth || 0;
const scrollDistanceH = Math.max(0, totalWidth - containerWidth);
containerFlex.style.setProperty('--scroll-distance-h', `-${scrollDistanceH}px`);
const durationH = Math.max(1, scrollDistanceH / SPEED_H);
if (scrollDistanceH === 0) {
containerFlex.style.animation = 'none';
} else {
containerFlex.style.animationName = 'scroll-h';
containerFlex.style.animationDuration = `${durationH}s`;
containerFlex.style.animationTimingFunction = 'linear';
containerFlex.style.animationIterationCount = 'infinite';
}
};
let pending = 0;
images.forEach(img => {
if (!img.complete || img.naturalWidth === 0) {
pending++;
img.addEventListener('load', () => {
pending--;
if (pending === 0) measureAndApplyH();
}, { once: true });
}
});
if (pending === 0) measureAndApplyH();
}
const containerNews = document.querySelector('.news-container');
const scrollBox = document.querySelector('.news-scroll');
if (containerNews && scrollBox) {
const totalHeight = scrollBox.scrollHeight || 0;
const visibleHeight = containerNews.offsetHeight || 0;
const scrollDistanceV = Math.max(0, totalHeight - visibleHeight);
scrollBox.style.setProperty('--scroll-distance-v', `-${scrollDistanceV}px`);
const durationV = Math.max(1, scrollDistanceV / SPEED_V);
if (scrollDistanceV === 0) {
scrollBox.style.animation = 'none';
} else {
scrollBox.style.animationName = 'scroll-v';
scrollBox.style.animationDuration = `${durationV}s`;
scrollBox.style.animationTimingFunction = 'linear';
scrollBox.style.animationIterationCount = 'infinite';
}
}
}
window.addEventListener('load', () => {
const news = document.querySelector('.news-scroll');
const aushang = document.querySelector('.aushang-flex');
if (news) news.style.animation = 'none';
if (aushang) aushang.style.animation = 'none';
setTimeout(setupScrolling, 50);
});
window.addEventListener('resize', () => {
const news = document.querySelector('.news-scroll');
const aushang = document.querySelector('.aushang-flex');
if (news) news.style.animation = 'none';
if (aushang) aushang.style.animation = 'none';
setTimeout(setupScrolling, 100);
});
</script>
</body>
</html>
@@ -0,0 +1,60 @@
<?php
header('Content-Type: application/json; charset=utf-8');
// Token/URL aus externer Config laden
require_once __DIR__ . '/config.local.php';
// Entity-ID (Standard: dein Sensor aus der Liste)
$entityId = isset($_GET['entity_id']) ? $_GET['entity_id'] : 'sensor.temp_temperatur';
$url = rtrim(HA_BASE_URL, '/') . '/api/states/' . urlencode($entityId);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . HA_TOKEN,
'Content-Type: application/json'
],
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode < 200 || $httpCode >= 300) {
http_response_code(502);
echo json_encode([
'error' => 'Home Assistant request failed',
'details' => $error ?: ('HTTP ' . $httpCode)
]);
exit;
}
$data = json_decode($response, true);
if (!is_array($data) || !isset($data['state'])) {
http_response_code(500);
echo json_encode(['error' => 'Unexpected HA response']);
exit;
}
$val = $data['state'];
$unit = $data['attributes']['unit_of_measurement'] ?? '°C';
$CALIBRATION_OFFSET = +0;
if (is_numeric($val)) {
$number = round((float)$val + $CALIBRATION_OFFSET, 1);
} else {
$number = $val;
}
echo json_encode([
'entity_id' => $entityId,
'value' => $number,
'unit' => $unit,
'friendly_name' => $data['attributes']['friendly_name'] ?? null,
'last_changed' => $data['last_changed'] ?? null,
]);
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

@@ -0,0 +1,340 @@
/* Gesamtes Seiten-Layout */
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'SUSE', sans-serif;
font-size: 20px;
background-image: url('cloudy.jpg');
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
overflow: hidden;
}
/* Content Board Layout */
.content-board {
background-color: #a8a6a7a7;
width: 99%;
height: 97%;
box-sizing: border-box;
display: flex;
justify-content: space-around;
align-items: center;
border-radius: 0;
padding: 0 6px 7px 3px;
}
.content-column-left {
width: 40%;
height: 100%;
box-sizing: border-box;
}
.content-column-right {
width: 58%;
height: 100%;
box-sizing: border-box;
}
.temperature {
display: flex;
justify-content: space-around;
align-items: center;
padding: 5px 0 0 0;
height: 70%;
}
.verse {
text-align: justify;
color: rgb(67, 67, 67);
padding: 5px 15px 5px 15px;
}
.date {
color: rgb(67, 67, 67);
font-weight: bold;
}
.content-column-right-upper-box-left {
width: 60%;
height: 100%;
box-sizing: border-box;
}
.content-column-right-upper-box-right {
width: 39%;
height: 100%;
box-sizing: border-box;
}
.content-column-right-upper-box {
width: 100%;
height: 51%;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
}
.content-item-time {
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #ffffffab;
width: 100%;
height: 30%;
margin: 15px 0 15px 0;
box-sizing: border-box;
}
.content-item-news {
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #ffffffab;
width: 100%;
height: 63%;
margin: 15px 0 15px 0;
box-sizing: border-box;
}
.content-item-video {
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #cbc5bc;
width: 100%;
height: 40%;
margin: 15px 0 15px 0;
box-sizing: border-box;
}
.content-item-middle-top {
position: relative;
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #cbc5bc;
width: 100%;
height: 60%;
margin: 15px 0 15px 0;
box-sizing: border-box;
overflow: hidden;
}
/* Pseudoelement für den animierten border-bottom Effekt */
.content-item-middle-top::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 15px;
background: linear-gradient(to bottom, transparent, #cbc5bc);
}
/* Monatsplan: jetzt mit weißem Hintergrund */
.content-item-month {
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #cbc5bc7d;
width: 100%;
height: 56%;
margin: 15px 0 15px 0;
box-sizing: border-box;
overflow: hidden;
}
.content-item-month-cell {
display: flex;
background-color: #ffffffe5;
justify-content: center; /* Zentriere die Bilder horizontal */
height: 94%;
align-items: center; /* Zentriere die Bilder vertikal */
}
.content-item-month-cell img {
max-width: 100%; /* Verhindert, dass das Bild größer als der Container wird */
max-height: 100%; /* Verhindert Verzerrungen, indem es die maximale Höhe auf die Containerhöhe beschränkt */
object-fit: contain; /* Stellt sicher, dass das Bild im Container bleibt, ohne gestreckt zu werden */
}
.content-item-daily {
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #edebe8;
width: 100%;
height: 63%;
margin: 15px 0 15px 0;
box-sizing: border-box;
overflow: hidden;
}
.header-sticky-note {
font-size: 25px;
display: flex;
flex-direction: column;
align-items: center;
height: 20px;
width: 100%;
padding: 5px 0 15px 0;
border-bottom: 2px solid #38312430;
background-color: #cbc5bc;
border-radius: 0;
}
.time {
font-size: 80px;
text-align: center;
padding: 8px 0 0 0;
}
.bible {
text-align: center;
font-size: 60px;
padding: 10px 0 10px 0;
border-bottom: 2px solid #38312456;
background-color: #edebe8;
}
.GB {
text-align: center;
font-size: 60px;
padding: 5px 0 0 0;
}
.GB-title {
text-align: center;
color: rgb(67, 67, 67);
}
.content-column-double-item {
width: 100%;
height: 33%;
margin: 15px 0 15px 0;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Elemente innerhalb des Content Boards */
.content-item {
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #edebe8;
height: 100%;
width: 49%;
}
.content-item-bottom-box {
border-radius: 0;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
background-color: #edebe8;
margin: 6px 0 0 0;
height: 47.5%;
width: 100%;
overflow: hidden;
}
.video-container {
border-radius: 0;
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
border: 5px solid #cbc5bc;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
}
.video-container video {
width: 100%;
height: 100%;
transform: scale(1.2);
object-fit: cover;
border-radius: 0;
}
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 40%;
background-color: #38312431;
border-radius: 0;
border: 2px solid #38312440;
}
.text-outdoor {
font-weight: bold;
padding: 9px 0 0 0;
margin: 0 0 15px 0;
}
.text-indoor {
font-weight: bold;
margin: 0 0 15px 0;
}
.news-box {
display: flex;
justify-content: flex-start;
padding: 5px 15px 5px 15px;
border-bottom: 2px solid #38312456;
background-color: #edebe8;
word-wrap: break-word;
}
.news-date {
font-weight: bold;
margin: 0 10px 0 0;
}
.news-subtitle {
color: rgba(67, 67, 67, 0.657);
}
.news-container {
height: 290px;
overflow: hidden;
position: relative;
}
.news-scroll {
display: flex;
flex-direction: column;
animation-name: scroll-v;
animation-timing-function: linear;
animation-iteration-count: infinite;
--scroll-distance-v: 0px;
}
@keyframes scroll-v {
0% { transform: translateY(0); }
100% { transform: translateY(var(--scroll-distance-v)); }
}
.aushang-flex img {
border-radius: 0;
margin: 0 10px 0 0;
border: 2px solid #38312440;
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.08);
}
.aushang-flex {
padding: 0 5px 0 5px;
display: flex;
justify-content: space-evenly;
align-items: center;
height: 90.5%;
border-radius: 0;
animation-name: scroll-h;
animation-timing-function: linear;
animation-iteration-count: infinite;
--scroll-distance-h: 0px;
}
@keyframes scroll-h {
0% { transform: translateX(0); }
100% { transform: translateX(var(--scroll-distance-h)); }
}
@@ -0,0 +1,54 @@
<?php
header('Content-Type: application/json; charset=utf-8');
date_default_timezone_set('Europe/Berlin');
// DB-Verbindung zentral laden
$pdo = require __DIR__ . '/../../general_files/ini/database.php';
$heute = date('Y-m-d');
try {
// --- Bibelwort, Bibelvers und Eingangslied ---
$sql = "SELECT bibelwort, bibelvers, eingang_nummer, eingang_titel
FROM gottesdienst
WHERE bibeldatum = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$heute]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$bibelwort = $row['bibelwort'];
$bibelvers = $row['bibelvers'];
$eingangNummer = $row['eingang_nummer'];
$eingangTitel = $row['eingang_titel'];
} else {
$bibelwort = "Bibelwort nicht eingetragen";
$bibelvers = "Bibelvers nicht gefunden.";
$eingangNummer = "N/A";
$eingangTitel = "Eingangslied nicht gefunden";
}
// --- Ansagen ---
$sqlAnsagen = "SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
FROM ansagen
WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
ORDER BY ansage_datum_von ASC";
$stmtAnsagen = $pdo->prepare($sqlAnsagen);
$stmtAnsagen->execute([$heute, $heute]);
$ansagenResult = $stmtAnsagen->fetchAll(PDO::FETCH_ASSOC);
// Rückgabe als JSON
echo json_encode([
'bibelwort' => $bibelwort,
'bibelvers' => $bibelvers,
'eingang_nummer' => $eingangNummer,
'eingang_titel' => $eingangTitel,
'ansagen' => $ansagenResult
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
// Optional: error_log($e->getMessage());
echo json_encode(['error' => 'Fehler bei der Verbindung zur Datenbank'], JSON_UNESCAPED_UNICODE);
} finally {
$pdo = null;
}
@@ -0,0 +1,112 @@
<?php /* Name: developer.php
Beschreibung: Entwicklungsprozess (Bearbeitungsseite)
- inkl. Aufruf von button_edit.php */
// Initialisierung
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
</head>
<body class="theme-green">
<?php /* Besondere Entwickler-Rechte checken */
// Fetch user details
$sql = "SELECT * FROM user_permissions up INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id WHERE up.user_id = {$_SESSION["user_id"]}";
$result = $pdo->query($sql);
$isDeveloper = $result->fetch(PDO::FETCH_ASSOC);
// Check if the user has developer access (Entwickler = 1)
if ($isDeveloper["Entwickler"] == 1) {
$sql_groups = "SELECT * FROM user_permissions";
$stmt = $pdo->prepare($sql_groups);
$stmt->execute();
$groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
header("Location: {$g_httpHomePath}");
exit();
}
?>
<?php // Es folgen ab hier die HTML-Ausgaben beginnend mit dem HTML-Header: ?>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php // Header (erweitert): Titel der Seite mit Funktionsaufruf ?>
<div class="header_title ht_dev text_white" id="header_title">
<div class="flexrow fullwidth" style="justify-content:space-between !important;">
<div class="header_title_text">Entwicklungsprozess</div>
<div class="small_text" style="margin-right:8px;margin-left:8px;align-self:center;">
<?php echo "$p_nac_base // $p_version ($p_status)" ?>
</div>
</div>
</div>
<?php // Die oberen Standard-Overlays müssen vor dem Content-Bereich platziert sein. ?>
<div class="content" id="base">
<div class="content_flexcontainer">
<div class="flexright">
<?php // Info-Panel einbinden
require_once 'developer_info.php'; ?>
<?php // Info-Panel einbinden
//require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/_templates/playground/table_openClose.php'; ?>
</div>
<div class="flexleft">
<?php // TODO: Durch die Developer-Textpanels iterieren
// TODO: Dev-Texte werden später noch in die Datenbank integriert
$edit_id = 1;
$filename = "work/todo.txt";
$filecontent = file_get_contents($filename);
$filecontent_output = markdownFormat($filecontent);
if (isset($_POST['speichern'])) {
$filecontent = $_POST['filecontent'];
file_put_contents($filename, $filecontent); //Speichern + Bearbeitungsmodus verlassen
header("Location: {$g_httpCurrentCleanUrl}");
exit();
}
?>
<div class="content_panel" id="1">
<div class="flexrow_right fullwidth">
<?php require $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/snippets/button_edit.php'; ?>
</div>
<?php if ( (isset($_GET['action']) && $_GET['action'] == 'edit') &&
(isset($edit_id) && $edit_id == 1) ) { ?>
<form class="display_within_dev" action="" method="post" id="<?php echo $edit_id; ?>">
<textarea id="filecontent" name="filecontent" autofocus><?php echo $filecontent; ?></textarea>
<div>
<input class="button" type="submit" value="Speichern" name="speichern">
<input class="button" type="button" value="Abbrechen" onclick="window.location.href=window.location.href.split('?')[0];">
</div>
</form>
<?php } else { ?>
<div class="content_panel_output"><?php echo $filecontent_output; ?></div>
<?php } ?>
</div>
</div>
</div>
</div> <!-- EOD: content -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,70 @@
<?php /* Name: developer_info.php
Beschreibung: Serverinformationen und Entwicklungsübersicht */
?>
<?php
$label1 = "Allgemein";
$label_class1 = "text_green";
$array_admin1 = [
"Name" => "Max Mustermann",
"Alter" => 30,
"Beruf" => "Softwareentwickler",
// Weitere Daten ...
];
?>
<?php // Info-Panel einbinden
// require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/_templates/playground/table_openClose.php'; ?>
<?php
$label2 = "Kundendaten";
$label_class2 = "text_blue";
$array_admin2 = [
"Kundennummer" => "12345",
"Name" => "Erika Musterfrau",
"E-Mail" => "erika@example.com",
// Weitere Daten ...
];
?>
<?php // Info-Panel einbinden
// require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/_templates/playground/table_openClose.php'; ?>
<div class="content_panel" id="data">
<table>
<thead>
<tr>
<th colspan="2">
<label class="text_green">Allgemeines</label>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($array_admin as $key => $value): ?>
<tr>
<td><?= $key ?></td>
<td><?= nvl($value, '---') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<table>
<thead>
<tr>
<th colspan="2">
<label class="text_green">Serverinformationen</label>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($array_dev as $key => $value): ?>
<tr>
<td><?= $key ?></td>
<td><?= nvl($value, '---') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
@@ -0,0 +1,16 @@
<h2>Grafiken</h2>
**SVG**
https://www.svgrepo.com/
- Iconsax Bold Oval Icons Collection
-- https://www.svgrepo.com/collection/iconsax-bold-oval-icons/
- Lightning Design Utility Icons Collection
-- https://www.svgrepo.com/collection/lightning-design-utility-icons/
- Bootstrap UI Icons Collection
-- https://www.svgrepo.com/collection/bootstrap-ui-icons/
- Zest Interface Icons Collection
-- https://www.svgrepo.com/collection/zest-interface-icons/
@@ -0,0 +1,159 @@
<h2>Relaunch: Einführung (Aufgaben Samu)</h2>
**Umstrukturierung: SeelsorgePlaner**
- projects\seelsorge\besuchsstellen.php (und die eingebundenen Unterseiten):
:: Fortsetzung Technik:
:::: 1. Bei Klicken auf Buttons und Bilder auf der aktuellen Ansicht verbleiben, anstelle auf die Startseite (active) zu wechseln (AJAX Funktion).
:::: 2. Sicherung: Aufruf aller Seiten zu Seelsorge nur für Amtsgeschwister !!!
- Alle Objekte die über den Browser theoretisch aufgerufen werden können: -> Sicherheit, dass kein Quellcode gelesen werden kann. Routing ggf. auf LogIn-Seite.
- Web-Server: Kein Laden eines Objektordners. Damit keine Dateien gesichtet oder explizit heruntergeladen werden können.
- Analyse: Mehrfach-Codes -> Optimierung: Auslagern in separate Files (snippets-Ordner)
*Admin-Seite*
- Profil als Pop-Up (analog Besuchsstellen-Seite)
*Seelsorge-Seiten*
- Workaround zu openCloseProfile ggf. optimieren oder besser neue Lösung schaffen.
*Prüfung*
- Alle Buttons prüfen. Ist die Textgröße in den Buttons für Bildschirm und Handy okay?
**Fehlerbehebung (SAMU) **
- Bugfix: *visit.php*
-- Nach einer Aktion (Erneuern/Priorisieren) wird die Seite neu geladen für den Fokus auf Datensatz. Das funktioniert aktuell nicht wg. Seitenverlinkung auf seelsorge.php. Hier brauchen wir einen neuen Denkansatz. Wie gestalten wir das? Ist ggf. doch eine Aufteilung der Seitenbereiche zurück auf zwei extra Seiten notwendig oder gibt es die Möglichkeit dies per Javascript zu steuern, indem beim Neuladen der Seite die zu aktivierenden Elemente bekannt sind und ein-/ausgeblendet werden? Kann auf ein Neuladen der Seite verzichtet werden, indem per AJAX nur der Inhalt aktualisiert wird?
- *calender_edit.php*
-- Datum in der Vergangenheit sind anwählbar im Edit-Modus. Erst ab dem aktuellen Tag darf das möglich sein.
** Datenbankanbindung der Verfügbarkeiten (Samu) --> "greenbox" auf Kalender-Item **
- Datenabfrage: bei abgelaufenen Tagen keine Prüfung auf Verfügbarkeit nötig -> ohne "greenbox"
** Fehlerbereinigung Browser: Alle bekannten Browser checken**
- Style-Fehler/-Optimierung melden und ggf. von Markus korrigieren lassen
**Bug auf Developer-Seite: (Hilfe durch Samu)**
- Wenn Cookie *modus=edit* und Seite verlassen wird, bleibt das Cookie erhalten.
-- Reset des Cookies beim Verlassen der Seite. Allerdings muss das Cookie bestehen bleiben, wenn weiterhin bearbeitet werden soll. Andere Cookies müssen ebenfalls resettet werden, die nichts mit anderen Seiten zu tun haben. Können wir dazu bestimmte Modi einsetzen? z. B. "modus=dev_edit" oder Kombi-Prüfung auf pageFile+modus?
-- Fehler beim Speichern beheben
-- Welche Lösungsansätze haben wir, um die einzelnen Aufgaben der ToDo-Liste zentral (und nur online) zu pflegen?
:: Einzelne Datensätze?
:: Einzelne Textdateien in einem geschützten Dateiordner?
** Seite Kalender: (Mitarbeit durch Samu)**
-- **Absprache:** Ist das Kalenderblatt final?
-- Was wollen wir ggf. in der zweiten Übersicht der Verfügbarkeiten oder einer zusätzlichen Sidebar auflisten? (Feiertage, besondere Termine aus dem CKalender)
:: >> Welche Informationen sind wichtig / welche sind nicht erforderlich?
:: >> Was erledigen wir im nächsten Release?
:: **Auflistung aller Besuchsstellen mit:**
-- Speicherung von "Letzter Besuch"?
<h2>Weiterentwicklung (Markus)</h2>
** Layout verfeinern **
- admin.php << Adminseite sowie seelsorge.php << Besuchsstellen-Seite:
-- Tabelle, bei Cursor-Tasten drücken: rauf/runter -> Markierung der Tabellenzeile wechseln
- Farben über Farbdefinitionen (var)
- Farben vereinheitlichen z.b. #00000 -> black-variable
** Kleine Optimierung:**
- Wiederholte absolute Pfade in Variablen speichern:
-- ../general_files
-- ../_central
-- ../intranet
-- ../kalli
-- ../seelsorge
-- ../authorization
- Wiederholte Codes in extra Code-Snippets auslagern:
-- z. B. admin.php: Bildausgabe
- Anstelle von "exit;" schreiben wir "exit();"
**Kalender:**
- Aufnahme Ferienzeiten Berlin/Brandenburg
<HR>
<h2>2. Release: Nice-to-have´s und Umsetzung von neuen Anforderungen</h2>
** Neue Seite: Chat**
- Implementierung des Chats
- Chatverlauf mit Datum, Uhrzeit, Nachricht, Autor, Profilbild und Möglichkeit zur Texteingabe
- Chatmöglichkeit mit jedem User oder nur Funktionsträger oder auschließlich nur für Amtsgeschwister?
** Kalli **
- Datum und Uhrzeit Felder direkt anwählbar? Denn es muss zweimal in das Feld geklickt werden.
- Bei nicht Media Felder nebeneinander darstellen? Ggf. Padding in Statusmeldung-Feld erhöhen?
** Seite Kalender:**
- CKalender-Einbindung: Welche Infos zur entsprechenden Rolle (Chorsänger, Putzgruppe, Technik) wollen wir zur Ausgabe bringen?
- Neben Kalenderblatt: Sidebar mit nächsten Feiertagen, Ferien, besondere Termine als Vorschau (mit welchem Zeitraum)
- Bessere Darstellung der Feiertage bei @media?
-- Zum Beispiel: Bei Media ausblenden der Feiertage und kleinen rosa Punkt anzeigen. Feiertag in Verfügbarkeit anzeigen.
** Seite Besuchsstellen (bzw. erweiterte Adminseite) **
- Sollen Administratoren Datensätze löschen und bearbeiten? Betrifft alle Daten: Priesterbezirk, Adresse, Kommunikationsdaten und Hinweis
-- für Priesterbezirk benötigen wir eine Auswahlfunktion, um den neuen Priester einzusetzen.
- Sollen Hinweistexte verkürtzt dargestellt und ausklappbar sein? Button mit Pfeil nach unten/oben.
-- Umsetzungsmöglichkeit: Button über Wechsel der Klasse mit enthaltenen bgimage. Text: Mit Display none, opacity 0. wird dann zu Display Block mit opacity 1.
- Aktions-Buttons nicht mit Text sondern Symbole und Toolbartext einblenden für eine kompaktere Darstellung? Oder dies bei @media für eine bessere Übersichtlichkeit?
** Seitenbereich Kalendertag **
- Macht eine Sidebar-Darstellung des Kalenderblatts Sinn und wäre es damit komfortabler? --> Mini-Kalender zum betreffenden Monat und Anklick-Möglichkeit zum Wechsel des Kalendertages in einer kleineren kompakten Ansicht. Oder sogar wählbar mit Vergrößern/Verkleinern-Button?
** Datenausgabe formatiert anzeigen**
- z. B. Telefonnummern. Was ist dafür notwendig, gibt es dazu verfügbare Funktionen (oder reguläre Ausdrücke)? Eventuell gibt es etwas externes, was wir einbinden können?
** Optimierung profile.php**
- Nach Bestätigung der Änderung Statuszeile/Overlay einblenden (ggf. nach ein paar Sekunden wegblenden)
** Style- Optimierung **
- Dunkelmodus anbieten und für jeden User speichern, ob hell/dunkel
- Profilfotos in thumbnails dublizieren. Nur in der maximal notwendigen Auflösung werden für die Ausgabe verwendet. Reichen 100x100px aus?
- Content-Bereich vergrößern/verschieben indem das Menü ausgeblendet wird. Beim Zeigen mit der Maus auf den Bereich soll es wieder angezeigt werden, Sliden. Oder durch Klicken kann der Bereich rein und raus geschoben werden, wobei sich der Content-Bereich anpasst.
- Entweder beim Scrollen innerhalb von FORM (das mit content_fullsize umschlossen ist) und mindestens die Seitengröße hat (ggf. ist ein Scrollbalken vorhanden) Schaltfläche mit scrollToTop oder ähnliches einblenden. Alternativ festen Button einsetzen.
- Layout: Farben: Prüfung/Korrektur auf richtigen Farbcode bzw. Farbverbesserung
- overlay_message.php: OK-Button oder ähnliches anstelle des Exit-Buttons?
-- Oder automatisches Ausblenden z. B. nach 2 bis 4 Sekunden (ggf. Message links unten kurz ein und wieder ausblenden - evntl. bei kleinen Bildschirmen anders formatiert anzeigen) - aös animierte Darstellung mit grünem oder weißen Haken.
** Seite Admin **
- Vorschlag: Nutzer als Blöcke untereinander bei Media. Onclick Element um Berechtigungen als Liste einzublenden. Bearbeitungs-Button um Einträge zu ändern, Toogle Button zum Bedienen. Entweder sofortige Speicherung oder Bestätigungs-Button?
** Seite Developer **
- Farbe azure harmony einsetzen für Schattierungen anstelle von #DDDDDD.
<h2>3. Release: - IDEE - Eigenständiges KALLI und Ablösung von CKalender</H2>
- Alle CKalender Funktionen werden adaptiert.
- Generischer Aufbau der HTML-Ausgabe anhand der Datenbankinhalte.
-- Online-Verwaltung von Terminarten
-- Online-Verwaltung von Nutzergruppen
-- Nutzerverwaltung: Ansicht und Bedienung vereinfachen und je nach Medium anders darstellen -> One-Button-Mechanik...
-- Aufteilung der Administration in mehrere Ebenen und ansprechendes Seitenmenü.
- Profil, eigenes: An/abwahl von Kontaktmedien: WhatsApp, Signal etc. -> Button mit Hintergrund mit Hovertext / oder als Text mit Tooglebar?
- Admin . Tabelle nach Namen sortieren
- w3schools.com CSS Templates einsetzen
- Kalendarium: Brauchen wir einen Countdown zum Reload? Wenn nein, Funktionen, Content und SVG löschen
- Seelsorge Menü - Javascript: auch bei Rezise nachjustieren
-- Bottom von Indikatorstreifen an menu-item ausrichten, zumindest bei @media
**Alle Codes optmieren**
- Feiertage: Berliner und regionale Feiertage vervollständigen. Ggf. andere API nutzen.
- Profilseite, persönliche: Alle Amtsgeschwister haben die Möglichkeit Ihre Zeitpräferenzen in Wochentagen (Buttons) und Uhrzeit von/bis (normale Uhrzeit-Felder) einzutragen - hier: Erweitern der Datenbank
**Anfragemöglichkeit für Amtsgeschwister**
-- Routenplaner: Eigene Adresse -> Amtsgeschwister-Adresse -> Besuchsstelle
... Kontaktaufnahem über Chat ...
- **profile.php**
- Änderung des Passwortes ermöglichen?
<h2>Ideen</h2>
**Feedback-Formular am Seitenrand**
- Jeder Nutzer kann den Entwicklern eine Info senden. Besonders am Anfang können Fehler auftreten. Beim Senden werden alle Entwickler per E-Mail und/oder Push-Notifikation informiert.
- Ggf. Hilfe und Feedback mit Fragezeichen-Button innerhalb des Profilmenüs
- Menü: Zurück-Button rechts oder links anbieten im Menütitel?
** Suchfunktion **
- Suche oder Filtermöglichkeit in den Tabellen von Besuchsstellen und/oder Admin anbieten?
** Sidbar **
Wenn mehr Seiten hinzu kommen werden: Header Menü als SideMenü. Menü-Symbol (3 Linien), beim Klicken Menü nach rechts anzeigen, Animation (rotieren). Bei erneuten klicken Pfeil nach links. Hoch runter Buttons bei möglichen Unterseiten mit Animation (rotieren).
** Darstellung von Buttons für Aktionen **
Wenn Anzahl der Buttons/Links auf einer Seite mehr werden, dann zumindest bei Media Aktionen und Hyperlinks nur als Symbol anzeigen und bei Hover Text einblenden.
@@ -0,0 +1,157 @@
<?php /* Name: index.php
Beschreibung: Intranet: Hauptseite */
// Initialisierung
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
</head>
<body class="theme-darkblue">
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<div class="header_title ht_darkblue text_white" id="header_title">
<span class="header_title_text">Hauptmenü</span>
</div>
<?php if (isset($_GET["kalli_success"]) && $_GET["kalli_success"] == 1): ?>
<div style="position:fixed;top:calc(var(--header-height) + 8px);left:50%;transform:translateX(-50%);z-index:2000;padding:12px 28px;background:hsl(145,50%,93%);color:hsl(145,50%,28%);font-weight:600;font-size:var(--font-size-sm);box-shadow:var(--shadow-md);white-space:nowrap;" id="kalliSuccessMsg">
Eintragung erfolgreich! Kalli wird Ihren Termin direkt vermerken.
</div>
<script>setTimeout(function(){var el=document.getElementById('kalliSuccessMsg');if(el)el.style.display='none';},5000);</script>
<?php endif; ?>
<div class="mainpage" id="mainpage">
<div class="mainpage_container">
<?php
$mainpageNotices = [];
try {
$noticeStmt = $pdo->prepare(
"SELECT id, title, message, type
FROM mainpage_notices
WHERE is_active = TRUE
AND (starts_at IS NULL OR starts_at <= CURRENT_TIMESTAMP)
AND (ends_at IS NULL OR ends_at > CURRENT_TIMESTAMP)
ORDER BY sort_order ASC, created_at ASC, id ASC"
);
$noticeStmt->execute();
$mainpageNotices = $noticeStmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$mainpageNotices = [];
}
$allowedNoticeTypes = ['info', 'warning', 'success', 'error'];
foreach ($mainpageNotices as $mainpageNotice):
$noticeType = in_array($mainpageNotice['type'], $allowedNoticeTypes, true) ? $mainpageNotice['type'] : 'info';
?>
<div class="mainpage_notice mainpage_notice--<?= htmlspecialchars($noticeType, ENT_QUOTES, 'UTF-8') ?>" role="status">
<div class="mainpage_notice_title"><?= htmlspecialchars($mainpageNotice['title'], ENT_QUOTES, 'UTF-8') ?></div>
<div class="mainpage_notice_text"><?= nl2br(htmlspecialchars($mainpageNotice['message'], ENT_QUOTES, 'UTF-8')) ?></div>
</div>
<?php endforeach; ?>
<?php if (isset($groups["Brandschutz"]) && $groups["Brandschutz"] == 1) {
// Prüfen ob Schulung überfällig ist
$bsOverdue = false;
$bsStmt = $pdo->prepare("SELECT last_training_date FROM brandschutz_training WHERE user_id = ?");
$bsStmt->execute([$_SESSION['user_id']]);
$bsLastTraining = $bsStmt->fetchColumn();
if ($bsLastTraining === false) {
$bsOverdue = true;
} else {
if (empty($bsLastTraining) || strtotime($bsLastTraining . ' +365 days') < time()) {
$bsOverdue = true;
}
}
?>
<?php if ($bsOverdue): ?>
<div class="mainpage_notice mainpage_notice--warning" role="status">
<div class="mainpage_notice_title">Brandschutzunterweisung fällig!</div>
<div class="mainpage_notice_text">Bitte führen Sie zeitnah die <strong><a href="window.location.href='/intranet_repo/projects/brandschutz/brandschutz.php'">Online-Brandschutzunterweisung</a></strong> durch. Die Schulung dauert nur ca. 15 bis 20 Minuten. Eine regelmäßige Durchführung ist gesetzlich vorgeschrieben. Vielen Dank!</div>
</div>
<?php endif; ?>
<?php
$kalliRoles = ["Amtsgeschwister", "Allgemein", "Musik", "Kinderunterrichte", "Jugend", "Technik", "Brandschutz", "Abwesenheiten"];
$hasKalliRole = false;
foreach ($kalliRoles as $role) {
if (isset($groups[$role]) && $groups[$role] == 1) { $hasKalliRole = true; break; }
}
if ($hasKalliRole): ?>
<button class="button_mainpage unselectable bg_blue" onclick="window.location.href='/intranet_repo/projects/kalli/kalli.php'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg>
<div class="text">Kalli</div>
</button>
<?php endif; ?>
<button class="button_mainpage unselectable bg_darkblue" onclick="window.open('https://nak-reinickendorf.de/pdf/terminuebersicht.pdf', '_blank');">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm0-12H5V6h14v2z"/></svg>
<div class="text">Monatsplan</div>
</button>
<?php if ($groups["Amtsgeschwister"] == 1) { ?>
<button class="button_mainpage unselectable bg_purple" onclick="window.location.href='/intranet_repo/projects/seelsorge/seelsorge.php'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
<div class="text">Seelsorge</div>
</button>
<?php } ?>
<button class="button_mainpage unselectable bg_orange" onclick="window.location.href='/intranet_repo/projects/brandschutz/brandschutz.php';" style="position:relative;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M12 12.9l-2.13 2.09C9.31 15.55 9 16.28 9 17.06 9 18.68 10.35 20 12 20s3-1.32 3-2.94c0-.78-.31-1.52-.87-2.07L12 12.9z M16 6l-.44.55C14.38 8.02 12 7.19 12 5.3V2S4 6 4 13c0 4.42 3.58 8 8 8s8-3.58 8-8c0-2.96-1.61-5.62-4-7z"/></svg>
<div class="text">Brandschutz</div>
<?php if ($bsOverdue): ?>
<span style="position:absolute;top:6px;right:6px;min-width:26px;height:26px;padding:0 6px;border-radius:9999px;background:var(--nak-red);color:#FFF;font-size:0.85em;font-weight:700;display:flex;align-items:center;justify-content:center;">!</span>
<?php endif; ?>
</button>
<?php } ?>
<button class="button_mainpage unselectable bg_teal" onclick="window.location.href='/intranet_repo/projects/intranet/profile.php'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
<div class="text">Konto</div>
</button>
<?php if (isset($groups["Allgemein"]) && $groups["Allgemein"] == 1) { ?>
<button class="button_mainpage unselectable bg_raspberry" onclick="window.location.href='/intranet_repo/projects/contentpanel/insertion/insert.php'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
<div class="text">Schwarzes Brett</div>
</button>
<?php } ?>
<?php if ($groups["Admin"] == 1) {
// Unbearbeitete Nachrichten zählen
$badgeStmt = $pdo->prepare("SELECT COUNT(*) FROM admin_messages WHERE status = 'pending'");
$badgeStmt->execute();
$unreadCount = $badgeStmt->fetchColumn();
?>
<button class="button_mainpage unselectable bg_black" id="spin" onclick="window.location.href='/intranet_repo/projects/adminpanel/admin.php'" style="position:relative;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
<div class="text">Admin</div>
<?php if ($unreadCount > 0): ?>
<span style="position:absolute;top:6px;right:6px;min-width:26px;height:26px;padding:0 6px;border-radius:9999px;background:var(--nak-red);color:#FFF;font-size:0.85em;font-weight:700;display:flex;align-items:center;justify-content:center;"><?= $unreadCount ?></span>
<?php endif; ?>
</button>
<?php } ?>
<?php if ($groups["Entwickler"] == 1) { ?>
<button class="button_mainpage unselectable bg_green" id="spin" onclick="window.location.href='/intranet_repo/projects/developerpanel/developer.php'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
<div class="text">Entwickler</div>
</button>
<?php } ?>
</div>
</div>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,261 @@
<?php /* Name: profile.php
Beschreibung: Profil des angemeldeten Nutzers */
// Initialisierung
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
</head>
<body class="theme-teal">
<?php // Durchführung der Aktionen
if (isset($_SESSION["user_id"])) {
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Passwort-Reset-Aktion
if (isset($_POST["action"]) && $_POST["action"] === "change_password") {
$current_password = $_POST["current_password"] ?? '';
$new_password = $_POST["new_password"] ?? '';
$new_password_confirm = $_POST["new_password_confirm"] ?? '';
// Aktuelles Passwort prüfen
$stmt = $pdo->prepare("SELECT password_hash FROM user_login WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$pw_result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$pw_result || !password_verify($current_password, $pw_result['password_hash'])) {
$pw_error = "Das aktuelle Passwort ist falsch.";
} elseif (strlen($new_password) < 8 || !preg_match("/[a-z]/i", $new_password) || !preg_match("/[0-9]/", $new_password)) {
$pw_error = "Das neue Passwort muss mindestens 8 Zeichen, einen Buchstaben und eine Zahl enthalten.";
} elseif ($new_password !== $new_password_confirm) {
$pw_error = "Die neuen Passwörter stimmen nicht überein.";
} else {
$password_hash = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE user_login SET password_hash = ? WHERE user_id = ?");
$stmt->execute([$password_hash, $_SESSION["user_id"]]);
$pw_success = "Passwort erfolgreich geändert.";
}
} else {
// Profildaten-Update
$mail = $_POST["mail"];
$phone = !empty($_POST["phone"]) ? $_POST["phone"] : null; // Wert auf NULL setzen, wenn das Feld leer ist
// Profilbild hochladen, wenn ein Bild ausgewählt wurde
if ($_FILES["profile_picture"]["error"] === UPLOAD_ERR_OK) {
$upload_dir = $_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/profile_pictures/";
// Lösche vorhandene Dateien mit übereinstimmender Benutzer-ID
$files = glob($upload_dir . $_SESSION["user_id"] . "-*.jpg");
foreach ($files as $file) {
unlink($file);
}
// Generiere einen eindeutigen Dateinamen basierend auf Benutzer-ID und Datum/Uhrzeit
$filename = $_SESSION["user_id"] . "-" . date("YmdHis") . ".jpg";
$upload_file = $upload_dir . $filename;
if (move_uploaded_file($_FILES["profile_picture"]["tmp_name"], $upload_file)) {
$message = "Profilbild erfolgreich hochgeladen.";
} else {
$message = "Beim Hochladen des Profilbildes ist ein Fehler aufgetreten.";
}
}
// Benutzerdaten aktualisieren
$sql_update = "UPDATE user_general_information
SET mail=?, phone=?
WHERE user_id=?";
$stmt = $pdo->prepare($sql_update);
$stmt->execute([$mail, $phone, $_SESSION["user_id"]]);
// Weiterleitung ohne Nachricht
header("Location: {$_SERVER['PHP_SELF']}");
exit();
} // Ende else (Profildaten-Update)
}
$sql = "SELECT * FROM user_general_information WHERE user_id = {$_SESSION["user_id"]}";
$result = $pdo->query($sql);
$user = $result->fetch(PDO::FETCH_ASSOC);
$sql = "SELECT * FROM user_permissions WHERE user_id = {$_SESSION["user_id"]}";
$result = $pdo->query($sql);
$groups = $result->fetch(PDO::FETCH_ASSOC);
} else {
header("Location: {$g_httpLoginPath}");
exit;
}
// ACHTUNG: Die PHP-Header Weiterleitungen müssen hier abgeschlossen sein.
?>
<?php // Es folgen ab hier die HTML-Ausgaben beginnend mit dem HTML-Header: ?>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php // Laden der Standard-Overlays: ?>
<?php // Header (erweitert): Titel der Seite mit Funktionsaufruf ?>
<div class="header_title ht_teal" id="header_title">
<span class="header_title_text">Kontoverwaltung</span>
</div>
<?php // Die oberen Standard-Overlays müssen vor dem Content-Bereich platziert sein. ?>
<div class="content" id="base">
<div class="content-tab-bar" id="profile-tabs">
<div class="content-tab-bar__item active" data-target="profile-tab-info">Informationen</div>
<div class="content-tab-bar__item" data-target="profile-tab-security">Sicherheit</div>
</div>
<div class="profile-tab-content active" id="profile-tab-info">
<div class="profile">
<div class="content_panel">
<?php
// Generiere den Dateinamen basierend auf Benutzer-ID und Datum/Uhrzeit
$profileImg = glob($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/profile_pictures/{$_SESSION["user_id"]}-*.jpg");
// Überprüfe, ob eine passende Datei existiert
if (!empty($profileImg)) {
// Sortiere die Dateien nach Datum/Uhrzeit und nimm die neueste
usort($profileImg, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$latestProfileImg = $profileImg[0];
// Entferne den absoluten Pfad
$relativePath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $latestProfileImg);
// Bild anzeigen
echo "<img class='unselectable' src='$relativePath' alt='Profil'>";
} else {
// Standardbild anzeigen, wenn keine passende Datei gefunden wurde
echo "<img class='unselectable' src='/intranet_repo/general_files/profile_pictures/default.jpg' alt='Profil'>";
}
?>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" enctype="multipart/form-data">
<h2><?php echo $user["first_name"]; ?> <?php echo $user["last_name"]; ?></h2>
<div class="eventbox_mini">
<div class="eventbox_item">
<label class="text_black left" for="mail"><strong>E-Mailadresse</strong></label>
<input class="field_fullwidth left" type="email" id="mail" name="mail" value="<?php echo $user["mail"]; ?>" required>
<label class="text_black left" for="phone"><strong>Telefon</strong></label>
<div class="flexrow_left fullwidth">
<div class="input-prefix">+49</div>
<?php if ($user["phone"] === null): ?>
<input class="phone-input left" type="tel" id="phone" name="phone">
<?php else: ?>
<input class="phone-input left" type="tel" id="phone" name="phone"
value="<?php echo $user["phone"]; ?>">
<?php endif; ?>
</div>
</div>
<div class="eventbox_item">
<div class="flexcolumn_left">
<label><strong>Profilbild ändern</strong></label>
<input style="padding:5px 0;" type="file" id="profile_picture" name="profile_picture" accept="image/jpeg, image/png">
</div>
</div>
<input class="button_fullwidth" type="submit" value="Profil aktualisieren">
</div>
</form>
<p><strong>Gruppen:</strong><br>
<?php
$responsibilities = [];
foreach ($groups as $key => $value) {
if ($value == 1) {
$responsibilities[] = $key;
}
}
echo implode(", ", $responsibilities);
?>
</p>
</div><!-- EOD: content_panel -->
</div><!-- EOD: profile -->
</div><!-- EOD: profile-tab-info -->
<div class="profile-tab-content" id="profile-tab-security">
<div class="profile">
<div class="content_panel">
<h2 style="margin-bottom: var(--space-lg);">Passwort ändern</h2>
<?php if (!empty($pw_error)): ?>
<div style="padding: 10px 14px; margin-bottom: var(--space-md); background: hsl(0,78%,95%); color: hsl(0,78%,40%); font-weight: 500; font-size: var(--font-size-sm);">
<?= htmlspecialchars($pw_error) ?>
</div>
<?php endif; ?>
<?php if (!empty($pw_success)): ?>
<div style="padding: 10px 14px; margin-bottom: var(--space-md); background: hsl(145,50%,93%); color: hsl(145,50%,28%); font-weight: 500; font-size: var(--font-size-sm);">
<?= htmlspecialchars($pw_success) ?>
</div>
<?php endif; ?>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<input type="hidden" name="action" value="change_password">
<div class="eventbox_mini">
<div class="eventbox_item">
<label class="text_black left" for="current_password"><strong>Aktuelles Passwort</strong></label>
<input class="field_fullwidth left" type="password" id="current_password" name="current_password" required>
<label class="text_black left" for="new_password"><strong>Neues Passwort</strong></label>
<input class="field_fullwidth left" type="password" id="new_password" name="new_password" required>
<label class="text_black left" for="new_password_confirm"><strong>Neues Passwort wiederholen</strong></label>
<input class="field_fullwidth left" type="password" id="new_password_confirm" name="new_password_confirm" required>
</div>
<input class="button_fullwidth" type="submit" value="Passwort ändern">
</div>
</form>
<p style="font-size: var(--font-size-sm); color: var(--gray-500); margin-top: var(--space-sm);">
Passwort vergessen? <a href="/intranet_repo/projects/authorization/reset-password.php">Per SMS/Mail zurücksetzen</a>
</p>
</div><!-- EOD: content_panel -->
</div><!-- EOD: profile -->
</div><!-- EOD: profile-tab-security -->
</div><!-- EOD: content base -->
<script>
document.querySelectorAll('#profile-tabs .content-tab-bar__item').forEach(function(tab) {
tab.addEventListener('click', function() {
document.querySelectorAll('#profile-tabs .content-tab-bar__item').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.profile-tab-content').forEach(function(c) { c.classList.remove('active'); });
tab.classList.add('active');
var target = document.getElementById(tab.getAttribute('data-target'));
if (target) target.classList.add('active');
});
});
<?php if (!empty($pw_error) || !empty($pw_success)): ?>
// Auto-switch to security tab if password action result
document.querySelector('[data-target="profile-tab-security"]').click();
<?php endif; ?>
</script>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,88 @@
<?php /* Name: show_profile.php
Beschreibung: Profil eines ausgewählten Nutzers (Abschnitt zur Einbindung) */
// Initialisierung
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
?>
<?php
if (isset($_SESSION["profile_id"])) {
$pdo = require($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
$sql = "SELECT * FROM user_general_information
WHERE user_id = {$_SESSION["profile_id"]}";
$result = $pdo->query($sql);
$partner = $result->fetch(PDO::FETCH_ASSOC);
if (isset($partner["mail"])) {
$sql_groups = "SELECT * FROM user_permissions WHERE user_id = ?";
$stmt = $pdo->prepare($sql_groups);
$stmt->execute([$_SESSION["profile_id"]]);
$groups = $stmt->fetch(PDO::FETCH_ASSOC);
}
}
else {
header("Location:{$g_httpLoginPath}");
exit();
}
?>
<div class="content" id="base">
<div class="profile">
<div class="content_panel">
<?php
// Generiere den Dateinamen basierend auf Benutzer-ID und Datum/Uhrzeit
$profileImg = glob($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/profile_pictures/{$_SESSION["profile_id"]}-*.jpg");
// Überprüfe, ob eine passende Datei existiert
if (!empty($profileImg)) {
// Sortiere die Dateien nach Datum/Uhrzeit und nimm die neueste
usort($profileImg, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$latestProfileImg = $profileImg[0];
// Entferne den absoluten Pfad
$relativePath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $latestProfileImg);
// Bild anzeigen
echo "<img src='$relativePath' alt='Profil'>";
} else {
// Standardbild anzeigen, wenn keine passende Datei gefunden wurde
echo "<img src='/intranet_repo/general_files/profile_pictures/default.jpg' alt='Profil'>";
}
?>
<?php if ($partner["first_name"] === null): ?>
<h3 style="color:var(--nak-red);">Dieser Nutzer ist noch nicht registriert.</h3>
<?php else: ?>
<h2><?php echo $partner["first_name"]; ?> <?php echo $partner["last_name"]; ?></h2>
<?php endif; ?>
<p><strong>E-Mail:</strong><br><a href="mailto:<?php echo $partner["mail"]; ?>"><?php echo $partner["mail"]; ?></a></p>
<?php if ($partner["phone"] != null): ?>
<p><strong>Telefon:</strong><br>
<a href="tel:0<?php echo $partner["phone"]; ?>">0<?php echo $partner["phone"]; ?></a></p>
<?php endif; ?>
<p><strong>Gruppen:</strong><br>
<?php
$responsibilities = [];
foreach ($groups as $key => $value) {
if ($value == 1) {
$responsibilities[] = $key;
}
}
echo implode(", ", $responsibilities);
?>
</p>
</div><!-- EOD: content_panel -->
</div><!-- EOD: profile -->
</div><!-- EOD: content base -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
+780
View File
@@ -0,0 +1,780 @@
<?php /* Name: kalli.php
Beschreibung: Kalli: Hauptseite */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
</head>
<body class="theme-blue">
<?php // Stelle sicher, dass die Autoload-Datei von PHPSpreadsheet geladen wird
require 'vendor/autoload.php'; ?>
<?php // Durchführung der Aktionen
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
if(isset($_POST['submit'])) {
// if no errors, carry on
if(!isset($error)) {
$name = htmlspecialchars($user["first_name"]) . " " . htmlspecialchars($user["last_name"]);
$info = "Von " . $name . " eingetragen (Eintragungsassistent)";
// Create a new PHPExcel object
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Set the header row
$sheet->setCellValue('A1', 'ID');
$sheet->setCellValue('B1', 'Lang-ID');
$sheet->setCellValue('C1', 'Termin-Typ');
$sheet->setCellValue('D1', 'Von-Datum');
$sheet->setCellValue('E1', 'Von-Zeit');
$sheet->setCellValue('F1', 'Bis-Datum');
$sheet->setCellValue('G1', 'Bis-Zeit');
$sheet->setCellValue('H1', 'Zeitzusatz');
$sheet->setCellValue('I1', 'Titel');
$sheet->setCellValue('J1', 'Text');
$sheet->setCellValue('K1', 'Ort');
$sheet->setCellValue('L1', 'Planungsstatus');
$sheet->setCellValue('M1', 'Anzeigeziel');
$sheet->setCellValue('N1', 'Gruppenzuordnung');
$sheet->setCellValue('O1', 'Zyklusart');
$sheet->setCellValue('P1', 'Zyklusintervall');
$sheet->setCellValue('Q1', 'Zyklusdetail');
$sheet->setCellValue('R1', 'Notiz');
$sheet->setCellValue('S1', 'Verantwortlich');
$sheet->setCellValue('T1', 'Hintergrundfarbe');
$sheet->setCellValue('U1', 'BucherID');
$sheet->setCellValue('V1', 'Benutzerdefinierte Felder');
$sheet->setCellValue('W1', 'Dokument-Titel');
$sheet->setCellValue('X1', 'Anzeigeziel Ort');
$sheet->setCellValue('Y1', 'Anzeigeziel Notiz');
$sheet->setCellValue('Z1', 'Anzeigeziel Verantwortlicher');
$sheet->setCellValue('AA1', 'Erzeugt');
$sheet->setCellValue('AB1', 'Aktualisiert');
// Set the data row
// Titel
$selectedGroup = isset($_POST['group']) ? $_POST['group'] : '';
$sheet->setCellValue('I2', $selectedGroup);
// Datum
$date_s = isset($_POST['date_s']) ? $_POST['date_s'] : '';
$sheet->setCellValue('D2', date('d.m.Y', strtotime($date_s)));
$date_e = isset($_POST['date_e']) ? $_POST['date_e'] : '';
if (date('d.m.Y', strtotime($date_e)) != "01.01.1970"){
$sheet->setCellValue('F2', date('d.m.Y', strtotime($date_e)));
}
// Zeit
$time_s = isset($_POST['time_s']) ? $_POST['time_s'] : '';
if ($time_s !== '' && preg_match('/^\d{2}:\d{2}$/', $time_s)) {
$sheet->setCellValue('E2', $time_s);
} elseif ($time_s !== '') {
$error[] = "Ungültiges Zeitformat. Verwenden Sie das Format hh:mm.";
}
$time_e = isset($_POST['time_e']) ? $_POST['time_e'] : '';
if ($time_e !== '' && preg_match('/^\d{2}:\d{2}$/', $time_e)) {
$sheet->setCellValue('G2', $time_e);
} elseif ($time_e !== '') {
$error[] = "Ungültiges Zeitformat. Verwenden Sie das Format hh:mm.";
}
// FIX
$sheet->setCellValue('C2', 'Einzel');
$sheet->setCellValue('O2', 'einmalig');
// Setze den Ort in Zelle K2
$location = isset($_POST['location']) ? htmlspecialchars($_POST['location']) : '';
$sheet->setCellValue('K2', $location);
// Setze den Text in Zelle J2
$further_text = isset($_POST['further_text']) ? htmlspecialchars($_POST['further_text']) : '';
$sheet->setCellValue('J2', $further_text);
$sheet->setCellValue('R2', $info);
$responsibility = isset($_POST['responsibility']) ? htmlspecialchars($_POST['responsibility']) : '';
$sheet->setCellValue('S2', $responsibility);
$userInput = isset($_POST['userinput']) ? $_POST['userinput'] : '';
$userInput2 = isset($_POST['userinput2']) ? $_POST['userinput2'] : '';
// DB-Lookup: Preset-Werte für die gewählte Terminart laden
$presetStmt = $pdo->prepare("SELECT planungsstatus, anzeigeziel, gruppenzuordnung, anzeigeziel_ort, anzeigeziel_notiz, anzeigeziel_verantwortlicher, default_text FROM kalli_appointment_types WHERE name = ? LIMIT 1");
$presetStmt->execute([$selectedGroup]);
$preset = $presetStmt->fetch(PDO::FETCH_ASSOC);
if ($preset) {
$sheet->setCellValue('L2', $preset['planungsstatus']);
$sheet->setCellValue('M2', $preset['anzeigeziel']);
$sheet->setCellValue('N2', $preset['gruppenzuordnung']);
$sheet->setCellValue('X2', $preset['anzeigeziel_ort']);
$sheet->setCellValue('Y2', $preset['anzeigeziel_notiz']);
$sheet->setCellValue('Z2', $preset['anzeigeziel_verantwortlicher']);
if (!empty($preset['default_text']) && empty($further_text)) {
$sheet->setCellValue('J2', $preset['default_text']);
}
} elseif ($userInput != "" && $selectedGroup == "oeffentlich") {
$sheet->setCellValue('I2', $userInput);
$sheet->setCellValue('L2', 'fix');
$sheet->setCellValue('M2', 'öffentlich');
$sheet->setCellValue('N2', 'AN ALLE');
$sheet->setCellValue('X2', 'öffentlich');
$sheet->setCellValue('Y2', 'intern');
$sheet->setCellValue('Z2', 'intern');
} elseif ($userInput != "" && $selectedGroup == "intern") {
$sheet->setCellValue('I2', $userInput);
$sheet->setCellValue('L2', 'fix');
$sheet->setCellValue('M2', 'intern');
$sheet->setCellValue('N2', 'AN ALLE');
$sheet->setCellValue('X2', 'intern');
$sheet->setCellValue('Y2', 'intern');
$sheet->setCellValue('Z2', 'intern');
}
// Save the file on the server
$xlsxDir = rtrim(intranet_env('CKAL_XLSX_DIR', '/data/ckal-xlsx'), '/');
if (!is_dir($xlsxDir) && !mkdir($xlsxDir, 0770, true) && !is_dir($xlsxDir)) {
throw new RuntimeException("CKalender-XLSX-Verzeichnis kann nicht angelegt werden: {$xlsxDir}");
}
$FileName = $xlsxDir . "/" . $name . "-" . date("Ymd-His") . ".xlsx";
$writer = new Xlsx($spreadsheet);
$writer->save($FileName);
// Redirect to prevent form resubmission on page reload
print "<script language='javascript'>window.location.href='/intranet_repo/projects/intranet/index.php?kalli_success=1';</script>";
//header("Location: kalli.php");
//exit();
}
}
// ACHTUNG: Die PHP-Header Weiterleitungen müssen hier abgeschlossen sein.
?>
<?php// Es folgen ab hier die HTML-Ausgaben beginnend mit dem HTML-Header:?>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<div class="header_title ht_kalli text_white" id="header_title">
<span class="header_title_text">Kalli: Eintragungsassistent</span>
</div>
<div class="content" id="base">
<?php if (isset($_SESSION["user_id"])): ?>
<?php
if (isset($_GET["error"])) {
$error = $_GET["error"];
$isSuccess = in_array($error, [3]);
$toastType = $isSuccess ? 'success' : 'error';
$toastIcon = $isSuccess
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg>'
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
$toastText = ($error == 3) ? 'Eintragung erfolgreich! Kalli wird Ihren Termin direkt vermerken.' : 'Es gab einen Fehler. Bitte kontaktieren Sie uns!';
echo "<div class='nak-toast nak-toast--{$toastType}' id='nakToast'>";
echo "<div class='nak-toast__icon'>{$toastIcon}</div>";
echo "<span class='nak-toast__text'>" . htmlspecialchars($toastText) . "</span>";
echo "<button class='nak-toast__close' onclick='dismissNakToast()' aria-label='Schließen'><svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='square' stroke-linejoin='miter'><line x1='18' y1='6' x2='6' y2='18'/><line x1='6' y1='6' x2='18' y2='18'/></svg></button>";
echo "</div>";
echo "<script>
requestAnimationFrame(function(){document.getElementById('nakToast').classList.add('nak-toast--visible');});
setTimeout(function(){dismissNakToast();},4000);
function dismissNakToast(){var t=document.getElementById('nakToast');if(!t||t.classList.contains('nak-toast--hiding'))return;t.classList.add('nak-toast--hiding');t.classList.remove('nak-toast--visible');setTimeout(function(){t.remove();},350);}
</script>";
}
?>
<style>
/* ── Kalli Page Wizard ── */
.kalli-wizard {
max-width: 1400px;
width: 90%;
margin: var(--space-xl) auto;
background: #ffffff;
box-shadow: var(--shadow-md);
overflow: hidden;
}
/* ── Progress Bar ── */
.kalli-progress {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 20px 28px 16px;
background: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
}
.kalli-progress-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.kalli-progress-circle {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--gray-500);
background: var(--gray-200);
border: 2px solid var(--gray-300);
transition: all 250ms ease;
}
.kalli-progress-step.active .kalli-progress-circle {
background: var(--nak-blue);
border-color: var(--nak-blue);
color: #ffffff;
box-shadow: 0 2px 8px rgba(39,134,207,0.35);
}
.kalli-progress-step.done .kalli-progress-circle {
background: #ffffff;
border-color: var(--nak-blue);
color: var(--nak-blue);
}
.kalli-progress-label {
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--gray-500);
white-space: nowrap;
transition: color 250ms ease;
}
.kalli-progress-step.active .kalli-progress-label,
.kalli-progress-step.done .kalli-progress-label {
color: var(--nak-blue);
}
.kalli-progress-line {
flex: 1;
height: 2px;
min-width: 20px;
max-width: 60px;
background: var(--gray-300);
margin: 0 6px;
margin-bottom: 20px;
transition: background 250ms ease;
}
.kalli-progress-line.done {
background: var(--nak-blue);
}
/* ── Body ── */
.kalli-modal-body {
padding: 22px 28px 16px;
}
.kalli-section-title {
margin: 0 0 16px 0;
font-size: var(--font-size-sm);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--gray-600);
}
/* ── Category Grid (Step 1) ── */
.kalli-category-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-lg);
}
.kalli-category {
display: flex;
flex-direction: column;
gap: 2px;
}
.kalli-category-label {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--gray-700);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
label.kalli-radio-item {
display: flex;
align-items: center;
gap:8px;
padding: 0px 5px;
cursor: pointer;
font-size: var(--font-size-sm);
font-weight: 300;
color: var(--gray-600);
border: 1px solid transparent;
transition: background 150ms ease, border-color 150ms ease;
text-transform: none;
letter-spacing: normal;
}
label.kalli-radio-item input[type="radio"],
label.kalli-radio-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--nak-blue);
margin: 0;
flex-shrink: 0;
align-self: center;
}
label.kalli-radio-item span {
hyphens: auto;
-webkit-hyphens: auto;
-ms-hyphens: auto;
word-break: break-word;
overflow-wrap: break-word;
line-height: 1.4;
}
label.kalli-radio-item:hover {
background: var(--gray-50);
border-color: var(--gray-200);
}
.kalli-custom-section {
margin-top: var(--space-lg);
padding: var(--space-md);
background: hsl(0,60%,98%);
border: 1px solid hsl(0,60%,90%);
}
/* ── Field Grid (Steps 2 & 3) ── */
.kalli-field-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-md);
}
.kalli-field-grid-single {
grid-template-columns: 1fr;
}
.kalli-field-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.kalli-label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--gray-700);
}
.kalli-field {
padding: 12px 14px;
font-size: var(--font-size-base);
font-family: var(--font-primary);
font-weight: 300;
border: 1px solid var(--gray-300);
background: var(--gray-50);
outline: none;
transition: border-color 150ms ease, box-shadow 150ms ease;
width: 100%;
box-sizing: border-box;
}
.kalli-field:focus {
border-color: var(--nak-blue);
box-shadow: 0 0 0 3px rgba(39,134,207,0.2);
background: #ffffff;
}
/* ── Summary (Step 4) ── */
.kalli-summary {
display: flex;
flex-direction: column;
gap: 0;
border: 1px solid var(--gray-200);
}
.kalli-summary-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--gray-100);
}
.kalli-summary-row:last-child { border-bottom: none; }
.kalli-summary-label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--gray-600);
}
.kalli-summary-value {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--gray-900);
text-align: right;
}
.kalli-validation-error {
margin-top: var(--space-md);
padding: 10px 14px;
background: hsl(0,78%,95%);
color: hsl(0,78%,40%);
font-weight: 600;
font-size: var(--font-size-sm);
}
/* ── Actions ── */
.kalli-modal-actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 16px 28px 20px;
border-top: 1px solid var(--gray-200);
}
.kalli-modal-actions-left,
.kalli-modal-actions-right {
display: flex;
gap: 8px;
}
.kalli-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 8px 18px;
font-size: var(--font-size-sm);
font-weight: 600;
font-family: var(--font-primary);
border: 1px solid var(--gray-300);
background: #ffffff;
color: var(--gray-800);
cursor: pointer;
transition: background 150ms ease, transform 150ms ease;
white-space: nowrap;
}
.kalli-btn:hover {
background: var(--gray-100);
transform: translateY(-1px);
}
.kalli-btn-primary {
background: var(--nak-blue);
border-color: var(--nak-blue);
color: #ffffff;
}
.kalli-btn-primary:hover {
background: var(--nak-blue-hover);
border-color: var(--nak-blue-hover);
}
.kalli-btn-submit {
background: linear-gradient(135deg, var(--nak-blue), var(--nak-blue-hover));
border-color: var(--nak-blue-hover);
color: #ffffff;
padding: 10px 24px;
font-weight: 700;
}
.kalli-btn-submit:hover {
background: linear-gradient(135deg, var(--nak-blue-hover), #185d8f);
}
/* ── Responsive ── */
@media (max-width: 600px) {
.kalli-wizard { margin: 0; width: 100%; }
.kalli-progress { padding: 14px 12px 10px; }
.kalli-progress-label { font-size: 0.65rem; }
.kalli-progress-circle { width: 26px; height: 26px; font-size: 0.7rem; }
.kalli-progress-line { min-width: 12px; margin-bottom: 16px; }
.kalli-modal-body { padding: 16px; }
.kalli-category-grid { grid-template-columns: 1fr; }
.kalli-field-grid { grid-template-columns: 1fr; }
.kalli-modal-actions { padding: 12px 16px 16px; flex-wrap: wrap; }
}
</style>
<div class="kalli-wizard">
<!-- Progress Bar -->
<div class="kalli-progress">
<div class="kalli-progress-step active" data-step="1">
<div class="kalli-progress-circle">1</div>
<span class="kalli-progress-label">Terminart</span>
</div>
<div class="kalli-progress-line"></div>
<div class="kalli-progress-step" data-step="2">
<div class="kalli-progress-circle">2</div>
<span class="kalli-progress-label">Zeitraum</span>
</div>
<div class="kalli-progress-line"></div>
<div class="kalli-progress-step" data-step="3">
<div class="kalli-progress-circle">3</div>
<span class="kalli-progress-label">Informationen</span>
</div>
<div class="kalli-progress-line"></div>
<div class="kalli-progress-step" data-step="4">
<div class="kalli-progress-circle">4</div>
<span class="kalli-progress-label">Übersicht</span>
</div>
</div>
<!-- Form wraps all steps -->
<form id="kalliForm" action="" method="post">
<!-- STEP 1: Terminart -->
<div class="kalli-step" id="kalliStep1">
<div class="kalli-modal-body">
<div class="kalli-section-title">Terminart auswählen</div>
<div class="kalli-category-grid">
<?php
// Kategorien und Terminarten aus der Datenbank laden
$catQuery = $pdo->query("SELECT * FROM kalli_categories ORDER BY sort_order, name");
$kalliCategories = [];
while ($cat = $catQuery->fetch(PDO::FETCH_ASSOC)) {
$kalliCategories[] = $cat;
}
$typeQuery = $pdo->query("SELECT * FROM kalli_appointment_types ORDER BY sort_order, name");
$kalliTypesByCategory = [];
while ($type = $typeQuery->fetch(PDO::FETCH_ASSOC)) {
$kalliTypesByCategory[$type['category_id']][] = $type;
}
// Alle verwendeten Rollen sammeln für die "Terminart nicht aufgelistet"-Sektion
$kalliAllRoles = [];
$isAdminUser = (isset($groups["Admin"]) && $groups["Admin"] == 1);
foreach ($kalliCategories as $cat):
$role = $cat['permission_role'];
$kalliAllRoles[$role] = 1;
if (!$isAdminUser && !(isset($groups[$role]) && $groups[$role] == 1)) continue;
$types = $kalliTypesByCategory[$cat['id']] ?? [];
if (empty($types)) continue;
?>
<div class="kalli-category">
<div class="kalli-category-label"><?= htmlspecialchars($cat['name']) ?></div>
<?php foreach ($types as $type):
$displayLabel = !empty($type['display_label']) ? $type['display_label'] : $type['name'];
?>
<label class="kalli-radio-item"><input type="radio" name="group" value="<?= htmlspecialchars($type['name']) ?>"><span><?= htmlspecialchars($displayLabel) ?></span></label>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php
$hasAnyKalliRole = $isAdminUser || count(array_intersect_key($groups, $kalliAllRoles)) > 0;
if ($hasAnyKalliRole): ?>
<div class="kalli-custom-section">
<div class="kalli-category-label" style="color:var(--nak-red);">Terminart nicht aufgelistet?</div>
<p style="margin:0 0 var(--space-sm) 0;font-size:var(--font-size-sm);color:var(--gray-600);">Nur ausfüllen, wenn die Terminart oben nicht vorhanden ist.</p>
<div style="display:flex;gap:var(--space-lg);flex-wrap:wrap;margin-bottom:var(--space-sm);">
<label class="kalli-radio-item"><input type="radio" name="group" value="oeffentlich"><span>Öffentlich</span></label>
<label class="kalli-radio-item"><input type="radio" name="group" value="intern"><span>Intern</span></label>
</div>
<input class="kalli-field" type="text" name="userinput" id="kalliUserInput" placeholder="Titel (nur für benutzerdefinierte Termine)">
</div>
<?php endif; ?>
</div>
<div class="kalli-modal-actions">
<div></div>
<div class="kalli-modal-actions-right">
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(2)">Weiter</button>
</div>
</div>
</div>
<!-- STEP 2: Zeitraum -->
<div class="kalli-step" id="kalliStep2" style="display:none;">
<div class="kalli-modal-body">
<div class="kalli-section-title">Zeitraum festlegen</div>
<div class="kalli-field-grid">
<div class="kalli-field-group">
<label class="kalli-label">Datum <span style="color:var(--nak-red);">*</span></label>
<input class="kalli-field" type="date" name="date_s" id="kalliDateS" required>
</div>
<div class="kalli-field-group">
<label class="kalli-label">Enddatum <span style="font-weight:300;color:var(--gray-500);">(falls mehrtägig)</span></label>
<input class="kalli-field" type="date" name="date_e" id="kalliDateE">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Zeit von <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="time" name="time_s" id="kalliTimeS">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Zeit bis <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="time" name="time_e" id="kalliTimeE">
</div>
</div>
</div>
<div class="kalli-modal-actions">
<div class="kalli-modal-actions-left">
<button type="button" class="kalli-btn" onclick="kalliPrevStep(1)">Zurück</button>
</div>
<div class="kalli-modal-actions-right">
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(3)">Weiter</button>
</div>
</div>
</div>
<!-- STEP 3: Informationen -->
<div class="kalli-step" id="kalliStep3" style="display:none;">
<div class="kalli-modal-body">
<div class="kalli-section-title">Zusätzliche Informationen</div>
<div class="kalli-field-grid kalli-field-grid-single">
<div class="kalli-field-group">
<label class="kalli-label">Textfeld <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="text" name="further_text" id="kalliFurtherText" placeholder="z.B. betreffende Person, etc.">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Ort <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="text" name="location" id="kalliLocation">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Verantwortliche/r <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="text" name="responsibility" id="kalliResponsibility">
</div>
</div>
</div>
<div class="kalli-modal-actions">
<div class="kalli-modal-actions-left">
<button type="button" class="kalli-btn" onclick="kalliPrevStep(2)">Zurück</button>
</div>
<div class="kalli-modal-actions-right">
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(4)">Weiter</button>
</div>
</div>
</div>
<!-- STEP 4: Übersicht -->
<div class="kalli-step" id="kalliStep4" style="display:none;">
<div class="kalli-modal-body">
<div class="kalli-section-title">Übersicht – Bitte prüfen Sie Ihre Eingaben</div>
<div class="kalli-summary">
<div class="kalli-summary-row">
<span class="kalli-summary-label">Terminart</span>
<span class="kalli-summary-value" id="sumGroup">–</span>
</div>
<div class="kalli-summary-row">
<span class="kalli-summary-label">Datum</span>
<span class="kalli-summary-value" id="sumDateS">–</span>
</div>
<div class="kalli-summary-row" id="sumDateERow" style="display:none;">
<span class="kalli-summary-label">Enddatum</span>
<span class="kalli-summary-value" id="sumDateE">–</span>
</div>
<div class="kalli-summary-row" id="sumTimeSRow" style="display:none;">
<span class="kalli-summary-label">Zeit von</span>
<span class="kalli-summary-value" id="sumTimeS">–</span>
</div>
<div class="kalli-summary-row" id="sumTimeERow" style="display:none;">
<span class="kalli-summary-label">Zeit bis</span>
<span class="kalli-summary-value" id="sumTimeE">–</span>
</div>
<div class="kalli-summary-row" id="sumTextRow" style="display:none;">
<span class="kalli-summary-label">Textfeld</span>
<span class="kalli-summary-value" id="sumText">–</span>
</div>
<div class="kalli-summary-row" id="sumLocRow" style="display:none;">
<span class="kalli-summary-label">Ort</span>
<span class="kalli-summary-value" id="sumLoc">–</span>
</div>
<div class="kalli-summary-row" id="sumRespRow" style="display:none;">
<span class="kalli-summary-label">Verantwortlich</span>
<span class="kalli-summary-value" id="sumResp">–</span>
</div>
</div>
<div id="kalliValidationError" class="kalli-validation-error" style="display:none;"></div>
</div>
<div class="kalli-modal-actions">
<div class="kalli-modal-actions-left">
<button type="button" class="kalli-btn" onclick="kalliPrevStep(3)">Zurück</button>
</div>
<div class="kalli-modal-actions-right">
<button type="submit" name="submit" class="kalli-btn kalli-btn-submit">Eintragung absenden</button>
</div>
</div>
</div>
</form>
</div>
<script>
var kalliCurrentStep = 1;
function kalliGoToStep(step) {
for (var i = 1; i <= 4; i++) {
document.getElementById('kalliStep' + i).style.display = (i === step) ? '' : 'none';
}
kalliCurrentStep = step;
kalliUpdateProgress(step);
window.scrollTo({top: 0, behavior: 'smooth'});
// Header wieder einblenden, falls er durch Scrollen ausgeblendet war
var headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
var elemHeaderMenue = document.getElementById('header_menue');
var elemHeaderTitle = document.getElementById('header_title');
if (elemHeaderMenue) { elemHeaderMenue.style.top = '0'; }
if (elemHeaderTitle) { elemHeaderTitle.style.top = headerHeight + 'px'; }
}
function kalliUpdateProgress(step) {
var steps = document.querySelectorAll('.kalli-progress-step');
var lines = document.querySelectorAll('.kalli-progress-line');
steps.forEach(function(el, idx) {
var s = idx + 1;
el.classList.remove('active', 'done');
if (s === step) el.classList.add('active');
else if (s < step) el.classList.add('done');
});
lines.forEach(function(el, idx) {
el.classList.toggle('done', idx < step - 1);
});
}
function kalliNextStep(step) {
var errEl = document.getElementById('kalliValidationError');
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
if (step === 2) {
var sel = document.querySelector('#kalliForm input[name="group"]:checked');
if (!sel) { alert('Bitte wählen Sie eine Terminart aus.'); return; }
if ((sel.value === 'oeffentlich' || sel.value === 'intern') && !document.getElementById('kalliUserInput').value.trim()) {
alert('Bitte geben Sie einen Titel für den benutzerdefinierten Termin ein.'); return;
}
}
if (step === 3) {
if (!document.getElementById('kalliDateS').value) { alert('Bitte geben Sie ein Datum ein.'); return; }
}
if (step === 4) { kalliBuildSummary(); }
kalliGoToStep(step);
}
function kalliPrevStep(step) { kalliGoToStep(step); }
function kalliBuildSummary() {
var grp = document.querySelector('#kalliForm input[name="group"]:checked');
var grpVal = grp ? grp.value : '–';
if ((grpVal === 'oeffentlich' || grpVal === 'intern') && document.getElementById('kalliUserInput')) {
var custom = document.getElementById('kalliUserInput').value.trim();
grpVal = custom ? custom + ' (' + (grpVal === 'oeffentlich' ? 'Öffentlich' : 'Intern') + ')' : grpVal;
}
document.getElementById('sumGroup').textContent = grpVal;
document.getElementById('sumDateS').textContent = kalliFormatDate(document.getElementById('kalliDateS').value);
kalliShowSummaryRow('sumDateERow', document.getElementById('kalliDateE').value, 'sumDateE', kalliFormatDate(document.getElementById('kalliDateE').value));
kalliShowSummaryRow('sumTimeSRow', document.getElementById('kalliTimeS').value, 'sumTimeS', document.getElementById('kalliTimeS').value);
kalliShowSummaryRow('sumTimeERow', document.getElementById('kalliTimeE').value, 'sumTimeE', document.getElementById('kalliTimeE').value);
kalliShowSummaryRow('sumTextRow', document.getElementById('kalliFurtherText').value.trim(), 'sumText', document.getElementById('kalliFurtherText').value.trim());
kalliShowSummaryRow('sumLocRow', document.getElementById('kalliLocation').value.trim(), 'sumLoc', document.getElementById('kalliLocation').value.trim());
kalliShowSummaryRow('sumRespRow', document.getElementById('kalliResponsibility').value.trim(), 'sumResp', document.getElementById('kalliResponsibility').value.trim());
}
function kalliShowSummaryRow(rowId, value, valId, displayVal) {
var row = document.getElementById(rowId);
if (value) { row.style.display = ''; document.getElementById(valId).textContent = displayVal; }
else { row.style.display = 'none'; }
}
function kalliFormatDate(iso) {
if (!iso) return '–';
var parts = iso.split('-');
return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : iso;
}
</script>
<?php endif; ?>
</div><!-- content -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,746 @@
<!-- Kalli Eintragungsassistent – Mehrstufiges Modal -->
<div class="standard_overlay" id="kalliModal" onclick="if(event.target.id==='kalliModal')closeKalliModal();" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;justify-content:center;align-items:center;background-color:hsla(210,57%,25%,0.8);z-index:9999;">
<div class="kalli-modal-card">
<!-- Close Button -->
<button type="button" class="kalli-modal-exit" onclick="closeKalliModal()" aria-label="Schließen">&times;</button>
<!-- Hero Header -->
<div class="kalli-modal-hero">
<div>
<div class="kalli-modal-title">Kalli – Eintragungsassistent</div>
<div class="kalli-modal-subtitle">Neuen Termin im CKalender eintragen</div>
</div>
</div>
<!-- Progress Bar -->
<div class="kalli-progress">
<div class="kalli-progress-step active" data-step="1">
<div class="kalli-progress-circle">1</div>
<span class="kalli-progress-label">Terminart</span>
</div>
<div class="kalli-progress-line"></div>
<div class="kalli-progress-step" data-step="2">
<div class="kalli-progress-circle">2</div>
<span class="kalli-progress-label">Zeitraum</span>
</div>
<div class="kalli-progress-line"></div>
<div class="kalli-progress-step" data-step="3">
<div class="kalli-progress-circle">3</div>
<span class="kalli-progress-label">Informationen</span>
</div>
<div class="kalli-progress-line"></div>
<div class="kalli-progress-step" data-step="4">
<div class="kalli-progress-circle">4</div>
<span class="kalli-progress-label">Übersicht</span>
</div>
</div>
<!-- Form wraps all steps -->
<form id="kalliForm" action="/intranet_repo/projects/kalli/kalli.php" method="post">
<!-- STEP 1: Terminart -->
<div class="kalli-step" id="kalliStep1">
<div class="kalli-modal-body">
<div class="kalli-section-title">Terminart auswählen</div>
<div class="kalli-category-grid">
<?php
$isAdminUser = (isset($groups["Admin"]) && $groups["Admin"] == 1);
// Kategorien und Terminarten aus der Datenbank laden
$catQueryModal = $pdo->query("SELECT * FROM kalli_categories ORDER BY sort_order, name");
$kalliCategoriesModal = [];
while ($catM = $catQueryModal->fetch(PDO::FETCH_ASSOC)) {
$kalliCategoriesModal[] = $catM;
}
$typeQueryModal = $pdo->query("SELECT * FROM kalli_appointment_types ORDER BY sort_order, name");
$kalliTypesByCategoryModal = [];
while ($typeM = $typeQueryModal->fetch(PDO::FETCH_ASSOC)) {
$kalliTypesByCategoryModal[$typeM['category_id']][] = $typeM;
}
$kalliAllRolesModal = [];
foreach ($kalliCategoriesModal as $catM):
$roleM = $catM['permission_role'];
$kalliAllRolesModal[$roleM] = 1;
if (!$isAdminUser && !(isset($groups[$roleM]) && $groups[$roleM] == 1)) continue;
$typesM = $kalliTypesByCategoryModal[$catM['id']] ?? [];
if (empty($typesM)) continue;
?>
<div class="kalli-category">
<div class="kalli-category-label"><?= htmlspecialchars($catM['name']) ?></div>
<?php foreach ($typesM as $typeM):
$displayLabelM = !empty($typeM['display_label']) ? $typeM['display_label'] : $typeM['name'];
?>
<label class="kalli-radio-item"><input type="radio" name="group" value="<?= htmlspecialchars($typeM['name']) ?>"><span><?= htmlspecialchars($displayLabelM) ?></span></label>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php
$hasAnyKalliRoleModal = $isAdminUser || count(array_intersect_key($groups, $kalliAllRolesModal)) > 0;
if ($hasAnyKalliRoleModal): ?>
<div class="kalli-custom-section">
<div class="kalli-category-label" style="color:var(--nak-red);">Terminart nicht aufgelistet?</div>
<p style="margin:0 0 var(--space-sm) 0;font-size:var(--font-size-sm);color:var(--gray-600);">Nur ausfüllen, wenn die Terminart oben nicht vorhanden ist.</p>
<div style="display:flex;gap:var(--space-lg);flex-wrap:wrap;margin-bottom:var(--space-sm);">
<label class="kalli-radio-item"><input type="radio" name="group" value="oeffentlich"><span>Öffentlich</span></label>
<label class="kalli-radio-item"><input type="radio" name="group" value="intern"><span>Intern</span></label>
</div>
<input class="kalli-field" type="text" name="userinput" id="kalliUserInput" placeholder="Titel (nur für benutzerdefinierte Termine)">
</div>
<?php endif; ?>
</div>
<div class="kalli-modal-actions">
<div></div>
<div class="kalli-modal-actions-right">
<button type="button" class="kalli-btn" onclick="closeKalliModal()">Abbrechen</button>
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(2)">Weiter</button>
</div>
</div>
</div>
<!-- STEP 2: Zeitraum -->
<div class="kalli-step" id="kalliStep2" style="display:none;">
<div class="kalli-modal-body">
<div class="kalli-section-title">Zeitraum festlegen</div>
<div class="kalli-field-grid">
<div class="kalli-field-group">
<label class="kalli-label">Datum <span style="color:var(--nak-red);">*</span></label>
<input class="kalli-field" type="date" name="date_s" id="kalliDateS" required>
</div>
<div class="kalli-field-group">
<label class="kalli-label">Enddatum <span style="font-weight:300;color:var(--gray-500);">(falls mehrtägig)</span></label>
<input class="kalli-field" type="date" name="date_e" id="kalliDateE">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Zeit von <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="time" name="time_s" id="kalliTimeS">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Zeit bis <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="time" name="time_e" id="kalliTimeE">
</div>
</div>
</div>
<div class="kalli-modal-actions">
<div class="kalli-modal-actions-left">
<button type="button" class="kalli-btn" onclick="kalliPrevStep(1)">Zurück</button>
</div>
<div class="kalli-modal-actions-right">
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(3)">Weiter</button>
</div>
</div>
</div>
<!-- STEP 3: Informationen -->
<div class="kalli-step" id="kalliStep3" style="display:none;">
<div class="kalli-modal-body">
<div class="kalli-section-title">Zusätzliche Informationen</div>
<div class="kalli-field-grid kalli-field-grid-single">
<div class="kalli-field-group">
<label class="kalli-label">Textfeld <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="text" name="further_text" id="kalliFurtherText" placeholder="z.B. betreffende Person, etc.">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Ort <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="text" name="location" id="kalliLocation">
</div>
<div class="kalli-field-group">
<label class="kalli-label">Verantwortliche/r <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
<input class="kalli-field" type="text" name="responsibility" id="kalliResponsibility">
</div>
</div>
</div>
<div class="kalli-modal-actions">
<div class="kalli-modal-actions-left">
<button type="button" class="kalli-btn" onclick="kalliPrevStep(2)">Zurück</button>
</div>
<div class="kalli-modal-actions-right">
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(4)">Weiter</button>
</div>
</div>
</div>
<!-- STEP 4: Übersicht -->
<div class="kalli-step" id="kalliStep4" style="display:none;">
<div class="kalli-modal-body">
<div class="kalli-section-title">Übersicht – Bitte prüfen Sie Ihre Eingaben</div>
<div class="kalli-summary">
<div class="kalli-summary-row">
<span class="kalli-summary-label">Terminart</span>
<span class="kalli-summary-value" id="sumGroup">–</span>
</div>
<div class="kalli-summary-row">
<span class="kalli-summary-label">Datum</span>
<span class="kalli-summary-value" id="sumDateS">–</span>
</div>
<div class="kalli-summary-row" id="sumDateERow" style="display:none;">
<span class="kalli-summary-label">Enddatum</span>
<span class="kalli-summary-value" id="sumDateE">–</span>
</div>
<div class="kalli-summary-row" id="sumTimeSRow" style="display:none;">
<span class="kalli-summary-label">Zeit von</span>
<span class="kalli-summary-value" id="sumTimeS">–</span>
</div>
<div class="kalli-summary-row" id="sumTimeERow" style="display:none;">
<span class="kalli-summary-label">Zeit bis</span>
<span class="kalli-summary-value" id="sumTimeE">–</span>
</div>
<div class="kalli-summary-row" id="sumTextRow" style="display:none;">
<span class="kalli-summary-label">Textfeld</span>
<span class="kalli-summary-value" id="sumText">–</span>
</div>
<div class="kalli-summary-row" id="sumLocRow" style="display:none;">
<span class="kalli-summary-label">Ort</span>
<span class="kalli-summary-value" id="sumLoc">–</span>
</div>
<div class="kalli-summary-row" id="sumRespRow" style="display:none;">
<span class="kalli-summary-label">Verantwortlich</span>
<span class="kalli-summary-value" id="sumResp">–</span>
</div>
</div>
<div id="kalliValidationError" class="kalli-validation-error" style="display:none;"></div>
</div>
<div class="kalli-modal-actions">
<div class="kalli-modal-actions-left">
<button type="button" class="kalli-btn" onclick="kalliPrevStep(3)">Zurück</button>
</div>
<div class="kalli-modal-actions-right">
<button type="submit" name="submit" class="kalli-btn kalli-btn-submit">Eintragung absenden</button>
</div>
</div>
</div>
</form>
</div>
</div>
<style>
/* ── Kalli Modal ── */
.kalli-modal-card {
position: relative;
width: min(720px, calc(100vw - 24px));
max-height: min(90vh, calc(100vh - 24px));
overflow-y: auto;
background: #ffffff;
box-shadow: 0 24px 70px rgba(0,0,0,0.36);
}
.kalli-modal-exit {
position: absolute;
top: 14px;
right: 14px;
width: 34px;
height: 34px;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.16);
color: #ffffff;
font-size: 1.35em;
line-height: 1;
cursor: pointer;
z-index: 1;
}
.kalli-modal-hero {
padding: 24px 28px;
color: #ffffff;
background:
radial-gradient(circle at 10% 0%, rgba(255,255,255,0.24), transparent 32%),
linear-gradient(135deg, var(--nak-blue), var(--nak-blue-hover));
}
.kalli-modal-title {
font-size: 1.35em;
font-weight: 700;
margin-bottom: 4px;
}
.kalli-modal-subtitle {
font-size: var(--font-size-sm);
font-weight: 300;
color: rgba(255,255,255,0.8);
}
/* ── Progress Bar ── */
.kalli-progress {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 20px 28px 16px;
background: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
}
.kalli-progress-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.kalli-progress-circle {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--gray-500);
background: var(--gray-200);
border: 2px solid var(--gray-300);
transition: all 250ms ease;
}
.kalli-progress-step.active .kalli-progress-circle {
background: var(--nak-blue);
border-color: var(--nak-blue);
color: #ffffff;
box-shadow: 0 2px 8px rgba(39,134,207,0.35);
}
.kalli-progress-step.done .kalli-progress-circle {
background: #ffffff;
border-color: var(--nak-blue);
color: var(--nak-blue);
}
.kalli-progress-label {
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--gray-500);
white-space: nowrap;
transition: color 250ms ease;
}
.kalli-progress-step.active .kalli-progress-label,
.kalli-progress-step.done .kalli-progress-label {
color: var(--nak-blue);
}
.kalli-progress-line {
flex: 1;
height: 2px;
min-width: 20px;
max-width: 60px;
background: var(--gray-300);
margin: 0 6px;
margin-bottom: 20px;
transition: background 250ms ease;
}
.kalli-progress-line.done {
background: var(--nak-blue);
}
/* ── Modal Body ── */
.kalli-modal-body {
padding: 22px 28px 16px;
}
.kalli-section-title {
margin: 0 0 16px 0;
font-size: var(--font-size-sm);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--gray-600);
}
/* ── Category Grid (Step 1) ── */
.kalli-category-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-lg);
}
.kalli-category {
display: flex;
flex-direction: column;
gap: 2px;
}
.kalli-category-label {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--gray-700);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
label.kalli-radio-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
cursor: pointer;
font-size: var(--font-size-sm);
font-weight: 400;
color: var(--gray-800);
border: 1px solid transparent;
transition: background 150ms ease, border-color 150ms ease;
text-transform: none;
letter-spacing: normal;
}
label.kalli-radio-item input[type="radio"],
label.kalli-radio-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--nak-blue);
margin: 0;
flex-shrink: 0;
align-self: center;
}
label.kalli-radio-item span {
hyphens: auto;
-webkit-hyphens: auto;
-ms-hyphens: auto;
word-break: break-word;
overflow-wrap: break-word;
line-height: 1.4;
}
label.kalli-radio-item:hover {
background: var(--gray-50);
border-color: var(--gray-200);
}
.kalli-custom-section {
margin-top: var(--space-lg);
padding: var(--space-md);
background: hsl(0,60%,98%);
border: 1px solid hsl(0,60%,90%);
}
/* ── Field Grid (Steps 2 & 3) ── */
.kalli-field-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-md);
}
.kalli-field-grid-single {
grid-template-columns: 1fr;
}
.kalli-field-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.kalli-label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--gray-700);
}
.kalli-field {
padding: 12px 14px;
font-size: var(--font-size-base);
font-family: var(--font-primary);
font-weight: 300;
border: 1px solid var(--gray-300);
background: var(--gray-50);
outline: none;
transition: border-color 150ms ease, box-shadow 150ms ease;
width: 100%;
}
.kalli-field:focus {
border-color: var(--nak-blue);
box-shadow: 0 0 0 3px var(--nak-blue-20);
background: #ffffff;
}
/* ── Summary (Step 4) ── */
.kalli-summary {
display: flex;
flex-direction: column;
gap: 0;
border: 1px solid var(--gray-200);
}
.kalli-summary-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--gray-100);
}
.kalli-summary-row:last-child {
border-bottom: none;
}
.kalli-summary-label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--gray-600);
}
.kalli-summary-value {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--gray-900);
text-align: right;
}
.kalli-validation-error {
margin-top: var(--space-md);
padding: 10px 14px;
background: hsl(0,78%,95%);
color: hsl(0,78%,40%);
font-weight: 600;
font-size: var(--font-size-sm);
}
/* ── Actions ── */
.kalli-modal-actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 16px 28px 20px;
border-top: 1px solid var(--gray-200);
}
.kalli-modal-actions-left,
.kalli-modal-actions-right {
display: flex;
gap: 8px;
}
.kalli-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 8px 18px;
font-size: var(--font-size-sm);
font-weight: 600;
font-family: var(--font-primary);
border: 1px solid var(--gray-300);
background: #ffffff;
color: var(--gray-800);
cursor: pointer;
transition: background 150ms ease, transform 150ms ease;
white-space: nowrap;
}
.kalli-btn:hover {
background: var(--gray-100);
transform: translateY(-1px);
}
.kalli-btn-primary {
background: var(--nak-blue);
border-color: var(--nak-blue);
color: #ffffff;
}
.kalli-btn-primary:hover {
background: var(--nak-blue-hover);
border-color: var(--nak-blue-hover);
}
.kalli-btn-submit {
background: linear-gradient(135deg, var(--nak-blue), var(--nak-blue-hover));
border-color: var(--nak-blue-hover);
color: #ffffff;
padding: 10px 24px;
font-weight: 700;
}
.kalli-btn-submit:hover {
background: linear-gradient(135deg, var(--nak-blue-hover), #185d8f);
}
/* ── Responsive ── */
@media (max-width: 600px) {
.kalli-modal-card {
width: 100vw;
max-height: 100vh;
max-height: 100dvh;
}
.kalli-modal-hero {
padding: 18px 16px;
}
.kalli-modal-title {
font-size: 1.15em;
}
.kalli-progress {
padding: 14px 12px 10px;
gap: 0;
}
.kalli-progress-label {
font-size: 0.65rem;
}
.kalli-progress-circle {
width: 26px;
height: 26px;
font-size: 0.7rem;
}
.kalli-progress-line {
min-width: 12px;
margin-bottom: 16px;
}
.kalli-modal-body {
padding: 16px;
}
.kalli-category-grid {
grid-template-columns: 1fr;
}
.kalli-field-grid {
grid-template-columns: 1fr;
}
.kalli-modal-actions {
padding: 12px 16px 16px;
flex-wrap: wrap;
}
}
</style>
<script>
var kalliCurrentStep = 1;
function openKalliModal() {
document.getElementById('kalliModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
kalliGoToStep(1);
}
function closeKalliModal() {
document.getElementById('kalliModal').style.display = 'none';
document.body.style.overflow = '';
}
function kalliGoToStep(step) {
for (var i = 1; i <= 4; i++) {
document.getElementById('kalliStep' + i).style.display = (i === step) ? '' : 'none';
}
kalliCurrentStep = step;
kalliUpdateProgress(step);
// Scroll modal body to top
document.querySelector('.kalli-modal-card').scrollTop = 0;
}
function kalliUpdateProgress(step) {
var steps = document.querySelectorAll('.kalli-progress-step');
var lines = document.querySelectorAll('.kalli-progress-line');
steps.forEach(function(el, idx) {
var s = idx + 1;
el.classList.remove('active', 'done');
if (s === step) el.classList.add('active');
else if (s < step) el.classList.add('done');
});
lines.forEach(function(el, idx) {
el.classList.toggle('done', idx < step - 1);
});
}
function kalliNextStep(step) {
var errEl = document.getElementById('kalliValidationError');
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
if (step === 2) {
// Validate step 1: a group must be selected
var sel = document.querySelector('#kalliForm input[name="group"]:checked');
if (!sel) {
alert('Bitte wählen Sie eine Terminart aus.');
return;
}
// If custom group, require title
if ((sel.value === 'oeffentlich' || sel.value === 'intern') && !document.getElementById('kalliUserInput').value.trim()) {
alert('Bitte geben Sie einen Titel für den benutzerdefinierten Termin ein.');
return;
}
}
if (step === 3) {
// Validate step 2: date is required
if (!document.getElementById('kalliDateS').value) {
alert('Bitte geben Sie ein Datum ein.');
return;
}
}
if (step === 4) {
kalliBuildSummary();
}
kalliGoToStep(step);
}
function kalliPrevStep(step) {
kalliGoToStep(step);
}
function kalliBuildSummary() {
var grp = document.querySelector('#kalliForm input[name="group"]:checked');
var grpVal = grp ? grp.value : '–';
if ((grpVal === 'oeffentlich' || grpVal === 'intern') && document.getElementById('kalliUserInput')) {
var custom = document.getElementById('kalliUserInput').value.trim();
grpVal = custom ? custom + ' (' + (grpVal === 'oeffentlich' ? 'Öffentlich' : 'Intern') + ')' : grpVal;
}
document.getElementById('sumGroup').textContent = grpVal;
var dateS = document.getElementById('kalliDateS').value;
document.getElementById('sumDateS').textContent = dateS ? kalliFormatDate(dateS) : '–';
var dateE = document.getElementById('kalliDateE').value;
kalliShowSummaryRow('sumDateERow', dateE, 'sumDateE', kalliFormatDate(dateE));
var timeS = document.getElementById('kalliTimeS').value;
kalliShowSummaryRow('sumTimeSRow', timeS, 'sumTimeS', timeS);
var timeE = document.getElementById('kalliTimeE').value;
kalliShowSummaryRow('sumTimeERow', timeE, 'sumTimeE', timeE);
var text = document.getElementById('kalliFurtherText').value.trim();
kalliShowSummaryRow('sumTextRow', text, 'sumText', text);
var loc = document.getElementById('kalliLocation').value.trim();
kalliShowSummaryRow('sumLocRow', loc, 'sumLoc', loc);
var resp = document.getElementById('kalliResponsibility').value.trim();
kalliShowSummaryRow('sumRespRow', resp, 'sumResp', resp);
}
function kalliShowSummaryRow(rowId, value, valId, displayVal) {
var row = document.getElementById(rowId);
if (value) {
row.style.display = '';
document.getElementById(valId).textContent = displayVal;
} else {
row.style.display = 'none';
}
}
function kalliFormatDate(iso) {
if (!iso) return '–';
var parts = iso.split('-');
if (parts.length === 3) return parts[2] + '.' + parts[1] + '.' + parts[0];
return iso;
}
</script>
+25
View File
@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit2e312969af0145e24c15300d55587a02::getLoader();
@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}
@@ -0,0 +1,19 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);
@@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
);
@@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'),
);
@@ -0,0 +1,17 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
);
@@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit2e312969af0145e24c15300d55587a02
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit2e312969af0145e24c15300d55587a02', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit2e312969af0145e24c15300d55587a02', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit2e312969af0145e24c15300d55587a02::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit2e312969af0145e24c15300d55587a02::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
@@ -0,0 +1,96 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit2e312969af0145e24c15300d55587a02
{
public static $files = array (
'2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
);
public static $prefixLengthsPsr4 = array (
'Z' =>
array (
'ZipStream\\' => 10,
),
'P' =>
array (
'Psr\\SimpleCache\\' => 16,
'Psr\\Http\\Message\\' => 17,
'Psr\\Http\\Client\\' => 16,
'PhpOffice\\PhpSpreadsheet\\' => 25,
'PHPMailer\\PHPMailer\\' => 20,
),
'M' =>
array (
'Matrix\\' => 7,
),
'C' =>
array (
'Complex\\' => 8,
),
);
public static $prefixDirsPsr4 = array (
'ZipStream\\' =>
array (
0 => __DIR__ . '/..' . '/maennchen/zipstream-php/src',
),
'Psr\\SimpleCache\\' =>
array (
0 => __DIR__ . '/..' . '/psr/simple-cache/src',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Psr\\Http\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-client/src',
),
'PhpOffice\\PhpSpreadsheet\\' =>
array (
0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',
),
'PHPMailer\\PHPMailer\\' =>
array (
0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
),
'Matrix\\' =>
array (
0 => __DIR__ . '/..' . '/markbaker/matrix/classes/src',
),
'Complex\\' =>
array (
0 => __DIR__ . '/..' . '/markbaker/complex/classes/src',
),
);
public static $prefixesPsr0 = array (
'H' =>
array (
'HTMLPurifier' =>
array (
0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library',
),
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit2e312969af0145e24c15300d55587a02::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit2e312969af0145e24c15300d55587a02::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit2e312969af0145e24c15300d55587a02::$prefixesPsr0;
$loader->classMap = ComposerStaticInit2e312969af0145e24c15300d55587a02::$classMap;
}, null, ClassLoader::class);
}
}

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