diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 190d4c4..a4c312f 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"CuXJ4t6uAEv30knG\",\"encryptedData\":\"dFWoHccSt3MhutiS5TcxH+jV3G7mqw0zyCltb49wsLynuE+Itq+8EJDGGrBJDI7LqycrQnWaMGAZwnIuEBLo/mGuUnR9HHoxMW/fCFJz/MmnvtAwWiYsnDhPVrwPVSYeakIGUsYhHQdOPAUT7y9lgtdccuXulYBzzc2oDabzmkiDn+GxY7K+roRcG9kDjdNqsMtdcH1t4SkKErJxRCei14DNEM65iUeO81s/wBdi4IgD/+NDV6jRmOTEfgbiaEvsmRR/FNk2QGyqCEt9NWUkmRBmm+KHDLg4M+p1A8FusBeR3TA35fm5UzTvpsNEW2x0lK6mb1h5Nf7nD2FF3Vhw7YThnBcCt+9xWhQYECyoNQ2prQPZmJpjGhRFRs6/eibT5viV0vu0Z/k3f/WYrXxHQ9s1R3oquUD3C+2NyLwnrznK7Jn905OyWJrsaqYZwWTWz04UdH79T4qoNHpwerEQoj4k6yREWH7c4z3nUsAPdDYIqGlkj65S+ytg0OWXRLUh61nTqWEkXBaS1QHUd63vrAZnOMTyQdj4bOMW2UNdWvz2LPVqrxPa01qb3BYCEJKLvYgrRvTuv84+i1EWqjAFSYwzlj4B1xrm9cKEIJcZvK19EeWQFgyIcJeCwFp5NumZSN98L22lVbesw4SCNkDioXAd+eKjxBc90B2HwD7D4wYe2PzalJERYQGmb7PJTWBQ4xvH4AlvaKTDiFk0+EuVNXoHOIKqkmQ+PS9KFM7ox9FQSvt2kDuOn+Zzma4rNP09yPmbbb4uBko3hij4h8w8z91pa3rwJctg6gact2uRz3x+Xv394cZYuAGqZwtDGu0LUbaYA8K1AIpjtqFlaYtW/hGULcc8o3WpoQZYEQvt3dKcGoQYX4k6Ijoq+OJWABE8OmEcJGMBfErF7JK0R4MbFnPg01VgLpzKh+3RC31Y3w4JF/n2iNGVBDqIIqlWhrq3dE9V59Sa+F9wO4Yb87dxJRIKHT2RSncaPIfgdX3X3Ij8M3SRz1IMjrChkjGsPmdopdTMnMSyo80IlqhLtzyDI3SOePr0zy5IRn7PDiCzFjSDB6CY10vsRKILDIjXoRIGg2k08rBF7N7zs2PGc5wrsgXeqBEJPy060wFGrt4kvvbIEVfhmSD6/b5024/LSVWbTy0oVEPu6GBQ445cNr/VIx2fGsni6rnfvshOcIIGomj7CIM41Ltt49ifmLzvbJyS9Fu4INGo3hFgBqg4ZhXZgPzlbjOt/zQFkCH3LCk3liapEuK0TjOwJQKZSYh+aDlXq3zdA62XLjPzB16Zz1Kr5TdacTr10CDLBRBCAdBV35pxH0Jy0xQbwBnGx7jcC+RPm+ajwH9NZdwJjnShm5k9z2m61Ly2uIYEWeFZB0yCvmbI39vT5KPz6yQSqJP+ta872ayx0+S5eNDMZZp/T2m5xmMHaetvIx7WtPF4IEyG7UQoL5RiPUN4R1bd205EYQlM7R4+B6x4zwwv77PcXho3EXKmU4mSTXsMRoAxJ1qnsaag6W/PDTqfl5JWTCIUWncMA33gfdO15EQIko9NAwPDb6p9RyE1npC9lIzsE9uzRV6OLtIQFiUq0VDhH36AkjWw9oHU76UHtHZSO1bg5YnLEc0LVCqXh03rwI/yWUyNTNb/H63TKRg+AfzYrgvBegNmnYUk4pH6oyK09UDuEaca+nTcBa6YNIx855hlKc9SSRVdxgq5TJVSHCbQpnD8Rxo/VjciDHQlV0Fq2YbSbAbn+BB+efBRmqkujLlk2bSeK17L3o87NVpMhrDAwgx+/2PlWsrKd5rNmEgCheN3c9aZeo1UWYChqyxpf59GtRgO4M69FvB1cckLiqBGdWpQIjMS4nLHzJOfDdbYSDrY3to10hcszV1i+lV4OEI/wS8UVc/kK/pdkzoWyGbNcHKF3rIrELl8TNgYvFgmwaQsV87SH/M6jcoshlBhkkv64P0YWWSjmTnDE0FCkcwxwQzlipGG8gUMOoNrNSUthlWlLpbG4dZjC+jRumEWwDlmVG4d+78tQplm6nietAWDw67ofFFSpaOxbMPfMMEow+Ly/fqBzAEUM0GnZyEA6Q3whQqJ9OsCiCqUyXM1Xs56IlOXoVEmxSmZVy8Yoidhv/ESFNejhtO/2XfNzkqfKuS5AiGoGjKNzKNXkaTxEt/DT8MbvOTybcdipvZn9HHtscZlWQUVFeZMJe1gXX2rV6yZ4/LSDpkbsp37LiD8UE0dj4kOkAkrLtXyPbx2Xqxvizk3K3GQvBZ5/lki3a+1a1f0QxY2szmZ0XSKMHC7NkWZBDee4UUHZdAER7DaeAWAGScwwWdB4YhCSt6n0Ivgy3iJG4i7q2QtePlVMWzxzSL7kNmVDu5MsVN6T5qdOQy43FZDaZvBnT6k4NMNX3HuVYk3xTPpslblACvqTfbzyfcZZil7QNvtcfnd2ibif9QcMvRD9RfKqG50pJIhBXEpAxZFILudSi/bUJ2BQdJCsslqfUx1LpHYQx4NZ2WUixZRzE7va8Hy6TfMiWwVvP2YVpU4eMHoHpnRbFhizyDhBmYA2qPb9H4h9Yk3aazngRw4pmlS3zZmRX8MZolum6hbLIOoLIhei/2tg6bIaZtksUxmYNDw5FRqSK+0RwOQpWKzhabzQayp5MxR+mc3ou8Z1KeCBBpxgOmTFN1tz0AERxJW/1nZ9yEeW3ZgY17cHBmZ670ShvWbhcagVJ/53ukik4ahh61o+FQ8LDXmAIy8513Gubb/3mCTQCgYefZi1DFx5ScCp1Kv+9o/2F/TQjHQUVtzykZ+CRMNUxfmjOsSM3ZBXH+uBKDFHQ5izf+Av11Wvv8WrrAZVOA9LrnzKwJT2tNbZpNP5eIIVxd0JS5oHM55rlDq0wphPBIN9Y1j4dY4+pgUw42CE20cNiPVQkX6jZuJGjcX9SHfJmPIX1RPMO0WDD5gRLDxPzyaq1vtPK2wsm+iqwlMarerZs1Aajeu8wQLJEE0K4NS8UkJMJMYhs44KP6d6k86DmY09BwDcwav2xFGd6L6VDoYswEdsU/uaaKZBIHuZSlFVClY96+bR/KYAhNK5eVwqChraMnY5uq67G9tUFhT2IlZ6vL2uFxruwU3gFKF0Sf183S/9DsvzjmZUQRAiXMkOEQoCRtBkpAHVP1c0rRf+B94V5de/lNou1RMJlbDZ5aKnjVD2CSo/g/QAWWYBKu+SHlhvgo+/zRvbUoI+AsZESx7epjwPjWmIVFmhTQFy3j/Hbif9Zz7u6q9X25lQex3Hgk2qClpKKWaOUWtKW41DknbVk8t5PrqX1B+Lzqmt/DOOmQhzPyHbZBHuinHwk7Er4p5RIHxJbKfDrtwXvVDCaa9mTEIl4uiliw3yTEGs4vWU9pdjrb3egWp0RFCTTyIkIGNMPHeY4ZaHJIolbTMKa4KegzE+Mx6oPWaiYwAWQbNswpR4wffveDun8sHUeqmMk/LYQo/Wcxrj4MXk3mx+CmmAuo9S6dESySOZf6DW5ZJ3jVMyzU+8AXIr1DJUFPLSKPYmQVdqxXqWqOZzrifeR6bu/mWkZS97g54epFCPjn9qMOgBpbbZzGS7w7zdA+O72klUKw7TI3j8i2kXaAxexv5EncO4ysu3iLK8JJ8S54ah4puYEomD6bNY75uJkYAPlAVc/0TqFbkT+m3pdzWAKWTrMy+ERtzUyJNI2TiahaUWtNshPV0vhXCfMfBPePs0VrVzjPJQTGOdmEXHpFIv6dLkDHGgPdu+mdsgkgrtPAYZPvki86czZS8j9ll7KdqrWpMv6AWGzrKVxdolCwGgdheeAz5FEskdWtTwnknAFSSCynoO2xWTsvU7DXc8odm3sXuwryrTXKbAnU4/hFHgpHpVS4UqlMvV9i4mUYDYpZmo2JOHv8Z99bB6wmdL9rwGeBFQ8LvtDp+uJVxg8QHcTkfznu6YoOBQRuDSYpHYstTPAl+GsNZDQ29H/clS/wwC7GrUSvi8qUZ9QGgzuaATVGpV39RluVubGYBWGtqnURVb2c3GecRnUcyx6lWtV9v4m7xlhjCU5Pr+O3egCZo51lQw37STRR/nadKKZbimyACHxHhMsnTMfSXb1ScBChcFPbqfzTvnpc3bJLHI/eMNcOEBCuM9sof7AXzPLUKjXwvzqcW4LKsC6WZxDRWMu6EFyaeS1M3U53skR08oOQsbfYx/HzvAftOz3jZuh2ZELbZW8jX+NAneae2rbAMqTOx2mFh4DeqGU9NcjlrfGtHb1f2GsGMZ6OH8BuuIhjvXL1GtzTECscbSWbGckt9lt9AAW3ZSTPiQAmYkr2N7Pg+6wp2AMnBkqzxXulzObBF+DKAMH6QZHXcQCJWsiemT/NopEo9qGe84PB3eP0vGZqDPOiQKEHR/6tyJiC5sQMdPpubUtsu19/2IP8LSKCPKzyyvABiQ7VXShBcVHtYxChVWZYLXUlZRcK/RiCzfAkkXG+lxXCJQsdr0GeVlvLX2hCNoHB5Lr+gaJJ8xY2S4WYaudDVgEeP3Zegm4PE6U0J+qT4jWDBEziJ8vCzOEovGQh+gQ4NEbwRPliPcdjJkawRzwE91Kd7cMeqa1ltSY7VLWChayusI2G7q9jbgLpx1FeIZoCgShnGJ7Xgf7OoCrr2rcSJVI9BOL8n9+UmLlisfZSE8jf6LP9Dm6KASUF9QI+E1G26KDn9hhdVBhWudE/WerrRBn80ydhx7w2dbVh7x4d3TlmUUip3Eq5KryifxIYkjDVI8ESmMyOd0PnU680Io4q28y1XyGJ4A49PQjnsmT8/HfB/c+yjuQdz21pj/xV0OmW1BJCA2cBlpFiMAM2nVec2S6knSoiu5dZ+HShRdlxag2XwRjRaBRpasvmoqasnEotVRkOtfProZ6rknUYrz8EIWrpExsi2+DrHPJNzFVFesqAOgNdL530hLEbC2vonLS0+YXzxgsgsDGZ/4VdKtFbJavjG2W3fTOl8t5YJAKbSXMx6M4aYCOULxiI5BKlMbh+XaHiUEJZy9GEbo8S8z8XWm5fsr6+tjRcsw/uXYwmVuYJ6znVXqSNLRRgQpBPefwot90hroMZPD/bK7RKP3gGtr5MBu6X1xM/yVezfrR8UDq0cm5Dr6U1UZe7tk3eLWQ6HCwijT6k83l89q8Aue4oEoPcTIMHnyllBKsSQJGv7CBqqxDApehBiCtD1wX1KGz7pmqE9IZ8rV7T5TxtxB+fRiWZgooiG4RaS152y1PiHC5m6DDbI9eJO+Xp1Ra9WhO/Ge4b/iix1v9xBTM/LtI9wyjtSOMrEj892G0FRVlg01dENGlTnobfajx+I0e6jN1Wi+KxY42ReXXkc4mwk6a+WCayy6ZGGxDVdNFMHIKYV9u7DDiyj7D8hI6OvWU1THUqgRnaVKZdfAyrgCZUR/0Kqf8p3EAsUTe61AzAUUfCqXS2+hZRzngtVuLeRpcGe9Nk/2fCq5xTA7AwpKMh+oQq7dbZL0b/JH0xy9L0fNJ4mVch3Vr2WPfj5jXnQV76yw9gZ5miQlL4eAh7ryXkzz5rfwmIz7fqIr7WhtQXma4NAdepV6j5ILg2DJxyCon10ApKArL/3K7F6vONUrm5JQkFbTsOuPuhlA0J+Vi9BDr/zzCweXuU77x70y0DkVrbQipoXGZ5SDoVQEO8unHtJH8xqgnY36+az5C+5mk4TgqqlCW9ux4iN4nIYtVtS6hv1ZDBoU642r91i65an3B2H+7NYT7H67WYL5CTP43JTBkLEuLmzNhPVt8Pp1acz5Fr+fiyWGIScbgPl80aAfXLuwBnLsVni8bhAupfJbU7E10jNJvZSv88gYFiyxKi5uF1QQvOddtyFS0k9l2p+ggW1kptbhcEUU1hfkur0EZX0uOMhT76txnNG/UvrldSRmP78noEcWpyr7Kz5emsUJLJmQ9ymrmfd0eMe6ssVnQmffqKrjPJ51s5SDfuoyBclBhGwBYf/TUjVVBjgPdCQBs54ukfU6WRpRs0zxpzEZr3rAhjZZGBoUu+Taajg8OapL5M1qkZCNgsjZ8Kt4w2SDqmlGe787fE6pmecq1jQSdlAnrWbpGltocTZCrc8g+vTlxx56nESsOucelRLSa0XOqndmu9fWQJ+cQKXS2dY1aCHPjtAEpaeFdngevVYtX3IbESVIjkWSxH8Ro1hRrgUW1uqgXpJclO2Thqw5Ap114QUtrI8HUAIExHeSyDhjLhKEnYP2kouTBraFbwoQ7nDViRmXE/EYvG4uliPv+rnVhEYgYntNKTp48Nj+HRqIyl2FApcn8kLLxegCJ3DSAlKpkqry8z1ZLLohg4C6SZJ68X1Bl9N95S4v1MxUQ0FS33ZX4r1nAww90TwyDoB4sTiBtwnoD2EAo7Y5K+KF5iMgh+ileFE/il0g+GVn9X1Zat5dkjvIVbuqiIkLX/VmoGe9MwoTmQNzZzPc4vp166TyxI5fdiTZD1fG5TyLh+FEBJhCQCkyNpz1y+VaI7sb4zDO+lK6phlI0z7HqWbccQbPT65a4hN9KnRiNShcwVdjp4KLUCfJ27zfQQdBxB7UoMTf9nbPMKGuGHAoVNAhooSCvrs4Gv/B+kM8pPD5y0Nfp4woAMitGyhhI51IihNgeYcXlIR7iJS9wEFyl6FAA0PCCIGvS6AiYKEeG+z14QE0DAjuqEv4btdt5s9lnofIS8RNKaseNm6oung/Xcfu2/VdHAeEf5kIQ/LlZEBGkxZ5HDHckJSNBPFCwzoL4e0PFiWomkDIr9OHgL8ItlkvAvnFD3JuExIpzXT53lOSVBlomIu6tt9ZpO631Fad3SgyxsY14hqa7PGyRH7rFpZvhdTmDz/1UDKgxFzOwdqYpG5q7BCoP8tiIb3A/rP2lJcTJN5mShAkTmfNSxcW42bFB0yvdilgCppawvCQCG+kgTjkY9R8sP/5BopyRkNwvToM6HorwlZsAQ8OWKzyIPW2YEzUGTGWzRHYvXziAWwkJUAARQTi1JmYW/kM4BnabB+ocDKpBjT7Dab3sxonlQmm0ORXQi12ag3UWGCCQWR6VoNUrfwb91Dvgo5tg1z8yVYz99u/tzWDnGzSXLMNQp6tU4IR/hxIZuCqCLqYXqqt5xTFaWLueUtGetoljZVePYdOru/W1knxMV8Nrwq/FnaqDD7flW2d8xEyM43g9JcXu5jtVfCRWHPNGxoSHpVlSMjcJF+I6X3P8gr3hhnCdxWxLdaSRyJlbu6hmziA1NR1VMmEoUYKOVRMXs7swhbvcaWAUQpIS5Fd7GlfbEp329zcuaS584azPR3oSgfTMc262hr/U1UuH5viSIBCLM0eV/CV0kpRbL4bXIL/wD/LoD6aP/aeiOT1XwisTKG6NibDbJKLPumerFYEGE9bREdKA5rV3WB7DPYwBUdjgePzS/Yj0+ccvsRoB1Lb0WoCIOxs9MvgPBvldan6b7rOTkcyI95+uITX6T+Rq2rCOPvWUQY7WdcTZ3hJyEFjGpvWAT6WwqILp/tngJSM0lPeSpezxGQG+BKjbBNGM1nnmFQiYckh72+HukIe3RnwhLUjf9hhETL5bSJzg0VXQ22zq/CuLiOzfJMjI3zx6Lhe9kR8pZHPf8VZ5RmX8WNbsMDk5PXA3W5VYbPwnNTJek5ISmzrkQqdhkEqTg1UCK+IljE+dntZUWToPDhCo537pmzlwQqKzpgKe2s4efA+N+gd9BCbow11u3wviU1ZsA2z1SvEI5XeS84BQS1EFHEl6ygUWgZeci+ZNLULP2Su9vUxXRjoCjElb6KJvHIYXUR6BnsNla6VM1syj3UKdMNCH70PGp+2ekyofJU/OD+GhKhTX8/jsUoOz4zQPmW//l95O/SRkMZofsrMTrsyDEuNoiDn8o5N1vYzMP8T9hWfpCacvmhQdRjaO3mDPMIfeoq8+cJ60XeTLnVdPLoncHuha3ULoj2EJFkAC4jjzm+W/TLeI9dXII6ebwQYJgiUyEFgf98WOMkH90M1JzTLhYDtTPFGKWCfHKyw6bVrev9odvugiuH7rei5uBtTSXgU6pY/gsbhj0qdH6S4VKPi2vLykh/GSAyTclSF4UI1/T7Wzr2mS5gf97GoTQ6n83HYOHmFnJG9SKu6014+qi2TYCfLEevtqVRuIWmF2+THWzTQh9z7WCkCfYGQVkwHnIeeZx3F37h2EDoCEkWLSXxFCPl8+N0MXI5z5GGTB4aKpk/BO1ZSfei12lCDuZIipmEapRyszH42gaauctrxPDA/HuM98o/dj65PlSNYs0ldhO0xvZFk/wk01qQnA9birQRBiyol0gPN2CrSpEA+DkJtEKygXvXqJUy7j/7KhLNdTlmc+EZcvruT/AzQcoPGvvgdeiHxTbAqvY6Y/xafGYjAVZdvFooBPhL48bxJ9UaZsDaBXkIRrkoDaDkDdfRmhUwT0zz01VKOSNNnfhaTLe9GxFgA8DjfcTUKXynctlFCPX5LPeeWmTfOwjVLMYqGaOGxrdYJp9pzFiZHvZnWyjfNGcjU4V1WeQfeEjb3SxVVOwqefnJwisG4zMkmmsvJGeU9XTrAJvUqdoIhB5vexB+jyEjTMqsD9tCFvCESuQ/dm/ggHNsygoStKRAMT0zEzwV8Udy9eCD6QuDhP67SsmyNly0Wvix2BTbRogoGDpABiHD2toySpbAm/d4GaZnRzZQKB61714wAJp3j6WCXoDVYj2lLCPOeJCXWsqjGdDYqSKO8rXL22kFRYpHPn+cG5xfj1RJaqHBH8lTJZ+mzGsyazVhkFP4iA+207vLGZQ8vMogAwrDoanQQN3xwyMBmOWpsx6aje0RxIqXM6uk+Fp6BioOdtjCU4NXU0C8yxTPI9taUsZiueqxJVyxG7GgiOfWX3aJIWuoh9O15lMbEJO7PK1BfgkhuHZVJT00mMbCPfMgyAlYt3YhWGkvfoiwNemqqSO0PE6XGrm3Ia6NKnyUfddHRRynZ7DiztbgcfIEkcmyLVC7XlngLNzZ/Pgtwe1+Xt4EQHrzrP+858zLnKYo+MTLrK71yZqjJcKwlzF/JcDhRZ98ka/GvKUa+8gJRbg9UoBPlrlLlb+9kDfEPAW9lcEYvf0gvpBaexlCGjTxUjE1uYV94mu7PV0Gi0TuGjvxjMaYXUduIfXM+teVbTRTvIRSoXwL9rYBfJq/2Zwz85b3ErFVf8lTWky+0YE0eClb9UXfzIwd+fM+3XlKyN05ybCERZdGs62UnTMRKDZZmdVQLdyHHtTKQV+ELMcK2uG19YP89DTcgL0bUYnRs4E1yiXnoiZI7V0QPKJyPyH51sz42+Hs8AGR30ntzxDAp77x6TWUGN/hK6homQ3KcfYqH3cR1RJb2jO39K3xaTEfUmRn/SFoGU+kCxIJojywHpSPedlmzrmrZzOif8o/O2omAAv/w5J98xP+07Q9o+7n4qMB5P4a1/31MlK57wrv4XK54bfMM7jUlyaxGc2VnNjH3zly8U4v1OIoZ76e5Mr4BOeuBydJCWSYoVkOMgAhFPhnVlH0L9uXOtXgSI1ptKAUvs0r4J4asjRUPOJvUCzw9WaET1/uY9zAdbii4Qz9KRRy6vJpwSYDM7/5NQgdKpWrpnIzBEqImfDqMxPy55oxZV7uirMpr4Rf64YzBsQA9f2rsBKsdASw1i+5vX9AqI1Jmm/l4C34If4vhLWrVPFGZ4sL0TIp5eFbhKE9XzM109WWmNDI/ugqLJgm4ZjIdz+tKciv5G4oxMSrH70GfiYzIiOu6cTfHfqOJLI2TO4sCMY+8AGQgT+8p7i/9/Q5lw7OeOts3b/43LepP+xLUZcY7gdt6a8UN4zFW7vnE7SUsSGI7PYNDoJMMlQpgxWD+3iq4pzayRs6I5u1okhuoUa1p6H2Sm0FYKXq0dICWZWD95BK5REydAvbdAmZ3pVRzQgOQWzKBRhLSV0HEn4xBgiTqXf9BTu0bbob/o9XmHdJkjMhqLK0DctQ/WBgUOaATmt5Q+7LwMX8+5OcytDUrzF412TBh0y5OHjLi3NY1xPb8J8M9DLfMGTuOWWoG7xsmTGd6FAlK55cmmc6VMtMU/08mS35Wr8VJWpEhUwFUJ2KbXWKsQlU5Lltp7CA/AKfCDHJ/Ye4Q0zV3KhKBBL+AHlJ1/bhyac94VI1IlVUivn60YV7mYKlAj4Y5aGS+ZzkA01jtaBFYH1PSlctUDPxW1OoHZnUyYpcS9byEjFj3NbdXOPRJFqH09zBRTqe1wjv868vB0e51/LIzS6M2AQ6WTpToHYFA3Krm8SIGZDD3r6GktCsQy5ENHVW0/8x3ujtyXcmcLvKdfD0L6UkNWCJyuTrtj0L5hOfrvcvz9mkXy2UNzL+qHYIP3oKlHwVm1BgCgAOdYtZFmY8ukrykufoNq1Z/HUS7YpwwIgsw32VoYKNp7VdvjbvRWpiZjIocQ5QbsG0fZ7tOePeUin6tPEI2cpk7GdmIK+hX/9qB3J5pvildRLAsZeyps2arsYfuXLeBIkDVh23fYFgXi7An92LsYtBDabaLkI6t/NrzEKXWZKsUnVRHwUmZFri2oS2FVCz+aCgyGFr7WBGwB0XsPaiChe2/gW1DJRSUO+Uc5yp6uxhr9sy3s8o5C4njulxqLovZ3+Tbdpuygnburnx2Ewy5dka1cgjr/jHTFaG2bkL9bxADrhhYKGzWyEUcfl6Hw/LmpdNGeGHkzaz2eUX2gnmqYFaZJY5heMJyFDmEmV0wXvzHMwieFnhtUULu11gSGicXFkEf8abVRzCnC2KCXapPPe5jzgby7HajzLvEmpu4A+wO7Aws2JwLanKomJ0zKRTTiEFQyArhd5Wq8xOY3pFkcjnQoAMm/C2f4cUyEn/izJfHJqXivJIiwbVqBAWzETq6kenP+3pvWGAbTRX3KP3VFA+Ts56FUpOs4uE9q52dX0SxbgnuvaTS8TgCWUTvrCTJgQKLYOud6Tg5BceNfT0ssTBltZ3DNd/GmIObyKa0RS0TGDnzJ5VS/6MW5iEMbYZ9c91A8jZXy5kuA3Qh38SN40LZwFsuX0gdLL5LNZTuMdtWlZn2AZ5SoaafQIpyTW7cjrUnLYpdCARaWkzCO2EUlBU+4BhHptPn0/YWYTFAiRGqoeJE5UPZ4gmrp4XgpPIkX5VU184bjH2HdQxFZATUBItX3zmGe47a/BwoMaN7lfjfIQ538cTgqgomJgwmQ6Whh8eu35Fbw23C3iJFSUbKGDI+qexrD+3Eo1xwJv0/ePX++Z6JEq7UuUq9A7LYTSKlbqPQvM0Ey7I4yhOX6dwbxGQu4grvuEWUh/BnvgNOYt0yXct6OOXE/wTTUybqFrAH9FDZd4KP0YqkzR1aDluKDzuxvYf1U2QTe1mEgb9gnMoZ5qoeEBzs9UnrdYh6M/fG95S70aR1Vz5gku2GWlexnW6nWmz5TdrGE8FzIiA18TXzV+U/8e/YLWATBRgHWbLVZzr7MPD1ig7/F0RZgfMX5XWkGVetOyEpFGkBaXE0KqoQruX1sCGq3uJA0ZIYiHPy3mpVemoemPs+I9Nc9h165Ct+B7on8LzOPu9MZCA81bqZNMEJuGeUUofchta3g3AEaJHKNfLfPHfWDbBYb5lQ9K3NGHRzuRmxRZwB8qQ30hoU2xnt+WdNyHjnln2WZGghsP3zxDN0dvnxKg1k96Mk1QAR8lvtQcv8QwQTHxRJlkvIyTrH1zLh333MwElS5YoHzivgqIkrB59ycWNxpoN3AdHsO2InuTwJSSgyaHOFT0fKkQ9ie7+9TuA4GYdW86b/htQdfdEYMV9Cuau9FRZiMDvSHpJIw4LKNDEGFfhNKivr7kYwEPVut3W9UQvOC4UMAqni4ZpjK8O1dhof6E+dRMMOPSXI1xr6sMGmtlVJRNEiJgLWG5DBhrHUVZGzBYGaT3tyRVVCoUiQRENVAgHa7GEsqfbnAi+HwBfX4onaLr/qVXYK7/KNrtMsNPk8cKFvdtyX527ANm4wg2mkePGEBxhfUWDt0dwpslJs7gfYKUepaaDzs0n4aW0VgTugw5qB/zzzSxXZu6FmNwil1tJkgcmU9/+oFXIsj+Yn9Y76JPQLelSuKHdCAR9oOfk+KeT1XL4jlyDh60hp7bUnkvl4SRkXau9GCgaqE+6Oy2a1F304FWLTRMtUIkiqoxUcOmtGA7Tjaq0/hIUCItAuSMQTEEpem8yxNKsvkxP7XnQavOp+PfPrYNZ9x0Wjy5xxIH1XiptgO/A6f0jHE1e5EW1+Hdp3A6vILv5UMbS7D2C1A5hQ8Nr3tSs5VkjLawcTsSN4ZgNQZ8bds4mgxtwIGS5IvDxDgIcbGQuMt87ouoCA2eInisJw7ebfhYomxN8uhdRh87+6FBzlPy9fkeUNzMHIaOutymJ1lOEL0FSta+YKizGYn3aXWh07J9VSHZ8nrbf15gUPwNcLcR+b18haGgDGYFaYKKSvjabkawU/tKZ+vakoG7ZOObMJEEQtILPg7d3n+zO8yBjU68qOYO70MWZUkBk7na84mGPcrkv/Lr2S3Wm80ybVUR36rHYKROw315RUZ8ujxV8/cQc5+qXHoUtdJR33YoErByodhRGG6+/SEB064F+c1n4k6x27/nlpLhgkSiPYHlRe0A0BCOvY38UOUiFLmskdhgA7FtYqTNDJH17zIz4ecOBhHFyhGro6c7+bNGhDXnAh3ng77bOq65lS6zabcn6ofnyID4CNVA3Q0mNBuIFTevXMVozCr0ArBjYGPHkMv7PZUkT63UWiAqv+3CSSXfzAPIhQqJXMOdnDKWrPIj3EN/1b83mIveOndEgGp1Wo93ob1jR+58uE8ok8NEWEjk+5Jf7PgKqdZjCQC2BDUaOKrmD3XIPufVD2LXtOpgwqW0hc1hp+SxPSAH7bl0IS8Ny2SODQqRpMNBhECz5UKVWOY+tzWzXNjJhWtnJigpFpHD6jz8T/AeUNUj6xCaOoRKbal6li8kQGcZhbYzZ959v/HJcZdyP0gQ8IWUqysGWRfzI0gZIcG3T++J/7/OEHlsLSX+Lrhln03vn3XHIqSw4RgRkAKJtDI9lHcFBzISFF2e6vEUlSaq+zbW9qHt4gcy1qNH8IuzXm8UJZPLHXDe0ufR6hUjq2qI13D/Ki2wB4RlXPZSSrSL2P1rBvWF5pPH9yeRNjy8HxolZ1EMWnwnUUWKMVj4LBCJlTe6WVnZGWPTeiTdpd8Lwg5adTplM5WzrYST3nuUQo4C4j+zDcxgtFVuCNy6A9oYRakRvNJoVROgthXJ69NJtzHj9I2/OPWvdd1lAX3ZvTWpigQUlesYZRkIa/rk9pRsj5wtX3I+HMwUpGxn8PbpaRUWl6w2PurK0SdYmsHHKba3satjT5Amv1fH3VXcGVTT9MO4/tVwKNuoh6FhXW3vNTdhM6fGCOyG1MgWjcbRDIq1pVc8MdRfcHLLcxLiWqZl1X76XU0XMly/GJwrqh/5djLrefHdst+1qlEpg8MTNr85wP9qdokMiwt8EicoA3xfxhR31q73r3+T+Qb0qAR+jFkbIHvKeC/5IG0f9GMCuQeyDG6fyTWWgT2IjMH+0zmG+0kROdQ0wjlhaaguo7pKPeOLR+F/qKLEnXgUmGuHYlrKDie1n1o3SPmTWb+r3DTuwqVFjazdJfOvvBwd1Tci9EOBd8mLQEPKBe5pJTNEpYqbjUZgKgkE+QZuBlIN41AjDaJbOkOKu02eZSQL3p7mYxxcdZ2S3NgkNPgncDGY8dm3cIk2kQOys/mldlBAbubo9icur4KzWY2S6zGtIpcYnneLzfbMcxH+CZmZtWT6/hobeGCrWazXy4Ph8X63FF3E1905X6JDgpt+DHTF19R21C59HE6+mpYHjdS3OpHp5qPlurgEBVOkXMDvT3Vsl+fXvoBgDiq0gAOEk5W+LNWEW0tVjR3fdjUSbaLuIphb6eTuLC4HPQn7VT+eSWJBiiAQvZ2NKlOAyVwIzsZljBuqg4c+T/OpeYYouQIwxaaYR6U5P4+//Aup/8o5v2UAgQIqOaafQ7pwF+y04a2nTkCozFaKeTG4HjlsoIpUk3G56Rhr6v3mzovODNModiHZctR4KhdK/C02+qiQH6fGOWWUAPPkeVQXPQ+frbIVoqDQAeaG2QZse7rYeRqH62vgwesNTCHjXr1kZzP1pscWSOf4S4XwlbRs67tNLj2pHy9/UnoTu1mJ4dDAclIHbiLAtlVEnj6CiBWWqpBA2dWDNKkVdonQSSrGakAbvjqcx51HL/6Y0VUs1JWsUl4C499WAJfqs7LgtRQnERJVwA4vVHy5ha5AgSapC6FJ8seRLPtmecI6gLMywWIon49tvsbBT4Syv/11mppuCA6Ekq6/TxzhnnrCCnNO3llL8DVTsIIZLYMSxeKb58nDDNlDa6YSiat9PQkkvWhKrjAtxe4kXHrFr1OTTCag9EbqhmrWyHp8YFkKack9HujEzJHBYw5IlujUw00AcNXtBOnbHb61IJL8Ms1QvQptR86+HdyXLIM7NjBfuzmsn7SyMDViYDviP56wZe/VR5y1A2c08xIojbyqm6wUBfuKXfxVxiQyNmfpI3mzhz02pXFZVDdeav8h0fJgfGib/lZ4NmfY6FyOgR/W8YI7e0AKCurPpiUlzurE4FvH5dIYi4vvr+onHu9H1hoxojJF/MGQQW3W4iJBiKgYbZTGmRzRvw0EagSZc+86QhgWZd6nxBY+f6mCGkG6Prxh0da5M/VX2Ci5tXkJ6C9VJlDV4TyV1HQV5Ruvtu0mN/Q55WnL9c/jrNzLxO/dJManIJmPbniiGORH/S0dBPbNK+uzTG30FPD/3nXW7r+ub/dEazo1O5xIoLCDYPVI5PEvCictH7DFn96QZ8/mJtNGvz/C82j6fyPO7FBUYuZP4ttJRQfeiaWYsbwStKRNDR7A/jXC08uwyWsZNNcAY2w7/yyJEDUBIAeMxvu70oIRTGYsT+5vSKwdGACjdlksj0LEFHfB9XPJKmc2l1u5EsiDtMjy/daNUkx+QUftfwiMiUsma1a+DlYVzbkin+bmnt9nYz2ctev7byGoUE2jJ5Lvb3UTU4K+43etr+MB1K6zTBTc1EzmPVPdYWduQL+sofVEAjhfW3ziM97NKnk/X8j1P4GEjFBG19zEaCtlyUXXdw5puBEM4kmMr3oLN7dgdAXds3IsgRBwFvR/cKCMrZCZCd1ZNHpjID3h8B8s5/Zbw3MEyaXLnOlUOlcaJQZ+ZJIjX8geJVKNi5KtkzHrgOlOurXIHroj7xJU7pNarDbIfxUP5pbvhOGWb9V2jWBgXZF/0Y3H8QDRIO5Zfcw53Qo6bHF92wv7SGuCOZyQ03MgJ2HCgEOtZYPrVICPecwsdXbmzXisv3MLVTsj2AS2BvuPpIiXuNX5MqFfLf6SS65yEISmObdBG1u+ogS8aNFyEci557wk2ztxZ6PaX7eJmXwywFV/riQemYzgFHQwiWOUxceTxAXuwVavuUTytHQ+MewuxD/JsM2V5hLfVF07IPFND9lnHGSgTdp+fLMRup0iBaTMZgFIzCqjNNJ8t0rrwG/aNjx+/4LhC7Mpr3kSnui1FmDD9OZ1YK+xb8VapJdhxnZDcJUycOW6u9jI2bOePMFB7tRSZxdOFHWSiEghSSYgSPS27nF/P1flXz53cUMgX4yk20HB+fqprkJE9kGzKnZ1w8sxnL5SP3bCDZSO2Te189a2c0Gnj9t/vlwKJ1FWGjAEN7BHbnTz1UP9BLesTmYGcs4tGHSI8f7NQvLu9DGA7Y6U6sM92sAtMREnfgTGa3nwC7Dx3rPdTrKWBylev9D9cHhIlKqK2N+mBehgdC/Z4TUlgcq+NFnd89HlX1hJ4TKIxFjciaiGrjtrXPi5CPjznpOGgUs+CBbmHPC5olcRWZt3araEjktuWj9NJyO2CPbVw2+nGGT6zGiDLGbreu1SeWgTDkaZ6pY63QGK8fq1dy2eyMUCFeWkYsr1bhrhczNQZZ8uPQxxf+JLa7N3slSvi1fOJiuU9cqJSEGiczfUyDgIY/lMWjE2WdcNt3+cDqsy0xdgv0BQf5gfLSEakWo1PYd2WgBHV62iR0YTsIZy+wL2TXRClVuiX/EMaMFmy4wvCAqWOKWVD5BmKttY4adZe1rcMIWpHhaQrwD0azEgHS1o8BnOTH558Dbfok/lilTr20p0pVMF6r/dR2TzPoQDbBHw9JV57BNYcu28qAR8NzE1MlE+xgz5mwwoKeDCr7gN0Du36KITVy0o2UYVl9AuWLkJ1U8aZkC72ZExuoqt2pkTJp+ShH7pffd8S75JhA7lF0YkiIEzbFNlW4sgQl9mYOOKP9TAb8vf4juHMoT8yrSlTVde2gX58Ft/E0Jh7Ts7RqTh9fR1N3F6RTAs1V7dP3KUMJdl5+Hm7iyaS88EtLXU9INo1Ak4bOVWXdqNFlTdm8iJB4cAUWuqF6+FmNM8c455IWTieBelA2xlyZ3onLtl4hdedh+vLjVqaEy4gjxoAISN/fIGEiyU3Cj3/6ElqyJPimKvvt4xZ0PWuN3Qr74W/IvO+9zFoL6LslOm6V80w/ahxEGBU0lc6c+kkKhDmPsHa0KSOwJsaZnSp1AubcYwFOOJ3453Uk2jZI2Mg35TixLSKAzcQYZQ0eGq67NOZvfkLj2dfQQw/TJm5Fb1j8yrwXGa0Q3n/W5OlaNH1AY511WvDCC26raywxI07t1mxJGudPl7+tfy+s3m9K+WAspO/ad7O0lNlQbQqt67UXYeL3UjPHVqyUqC6W+X9SgoHiRo7yEeqBEAqr/Ot1WrSAs8gn1xtI7FtjXoLI4i4axSD0yCANlp1Ds5ixNV8ivDKWQiFy/Azkd0sYuholczM70R3glt332pkW42untCphN2f3Jre9k0JzaHQhsVxwn2u9EGmbi/4a/ni3CjVQ+cggNYvM6hxieA0woKFccKe9/EWF967kl9RzmOlMRMugdK7992w7zpnVEgRj2AvuS6J0q7JIynpjO4xrthAM/3TeLm4b6seZTpshyc3tbC23cjIIT1cFGKT7w0JmIrRHtS4+IZoZf2dip7nbSrdZmW5+K6b+62V+5VPJxShJfcIiOLcRtYDStJkP+Zyz1ofUZy6dPk7ZUQ1FAGXCxGlsRQQEDXt+QrUQWu/90Ti33id1NRMEKC80swOiuObGd1Mx4OUGYBVEt2RnUNutOCZYVDP++BOFssbDw+hr3I2/8fTo2POeCughbFBhwbbC5B58sAOGXwY24Q2wLOEw4Ps/nP39eXEMclUALRyeZitmxxaPdMtPGkczk+5NsiO+QOJ95NoVv0VtLXJH5Up0k2dpXXpSUKGALohTz7aYhxrZzW9LlFf/GLZXDKDySQVKR5PkG/BYRSeUkzfbh+0ivIhd4Q/1gkqv/HTCvw0ndnfWpK44xp5aXG1hvv1455FW5EKxgGsOu9LzEWhPqFCFQU9upnM9hg3op9wb6pSxPWYnGguSTiaXYAFeIPlWZ43jeU+wx0KOLE+srQt3e2B+F2P4JLXoazx+RtlQIXBYabTeh7UMugGKYHc9DJd/qxW4CRv1BAy/hWVasFCcJMmVF0977Nh3i0wuJZONgnP19qi5SKA4K8ix3mmfHdMVMV+9eXpS1crsnhKg1idd/oyD2qWgcoZNCfrHWmSj6ZZeCDNX8x8Dx6H5Oxxo9iCZvg693G0RfrZScYpkmHJFG226xBsKS2RWZncsJvXpbkvKKeNrbrB02/RcpX2R2CngRiNs8/X+5F3R1y1mcVYRsw2r9tbZ7hrgFhTwNILF6TeAApioQipYd8GJVRx+DN5iCy63CW8bID20bBad6uxVD3id0lvNuXFIQbWwHTH+bvdg/V+EB2OfwFsNMpnsYrl7SPTZlKeCeh3LzsEC3bwEx9i0LRGU5GU3aMHMLfMi0bG6NMbiaE3WOa2hEo5lnJzr6Rqt1jYJWONIUr2L0+F8O/w/sV8RyJA6wdxQmfQM2xmzR4qLfL8N4oo60SDqMpBbXAbtnJJPPiNmxfprh8PT8A56jiuhuoMKS3kxMtuVupFUSej0IEVXeDfIhc/JdL5kY8+N4LfZsuBBGDwQWUvUtcLQ5jbgbOZB9wWmkIJ6YQlNL76SzaWiMApqFratyJbMJLTpS2GdXHOvZctwjKIQEUrQecTJm4uhVDWkFQ+E3YF6jPsm9dlj2TJg+2b2Ek5pJ3B6IRWaGJebwJnM0FNC76JohIcJm5ujUEeRKtKl0/ziU0L6gpMyFf5aM6p6XjdqRMLqaqMrGMfVCJL5gZ9ms+k/dhKy5ZFt9VIgzxmY/hifqvF4iyYcsGISV9ZEiXE4Db4HZgKP2CFOmUk0sj4AhqngE2fo0tIwq/SLzdXDCwfGxUMDUcY6JJSsXN5tpKECaXLWPldg/yPAck1FsS18o8VS+nRbpM95G46FSV+3lN3FCYRFgpJIuTrLc7i8ZFFSX3sIHLdSQSgK8g/SMvZKRjb63JU1FBv6RjhxMsRDRGVm8G31bIvmAwPix0pyUQnp8QZg37S+28GXkEwkQHWHpbz55KTSkSuC5Aez4VutBtV6wIrwmGdhWcyYtt53DaPRp1/+MPDhM9T6UL106iIdetr67QGP7ItXADUlcxt/ZY0IlJrsWf72Swv30bkDjBXF+3eXkyaXBFTu7hVoGt+Fgiy9aObiYPfoiu8Vf9Toe2e+BvIy1Kd/98nDkdiEzRKDmvE0FbNKvim/QUk1DukICpEX88PeJ1FKyfibWsUJ+G+YIEhsZgkp9/Jy/mS6CS7YJOrJrX/cPWTpBWxcMiogIOOWhztxNm+LmoN6pYDTbmZ7JYdc32PFcyFJwQD9FocqCnz8sKAT9qZgMydJRQ4X9CYWwEHOon2RrLd0l+5aTYK0xvCUC2RDmEFvlDjaRvUwZr4HCw6p5Quq3emPnpLJGaqylMWTj0igbQa9QdZRUlmzbQIV48nYNLgIK0icngMZkXqAd0A7rgNVzOAVOR/7O3B/IPqZzExRarX5hN9VKmHDicbH+vZMhC1Hz0WFPtm6XK3AoZZyCWWEDsrEXMavZix333bLECukRDCE97XGD7ZgS9L0flvw0kgXNp9hHcqm1dyvp8Tb/015ztlJRnG8YJTEVUVhgOs4JIx816rd4o15+qqv6dGBZ2x6BsfCgtqrZEbsLXWJ5qoddYhKkSmllvEIyD5coKdAX/BxvQ1osKTZTV75BnToRQsYyMwOrQ938WIhwdDCeA1SoPSEX6mYW7dnZQ0Ej+EZbOsju411rEnb4CUh1w/Yv9rHS7eLzBGmFjlwLBVk7SE5OvLICjs9vgYBPJb7cmukw7W5vvAnR/NyT9mg3h7qcHen3VwRVJtnLwLy9ScucwBhvhwHz3lGptlNTQjEN5hjBA6lcXO1SF+Fc8nBty2cBbf96odSHPULUYXRi+MCRXxUIN79T3/07y+ZsSbghjUInGZsDEkiUWWPaSjnot2PRTrdnyTcu7LbptBEhtF8uSjIVmMQjJZqUclnfDNqUf45BTlLcvEJI9MAIitHRarzyK1dVZ+JGM8D0AtTDItExBR1hFB/VVUegJUawL14Y8UeAGaa7PxqCh+SsUbiDJF5KMKLkeZupqIiaooQgzD7DwAdNU36jYtbwVxW4HEmUvwTkikydy1pLj4WI1Rd7Qt2hbv8MYENnDK8JnMYNzhd+UBuSNqWnww0G+YCUtzgvOcX8uO+DSnUrgc1EleGzrT8ODT2LT/D/pRrouNntpJiEz5kXykHOII3ug00aAmrQABXwRE5BLOGPznK99D6xCdM0PcWq/xDciLlj9ielbIXRd9gFkjMuUc96o6ee4O7MOcKI7k5ISRmnFQYZTXzuhHFg5Y6LYGjQgHNXq1KG8R/SsFAFWvH4Up4Nt8VJXFdiR4ccSidsdy/jVizC0/fXaNdpbQV6a6Og4onVL4ZINyMqkn1dsSjQ+T7bXcGlAEompbKrymwsbaIbF8/+PX7B6TMPcclKq5R1+odUgqrzbatiXQA+5WHmv/dJ0G/S4enAcGnIwDkoAB+K1XeST8Mc/iSesXHH3uZ5ZfIchEpeNJMJmDSavNpqyzdjvJ4F+v6BzxdbI2unLeUWGOiiRV3SPt2kUjC2Ry17+cpWVCc0WEO1wfp35/Idf3JUrpZnX9sZ0cG9yhPBuInAc+wRGoBlKb1KPCSWQimb4N5cFnCDEA8ARbc9G/OrIcVU19lHkSpWbGj/uPdb/1dikoD5dOBoXxOrMiBnUsMHpoBuI6bilhgmTRSCkmTzF4JA2AQeqfWRBcHk1GQbBQIgkPKdY3fiZxZyEJYiqFY9ujJmQZeuhm30aTJOT/ByQdU4f4eKXYyabSFPpUguoWuqxXk9+04EqnrkwVPMxJodrjfF5ktyUVHHzaEcX/sIDD/AliAYm26VKOixRrUz4QOKItDDsOJrPp9mWW1aFHPZn68LVA6VZR+6sdStk1QSndEQwwJr95kKghv47dpYDFt8SSw/SBgOMQDEpYuXYPFo6cH3WDram8xGjLZtSV6CNwLH/PoFeFMv1P3pGoyIYUyiQwHm36/IZ+ry6yD7yfDTpZeLtx2N7mrqlUlh+1rUkfb2ESXu3ei7PZE3LT4EkVtIMD5T4/hFDPIFLd7l3KQVVzKDVpU1xK2MYKqZjK14JEYmkZVoVpbGcrz1IZYYrhWd8Dw2QXDuTBHEB7p6chmPLDRZcCS2Btb9YBfJZXGfyROwkq+YX4ZLDXrW0JM6ve8H+U51nMFeRrKh+2/I6VFbm8DyPZ2hFk7eklhVM3Xy+uLL5kzGU2/9qi9IVpdZIO0+g08hEbH5mayArjOn+hN/JnGFbbv2bB1N13dm4nIoblccf0p93T9HTDaA7I2q9qWlxlwmW5yeCoE42tXJcqyj1ZLl5jngUueRIqKAM3Ni8nksh1qxTihBXT+H2IoK1YVCU4J95W5pgUNVkQNsPttAGmV4CNlDKLPjmec+owhMbk1PDpVyzsfp/BGWWepduivJ7XT2Jwq2LsUNer6mqso0r6tfFUc6Fe0ZgQlahHL2JhJaxgkA6nXSPqk7CqjewnyRQnxzFlHujFgBrjnEDcJxfUJqKp5zuz8rtJjuAPDz8yXAU8w5OgySp6Ev5sErjz3K4pqt99ADIFzF/Eu9aLviQhys7dhpiEzRSoEozl8sZfv184sQhOOqLXQW59NZlwZ5L3j33xCQa0tPxg0vNCZVtfZ9Pa6oha1LzGrqUi9oqPJAhaD4Vbk0v/Cvn00+/zRudkVhzfY7ZqZcfIyhvh6JgaH94Gi7EB3CNDWKUiFKrkXyuQwupIhF54qEzJX4a7woXZD732TEDYykYMHutV/QOmKfpO49gsHftqAI36LH0OSFYQq3gisChTsiCMwsrK2uhYAg5VBa+6IFEa7vAWU8fH3Vf0MRVue+kBKOAYD5ZCzbxzDM+HCgLqdvn/UvHT7vJLzzLyN3RSXpZChKFLuiFYz8YLCf0mRYPCeY2prk54O1rM8s7zy4IP+wEwnte1Ore55+3bUVL1y2TmMIavcEujXqt8njjt+QhvfAb+aLs/UABdxR1c0uq6eGG+rIQB2u6NZNHRL6SqFHj9fujE2k/uRDvYGrAa55wL35Z5WPni/iQKz6J4uabwoQjLMEt3ZTiaWIFpI7ofchUoTMOiW4wBxRVw9TTxQLPQcNloQTrwgw6F5nV6efVpuV2ekz4qDs4qnxbXPVfrWpedOdcilqK5FsRZbQRzx8MVdzEfj21pQAeUE7pkEMSoDxrXMwpkA74fPwQps+WLJMdW7kKLL2kfYbR0IEUMd0lCNS2OsYbmNsvAYJgsfNjzyppoYIFlq/SLco3WD6igpTIhceMo//oce+RVRONspJCs/aVpR5sTCCUuHtKD3GRHp0NkvpBiMXbSsfwvUtg5cW9GJUxqbBScftswN1UbynXqFsXen47RZhTOxLhjAHtMwhMZ4l8lWaDAX9VGeKkWE1RbQtT3dA3AGPvJLBbKuAUHxGg18hshoGhac7MsgXF2VZHpDqNegUnmLT5tplAvY1tQSz9Fk0Fs2yYspo9sONsY4g05+NwVV2FyMEHiRlFmthlhcgjQ90JJqmNJPS1C+yY+s2QwrckshPMicIMBIH/3wrT95la0j9+e1xcBpt+8VBZz4qxjUHEOJJX04Q9R6/qMwukO+06Umg3CBybPdRr6gTPbGeWyyo4QNWl3ZCV1HuQ6/gJQEzAWwDAloKFVmXedWPQFNzGE8us+v0uKOXIHCJWhVTiOmlA/mcPxO2jY/azqpG5VWNEqgDuUwVGWaxfQAhcO7/anl/l49XpIcA1fvt/kCNam3zfOP+0rYlXdBoZwqL/mV8qJNqPNuZjl1MYNRUBMMXWqsMONK5ecDfTXct5bRDSXhSj+MlDGjBTxOsMxu3RKCVX7TOX+Tlty4lhVOaecA0Ng7YBP2XrXjRgRhgLUrk1l//MXhq+/VhdCIVSwqndGuqxeOrDXqJ6CLUF3twpTGFj8hFGDs8Q1SbSjWURB05gA6qaFiN054HVVoImJQ+3LiOH5MgpvblqScDMSw1U8iNlcz8t7WtXXI2iiESuIgmfC993SBGo/I5FRtVxaZhwbyypceAt8gTGr7OnfJvzw1Z3h4J4aO0BaTSA2WNJ82irQfl3VidUOi4BacKHcePc2fUSyMRF/7NXObNG+wE0lf9+/yCkLzloPvAi7IKtQHTpf2Ya//dE7OMJXvuq5aLm/wYpnbRkv0Eg8iI1rRpxijGbbxE8/Zq9sbyRtmJpjphjYkPaVn5q2eAxZdTjV1roynthzY84EimprjRD9VKrk4RhHHBN6WReQfnvIEI6lpZlqfXfvYAnS2zKczZYz0tKVTTty/gyN6NVqzDMI4/5ibd797zd5LBdPdVgz1pBoOhcnM8Uese18cxWmk+ikeEWVVOd0sh9A24+XUgRgkRJY6SPs12Lua/BELjdkGeLMOcq1X9IuZgjbiKF+PhZlCJFDM4zDoKMUsjWqftGUxVrMdZQOkBu7XoRGMO+sft32UD9dtEDH91rq/Z5qbIX4L2L+3K5tE8ocJcQ5kapjTpQ4OrcozcssGUVaR9qXoCspaIkABp3rpHztYcFGL4lqnJTcC34AxIh2LDSsjvB66xj288dREZp8uwzAapE9pSy9iKJkVOTUa3NI5O1DBn87pN3L9K4e+m7d0c0gMlSSCykJjJhvr8RafLrsU/KDDYBLVqKC8WEoG/7j2EaRZ37lkZPGGd9bbSl/jlUCeGWC5qWURLAvRYUBXDBV5Uccz+JGLD28ILnLCTEboQGbIGdOb6waMq2SBO51QA==\"}" -} + "Initial version": "{\"iv\":\"CuXJ4t6uAEv30knG\",\"encryptedData\":\"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\"}", + "Updated via schema editor on 2025-07-06 20:08": "{\"iv\":\"f/Hw10tsWAdjXQSq\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/field_visits.js b/backend/src/db/api/field_visits.js index 147af88..48562fb 100644 --- a/backend/src/db/api/field_visits.js +++ b/backend/src/db/api/field_visits.js @@ -34,6 +34,10 @@ module.exports = class Field_visitsDBApi { transaction, }); + await field_visits.setOrganizations(data.organizations || null, { + transaction, + }); + return field_visits; } @@ -114,6 +118,14 @@ module.exports = class Field_visitsDBApi { ); } + if (data.organizations !== undefined) { + await field_visits.setOrganizations( + data.organizations, + + { transaction }, + ); + } + return field_visits; } @@ -182,6 +194,10 @@ module.exports = class Field_visitsDBApi { transaction, }); + output.organizations = await field_visits.getOrganizations({ + transaction, + }); + return output; } @@ -223,6 +239,32 @@ module.exports = class Field_visitsDBApi { } : {}, }, + + { + model: db.organizations, + as: 'organizations', + + where: filter.organizations + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.organizations + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + id: { + [Op.or]: filter.organizations + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, ]; if (filter) { @@ -258,24 +300,6 @@ module.exports = class Field_visitsDBApi { }; } - if (filter.calendarStart && filter.calendarEnd) { - where = { - ...where, - [Op.or]: [ - { - start_date: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - { - return_date: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - ], - }; - } - if (filter.start_dateRange) { const [start, end] = filter.start_dateRange; diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js new file mode 100644 index 0000000..aad7ccf --- /dev/null +++ b/backend/src/db/api/organizations.js @@ -0,0 +1,276 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class OrganizationsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return organizations; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const organizationsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const organizations = await db.organizations.bulkCreate(organizationsData, { + transaction, + }); + + // For each item created, replace relation files + + return organizations; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await organizations.update(updatePayload, { transaction }); + + return organizations; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of organizations) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of organizations) { + await record.destroy({ transaction }); + } + }); + + return organizations; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findByPk(id, options); + + await organizations.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await organizations.destroy({ + transaction, + }); + + return organizations; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findOne( + { where }, + { transaction }, + ); + + if (!organizations) { + return organizations; + } + + const output = organizations.get({ plain: true }); + + output.users_organizations = await organizations.getUsers_organizations({ + transaction, + }); + + output.field_visits_organizations = + await organizations.getField_visits_organizations({ + transaction, + }); + + output.payments_organizations = + await organizations.getPayments_organizations({ + transaction, + }); + + output.projects_organizations = + await organizations.getProjects_organizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('organizations', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.organizations.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('organizations', 'id', query), + ], + }; + } + + const records = await db.organizations.findAll({ + attributes: ['id', 'id'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } +}; diff --git a/backend/src/db/api/payments.js b/backend/src/db/api/payments.js index 15d096d..5aa8e72 100644 --- a/backend/src/db/api/payments.js +++ b/backend/src/db/api/payments.js @@ -34,6 +34,10 @@ module.exports = class PaymentsDBApi { transaction, }); + await payments.setOrganizations(data.organizations || null, { + transaction, + }); + return payments; } @@ -109,6 +113,14 @@ module.exports = class PaymentsDBApi { ); } + if (data.organizations !== undefined) { + await payments.setOrganizations( + data.organizations, + + { transaction }, + ); + } + return payments; } @@ -174,6 +186,10 @@ module.exports = class PaymentsDBApi { transaction, }); + output.organizations = await payments.getOrganizations({ + transaction, + }); + return output; } @@ -215,6 +231,32 @@ module.exports = class PaymentsDBApi { } : {}, }, + + { + model: db.organizations, + as: 'organizations', + + where: filter.organizations + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.organizations + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + id: { + [Op.or]: filter.organizations + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, ]; if (filter) { diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index 61dc291..b8b293c 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -21,6 +21,7 @@ module.exports = class ProjectsDBApi { start_date: data.start_date || null, end_date: data.end_date || null, status: data.status || null, + cash_at_bank: data.cash_at_bank || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -28,6 +29,10 @@ module.exports = class ProjectsDBApi { { transaction }, ); + await projects.setOrganizations(data.organizations || null, { + transaction, + }); + return projects; } @@ -45,6 +50,7 @@ module.exports = class ProjectsDBApi { start_date: item.start_date || null, end_date: item.end_date || null, status: item.status || null, + cash_at_bank: item.cash_at_bank || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -83,10 +89,21 @@ module.exports = class ProjectsDBApi { if (data.status !== undefined) updatePayload.status = data.status; + if (data.cash_at_bank !== undefined) + updatePayload.cash_at_bank = data.cash_at_bank; + updatePayload.updatedById = currentUser.id; await projects.update(updatePayload, { transaction }); + if (data.organizations !== undefined) { + await projects.setOrganizations( + data.organizations, + + { transaction }, + ); + } + return projects; } @@ -156,6 +173,10 @@ module.exports = class ProjectsDBApi { transaction, }); + output.organizations = await projects.getOrganizations({ + transaction, + }); + return output; } @@ -171,7 +192,33 @@ module.exports = class ProjectsDBApi { const transaction = (options && options.transaction) || undefined; - let include = []; + let include = [ + { + model: db.organizations, + as: 'organizations', + + where: filter.organizations + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.organizations + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + id: { + [Op.or]: filter.organizations + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; if (filter) { if (filter.id) { @@ -272,6 +319,30 @@ module.exports = class ProjectsDBApi { } } + if (filter.cash_at_bankRange) { + const [start, end] = filter.cash_at_bankRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + cash_at_bank: { + ...where.cash_at_bank, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + cash_at_bank: { + ...where.cash_at_bank, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js index 7a6596d..3324dae 100644 --- a/backend/src/db/api/roles.js +++ b/backend/src/db/api/roles.js @@ -17,6 +17,8 @@ module.exports = class RolesDBApi { name: data.name || null, role_customization: data.role_customization || null, + globalAccess: data.globalAccess || false, + importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -41,6 +43,8 @@ module.exports = class RolesDBApi { name: item.name || null, role_customization: item.role_customization || null, + globalAccess: item.globalAccess || false, + importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -68,6 +72,9 @@ module.exports = class RolesDBApi { if (data.role_customization !== undefined) updatePayload.role_customization = data.role_customization; + if (data.globalAccess !== undefined) + updatePayload.globalAccess = data.globalAccess; + updatePayload.updatedById = currentUser.id; await roles.update(updatePayload, { transaction }); @@ -201,6 +208,13 @@ module.exports = class RolesDBApi { }; } + if (filter.globalAccess) { + where = { + ...where, + globalAccess: filter.globalAccess, + }; + } + if (filter.permissions) { const searchTerms = filter.permissions.split('|'); diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 3a11e69..1dc4652 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -56,6 +56,10 @@ module.exports = class UsersDBApi { }); } + await users.setOrganizations(data.data.organizations || null, { + transaction, + }); + await users.setCustom_permissions(data.data.custom_permissions || [], { transaction, }); @@ -190,6 +194,14 @@ module.exports = class UsersDBApi { ); } + if (data.organizations !== undefined) { + await users.setOrganizations( + data.organizations, + + { transaction }, + ); + } + if (data.custom_permissions !== undefined) { await users.setCustom_permissions(data.custom_permissions, { transaction, @@ -285,6 +297,10 @@ module.exports = class UsersDBApi { transaction, }); + output.organizations = await users.getOrganizations({ + transaction, + }); + return output; } @@ -327,6 +343,32 @@ module.exports = class UsersDBApi { : {}, }, + { + model: db.organizations, + as: 'organizations', + + where: filter.organizations + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.organizations + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + id: { + [Op.or]: filter.organizations + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + { model: db.permissions, as: 'custom_permissions', diff --git a/backend/src/db/migrations/1751832486825.js b/backend/src/db/migrations/1751832486825.js new file mode 100644 index 0000000..6c35854 --- /dev/null +++ b/backend/src/db/migrations/1751832486825.js @@ -0,0 +1,173 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'organizations', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'field_visits', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'payments', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'projects', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'organizations', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'roles', + 'globalAccess', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('roles', 'globalAccess', { + transaction, + }); + + await queryInterface.removeColumn('organizations', 'name', { + transaction, + }); + + await queryInterface.removeColumn('projects', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('payments', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('field_visits', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('users', 'organizationsId', { + transaction, + }); + + await queryInterface.dropTable('organizations', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/field_visits.js b/backend/src/db/models/field_visits.js index 86a1d57..8b00c6f 100644 --- a/backend/src/db/models/field_visits.js +++ b/backend/src/db/models/field_visits.js @@ -74,6 +74,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.field_visits.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + db.field_visits.belongsTo(db.users, { as: 'createdBy', }); diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js new file mode 100644 index 0000000..2970d80 --- /dev/null +++ b/backend/src/db/models/organizations.js @@ -0,0 +1,81 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const organizations = sequelize.define( + 'organizations', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + organizations.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.organizations.hasMany(db.users, { + as: 'users_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.field_visits, { + as: 'field_visits_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.payments, { + as: 'payments_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.projects, { + as: 'projects_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + //end loop + + db.organizations.belongsTo(db.users, { + as: 'createdBy', + }); + + db.organizations.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return organizations; +}; diff --git a/backend/src/db/models/payments.js b/backend/src/db/models/payments.js index 4546331..4b0ce8e 100644 --- a/backend/src/db/models/payments.js +++ b/backend/src/db/models/payments.js @@ -76,6 +76,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.payments.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + db.payments.belongsTo(db.users, { as: 'createdBy', }); diff --git a/backend/src/db/models/projects.js b/backend/src/db/models/projects.js index 3013393..9bc1ae3 100644 --- a/backend/src/db/models/projects.js +++ b/backend/src/db/models/projects.js @@ -40,6 +40,10 @@ module.exports = function (sequelize, DataTypes) { values: ['InProgress', 'Completed'], }, + cash_at_bank: { + type: DataTypes.DECIMAL, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -74,6 +78,14 @@ module.exports = function (sequelize, DataTypes) { //end loop + db.projects.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + db.projects.belongsTo(db.users, { as: 'createdBy', }); diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.js index 0ff5736..0f144d5 100644 --- a/backend/src/db/models/roles.js +++ b/backend/src/db/models/roles.js @@ -22,6 +22,13 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + globalAccess: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 74e549e..8c52d13 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -112,6 +112,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.users.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + db.users.hasMany(db.file, { as: 'avatar', foreignKey: 'belongsToId', diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 8336a2c..07a85d3 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -95,6 +95,7 @@ module.exports = { 'projects', 'roles', 'permissions', + 'organizations', , ]; await queryInterface.bulkInsert( @@ -678,6 +679,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ORGANIZATIONS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 2145e66..5b32640 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -7,6 +7,8 @@ const Payments = db.payments; const Projects = db.projects; +const Organizations = db.organizations; + const FieldVisitsData = [ { employee_name: 'Alice Johnson', @@ -26,6 +28,8 @@ const FieldVisitsData = [ amount: 200, purpose: 'Site Inspection', + + // type code here for "relation_one" field }, { @@ -41,11 +45,13 @@ const FieldVisitsData = [ return_date: new Date('2023-06-22T00:00:00Z'), - payment_type: 'Transport', + payment_type: 'Fuel', amount: 150, purpose: 'Equipment Setup', + + // type code here for "relation_one" field }, { @@ -61,11 +67,13 @@ const FieldVisitsData = [ return_date: new Date('2023-07-07T00:00:00Z'), - payment_type: 'PerDiem', + payment_type: 'Fuel', amount: 100, purpose: 'Nutritional Workshop', + + // type code here for "relation_one" field }, { @@ -81,11 +89,13 @@ const FieldVisitsData = [ return_date: new Date('2023-07-18T00:00:00Z'), - payment_type: 'Fuel', + payment_type: 'Transport', amount: 250, purpose: 'Community Meeting', + + // type code here for "relation_one" field }, { @@ -106,6 +116,8 @@ const FieldVisitsData = [ amount: 180, purpose: 'Educational Seminar', + + // type code here for "relation_one" field }, ]; @@ -128,6 +140,8 @@ const PaymentsData = [ status: 'Filled', shelf_no: 'A1', + + // type code here for "relation_one" field }, { @@ -141,13 +155,15 @@ const PaymentsData = [ amount: 12000, - document_type: 'RV', + document_type: 'JV', auto_generated_document_no: 'CDC002-JV-002', - status: 'InProgress', + status: 'Filled', shelf_no: 'B2', + + // type code here for "relation_one" field }, { @@ -161,13 +177,15 @@ const PaymentsData = [ amount: 8000, - document_type: 'JV', + document_type: 'BPV', auto_generated_document_no: 'NNP003-RV-003', status: 'InProgress', shelf_no: 'C3', + + // type code here for "relation_one" field }, { @@ -181,13 +199,15 @@ const PaymentsData = [ amount: 15000, - document_type: 'RV', + document_type: 'JV', auto_generated_document_no: 'ACDC004-BPV-004', - status: 'Filled', + status: 'InProgress', shelf_no: 'D4', + + // type code here for "relation_one" field }, { @@ -201,13 +221,15 @@ const PaymentsData = [ amount: 3000, - document_type: 'BPV', + document_type: 'JV', auto_generated_document_no: 'EDU005-JV-005', - status: 'Filled', + status: 'InProgress', shelf_no: 'E5', + + // type code here for "relation_one" field }, ]; @@ -223,7 +245,11 @@ const ProjectsData = [ end_date: new Date('2023-12-31T00:00:00Z'), - status: 'Completed', + status: 'InProgress', + + cash_at_bank: 96.92, + + // type code here for "relation_one" field }, { @@ -238,6 +264,10 @@ const ProjectsData = [ end_date: new Date('2023-11-30T00:00:00Z'), status: 'Completed', + + cash_at_bank: 85.37, + + // type code here for "relation_one" field }, { @@ -251,7 +281,11 @@ const ProjectsData = [ end_date: new Date('2023-10-15T00:00:00Z'), - status: 'Completed', + status: 'InProgress', + + cash_at_bank: 62.46, + + // type code here for "relation_one" field }, { @@ -265,7 +299,11 @@ const ProjectsData = [ end_date: new Date('2023-09-20T00:00:00Z'), - status: 'Completed', + status: 'InProgress', + + cash_at_bank: 52.87, + + // type code here for "relation_one" field }, { @@ -279,12 +317,95 @@ const ProjectsData = [ end_date: new Date('2023-12-01T00:00:00Z'), - status: 'Completed', + status: 'InProgress', + + cash_at_bank: 52.87, + + // type code here for "relation_one" field + }, +]; + +const OrganizationsData = [ + { + name: 'Albrecht von Haller', + }, + + { + name: 'Louis Pasteur', + }, + + { + name: 'Ernst Haeckel', + }, + + { + name: 'John von Neumann', + }, + + { + name: 'Comte de Buffon', }, ]; // Similar logic for "relation_many" +async function associateUserWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User0 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (User0?.setOrganization) { + await User0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User1 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (User1?.setOrganization) { + await User1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User2 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (User2?.setOrganization) { + await User2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User3 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (User3?.setOrganization) { + await User3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User4 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (User4?.setOrganization) { + await User4.setOrganization(relatedOrganization4); + } +} + async function associateFieldVisitWithProject() { const relatedProject0 = await Projects.findOne({ offset: Math.floor(Math.random() * (await Projects.count())), @@ -342,6 +463,63 @@ async function associateFieldVisitWithProject() { } } +async function associateFieldVisitWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FieldVisit0 = await FieldVisits.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FieldVisit0?.setOrganization) { + await FieldVisit0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FieldVisit1 = await FieldVisits.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FieldVisit1?.setOrganization) { + await FieldVisit1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FieldVisit2 = await FieldVisits.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FieldVisit2?.setOrganization) { + await FieldVisit2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FieldVisit3 = await FieldVisits.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FieldVisit3?.setOrganization) { + await FieldVisit3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FieldVisit4 = await FieldVisits.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FieldVisit4?.setOrganization) { + await FieldVisit4.setOrganization(relatedOrganization4); + } +} + async function associatePaymentWithProject() { const relatedProject0 = await Projects.findOne({ offset: Math.floor(Math.random() * (await Projects.count())), @@ -399,6 +577,120 @@ async function associatePaymentWithProject() { } } +async function associatePaymentWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Payment0 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Payment0?.setOrganization) { + await Payment0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Payment1 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Payment1?.setOrganization) { + await Payment1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Payment2 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Payment2?.setOrganization) { + await Payment2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Payment3 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Payment3?.setOrganization) { + await Payment3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Payment4 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Payment4?.setOrganization) { + await Payment4.setOrganization(relatedOrganization4); + } +} + +async function associateProjectWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Project0 = await Projects.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Project0?.setOrganization) { + await Project0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Project1 = await Projects.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Project1?.setOrganization) { + await Project1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Project2 = await Projects.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Project2?.setOrganization) { + await Project2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Project3 = await Projects.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Project3?.setOrganization) { + await Project3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Project4 = await Projects.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Project4?.setOrganization) { + await Project4.setOrganization(relatedOrganization4); + } +} + module.exports = { up: async (queryInterface, Sequelize) => { await FieldVisits.bulkCreate(FieldVisitsData); @@ -407,12 +699,22 @@ module.exports = { await Projects.bulkCreate(ProjectsData); + await Organizations.bulkCreate(OrganizationsData); + await Promise.all([ // Similar logic for "relation_many" + await associateUserWithOrganization(), + await associateFieldVisitWithProject(), + await associateFieldVisitWithOrganization(), + await associatePaymentWithProject(), + + await associatePaymentWithOrganization(), + + await associateProjectWithOrganization(), ]); }, @@ -422,5 +724,7 @@ module.exports = { await queryInterface.bulkDelete('payments', null, {}); await queryInterface.bulkDelete('projects', null, {}); + + await queryInterface.bulkDelete('organizations', null, {}); }, }; diff --git a/backend/src/db/seeders/20250706200806.js b/backend/src/db/seeders/20250706200806.js new file mode 100644 index 0000000..a0d9408 --- /dev/null +++ b/backend/src/db/seeders/20250706200806.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['organizations']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index 84b4542..3b939ae 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -29,6 +29,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const organizationsRoutes = require('./routes/organizations'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -130,6 +132,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/organizations', + passport.authenticate('jwt', { session: false }), + organizationsRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/organizations.js b/backend/src/routes/organizations.js new file mode 100644 index 0000000..a0db1d9 --- /dev/null +++ b/backend/src/routes/organizations.js @@ -0,0 +1,444 @@ +const express = require('express'); + +const OrganizationsService = require('../services/organizations'); +const OrganizationsDBApi = require('../db/api/organizations'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('organizations')); + +/** + * @swagger + * components: + * schemas: + * Organizations: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Organizations + * description: The Organizations managing API + */ + +/** + * @swagger + * /api/organizations: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Organizations" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await OrganizationsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await OrganizationsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Organizations" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await OrganizationsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await OrganizationsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await OrganizationsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Get all organizations + * description: Get all organizations + * responses: + * 200: + * description: Organizations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll(req.query, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/organizations/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Count all organizations + * description: Count all organizations + * responses: + * 200: + * description: Organizations count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Find all organizations that match search criteria + * description: Find all organizations that match search criteria + * responses: + * 200: + * description: Organizations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await OrganizationsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/organizations/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await OrganizationsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/organizations.js b/backend/src/services/organizations.js new file mode 100644 index 0000000..453aaf3 --- /dev/null +++ b/backend/src/services/organizations.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const OrganizationsDBApi = require('../db/api/organizations'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class OrganizationsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await OrganizationsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await OrganizationsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let organizations = await OrganizationsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!organizations) { + throw new ValidationError('organizationsNotFound'); + } + + const updatedOrganizations = await OrganizationsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOrganizations; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await OrganizationsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await OrganizationsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/search.js b/backend/src/services/search.js index 5fae432..7093533 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -56,11 +56,15 @@ module.exports = class SearchService { ], projects: ['name', 'code', 'source_of_funds'], + + organizations: ['name'], }; const columnsInt = { field_visits: ['amount'], payments: ['amount'], + + projects: ['cash_at_bank'], }; let allFoundRecords = []; diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Field_visits/TableField_visits.tsx b/frontend/src/components/Field_visits/TableField_visits.tsx index 0a10a4b..ad6ffa8 100644 --- a/frontend/src/components/Field_visits/TableField_visits.tsx +++ b/frontend/src/components/Field_visits/TableField_visits.tsx @@ -20,10 +20,7 @@ import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter'; import { dataGridStyles } from '../../styles'; -import BigCalendar from '../BigCalendar'; -import { SlotInfo } from 'react-big-calendar'; - -const perPage = 100; +const perPage = 10; const TableSampleField_visits = ({ filterItems, @@ -104,12 +101,6 @@ const TableSampleField_visits = ({ setIsModalTrashActive(false); }; - const handleCreateEventAction = ({ start, end }: SlotInfo) => { - router.push( - `/field_visits/field_visits-new?dateRangeStart=${start.toISOString()}&dateRangeEnd=${end.toISOString()}`, - ); - }; - const handleDeleteModalAction = (id: string) => { setId(id); setIsModalTrashActive(true); @@ -473,25 +464,7 @@ const TableSampleField_visits = ({

