diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 88f6126..c607c00 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,4 +1,5 @@ { "Initial version": "{\"iv\":\"AHKOiwdeOuoeiNVz\",\"encryptedData\":\"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\"}", - "": "{\"iv\":\"6rpS/qNQl3y2mmUT\",\"encryptedData\":\"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\"}" + "": "{\"iv\":\"yBl9+2UEcTrIbxOI\",\"encryptedData\":\"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\"}", + "n": "{\"iv\":\"gRg/zOOkFm2RAPnu\",\"encryptedData\":\"fPsM2aESksszsYAHSV663mXcUqnRaLXo9mwSzXW7+t579QAzZKeIQyApHF3k9t8cdDh+N0sfG2jX1DWiv8dcJmMHqaq96o9wELj7iGcm8tCjd9+z41oUObJQGJs0bvyP0xukmuGTt6zO7z+7Svk5Pn5Y2BRjHuP3AG+IFMXK+zT249a5NAWnWcGVoqJO60R6e9zgtQWtHBo4iJvVZW0MnqR396uLnaYgqFVvZeNd8V16OazGc0riaXcMX4OfpIJPixS9B7CDt1+lFaucjBS19SQsIOqdtMgUmVpsNynWbYM0woP41woKvIyA706nHcniGB37zp5zhgvFgp/gfqqpJqVk1+abOrBtCMhvAKuKSXemWopZ2XULCp7vKO2SOIF3yhZD7/Yb1p0oZd/OrO8ZwEL4tJB/ZSZbKAg5rldn9Qe6UIEsJtxH+GoSnLae/858zr5lIxTVFfBCfgHYylmiyJpf46//0asH+F2tez2Mr5H98u7Sr4zUnPBqinBnHuIz8X45JRMyMS19Ad8bVWkp+icWtax5ewz18WHy7gC1cKQXOBTi6unmWWsZ5D9XOT2afy5HV1A6eqSUsyMPyt7acb5/OaMTS4SaQMjUSZ7ZOo9z6cv/kKvNkceKDwgayqSKGPMNeW/hbQgwUXCKEoKshOeb+1+Ijvbk1pHcZCORHyBPQopi4vpd5zZZ37KurcBzGvAvJejh/x0j1QHWbUWhYN4nScjrXTESfFL3Y7EPwTjiEYKvB8MYba8ePM3zyIDNg+DOdX7m1a3xlmosTC2PTmDz1hWPUsEzNB0qqAOy/jZp0n1i55fitzXKbWKvByriMP1+Utk+n7jb0jz9P88p2/exSatMwEWEaATaT9hIIVXXLl1S4PtfVbUJ2UWpY+vk5ltr0zm/dBBTPA9aNqGU42mrf9bkT6XpvJkZFeYqjLFdp4tgiOq/XQ7kFULRc0fYc9B3UZtdV+3RYPquZQIDQSakcdHVV85uDfYdIRBBQvsWM4znuQg+/g+jEYjAU1L2XC+h1m8xFxGOBPRnjndNo5ByYsBXeM7JIKkOLV9SGSTxs/e9tRzLC9Rz1o9P0HVdCoUpZG6fWUoprMTy58GmV4x6uZbuitO7BcjFcepEwCCq5Bg8iM/wrFTz72krFpGaTsqop9uDwD6xpC04jpVDUv1nE0Sgf/Y3UuNc67joDDz/ucRq0+Zpor520u7RONPkyr9HXixa/CYFxuUBCgL+sSQxeEGMOKG+hWre+fSezz1B3GnfQSzubzr50Qh0F46TA2NUrUN7+dMl4RM4E4/+iXgLOfP9vi96YunXchVBxENwZXPsxbqyoaVRR3ESW8ONcU96XP6309X1TMPu+LUHvnGcUV37N74bcJRRHCQK0SL9r93yVkR8476LqOwzrQaDKAGqqnVkJ2fVqrSA3DyUmYcUyucvuXy5UjVWVToZXjotR3dUh8w2eyrgtgn8R+a/gwTXAF/GU59g0VXUfJxOXmk6pucB1h0xlju37WB2zuwt/mkL654mwN6zfPcDjj9/P+w5Lno9myH9FMuoLOVv/CCelhpWUkf6qOoRCSnAVXF57m3gGxKYQmTLdfxOcE+kv+NLIChyRFIxnZfrsUPX4ObGuPlLJXbHm6Q709KsYWAMx+XO/QBL8RckGHAxZXhp8ANfZyeodmeW4BaXeP85Z+zAt5gRFqGUx/1YTD4vJthyW93IYy17PGSmrmK2mTDfuDo/AFtsA8Yq9X/S6kL1+Pc816QvBf7OdFyVSgyD7boe7QizT56El0fEeBBYqY+VsFdEj49NwtE8gS4WkiaGELYACqtcL1QHTPbUAO2FZ/baPy4ldn4PEvKulH4KWmv0nMNvcRoSJs0P1r2mMrb45jjitfNKAuqa8en5ARRHaR8+OL4Pc/ZcPq1O21lhKxPBFPqDFi4TVa151bA47btnCD3F04swTJY0HSY3OorBOVwI27ryfpoeoPlyeEMlpBLOcrHBhlr3MQozuTVIgGD96vgyFqyyCbq8Up+PNoykm7b7e/jCp7XDqEhBDw2fDLTIUAUhtpZwn4qfGRA30fBT3PJJSTZGXfM+Iwe6q+77ujlluy9Et9fGZDvWB4BgqjkcGUP7Iz3l2GKc7i8MHe3O7vLr5Ae+Bd+IaZeW1Hd0Jc3Q142wKCmp4LfuWMj8LwOS9IWy8iN3gXVb/Dh+nPS31JAueDcphxZozUvGwbbG3aAjLsM0+k0oATGTzgLKpR+b5NF9V7euSR3Mbami7sQO80WBUogncfQwNWi2SgQrgZKdwgZgn+mkVQOIz3qp9xoLtVa9gG3OUAPzOaBbrb3d0eA9J78RyI/b8Ttt1LJ2USDMkqu76hZZgidWbrQgsktgzx8Qhpf82GRm7lN0M8qxMN0SR4aJQ8SXni/fOwbE5U3p0IF3AqYH6qaFOn37fEFZZUFCHw229RaP4jiBndWy5Q0MurROPCXJksVeDAxihTT3ad1SVkU9M+vo+Pl/byrjH6Muws0eM5lYXCNEP7i2/sCW7I56C/qW0bOK7y/AY+7bL/64ddiSj2LnZbrREoPw3s8kmwio54dFYxuBe/jZlRV8kBeNylw+736lMPoeDnS5YLZn7GLAglcEmrspNpZNCEYYdnbWQA4kgESWo7ZU6l69neBfWyuNSRDVCIAGUdGcoa6ckC81/WHy7EvDrDwF1OQQrBAW+HoIqR5lsFmZBhv6gY99tk+WATAutTzxkuA6YrETfnNzgiJFq5rs9kNxJ5Qe82GZ//PxYgPtpN0u/VaiT+qmMHITF8UixsfCag8T/90vUl0VZyMKmnGhaCuK+qith8GoctWSl7W1itMuYrsVrygj0Z8IpRqLqnPw/OL0YZxssWcZNozqVIPrLtklIICFbQ/DcHE93m4q52B92P5h/o3keTK8Mrtrdy0PuPSdjjxtYuclagd0OgvLe+cejMCXtKupYiBgqfQN88j31u2TbnrFAfnforPJEn4K9w4Zu9KHFnAhQHc71x0Nr3bxpthEzIKAf8yUVKjWYZ5tnJeluqC23AXQrQwuUJjShE6mSEIUf8QnhhJa1H5Xilatwc7DYc5FhD33wm03mzktuPQxpYgXHUyRmUbDY3wvdWP5bmQ7N5zV6DPJ0QkJJcHL+hkSUEnVYIFzQYaN2CEr2DXSDUcvMUnw3HgrGkTnmBKDTX0dOajF5nJdQCpeaXK1Xy377oP9d0LtA7ihSokIMHncwdrV2dzPvpiJb8rB9f2o3kMT/gKy+Q1lnXYXjm3Dno0UMWhpz6qkhikhZmO/hZd1IrkbN3HWp+++3MU/EyMCyk4tUMlwAVA65JijC141TpF8uFiyA+m63EKzADLhmcymQVPDCww5UzSk4iegfMeEkSuBiqDhzZMl5iddIlZ57FgWIiOBd2KxmBk9pGQzUFZuhkdUp2vYDFV2yzdQAmXwbzmcoqmTRpogYA+uiCdT22b98ubFCeduYxb+ZcrfA/QZwiZ60usoaLEK+zNY48RdFIfiWlelokND1uJ5TCklHCN7PHnFNvfJ1ZseaS7NkYEzDBIT7CFG78jWQ/cPxxQq8d4hiiHkxjcXSJGOIZBTVn9OlOM7qbrIs7mrRAm1sySTJboA2wAJ1xO2o6IeAhlbSGv2YQdc5ZwES0KdSi2JBAg4fA3YOIy4NB7TLDXoUj1fCw6CE1yVrm1bCo1FA29skAcJ/Hq9oZi6yESxyuMRv5UKYPReAxRXOXy7DT0dKG1x0njedNKjV/KZNyidKvH2eC9BMDjo3UewzZ/+eJjw/1yh4sPCZ9pRevB/bLKN98HbDpXhSvLNiEItziC7P8+oE9L/veVhZJQlQZmpLLSj/16b1q/xvEXu0Dayzb1SleKIBYoVpcPh0d3iWt+njJ/M+lZ+9TG0eHpWi2jxV6QUb0zG40B1X7YVr+RDBxI8DnPH1vfq275pE0n0l3bRAmCu+/74HWIV2BmsgflRsH9EvjtI3hXyCrmIxQQfmxbup14rA8vWSGscW0ALBqcF8LUS+8HVy2+i+mowzIIRAI8AaoSbmtc/5PWeJJXEheZp8YqLHoBDWorn50P7cODwtGu83jAQxjcVkfTPYjqAZnefBgzHRKU60Bnm42ktguvFBbvxuD94pE6FuPMgXYvQs76rR6KMs0osAcXNh8nVY7JceXiZMyXT+wS4zaoFN/WZXlccN+ksPMam0VY5erOWFZMr7vTtWKeDliLLGcLJc6x6Ok2+33c4GFExFCqgzmpi1dF75DhC+4CX50FiWDbayiamikrFtWzDggDd4U4x6xd0jsEEYex2YZ3EuQNaN5apQ9LV/wPoXi6yUlp9pmYiBgEzEApRJeSaXZIoeGGwonKjrxec65O9/6//S3cBboO2ZWmkNwZrl6qu7raDFgAf6fYZOmTrgx5llm5L9EHV+Qq60+PNoplAKulnqQf1P78CDJ/By/I0nIyKHCedkPHUMMrD9bEPDboelihxG92fPRVa0aYnQvGy26SJY4ybpA6oY6dOfYbGAkcbe8yPqws8Cl1f6O/pohgzH/A2NzdqpaYT2lYUjDOGEGu+Rn/6pKlMxKvd9TeHs2Ul0sKi5nv0rgz3i+54P5p/SySayZC/2dL5x1z/BCoDvei8aSmUt+yEhPRof5tTQuXhgd6YcSgLnnFTcJD5UTA2I4K/JXfSLd2++FU5OuBPj/ernIKx9tv8ypPcHfCQaw3hnHN2EEqjKZTdfrTaYE9Z7qNJaJAnpwRRcN3G/2CX2sX5LzhL0aj9+l/LeMIiQ4Gw2p4EmE3pSD6jPz/yXPf91GY/N39rLEzV1KLN30Os7//FzxAqS9w1Axvk2lgdFdjX9uvoQ7uyZ30leuDA2lcC+TcpjCvYhKsU77JkD7U+Os2YNX5xo0Tk+c4DoV3Mx/ozNPpOm/c2TxfvX0aLJfzz2YeqkUOeLokgr/Zb4/1Mr75BoiBpHU+f6FVYssRwx6lUeBJHcyXzJ6ChBBleQeubGBAcJd2ptuse9sZdH/DUmuhUXM0kwA4Og4TXZepY86pta7nCNGP0K7R/N1Q4mPbghmIzLOKQIbE+CrF/tT3nJsAW0Lbiqxm/4qhQtQB9O9zzLxxBTEeq7ulPB8lNRUpPeG3AvDeVHAQAXy7ZToI+Z0r1DxXX6rUYzOvdMQAK9NmCaD8B2fREJv8NmYJwq9TApH1T7MPcKwYdAut/OZhFOk3efkM+NpTfZvAYHW/XTR+H4C4QdbxaCSpifjgPJto6yrNsby1E4/UKY3S8d5OCSxxHOEgRtO8wmEu6EGVMVbsL0kSSXa+qZSh1tAfJVImRajG7XpbRuSJT3rlz4y2VkyFFWoS1XluiDU401JtB+U/soYvx+9gSU1V464cTPcIgcbsGduFG+MwDekBPPvrKxKCBnrai10a2erQohGL8v6/Ov4BDYSFk39RRw78oISAuIR575XqT/VHV1746ytSD/mbSJ03iQwficsgthuBc29Cpw3gMt60w+V0cm42G8KOoBSvWRufPygaRV2fqzkAHqtS3lM50S5F/D9SBO7PZ7QOYQMfkEuSGbAUa+wJ9FWfo0Iu8veEMsg0IGdcwfkGZ/9YRKb6zCfynncJdwstnm9USulevJ1T+36nNF1VvNtnxAIQYELs+PPGkT6cnqJdewnzyvHcaNEdOf3w3VGsXdC7nHCOgh0M+hMKe9N39vEwHVIRT2GqYlcOKoSj/Aqo3jHP2SiTgqzSc8biO5OD7w0fbu4Pc3fBDav/THUJIft07XpMbmVy2+fUQsVKEqIo54PxyqyGsqqIvbiYS59BC4azz8Xq8LMn2GwIW8u1pvi8qt6uH/l4s+mXmVPI2nH1CtLLZwhdWU3BtULb1H8LFZO+tg3gVJtqli6mxT+Nogzs/8M/Sgn9eE8s1l3Cb3pw+yDksALivHnZz0jVoQwQ/15e1NAQXS6SEL0LJtYh/hUZXhoo/bBFWjI6d9XOAd0v7st8ZO/JRVfVI64m2lJyIdY+sRbjSjLeFF5SOULsjAwc1zv18DThQzagmBwnMCxajfAG/hv/j99M2d9XL+0Lbu9tbS2duONxTDB++baL/QcSRavHzc8Qua5f/SvFDK0KftBp+m+j0FFiEWHIKX9Jmmq9aQTC2GUn8JQwY9pumx4eCvsAJqFBxLOjISdUTznOf8YmW1SSxRBajq3MlnvR+aXmfTl0Vg+7T6u3d34qjM8tbL6V67CtEO19bQC04LIaVYYzfue0QrpO7jCYlUHAN9Vv+pT1RPJvX55uq75oTyBCrk/HIJB3eYkwaHc+tMIDmktuxZiYwWrF5a21XSsHR2mV7X17Ta6CE73+Oih+x+fdZ2VfXMHTKUw9dY2GrGEBvMaAO0Jq9w88rPYmO8Kq05NWFWNIvMld+73ioNbNXsTCq3wB2VIqrLylgXw18EgLkvLWgYHbgiG2qfQzE/pD/AD+BECqW6/mQ1tsbI7+qTgBJIFA/AGh2tM3ZpfdqG71YSwf/KfSch46YuFgvx3KiTKazWGVeOIZgBBOKLCVZ5eH/FvnInFpcFFOcDH0TxM8cfE63Iw0v2AGleytQHfjfRWwxflIEG3LDEln+bdRrD7+ZEggJmQ7IAhGrTITUhr3lF8EVpsyDRw4Nw8AeIddnvMORYiXDnNtFvseAy0/PMxGUtVlr0N3u7/Z6KSCIRQPDnYz5xMwniVdpOYfvx9BuxSF1i6EWesaEG9EC1pNizHQLoMntcBJ/Tkvd+WxCu4u2J6h/ky+JRTSYfA9yDQzqGjzqeNEBWdJ6A1iukwfEzr3XfbpyE3lPVctFC8S+Im7MnXa1d6UpNf7uSTkJW8cPZxy7Jjq9s3hdrkgQCNdWpcOnUmpO47GLRb3KcB5iRxIKe+bsxo2HKHVj2S2JkYQ/MFVGl1Y8G+tXSKZ/wXnCb4icW1pMl58wToABbNF+ObVa8WGhfwete1MeKZ3dKW78BEXOwOfbLxIyUmuDYkgq7S9Dr2nXGetbj1lDTMpzI5BzeCiYd9P1ASAIRK+x3BlOaSymehgeou2WVUpy8Ptq/J8ii6ysI744nSitgifF98bJYtv8gmBJFXXjOehf2WOzUTt+IwudAPnlom0z8yAC5fzBS0ephTxYEA94oTPVYeD6vjj1fVIiSOx16D6b2yUElv/PuAZ941GJ2t5BzDF2ZHD9V7GiyNvdmnU/4izahJZ2cVCdnS+E0V3/1sE4IsXUvKJk63ByjaxkYgQTX8DH24WJZKbLC8ZTZ8zIrBWP5j89QVdypZ0cv489IfMIgE/6aKa9trNpHaoJNQPvZ8q4FkkDxvM33v2xiL13xQtFj9uTh+zn1y4sdJJKib298vB8sFjSF229boaZmT+zn6xwJVjZnB8oumAIl+VZYStHUX0RTtxz0l2iF5ktzfLXEpGKQmFxsjPhpdqW/0jr5IvT6Y159p2LPUYqzEfbqkhAtlQDOYUHM72uJ/XgcxDK+aS0rp88OcdhJiT3NXoG8AYFAOgdbAluJNGtufF+z0065+HeAE3VyoEQ1eJsHzzTlFqTbT9a6d30/TuFFv0F6QNB5dIRVCLiZBQ6TjA/ViiddhdWSfc8n/SyQqHEE+j4WJFvYzfBZiNEhpI3yUCIBzpzYJBUuIOQKqC0Ug+hbZFoESB34CukzdVM36ARS3p9wr/g/nmz6yvWQsMV5/dnhstgxIVX2m0UjbqWp02ymd2xstr/KC3yrmyGZ0qBy+ItJJkQSjJcAvmW7vDv4K/2Ny7+R63ORlaW+JXZTXXxLOyIIkfQTq1fnCkfbgeScaTjbUhBKt6+euCXN/uc4LAJVHNjH96U9X4Fp5oEb1MeQsgm7aWR+cHoj+4YK+EW7YfFl2O/9+siwlr4yclXURMUVWiZv6+RV7N6/EvrrKbNStY206LybvNM2QkHIOBnhXHBKJT3wrJbWZ3cGJO8sxVLd27uUj7PN0MVCEIKXrFPOlxHUiKYyjawkQOvrVG/YRMCe2nAKB8c8TB6JPC9UTeRZpKctE2m7Y6OsW3hmxQUTkNQn4+gRfXOFKHbmcfbVdCQ/qfFmNUtuYwQnSbjEu/73DCS172GNES3Q25+xCrjpSw12iMnBAiDdFkxTHaqqRc3mHPgkxrkii8SWReNhmQNiEw6YIzDHbjNvebpG09s2XumfXBsbAapFgN/l2pucLOyM5sc1HD2DyvGDAZc+KPG7RRQN69IsY0BN5MYxsKzEwRdFXDqadCwV9F1hvD8mmc0M5ZtQy+D7cmdlXT4oRWe60zC3VblejCFmKdX2lN5kIMzZieArPzovedwdcwwSZBQ2+5WyqxIFzXfYres1na8ZhnCsB8N4aZrpuQY6h4H66mxllIcxGYG+QDjSg/EKoEjEkYkREDChWsvdeJRXm3e8IFgPYKZimsWCQCmtP+klW4xeSAJow55kB6aKnb4MckdMkvq/SP/XFWJhmCZ0wAIqm42FNlJgwNLTA+ZiJgr5uTLhCHYMhmGdb383wth/+DHh9N34vjT9G++LOXpo9B33LlZwLg+1BQJNPNmrooK3ETS9790UdvVmtPnlGPZ/fb/oO8umHF09sEtDPEavE+cyUQfvFHr+qoxU8rp7eSYB6vpSJq63h2QwMwnhe4JgIzCPYblU/SvBYkLkhaIVuV8DOT48QAROIgsXYarU1Nm6T8acgugLD1QhxTRofthlhTllV2ZiCHFHlgGUj2GzrgIpfEk0BhUp8A/owgVaooOeKk99C+NqBkwTRJZAMHFvGE+p5nvU+uLdFPhe2F7MmCmwUW/9DfafhbyfF3FAGg6GQFnDcB02Y8FHR8lWkaIkU9+TFSf9O4nSc8XLcjryD9w4P31CKFb6zLTnVFjaDvojokjjDoVE0ogly1LJnsTpGQDHZ5GkzpwwldEbwDr6qSBt39fWXkXQtaS+1mOTD0MvTFDGtajI6pf1zoruUcvEBsoj/78o4zhffZyITVu8cgoe2005eunlbL2z49ztMofk/R4/rjttT1WqKpBWQcoo8OLPv+u1J5+pAF5pApnAJ4nNsg1kvCC9T+bYeVb1IsG5KKfI5AHzKPdWCrf9wsA3WHINUgUXU2UERhP+Yw7Yn/z6LCDtd+HrNfLNni7nZbx6wx8HlK+XJiA2uwcvr55IB+9T7T3JAkQhuWek+CcyeGRNfJzU6ycrhGG1PG8eLNaZOJVOzEkTn5QOpBNEAXW1YiLfTxi1OEc5XK6J3mfkAGgcBW8YkyfnGa7my3OvxtIwaKBYzbjiJHQA3zW7eoPOrKWpP6WocPMZ39HuKKk1CNx8ujQuM3EFg+OOxdJxjicl3MSjfpiLEXWCq5eUT/CNwppVdxfeSG/+dq0onZsaMzAIaNaiiVqpaZ3NOML1ce4UxiFRbYMSo2MFOm3E3JiUty7s+XRR+Wna7fYddG2jIbTJKrnE0yytFJs/LeRTsSTeZir9Oz8zNyN8CL3ZxJVQ83vqPPjaLhC1oKMBG//h/lcgJM4/884i6q9hm51D1rHHyJ+2HfqGWdpDI/HPNe8UvB6spt+XRtvKsZU/R+8jpV8PFYar1W/K3UGZOeBw+etSTCux1uGGy6Mtm93CBZDiVkX3U9XIXX5UsoXWafaVpAaPNNTpjO3aIVMRiz3qDELLyfHoTlhoy06JU6ThFK+J/jmldu4FF/KrsT9AdiosKjkb4CemhZcnSOnjbzq9W0mSp//Dv9Ns6fGXX+z4kXg1hbdKHyE+LHgGfLegapA959Jowc+zRd0zIyoqWuMAis0JgoXlRcU5QNkZK9593e+bzBAV6+rAuKaf8ow9AX5tAQKHtdKnxCaAL9ujlHY4Z3rUrojJ3GCRUUU5HWxxS5Q2r373KV7rOC+eOdN9uhFpnEz7EHE+q8aPPDtGnoPeb0ILz1oHdcWdxE7ajbJnYKqmSPEN7Q8Ma41kgRx7aaQMO/TIt+gyj0xh5tnNMXFShkByvoF55YjvLuXp6s8e6PS7iObkDAeyK2gkczL6zTWRdFQTPcX/pkT2pEoFSE2IXD+4Ys0vrzzjYxRr6foGgi5yiUapZNnKSjDjX3/21euC0B6L6sO2naQ3eScuDIt8T6bEoup5hPfPB26uevMdtLeC3nZ9XAihFsFw+Zqk3KGu0ogZHQBxJ1AbFaDTijcEtgmJdr+2oAFZHPM9743lPCYr96kTqWy5SLm5dje2KJyr1v3bRLskzSXuscfOoryF2No+69iU20OD3B+QHhT+Uv33C8ktDPydI4RyyVLe1TuNBsEMC9VICYiLEvk0MIGIKbRrLLES7sLd6F7M4vbfnU6WOqNf0bMrmDQcWq7KN0aTkpAnvIOpTT8km/M1CctY5Vlbia70YUSz1jZ0HPpwNUemkHSInTtPiy3uWoCOuFmnOFo6c067LtzaxR44As1JLqDHWbyisG7txRWiiKJQpgA3Q0oRBDIXZ8074cidO7H45Liwpb2Nb/Gi0V54N6iLC0MYlTeGJg+lCZ1vu6jzn1/SSAJcsz0qwSOuxrGj0wSIp7XEyVSKnD19Ca/DDfHP4Ds9z+WEw2qB7ar8Z1lfliO4ZBIWrq6wVtALoD1gzktVOKP+E5ft58m36IAV+vjwZ75EAqx6c9jHCoF4TVrnwzcE34IV6CcUP2zE5D/g7ui1jD/Yzy9scsrQew+i897439fekHjMaWMBumRwXdSU7mqTYb3wp9V39ocSXxaUsXtD0LadNOLIlAwEDV30vBxQnU6WBGsYWB3NribujxOm6pphsSwAN7Qgbqep9kOW7JydFpEbhPCNFCDUPRX0gAsxcLhjUEu0hSzRXrQFy9t9mZpzHTX7l/QtYj9lQCiwnZjsuTvCbwt1MIIKYJTub+A0HsmQsGykKrPVumWzxkVLQpOu/CUQmzzfkaybf3GorE6MZjzJupOlLrzmAsTYtPGe6nGznKL/0qRWpXj5omGFlzKFIgvq/3Za6s/gsUqhE7FFVVDNIB7+I+gXZfnFagV07ETyTDvG8jYdvjZsylBtOW1qc3TRVaLz8dIdAlszrpxNA1LHKLlJSIDFmHVFN5RVW7DVODc9PufK+grzTIL+uCezWnjOu8Y6HsMIMW+hSrZ0CB060KicPj4mVWD+2Q9OKrI/Z/MJ57WiP+JXnXFrSZtvjPdYnz9HotgkM6ucDAffps6CPbVsUiuOoVCgI3G/NIBncNAl/IFV40H8ftmSnehz7R91JyukK41Gzulsf/lEvGCqvvYhUuk/emmQjovWoC1toi/gbrKrLMbQgfrxt9Hhl04aGnIofEXWatqJr9R56voVlq/T25ceKM+Kt2z4CW3dTjci7h78emEBAMeqGdLKPCoO7E4BJj7bQfsNEdHQoxvJ9uBgWqVfz0JZ59hSux8y2sqXLNk32tGsn5+tqn4agL0BoQk+HxGpElcTi2V9aXg5fBGgzDijEQOsSHifuMhnrTHebn8ISMoUNKuEUYNwie+fgnRftTNHce+NIocjEA1l6CTpOlcDbSDPLKZNfJ54+KE3ZRIMSz7Gzr/Y32FIs+imVGInz5iMyW+QxxXeJeCBuQqPSaedcLcLrVcYWcYT3b/IVi8OB3XoL9rcI9hdt41b2GgFvU8pFyCwXj3sTzLSTCY2pVeUaU76rmHA1w14SEpoJvSF85llvbXHzV/K0cwLvm3mVFyNc+jF+zmmu+cefqCKeUwfFFt0AJ42JLdmEOQOxXrbvPnT3qTTLzs7lGXX1DJFYGChId50s+YuUzTBp4EuiGGfEo6xF0jLoeSlITrRJ21kugHLEGd68cCquoDBLiwIURT0d2R/0k0qJIyGkA/enBqTFNyqNhH1lGTgmDbEiQOvNDroeU/5JH4sUi4XdD94qTzs4DDLp7LIvYC3PXDL+M9fYV2iSXqCaOTP0ZcKkjWY63o/yIC5ZhVxlLo3uG7Hhhd0NaI3VX8jTrE7lS1PhqbNn8AjLju/JaQnoxveHLsOrpiEbcrw6/YqYYzgUUoz0FbGIS7seAyQTaxQlknM7CFBffCR6KmjKqH1WWDM9QXUVaa9CGUnc+M421dShdMUI412sEnktFSPpZ7rnA10th2q9ciLEUvgoJPryK4zAWwRnGy9iQKy2gLCwmCqJ530mNaMbqhpk+6GB6GsgcqONGvbW8LF9F6/ngRF9jZnijVZlT2EshpCBJteLe/R6KtP4HPahyabGD67F4esjGgrIV2XLzthLFC4QLpO8Wo73ic6+GmcE1EwshwiABCruZiatADenV/uY0SvXkjplcMGCdUuyugbQQ1oPB2k69Ahr6NoLcZH7sO46vnkkhEng1nD2WpXpRjmezpH2NxqNB/vn8AljM/fvQ4D0yuUkVSizvAbjeWuU4S/x5jT00MHOOypsenuPKKgF/mhJ5MUfqNZ2r0B9XHQz5F/ZgHVant5+uwRyh5l7r1jUac+/SvFT4RQCeuYDMLfansvenmzESjImy8Wbq5k0zzvoXh0vJXCsNL/XEgb5KNiHSkLQo2YOZV9kajuUC/I5w1wxE5yiCjDQJaaK2Io8c2lLRlMwQ9933tqSI9TVdcnkeuSYQLf6dyxGq7imV1tUxWe3y9u7NMl38ESB3k+Rb92H+A7WYwaktoZGFJnPxRFVwqBn/9jaTpBeUwHnM04ceJIReb6yMr/nHNOzF20CXv2OJF4AmwbE3E7Xejrlg1aGztCc7lrhZx0fLUXBR/ua+Yiphn//Rn/y/+qRpv50jYXKqWOg0F5gDopn7jxFetF2EBhVZd/b3q/kr0o+QsFiaK3V5DWjdAjmGWwEo3JfQxq0adGLxzTmDoMIDulv7Oz0KCwW/37AaLZTLhH62T64ijzh5OD+20HDcyUfNACBXNxg2dKIAzfaRrdkxzg7m7Y3WDFvCVIjiE+o++xNtcCRhJBad72gg3GR+8DEfJoc/rySKVaY+GCK/ST2Hea4DjFABQFsfoGQk8XaZxm11kWiGXY0bpqaoI1sOO01QXf6kFHLCYFCKmtfsdidsnBn1IHGzFQe/OCbUa7ZR0cg6SJvLkPZ/lcxgD7a0/jUjLtnXMsMgtovHU+UXGrSB34sgeJVXbiuXQwTnbNlwc88SUy30AuVpRpN3aLO5+sGlpBRKEbqSLDphjSGulprqjX980/52L41vhhF7//pZJCd7la8jd97FzToGj+nmK+aqEeg/Vz28ols1mS76EIFNX6TtnJtn0bvQc6TOMag+slWPcQwU3Rm1dqWoRewLyE7qFpmj1g1g9aShdGwK724UYO3FTV7xff+mlxFVUpxGDv81Df9/EZnmfC1pKjM8N3K3TbVOwMKtMm2Tgi8JYPXTDpt+Qx8+CKkDhI9zLintvnngfbRzQuJ4Z+hmHhiUpx/vyvIwvOfqN5tAKnL4CO5IsyHIfuLeawALY11x8UH1Tzmzx14xD+Aco8xxtdT+25DDBe9lozsEjJx6zbbMVHHWT3uYiFY8keoipHq7a6nz2ww1/5fXcs/rxIqHIieJ3CjsdvGBsY4KK1QhF2GpGMdxvNXuvTHfbh56aHDX7VLkVcer5urZHCp01J74U2pAohRWtg6lcFYWU+TscS7fIv3rSTXYwrigOO2O+GIWwas7IorK5MXLVbO+zeyfqd9u/RLRZmkF5ootOgFl6xzj0BaU7tP2dYkeWGEax7Zi6+eHhUz5T8bvlxJXTdqtz1bGZ3YS21p0OdOwjliuJJ8M19rUVSIvLlZnabyt1junDAYMyTzUxKJEHWB2iIC9eopbze8C+kdgGI3/Nly/sRY8dRogsqJUgloBwRpQb1v4qBkm9Jmjq14+HuLN4h4oYjKAgYsbJvl2Tm73H/z7EwFSEqJIA8ht6e13VsN4VmeqJJzFyX+IZMuUfEbpj21CaFK96lgHqa68n1K0FBPqxHby5OWhL50Z7tsXi01pWQXq7ICQeR2hXyqRfE7rLxFL9FdaDk+LmUxW2ZKJD8unEe/Mrx24mfLe3Rgsli0E9gaehVlLmFMFBeXdEZu6diZlv5RIbhJCwgdB7O6dHB65cerRqa3te/ilv//G8bshRSqgeAw22ZjAmRH+wXlelqVV+Tjj8lpfENhTjr/jDLjgfu2wXBi7jspI+/FhyIza6m+43kL+0pEdb40UrOid/oH3jryoe+nbTRzKC1Cux3pz0P0ktZB3fsAmdd0Xa2sYPUlAoeqtUG/qcFsob09QlHKt0wavToL/WdRbZcn5irdH+7YBnz3OQM/uMm1FISczSEZXDl0lxG2qHqA+XNahHp5xl+PCDE9SbjODTXMCmAagcaFA3gilRasLhsHddm1Nmj0u0Ox4hOcLTOYgy0rLSppnZYHswOGvqYVf/yl/u+LWNOzFRuQrSrq0GrfdzYQ//2zvVxbz11OqPuGGqOs/YYK515o/shsVHWny34tbpn81h6jIpP8PFPmgMVO85CH6MigO1seohd+7wM5t0TGvoyIPQ93cCWDR05tAyuUT2XO8pSk0/rMWZZtYIgfe3bfXYBcUXLHbkb1cZvYNT0fYe91m76lWR0ScTUJPe2nrAOL6eJKZgb7ZVF7CM2Dy/fXNgFyTWXObJuA4pkDOCz35/vdjVVDy1IOr1Ehjo2NnAc+3JeGlnIGFSPZV6ILShZMW5Ut5m3CubXAfs8x0zzudkHIjuLoYSOfACiHf+zkXpXwvPTnRRa67EeArA4ooFHnLtGDa3SyoVbstjl83213mIQQcfAa67qjaDf0ya5qsJ3jR8AfmuSellsmsFLhNL0/iUP5E9aW1J98vByrNjWfleVvit6j/KI0W+DRCeM3JkTOvtVtLTAOLdU/Oo0941koIifkUSaYZumaSD2rYGlgKPsAyE4TraE0XuuROPZTsbKVcrobKuz63pqmmgYjDVqQY5Pfpb3dpNN5JS18/jxkdzVS8Mb3dcCVB4Rg/qB/dnKudv65muPHmZPgiwn9h3ug2d3GPB60QBvAi7fwv1Spf5vfzNkb6PAtqToEvh/ux6vp+aUvaEGgLdEIBTH78a6ViZQ/OY8A4r5g7rdumpZftVHuRv6yRFVWxOq9+aP0C+tUSFtzGx7TjBaUIp9t12waLTPyu+gX+Z+Npj9p76kiLF6n75vrRJd0p5evIaa5AzOtGWMK1MbQoG7FwmrjRmq9dv3ppUPtG9+HlYpNulSIrYkzckb2bhS4v1yROVi8vYCCetzts1/fReeP6/zA/VcqDF+F+uqPZ9oONm3HHtRVz0ECxA96SAE1r+hX7YfCo0zTQb3COa30kmFYbEjiBPucdz4zxvymfmQPNskcl2WS5QhE5f7KRHocIQptSLQ0I20KhMcaThHzqWXEWEk/P+Ntas/u8ZGAbtVfyvSbzyvdB/S6Z+Im+DuxqwwpdoqjYr6mIqjm43RF6lkKZ5mmTfLoIYLIWmbcWZOMAupSc9T5/XPMa6aBJN/ysP1xLsYJ0BhgUv/r7/k1EKRhH2pltSJQFTowJkyHg22v6MTNr1d5FvT/kKU8383Vn4X5L9HShCGGSoD7rF1gc4hG3LOL9qQjlmniHR15du/EUCd9+YWLsnEKviThric7lMhjYrXtbMrs7PnvHOaG7hHl3oXa4pPB0pLy1UFtTm4i4gIdbaXu1ywNA5/6NqrzYQ9YKDSGl9FTNfmNBplMOLCZ1uRMA3EQKvZ0o+iTBcIZwPE09IpC2EWV60zKvkQE8v0pnSI9f4x0P0juq83Uq2zNZdShBCqeXVfwopOvJcA8h0/MrPc6Gke8d2oo3hGB+O9BDh2dr73Mf11X66DcHN01x4VD4nSuf6CLvk0p0JxSqSw6hncVpN84djb1dmhLeBogQ6khtID6ymrGUEP06ziHmzRHuEnznbEGtpY3dkx0fF844U1guQq5NkzNzMaZsYY0GUGRigRp6wmU42KD0B/SHNR5SCSAEyx3Foc+Zi7a1wv+HGblEgmPsfQUZcsOHYi1eKd+SKPKh8kAKT1e8NW187oQDqizU2efhFYNs4Q2uYbPjsOexSrbnlHbXBefoL1Qgw3MfO1STV+Nc5lCWGm+Ol+bEme3MyAOQheNzRB01lnIaDXZGIRkygXAxN/mi8gHm1wxWB4O/VAetK11Cj5nliQsQVTQfICEk//05rP3yowoPXOM1zPNBft0kAUU2KgFjmEmSP3oIc2sah7sJPr3j+Sn1Dnulscq7LTkyRq4jBEnf4tHgXCiy9vIGkpfVIW1ip29nxsdZjDTzChBLKGTSkA/2w2T97pjQAsfgmm0MA5nslSlOfluto/xEegBSaIO7Qesw0h+MjlwXp4NcLvB4n8eKTWgwjfJAfYmq4abhFuOTVcGSh/KHFwdrRckq7UzH2zPYGFfWlYfa/ygH0wqg1ev7MquiJzY+kwxhrVJCOtt62BU3y6r3Q/rINyOF68XMivIbrM1OXtL7XNadLVFsXyMVadwZd0fvGQ2GIOdT18bViVRWe2aFXs5hZKF68su8MVEfzYckVXtA9FtcbcQyfae4Qa9LtWYwZH593KF9aGoUkOSogy+VXOQC6dg72nxV/q9UFDSKn6U1g0y7oogaBUDzGhxYSivzqvWwRXoA5RJgRYcs2/GGQTt74iWODLSpubyZD4U+aHxTab/N7W7cOvsP3sHd9RIPPoyO849A04bryKSVOHUyzNmcSUnOvIrRqcfM+jvHK2IhSR2dEdK625jeQnkLVwL4Y+l9Cj10T4+Eeyi9fRVIUvcLn2wU+u6zuBtvOUyFX7kcc0v32xqMeO0PH7vJyF7qXFo6uT9iuGUYuroR7zTT7oXL+xHr6Ux7lOc/OrVJRS7sd7pZfETcvwAusZXqip5U8HFIB1Qs4CJcH/jiyaOpoDMGwzYUB1PWxOjogbdFrgodqnAMGT8TrIm6/pwd8yDjN+zHadaHeu5e2XK2Pynd1AFQJ2XWXtPd4SyXrj9JdtwUy3LZAFE/eC9fuaE4NiUG+3sndw0chPoaKY6qDeeb0oaebsiwXCSxsdPNoWL5nuy2qXsZVlBiZhs2qtd2SjTA7o5whNPxwI0DXEKb4mEYF8EFUP7IxNnK9yYMGHRiz+XNnskAQAjjCBAGv1rGIEIjQ/pj+sQ33u9VejteLECaIaJtcHkhOFMA0zpOI6SXw7+LmPdkWZn9RaPV1nrOABRxcM7lfCT6HuWnVSix/Ypq7pWRnvsDCc6bBULu/pSq6wu/DbOdWKKq0uWc+MN0mt/p6YlcLpq9lEr2n3O467PMT+ZjA7HnuT8F71QiUzDN5z7cqWmzhNYHigGOKtAeckwZMIqZVQjHtNhATwzYvc2Vu6ejrLUHR99xHzImxNp49Z+q7oKd8sIvd7RFXQvQDwMZghTBc87K+inlHTfcp0Zo+mtiSFQgeOOOqZM+nNkpLlKfjZ+Qp6ewy7LCOD9JjgtpWRtroXjNRG1h9q/AXYugOCULec6q9XWwxxQzDOEcFIf25PHStzrifCJs5WxMhlikiD7Ui5mcx19h2VB/ZEjWLBUOkTTIk7k3KzotK3bw//17ufDWOH7+tCuMSwOHIViIQXLVdQPVqrlmGRzWFyyVs1ewH5BwO1JGMEJlvSvZLliShFGb33547ZWMJs5TKvpOGplSZCHLgWwVNLNabH978Mn4n3/sW48eyVXC3LJPU4eNbmPnMIItJ/E08kkt9aVw/k7Jmx/Ah7WfkXBXm3kAbZoGQsuPAXw+3sooczeWBUP4GUOX89KT3rz4hIJ4f2A5isSOoynwdQlvPc1T2KVOMOrxKvfeUHfzKjlQ1XvNGObGqdRNq3QGeT/lejkqmqduSGvEzudBCvA2NofLHTxSt/9FOHt9LxURwHYOq26Vi++xkuBq8p/oV8+qH0DFNdAM/Ql7z9j7K6bOZ7aDXjpRubUptIUFAUoNBXpPjvUtgRP3sADnWOyVDzB5oT5XhoxW3vkhoq5xa02KfeVRE2HNJVMBktfwqR22Wp+bWLK+wSbLNJLKjp20ICl6r8LTmne8Pjsd6eGVKzTdetJfC+YiYdrAiSWJ3a/hfujlPawrO73eRZV+hU1I9+j28kzVAYQncO5MTrRkTuK0Gc0J++ZU49LN6YvoXaL12udw+lBFTt/OgAGsbo+BipMqolpX5DZ6GBcgaYBF2uZNSk9GvVi4b6pKdU49obPdx2d4F55WDWfKmOvk+AF6mHHo7Z14H+1W1ErYZJlpEZZGSgXGqs/QWY7WhjTieeix9wvgy7Xq2jfIEH4wVGbemfV7Cok30YFgbexiefaV1jUoQpAsVnXpabGpBPpV63cidvdMDcOBXwPzl56ifJjI8ARGN0pJEqZG8CwViuelGjl6WhikcCmQveTdrLPs8r8kHG3/NtBrLbmQvvhKQIdWwqf0QaYQJ61ToCec+c8pwZfSolbBIPz/yd24zhVgTeQEJSZqC1VQSgORozWHRWAUhdPFEo88pFwuGS7CDl+OuBIjNl8/BhQGU4ECh/nObF530ipAzuoEqA9qQFRTS9jGBwVLGPL/yrn8+YqrGakCL8FDM/K1ihh+t5IHOo5Q4yOF0WqwZCOr4aNTCjWjjDvWcjEJUpnTlPZzu/ocBheKUZoZZZ22WG91WGezhvdfBqMorXO5Kp2Ig2n2NcWARgCRq3juo1Xzm70WuRMj4O8eb43P/EuQNzkMomJU15aKfpZV/ue4E4bQtDhO/RjAKBdvikBkSJ74EjRjum6Jtpfg7QQZz5wtDu+820ak7zhaMxB9lqBslJJOJ6vy29MB5TEJvlokZCSHldOBb6g8vvtkgD9KudlDt33o52iPj+pQIGJh7NguWtTWz7DXtw7aeJy8IP9lXtCX2hMn8TgqZwMkjQAlSeL95pJYyrrLuZ+QnhgiwgkJ8vDgEYQLG2wierHBpSA+tvILRWFRD3JtDpGX9euIGOOG1pk14yORS6kkv8C3d8ruBqmOCyc4nojY/jTVyOnJ1A2bDNC/dxI5bzUe84l6uokvMqVZk0GAcOZ5LPtVAhwBUAtarkrdCZFMJi/3KZv+PONhNrfNxxkGNrUfbMz683JcmQiX2qxNeLlfbgxBxRKFsr5p30FQ7jSlwMoJ3w1Qb4QHASNWrlQZFP1ER0EMw0HKe6A/G2c9G8NP9ZLnvVyExdd9TCRzEcqYEz3YG+Gl6v5cSpa8pm9/ncx/6DnGJeJx8B7nno8J1q4soNdQgwY3Zw59BtROP3gSw2nMRKTR6cO72tUflOXlNXH3Cjr3LpuSUW3EeNRFRCfJvpDJHQxpmGdntE2OaBR0lHuP1k9GqI65AKqQCqx9BQKg+2MfRNv8U0waFib5bwwRxNiAFhxQTWdlLtSUVwHgrpANGYU2t9AlwMCDPmykpan1NGXtI87w7NHkTliMoXoK+k1VrxZXtGpuTSjW7Vw8JP3DH4fXOdqlnGSyGqUpM1B9OZv6RUZ/0r4ojkdTRHuYqHSt60PFboxSs37lTRxBbZQD1nW2o4HS/B1yOpIuloW2XpfzOlv6+wHA4ZBTo434nNLsxziQnD4OFSgZi/T8J5sNY1+X34K0Krz71yDQ0wkwOl+BDd62roeaZtr6AUOh/X33Tvl2mvEk0gAbWgRbbX18fndUxo0nSQEMiZFF74GoMwdhG7E6bjXj3EDpWkOhfVPNw2Zn1Pw4StbfUnQdAlYpXPJjKaHmwhYjkxqQqoAWjzsxKLFPsYTXvOpZZpc8nKwkByhv7fhCLayrxTtK2kyWlj28cdD6NNQLYzkDFnmIu+Rpc0VfD9pCmB+xO9jZBrKFmuLe+rB0FGgdVDi2GckSApQ6q4PW8LBbM8rP/Nr9EthTIe2ZbOsCjsyLHmJO8vQTfe9fHCcISzIXFoHp+9Sg71oXJGXvb6M7BZzgNNjzkQBMa/+T3e8E2pQJjTdlSYIAft1LABkWTjedfNlNVEh9ZVyS+67icYvI8ZvgEfojdIFObB0cPvQ1YJq5o1Yb7TPxqF0Ic+iFwAWTma9xHnyBc/yJqJ476hqd56nEcGuN1MiHj88k+F/oRpwJYlzgQCjwPUgy9yTJkPbLPA58EhMRNXbF8LbUjpYwqApVjIAVyd6prspwwxMi5xZm8tswBnvdDcA8TUqJdbThuO5Q5Eci2TWwU6BI8+TCMJCIIK/8h1ztxCFLFi49rDAinlWNUMef+3c8Su49B3CVHXLugp6p02GpNoNrO8JpdpvngDuXJ2kuNooGCuC/NEqQ8A+zsDJSv2JC/+/UzVRg2lgOaDo/FHL6Jzco3EoeGiGCX6uMURCxiR35kh0U6GksFmPNnR9kfaEk2fazc26x/2TJj4zry7sMNTcRzvBtuU1f94Y57rVx9rvhaVYOOzBpnrfu5nd4AZq2PTonWL+3pUJFEuNg33lEsaoSVPPAliCKd3iLOsg79hv6Kv1/LnqpqewHfQDjmKJSSDSq5MwqAmetR7c7GDJ5A0ZE+wgT7l8BXnRbqg6p8R13Z7tAAzdelJJl0XmAqHlr8EvYforUQ7tyZAsKT5EBVvRSrQyesDP8Fj3KmvAoNeEowpbsVGPY7sKnDiITqD/4I324pGTDhty2hBghqFweNQvgfJqCU9ICD88biU59Rb6/MzjbYInoLyHMuwKpt/hugNm0TycEQfiG59tpTDUf1gfgoL7iFavQym4a28mq538k2dQPslpQRZCc6LEgHNpOjlZ+p3Ci4bNLBaHPJZNFJnWolk+SV8/yElVikmeWszpcydAvAs8fktIjThghUNcBV2rkPeN2Fk6ZnkOwhCXCDUaHEfm2ta9BqND+behzTAT1qBBeZcA8/yO0tNFalrlaUTQMfL34MBuHJhxJOhw5LPbhIF/eZAy4HvhFhcb3DkTw//x6nBYPDkElakGKEsvhw8TfEnzmiTtQ9QyQXRv3r6gEMb0FHAOtIcr/SMp6chJR6EAI+ZYA2IEpFSWuECcMRAaBx/+gVYbghcRLCJbXzvlJB2K4/+RYqpQF4q8/vFjsZxOiUyFCnix5gTQpiWpldrLjC2fwM5eMpXRHoJdC/5tt/vxWwRDled6nNkERv6Gf5nRklhlhzTbnPlYWlsr5HVRbNUkRtOzgC9J9nQQPWd71v/v+QjewxPu6LQWbfyOipfosjciBL5S5fBtGsBxSzUKIKwxWIwgCXSOio78YqcKhwTnURIMNr+am2QwkeQ5HCbqcDbyGwBCKEEdT9Nqckv4Jltr1gADQkUAfLHU7E27g9oW0nm3H/8E0nH7Xfp1dPSjasEDyYnUVH+YNhI+7uIKUlG5V2W6l7cOMIXb81lcTJmI3oahflTkY842vKpF3V+1UQQPqgrEyD3TQKm4QVY42ql3Cr3s1KyHwqM9GKfAFLDMmUOq0Ck55wqZNYubQ5e6iebLIrz3ManjeYuRQUdqyDUkVtAQgHRb4uobA57fLaVl9RP38ELTZ1+csRQiicXhABuuAzLk264em74zyDF6ZP7ZnNQRvVeeCra5SU/JhJcq5/3J2IAv7fFAOPjA/JrI0YPVY0ccv9losTDbylPNMv1eVVrQdzoVbyTKcW173WAa61AndL6dyGSKyuLj5v18y+3n0cPypO2q2oRl8S2rmaTEHtY4SpfH2o23oTblWNAzFADmmk+X54ft3U5WDyc7FPozIneEWXn7DmxNwm6UJPYaFVaH/GG5VmTcY0oYmSQvu3RW/lxq0DGaBfjgNRs3QdEJj7Zb0RmhQkwkS/Vjxa6Ylv7ukwByv/nH18WQVHqZzKnm1tMSpi5KQ5OWUGqaoJpa38lAzjgP65ZNQ7nJp7S3AHwDFkYMaNpLPVMnODcqoBE1nHWhnnFUMne9gyvEbtT5zxnvNRqk2v6jiz/K7Yi1dyr10u7VyMLHQTKG8kgvX+1ezVVben1KmCPOZteuv9fLjU2Fd+9hTd0zyzU+WrHSK3iD0Ts0K64LHPnuOTC2HFXtDP6MT8ZneBxGAZcpCPeavnAqDVyMSM7vVQGOihqBiVq8tg8UdvVusIRyFsAMZKi7ugewEX4CfBXROPYjxuvC/d/0xEaJg0oPk6XGAscDMzHGCJnDvwSgOWhP04jad7Y9UivJ3NFIIcJz018KliG6EpolyAGIPoxvLsezIjmEh88C/2cNOyjMUAOba005fdCh8doNAW6/qH9n94zE/1DyJqnHFP32tUF/QHQsXNS2i7XO95LHEkknuCtr2LttEu1WmwAc584WbqEF9WukEPPqE7qi5Kbd7xhM8dFJ+Fc1GROO6HmpKeWkqJUrouyk/7gz+TO1lbozkrlQU6HHCzD0tlkincE6Cn9FgcnZKMoKYDDLFowpQrvaFv2/M9vxGR+w2lunhUESklOf9qbBfv1/8DsbVsi8mi0NIiiA1EueAFypyLkFIJfM5x+SfNZ0Ilv0opO4HkoM0PgrvAHU5D1jjMhthAggiERYjIWuPFBXfz+1TqL5oNON7SNlN+QVV5vXZP+vKGU3M9wOOGikV41OJ5C/q3gWtrJec/lgzvmYWNDiKPxSMnVdvn2hSdb0Zg52S3Pu8p2ODPHtXZp7kqgzXLOe3Kzqd8GT/LqpOxR/NWwdZRvXuymilWoKRFlBOIjRPsqMXfrA7jQT7h6G0RDeV3+iThjzIbFq+IU0m3udKXj9ch1rUJoDP5aplGOcSIjP2/iKEbkdC9GpKwkCBW8ZrO4ug2mRc1DqCmTIy4Xu3ZIASDfF/sQTiG68cijZVFz2bKhHrIWi61EywuTYjQ2sR6c5p2ZmUWl2SJcGXuxof0hihmE7ocIq98+j2TKlVZS+SOhsG1ESi4ZgNn512ryJedrxVgT6Acslj/SX/KsuFfZcCdm5l6RD+IaioYdT25E2gUQLvp9VijEFuoqWtPmmtyN7oJtiDwO+AJ4T2AX1VlRBGsIqNcMcetDmanBZ6dRpJhu4C0u5fkXDA6b9wpnSMmJSu8QT0cE4NjmvcMNMSz3k1rASDFX1+lq7w8uk8YJCFBu3r38+OYVaIdNLgUkhIfzlPVO0YLgFj1x9KR7tMblOrEe3ataae5ciYgFFRiBn+sSR3hcNxc8099PEZCJJROVccz1yvQKv/1fGQFYNK+UgbQ1Dug7AU12XvYVXnC/quXIzcdHXiePK7xJDMuMUY3hFuqTNXhvSBJ0FLbw+1Ce8hZhNcRTl+Z+TxNM305/L5UhnB7EO8azGdodQJWQZk/A7L7eyYHuLr90U5kKVIhFfacq8HCsU3BXlMgf0aTrD6aeowRUkG99ubHVw5jr6NptWEalwtpB5CTDavUoEbxEEBos2lz4olQQ7Em1V3uHIzQEjH2SXbfAlqq9xgdCsRB2FH3cbg+7DZeopN94osbTzJnz205WGXRSlms2lzvfSq+bRIa3dymCWXZ0PxNcUe4BDl9PE7aZTTKvSFKSaogx2D8pXvM8yW/LsPFZZGrw3nKT+7DuRBh1OU17RJHW4Nd9ivZmm3Ndy3uXdPmYJyJzKJ6K7x2AukAo0MJn3FRNbT+p63CkY0SLkY4wTM2HUYjSsMOh7Of1p/ZwvknHImcR4EwyzmvZBSnyp8VnaJI2JwjJOsIMPXWWz5/+49TsR/4nsVRCNLp9FBK6ODuZsvkA8cs0Mt27iXWyWFmK6RoqJR2chZHXrZDKWWJJiJZg76uUyBbBaTSRcpFgH2rePIJznlYRrTUHEC0D1QQtHKwzwd+ZYLgYwXBZ09hFpM36Nj5sZLQFhWQ+I82RFgquv56lRXdWjdd0/mgSXk8hCP/gizCaKVzD4+OXzLiu0zoaMU3es6pcdGPvN1IywD7mnbkEYKnh4JIvwccJwcsxwxCOhGRdZZQWC6ozS2lUqH3mpCZUM08d9Fkkm7o13a682Eyj/XZ2o1GuKpR/i03pU1AAieuTDCy19BFKndMeCreN8L+3+WZg8xbi96jM1ZRMVr12TUt79s3SeRgMqzmGwfb2wecNWG0plc4ksOUBfPUHlCWktPMwlz5rkAN+CrUPqcFV2Bfd8WVKIvF9TwC+SYQ9HrRwQDWloPbFW0lkL36Ci9AYnOhm6P3WLBxf0q6XeN5RHdM+XmRQtXDkJ6da07xTXs8BmcDkOA723bpXdL8BC6xenR0Pm3iKh3F4MCJ6LHv13pBTfxJhJPwYfuGzXvOeypFHk1toWR66Wo/NFYaWGSZ+0prI8MZBoGPGprQuzdmIxvA5Y+ZUWagIKFigbu+YnuZpxnh+pTUCvdzPVwFsOOk6uEcMeKVO7VrbQ4TleYJc7i3lswu6MaHxvJU5mFES7aHrPfdWRjBbjD1v4OHX1VgT73B2N+LuWLcSVC5hToTWvjVCBmh++BYF9LTK0iE1l6Lual6hn96za+3Z5MSv3VDWgNiCshuX3TM40qu3CKEWAwA82kDPtAzNl1YTrDeZFfgygMI4nv+DwVhGR/YnOxOCaNvJDnFTx9Bxpy/Jo5gfxGbeXdRWngECWaYMXG1aggzcK4vUdEzS6kUm5ZnB77d88Bg2sx03Si5QzSwRrfEPNtqSafH3QqYxmS2h0UJZbWA8+mCwaHdtckIWj08VT6Rjd6IgDJebArkNfI4aKHHCg6ZQYzlTGMKK615XJoHDa7XIruUWYMovvsVAZBoYIH6s/BxUz1I9W16Iqi5nX4RCh1l9sofeuJn9t8vQMpaNiqj3eCHTUGdW8flbBW9qwN3ib0MNglE4MJyjO8RZAFNkrqFSK3Tn4qSKl+QvCa/ujvK656BM6LlawCUT4BUU8iHM7MdqHhNtWjWTNP9mfU0X+w7tie0SmRW8B3eI42PfwWowJx8krJX5LtwDMGE5nIROyXBIMai7zmL21LD54SbyHMUoyQqFx3/r+dSmRClpeeDIjSDn09U276628cW3RDkYm7BmzxO/XKJnhra2rc2XPdsaf4lx8ZO++yRCrAswcebp828rsLruzaEQTylt0m8NxYHjR1GaCI4tZsNoh4nH6dtsvprPhq8iS4NhkG++51dWJR/pep1SQQ1fTrTH2ApbwXCK4/1FrDW/tDXp4IzikL97oDnSX1CjqBq4chXqeLSja3a1Ci1fQ0dc8yArSPlmHAWISDIjODZKpzkU1bp8/szmMkHygiixmIWhKnEEEeAZRQyiDdEAcZSg37wnrpoiHcjk1Vn+V7DddZ8Rz8baouQj11Dp85hEojQVZJH2k7m1Y6liK1JHwlzrlPhkIPBXl4ius0LIr1Z+gi6/DmxKRmb/Syg5v93mLfxsaQgTRkFpBWYNmTdB381SifudiHM12Ta3azDDvBWVxWwV+xuO/Ep14Aq3B5+rc9kfvCH8L6q2A93Xd8zRawhEgFvPU3xyp+F/lms1BUjbb60u1k/V9t+h/owyJWfvL2zesf9j7yerf6mpKldwKb1ndUvlezcXna0XBGeK68ngYGr6VLyZANnpkS4UKHUYzDBV80nUt3K5f8YXI7lqKqTAqbfMxIAyop8QLNJ3b0n0kkrRjORbnbI1q4pkf8XACL7IXIiG1Cqg/CeZ/YB4WX1Ajf8xKrG9XQgI5UVX3fTNrj1ljh4mTwkWldpJzyqU5U/kVNP7ZI9PvNp7IbwOVTmHr7KNNSrRRSWRFEKqq9gYS6bL2YtNtFdvaYK7BW2ssqGv4uQcxM9JKOqS9BZCBFOn0t1QMdynNn1bzfHOl5hgGjNg7Qri30kZi2sOHMtYc2azTCYl4HLescAob4LZA2O6+wCh60tmQI4V7JaW4d8tH/FKT+j2lmZsotxsRwXtpd3moOWWnZ49X6l6a9kNXRPFUh3qW7mkosk5m2zpE51/sthM5eEilDyZcXOmhb/G2XObgXtdjeYzoC6DZZOc9SIidoYgmupLEGpACPHekqraFoMVSDK+b9HzdtjS7KzILYg3DHaueg2j87BOYCWqLTJz447J2jG3o9afeaUoUYcHR9FD/k7oX7EnFyAdeTzIF4q1wiR3jAO0ypm63H+4bQaqqZs0XF0xjaE8+ZPRHNoZofBC57Lcw5zAvx7Xp2qSQDHIXADmMXAFj3I2/3YcTtUTapStAjKZeckwa1s+sLJcGscqrv4U6o+dDIgFIwr9xaVbbnwVPFey50lBLkC1do9NxNDT7bR4Dn3n0RNMiEDv6krIYtEYhn888kX/mdGx//PjFgj1e96PjWKZl3hPbcFHhyDaydE0yrtxzTgrFOZSbk4q7lSeCp3q4ynk0ASHcEcKPuBqTafSY1lXdAlHU63Z5fu0h/5u1Uib34rkwDGXZZvPNYNdJcCvas4BPC2kiL3ZUR70BClIWMYrGbGiBUks9unwYAIMZ4xmQ8Joq1fZrKQ/UCQ36A3yTxMQMaw30PCR4xVUyKFG+FpyZQcSFNxV3fkemSwLCQ39fGlFoxq0gy2DnnveJy14FpAMi1+j43ZSz+mXXTPDqCaepEU0dhOWJI0kUX05+F0vL0Z4/INE/JN6ZOiHECr/2w0Gdkxh7IoBHhMhe7FhFOwyxpFvMvtyohIcnnY2BywVrHBWRjAjpPLoJ8z1k+Ce0kg/8M4WbIAXSSS9knlPJlhIr41NL+gTEJxyOth7EKBDwkCORSS13FFPst6U1E8CiT3deL0fB8Z6EPBcGQDmlQvzvkBEsXsV9iznVJLZTWFLmxP8OkRfCCBxOjGJL9FIbGGmD1lyNpH9rRGnfVgNlNxtGDkc5IiDOCfZ7oifO4+1pUKxVpxcRHka6+cVn6i4mUx3e6Y2rtaf5B1O431ByA54vg8MuxqGU+oCIqO/pHS3pOioWAly4iyK9pnTrxQ+gQtCZL/iFRBIJAOJV9/QNLQzZJ/f5wjXjY4NqilrYaxCdEsJUkYyuS4gn7a/S7LuGTyH70BdAy2XnwVqoS2WpHiq0q6dET8dt7OPKf2z//TfBN/93XGfi5aGmvg/urLZQQMIzYeiUPZop0HmYtfKfo0yvN66bxt8ch+QB1yIeTNpBc9erMt1KkIOmrL51QlJw1ufPanzkiMqbjsaLcZ9MSgVPTgCNJWmr+XKLM65h5V+hnC3mdyFfEQmtPm7BqiAw6mntQ9xauLc96evVd3bYhKEC6+6EKXHws76CJessnvEqkoXlkIpiYSAHr4DFPnwWPoLMJiYDApGFN4ChQD1F1NTS4bm/IolHNbs2mbvmnjDVL1lLwLSPVJmXU7eTvhc5q08H8fyv1EC+Laqtc7TEk8SBGvuzLVPEgnMh3otm/ihujuPxrJHkYrD+UEiDh57Kumxt9zhQn4VJaMQvbVuZ5DKH76TihmpMMTZOXmP0Ao1ES11urn+QfuYtB8bmX/0bBAx0jQiLbuJxVV61sCm+ob+oT5/rDj8QOhttqUnQHbC0ZrtQRHxVxCs+BKnU49tp5tLea4sSEbAlMw75z3PmT97e51rncE47+qfK+l5VPtKMv/pu/UHeDQIL9zY3J61DJbwaDQrGW9UaGc/mvx+lQ6VSaudHc8CJ9nG8t+nZ0wNz+9czs9MLSptD1utCIt/yuDcVPja+5kY4r/FGSpbRkrDIuwzoCDVcMtsKGKVggdw9a/GfCmmhS+3ZrI3tYj2k98NO5tRymqdgGaTazthcaZmFfQ03Mj5dXEud84m5URx99HBu5eeg4P/rWzHgnmDvHvUuwmwxVcltYzWrxyb6MGBylTnxESrj1WPp7UJUirWqqf4fOK7qE0wLVvmIJOIceqxmX+n7zWNvNnoCoa7HUF3DDwOJLYuER4p2ZUoJtxYnPykdvWgkSLDiT4XYMcBq1GzzhISswNsTDiim/hB9pqF6p62kUTZSdjuVaYcLA65LCjo2shnkAya8QaZ/9y7LaSNKeeyUl+3feZnM9oMDqCF/v5gFppk/le3gn8jvR1Hkajda6hF1lQ6HxGvX6aeytF25Z7ywB4Bch3fK6JbfQhOZaEJZOssaAuzg6lzKPAhkHrsRW43sUtVF6y8kajuIv1Ajm0K70pzJOC9DaCCNtXJfzGmqwZ+DjS0bymXLty025P9dgm0MIVpxmR0fqXkQfXzeHoL1EWEJJIQW+ubs2mXQTNoyvOlbog4yh15h3SKIMlVTyVojVIG7qmlA96XWMqG0gvduKFuTcDQ2pGkYVxfsDbaYrSdXfm/IP708tEORvF/PmhDSWQkoKZT1LEGN3CMRlKV7pARMzeO3Csy3I6KxOWwtAPRKreyUFUEbYQ5ulhkgaIkfrNlwwSkceJcO97Ms5+VOhY1H386rPZ6cbwHf+60idxgZVgj0XQNe7u+nQbEmECXHhzbVCopGPcFzPHlRxr0vISJNDjb+6j4Nx//8nUZDx1IchOv1o0lT+0vuXlAK+B/sBPlCYmKliBTLYsGwbIV5QihrgwcuL01b28KHwqZsgkYD37n3dh35uR4I/xTNf/bgDKTml/7p3dvYSaJvzjIBlAsQ8hdhrJhNF6Co91q93Pizil9CiO+d+7X8oXrVOhMLkQrZZB651kGzXp1WaFpnw3/01hqjkIHeqgHzopqD973qe74VG4jNr7Wjcm+OGXi6Tvtl4X7Mn2su6i6dnUSr4k+O9e1Tbq+tgum2622d2Gpl1OJZjyT3FdDjCTwGElGgnJqF3cBnZKSkvvFUdd+vV9I4tXEXMfcChdyqx1tpJg5mBGs993U1Wr5DS9L7teNUyfzYN75BnuPQPYwfncH20kIZhwZcYBKZVm7Gzdrv+ssB8gDaVcMw9qYUn9IWWNQRujFf6ZSGWywjQA/qlyWpnV1UBwv7y/dq6Vam5aiWL1EaJgqjWZKIxg4HNkiM9dl9KJBkApodnzFvNGMlPGive0IE1h8X6IP4WsYuYNQutYzlyQxqEmaxfYotR3xVHUaG0kd3kUOJsJu06HYii08CwmaAHbRcFkz7GV7BChBjZXKb2trzvb1e62HdmetBxjtjGF0wwfirI4NsDU9tefo64+C4grGSGRNvgzaiijUuG1TeJFVEPwdhazfHBXs+KmHuDSPZm5tpJXVIj29oFFcwKiMmAh7OukqMvS4bvjYSqA0S6YvvrBSRvbwJaNsem4FlniE8vjWlGwKza7kr8MsXH+iGO0N+fyr+zjhx1XWBIREMRwNNLjvPTH3fn4oa047q/aJCNGqRvNtX/EtjBl3MwmscAkSTI9wHYqrs+hAaEXl0dff0mkvjQNNV+sjC//iKHtlJMCNd09/yrqZWYdnAnilO2RksjY9vXOrb1/WvEudQw9OHErXg2hrMmX2eLl2lD4Mxtwyrt+BhErRYILDeXp/OAOtHRv1L/YxGJ8/p8mgtN0AJBF0IaNMx5HUbggQauRTNlmKZWQkpVGS/3JF1kZs1Y84ydKhqSQ3C6IcRbiXybLY0Bh4bD/7xaKO9LiDenhr3EfAduIJE874XrVMDiTdFFq2tYd30AQ5z6XmaIpk5G1zQYOBXimlVIfdmCJv8shlJhvB/SgF3fWc22tWp/L99TXMovfZCQ0ztfnrvcRD8x2rijoHwvYpZNOh1AGFeqczHjTCqKgpTZvr1rVh03DpAzT9Q6d71sMhcA9scav4U6EEbKaN8efrShHnBAkNOkAeyDTMshcRq+jHoqwj5e5XrZ9UeO1I84S+pTbwycGiOWah5BMrrpGGslhA9V89U11B6bP+x9dGCsQaNro3y8IirzN7F7nNfgYI3C0Qga88DvM1cBKr+zGuZVJ2EAWazIdDjIQDzGBamvEmAKmTvOqwYAARgrpwx7MntaWznva+3c7IyI1hxPt77Z16dK+s5AnrD7xKq80YQ6pvWl8BCTfMKlVNT+NoYyWzK8DIquqlBqyA94VxkkKzATDinjWQ6zg43eQFNOzXjHJFXuqOBdVustHHz9L589dmgHFU+wZb/tsG/UrYUHdkZrL6tK1djZnyzQC1KSKhsRef3BJ7AulvBN8bmssnRaUWJVjOf8SaU60VDA6tKmWEMQXtl6ddvcSC70zpX7gXvFXtNVO1mNDBmg9ozg5a+OCYNh3RTA98suXAu+saEaSknhBzCuGdPjx4AtLkn7slelQV+j4pqxryegve11GLIf1ynr0eGL1++AGaSiwa4HytILW+XDHgYTAFnqae5BOl7sl7GpdzvxoVczuPcHFdk8Ax1DctrgNJvPASDxeT39XjQc7LXk4zxaTguwg34tNntNFZFaDU4JQcwy4wJxxhx+X0U7Oh+Vgn1fKsAHTT7BZ9nRuzPwsXWoMgCCr7cO5gR7DWlo/U8wAeJxUCw0W3kNNHvSworFWBkYysxw5YjZWESIHZoZTsFAVxfmx7tVz8LK2B4f2EubdI4yhBIOLHyC2RAJj1qyK4NJQkuX6vMR6O1Yx9qOPiqD7k1H0mIrjDalHP8tO90A6GHgxk9kDVbNtC1Yoe2AaDxDw0fJHHg6ssOrF/dia5meRDwW87DubTf/ikvejoG2vEYZEZBqv4PXGTaJzY4CVIoqkLm/hDSBj0BpTyflJWH+vyYeJlroXaqc7qJx6i0n3r51SNpz955u5NVpMfqw+80FXDvSLZxPFp0j2cp1yczpVIMk03fMxaD3eOgtlK0akP4VOzL3OQvOlBtkVoIa48RRgQXSgn2rVl67DlsQR8i///6mOUiHXcreNmHkArI4byOuS/LCxC1iT88Cxx9tOofHyTjlK6Wl5lSy5iz+yr1VgiiQZ6hQPX1SzfOJDAscHB9jq2vKPZgrfGFiZXzisATzDXQc2A6AAOyKkm9Ynkfd0i5ULMoQM0GvkU5uxM+sm5TcMu0/rRtc5JkPTSPtGlPrlQZtahfyg1ZD6aIfXIiS6Am02xiQ1gOqXgB6XUS8MrBl0U2lDdc1vZz99wAQLK9zpIFWK43E2COFUntvC2uVUHj/OkPiIwTjfiYQxaxx7ttlHlgRVRPSgF9D9INNY7hWeCOmAPnY6kiFDnT5Xy9VPshemLn9kYOIpaafFazzzAjT4eBnK5tWUMYYCc5FAYI9vDuM2WQhZR3N/4ad/9B314ezCQ9s0kYiaN6o+V0x8sBPbbQ==\"}" } \ No newline at end of file diff --git a/backend/src/db/api/bookings.js b/backend/src/db/api/bookings.js index 9cb55a0..0cea0a1 100644 --- a/backend/src/db/api/bookings.js +++ b/backend/src/db/api/bookings.js @@ -169,6 +169,10 @@ module.exports = class BookingsDBApi { transaction, }); + output.redemption_booking = await bookings.getRedemption_booking({ + transaction, + }); + output.customer = await bookings.getCustomer({ transaction, }); diff --git a/backend/src/db/api/loyaltytier.js b/backend/src/db/api/loyaltytier.js new file mode 100644 index 0000000..c0d966a --- /dev/null +++ b/backend/src/db/api/loyaltytier.js @@ -0,0 +1,330 @@ +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 LoyaltytierDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.create( + { + id: data.id || undefined, + + name: data.name || null, + annualfee: data.annualfee || null, + pointspersar: data.pointspersar || null, + description: data.description || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return loyaltytier; + } + + 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 loyaltytierData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + annualfee: item.annualfee || null, + pointspersar: item.pointspersar || null, + description: item.description || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const loyaltytier = await db.loyaltytier.bulkCreate(loyaltytierData, { + transaction, + }); + + // For each item created, replace relation files + + return loyaltytier; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.annualfee !== undefined) updatePayload.annualfee = data.annualfee; + + if (data.pointspersar !== undefined) + updatePayload.pointspersar = data.pointspersar; + + if (data.description !== undefined) + updatePayload.description = data.description; + + updatePayload.updatedById = currentUser.id; + + await loyaltytier.update(updatePayload, { transaction }); + + return loyaltytier; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of loyaltytier) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of loyaltytier) { + await record.destroy({ transaction }); + } + }); + + return loyaltytier; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findByPk(id, options); + + await loyaltytier.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await loyaltytier.destroy({ + transaction, + }); + + return loyaltytier; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findOne( + { where }, + { transaction }, + ); + + if (!loyaltytier) { + return loyaltytier; + } + + const output = loyaltytier.get({ plain: true }); + + output.users_loyaltytier = await loyaltytier.getUsers_loyaltytier({ + 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('loyaltytier', 'name', filter.name), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loyaltytier', + 'description', + filter.description, + ), + }; + } + + if (filter.annualfeeRange) { + const [start, end] = filter.annualfeeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + annualfee: { + ...where.annualfee, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + annualfee: { + ...where.annualfee, + [Op.lte]: end, + }, + }; + } + } + + if (filter.pointspersarRange) { + const [start, end] = filter.pointspersarRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pointspersar: { + ...where.pointspersar, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pointspersar: { + ...where.pointspersar, + [Op.lte]: end, + }, + }; + } + } + + 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.loyaltytier.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('loyaltytier', 'name', query), + ], + }; + } + + const records = await db.loyaltytier.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/redemption.js b/backend/src/db/api/redemption.js new file mode 100644 index 0000000..e23b84c --- /dev/null +++ b/backend/src/db/api/redemption.js @@ -0,0 +1,402 @@ +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 RedemptionDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.create( + { + id: data.id || undefined, + + pointsused: data.pointsused || null, + redemptiontype: data.redemptiontype || null, + discountvalue: data.discountvalue || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await redemption.setUser(data.user || null, { + transaction, + }); + + await redemption.setBooking(data.booking || null, { + transaction, + }); + + return redemption; + } + + 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 redemptionData = data.map((item, index) => ({ + id: item.id || undefined, + + pointsused: item.pointsused || null, + redemptiontype: item.redemptiontype || null, + discountvalue: item.discountvalue || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const redemption = await db.redemption.bulkCreate(redemptionData, { + transaction, + }); + + // For each item created, replace relation files + + return redemption; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.pointsused !== undefined) + updatePayload.pointsused = data.pointsused; + + if (data.redemptiontype !== undefined) + updatePayload.redemptiontype = data.redemptiontype; + + if (data.discountvalue !== undefined) + updatePayload.discountvalue = data.discountvalue; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await redemption.update(updatePayload, { transaction }); + + if (data.user !== undefined) { + await redemption.setUser( + data.user, + + { transaction }, + ); + } + + if (data.booking !== undefined) { + await redemption.setBooking( + data.booking, + + { transaction }, + ); + } + + return redemption; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of redemption) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of redemption) { + await record.destroy({ transaction }); + } + }); + + return redemption; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findByPk(id, options); + + await redemption.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await redemption.destroy({ + transaction, + }); + + return redemption; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findOne({ where }, { transaction }); + + if (!redemption) { + return redemption; + } + + const output = redemption.get({ plain: true }); + + output.user = await redemption.getUser({ + transaction, + }); + + output.booking = await redemption.getBooking({ + 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 = [ + { + model: db.users, + as: 'user', + + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.bookings, + as: 'booking', + + where: filter.booking + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.booking + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + service_type: { + [Op.or]: filter.booking + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.pointsusedRange) { + const [start, end] = filter.pointsusedRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pointsused: { + ...where.pointsused, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pointsused: { + ...where.pointsused, + [Op.lte]: end, + }, + }; + } + } + + if (filter.discountvalueRange) { + const [start, end] = filter.discountvalueRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + discountvalue: { + ...where.discountvalue, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + discountvalue: { + ...where.discountvalue, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.redemptiontype) { + where = { + ...where, + redemptiontype: filter.redemptiontype, + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + 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.redemption.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('redemption', 'id', query), + ], + }; + } + + const records = await db.redemption.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/referral.js b/backend/src/db/api/referral.js new file mode 100644 index 0000000..cf5ac51 --- /dev/null +++ b/backend/src/db/api/referral.js @@ -0,0 +1,306 @@ +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 ReferralDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.create( + { + id: data.id || undefined, + + referredemail: data.referredemail || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await referral.setReferrer(data.referrer || null, { + transaction, + }); + + return referral; + } + + 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 referralData = data.map((item, index) => ({ + id: item.id || undefined, + + referredemail: item.referredemail || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const referral = await db.referral.bulkCreate(referralData, { + transaction, + }); + + // For each item created, replace relation files + + return referral; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.referredemail !== undefined) + updatePayload.referredemail = data.referredemail; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await referral.update(updatePayload, { transaction }); + + if (data.referrer !== undefined) { + await referral.setReferrer( + data.referrer, + + { transaction }, + ); + } + + return referral; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of referral) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of referral) { + await record.destroy({ transaction }); + } + }); + + return referral; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findByPk(id, options); + + await referral.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await referral.destroy({ + transaction, + }); + + return referral; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findOne({ where }, { transaction }); + + if (!referral) { + return referral; + } + + const output = referral.get({ plain: true }); + + output.referrer = await referral.getReferrer({ + 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 = [ + { + model: db.users, + as: 'referrer', + + where: filter.referrer + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.referrer + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.referrer + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.referredemail) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'referral', + 'referredemail', + filter.referredemail, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + 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.referral.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('referral', 'id', query), + ], + }; + } + + const records = await db.referral.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/users.js b/backend/src/db/api/users.js index 3a11e69..d7e8580 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -34,6 +34,8 @@ module.exports = class UsersDBApi { passwordResetTokenExpiresAt: data.data.passwordResetTokenExpiresAt || null, provider: data.data.provider || null, + pointsbalance: data.data.pointsbalance || null, + referralcode: data.data.referralcode || null, importHash: data.data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -56,6 +58,10 @@ module.exports = class UsersDBApi { }); } + await users.setLoyaltytier(data.data.loyaltytier || null, { + transaction, + }); + await users.setCustom_permissions(data.data.custom_permissions || [], { transaction, }); @@ -96,6 +102,8 @@ module.exports = class UsersDBApi { passwordResetToken: item.passwordResetToken || null, passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null, provider: item.provider || null, + pointsbalance: item.pointsbalance || null, + referralcode: item.referralcode || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -178,6 +186,12 @@ module.exports = class UsersDBApi { if (data.provider !== undefined) updatePayload.provider = data.provider; + if (data.pointsbalance !== undefined) + updatePayload.pointsbalance = data.pointsbalance; + + if (data.referralcode !== undefined) + updatePayload.referralcode = data.referralcode; + updatePayload.updatedById = currentUser.id; await users.update(updatePayload, { transaction }); @@ -190,6 +204,14 @@ module.exports = class UsersDBApi { ); } + if (data.loyaltytier !== undefined) { + await users.setLoyaltytier( + data.loyaltytier, + + { transaction }, + ); + } + if (data.custom_permissions !== undefined) { await users.setCustom_permissions(data.custom_permissions, { transaction, @@ -267,6 +289,14 @@ module.exports = class UsersDBApi { const output = users.get({ plain: true }); + output.referral_referrer = await users.getReferral_referrer({ + transaction, + }); + + output.redemption_user = await users.getRedemption_user({ + transaction, + }); + output.avatar = await users.getAvatar({ transaction, }); @@ -285,6 +315,10 @@ module.exports = class UsersDBApi { transaction, }); + output.loyaltytier = await users.getLoyaltytier({ + transaction, + }); + return output; } @@ -327,6 +361,32 @@ module.exports = class UsersDBApi { : {}, }, + { + model: db.loyaltytier, + as: 'loyaltytier', + + where: filter.loyaltytier + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.loyaltytier + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.loyaltytier + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + { model: db.permissions, as: 'custom_permissions', @@ -411,6 +471,13 @@ module.exports = class UsersDBApi { }; } + if (filter.referralcode) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'referralcode', filter.referralcode), + }; + } + if (filter.emailVerificationTokenExpiresAtRange) { const [start, end] = filter.emailVerificationTokenExpiresAtRange; @@ -459,6 +526,30 @@ module.exports = class UsersDBApi { } } + if (filter.pointsbalanceRange) { + const [start, end] = filter.pointsbalanceRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pointsbalance: { + ...where.pointsbalance, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pointsbalance: { + ...where.pointsbalance, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/migrations/1748078864723.js b/backend/src/db/migrations/1748078864723.js new file mode 100644 index 0000000..8d15a81 --- /dev/null +++ b/backend/src/db/migrations/1748078864723.js @@ -0,0 +1,72 @@ +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( + 'loyaltytier', + { + 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 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.dropTable('loyaltytier', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748078906634.js b/backend/src/db/migrations/1748078906634.js new file mode 100644 index 0000000..513f6a5 --- /dev/null +++ b/backend/src/db/migrations/1748078906634.js @@ -0,0 +1,47 @@ +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.addColumn( + 'loyaltytier', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { 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('loyaltytier', 'name', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748078935638.js b/backend/src/db/migrations/1748078935638.js new file mode 100644 index 0000000..a84af4c --- /dev/null +++ b/backend/src/db/migrations/1748078935638.js @@ -0,0 +1,49 @@ +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.addColumn( + 'loyaltytier', + 'annualfee', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { 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('loyaltytier', 'annualfee', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748078966955.js b/backend/src/db/migrations/1748078966955.js new file mode 100644 index 0000000..5241419 --- /dev/null +++ b/backend/src/db/migrations/1748078966955.js @@ -0,0 +1,49 @@ +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.addColumn( + 'loyaltytier', + 'pointspersar', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { 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('loyaltytier', 'pointspersar', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079001996.js b/backend/src/db/migrations/1748079001996.js new file mode 100644 index 0000000..0a09ff8 --- /dev/null +++ b/backend/src/db/migrations/1748079001996.js @@ -0,0 +1,49 @@ +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.addColumn( + 'loyaltytier', + 'description', + { + type: Sequelize.DataTypes.TEXT, + }, + { 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('loyaltytier', 'description', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079044910.js b/backend/src/db/migrations/1748079044910.js new file mode 100644 index 0000000..72ff531 --- /dev/null +++ b/backend/src/db/migrations/1748079044910.js @@ -0,0 +1,54 @@ +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.addColumn( + 'users', + 'loyaltytierId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'loyaltytier', + key: 'id', + }, + }, + { 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('users', 'loyaltytierId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079122373.js b/backend/src/db/migrations/1748079122373.js new file mode 100644 index 0000000..ea9264b --- /dev/null +++ b/backend/src/db/migrations/1748079122373.js @@ -0,0 +1,49 @@ +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.addColumn( + 'users', + 'pointsbalance', + { + type: Sequelize.DataTypes.INTEGER, + }, + { 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('users', 'pointsbalance', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079157620.js b/backend/src/db/migrations/1748079157620.js new file mode 100644 index 0000000..9fc48dc --- /dev/null +++ b/backend/src/db/migrations/1748079157620.js @@ -0,0 +1,49 @@ +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.addColumn( + 'users', + 'referralcode', + { + type: Sequelize.DataTypes.TEXT, + }, + { 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('users', 'referralcode', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079188113.js b/backend/src/db/migrations/1748079188113.js new file mode 100644 index 0000000..74b0a70 --- /dev/null +++ b/backend/src/db/migrations/1748079188113.js @@ -0,0 +1,72 @@ +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( + 'referral', + { + 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 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.dropTable('referral', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079231733.js b/backend/src/db/migrations/1748079231733.js new file mode 100644 index 0000000..2759bff --- /dev/null +++ b/backend/src/db/migrations/1748079231733.js @@ -0,0 +1,54 @@ +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.addColumn( + 'referral', + 'referrerId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + }, + { 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('referral', 'referrerId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079291100.js b/backend/src/db/migrations/1748079291100.js new file mode 100644 index 0000000..811e528 --- /dev/null +++ b/backend/src/db/migrations/1748079291100.js @@ -0,0 +1,49 @@ +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.addColumn( + 'referral', + 'referredemail', + { + type: Sequelize.DataTypes.TEXT, + }, + { 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('referral', 'referredemail', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079326438.js b/backend/src/db/migrations/1748079326438.js new file mode 100644 index 0000000..a824747 --- /dev/null +++ b/backend/src/db/migrations/1748079326438.js @@ -0,0 +1,49 @@ +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.addColumn( + 'referral', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + }, + { 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('referral', 'status', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079355731.js b/backend/src/db/migrations/1748079355731.js new file mode 100644 index 0000000..7823ac4 --- /dev/null +++ b/backend/src/db/migrations/1748079355731.js @@ -0,0 +1,72 @@ +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( + 'redemption', + { + 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 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.dropTable('redemption', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079386360.js b/backend/src/db/migrations/1748079386360.js new file mode 100644 index 0000000..34e2297 --- /dev/null +++ b/backend/src/db/migrations/1748079386360.js @@ -0,0 +1,54 @@ +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.addColumn( + 'redemption', + 'userId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + }, + { 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('redemption', 'userId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079423627.js b/backend/src/db/migrations/1748079423627.js new file mode 100644 index 0000000..b9baad9 --- /dev/null +++ b/backend/src/db/migrations/1748079423627.js @@ -0,0 +1,49 @@ +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.addColumn( + 'redemption', + 'pointsused', + { + type: Sequelize.DataTypes.INTEGER, + }, + { 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('redemption', 'pointsused', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079463221.js b/backend/src/db/migrations/1748079463221.js new file mode 100644 index 0000000..39fa979 --- /dev/null +++ b/backend/src/db/migrations/1748079463221.js @@ -0,0 +1,51 @@ +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.addColumn( + 'redemption', + 'redemptiontype', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + }, + { 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('redemption', 'redemptiontype', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079630221.js b/backend/src/db/migrations/1748079630221.js new file mode 100644 index 0000000..a01ad58 --- /dev/null +++ b/backend/src/db/migrations/1748079630221.js @@ -0,0 +1,49 @@ +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.addColumn( + 'redemption', + 'discountvalue', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { 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('redemption', 'discountvalue', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079688214.js b/backend/src/db/migrations/1748079688214.js new file mode 100644 index 0000000..e47c8f8 --- /dev/null +++ b/backend/src/db/migrations/1748079688214.js @@ -0,0 +1,51 @@ +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.addColumn( + 'redemption', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + }, + { 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('redemption', 'status', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079723177.js b/backend/src/db/migrations/1748079723177.js new file mode 100644 index 0000000..1ecb17b --- /dev/null +++ b/backend/src/db/migrations/1748079723177.js @@ -0,0 +1,54 @@ +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.addColumn( + 'redemption', + 'bookingId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'bookings', + key: 'id', + }, + }, + { 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('redemption', 'bookingId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/bookings.js b/backend/src/db/models/bookings.js index 1851ceb..247adf6 100644 --- a/backend/src/db/models/bookings.js +++ b/backend/src/db/models/bookings.js @@ -60,6 +60,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.bookings.hasMany(db.redemption, { + as: 'redemption_booking', + foreignKey: { + name: 'bookingId', + }, + constraints: false, + }); + //end loop db.bookings.belongsTo(db.customers, { diff --git a/backend/src/db/models/loyaltytier.js b/backend/src/db/models/loyaltytier.js new file mode 100644 index 0000000..2087e9e --- /dev/null +++ b/backend/src/db/models/loyaltytier.js @@ -0,0 +1,69 @@ +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 loyaltytier = sequelize.define( + 'loyaltytier', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + annualfee: { + type: DataTypes.DECIMAL, + }, + + pointspersar: { + type: DataTypes.DECIMAL, + }, + + description: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + loyaltytier.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.loyaltytier.hasMany(db.users, { + as: 'users_loyaltytier', + foreignKey: { + name: 'loyaltytierId', + }, + constraints: false, + }); + + //end loop + + db.loyaltytier.belongsTo(db.users, { + as: 'createdBy', + }); + + db.loyaltytier.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return loyaltytier; +}; diff --git a/backend/src/db/models/redemption.js b/backend/src/db/models/redemption.js new file mode 100644 index 0000000..fb3c033 --- /dev/null +++ b/backend/src/db/models/redemption.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 redemption = sequelize.define( + 'redemption', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + pointsused: { + type: DataTypes.INTEGER, + }, + + redemptiontype: { + type: DataTypes.ENUM, + + values: ['value'], + }, + + discountvalue: { + type: DataTypes.DECIMAL, + }, + + status: { + type: DataTypes.ENUM, + + values: ['value'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + redemption.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.redemption.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.redemption.belongsTo(db.bookings, { + as: 'booking', + foreignKey: { + name: 'bookingId', + }, + constraints: false, + }); + + db.redemption.belongsTo(db.users, { + as: 'createdBy', + }); + + db.redemption.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return redemption; +}; diff --git a/backend/src/db/models/referral.js b/backend/src/db/models/referral.js new file mode 100644 index 0000000..3cfd722 --- /dev/null +++ b/backend/src/db/models/referral.js @@ -0,0 +1,63 @@ +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 referral = sequelize.define( + 'referral', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + referredemail: { + type: DataTypes.TEXT, + }, + + status: { + type: DataTypes.ENUM, + + values: ['value'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + referral.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.referral.belongsTo(db.users, { + as: 'referrer', + foreignKey: { + name: 'referrerId', + }, + constraints: false, + }); + + db.referral.belongsTo(db.users, { + as: 'createdBy', + }); + + db.referral.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return referral; +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 74e549e..27d86c7 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -68,6 +68,14 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + pointsbalance: { + type: DataTypes.INTEGER, + }, + + referralcode: { + type: DataTypes.TEXT, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -102,6 +110,22 @@ module.exports = function (sequelize, DataTypes) { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.users.hasMany(db.referral, { + as: 'referral_referrer', + foreignKey: { + name: 'referrerId', + }, + constraints: false, + }); + + db.users.hasMany(db.redemption, { + as: 'redemption_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + //end loop db.users.belongsTo(db.roles, { @@ -112,6 +136,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.users.belongsTo(db.loyaltytier, { + as: 'loyaltytier', + foreignKey: { + name: 'loyaltytierId', + }, + 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 48c0117..92e53b6 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -102,6 +102,9 @@ module.exports = { 'vouchers', 'roles', 'permissions', + 'loyaltytier', + 'referral', + 'redemption', , ]; await queryInterface.bulkInsert( @@ -686,6 +689,81 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_LOYALTYTIER'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_LOYALTYTIER'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_LOYALTYTIER'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_LOYALTYTIER'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_REFERRAL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_REFERRAL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_REFERRAL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_REFERRAL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_REDEMPTION'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_REDEMPTION'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_REDEMPTION'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_REDEMPTION'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 5f91fc3..36435c6 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,6 +11,12 @@ const Payments = db.payments; const Vouchers = db.vouchers; +const Loyaltytier = db.loyaltytier; + +const Referral = db.referral; + +const Redemption = db.redemption; + const AgentsData = [ { name: 'Agent A', @@ -35,13 +41,29 @@ const AgentsData = [ phone_number: '3453453456', }, + + { + name: 'Agent D', + + email: 'agent.d@example.com', + + phone_number: '4564564567', + }, + + { + name: 'Agent E', + + email: 'agent.e@example.com', + + phone_number: '5675675678', + }, ]; const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Hotel', + service_type: 'Package', booking_date: new Date('2023-11-01T10:00:00Z'), @@ -53,7 +75,7 @@ const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Flight', + service_type: 'Tour', booking_date: new Date('2023-11-02T12:00:00Z'), @@ -73,6 +95,30 @@ const BookingsData = [ // type code here for "relation_one" field }, + + { + // type code here for "relation_one" field + + service_type: 'Package', + + booking_date: new Date('2023-11-04T16:00:00Z'), + + total_amount: 200, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + service_type: 'Tour', + + booking_date: new Date('2023-11-05T18:00:00Z'), + + total_amount: 450, + + // type code here for "relation_one" field + }, ]; const CustomersData = [ @@ -105,6 +151,26 @@ const CustomersData = [ phone_number: '1122334455', }, + + { + first_name: 'Bob', + + last_name: 'Brown', + + email: 'bob.brown@example.com', + + phone_number: '5566778899', + }, + + { + first_name: 'Charlie', + + last_name: 'Davis', + + email: 'charlie.davis@example.com', + + phone_number: '6677889900', + }, ]; const PaymentsData = [ @@ -115,7 +181,7 @@ const PaymentsData = [ amount: 500, - status: 'Failed', + status: 'Pending', }, { @@ -137,6 +203,26 @@ const PaymentsData = [ status: 'Completed', }, + + { + // type code here for "relation_one" field + + payment_date: new Date('2023-11-04T17:00:00Z'), + + amount: 200, + + status: 'Pending', + }, + + { + // type code here for "relation_one" field + + payment_date: new Date('2023-11-05T19:00:00Z'), + + amount: 450, + + status: 'Failed', + }, ]; const VouchersData = [ @@ -163,10 +249,249 @@ const VouchersData = [ issue_date: new Date('2023-11-03T16:00:00Z'), }, + + { + // type code here for "relation_one" field + + voucher_code: 'VCH45678', + + issue_date: new Date('2023-11-04T18:00:00Z'), + }, + + { + // type code here for "relation_one" field + + voucher_code: 'VCH56789', + + issue_date: new Date('2023-11-05T20:00:00Z'), + }, +]; + +const LoyaltytierData = [ + { + name: 'John Dalton', + + annualfee: 74.62, + + pointspersar: 51.91, + + description: 'Jean Baptiste Lamarck', + }, + + { + name: 'B. F. Skinner', + + annualfee: 10.09, + + pointspersar: 44.45, + + description: 'Werner Heisenberg', + }, + + { + name: 'Tycho Brahe', + + annualfee: 86.15, + + pointspersar: 10.74, + + description: 'Emil Fischer', + }, + + { + name: 'Francis Crick', + + annualfee: 20.76, + + pointspersar: 72.74, + + description: 'William Bayliss', + }, + + { + name: 'Thomas Hunt Morgan', + + annualfee: 97.56, + + pointspersar: 39.08, + + description: 'Johannes Kepler', + }, +]; + +const ReferralData = [ + { + // type code here for "relation_one" field + + referredemail: 'Ernst Haeckel', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Heike Kamerlingh Onnes', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Robert Koch', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Jean Piaget', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Jean Baptiste Lamarck', + + status: 'value', + }, +]; + +const RedemptionData = [ + { + // type code here for "relation_one" field + + pointsused: 5, + + redemptiontype: 'value', + + discountvalue: 62.55, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 5, + + redemptiontype: 'value', + + discountvalue: 99.45, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 8, + + redemptiontype: 'value', + + discountvalue: 29.35, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 5, + + redemptiontype: 'value', + + discountvalue: 91.62, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 9, + + redemptiontype: 'value', + + discountvalue: 87.53, + + status: 'value', + + // type code here for "relation_one" field + }, ]; // Similar logic for "relation_many" +async function associateUserWithLoyaltytier() { + const relatedLoyaltytier0 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User0 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (User0?.setLoyaltytier) { + await User0.setLoyaltytier(relatedLoyaltytier0); + } + + const relatedLoyaltytier1 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User1 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (User1?.setLoyaltytier) { + await User1.setLoyaltytier(relatedLoyaltytier1); + } + + const relatedLoyaltytier2 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User2 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (User2?.setLoyaltytier) { + await User2.setLoyaltytier(relatedLoyaltytier2); + } + + const relatedLoyaltytier3 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User3 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (User3?.setLoyaltytier) { + await User3.setLoyaltytier(relatedLoyaltytier3); + } + + const relatedLoyaltytier4 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User4 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (User4?.setLoyaltytier) { + await User4.setLoyaltytier(relatedLoyaltytier4); + } +} + async function associateBookingWithCustomer() { const relatedCustomer0 = await Customers.findOne({ offset: Math.floor(Math.random() * (await Customers.count())), @@ -200,6 +525,28 @@ async function associateBookingWithCustomer() { if (Booking2?.setCustomer) { await Booking2.setCustomer(relatedCustomer2); } + + const relatedCustomer3 = await Customers.findOne({ + offset: Math.floor(Math.random() * (await Customers.count())), + }); + const Booking3 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Booking3?.setCustomer) { + await Booking3.setCustomer(relatedCustomer3); + } + + const relatedCustomer4 = await Customers.findOne({ + offset: Math.floor(Math.random() * (await Customers.count())), + }); + const Booking4 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Booking4?.setCustomer) { + await Booking4.setCustomer(relatedCustomer4); + } } async function associateBookingWithAgent() { @@ -235,6 +582,28 @@ async function associateBookingWithAgent() { if (Booking2?.setAgent) { await Booking2.setAgent(relatedAgent2); } + + const relatedAgent3 = await Agents.findOne({ + offset: Math.floor(Math.random() * (await Agents.count())), + }); + const Booking3 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Booking3?.setAgent) { + await Booking3.setAgent(relatedAgent3); + } + + const relatedAgent4 = await Agents.findOne({ + offset: Math.floor(Math.random() * (await Agents.count())), + }); + const Booking4 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Booking4?.setAgent) { + await Booking4.setAgent(relatedAgent4); + } } async function associatePaymentWithBooking() { @@ -270,6 +639,28 @@ async function associatePaymentWithBooking() { if (Payment2?.setBooking) { await Payment2.setBooking(relatedBooking2); } + + const relatedBooking3 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Payment3 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Payment3?.setBooking) { + await Payment3.setBooking(relatedBooking3); + } + + const relatedBooking4 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Payment4 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Payment4?.setBooking) { + await Payment4.setBooking(relatedBooking4); + } } async function associateVoucherWithBooking() { @@ -305,6 +696,199 @@ async function associateVoucherWithBooking() { if (Voucher2?.setBooking) { await Voucher2.setBooking(relatedBooking2); } + + const relatedBooking3 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Voucher3 = await Vouchers.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Voucher3?.setBooking) { + await Voucher3.setBooking(relatedBooking3); + } + + const relatedBooking4 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Voucher4 = await Vouchers.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Voucher4?.setBooking) { + await Voucher4.setBooking(relatedBooking4); + } +} + +async function associateReferralWithReferrer() { + const relatedReferrer0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral0 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Referral0?.setReferrer) { + await Referral0.setReferrer(relatedReferrer0); + } + + const relatedReferrer1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral1 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Referral1?.setReferrer) { + await Referral1.setReferrer(relatedReferrer1); + } + + const relatedReferrer2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral2 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Referral2?.setReferrer) { + await Referral2.setReferrer(relatedReferrer2); + } + + const relatedReferrer3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral3 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Referral3?.setReferrer) { + await Referral3.setReferrer(relatedReferrer3); + } + + const relatedReferrer4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral4 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Referral4?.setReferrer) { + await Referral4.setReferrer(relatedReferrer4); + } +} + +async function associateRedemptionWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption0 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Redemption0?.setUser) { + await Redemption0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption1 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Redemption1?.setUser) { + await Redemption1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption2 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Redemption2?.setUser) { + await Redemption2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption3 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Redemption3?.setUser) { + await Redemption3.setUser(relatedUser3); + } + + const relatedUser4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption4 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Redemption4?.setUser) { + await Redemption4.setUser(relatedUser4); + } +} + +async function associateRedemptionWithBooking() { + const relatedBooking0 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption0 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Redemption0?.setBooking) { + await Redemption0.setBooking(relatedBooking0); + } + + const relatedBooking1 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption1 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Redemption1?.setBooking) { + await Redemption1.setBooking(relatedBooking1); + } + + const relatedBooking2 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption2 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Redemption2?.setBooking) { + await Redemption2.setBooking(relatedBooking2); + } + + const relatedBooking3 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption3 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Redemption3?.setBooking) { + await Redemption3.setBooking(relatedBooking3); + } + + const relatedBooking4 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption4 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Redemption4?.setBooking) { + await Redemption4.setBooking(relatedBooking4); + } } module.exports = { @@ -319,9 +903,17 @@ module.exports = { await Vouchers.bulkCreate(VouchersData); + await Loyaltytier.bulkCreate(LoyaltytierData); + + await Referral.bulkCreate(ReferralData); + + await Redemption.bulkCreate(RedemptionData); + await Promise.all([ // Similar logic for "relation_many" + await associateUserWithLoyaltytier(), + await associateBookingWithCustomer(), await associateBookingWithAgent(), @@ -329,6 +921,12 @@ module.exports = { await associatePaymentWithBooking(), await associateVoucherWithBooking(), + + await associateReferralWithReferrer(), + + await associateRedemptionWithUser(), + + await associateRedemptionWithBooking(), ]); }, @@ -342,5 +940,11 @@ module.exports = { await queryInterface.bulkDelete('payments', null, {}); await queryInterface.bulkDelete('vouchers', null, {}); + + await queryInterface.bulkDelete('loyaltytier', null, {}); + + await queryInterface.bulkDelete('referral', null, {}); + + await queryInterface.bulkDelete('redemption', null, {}); }, }; diff --git a/backend/src/db/seeders/20250524092744.js b/backend/src/db/seeders/20250524092744.js new file mode 100644 index 0000000..7d7ab98 --- /dev/null +++ b/backend/src/db/seeders/20250524092744.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 = ['loyaltytier']; + + 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/db/seeders/20250524093308.js b/backend/src/db/seeders/20250524093308.js new file mode 100644 index 0000000..d550eac --- /dev/null +++ b/backend/src/db/seeders/20250524093308.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 = ['referral']; + + 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/db/seeders/20250524093555.js b/backend/src/db/seeders/20250524093555.js new file mode 100644 index 0000000..fbd0aac --- /dev/null +++ b/backend/src/db/seeders/20250524093555.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 = ['redemption']; + + 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/db/seeders/2025052410000000-add-arwablus-loyalty-tiers.js b/backend/src/db/seeders/2025052410000000-add-arwablus-loyalty-tiers.js new file mode 100644 index 0000000..b7df56e --- /dev/null +++ b/backend/src/db/seeders/2025052410000000-add-arwablus-loyalty-tiers.js @@ -0,0 +1,41 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + const now = new Date(); + await queryInterface.bulkInsert('loyaltytier', [ + { + name: 'Mosafer', + annualfee: 0.00, + pointspersar: 0.01, + description: 'Basic support, Track bookings & visa status', + createdAt: now, + updatedAt: now, + }, + { + name: 'Elite', + annualfee: 99.00, + pointspersar: 0.02, + description: 'Priority support, Exclusive hotel deals, Free booking cancellations', + createdAt: now, + updatedAt: now, + }, + { + name: 'VIB+', + annualfee: 0.00, + pointspersar: 0.03, + description: 'Dedicated travel concierge, Free upgrades on select flights, VIP Umrah & Hajj benefits', + createdAt: now, + updatedAt: now, + }, + ], {}); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'loyaltytier', + { name: { [Sequelize.Op.in]: ['Mosafer', 'Elite', 'VIB+'] } }, + {} + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index 237eb50..f6d1aab 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -35,6 +35,12 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const loyaltytierRoutes = require('./routes/loyaltytier'); + +const referralRoutes = require('./routes/referral'); + +const redemptionRoutes = require('./routes/redemption'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -148,6 +154,24 @@ app.use( permissionsRoutes, ); +app.use( + '/api/loyaltytier', + passport.authenticate('jwt', { session: false }), + loyaltytierRoutes, +); + +app.use( + '/api/referral', + passport.authenticate('jwt', { session: false }), + referralRoutes, +); + +app.use( + '/api/redemption', + passport.authenticate('jwt', { session: false }), + redemptionRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/loyaltytier.js b/backend/src/routes/loyaltytier.js new file mode 100644 index 0000000..4033e0f --- /dev/null +++ b/backend/src/routes/loyaltytier.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const LoyaltytierService = require('../services/loyaltytier'); +const LoyaltytierDBApi = require('../db/api/loyaltytier'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('loyaltytier')); + +/** + * @swagger + * components: + * schemas: + * Loyaltytier: + * type: object + * properties: + + * name: + * type: string + * default: name + * description: + * type: string + * default: description + + * annualfee: + * type: integer + * format: int64 + * pointspersar: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Loyaltytier + * description: The Loyaltytier managing API + */ + +/** + * @swagger + * /api/loyaltytier: + * post: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierService.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: [Loyaltytier] + * 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/Loyaltytier" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * 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 LoyaltytierService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await LoyaltytierService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * summary: Get all loyaltytier + * description: Get all loyaltytier + * responses: + * 200: + * description: Loyaltytier list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name', 'description', 'annualfee', 'pointspersar']; + 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/loyaltytier/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * summary: Count all loyaltytier + * description: Count all loyaltytier + * responses: + * 200: + * description: Loyaltytier count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * summary: Find all loyaltytier that match search criteria + * description: Find all loyaltytier that match search criteria + * responses: + * 200: + * description: Loyaltytier list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loyaltytier" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await LoyaltytierDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/loyaltytier/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * 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 LoyaltytierDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/redemption.js b/backend/src/routes/redemption.js new file mode 100644 index 0000000..fe1f63b --- /dev/null +++ b/backend/src/routes/redemption.js @@ -0,0 +1,444 @@ +const express = require('express'); + +const RedemptionService = require('../services/redemption'); +const RedemptionDBApi = require('../db/api/redemption'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('redemption')); + +/** + * @swagger + * components: + * schemas: + * Redemption: + * type: object + * properties: + + * pointsused: + * type: integer + * format: int64 + + * discountvalue: + * type: integer + * format: int64 + + * + * + */ + +/** + * @swagger + * tags: + * name: Redemption + * description: The Redemption managing API + */ + +/** + * @swagger + * /api/redemption: + * post: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionService.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: [Redemption] + * 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/Redemption" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * 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 RedemptionService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await RedemptionService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * summary: Get all redemption + * description: Get all redemption + * responses: + * 200: + * description: Redemption list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'pointsused', 'discountvalue']; + 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/redemption/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * summary: Count all redemption + * description: Count all redemption + * responses: + * 200: + * description: Redemption count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * summary: Find all redemption that match search criteria + * description: Find all redemption that match search criteria + * responses: + * 200: + * description: Redemption list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Redemption" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await RedemptionDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/redemption/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * 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 RedemptionDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/referral.js b/backend/src/routes/referral.js new file mode 100644 index 0000000..97e008c --- /dev/null +++ b/backend/src/routes/referral.js @@ -0,0 +1,439 @@ +const express = require('express'); + +const ReferralService = require('../services/referral'); +const ReferralDBApi = require('../db/api/referral'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('referral')); + +/** + * @swagger + * components: + * schemas: + * Referral: + * type: object + * properties: + + * referredemail: + * type: string + * default: referredemail + + * + */ + +/** + * @swagger + * tags: + * name: Referral + * description: The Referral managing API + */ + +/** + * @swagger + * /api/referral: + * post: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Referral" + * 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 ReferralService.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: [Referral] + * 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/Referral" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Referral" + * 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 ReferralService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Referral" + * 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 ReferralService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * 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 ReferralService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ReferralService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * summary: Get all referral + * description: Get all referral + * responses: + * 200: + * description: Referral list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Referral" + * 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 ReferralDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'referredemail']; + 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/referral/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * summary: Count all referral + * description: Count all referral + * responses: + * 200: + * description: Referral count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Referral" + * 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 ReferralDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * summary: Find all referral that match search criteria + * description: Find all referral that match search criteria + * responses: + * 200: + * description: Referral list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Referral" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ReferralDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/referral/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * 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 ReferralDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index a6d214f..f1bc61f 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -32,6 +32,13 @@ router.use(checkCrudPermissions('users')); * email: * type: string * default: email + * referralcode: + * type: string + * default: referralcode + + * pointsbalance: + * type: integer + * format: int64 */ @@ -308,7 +315,15 @@ router.get( const currentUser = req.currentUser; const payload = await UsersDBApi.findAll(req.query, { currentUser }); if (filetype && filetype === 'csv') { - const fields = ['id', 'firstName', 'lastName', 'phoneNumber', 'email']; + const fields = [ + 'id', + 'firstName', + 'lastName', + 'phoneNumber', + 'email', + 'referralcode', + 'pointsbalance', + ]; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/services/loyaltytier.js b/backend/src/services/loyaltytier.js new file mode 100644 index 0000000..413a30a --- /dev/null +++ b/backend/src/services/loyaltytier.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const LoyaltytierDBApi = require('../db/api/loyaltytier'); +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 LoyaltytierService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LoyaltytierDBApi.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 LoyaltytierDBApi.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 loyaltytier = await LoyaltytierDBApi.findBy({ id }, { transaction }); + + if (!loyaltytier) { + throw new ValidationError('loyaltytierNotFound'); + } + + const updatedLoyaltytier = await LoyaltytierDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedLoyaltytier; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LoyaltytierDBApi.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 LoyaltytierDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/redemption.js b/backend/src/services/redemption.js new file mode 100644 index 0000000..c18a1c6 --- /dev/null +++ b/backend/src/services/redemption.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const RedemptionDBApi = require('../db/api/redemption'); +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 RedemptionService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await RedemptionDBApi.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 RedemptionDBApi.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 redemption = await RedemptionDBApi.findBy({ id }, { transaction }); + + if (!redemption) { + throw new ValidationError('redemptionNotFound'); + } + + const updatedRedemption = await RedemptionDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedRedemption; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await RedemptionDBApi.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 RedemptionDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/referral.js b/backend/src/services/referral.js new file mode 100644 index 0000000..09ecc3e --- /dev/null +++ b/backend/src/services/referral.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const ReferralDBApi = require('../db/api/referral'); +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 ReferralService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ReferralDBApi.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 ReferralDBApi.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 referral = await ReferralDBApi.findBy({ id }, { transaction }); + + if (!referral) { + throw new ValidationError('referralNotFound'); + } + + const updatedReferral = await ReferralDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedReferral; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ReferralDBApi.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 ReferralDBApi.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 4e419ec..595c552 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -41,18 +41,38 @@ module.exports = class SearchService { throw new ValidationError('iam.errors.searchQueryRequired'); } const tableColumns = { - users: ['firstName', 'lastName', 'phoneNumber', 'email'], + users: [ + 'firstName', + + 'lastName', + + 'phoneNumber', + + 'email', + + 'referralcode', + ], agents: ['name', 'email', 'phone_number'], customers: ['first_name', 'last_name', 'email', 'phone_number'], vouchers: ['voucher_code'], + + loyaltytier: ['name', 'description'], + + referral: ['referredemail'], }; const columnsInt = { + users: ['pointsbalance'], + bookings: ['total_amount'], payments: ['amount'], + + loyaltytier: ['annualfee', 'pointspersar'], + + redemption: ['pointsused', 'discountvalue'], }; 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/Loyaltytier/CardLoyaltytier.tsx b/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx new file mode 100644 index 0000000..dfa34a1 --- /dev/null +++ b/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx @@ -0,0 +1,138 @@ +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 = { + loyaltytier: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLoyaltytier = ({ + loyaltytier, + 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_LOYALTYTIER'); + + return ( +
+ {loading && } +
    + {!loading && + loyaltytier.map((item, index) => ( +
  • +
    + + {item.name} + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    + +
    +
    + Annualfee +
    +
    +
    + {item.annualfee} +
    +
    +
    + +
    +
    + Pointspersar +
    +
    +
    + {item.pointspersar} +
    +
    +
    + +
    +
    + Description +
    +
    +
    + {item.description} +
    +
    +
    +
    +
  • + ))} + {!loading && loyaltytier.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardLoyaltytier; diff --git a/frontend/src/components/Loyaltytier/ListLoyaltytier.tsx b/frontend/src/components/Loyaltytier/ListLoyaltytier.tsx new file mode 100644 index 0000000..9598b39 --- /dev/null +++ b/frontend/src/components/Loyaltytier/ListLoyaltytier.tsx @@ -0,0 +1,104 @@ +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 = { + loyaltytier: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListLoyaltytier = ({ + loyaltytier, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_LOYALTYTIER'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + loyaltytier.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Name

+

{item.name}

+
+ +
+

Annualfee

+

{item.annualfee}

+
+ +
+

Pointspersar

+

{item.pointspersar}

+
+ +
+

Description

+

{item.description}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListLoyaltytier; diff --git a/frontend/src/components/Loyaltytier/TableLoyaltytier.tsx b/frontend/src/components/Loyaltytier/TableLoyaltytier.tsx new file mode 100644 index 0000000..95972d9 --- /dev/null +++ b/frontend/src/components/Loyaltytier/TableLoyaltytier.tsx @@ -0,0 +1,487 @@ +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/loyaltytier/loyaltytierSlice'; +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 './configureLoyaltytierCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleLoyaltytier = ({ + 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 { + loyaltytier, + loading, + count, + notify: loyaltytierNotify, + refetch, + } = useAppSelector((state) => state.loyaltytier); + 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 (loyaltytierNotify.showNotification) { + notify( + loyaltytierNotify.typeNotification, + loyaltytierNotify.textNotification, + ); + } + }, [loyaltytierNotify.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, `loyaltytier`, 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={loyaltytier ?? []} + 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 TableSampleLoyaltytier; diff --git a/frontend/src/components/Loyaltytier/configureLoyaltytierCols.tsx b/frontend/src/components/Loyaltytier/configureLoyaltytierCols.tsx new file mode 100644 index 0000000..13aefc1 --- /dev/null +++ b/frontend/src/components/Loyaltytier/configureLoyaltytierCols.tsx @@ -0,0 +1,114 @@ +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_LOYALTYTIER'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'annualfee', + headerName: 'Annualfee', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'pointspersar', + headerName: 'Pointspersar', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'description', + headerName: 'Description', + 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/Redemption/CardRedemption.tsx b/frontend/src/components/Redemption/CardRedemption.tsx new file mode 100644 index 0000000..a99e0d8 --- /dev/null +++ b/frontend/src/components/Redemption/CardRedemption.tsx @@ -0,0 +1,162 @@ +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 = { + redemption: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardRedemption = ({ + redemption, + 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_REDEMPTION'); + + return ( +
+ {loading && } +
    + {!loading && + redemption.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    User
    +
    +
    + {dataFormatter.usersOneListFormatter(item.user)} +
    +
    +
    + +
    +
    + Pointsused +
    +
    +
    + {item.pointsused} +
    +
    +
    + +
    +
    + Redemptiontype +
    +
    +
    + {item.redemptiontype} +
    +
    +
    + +
    +
    + Discountvalue +
    +
    +
    + {item.discountvalue} +
    +
    +
    + +
    +
    + Status +
    +
    +
    + {item.status} +
    +
    +
    + +
    +
    + Booking +
    +
    +
    + {dataFormatter.bookingsOneListFormatter(item.booking)} +
    +
    +
    +
    +
  • + ))} + {!loading && redemption.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardRedemption; diff --git a/frontend/src/components/Redemption/ListRedemption.tsx b/frontend/src/components/Redemption/ListRedemption.tsx new file mode 100644 index 0000000..330d989 --- /dev/null +++ b/frontend/src/components/Redemption/ListRedemption.tsx @@ -0,0 +1,122 @@ +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 = { + redemption: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListRedemption = ({ + redemption, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_REDEMPTION'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + redemption.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

User

+

+ {dataFormatter.usersOneListFormatter(item.user)} +

+
+ +
+

Pointsused

+

{item.pointsused}

+
+ +
+

+ Redemptiontype +

+

{item.redemptiontype}

+
+ +
+

+ Discountvalue +

+

{item.discountvalue}

+
+ +
+

Status

+

{item.status}

+
+ +
+

Booking

+

+ {dataFormatter.bookingsOneListFormatter(item.booking)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListRedemption; diff --git a/frontend/src/components/Redemption/TableRedemption.tsx b/frontend/src/components/Redemption/TableRedemption.tsx new file mode 100644 index 0000000..7fcc7f5 --- /dev/null +++ b/frontend/src/components/Redemption/TableRedemption.tsx @@ -0,0 +1,487 @@ +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/redemption/redemptionSlice'; +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 './configureRedemptionCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleRedemption = ({ + 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 { + redemption, + loading, + count, + notify: redemptionNotify, + refetch, + } = useAppSelector((state) => state.redemption); + 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 (redemptionNotify.showNotification) { + notify( + redemptionNotify.typeNotification, + redemptionNotify.textNotification, + ); + } + }, [redemptionNotify.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, `redemption`, 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={redemption ?? []} + 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 TableSampleRedemption; diff --git a/frontend/src/components/Redemption/configureRedemptionCols.tsx b/frontend/src/components/Redemption/configureRedemptionCols.tsx new file mode 100644 index 0000000..7bf49eb --- /dev/null +++ b/frontend/src/components/Redemption/configureRedemptionCols.tsx @@ -0,0 +1,160 @@ +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_REDEMPTION'); + + return [ + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'pointsused', + headerName: 'Pointsused', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'redemptiontype', + headerName: 'Redemptiontype', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'singleSelect', + valueOptions: ['value'], + }, + + { + field: 'discountvalue', + headerName: 'Discountvalue', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'singleSelect', + valueOptions: ['value'], + }, + + { + field: 'booking', + headerName: 'Booking', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('bookings'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Referral/CardReferral.tsx b/frontend/src/components/Referral/CardReferral.tsx new file mode 100644 index 0000000..e71b95b --- /dev/null +++ b/frontend/src/components/Referral/CardReferral.tsx @@ -0,0 +1,131 @@ +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 = { + referral: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardReferral = ({ + referral, + 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_REFERRAL'); + + return ( +
+ {loading && } +
    + {!loading && + referral.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Referrer +
    +
    +
    + {dataFormatter.usersOneListFormatter(item.referrer)} +
    +
    +
    + +
    +
    + Referredemail +
    +
    +
    + {item.referredemail} +
    +
    +
    + +
    +
    + Status +
    +
    +
    + {item.status} +
    +
    +
    +
    +
  • + ))} + {!loading && referral.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardReferral; diff --git a/frontend/src/components/Referral/ListReferral.tsx b/frontend/src/components/Referral/ListReferral.tsx new file mode 100644 index 0000000..4df1e9a --- /dev/null +++ b/frontend/src/components/Referral/ListReferral.tsx @@ -0,0 +1,103 @@ +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 = { + referral: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListReferral = ({ + referral, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_REFERRAL'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + referral.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Referrer

+

+ {dataFormatter.usersOneListFormatter(item.referrer)} +

+
+ +
+

+ Referredemail +

+

{item.referredemail}

+
+ +
+

Status

+

{item.status}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListReferral; diff --git a/frontend/src/components/Referral/TableReferral.tsx b/frontend/src/components/Referral/TableReferral.tsx new file mode 100644 index 0000000..6eff4c8 --- /dev/null +++ b/frontend/src/components/Referral/TableReferral.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/referral/referralSlice'; +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 './configureReferralCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleReferral = ({ + 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 { + referral, + loading, + count, + notify: referralNotify, + refetch, + } = useAppSelector((state) => state.referral); + 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 (referralNotify.showNotification) { + notify(referralNotify.typeNotification, referralNotify.textNotification); + } + }, [referralNotify.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, `referral`, 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={referral ?? []} + 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 TableSampleReferral; diff --git a/frontend/src/components/Referral/configureReferralCols.tsx b/frontend/src/components/Referral/configureReferralCols.tsx new file mode 100644 index 0000000..ee2a9ec --- /dev/null +++ b/frontend/src/components/Referral/configureReferralCols.tsx @@ -0,0 +1,109 @@ +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_REFERRAL'); + + return [ + { + field: 'referrer', + headerName: 'Referrer', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'referredemail', + headerName: 'Referredemail', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'singleSelect', + valueOptions: ['value'], + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Users/CardUsers.tsx b/frontend/src/components/Users/CardUsers.tsx index 91f2dda..4d86d52 100644 --- a/frontend/src/components/Users/CardUsers.tsx +++ b/frontend/src/components/Users/CardUsers.tsx @@ -173,6 +173,41 @@ const CardUsers = ({ + +
+
+ Loyaltytier +
+
+
+ {dataFormatter.loyaltytierOneListFormatter( + item.loyaltytier, + )} +
+
+
+ +
+
+ Pointsbalance +
+
+
+ {item.pointsbalance} +
+
+
+ +
+
+ Referralcode +
+
+
+ {item.referralcode} +
+
+
))} diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx index 2533b8a..d2c2bb8 100644 --- a/frontend/src/components/Users/ListUsers.tsx +++ b/frontend/src/components/Users/ListUsers.tsx @@ -115,6 +115,27 @@ const ListUsers = ({ .join(', ')}

+ +
+

Loyaltytier

+

+ {dataFormatter.loyaltytierOneListFormatter( + item.loyaltytier, + )} +

+
+ +
+

+ Pointsbalance +

+

{item.pointsbalance}

+
+ +
+

Referralcode

+

{item.referralcode}

+
value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('loyaltytier'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'pointsbalance', + headerName: 'Pointsbalance', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'referralcode', + headerName: 'Referralcode', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js index 0350555..dc9616c 100644 --- a/frontend/src/helpers/dataFormatter.js +++ b/frontend/src/helpers/dataFormatter.js @@ -39,6 +39,25 @@ export default { }); }, + usersManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.firstName); + }, + usersOneListFormatter(val) { + if (!val) return ''; + return val.firstName; + }, + usersManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.firstName }; + }); + }, + usersOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.firstName, id: val.id }; + }, + agentsManyListFormatter(val) { if (!val || !val.length) return []; return val.map((item) => item.name); @@ -133,4 +152,23 @@ export default { if (!val) return ''; return { label: val.name, id: val.id }; }, + + loyaltytierManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + loyaltytierOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + loyaltytierManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + loyaltytierOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, }; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 57aed54..a58fa17 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -87,6 +87,30 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, permissions: 'READ_PERMISSIONS', }, + { + href: '/loyaltytier/loyaltytier-list', + label: 'Loyaltytier', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_LOYALTYTIER', + }, + { + href: '/referral/referral-list', + label: 'Referral', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_REFERRAL', + }, + { + href: '/redemption/redemption-list', + label: 'Redemption', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_REDEMPTION', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/menuNavBar.ts.temp b/frontend/src/menuNavBar.ts.temp new file mode 100644 index 0000000..b8e6c5f --- /dev/null +++ b/frontend/src/menuNavBar.ts.temp @@ -0,0 +1,76 @@ + { + href: '/loyalty', + label: 'loyalty', + }, +import { + mdiMenu, + mdiClockOutline, + mdiCloud, + mdiCrop, + mdiAccount, + mdiCogOutline, + mdiEmail, + mdiLogout, + mdiThemeLightDark, + mdiGithub, + mdiVuejs, +} from '@mdi/js'; +import { MenuNavBarItem } from './interfaces'; + +const menuNavBar: MenuNavBarItem[] = [ + { + isCurrentUser: true, + menu: [ + { + icon: mdiAccount, + label: 'My Profile', + href: '/profile', + }, + { + isDivider: true, + }, + { + icon: mdiLogout, + label: 'Log Out', + isLogout: true, + }, + ], + }, + { + icon: mdiThemeLightDark, + label: 'Light/Dark', + isDesktopNoLabel: true, + isToggleLightDark: true, + }, + { + icon: mdiLogout, + label: 'Log out', + isDesktopNoLabel: true, + isLogout: true, + }, +]; + +export const webPagesNavBar = [ + { + href: '/home', + label: 'home', + }, + { + href: '/about', + label: 'about', + }, + { + href: '/services', + label: 'services', + }, + { + href: '/contact', + label: 'contact', + }, + { + href: '/faq', + label: 'FAQ', + }, +]; + +export default menuNavBar; diff --git a/frontend/src/pages/bookings/bookings-view.tsx b/frontend/src/pages/bookings/bookings-view.tsx index aca2f70..8e67b85 100644 --- a/frontend/src/pages/bookings/bookings-view.tsx +++ b/frontend/src/pages/bookings/bookings-view.tsx @@ -185,6 +185,59 @@ const BookingsView = () => { + <> +

Redemption Booking

+ +
+ + + + + + + + + + + + + + {bookings.redemption_booking && + Array.isArray(bookings.redemption_booking) && + bookings.redemption_booking.map((item: any) => ( + + router.push( + `/redemption/redemption-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
PointsusedRedemptiontypeDiscountvalueStatus
{item.pointsused} + {item.redemptiontype} + + {item.discountvalue} + {item.status}
+
+ {!bookings?.redemption_booking?.length && ( +
No data
+ )} +
+ + { + return ( +
+

Checkout

+

Please log in to your account to proceed with payment and complete your booking.

+ {/* TODO: Insert checkout form and payment integration here */} +
+ ); +}; + +CheckoutPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default CheckoutPage; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index a4effe4..f54bcd8 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -36,6 +36,9 @@ const Dashboard = () => { const [vouchers, setVouchers] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [loyaltytier, setLoyaltytier] = React.useState(loadingMessage); + const [referral, setReferral] = React.useState(loadingMessage); + const [redemption, setRedemption] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -55,6 +58,9 @@ const Dashboard = () => { 'vouchers', 'roles', 'permissions', + 'loyaltytier', + 'referral', + 'redemption', ]; const fns = [ setUsers, @@ -65,6 +71,9 @@ const Dashboard = () => { setVouchers, setRoles, setPermissions, + setLoyaltytier, + setReferral, + setRedemption, ]; const requests = entities.map((entity, index) => { @@ -456,6 +465,102 @@ const Dashboard = () => { )} + + {hasPermission(currentUser, 'READ_LOYALTYTIER') && ( + +
+
+
+
+ Loyaltytier +
+
+ {loyaltytier} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_REFERRAL') && ( + +
+
+
+
+ Referral +
+
+ {referral} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_REDEMPTION') && ( + +
+
+
+
+ Redemption +
+
+ {redemption} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 4803e62..b0e28bd 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -139,7 +139,7 @@ export default function WebSite() { { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + + annualfee: '', + + pointspersar: '', + + description: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { loyaltytier } = useAppSelector((state) => state.loyaltytier); + + const { loyaltytierId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: loyaltytierId })); + }, [loyaltytierId]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + setInitialValues(loyaltytier); + } + }, [loyaltytier]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = loyaltytier[el]), + ); + + setInitialValues(newInitialVal); + } + }, [loyaltytier]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: loyaltytierId, data })); + await router.push('/loyaltytier/loyaltytier-list'); + }; + + return ( + <> + + {getPageTitle('Edit loyaltytier')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/loyaltytier/loyaltytier-list')} + /> + + +
+
+
+ + ); +}; + +EditLoyaltytier.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLoyaltytier; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx b/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx new file mode 100644 index 0000000..6ac3fff --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx @@ -0,0 +1,146 @@ +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/loyaltytier/loyaltytierSlice'; +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 EditLoyaltytierPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + + annualfee: '', + + pointspersar: '', + + description: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { loyaltytier } = useAppSelector((state) => state.loyaltytier); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + setInitialValues(loyaltytier); + } + }, [loyaltytier]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = loyaltytier[el]), + ); + setInitialValues(newInitialVal); + } + }, [loyaltytier]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/loyaltytier/loyaltytier-list'); + }; + + return ( + <> + + {getPageTitle('Edit loyaltytier')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/loyaltytier/loyaltytier-list')} + /> + + +
+
+
+ + ); +}; + +EditLoyaltytierPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLoyaltytierPage; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-list.tsx b/frontend/src/pages/loyaltytier/loyaltytier-list.tsx new file mode 100644 index 0000000..47a34a1 --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-list.tsx @@ -0,0 +1,171 @@ +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 TableLoyaltytier from '../../components/Loyaltytier/TableLoyaltytier'; +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/loyaltytier/loyaltytierSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LoyaltytierTablesPage = () => { + 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' }, + { label: 'Description', title: 'description' }, + + { label: 'Annualfee', title: 'annualfee', number: 'true' }, + { label: 'Pointspersar', title: 'pointspersar', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LOYALTYTIER'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLoyaltytierCSV = async () => { + const response = await axios({ + url: '/loyaltytier?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 = 'loyaltytierCSV.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('Loyaltytier')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +LoyaltytierTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierTablesPage; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-new.tsx b/frontend/src/pages/loyaltytier/loyaltytier-new.tsx new file mode 100644 index 0000000..e266f93 --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-new.tsx @@ -0,0 +1,120 @@ +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/loyaltytier/loyaltytierSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', + + annualfee: '', + + pointspersar: '', + + description: '', +}; + +const LoyaltytierNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/loyaltytier/loyaltytier-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/loyaltytier/loyaltytier-list')} + /> + + +
+
+
+ + ); +}; + +LoyaltytierNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierNew; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-table.tsx b/frontend/src/pages/loyaltytier/loyaltytier-table.tsx new file mode 100644 index 0000000..cd51786 --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-table.tsx @@ -0,0 +1,170 @@ +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 TableLoyaltytier from '../../components/Loyaltytier/TableLoyaltytier'; +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/loyaltytier/loyaltytierSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LoyaltytierTablesPage = () => { + 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' }, + { label: 'Description', title: 'description' }, + + { label: 'Annualfee', title: 'annualfee', number: 'true' }, + { label: 'Pointspersar', title: 'pointspersar', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LOYALTYTIER'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLoyaltytierCSV = async () => { + const response = await axios({ + url: '/loyaltytier?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 = 'loyaltytierCSV.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('Loyaltytier')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +LoyaltytierTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierTablesPage; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-view.tsx b/frontend/src/pages/loyaltytier/loyaltytier-view.tsx new file mode 100644 index 0000000..fc2e9d7 --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-view.tsx @@ -0,0 +1,161 @@ +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/loyaltytier/loyaltytierSlice'; +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 LoyaltytierView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { loyaltytier } = useAppSelector((state) => state.loyaltytier); + + 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 loyaltytier')} + + + + + + +
+

Name

+

{loyaltytier?.name}

+
+ +
+

Annualfee

+

{loyaltytier?.annualfee || 'No data'}

+
+ +
+

Pointspersar

+

{loyaltytier?.pointspersar || 'No data'}

+
+ +
+

Description

+

{loyaltytier?.description}

+
+ + <> +

Users Loyaltytier

+ +
+ + + + + + + + + + + + + + + + + + + + {loyaltytier.users_loyaltytier && + Array.isArray(loyaltytier.users_loyaltytier) && + loyaltytier.users_loyaltytier.map((item: any) => ( + + router.push(`/users/users-view/?id=${item.id}`) + } + > + + + + + + + + + + + + + + + ))} + +
First NameLast NamePhone NumberE-MailDisabledPointsbalanceReferralcode
{item.firstName}{item.lastName}{item.phoneNumber}{item.email} + {dataFormatter.booleanFormatter(item.disabled)} + + {item.pointsbalance} + {item.referralcode}
+
+ {!loyaltytier?.users_loyaltytier?.length && ( +
No data
+ )} +
+ + + + + router.push('/loyaltytier/loyaltytier-list')} + /> +
+
+ + ); +}; + +LoyaltytierView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierView; diff --git a/frontend/src/pages/redemption/[redemptionId].tsx b/frontend/src/pages/redemption/[redemptionId].tsx new file mode 100644 index 0000000..569cb01 --- /dev/null +++ b/frontend/src/pages/redemption/[redemptionId].tsx @@ -0,0 +1,186 @@ +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/redemption/redemptionSlice'; +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 EditRedemption = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + pointsused: '', + + redemptiontype: '', + + discountvalue: '', + + status: '', + + booking: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { redemption } = useAppSelector((state) => state.redemption); + + const { redemptionId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: redemptionId })); + }, [redemptionId]); + + useEffect(() => { + if (typeof redemption === 'object') { + setInitialValues(redemption); + } + }, [redemption]); + + useEffect(() => { + if (typeof redemption === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = redemption[el]), + ); + + setInitialValues(newInitialVal); + } + }, [redemption]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: redemptionId, data })); + await router.push('/redemption/redemption-list'); + }; + + return ( + <> + + {getPageTitle('Edit redemption')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/redemption/redemption-list')} + /> + + +
+
+
+ + ); +}; + +EditRedemption.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditRedemption; diff --git a/frontend/src/pages/redemption/redemption-edit.tsx b/frontend/src/pages/redemption/redemption-edit.tsx new file mode 100644 index 0000000..a8fe30f --- /dev/null +++ b/frontend/src/pages/redemption/redemption-edit.tsx @@ -0,0 +1,184 @@ +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/redemption/redemptionSlice'; +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 EditRedemptionPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + pointsused: '', + + redemptiontype: '', + + discountvalue: '', + + status: '', + + booking: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { redemption } = useAppSelector((state) => state.redemption); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof redemption === 'object') { + setInitialValues(redemption); + } + }, [redemption]); + + useEffect(() => { + if (typeof redemption === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = redemption[el]), + ); + setInitialValues(newInitialVal); + } + }, [redemption]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/redemption/redemption-list'); + }; + + return ( + <> + + {getPageTitle('Edit redemption')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/redemption/redemption-list')} + /> + + +
+
+
+ + ); +}; + +EditRedemptionPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditRedemptionPage; diff --git a/frontend/src/pages/redemption/redemption-list.tsx b/frontend/src/pages/redemption/redemption-list.tsx new file mode 100644 index 0000000..0cb8204 --- /dev/null +++ b/frontend/src/pages/redemption/redemption-list.tsx @@ -0,0 +1,177 @@ +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 TableRedemption from '../../components/Redemption/TableRedemption'; +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/redemption/redemptionSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const RedemptionTablesPage = () => { + 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: 'Pointsused', title: 'pointsused', number: 'true' }, + { label: 'Discountvalue', title: 'discountvalue', number: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'Booking', title: 'booking' }, + + { + label: 'Redemptiontype', + title: 'redemptiontype', + type: 'enum', + options: ['value'], + }, + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REDEMPTION'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getRedemptionCSV = async () => { + const response = await axios({ + url: '/redemption?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 = 'redemptionCSV.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('Redemption')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +RedemptionTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionTablesPage; diff --git a/frontend/src/pages/redemption/redemption-new.tsx b/frontend/src/pages/redemption/redemption-new.tsx new file mode 100644 index 0000000..5e4e538 --- /dev/null +++ b/frontend/src/pages/redemption/redemption-new.tsx @@ -0,0 +1,156 @@ +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/redemption/redemptionSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + user: '', + + pointsused: '', + + redemptiontype: '', + + discountvalue: '', + + status: '', + + booking: '', +}; + +const RedemptionNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/redemption/redemption-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/redemption/redemption-list')} + /> + + +
+
+
+ + ); +}; + +RedemptionNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionNew; diff --git a/frontend/src/pages/redemption/redemption-table.tsx b/frontend/src/pages/redemption/redemption-table.tsx new file mode 100644 index 0000000..20215e8 --- /dev/null +++ b/frontend/src/pages/redemption/redemption-table.tsx @@ -0,0 +1,176 @@ +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 TableRedemption from '../../components/Redemption/TableRedemption'; +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/redemption/redemptionSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const RedemptionTablesPage = () => { + 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: 'Pointsused', title: 'pointsused', number: 'true' }, + { label: 'Discountvalue', title: 'discountvalue', number: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'Booking', title: 'booking' }, + + { + label: 'Redemptiontype', + title: 'redemptiontype', + type: 'enum', + options: ['value'], + }, + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REDEMPTION'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getRedemptionCSV = async () => { + const response = await axios({ + url: '/redemption?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 = 'redemptionCSV.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('Redemption')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +RedemptionTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionTablesPage; diff --git a/frontend/src/pages/redemption/redemption-view.tsx b/frontend/src/pages/redemption/redemption-view.tsx new file mode 100644 index 0000000..94f8c8c --- /dev/null +++ b/frontend/src/pages/redemption/redemption-view.tsx @@ -0,0 +1,110 @@ +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/redemption/redemptionSlice'; +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 RedemptionView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { redemption } = useAppSelector((state) => state.redemption); + + 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 redemption')} + + + + + + +
+

User

+ +

{redemption?.user?.firstName ?? 'No data'}

+
+ +
+

Pointsused

+

{redemption?.pointsused || 'No data'}

+
+ +
+

Redemptiontype

+

{redemption?.redemptiontype ?? 'No data'}

+
+ +
+

Discountvalue

+

{redemption?.discountvalue || 'No data'}

+
+ +
+

Status

+

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

+
+ +
+

Booking

+ +

{redemption?.booking?.service_type ?? 'No data'}

+
+ + + + router.push('/redemption/redemption-list')} + /> +
+
+ + ); +}; + +RedemptionView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionView; diff --git a/frontend/src/pages/referral/[referralId].tsx b/frontend/src/pages/referral/[referralId].tsx new file mode 100644 index 0000000..88edbc4 --- /dev/null +++ b/frontend/src/pages/referral/[referralId].tsx @@ -0,0 +1,147 @@ +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/referral/referralSlice'; +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 EditReferral = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + referrer: null, + + referredemail: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { referral } = useAppSelector((state) => state.referral); + + const { referralId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: referralId })); + }, [referralId]); + + useEffect(() => { + if (typeof referral === 'object') { + setInitialValues(referral); + } + }, [referral]); + + useEffect(() => { + if (typeof referral === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = referral[el])); + + setInitialValues(newInitialVal); + } + }, [referral]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: referralId, data })); + await router.push('/referral/referral-list'); + }; + + return ( + <> + + {getPageTitle('Edit referral')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/referral/referral-list')} + /> + + +
+
+
+ + ); +}; + +EditReferral.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReferral; diff --git a/frontend/src/pages/referral/referral-edit.tsx b/frontend/src/pages/referral/referral-edit.tsx new file mode 100644 index 0000000..7430389 --- /dev/null +++ b/frontend/src/pages/referral/referral-edit.tsx @@ -0,0 +1,145 @@ +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/referral/referralSlice'; +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 EditReferralPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + referrer: null, + + referredemail: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { referral } = useAppSelector((state) => state.referral); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof referral === 'object') { + setInitialValues(referral); + } + }, [referral]); + + useEffect(() => { + if (typeof referral === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = referral[el])); + setInitialValues(newInitialVal); + } + }, [referral]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/referral/referral-list'); + }; + + return ( + <> + + {getPageTitle('Edit referral')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/referral/referral-list')} + /> + + +
+
+
+ + ); +}; + +EditReferralPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReferralPage; diff --git a/frontend/src/pages/referral/referral-list.tsx b/frontend/src/pages/referral/referral-list.tsx new file mode 100644 index 0000000..c3391f2 --- /dev/null +++ b/frontend/src/pages/referral/referral-list.tsx @@ -0,0 +1,168 @@ +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 TableReferral from '../../components/Referral/TableReferral'; +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/referral/referralSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReferralTablesPage = () => { + 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: 'Referredemail', title: 'referredemail' }, + + { label: 'Referrer', title: 'referrer' }, + + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REFERRAL'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReferralCSV = async () => { + const response = await axios({ + url: '/referral?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 = 'referralCSV.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('Referral')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ReferralTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralTablesPage; diff --git a/frontend/src/pages/referral/referral-new.tsx b/frontend/src/pages/referral/referral-new.tsx new file mode 100644 index 0000000..a578d8b --- /dev/null +++ b/frontend/src/pages/referral/referral-new.tsx @@ -0,0 +1,120 @@ +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/referral/referralSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + referrer: '', + + referredemail: '', + + status: '', +}; + +const ReferralNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/referral/referral-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/referral/referral-list')} + /> + + +
+
+
+ + ); +}; + +ReferralNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralNew; diff --git a/frontend/src/pages/referral/referral-table.tsx b/frontend/src/pages/referral/referral-table.tsx new file mode 100644 index 0000000..56ade11 --- /dev/null +++ b/frontend/src/pages/referral/referral-table.tsx @@ -0,0 +1,167 @@ +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 TableReferral from '../../components/Referral/TableReferral'; +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/referral/referralSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReferralTablesPage = () => { + 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: 'Referredemail', title: 'referredemail' }, + + { label: 'Referrer', title: 'referrer' }, + + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REFERRAL'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReferralCSV = async () => { + const response = await axios({ + url: '/referral?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 = 'referralCSV.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('Referral')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ReferralTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralTablesPage; diff --git a/frontend/src/pages/referral/referral-view.tsx b/frontend/src/pages/referral/referral-view.tsx new file mode 100644 index 0000000..d188b3c --- /dev/null +++ b/frontend/src/pages/referral/referral-view.tsx @@ -0,0 +1,94 @@ +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/referral/referralSlice'; +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 ReferralView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { referral } = useAppSelector((state) => state.referral); + + 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 referral')} + + + + + + +
+

Referrer

+ +

{referral?.referrer?.firstName ?? 'No data'}

+
+ +
+

Referredemail

+

{referral?.referredemail}

+
+ +
+

Status

+

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

+
+ + + + router.push('/referral/referral-list')} + /> +
+
+ + ); +}; + +ReferralView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralView; diff --git a/frontend/src/pages/roles/roles-view.tsx b/frontend/src/pages/roles/roles-view.tsx index b7368ff..1a75133 100644 --- a/frontend/src/pages/roles/roles-view.tsx +++ b/frontend/src/pages/roles/roles-view.tsx @@ -115,6 +115,10 @@ const RolesView = () => { E-Mail Disabled + + Pointsbalance + + Referralcode @@ -138,6 +142,12 @@ const RolesView = () => { {dataFormatter.booleanFormatter(item.disabled)} + + + {item.pointsbalance} + + + {item.referralcode} ))} diff --git a/frontend/src/pages/users/[usersId].tsx b/frontend/src/pages/users/[usersId].tsx index e16077f..a21bdd8 100644 --- a/frontend/src/pages/users/[usersId].tsx +++ b/frontend/src/pages/users/[usersId].tsx @@ -52,6 +52,12 @@ const EditUsers = () => { custom_permissions: [], + loyaltytier: null, + + pointsbalance: '', + + referralcode: '', + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -170,6 +176,29 @@ const EditUsers = () => { > + + + + + + + + + + + + diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx index 87d7090..7baef37 100644 --- a/frontend/src/pages/users/users-edit.tsx +++ b/frontend/src/pages/users/users-edit.tsx @@ -52,6 +52,12 @@ const EditUsersPage = () => { custom_permissions: [], + loyaltytier: null, + + pointsbalance: '', + + referralcode: '', + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -168,6 +174,29 @@ const EditUsersPage = () => { > + + + + + + + + + + + + diff --git a/frontend/src/pages/users/users-list.tsx b/frontend/src/pages/users/users-list.tsx index 6d03ebc..4768be2 100644 --- a/frontend/src/pages/users/users-list.tsx +++ b/frontend/src/pages/users/users-list.tsx @@ -33,9 +33,13 @@ const UsersTablesPage = () => { { label: 'Last Name', title: 'lastName' }, { label: 'Phone Number', title: 'phoneNumber' }, { label: 'E-Mail', title: 'email' }, + { label: 'Referralcode', title: 'referralcode' }, + { label: 'Pointsbalance', title: 'pointsbalance', number: 'true' }, { label: 'App Role', title: 'app_role' }, + { label: 'Loyaltytier', title: 'loyaltytier' }, + { 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..a7649dd 100644 --- a/frontend/src/pages/users/users-new.tsx +++ b/frontend/src/pages/users/users-new.tsx @@ -48,6 +48,12 @@ const initialValues = { app_role: '', custom_permissions: [], + + loyaltytier: '', + + pointsbalance: '', + + referralcode: '', }; const UsersNew = () => { @@ -140,6 +146,28 @@ const UsersNew = () => { > + + + + + + + + + + + + diff --git a/frontend/src/pages/users/users-table.tsx b/frontend/src/pages/users/users-table.tsx index a408cd6..bcf1102 100644 --- a/frontend/src/pages/users/users-table.tsx +++ b/frontend/src/pages/users/users-table.tsx @@ -33,9 +33,13 @@ const UsersTablesPage = () => { { label: 'Last Name', title: 'lastName' }, { label: 'Phone Number', title: 'phoneNumber' }, { label: 'E-Mail', title: 'email' }, + { label: 'Referralcode', title: 'referralcode' }, + { label: 'Pointsbalance', title: 'pointsbalance', number: 'true' }, { label: 'App Role', title: 'app_role' }, + { label: 'Loyaltytier', title: 'loyaltytier' }, + { 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..b7260b4 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -138,6 +138,118 @@ const UsersView = () => { +
+

Loyaltytier

+ +

{users?.loyaltytier?.name ?? 'No data'}

+
+ +
+

Pointsbalance

+

{users?.pointsbalance || 'No data'}

+
+ +
+

Referralcode

+

{users?.referralcode}

+
+ + <> +

Referral Referrer

+ +
+ + + + + + + + + + {users.referral_referrer && + Array.isArray(users.referral_referrer) && + users.referral_referrer.map((item: any) => ( + + router.push( + `/referral/referral-view/?id=${item.id}`, + ) + } + > + + + + + ))} + +
ReferredemailStatus
+ {item.referredemail} + {item.status}
+
+ {!users?.referral_referrer?.length && ( +
No data
+ )} +
+ + + <> +

Redemption User

+ +
+ + + + + + + + + + + + + + {users.redemption_user && + Array.isArray(users.redemption_user) && + users.redemption_user.map((item: any) => ( + + router.push( + `/redemption/redemption-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
PointsusedRedemptiontypeDiscountvalueStatus
{item.pointsused} + {item.redemptiontype} + + {item.discountvalue} + {item.status}
+
+ {!users?.redemption_user?.length && ( +
No data
+ )} +
+ + { + const { id, query } = data; + const result = await axios.get( + `loyaltytier${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'loyaltytier/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('loyaltytier/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'loyaltytier/deleteLoyaltytier', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`loyaltytier/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'loyaltytier/createLoyaltytier', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('loyaltytier', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'loyaltytier/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('loyaltytier/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( + 'loyaltytier/updateLoyaltytier', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`loyaltytier/${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 loyaltytierSlice = createSlice({ + name: 'loyaltytier', + 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.loyaltytier = action.payload.rows; + state.count = action.payload.count; + } else { + state.loyaltytier = 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, 'Loyaltytier 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, `${'Loyaltytier'.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, `${'Loyaltytier'.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, `${'Loyaltytier'.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, 'Loyaltytier 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 } = loyaltytierSlice.actions; + +export default loyaltytierSlice.reducer; diff --git a/frontend/src/stores/redemption/redemptionSlice.ts b/frontend/src/stores/redemption/redemptionSlice.ts new file mode 100644 index 0000000..6028d20 --- /dev/null +++ b/frontend/src/stores/redemption/redemptionSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + redemption: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + redemption: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('redemption/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`redemption${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'redemption/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('redemption/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'redemption/deleteRedemption', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`redemption/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'redemption/createRedemption', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('redemption', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'redemption/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('redemption/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( + 'redemption/updateRedemption', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`redemption/${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 redemptionSlice = createSlice({ + name: 'redemption', + 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.redemption = action.payload.rows; + state.count = action.payload.count; + } else { + state.redemption = 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, 'Redemption 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, `${'Redemption'.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, `${'Redemption'.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, `${'Redemption'.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, 'Redemption 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 } = redemptionSlice.actions; + +export default redemptionSlice.reducer; diff --git a/frontend/src/stores/referral/referralSlice.ts b/frontend/src/stores/referral/referralSlice.ts new file mode 100644 index 0000000..5f50910 --- /dev/null +++ b/frontend/src/stores/referral/referralSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + referral: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + referral: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('referral/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`referral${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'referral/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('referral/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'referral/deleteReferral', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`referral/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'referral/createReferral', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('referral', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'referral/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('referral/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( + 'referral/updateReferral', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`referral/${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 referralSlice = createSlice({ + name: 'referral', + 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.referral = action.payload.rows; + state.count = action.payload.count; + } else { + state.referral = 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, 'Referral 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, `${'Referral'.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, `${'Referral'.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, `${'Referral'.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, 'Referral 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 } = referralSlice.actions; + +export default referralSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index addd439..498532f 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,9 @@ import paymentsSlice from './payments/paymentsSlice'; import vouchersSlice from './vouchers/vouchersSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import loyaltytierSlice from './loyaltytier/loyaltytierSlice'; +import referralSlice from './referral/referralSlice'; +import redemptionSlice from './redemption/redemptionSlice'; export const store = configureStore({ reducer: { @@ -28,6 +31,9 @@ export const store = configureStore({ vouchers: vouchersSlice, roles: rolesSlice, permissions: permissionsSlice, + loyaltytier: loyaltytierSlice, + referral: referralSlice, + redemption: redemptionSlice, }, });