Are you sure you want to delete this item?

- {!showGrid && ( - { - loadData( - 0, - `&calendarStart=${range.start}&calendarEnd=${range.end}`, - ); - }} - entityName={'field_visits'} - /> - )} + {dataGrid} {showGrid && dataGrid} diff --git a/frontend/src/components/Organizations/CardOrganizations.tsx b/frontend/src/components/Organizations/CardOrganizations.tsx new file mode 100644 index 0000000..cc0a7cc --- /dev/null +++ b/frontend/src/components/Organizations/CardOrganizations.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + organizations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardOrganizations = ({ + organizations, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_ORGANIZATIONS', + ); + + return ( +
+ {loading && } +
    + {!loading && + organizations.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    +
    +
  • + ))} + {!loading && organizations.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardOrganizations; diff --git a/frontend/src/components/Organizations/ListOrganizations.tsx b/frontend/src/components/Organizations/ListOrganizations.tsx new file mode 100644 index 0000000..a383510 --- /dev/null +++ b/frontend/src/components/Organizations/ListOrganizations.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + organizations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListOrganizations = ({ + organizations, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_ORGANIZATIONS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + organizations.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Name

+

{item.name}

+
+ + +
+
+
+ ))} + {!loading && organizations.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListOrganizations; diff --git a/frontend/src/components/Organizations/TableOrganizations.tsx b/frontend/src/components/Organizations/TableOrganizations.tsx new file mode 100644 index 0000000..5175c54 --- /dev/null +++ b/frontend/src/components/Organizations/TableOrganizations.tsx @@ -0,0 +1,484 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/organizations/organizationsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureOrganizationsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleOrganizations = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + organizations, + loading, + count, + notify: organizationsNotify, + refetch, + } = useAppSelector((state) => state.organizations); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (organizationsNotify.showNotification) { + notify( + organizationsNotify.typeNotification, + organizationsNotify.textNotification, + ); + } + }, [organizationsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `organizations`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={organizations ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleOrganizations; diff --git a/frontend/src/components/Organizations/configureOrganizationsCols.tsx b/frontend/src/components/Organizations/configureOrganizationsCols.tsx new file mode 100644 index 0000000..b8b52e0 --- /dev/null +++ b/frontend/src/components/Organizations/configureOrganizationsCols.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_ORGANIZATIONS'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Roles/CardRoles.tsx b/frontend/src/components/Roles/CardRoles.tsx index 4daa835..86d10a4 100644 --- a/frontend/src/components/Roles/CardRoles.tsx +++ b/frontend/src/components/Roles/CardRoles.tsx @@ -95,6 +95,17 @@ const CardRoles = ({ + +
+
+ Global Access +
+
+
+ {dataFormatter.booleanFormatter(item.globalAccess)} +
+
+
))} diff --git a/frontend/src/components/Roles/ListRoles.tsx b/frontend/src/components/Roles/ListRoles.tsx index e4299d9..ef2de05 100644 --- a/frontend/src/components/Roles/ListRoles.tsx +++ b/frontend/src/components/Roles/ListRoles.tsx @@ -64,6 +64,15 @@ const ListRoles = ({ .join(', ')}

+ +
+

+ Global Access +

+

+ {dataFormatter.booleanFormatter(item.globalAccess)} +

+
+ +
+
+ Organizations +
+
+
+ {dataFormatter.organizationsOneListFormatter( + item.organizations, + )} +
+
+
))} diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx index 61fe76d..e735124 100644 --- a/frontend/src/components/Users/ListUsers.tsx +++ b/frontend/src/components/Users/ListUsers.tsx @@ -113,6 +113,17 @@ const ListUsers = ({ .join(', ')}

+ +
+

+ Organizations +

+

+ {dataFormatter.organizationsOneListFormatter( + item.organizations, + )} +

+
value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js index 13d5685..7111a2e 100644 --- a/frontend/src/helpers/dataFormatter.js +++ b/frontend/src/helpers/dataFormatter.js @@ -95,4 +95,23 @@ export default { if (!val) return ''; return { label: val.name, id: val.id }; }, + + organizationsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.id); + }, + organizationsOneListFormatter(val) { + if (!val) return ''; + return val.id; + }, + organizationsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.id }; + }); + }, + organizationsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.id, id: val.id }; + }, }; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 7352781..b498cf5 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -65,6 +65,14 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, permissions: 'READ_PERMISSIONS', }, + { + href: '/organizations/organizations-list', + label: 'Organizations', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_ORGANIZATIONS', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 6723809..eeacb50 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -34,6 +34,7 @@ const Dashboard = () => { const [projects, setProjects] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [organizations, setOrganizations] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -51,6 +52,7 @@ const Dashboard = () => { 'projects', 'roles', 'permissions', + 'organizations', ]; const fns = [ setUsers, @@ -59,6 +61,7 @@ const Dashboard = () => { setProjects, setRoles, setPermissions, + setOrganizations, ]; const requests = entities.map((entity, index) => { @@ -376,6 +379,38 @@ const Dashboard = () => { )} + + {hasPermission(currentUser, 'READ_ORGANIZATIONS') && ( + +
+
+
+
+ Organizations +
+
+ {organizations} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/field_visits/[field_visitsId].tsx b/frontend/src/pages/field_visits/[field_visitsId].tsx index cc1ad63..376a274 100644 --- a/frontend/src/pages/field_visits/[field_visitsId].tsx +++ b/frontend/src/pages/field_visits/[field_visitsId].tsx @@ -53,6 +53,8 @@ const EditField_visits = () => { amount: '', purpose: '', + + organizations: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -201,6 +203,17 @@ const EditField_visits = () => { + + + + diff --git a/frontend/src/pages/field_visits/field_visits-edit.tsx b/frontend/src/pages/field_visits/field_visits-edit.tsx index 246f89d..0461eaf 100644 --- a/frontend/src/pages/field_visits/field_visits-edit.tsx +++ b/frontend/src/pages/field_visits/field_visits-edit.tsx @@ -53,6 +53,8 @@ const EditField_visitsPage = () => { amount: '', purpose: '', + + organizations: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -199,6 +201,17 @@ const EditField_visitsPage = () => { + + + + diff --git a/frontend/src/pages/field_visits/field_visits-new.tsx b/frontend/src/pages/field_visits/field_visits-new.tsx index 5a5cfff..474a0e7 100644 --- a/frontend/src/pages/field_visits/field_visits-new.tsx +++ b/frontend/src/pages/field_visits/field_visits-new.tsx @@ -50,15 +50,14 @@ const initialValues = { amount: '', purpose: '', + + organizations: '', }; const Field_visitsNew = () => { const router = useRouter(); const dispatch = useAppDispatch(); - // get from url params - const { dateRangeStart, dateRangeEnd } = router.query; - const handleSubmit = async (data) => { await dispatch(create(data)); await router.push('/field_visits/field_visits-list'); @@ -78,17 +77,7 @@ const Field_visitsNew = () => { handleSubmit(values)} >
@@ -152,6 +141,16 @@ const Field_visitsNew = () => { + + + + diff --git a/frontend/src/pages/field_visits/field_visits-table.tsx b/frontend/src/pages/field_visits/field_visits-table.tsx index 0783251..1780143 100644 --- a/frontend/src/pages/field_visits/field_visits-table.tsx +++ b/frontend/src/pages/field_visits/field_visits-table.tsx @@ -143,7 +143,7 @@ const Field_visitsTablesPage = () => {
- Back to calendar + Back to diff --git a/frontend/src/pages/field_visits/field_visits-view.tsx b/frontend/src/pages/field_visits/field_visits-view.tsx index ade8696..5abbb76 100644 --- a/frontend/src/pages/field_visits/field_visits-view.tsx +++ b/frontend/src/pages/field_visits/field_visits-view.tsx @@ -148,6 +148,12 @@ const Field_visitsView = () => {

{field_visits?.purpose}

+
+

organizations

+ +

{field_visits?.organizations?.id ?? 'No data'}

+
+ { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { organizations } = useAppSelector((state) => state.organizations); + + const { organizationsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: organizationsId })); + }, [organizationsId]); + + useEffect(() => { + if (typeof organizations === 'object') { + setInitialValues(organizations); + } + }, [organizations]); + + useEffect(() => { + if (typeof organizations === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = organizations[el]), + ); + + setInitialValues(newInitialVal); + } + }, [organizations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: organizationsId, data })); + await router.push('/organizations/organizations-list'); + }; + + return ( + <> + + {getPageTitle('Edit organizations')} + + + + {''} + + + handleSubmit(values)} + > + + + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + + + + + + ); +}; + +EditOrganizations.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOrganizations; diff --git a/frontend/src/pages/organizations/organizations-edit.tsx b/frontend/src/pages/organizations/organizations-edit.tsx new file mode 100644 index 0000000..5b37f27 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-edit.tsx @@ -0,0 +1,126 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/organizations/organizationsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditOrganizationsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { organizations } = useAppSelector((state) => state.organizations); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof organizations === 'object') { + setInitialValues(organizations); + } + }, [organizations]); + + useEffect(() => { + if (typeof organizations === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = organizations[el]), + ); + setInitialValues(newInitialVal); + } + }, [organizations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/organizations/organizations-list'); + }; + + return ( + <> + + {getPageTitle('Edit organizations')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
+
+
+ + ); +}; + +EditOrganizationsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOrganizationsPage; diff --git a/frontend/src/pages/organizations/organizations-list.tsx b/frontend/src/pages/organizations/organizations-list.tsx new file mode 100644 index 0000000..9e0a09b --- /dev/null +++ b/frontend/src/pages/organizations/organizations-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOrganizations from '../../components/Organizations/TableOrganizations'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/organizations/organizationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOrganizationsCSV = async () => { + const response = await axios({ + url: '/organizations?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'organizationsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Organizations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsTablesPage; diff --git a/frontend/src/pages/organizations/organizations-new.tsx b/frontend/src/pages/organizations/organizations-new.tsx new file mode 100644 index 0000000..2787572 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-new.tsx @@ -0,0 +1,100 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/organizations/organizationsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', +}; + +const OrganizationsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/organizations/organizations-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
+
+
+ + ); +}; + +OrganizationsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsNew; diff --git a/frontend/src/pages/organizations/organizations-table.tsx b/frontend/src/pages/organizations/organizations-table.tsx new file mode 100644 index 0000000..489c393 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-table.tsx @@ -0,0 +1,164 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOrganizations from '../../components/Organizations/TableOrganizations'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/organizations/organizationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOrganizationsCSV = async () => { + const response = await axios({ + url: '/organizations?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'organizationsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Organizations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsTablesPage; diff --git a/frontend/src/pages/organizations/organizations-view.tsx b/frontend/src/pages/organizations/organizations-view.tsx new file mode 100644 index 0000000..93c58b5 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-view.tsx @@ -0,0 +1,355 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/organizations/organizationsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const OrganizationsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { organizations } = useAppSelector((state) => state.organizations); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View organizations')} + + + + + + +
+

Name

+

{organizations?.name}

+
+ + <> +

Users Organizations

+ +
+ + + + + + + + + + + + + + + + {organizations.users_organizations && + Array.isArray(organizations.users_organizations) && + organizations.users_organizations.map((item: any) => ( + + router.push(`/users/users-view/?id=${item.id}`) + } + > + + + + + + + + + + + ))} + +
First NameLast NamePhone NumberE-MailDisabled
{item.firstName}{item.lastName}{item.phoneNumber}{item.email} + {dataFormatter.booleanFormatter(item.disabled)} +
+
+ {!organizations?.users_organizations?.length && ( +
No data
+ )} +
+ + + <> +

Field_visits organizations

+ +
+ + + + + + + + + + + + + + + + + + + + + + {organizations.field_visits_organizations && + Array.isArray(organizations.field_visits_organizations) && + organizations.field_visits_organizations.map( + (item: any) => ( + + router.push( + `/field_visits/field_visits-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + ), + )} + +
EmployeeNameSite/PlaceNameStartDateDepartureDateReturnDatePaymentTypeAmountPurpose
+ {item.employee_name} + {item.site_name} + {dataFormatter.dateTimeFormatter(item.start_date)} + + {dataFormatter.dateTimeFormatter( + item.departure_date, + )} + + {dataFormatter.dateTimeFormatter( + item.return_date, + )} + + {item.payment_type} + {item.amount}{item.purpose}
+
+ {!organizations?.field_visits_organizations?.length && ( +
No data
+ )} +
+ + + <> +

Payments organizations

+ +
+ + + + + + + + + + + + + + + + + + + + + + {organizations.payments_organizations && + Array.isArray(organizations.payments_organizations) && + organizations.payments_organizations.map((item: any) => ( + + router.push( + `/payments/payments-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + ))} + +
PayeePaymentDatePurposeAmountDocumentTypeAuto-generatedDocumentNo.StatusShelfNo.
{item.payee} + {dataFormatter.dateTimeFormatter(item.payment_date)} + {item.purpose}{item.amount} + {item.document_type} + + {item.auto_generated_document_no} + {item.status}{item.shelf_no}
+
+ {!organizations?.payments_organizations?.length && ( +
No data
+ )} +
+ + + <> +

Projects organizations

+ +
+ + + + + + + + + + + + + + + + + + + + {organizations.projects_organizations && + Array.isArray(organizations.projects_organizations) && + organizations.projects_organizations.map((item: any) => ( + + router.push( + `/projects/projects-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + ))} + +
ProjectNameProjectCodeSourceofFundsStartDateEndDateStatusCash at bank
{item.name}{item.code} + {item.source_of_funds} + + {dataFormatter.dateTimeFormatter(item.start_date)} + + {dataFormatter.dateTimeFormatter(item.end_date)} + {item.status}{item.cash_at_bank}
+
+ {!organizations?.projects_organizations?.length && ( +
No data
+ )} +
+ + + + + router.push('/organizations/organizations-list')} + /> +
+
+ + ); +}; + +OrganizationsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsView; diff --git a/frontend/src/pages/payments/[paymentsId].tsx b/frontend/src/pages/payments/[paymentsId].tsx index 00674e8..3f7eec0 100644 --- a/frontend/src/pages/payments/[paymentsId].tsx +++ b/frontend/src/pages/payments/[paymentsId].tsx @@ -53,6 +53,8 @@ const EditPayments = () => { status: '', shelf_no: '', + + organizations: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -180,6 +182,17 @@ const EditPayments = () => { + + + + diff --git a/frontend/src/pages/payments/payments-edit.tsx b/frontend/src/pages/payments/payments-edit.tsx index 3b14988..da1d619 100644 --- a/frontend/src/pages/payments/payments-edit.tsx +++ b/frontend/src/pages/payments/payments-edit.tsx @@ -53,6 +53,8 @@ const EditPaymentsPage = () => { status: '', shelf_no: '', + + organizations: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -178,6 +180,17 @@ const EditPaymentsPage = () => { + + + + diff --git a/frontend/src/pages/payments/payments-new.tsx b/frontend/src/pages/payments/payments-new.tsx index 21a71bc..816c65a 100644 --- a/frontend/src/pages/payments/payments-new.tsx +++ b/frontend/src/pages/payments/payments-new.tsx @@ -50,6 +50,8 @@ const initialValues = { status: 'InProgress', shelf_no: '', + + organizations: '', }; const PaymentsNew = () => { @@ -142,6 +144,16 @@ const PaymentsNew = () => { + + + + diff --git a/frontend/src/pages/payments/payments-view.tsx b/frontend/src/pages/payments/payments-view.tsx index 8683987..921e769 100644 --- a/frontend/src/pages/payments/payments-view.tsx +++ b/frontend/src/pages/payments/payments-view.tsx @@ -114,6 +114,12 @@ const PaymentsView = () => {

{payments?.shelf_no}

+
+

organizations

+ +

{payments?.organizations?.id ?? 'No data'}

+
+ { end_date: new Date(), status: '', + + cash_at_bank: '', + + organizations: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -157,6 +161,25 @@ const EditProjects = () => {
+ + + + + + + + diff --git a/frontend/src/pages/projects/projects-edit.tsx b/frontend/src/pages/projects/projects-edit.tsx index d8bae62..3a25f71 100644 --- a/frontend/src/pages/projects/projects-edit.tsx +++ b/frontend/src/pages/projects/projects-edit.tsx @@ -47,6 +47,10 @@ const EditProjectsPage = () => { end_date: new Date(), status: '', + + cash_at_bank: '', + + organizations: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -155,6 +159,25 @@ const EditProjectsPage = () => {
+ + + + + + + + diff --git a/frontend/src/pages/projects/projects-new.tsx b/frontend/src/pages/projects/projects-new.tsx index a28a764..e0e0a94 100644 --- a/frontend/src/pages/projects/projects-new.tsx +++ b/frontend/src/pages/projects/projects-new.tsx @@ -44,6 +44,10 @@ const initialValues = { end_date: '', status: 'InProgress', + + cash_at_bank: '', + + organizations: '', }; const ProjectsNew = () => { @@ -121,6 +125,24 @@ const ProjectsNew = () => {
+ + + + + + + + diff --git a/frontend/src/pages/projects/projects-view.tsx b/frontend/src/pages/projects/projects-view.tsx index dcc96d7..b1d7d23 100644 --- a/frontend/src/pages/projects/projects-view.tsx +++ b/frontend/src/pages/projects/projects-view.tsx @@ -112,6 +112,17 @@ const ProjectsView = () => {

{projects?.status ?? 'No data'}

+
+

Cash at bank

+

{projects?.cash_at_bank || 'No data'}

+
+ +
+

organizations

+ +

{projects?.organizations?.id ?? 'No data'}

+
+ <>

Field_visits Project

{ name: '', permissions: [], + + globalAccess: false, }; const [initialValues, setInitialValues] = useState(initVals); @@ -106,6 +108,14 @@ const EditRoles = () => { >
+ + + + diff --git a/frontend/src/pages/roles/roles-edit.tsx b/frontend/src/pages/roles/roles-edit.tsx index 88500cd..0059e70 100644 --- a/frontend/src/pages/roles/roles-edit.tsx +++ b/frontend/src/pages/roles/roles-edit.tsx @@ -39,6 +39,8 @@ const EditRolesPage = () => { name: '', permissions: [], + + globalAccess: false, }; const [initialValues, setInitialValues] = useState(initVals); @@ -104,6 +106,14 @@ const EditRolesPage = () => { >
+ + + + diff --git a/frontend/src/pages/roles/roles-new.tsx b/frontend/src/pages/roles/roles-new.tsx index 1c068c1..18f111f 100644 --- a/frontend/src/pages/roles/roles-new.tsx +++ b/frontend/src/pages/roles/roles-new.tsx @@ -36,6 +36,8 @@ const initialValues = { name: '', permissions: [], + + globalAccess: false, }; const RolesNew = () => { @@ -79,6 +81,14 @@ const RolesNew = () => { >
+ + + + diff --git a/frontend/src/pages/roles/roles-view.tsx b/frontend/src/pages/roles/roles-view.tsx index b7368ff..9cf5d43 100644 --- a/frontend/src/pages/roles/roles-view.tsx +++ b/frontend/src/pages/roles/roles-view.tsx @@ -96,6 +96,14 @@ const RolesView = () => { + + null }} + disabled + /> + + <>

Users App Role

{ custom_permissions: [], + organizations: null, + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -170,6 +172,17 @@ const EditUsers = () => { > + + + + diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx index 87d7090..67b3a95 100644 --- a/frontend/src/pages/users/users-edit.tsx +++ b/frontend/src/pages/users/users-edit.tsx @@ -52,6 +52,8 @@ const EditUsersPage = () => { custom_permissions: [], + organizations: null, + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -168,6 +170,17 @@ const EditUsersPage = () => { > + + + + diff --git a/frontend/src/pages/users/users-list.tsx b/frontend/src/pages/users/users-list.tsx index 6d03ebc..8a5de89 100644 --- a/frontend/src/pages/users/users-list.tsx +++ b/frontend/src/pages/users/users-list.tsx @@ -36,6 +36,8 @@ const UsersTablesPage = () => { { label: 'App Role', title: 'app_role' }, + { label: 'Organizations', title: 'organizations' }, + { label: 'Custom Permissions', title: 'custom_permissions' }, ]); diff --git a/frontend/src/pages/users/users-new.tsx b/frontend/src/pages/users/users-new.tsx index 4f3fa45..8c25d83 100644 --- a/frontend/src/pages/users/users-new.tsx +++ b/frontend/src/pages/users/users-new.tsx @@ -48,6 +48,8 @@ const initialValues = { app_role: '', custom_permissions: [], + + organizations: '', }; const UsersNew = () => { @@ -140,6 +142,16 @@ const UsersNew = () => { > + + + + diff --git a/frontend/src/pages/users/users-table.tsx b/frontend/src/pages/users/users-table.tsx index a408cd6..de28081 100644 --- a/frontend/src/pages/users/users-table.tsx +++ b/frontend/src/pages/users/users-table.tsx @@ -36,6 +36,8 @@ const UsersTablesPage = () => { { label: 'App Role', title: 'app_role' }, + { label: 'Organizations', title: 'organizations' }, + { label: 'Custom Permissions', title: 'custom_permissions' }, ]); diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx index ce0851d..6af29e8 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -138,6 +138,12 @@ const UsersView = () => { +
+

Organizations

+ +

{users?.organizations?.id ?? 'No data'}

+
+ { + const { id, query } = data; + const result = await axios.get( + `organizations${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'organizations/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('organizations/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'organizations/deleteOrganizations', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`organizations/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'organizations/createOrganizations', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('organizations', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'organizations/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('organizations/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'organizations/updateOrganizations', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`organizations/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const organizationsSlice = createSlice({ + name: 'organizations', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.organizations = action.payload.rows; + state.count = action.payload.count; + } else { + state.organizations = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Organizations has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Organizations'.slice(0, -1)} has been deleted`, + ); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Organizations'.slice(0, -1)} has been created`, + ); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Organizations'.slice(0, -1)} has been updated`, + ); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Organizations has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = organizationsSlice.actions; + +export default organizationsSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 97f8a4e..63b6a70 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -10,6 +10,7 @@ import paymentsSlice from './payments/paymentsSlice'; import projectsSlice from './projects/projectsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import organizationsSlice from './organizations/organizationsSlice'; export const store = configureStore({ reducer: { @@ -24,6 +25,7 @@ export const store = configureStore({ projects: projectsSlice, roles: rolesSlice, permissions: permissionsSlice, + organizations: organizationsSlice, }, });