diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4510ba8 Binary files /dev/null and b/.DS_Store differ diff --git a/frontend/angular.json b/frontend/angular.json index 608b081..2ce7b0e 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -55,7 +55,8 @@ } ], "styles": [ - "src/styles.scss" + "src/styles.scss", + "node_modules/primeflex/primeflex.css" ] }, "configurations": { @@ -63,8 +64,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" + "maximumWarning": "1MB", + "maximumError": "2MB" }, { "type": "anyComponentStyle", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6a48e3b..85f3962 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@angular/forms": "^21.1.0", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "primeflex": "^4.0.0", "primeicons": "^7.0.0", "primeng": "^17.18.0", "quill": "^2.0.3", @@ -6570,6 +6571,12 @@ "dev": true, "license": "MIT" }, + "node_modules/primeflex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/primeflex/-/primeflex-4.0.0.tgz", + "integrity": "sha512-UOEZCRjR36+sm5bUpDhS1xbA068l9VC6y1aTNVqQPtXuKIdPTqAWHRUxj3mKAoPrQ9W373ooJJMgNVXfiaw04g==", + "license": "MIT" + }, "node_modules/primeicons": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index cf7afe0..5c078e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "@angular/forms": "^21.1.0", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "primeflex": "^4.0.0", "primeicons": "^7.0.0", "primeng": "^17.18.0", "quill": "^2.0.3", diff --git a/frontend/public/assets/fonts/poppins/OFL.txt b/frontend/public/assets/fonts/poppins/OFL.txt new file mode 100644 index 0000000..da31af0 --- /dev/null +++ b/frontend/public/assets/fonts/poppins/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/public/assets/fonts/poppins/Poppins-Black.ttf b/frontend/public/assets/fonts/poppins/Poppins-Black.ttf new file mode 100644 index 0000000..71c0f99 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-Black.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-BlackItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-BlackItalic.ttf new file mode 100644 index 0000000..7aeb58b Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-BlackItalic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-Bold.ttf b/frontend/public/assets/fonts/poppins/Poppins-Bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-Bold.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-BoldItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-BoldItalic.ttf new file mode 100644 index 0000000..e61e8e8 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-BoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-ExtraBold.ttf b/frontend/public/assets/fonts/poppins/Poppins-ExtraBold.ttf new file mode 100644 index 0000000..df70936 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-ExtraBold.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-ExtraBoldItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-ExtraBoldItalic.ttf new file mode 100644 index 0000000..14d2b37 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-ExtraBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-ExtraLight.ttf b/frontend/public/assets/fonts/poppins/Poppins-ExtraLight.ttf new file mode 100644 index 0000000..e76ec69 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-ExtraLight.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-ExtraLightItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-ExtraLightItalic.ttf new file mode 100644 index 0000000..89513d9 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-ExtraLightItalic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-Italic.ttf b/frontend/public/assets/fonts/poppins/Poppins-Italic.ttf new file mode 100644 index 0000000..12b7b3c Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-Italic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-Light.ttf b/frontend/public/assets/fonts/poppins/Poppins-Light.ttf new file mode 100644 index 0000000..bc36bcc Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-Light.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-LightItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-LightItalic.ttf new file mode 100644 index 0000000..9e70be6 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-LightItalic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-Medium.ttf b/frontend/public/assets/fonts/poppins/Poppins-Medium.ttf new file mode 100644 index 0000000..6bcdcc2 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-Medium.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-MediumItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-MediumItalic.ttf new file mode 100644 index 0000000..be67410 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-MediumItalic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-Regular.ttf b/frontend/public/assets/fonts/poppins/Poppins-Regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-Regular.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-SemiBold.ttf b/frontend/public/assets/fonts/poppins/Poppins-SemiBold.ttf new file mode 100644 index 0000000..74c726e Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-SemiBold.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-SemiBoldItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-SemiBoldItalic.ttf new file mode 100644 index 0000000..3e6c942 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-SemiBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-Thin.ttf b/frontend/public/assets/fonts/poppins/Poppins-Thin.ttf new file mode 100644 index 0000000..03e7366 Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-Thin.ttf differ diff --git a/frontend/public/assets/fonts/poppins/Poppins-ThinItalic.ttf b/frontend/public/assets/fonts/poppins/Poppins-ThinItalic.ttf new file mode 100644 index 0000000..e26db5d Binary files /dev/null and b/frontend/public/assets/fonts/poppins/Poppins-ThinItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-Black.ttf b/frontend/public/assets/fonts/roboto/Roboto-Black.ttf new file mode 100644 index 0000000..d51221a Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-Black.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-BlackItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..c71c549 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-BlackItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-Bold.ttf b/frontend/public/assets/fonts/roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..9d7cf22 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-Bold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-BoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..f73d681 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-ExtraBold.ttf b/frontend/public/assets/fonts/roboto/Roboto-ExtraBold.ttf new file mode 100644 index 0000000..7092a88 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-ExtraBold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-ExtraBoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-ExtraBoldItalic.ttf new file mode 100644 index 0000000..a5536f5 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-ExtraBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-ExtraLight.ttf b/frontend/public/assets/fonts/roboto/Roboto-ExtraLight.ttf new file mode 100644 index 0000000..75608c6 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-ExtraLight.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-ExtraLightItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-ExtraLightItalic.ttf new file mode 100644 index 0000000..23dbbef Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-ExtraLightItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-Italic.ttf b/frontend/public/assets/fonts/roboto/Roboto-Italic.ttf new file mode 100644 index 0000000..c3abaef Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-Italic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-Light.ttf b/frontend/public/assets/fonts/roboto/Roboto-Light.ttf new file mode 100644 index 0000000..6fcd5f9 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-Light.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-LightItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-LightItalic.ttf new file mode 100644 index 0000000..a6e5047 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-LightItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-Medium.ttf b/frontend/public/assets/fonts/roboto/Roboto-Medium.ttf new file mode 100644 index 0000000..d629e98 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-Medium.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-MediumItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..ef9ed1b Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-MediumItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-Regular.ttf b/frontend/public/assets/fonts/roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-Regular.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-SemiBold.ttf b/frontend/public/assets/fonts/roboto/Roboto-SemiBold.ttf new file mode 100644 index 0000000..3f34834 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-SemiBold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-SemiBoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-SemiBoldItalic.ttf new file mode 100644 index 0000000..132cca1 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-SemiBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-Thin.ttf b/frontend/public/assets/fonts/roboto/Roboto-Thin.ttf new file mode 100644 index 0000000..6ee97b8 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-Thin.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto-ThinItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..0381198 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-Black.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Black.ttf new file mode 100644 index 0000000..7529d1b Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Black.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-BlackItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-BlackItalic.ttf new file mode 100644 index 0000000..0c31e9f Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-BlackItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-Bold.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Bold.ttf new file mode 100644 index 0000000..c3ccd49 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Bold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-BoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-BoldItalic.ttf new file mode 100644 index 0000000..d269187 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-BoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraBold.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraBold.ttf new file mode 100644 index 0000000..782442a Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraBold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraBoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraBoldItalic.ttf new file mode 100644 index 0000000..aeff7c2 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraLight.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraLight.ttf new file mode 100644 index 0000000..16a1560 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraLight.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraLightItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraLightItalic.ttf new file mode 100644 index 0000000..0f6fe70 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ExtraLightItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-Italic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Italic.ttf new file mode 100644 index 0000000..3b387eb Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Italic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-Light.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Light.ttf new file mode 100644 index 0000000..e70c357 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Light.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-LightItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-LightItalic.ttf new file mode 100644 index 0000000..9f623e0 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-LightItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-Medium.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Medium.ttf new file mode 100644 index 0000000..dd2842b Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Medium.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-MediumItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-MediumItalic.ttf new file mode 100644 index 0000000..80ff64e Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-MediumItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-Regular.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Regular.ttf new file mode 100644 index 0000000..5af42d4 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Regular.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-SemiBold.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-SemiBold.ttf new file mode 100644 index 0000000..4297f17 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-SemiBold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-SemiBoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-SemiBoldItalic.ttf new file mode 100644 index 0000000..6cb4656 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-SemiBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-Thin.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Thin.ttf new file mode 100644 index 0000000..1ccebcc Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-Thin.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_Condensed-ThinItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ThinItalic.ttf new file mode 100644 index 0000000..e58e966 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_Condensed-ThinItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Black.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Black.ttf new file mode 100644 index 0000000..8eedb64 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Black.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-BlackItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-BlackItalic.ttf new file mode 100644 index 0000000..19a5096 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-BlackItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Bold.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Bold.ttf new file mode 100644 index 0000000..98d7b0d Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Bold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-BoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-BoldItalic.ttf new file mode 100644 index 0000000..8604aee Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-BoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraBold.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraBold.ttf new file mode 100644 index 0000000..36423c3 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraBold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraBoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraBoldItalic.ttf new file mode 100644 index 0000000..b40ce77 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraLight.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraLight.ttf new file mode 100644 index 0000000..e1c25a0 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraLight.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraLightItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraLightItalic.ttf new file mode 100644 index 0000000..929a093 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ExtraLightItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Italic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Italic.ttf new file mode 100644 index 0000000..23454ff Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Italic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Light.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Light.ttf new file mode 100644 index 0000000..b9aedcd Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Light.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-LightItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-LightItalic.ttf new file mode 100644 index 0000000..c096473 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-LightItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Medium.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Medium.ttf new file mode 100644 index 0000000..e9c34d6 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Medium.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-MediumItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-MediumItalic.ttf new file mode 100644 index 0000000..ab34b70 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-MediumItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Regular.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Regular.ttf new file mode 100644 index 0000000..36109ba Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Regular.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-SemiBold.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-SemiBold.ttf new file mode 100644 index 0000000..6d10b33 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-SemiBold.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-SemiBoldItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-SemiBoldItalic.ttf new file mode 100644 index 0000000..e88bc4a Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-SemiBoldItalic.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Thin.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Thin.ttf new file mode 100644 index 0000000..8ed8d79 Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-Thin.ttf differ diff --git a/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ThinItalic.ttf b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ThinItalic.ttf new file mode 100644 index 0000000..81afeea Binary files /dev/null and b/frontend/public/assets/fonts/roboto/Roboto_SemiCondensed-ThinItalic.ttf differ diff --git a/frontend/public/assets/images/logo.png b/frontend/public/assets/images/logo.png new file mode 100644 index 0000000..642e236 Binary files /dev/null and b/frontend/public/assets/images/logo.png differ diff --git a/frontend/src/app/admin-layout/admin-layout.component.ts b/frontend/src/app/admin-layout/admin-layout.component.ts index 37df214..decf24b 100644 --- a/frontend/src/app/admin-layout/admin-layout.component.ts +++ b/frontend/src/app/admin-layout/admin-layout.component.ts @@ -1,81 +1,21 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; -import { MenuItem } from 'primeng/api'; -import { AuthService } from '../auth.service'; - -// PrimeNG -import { MenubarModule } from 'primeng/menubar'; -import { ButtonModule } from 'primeng/button'; +import { MenuComponent } from '../fragments/menu/menu.component'; @Component({ selector: 'app-admin-layout', standalone: true, - imports: [CommonModule, RouterModule, MenubarModule, ButtonModule], + imports: [CommonModule, RouterModule, MenuComponent], template: ` -
- - - - - -
+
+ +
` }) -export class AdminLayoutComponent implements OnInit { - items: MenuItem[] = []; - - constructor(private auth: AuthService) {} - - ngOnInit() { - this.items = [ - { - label: 'Home', - icon: 'pi pi-home', - routerLink: '/' - }, - { - label: 'OCR Module', - icon: 'pi pi-search', - items: [ - { - label: 'Upload', - icon: 'pi pi-upload', - routerLink: '/ocr' - }, - { - label: 'History', - icon: 'pi pi-history' - } - ] - - }, - { - label: 'Mailbox', - icon: 'pi pi-envelope', - routerLink: '/mailbox' - }, - { - label: 'Settings', - icon: 'pi pi-cog', - items: [ - { - label: 'Profile', - icon: 'pi pi-user' - }, - { - label: 'Preferences', - icon: 'pi pi-sliders-h' - } - ] - } - ]; - } - - logout() { - this.auth.logout(); - } +export class AdminLayoutComponent { + constructor() {} } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts deleted file mode 100644 index cd86a1e..0000000 --- a/frontend/src/app/app-routing.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { LoginComponent } from './login/login.component'; -import { AdminLayoutComponent } from './admin-layout/admin-layout.component'; -import { OcrComponent } from './ocr/ocr.component'; -import { AuthGuard } from './auth.guard'; - -const routes: Routes = [ - { path: 'login', component: LoginComponent }, - { - path: '', - component: AdminLayoutComponent, - canActivate: [AuthGuard], - children: [ - { path: '', redirectTo: 'ocr', pathMatch: 'full' }, - { path: 'ocr', component: OcrComponent } - ] - }, - { path: '**', redirectTo: '' } -]; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] -}) -export class AppRoutingModule { } diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 9118a55..000e680 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,7 +1,8 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { AuthInterceptor } from './interceptors/auth.interceptor'; import { routes } from './app.routes'; @@ -10,6 +11,6 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), - provideHttpClient() + provideHttpClient(withInterceptors([AuthInterceptor])) ] }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 8199137..c614c62 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -3,18 +3,40 @@ import { LoginComponent } from './login/login.component'; import { AdminLayoutComponent } from './admin-layout/admin-layout.component'; import { OcrComponent } from './ocr/ocr.component'; import { MailboxComponent } from './mailbox/mailbox.component'; -import { AuthGuard } from './auth.guard'; + +import { AuthorizeComponent } from './pages/session/auth/authorize.component'; +import { AuthorizeGuard } from './interceptors/authorize.guard'; +import { ProfileComponent } from './pages/session/profile/profile.component'; +import { SubsidiaryComponent } from './pages/account/company/subsidiary/subsidiary.component'; +import { DepartmentComponent } from './pages/account/company/department/department.component'; +import { DesignationComponent } from './pages/account/company/desigation/designation.component'; +import { EmployeeComponent } from './pages/account/company/employee/employee.component'; +import { UserComponent } from './pages/account/user/user.component'; export const routes: Routes = [ - { path: 'login', component: LoginComponent }, + { path: '', component: LoginComponent }, + { path: 'authorize', component: AuthorizeComponent, canActivate: [AuthorizeGuard]}, { - path: '', + path: 'dashboard', component: AdminLayoutComponent, - canActivate: [AuthGuard], + canActivate: [AuthorizeGuard], + canActivateChild: [AuthorizeGuard], children: [ { path: '', redirectTo: 'ocr', pathMatch: 'full' }, { path: 'ocr', component: OcrComponent }, - { path: 'mailbox', component: MailboxComponent } + { path: 'mailbox', component: MailboxComponent }, + { path: 'user', + canActivate: [AuthorizeGuard], + canActivateChild: [AuthorizeGuard], + children: [ + { path: 'profile', component: ProfileComponent }, + { path: 'subsidiaries', component: SubsidiaryComponent }, + { path: 'departments', component: DepartmentComponent }, + { path: 'designations', component: DesignationComponent }, + { path: 'employees', component: EmployeeComponent }, + { path: 'users', component: UserComponent } + ] + } ] }, { path: '**', redirectTo: '' } diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts deleted file mode 100644 index d070bb4..0000000 --- a/frontend/src/app/auth.guard.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; -import { CanActivate, Router } from '@angular/router'; -import { AuthService } from './auth.service'; - -@Injectable({ - providedIn: 'root' -}) -export class AuthGuard implements CanActivate { - constructor(private authService: AuthService, private router: Router) {} - - canActivate(): boolean { - if (this.authService.isLoggedIn()) { - return true; - } else { - this.router.navigate(['/login']); - return false; - } - } -} diff --git a/frontend/src/app/auth.service.ts b/frontend/src/app/auth.service.ts deleted file mode 100644 index b03639f..0000000 --- a/frontend/src/app/auth.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Router } from '@angular/router'; -import { Observable, of, tap } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class AuthService { - private apiUrl = '/ocrb/api'; - - constructor(private http: HttpClient, private router: Router) { } - - login(username: string, password: string): Observable { - return this.http.post(`${this.apiUrl}/login`, { username, password }).pipe( - tap((res: any) => { - if (res.token) { - localStorage.setItem('token', res.token); - } - }) - ); - } - - logout() { - localStorage.removeItem('token'); - this.router.navigate(['/login']); - } - - isLoggedIn(): boolean { - return !!localStorage.getItem('token'); - } -} diff --git a/frontend/src/app/fragments/menu/menu.component.html b/frontend/src/app/fragments/menu/menu.component.html new file mode 100644 index 0000000..e4083be --- /dev/null +++ b/frontend/src/app/fragments/menu/menu.component.html @@ -0,0 +1,21 @@ +
+ + + + + +
+
+ + {{ companyName }} + + + {{ name }} | {{ branchName }} + +
+ + +
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/fragments/menu/menu.component.ts b/frontend/src/app/fragments/menu/menu.component.ts new file mode 100644 index 0000000..07575d0 --- /dev/null +++ b/frontend/src/app/fragments/menu/menu.component.ts @@ -0,0 +1,114 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { MenuItem } from 'primeng/api'; +import { MenubarModule } from 'primeng/menubar'; +import { HttpService } from '../../services/http.service'; +import { Request } from '../../models/request.model'; +import { AvatarModule } from 'primeng/avatar'; +import { MenuModule } from 'primeng/menu'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { SessionService } from '../../services/commons/session.service'; +import { EncryptionService } from '../../services/utilities/encryption.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { environment } from '../../../environments/environment'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-menu', + standalone: true, + imports: [CommonModule, MenubarModule, AvatarModule, ButtonModule, MenuModule, InputTextModule, TooltipModule], + templateUrl: './menu.component.html' +}) +export class MenuComponent implements OnInit { + items: MenuItem[] = []; + profileItems: MenuItem[] | undefined; + companyName: string = ''; + branchName: string = ''; + roleName: string = ''; + name: string = ''; + constructor(private router: Router, private http: HttpService, private session: SessionService, private enc: EncryptionService) { } + ngOnInit() { + if (typeof window !== 'undefined' && sessionStorage) { + let menuString = this.session.getItem("nav"); + if (menuString) { + this.enc.decrypt(menuString).then(decrypted => { + if (decrypted) { + this.items = JSON.parse(decrypted).map((m: any) => this.mapMenu(m)); + console.log(this.items); + } + }); + } + // SessionService now returns parsed objects + const companies = this.session.getItem("companies"); + const userDetails = this.session.getItem('userDetails'); + + this.name = userDetails ? userDetails.displayName : ''; + this.companyName = (companies && companies.length > 0) ? companies[0].companyName : ''; + this.branchName = (companies && companies.length > 0) ? companies[0].branches[0].branchCode : ''; + this.roleName = (companies && companies.length > 0) ? companies[0].branches[0].roles[0].groupName : ''; + } + this.profileItems = [ + { + label: this.roleName, + items: [ + { + label: 'Details', + icon: 'pi pi-id-card', + command: () => { + this.router.navigateByUrl(`/dashboard/user/profile`); + } + }, + { + label: 'Sign Out', + icon: 'pi pi-sign-out', + command: () => { + this.logout(); + } + } + ] + } + ]; + } + + + logout() { + const requestPayload: Request = { + data: '', + compressed: false + }; + this.http.post(environment.authService + '/signout', requestPayload).subscribe({ + next: () => { + sessionStorage.clear(); + this.router.navigate(['/']); + }, + error: (err) => { + console.error('Logout failed', err); + } + }); + } + + mapMenu(menu: any): MenuItem { + const hasChildren = Array.isArray(menu.items) && menu.items.length > 0; + + return { + label: menu.label, + icon: menu.icon, + routerLink: !hasChildren && menu.route !== '#' + ? `/dashboard/user/${menu.route}` + : undefined, + command: !hasChildren && menu.route !== '#' + ? () => { + this.router.navigateByUrl(`/dashboard/user/${menu.route}`); + } + : undefined, + items: hasChildren + ? menu.items.map((c: any) => this.mapMenu(c)) + : undefined + }; + } + + gotToDashboard() { + this.router.navigate(['/']); + } +} diff --git a/frontend/src/app/interceptors/auth.interceptor.ts b/frontend/src/app/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..1a87534 --- /dev/null +++ b/frontend/src/app/interceptors/auth.interceptor.ts @@ -0,0 +1,139 @@ +import { SessionService } from './../services/commons/session.service'; +import { + HttpInterceptorFn, + HttpRequest, + HttpHandlerFn, + HttpEvent, + HttpResponse, + HttpErrorResponse, + HttpHeaders +} from '@angular/common/http'; +import { Observable, tap, catchError, throwError, from, switchMap } from 'rxjs'; +import { EncryptionService } from '../services/utilities/encryption.service'; +import { inject } from '@angular/core'; +import { ResponseDto } from '../models/response.dto'; + +export const AuthInterceptor: HttpInterceptorFn = ( + req: HttpRequest, + next: HttpHandlerFn +): Observable> => { + + const encryptionService = inject(EncryptionService); + const sessionService = inject(SessionService); + + /* ----------------------------------------- + * 1️⃣ Always attach Authorization header + * ----------------------------------------- */ + // OCR uses sessionStorage for token now (migrated previously) + const token = sessionStorage.getItem('token'); + + let reqHeaders = req.headers.set('Content-Type', 'application/json'); + + if (token) { + reqHeaders = reqHeaders.set( + 'Authorization', + `Bearer ${token}` + ); + } + + /* ----------------------------------------- + * 2️⃣ Handle encrypted (compressed) requests + * ----------------------------------------- */ + if ( + req.body && + typeof req.body === 'object' && + req.body.compressed + ) { + return from(encryptionService.encrypt(req.body.data)).pipe( + switchMap(encryptedBody => { + + const payload = { + scopes: req.body.scopes ?? [], + data: encryptedBody, + target: req.body.target ?? null, + compressed: true + }; + + const modifiedReq = req.clone({ + body: payload, + headers: reqHeaders + }); + + // Use 'any' type for event to avoid strict type mismatch if ResponseDto structure varies + return next(modifiedReq).pipe( + tap((event: any) => handleResponse(event, sessionService, encryptionService)), + catchError((error: HttpErrorResponse) => + throwError(() => error) + ) + ); + }) + ); + } + + /* ----------------------------------------- + * 3️⃣ Non-encrypted requests (GET included) + * ----------------------------------------- */ + const modifiedReq = req.clone({ + headers: reqHeaders + }); + + return next(modifiedReq).pipe( + tap((event: any) => handleResponse(event, sessionService, encryptionService)), + catchError((error: HttpErrorResponse) => + throwError(() => error) + ) + ); +}; + +function handleResponse( + event: HttpEvent, + sessionService: SessionService, + encryptionService: EncryptionService +): void { + + if (!(event instanceof HttpResponse) || !event.ok) { + return; + } + + // Handle Authenticate Response + if (event.url?.endsWith('/authenticate')) { + const response = event.body as ResponseDto; + if (response?.data?.token) { + sessionStorage.setItem('token', response.data.token); + } + if (response?.data?.companies) { + sessionService.setItem('companies', response.data.companies); + } + } + + // Handle Authorize Response + if (event.url?.endsWith('/authorize')) { + const response = event.body as ResponseDto; + // Clearing session might be too aggressive if we just logged in, but following source logic + // sessionService.clear(); + + if (response?.data?.token) { + sessionStorage.setItem('token', response.data.token); + } + if (response?.data?.refreshToken) { + sessionService.setItem('refreshToken', response.data.refreshToken); + } + if (response?.data?.userDetails) { + sessionService.setItem('userDetails', response.data.userDetails); + } + + if (response?.data?.data) { + encryptionService.encrypt(response.data.data) + .then(encryptedNav => { + sessionService.setItem('nav', encryptedNav); + }); + } + + if (response?.data?.companies) { + sessionService.setItem( + 'companies', + response.data.companies + ); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/interceptors/authorize.guard.ts b/frontend/src/app/interceptors/authorize.guard.ts new file mode 100644 index 0000000..0789af8 --- /dev/null +++ b/frontend/src/app/interceptors/authorize.guard.ts @@ -0,0 +1,40 @@ +import { Injectable, inject, PLATFORM_ID } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'; +import { isPlatformBrowser } from '@angular/common'; + +@Injectable({ providedIn: 'root' }) +export class AuthorizeGuard implements CanActivate, CanActivateChild { + + private platformId = inject(PLATFORM_ID); + private router = inject(Router); + + private checkAuth(): boolean { + if (!isPlatformBrowser(this.platformId)) { + return false; + } + + const token = sessionStorage.getItem('token'); + + if (!token) { + sessionStorage.clear(); + this.router.navigate(['/']); + return false; + } + + return true; + } + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): boolean { + return this.checkAuth(); + } + + canActivateChild( + childRoute: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): boolean { + return this.checkAuth(); + } +} diff --git a/frontend/src/app/login/login.component.css b/frontend/src/app/login/login.component.css new file mode 100644 index 0000000..fe96441 --- /dev/null +++ b/frontend/src/app/login/login.component.css @@ -0,0 +1,42 @@ +::ng-deep .p-password-input { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.login-page { + height: 99vh; +} + +::ng-deep .p-card { + position: relative; + border-radius: 24px !important; + background: white; + padding: 1rem; + overflow: hidden; +} + +/* Gradient border */ +::ng-deep .p-card::before { + content: ""; + position: absolute; + inset: 0; + padding: 3px; /* border thickness */ + border-radius: 24px; + filter: drop-shadow(0 0 12px rgba(91, 185, 138, 0.35)); + background: linear-gradient( + 180deg, + #5bb98a 0%, + rgba(91, 185, 138, 0.6) 40%, + rgba(91, 185, 138, 0.15) 70%, + transparent 100% + ); + + /* Mask trick = border only */ + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + + pointer-events: none; +} diff --git a/frontend/src/app/login/login.component.html b/frontend/src/app/login/login.component.html new file mode 100644 index 0000000..61006ed --- /dev/null +++ b/frontend/src/app/login/login.component.html @@ -0,0 +1,54 @@ + diff --git a/frontend/src/app/login/login.component.ts b/frontend/src/app/login/login.component.ts index 221bde1..0081f4b 100644 --- a/frontend/src/app/login/login.component.ts +++ b/frontend/src/app/login/login.component.ts @@ -1,77 +1,67 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { CommonModule} from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { AuthService } from '../auth.service'; -import { Router } from '@angular/router'; -import { MessageService } from 'primeng/api'; - -// PrimeNG Imports -import { CardModule } from 'primeng/card'; -import { InputTextModule } from 'primeng/inputtext'; +import { Component } from '@angular/core'; import { ButtonModule } from 'primeng/button'; -import { ToastModule } from 'primeng/toast'; +import { InputTextModule } from 'primeng/inputtext'; +import { CardModule } from 'primeng/card'; +import { PasswordModule } from 'primeng/password'; +import { KeyFilterModule } from 'primeng/keyfilter'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule} from '@angular/forms'; +import { Request } from '../models/request.model'; +import { HttpService } from '../services/http.service'; +import { MessageModule } from 'primeng/message'; +import { Router } from '@angular/router'; +import { environment } from '../../environments/environment'; +import { BadgeModule } from "primeng/badge"; @Component({ selector: 'app-login', + templateUrl: './login.component.html', standalone: true, + styleUrl: './login.component.css', imports: [ CommonModule, FormsModule, + ReactiveFormsModule, CardModule, - InputTextModule, ButtonModule, - ToastModule - ], - providers: [MessageService], - template: ` - - `, - styles: [` - .login-container { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - background: var(--surface-card); - } - .w-full { width: 100%; } - .mt-4 { margin-top: 1.5rem; } - /* PrimeNG handles the rest */ - `] + InputTextModule, + PasswordModule, + KeyFilterModule, + MessageModule, + BadgeModule + ] }) export class LoginComponent { - username = ''; - password = ''; + loginForm: FormGroup; + message?: string; - constructor(private auth: AuthService, private router: Router, private messageService: MessageService) {} - - onLogin() { - this.auth.login(this.username, this.password).subscribe({ - next: () => { - this.router.navigate(['/']); - }, - error: () => { - this.messageService.add({severity:'error', summary:'Error', detail:'Invalid Credentials'}); - } + constructor(private fb: FormBuilder, private http: HttpService, private router: Router) { + this.loginForm = this.fb.group({ + username: ['', Validators.required], + password: ['', Validators.required] }); } + + onLogin() { + this.message = ''; + if (this.loginForm.valid) { + const requestPayload: Request = { + data: this.loginForm.value, + compressed: true, + target: 'models.auth.Login' + }; + // Matching cygnus-ui endpoint structure + this.http.post(`${environment.authService}/3z4mkell5g5aset/authenticate`, requestPayload).subscribe({ + next: (response) => { + // Token is handled by AuthInterceptor now for 'authenticate' endpoint + // But we can double check or just navigate + this.router.navigate(['/authorize']); + }, + error: (err) => { + this.message = err.message || 'Login Failed'; + } + }); + } + } } diff --git a/frontend/src/app/models/account.model.ts b/frontend/src/app/models/account.model.ts new file mode 100644 index 0000000..ecc426e --- /dev/null +++ b/frontend/src/app/models/account.model.ts @@ -0,0 +1,91 @@ +export class SubsidiaryDTO { + id?: string; + companyId?: string; + companyName?: string; + code?: string; + name?: string; + officeNo?: string; + street?: string; + locality?: string; + cityId?: string; + stateId?: string; + cityName?: string; + stateName?: string; + pinCode?: string; + emailId?: string; + contactNo?: string; + contactPerson?: string; + panNo?: string; + cinNo?: string; + msmeNo?: string; + updatedAt?: Date; + updatedBy?: string; + updatedUser?: string; + active?: boolean; + + constructor(init?: Partial) { + Object.assign(this, init); + if (init?.updatedAt) { + this.updatedAt = new Date(init.updatedAt); + } + } +} + +export interface DepartmentDTO { + id: string; + companyId: string; + department: string; + parentDepartment?: string; + parentDepartmentName?: string; + createdUser: string; + updatedAt: string; + updatedUser: string; + active: boolean; +} + +export interface DesignationDTO { + id: string; + companyId: string; + departmentId: string; + departmentName: string; + designation: string; + createdUser: string; + updatedAt: string; + updatedUser: string; + active: boolean; + hod: boolean; + payGrade?: string; +} + +export interface EmployeeDTO { + id: string; + subsidiaryId: string; + departmentId: string; + designationId: string; + subsidiaryName: string; + department: string; + designation: string; + employeeId?: string; + joiningDate?: string; + fullName: string; + fatherName?: string; + gender?: string; + dob?: string; + residenceAddress?: string; + residenceCityId?: string; + residenceStateId?: string; + residenceCityName?: string; + residenceStateName?: string; + permanentAddress?: string; + permanentCityId?: string; + permanentStateId?: string; + permanentCityName?: string; + permanentStateName?: string; + contactNo: string; + alternateNo?: string; + emailId: string; + createdUser: string; + updatedAt: string; + updatedUser: string; + active: boolean; +} \ No newline at end of file diff --git a/frontend/src/app/models/masters/masters.ts b/frontend/src/app/models/masters/masters.ts new file mode 100644 index 0000000..9d93f0e --- /dev/null +++ b/frontend/src/app/models/masters/masters.ts @@ -0,0 +1,16 @@ +export interface CityDTO { + id?:string, + stateId?:string, + stateName?:string, + cityName?:string, + stateCode?:string, + gstCode?:string +} + +export interface SearchDTO { + module?:string, + searchBy?:string, + searchValue?:string, + offset?:number, + limit?:number +} \ No newline at end of file diff --git a/frontend/src/app/models/masters/member/memberlist.ts b/frontend/src/app/models/masters/member/memberlist.ts new file mode 100644 index 0000000..d823bf6 --- /dev/null +++ b/frontend/src/app/models/masters/member/memberlist.ts @@ -0,0 +1,16 @@ +export interface MemberList { + id:string, + memberno:string, + membername:string, + fathername:string, + mobileno?:string, + membershipdate?:Date, + emailid?:string, + dob?:Date, + age?:number, + gender?:string, + status:string + isactive:boolean, + updatedat:Date, + updatedby:string +} diff --git a/frontend/src/app/models/request.model.ts b/frontend/src/app/models/request.model.ts new file mode 100644 index 0000000..0f44bda --- /dev/null +++ b/frontend/src/app/models/request.model.ts @@ -0,0 +1,6 @@ +export interface Request { + scopes?: string[]; + data: T; + target?: string; + compressed?: boolean; +} diff --git a/frontend/src/app/models/response.dto.ts b/frontend/src/app/models/response.dto.ts new file mode 100644 index 0000000..f3ce991 --- /dev/null +++ b/frontend/src/app/models/response.dto.ts @@ -0,0 +1,6 @@ +export interface ResponseDto { + statuscode : number; + message?: string; + description?: string; + data?: any; +} diff --git a/frontend/src/app/models/session.model.ts b/frontend/src/app/models/session.model.ts new file mode 100644 index 0000000..07687cf --- /dev/null +++ b/frontend/src/app/models/session.model.ts @@ -0,0 +1,35 @@ +export interface Company { + id?: string; + companyName?: string; + companyCode?: string; + branches?: Branch[]; +} + +export interface Branch { + id?: string; + branchName?: string; + branchCode?: string; + roles?: Role[]; +} + +export interface Role { + roleName?: string; + groupName?: string; + defaultRole?: boolean; +} + +export interface UserProfile { + username?: string; + employeeId?: string; + joiningDate?: Date; + displayName?: string; + department?: string; + designation?: string; + name?: string; + fatherName?: string; + gender?: string; + dob?: Date; + contactNo?: string; + alternateContactNo?: string; + emailId?: string; +} diff --git a/frontend/src/app/models/user.model.ts b/frontend/src/app/models/user.model.ts new file mode 100644 index 0000000..8589f36 --- /dev/null +++ b/frontend/src/app/models/user.model.ts @@ -0,0 +1,29 @@ +export interface UserDTO { + id?: string; + loginId?: string; + loginPassword?: string; + displayName?: string; + fkEmployeeId?: string; + employeeId?: string; + employeeName?: string; + fatherName?: string; + department?: string; + designation?: string; + status?: string; + updatedAt?: string; + updatedUser?: string; + active?: boolean; + userRoles?: UserRoleDTO[]; +} + +export interface UserRoleDTO { + id?: string; + roleId?: string; + branchId?: string; + roleName?: string; + groupName?: string; + branchName?: string; + updatedAt?: string; + updatedUser?: string; + active?: boolean; +} \ No newline at end of file diff --git a/frontend/src/app/pages/account/company/department/department.component.css b/frontend/src/app/pages/account/company/department/department.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/app/pages/account/company/department/department.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/pages/account/company/department/department.component.html b/frontend/src/app/pages/account/company/department/department.component.html new file mode 100644 index 0000000..12efa7b --- /dev/null +++ b/frontend/src/app/pages/account/company/department/department.component.html @@ -0,0 +1,162 @@ +
+ + + + + + + + + + + + + + +
+

Manage Departments

+ + + + +
+
+ + + # + +
+ Department + +
+ + +
+ Parent Department + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ department.department }} + + + {{ department.parentDepartmentName }} + + + + Not Available + + + {{ department.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ department.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/company/department/department.component.spec.ts b/frontend/src/app/pages/account/company/department/department.component.spec.ts new file mode 100644 index 0000000..57045f3 --- /dev/null +++ b/frontend/src/app/pages/account/company/department/department.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DepartmentComponent } from './department.component'; + +describe('DepartmentComponent', () => { + let component: DepartmentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DepartmentComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(DepartmentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/account/company/department/department.component.ts b/frontend/src/app/pages/account/company/department/department.component.ts new file mode 100644 index 0000000..5b50835 --- /dev/null +++ b/frontend/src/app/pages/account/company/department/department.component.ts @@ -0,0 +1,283 @@ +import { DepartmentDTO } from './../../../../models/account.model'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { TableModule, Table } from 'primeng/table'; +import { DialogModule } from 'primeng/dialog'; +import { RippleModule } from 'primeng/ripple'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { CommonModule } from '@angular/common'; +import { FileUploadModule } from 'primeng/fileupload'; +import { DropdownModule } from 'primeng/dropdown'; +import { TagModule } from 'primeng/tag'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { RatingModule } from 'primeng/rating'; +import { SkeletonModule } from 'primeng/skeleton'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { InputNumberModule } from 'primeng/inputnumber'; +import { IconFieldModule } from 'primeng/iconfield'; +import { InputIconModule } from 'primeng/inputicon'; +import { CompanyService } from '../../../../services/account/company/company.service'; +import { ValidationService } from '../../../../services/utilities/validation.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { AutoCompleteModule } from 'primeng/autocomplete'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-department', + templateUrl: './department.component.html', + standalone: true, + imports: [ + CommonModule, FormsModule, ReactiveFormsModule, + TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, + InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule, + RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule, + InputIconModule, TooltipModule, AutoCompleteModule, RippleModule + ], + providers: [MessageService, ConfirmationService], + styleUrl: './department.component.css' +}) +export class DepartmentComponent implements OnInit{ + departmentForm: FormGroup; + departmentDialog: boolean = false; + departments!: DepartmentDTO[]; + + department: DepartmentDTO | undefined; + + selectedDepartments!: DepartmentDTO[] | null; + + submitted: boolean = false; + + isLoading: boolean = true; + + skeletonData: any[] = Array(10).fill({}); + + statuses!: any[]; + + departmentOptions: any[] = []; + + @ViewChild('dt') dt!: Table; + + cols!: Column[]; + + exportColumns!: ExportColumn[]; + + constructor( + private companyService: CompanyService, + private messageService: MessageService, + private confirmationService: ConfirmationService, + private cd: ChangeDetectorRef, + private fb: FormBuilder, + ) { + + this.departmentForm = this.fb.group({ + id: [{ value: '', disabled: true }], + department: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]], + parentDepartment: [{ value: '', disabled: false }], + parentDepartmentName: [{ value: '', disabled: true }], + active: [{ value: true, disabled: false }] + }); + } + + exportCSV() { + this.dt.exportCSV(); + } + + ngOnInit() { + this.loadAllDepartments(); + } + + loadAllDepartments() { + this.isLoading = true; + this.companyService.getAllDepartments().subscribe({ + next: (data) => { + this.isLoading = false; + this.departments = data; + this.departmentOptions = [ + { label: 'None', value: '' }, + ...this.departments.map(d => ({ label: d.department, value: d.id })) + ]; + this.cd.markForCheck(); + }, + error: (err) => { + this.isLoading = false; + console.error(err); + } + }); + + this.statuses = [ + { label: 'Active', value: true }, + { label: 'Inactive', value: false } + ]; + + this.cols = [ + { field: 'department', header: 'Department', customExportHeader: 'Department' }, + { field: 'parentDepartmentName', header: 'Parent Department' }, + { field: 'updatedAt', header: 'Last Updated At' }, + { field: 'updatedUser', header: 'Last Updated By' } + ]; + + this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field })); + } + + openNew() { + this.department = undefined; + this.departmentForm.reset(); + this.submitted = false; + this.departmentDialog = true; + } + + editDepartment(department: DepartmentDTO) { + const parentDepartment = this.departmentOptions.find(dep => dep.label === department.parentDepartmentName); + this.departmentForm.reset(); + this.department = { ...department }; + this.department.parentDepartment = parentDepartment?.value ?? ''; + this.departmentForm.patchValue(this.department); + this.departmentDialog = true; + } + + + hideDialog() { + this.departmentDialog = false; + this.submitted = false; + } + + toggleActive(department: DepartmentDTO) { + const isActivating = !department.active; + this.confirmationService.confirm({ + message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + department.department + '?', + header: 'Confirm', + icon: 'pi pi-exclamation-triangle', + rejectButtonStyleClass: 'p-button-text p-button-secondary', + acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger', + accept: () => { + this.companyService.activateDeactivateDepartment(department.id, isActivating).subscribe({ + next: (updatedDepartment) => { + department.active = updatedDepartment.active; + this.departments = [...this.departments]; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: `Department ${isActivating ? 'Activated' : 'Deactivated'}`, + life: 3000 + }); + }, + error: (err) => { + console.error('Error toggling department active status', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to update department status', + life: 3000 + }); + } + }); + } + }); + } + + getSeverity(status: boolean) { + switch (status) { + case true: + return 'success'; + case false: + return 'warning'; + } + } + + getErrorMessage(fieldName: string): string { + const control = this.departmentForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isFieldInvalid(fieldName: string): boolean { + const control = this.departmentForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submitted)); + } + + saveDepartment() { + this.submitted = true; + + if (this.departmentForm.invalid) { + return; + } + + const departmentData = this.departmentForm.getRawValue() as DepartmentDTO; + + // ✅ Normalize null → empty array + const departments = this.departments ?? []; + + // 🔍 Duplicate check (case-insensitive) + const existing = departments.some(dep => + dep.department?.trim().toLowerCase() === departmentData.department?.trim().toLowerCase() && + dep.id !== departmentData.id + ); + + if (existing) { + this.messageService.add({ + severity: 'error', + summary: 'Validation Error', + detail: 'Department name already exists', + life: 3000 + }); + return; + } + + this.companyService.saveDepartment(departmentData).subscribe({ + next: (savedDepartment) => { + + const index = departmentData.id + ? departments.findIndex(dep => dep.id === departmentData.id) + : -1; + + if (index !== -1) { + // ✅ UPDATE + departments[index] = savedDepartment; + } else { + // ✅ CREATE + departments.push(savedDepartment); + } + + // ✅ Reassign once (change detection + null safety) + this.departments = [...departments]; + + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: index !== -1 + ? 'Department Updated' + : 'Department Created', + life: 3000 + }); + + this.departmentDialog = false; + this.department = undefined; + this.departmentForm.reset(); + this.submitted = false; + }, + error: (err) => { + console.error('Error saving department', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to save department', + life: 3000 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.css b/frontend/src/app/pages/account/company/desigation/designation.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/app/pages/account/company/desigation/designation.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.html b/frontend/src/app/pages/account/company/desigation/designation.component.html new file mode 100644 index 0000000..7a0728d --- /dev/null +++ b/frontend/src/app/pages/account/company/desigation/designation.component.html @@ -0,0 +1,183 @@ +
+ + + + + + + + + + + + + + +
+

Manage Designations

+ + + + +
+
+ + + # + +
+ Designation + +
+ + +
+ Department + +
+ + +
+ HOD + +
+ + +
+ Pay Grade + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ designation.designation }} + {{ designation.departmentName }} + {{ designation.hod ? 'Yes' : 'No' }} + {{ designation.payGrade || 'N/A' }} + {{ designation.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ designation.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.spec.ts b/frontend/src/app/pages/account/company/desigation/designation.component.spec.ts new file mode 100644 index 0000000..de59e48 --- /dev/null +++ b/frontend/src/app/pages/account/company/desigation/designation.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DesignationComponent } from './designation.component'; + +describe('DesignationComponent', () => { + let component: DesignationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DesignationComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(DesignationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.ts b/frontend/src/app/pages/account/company/desigation/designation.component.ts new file mode 100644 index 0000000..18e6ca4 --- /dev/null +++ b/frontend/src/app/pages/account/company/desigation/designation.component.ts @@ -0,0 +1,295 @@ +import { DesignationDTO, DepartmentDTO } from './../../../../models/account.model'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { TableModule, Table } from 'primeng/table'; +import { DialogModule } from 'primeng/dialog'; +import { RippleModule } from 'primeng/ripple'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { CommonModule } from '@angular/common'; +import { FileUploadModule } from 'primeng/fileupload'; +import { DropdownModule } from 'primeng/dropdown'; +import { TagModule } from 'primeng/tag'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { RatingModule } from 'primeng/rating'; +import { SkeletonModule } from 'primeng/skeleton'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { InputNumberModule } from 'primeng/inputnumber'; +import { CompanyService } from '../../../../services/account/company/company.service'; +import { ValidationService } from '../../../../services/utilities/validation.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { CheckboxModule } from 'primeng/checkbox'; +import { AutoCompleteModule } from 'primeng/autocomplete'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-designation', + templateUrl: './designation.component.html', + standalone: true, + imports: [ + CommonModule, FormsModule, ReactiveFormsModule, + TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, + InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule, + RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, + TooltipModule, AutoCompleteModule, RippleModule, CheckboxModule + ], + providers: [MessageService, ConfirmationService], + styleUrl: './designation.component.css' +}) +export class DesignationComponent implements OnInit{ + designationForm: FormGroup; + designationDialog: boolean = false; + designations!: DesignationDTO[]; + + designation: DesignationDTO | undefined; + + selectedDesignations!: DesignationDTO[] | null; + + submitted: boolean = false; + + isLoading: boolean = true; + + skeletonData: any[] = Array(10).fill({}); + + departments: DepartmentDTO[] = []; + + statuses!: any[]; + + @ViewChild('dt') dt!: Table; + + cols!: Column[]; + + exportColumns!: ExportColumn[]; + + constructor( + private companyService: CompanyService, + private messageService: MessageService, + private confirmationService: ConfirmationService, + private cd: ChangeDetectorRef, + private fb: FormBuilder, + ) { + + this.designationForm = this.fb.group({ + id: [{ value: '', disabled: true }], + designation: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]], + departmentId: [{ value: null, disabled: false }, [Validators.required]], + hod: [{ value: false, disabled: false }], + active: [{ value: true, disabled: false }] + }); + } + + exportCSV() { + this.dt.exportCSV(); + } + + ngOnInit() { + this.loadAllDesignations(); + this.loadDepartments(); + } + + loadDepartments() { + this.companyService.getAllDepartments().subscribe({ + next: (data) => { + this.departments = data.filter(d => d.active); + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Error loading departments', err); + } + }); + } + + loadAllDesignations() { + this.isLoading = true; + this.companyService.getAllDesignations().subscribe({ + next: (data) => { + this.isLoading = false; + this.designations = data; + this.cd.markForCheck(); + }, + error: (err) => { + this.isLoading = false; + console.error(err); + } + }); + + this.statuses = [ + { label: 'Active', value: true }, + { label: 'Inactive', value: false } + ]; + + this.cols = [ + { field: 'designation', header: 'Designation', customExportHeader: 'Designation' }, + { field: 'departmentName', header: 'Department' }, + { field: 'hod', header: 'HOD' }, + { field: 'updatedAt', header: 'Last Updated At' }, + { field: 'updatedUser', header: 'Last Updated By' } + ]; + + this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field })); + } + + openNew() { + this.designation = undefined; + this.designationForm.reset(); + this.submitted = false; + this.designationDialog = true; + } + + editDesignation(designation: DesignationDTO) { + const parentDepartment = this.departments.find(dep => dep.department === designation.departmentName); + console.log(parentDepartment); + this.designationForm.reset(); + this.designation = { ...designation }; + this.designation.departmentId = parentDepartment?.id ?? ''; + console.log(this.designation); + this.designationForm.patchValue(this.designation); + this.designationDialog = true; + } + + + hideDialog() { + this.designationDialog = false; + this.submitted = false; + } + + toggleActive(designation: DesignationDTO) { + const isActivating = !designation.active; + this.confirmationService.confirm({ + message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + designation.designation + '?', + header: 'Confirm', + icon: 'pi pi-exclamation-triangle', + rejectButtonStyleClass: 'p-button-text p-button-secondary', + acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger', + accept: () => { + this.companyService.activateDeactivateDesignation(designation.id, isActivating).subscribe({ + next: (updatedDesignation) => { + designation.active = updatedDesignation.active; + this.designations = [...this.designations]; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: `Designation ${isActivating ? 'Activated' : 'Deactivated'}`, + life: 3000 + }); + }, + error: (err) => { + console.error('Error toggling designation active status', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to update designation status', + life: 3000 + }); + } + }); + } + }); + } + + getSeverity(status: boolean) { + switch (status) { + case true: + return 'success'; + case false: + return 'warning'; + } + } + + getErrorMessage(fieldName: string): string { + const control = this.designationForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isFieldInvalid(fieldName: string): boolean { + const control = this.designationForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submitted)); + } + + saveDesignation() { + this.submitted = true; + + if (this.designationForm.invalid) { + return; + } + + const designationData = this.designationForm.getRawValue() as DesignationDTO; + + // ✅ Normalize null → empty array + const designations = this.designations ?? []; + + // 🔍 Duplicate check (case-insensitive, department-specific) + const existing = designations.some(des => + des.designation?.trim().toLowerCase() === designationData.designation?.trim().toLowerCase() && + des.departmentId === designationData.departmentId && + des.id !== designationData.id + ); + + if (existing) { + this.messageService.add({ + severity: 'error', + summary: 'Validation Error', + detail: 'Designation already exists in this department', + life: 3000 + }); + return; + } + + this.companyService.saveDesignation(designationData).subscribe({ + next: (savedDesignation) => { + + const index = designationData.id + ? designations.findIndex(des => des.id === designationData.id) + : -1; + + if (index !== -1) { + // ✅ UPDATE + designations[index] = savedDesignation; + } else { + // ✅ CREATE + designations.push(savedDesignation); + } + + // ✅ Reassign once (change detection + null safety) + this.designations = [...designations]; + + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: index !== -1 + ? 'Designation Updated' + : 'Designation Created', + life: 3000 + }); + + this.designationDialog = false; + this.designation = undefined; + this.designationForm.reset(); + this.submitted = false; + }, + error: (err) => { + console.error('Error saving designation', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to save designation', + life: 3000 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/account/company/employee/employee.component.css b/frontend/src/app/pages/account/company/employee/employee.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/app/pages/account/company/employee/employee.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/pages/account/company/employee/employee.component.html b/frontend/src/app/pages/account/company/employee/employee.component.html new file mode 100644 index 0000000..3a8c54a --- /dev/null +++ b/frontend/src/app/pages/account/company/employee/employee.component.html @@ -0,0 +1,411 @@ +
+ + + + + + + + + + + + + + +
+

Manage Employees

+ + + + +
+
+ + + # + +
+ Full Name + +
+ + +
+ Subsidiary + +
+ + +
+ Department + +
+ + +
+ Designation + +
+ + +
+ Contact No + +
+ + +
+ Email + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ employee.fullName }} + {{ employee.subsidiaryName }} + {{ employee.department }} + {{ employee.designation }} + {{ employee.contactNo }} + {{ employee.emailId }} + {{ employee.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ employee.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
Permanent Address
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
Residence Address
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/company/employee/employee.component.spec.ts b/frontend/src/app/pages/account/company/employee/employee.component.spec.ts new file mode 100644 index 0000000..6f30928 --- /dev/null +++ b/frontend/src/app/pages/account/company/employee/employee.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EmployeeComponent } from './employee.component'; + +describe('EmployeeComponent', () => { + let component: EmployeeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmployeeComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(EmployeeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/account/company/employee/employee.component.ts b/frontend/src/app/pages/account/company/employee/employee.component.ts new file mode 100644 index 0000000..6ee2ee2 --- /dev/null +++ b/frontend/src/app/pages/account/company/employee/employee.component.ts @@ -0,0 +1,429 @@ +import { EmployeeDTO, SubsidiaryDTO, DepartmentDTO, DesignationDTO } from './../../../../models/account.model'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { TableModule, Table } from 'primeng/table'; +import { DialogModule } from 'primeng/dialog'; +import { RippleModule } from 'primeng/ripple'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { CommonModule } from '@angular/common'; +import { FileUploadModule } from 'primeng/fileupload'; +import { DropdownModule } from 'primeng/dropdown'; +import { TagModule } from 'primeng/tag'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { RatingModule } from 'primeng/rating'; +import { SkeletonModule } from 'primeng/skeleton'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { InputNumberModule } from 'primeng/inputnumber'; +import { IconFieldModule } from 'primeng/iconfield'; +import { InputIconModule } from 'primeng/inputicon'; +import { CompanyService } from '../../../../services/account/company/company.service'; +import { ValidationService } from '../../../../services/utilities/validation.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { CalendarModule } from 'primeng/calendar'; +import { MasterService } from '../../../../services/masters/master.service'; +import { CityDTO, SearchDTO } from '../../../../models/masters/masters'; +import { debounceTime, Subject } from 'rxjs'; +import { Request } from '../../../../models/request.model'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-employee', + templateUrl: './employee.component.html', + standalone: true, + imports: [ + CommonModule, FormsModule, ReactiveFormsModule, + TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, + InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule, + RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule, + InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, CalendarModule + ], + providers: [MessageService, ConfirmationService], + styleUrl: './employee.component.css' +}) +export class EmployeeComponent implements OnInit{ + employeeForm: FormGroup; + employeeDialog: boolean = false; + employees!: EmployeeDTO[]; + + employee: EmployeeDTO | undefined; + + selectedEmployees!: EmployeeDTO[] | null; + + submitted: boolean = false; + + isLoading: boolean = true; + + skeletonData: any[] = Array(10).fill({}); + + subsidiaries: SubsidiaryDTO[] = []; + + departments: DepartmentDTO[] = []; + + designations: DesignationDTO[] = []; + + suggestions: CityDTO[] = []; + + private searchSubject = new Subject(); + + genders: any[] = [ + { label: 'Male', value: 'Male' }, + { label: 'Female', value: 'Female' }, + { label: 'Other', value: 'Other' } + ]; + + statuses!: any[]; + + @ViewChild('dt') dt!: Table; + + cols!: Column[]; + + exportColumns!: ExportColumn[]; + + constructor( + private companyService: CompanyService, + private masterService: MasterService, + private messageService: MessageService, + private confirmationService: ConfirmationService, + private cd: ChangeDetectorRef, + private fb: FormBuilder, + ) { + + this.employeeForm = this.fb.group({ + id: [{ value: '', disabled: true }], + subsidiaryId: [{ value: null, disabled: false }, [Validators.required]], + departmentId: [{ value: null, disabled: false }, [Validators.required]], + designationId: [{ value: null, disabled: false }, [Validators.required]], + joiningDate: [{ value: '', disabled: false }], + employeeId: [{ value: '', disabled: false }, [Validators.required]], + fullName: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]], + gender: [{ value: '', disabled: false }, [Validators.required]], + dob: [{ value: '', disabled: false }, [Validators.required]], + contactNo: [{ value: '', disabled: false }, [Validators.required, ValidationService.mobileValidator()]], + alternateNo: [{ value: '', disabled: false }, [ValidationService.mobileValidator()]], + emailId: [{ value: '', disabled: false }, [Validators.required, ValidationService.emailValidator()]], + residenceAddress: [{ value: '', disabled: false }], + residenceCityId: [{ value: '', disabled: true }], + residenceStateId: [{ value: '', disabled: true }], + residenceCityName: [{ value: '', disabled: false }], + residenceStateName: [{ value: '', disabled: true }], + permanentAddress: [{ value: '', disabled: false }], + permanentCityId: [{ value: '', disabled: true }], + permanentStateId: [{ value: '', disabled: true }], + permanentCityName: [{ value: '', disabled: false }], + permanentStateName: [{ value: '', disabled: true }], + active: [{ value: true, disabled: false }] + }); + } + + exportCSV() { + this.dt.exportCSV(); + } + + ngOnInit() { + this.loadAllEmployees(); + this.loadSubsidiaries(); + this.loadDepartments(); + this.loadDesignations(); + } + + loadSubsidiaries() { + this.companyService.getAllSubsidiaries().subscribe({ + next: (data) => { + this.subsidiaries = data.filter(s => s.active); + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Error loading subsidiaries', err); + } + }); + } + + loadDepartments() { + this.companyService.getAllDepartments().subscribe({ + next: (data) => { + this.departments = data.filter(d => d.active); + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Error loading departments', err); + } + }); + } + + loadDesignations() { + this.companyService.getAllDesignations().subscribe({ + next: (data) => { + this.designations = data.filter(d => d.active); + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Error loading designations', err); + } + }); + } + + searchCities(event: any) { + const query = event.query; + this.searchSubject.next(query); + } + + onSelectCity(event: any, type: 'permanent' | 'residence') { + const city = event.value as CityDTO; + if (type === 'permanent') { + this.employeeForm.patchValue({ + permanentCityId: city.id, + permanentStateId: city.stateId, + permanentCityName: city.cityName, + permanentStateName: city.stateName + }); + } else { + this.employeeForm.patchValue({ + residenceCityId: city.id, + residenceStateId: city.stateId, + residenceCityName: city.cityName, + residenceStateName: city.stateName + }); + } + } + + loadAllEmployees() { + this.isLoading = true; + this.companyService.getAllEmployees().subscribe({ + next: (data) => { + this.isLoading = false; + this.employees = data; + this.cd.markForCheck(); + }, + error: (err) => { + this.isLoading = false; + console.error(err); + } + }); + + this.statuses = [ + { label: 'Active', value: true }, + { label: 'Inactive', value: false } + ]; + + this.cols = [ + { field: 'fullName', header: 'Full Name', customExportHeader: 'Full Name' }, + { field: 'subsidiaryName', header: 'Subsidiary' }, + { field: 'department', header: 'Department' }, + { field: 'designation', header: 'Designation' }, + { field: 'contactNo', header: 'Contact No' }, + { field: 'emailId', header: 'Email' }, + { field: 'updatedAt', header: 'Last Updated At' }, + { field: 'updatedUser', header: 'Last Updated By' } + ]; + + this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field })); + + this.searchSubject.pipe(debounceTime(300)).subscribe(query => { + if (query && query.length >= 2) { + const requestPayload: Request = { + data: { + searchBy: 'CITY', + searchValue: query + }, + compressed: true, + target: 'models.commons.Search' + }; + this.masterService.searchCityStates(requestPayload).subscribe({ + next: (cities) => { + this.suggestions = cities.map(city => ({ + ...city, + display: `${city.cityName}, ${city.stateName}` + })); + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Error searching cities', err); + this.suggestions = []; + } + }); + } else { + this.suggestions = []; + } + }); + } + + openNew() { + this.employee = undefined; + this.employeeForm.reset(); + this.submitted = false; + this.employeeDialog = true; + } + + editEmployee(employee: EmployeeDTO) { + const subsidiary = this.subsidiaries.find(sub => sub.name === employee.subsidiaryName); + const department = this.departments.find(dep => dep.department === employee.department); + const designation = this.designations.find(des => des.designation === employee.designation); + + this.employeeForm.reset(); + this.employee = { ...employee }; + this.employee.subsidiaryId = subsidiary?.id ?? ''; + this.employee.departmentId = department?.id ?? ''; + this.employee.designationId = designation?.id ?? ''; + + // Prepare data for patching, converting dates + const employeeToPatch = { ...this.employee }; + if (employeeToPatch.joiningDate) { + (employeeToPatch as any).joiningDate = new Date(employeeToPatch.joiningDate); + } + if (employeeToPatch.dob) { + (employeeToPatch as any).dob = new Date(employeeToPatch.dob); + } + + this.employeeForm.patchValue(employeeToPatch); + this.employeeDialog = true; + console.log(this.employeeForm.getRawValue()); + } + + + hideDialog() { + this.employeeDialog = false; + this.submitted = false; + } + + toggleActive(employee: EmployeeDTO) { + const isActivating = !employee.active; + this.confirmationService.confirm({ + message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + employee.fullName + '?', + header: 'Confirm', + icon: 'pi pi-exclamation-triangle', + rejectButtonStyleClass: 'p-button-text p-button-secondary', + acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger', + accept: () => { + this.companyService.activateDeactivateEmployee(employee.id, isActivating).subscribe({ + next: (updatedEmployee) => { + employee.active = updatedEmployee.active; + this.employees = [...this.employees]; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: `Employee ${isActivating ? 'Activated' : 'Deactivated'}`, + life: 3000 + }); + }, + error: (err) => { + console.error('Error toggling employee active status', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to update employee status', + life: 3000 + }); + } + }); + } + }); + } + + getSeverity(status: boolean) { + switch (status) { + case true: + return 'success'; + case false: + return 'warning'; + } + } + + getErrorMessage(fieldName: string): string { + const control = this.employeeForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isFieldInvalid(fieldName: string): boolean { + const control = this.employeeForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submitted)); + } + + saveEmployee() { + this.submitted = true; + + if (this.employeeForm.invalid) { + return; + } + + const employeeData = this.employeeForm.getRawValue() as EmployeeDTO; + + // ✅ Normalize null → empty array + const employees = this.employees ?? []; + + // 🔍 Duplicate check (case-insensitive) + const existing = employees.some(emp => emp.id !== employeeData.id && ( + emp.fullName?.trim().toLowerCase() === employeeData.fullName?.trim().toLowerCase() || + emp.contactNo?.trim() === employeeData.contactNo?.trim() || + emp.emailId?.trim().toLowerCase() === employeeData.emailId?.trim().toLowerCase() + )); + + if (existing) { + this.messageService.add({ + severity: 'error', + summary: 'Validation Error', + detail: 'Employee with same name, contact or email already exists', + life: 3000 + }); + return; + } + + this.companyService.saveEmployee(employeeData).subscribe({ + next: (savedEmployee) => { + + const index = employeeData.id + ? employees.findIndex(emp => emp.id === employeeData.id) + : -1; + + if (index !== -1) { + // ✅ UPDATE + employees[index] = savedEmployee; + } else { + // ✅ CREATE + employees.push(savedEmployee); + } + + // ✅ Reassign once (change detection + null safety) + this.employees = [...employees]; + + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: index !== -1 + ? 'Employee Updated' + : 'Employee Created', + life: 3000 + }); + + this.employeeDialog = false; + this.employee = undefined; + this.employeeForm.reset(); + this.submitted = false; + }, + error: (err) => { + console.error('Error saving employee', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to save employee', + life: 3000 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.css b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.css new file mode 100644 index 0000000..463526a --- /dev/null +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.css @@ -0,0 +1,5 @@ +:host ::ng-deep .p-dialog .product-image { + width: 150px; + margin: 0 auto 2rem auto; + display: block; +} \ No newline at end of file diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html new file mode 100644 index 0000000..0023a1a --- /dev/null +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html @@ -0,0 +1,322 @@ +
+ + + + + + + + + + + + + + +
+

Manage Subsidiaries

+ + + + +
+
+ + + # + Code + +
+ Name + +
+ + +
+ Email + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ subsidiary.code }} + {{ subsidiary.name }} + + + {{ subsidiary.emailId }} + + + + Not Available + + + {{ subsidiary.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ subsidiary.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + + +
+ + +
+ diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.spec.ts b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.spec.ts new file mode 100644 index 0000000..ac76031 --- /dev/null +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SubsidiaryComponent } from './subsidiary.component'; + +describe('SubsidiaryComponent', () => { + let component: SubsidiaryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubsidiaryComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(SubsidiaryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts new file mode 100644 index 0000000..2de8ff9 --- /dev/null +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts @@ -0,0 +1,329 @@ +import { SearchDTO } from './../../../../models/masters/masters'; +import { SubsidiaryDTO } from './../../../../models/account.model'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { TableModule, Table } from 'primeng/table'; +import { DialogModule } from 'primeng/dialog'; +import { RippleModule } from 'primeng/ripple'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { CommonModule } from '@angular/common'; +import { FileUploadModule } from 'primeng/fileupload'; +import { DropdownModule } from 'primeng/dropdown'; +import { TagModule } from 'primeng/tag'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { RatingModule } from 'primeng/rating'; +import { SkeletonModule } from 'primeng/skeleton'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { InputNumberModule } from 'primeng/inputnumber'; +import { IconFieldModule } from 'primeng/iconfield'; +import { InputIconModule } from 'primeng/inputicon'; +import { CompanyService } from '../../../../services/account/company/company.service'; +import { ValidationService } from '../../../../services/utilities/validation.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { MasterService } from '../../../../services/masters/master.service'; +import { CityDTO } from '../../../../models/masters/masters'; +import { debounceTime, Subject } from 'rxjs'; +import { Request } from '../../../../models/request.model'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-subsidiary', + templateUrl: './subsidiary.component.html', + standalone: true, + imports: [ + CommonModule, FormsModule, ReactiveFormsModule, + TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, + InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule, + RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule, + InputIconModule, TooltipModule, AutoCompleteModule, RippleModule + ], + providers: [MessageService, ConfirmationService], + styleUrl: './subsidiary.component.css' +}) +export class SubsidiaryComponent implements OnInit{ + subsidiaryForm: FormGroup; + subsidiaryDialog: boolean = false; + subsidiaries!: SubsidiaryDTO[]; + + subsidiary!: SubsidiaryDTO; + + selectedSubsidiaries!: SubsidiaryDTO[] | null; + + submitted: boolean = false; + + isLoading: boolean = true; + + skeletonData: any[] = Array(10).fill({}); + + suggestions: CityDTO[] = []; + + private searchSubject = new Subject(); + + statuses!: any[]; + + @ViewChild('dt') dt!: Table; + + cols!: Column[]; + + exportColumns!: ExportColumn[]; + + constructor( + private subsidiaryService: CompanyService, + private masterService: MasterService, + private messageService: MessageService, + private confirmationService: ConfirmationService, + private cd: ChangeDetectorRef, + private fb: FormBuilder, + ) { + + this.subsidiaryForm = this.fb.group({ + id: [{ value: '', disabled: true }], + cityId: [{ value: null, disabled: true }], + code: [{ value: '', disabled: false }, [Validators.required, ValidationService.codeValidator()]], + name: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]], + officeNo: [{ value: '', disabled: false }], + street: [{ value: '', disabled: false }], + locality: [{ value: '', disabled: false }], + cityName: [{ value: '', disabled: false }], + stateName: [{ value: '', disabled: true }], + pinCode: [{ value: '', disabled: false }, [ValidationService.pincodeValidator()]], + emailId: [{ value: '', disabled: false }, [ValidationService.emailValidator()]], + contactNo: [{ value: '', disabled: false }, [ValidationService.mobileValidator()]], + contactPerson: [{ value: '', disabled: false }, [ValidationService.contactPersonValidator()]], + panNo: [{ value: '', disabled: false }, [ValidationService.panValidator()]], + cinNo: [{ value: '', disabled: false }, [ValidationService.cinValidator()]], + msmeNo: [{ value: '', disabled: false }, [ValidationService.msmeValidator()]], + stateId: [{ value: '', disabled: true }], + stateCode: [{ value: '', disabled: true }], + gstCode: [{ value: '', disabled: true }] + }); + } + + exportCSV() { + this.dt.exportCSV(); + } + + onSearch(event: Event) { + const input = event.target as HTMLInputElement; + this.dt.filterGlobal(input.value, 'contains'); + } + + searchCities(event: any) { + const query = event.query; + this.searchSubject.next(query); + } + + onSelectCity(event: any) { + const city = event.value as CityDTO; + this.subsidiaryForm.patchValue({ + cityId: city.id, + stateId: city.stateId, + cityName: city.cityName, + stateName: city.stateName, + stateCode: city.stateCode, + gstCode: city.gstCode + }); + } + + ngOnInit() { + this.loadAllSubsidiaries(); + } + + loadAllSubsidiaries() { + this.isLoading = true; + this.subsidiaryService.getAllSubsidiaries().subscribe({ + next: (data) => { + this.isLoading = false; + this.subsidiaries = data; + this.cd.markForCheck(); + }, + error: (err) => { + this.isLoading = false; + console.error(err); + } + }); + + this.statuses = [ + { label: 'Active', value: true }, + { label: 'Inactive', value: false } + ]; + + this.cols = [ + { field: 'code', header: 'Code', customExportHeader: 'Code' }, + { field: 'name', header: 'Name' }, + { field: 'officeNo', header: 'Office No / Building / Floor' }, + { field: 'street', header: 'Street / Road' }, + { field: 'locality', header: 'Locality' }, + { field: 'cityName', header: 'City' }, + { field: 'emailId', header: 'Email' }, + { field: 'contactNo', header: 'Contact No' }, + { field: 'contactPerson', header: 'Contact Person' }, + { field: 'panNo', header: 'PAN No.' }, + { field: 'cinNo', header: 'CIN No.' }, + { field: 'msmeNo', header: 'MSME No.' }, + { field: 'updatedAt', header: 'Last Updated At' }, + { field: 'updatedUser', header: 'Last Updated By' } + ]; + + this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field })); + + this.searchSubject.pipe(debounceTime(300)).subscribe(query => { + if (query && query.length >= 2) { + const requestPayload: Request = { + data: { + searchBy: 'CITY', + searchValue: query + }, + compressed: true, + target: 'models.commons.Search' + }; + this.masterService.searchCityStates(requestPayload).subscribe({ + next: (cities) => { + this.suggestions = cities.map(city => ({ + ...city, + display: `${city.cityName}, ${city.stateName}` + })); + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Error searching cities', err); + this.suggestions = []; + } + }); + } else { + this.suggestions = []; + } + }); + } + + openNew() { + this.subsidiary = {}; + this.subsidiaryForm.reset(); + this.submitted = false; + this.subsidiaryDialog = true; + } + + editSubsidiary(subsidiary: SubsidiaryDTO) { + this.subsidiaryForm.reset(); + this.subsidiary = { ...subsidiary }; + this.subsidiaryForm.patchValue(subsidiary); + this.subsidiaryDialog = true; + console.log(this.subsidiaryForm.getRawValue()); + } + + + hideDialog() { + this.subsidiaryDialog = false; + this.submitted = false; + } + + toggleActive(subsidiary: SubsidiaryDTO) { + const isActivating = !subsidiary.active; + this.confirmationService.confirm({ + message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + subsidiary.name + '?', + header: 'Confirm', + icon: 'pi pi-exclamation-triangle', + rejectButtonStyleClass: 'p-button-text p-button-secondary', + acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger', + accept: () => { + this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id?? '', isActivating).subscribe({ + next: (updatedSubsidiary) => { + subsidiary.active = updatedSubsidiary.active; + this.subsidiaries = [...this.subsidiaries]; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: `Subsidiary ${isActivating ? 'Activated' : 'Deactivated'}`, + life: 3000 + }); + }, + error: (err) => { + console.error('Error toggling subsidiary active status', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to update subsidiary status', + life: 3000 + }); + } + }); + } + }); + } + + getSeverity(status: boolean) { + switch (status) { + case true: + return 'success'; + case false: + return 'warning'; + } + } + + getErrorMessage(fieldName: string): string { + const control = this.subsidiaryForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isFieldInvalid(fieldName: string): boolean { + const control = this.subsidiaryForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submitted)); + } + + saveSubsidiary() { + this.submitted = true; + + if (this.subsidiaryForm.valid) { + const subsidiaryData = this.subsidiaryForm.getRawValue(); + const existing = this.subsidiaries.find(sub => sub.id !== subsidiaryData.id && (sub.code === subsidiaryData.code || sub.name === subsidiaryData.name)); + if (existing) { + this.messageService.add({ + severity: 'error', + summary: 'Validation Error', + detail: 'Subsidiary code or name already exists', + life: 3000 + }); + return; + } + this.subsidiaryService.saveSubsidiary(subsidiaryData).subscribe({ + next: (newSubsidiary) => { + this.subsidiaries.push(newSubsidiary); + this.subsidiaries = [...this.subsidiaries]; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: 'Subsidiary Created', + life: 3000 + }); + this.subsidiaryDialog = false; + this.subsidiary = {}; + }, + error: (err) => { + console.error('Error saving subsidiary', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to save subsidiary', + life: 3000 + }); + } + }); + } + } +} diff --git a/frontend/src/app/pages/account/user/user.component.css b/frontend/src/app/pages/account/user/user.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/app/pages/account/user/user.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/pages/account/user/user.component.html b/frontend/src/app/pages/account/user/user.component.html new file mode 100644 index 0000000..c0540ea --- /dev/null +++ b/frontend/src/app/pages/account/user/user.component.html @@ -0,0 +1,303 @@ +
+ + + + + + + + + + + + + + +
+

Manage Users

+ + + + +
+
+ + + # + +
+ Login ID + +
+ + +
+ Display Name + +
+ + +
+ Employee ID + +
+ + +
+ Status + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Active + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ user.loginId }} + {{ user.displayName }} + + + {{ user.employeeId }} + + + Not Available + + + + + + {{ user.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ user.updatedUser }} + + + Not Available + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+ + + + +
+
+ + + + +
+
+ +
+
+ + + + # + Role Name + Group Name + Branch Name + Action + + + + + {{ rowIndex + 1 }} + {{role.roleName}} + {{role.groupName}} + {{role.branchName}} + + + + + + + + No roles assigned. + + + +
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/user/user.component.spec.ts b/frontend/src/app/pages/account/user/user.component.spec.ts new file mode 100644 index 0000000..56317f0 --- /dev/null +++ b/frontend/src/app/pages/account/user/user.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UserComponent } from './user.component'; + +describe('UserComponent', () => { + let component: UserComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(UserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/account/user/user.component.ts b/frontend/src/app/pages/account/user/user.component.ts new file mode 100644 index 0000000..07b88d4 --- /dev/null +++ b/frontend/src/app/pages/account/user/user.component.ts @@ -0,0 +1,362 @@ +import { UserDTO, UserRoleDTO } from '../../../models/user.model'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { TableModule, Table } from 'primeng/table'; +import { DialogModule } from 'primeng/dialog'; +import { RippleModule } from 'primeng/ripple'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { CommonModule } from '@angular/common'; +import { FileUploadModule } from 'primeng/fileupload'; +import { DropdownModule } from 'primeng/dropdown'; +import { TagModule } from 'primeng/tag'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { RatingModule } from 'primeng/rating'; +import { SkeletonModule } from 'primeng/skeleton'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { InputNumberModule } from 'primeng/inputnumber'; +import { UserService } from '../../../services/account/user/user.service'; +import { CompanyService } from '../../../services/account/company/company.service'; +import { ValidationService } from '../../../services/utilities/validation.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { FieldsetModule } from 'primeng/fieldset'; +import { debounceTime, Subject } from 'rxjs'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-user', + templateUrl: './user.component.html', + standalone: true, + imports: [ + CommonModule, FormsModule, ReactiveFormsModule, + TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, + InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule, + RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, + TooltipModule, AutoCompleteModule, FieldsetModule, RippleModule + ], + providers: [MessageService, ConfirmationService], + styleUrl: './user.component.css' +}) +export class UserComponent implements OnInit{ + userForm: FormGroup; + userDialog: boolean = false; + users!: UserDTO[]; + + user: UserDTO | undefined; + + selectedUsers!: UserDTO[] | null; + + submitted: boolean = false; + + isLoading: boolean = true; + + skeletonData: any[] = Array(10).fill({}); + + userRoles: UserRoleDTO[] = []; + + branches: any[] = []; + + userRolesOptions: any[] = []; + + suggestions: any[] = []; + + private searchSubject = new Subject(); + + + statuses!: any[]; + + @ViewChild('dt') dt!: Table; + + cols!: Column[]; + + exportColumns!: ExportColumn[]; + + constructor( + private userService: UserService, + private companyService: CompanyService, + private messageService: MessageService, + private confirmationService: ConfirmationService, + private cd: ChangeDetectorRef, + private fb: FormBuilder, + ) { + + this.userForm = this.fb.group({ + id: [{ value: '', disabled: true }], + loginId: [{ value: '', disabled: false }, [Validators.required]], + displayName: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]], + fkEmployeeId: [{ value: '', disabled: false }], + employeeId: [{ value: '', disabled: false }], + employeeName: [{ value: '', disabled: false }], + fatherName: [{ value: '', disabled: false }], + department: [{ value: '', disabled: false }], + designation: [{ value: '', disabled: false }], + status: [{ value: 'Active', disabled: false }, [Validators.required]], + active: [{ value: true, disabled: false }] + }); + } + + exportCSV() { + this.dt.exportCSV(); + } + + onSearch(event: Event) { + const input = event.target as HTMLInputElement; + this.dt.filterGlobal(input.value, 'contains'); + } + + ngOnInit() { + this.loadAllUsers(); + } + + loadAllUsers() { + this.isLoading = true; + this.userService.getAllUsers().subscribe({ + next: (data) => { + this.isLoading = false; + this.users = data; + this.cd.markForCheck(); + }, + error: (err) => { + this.isLoading = false; + console.error(err); + } + }); + + this.statuses = [ + { label: 'Active', value: 'Active' }, + { label: 'Inactive', value: 'Inactive' } + ]; + + this.cols = [ + { field: 'loginId', header: 'Login ID', customExportHeader: 'Login ID' }, + { field: 'displayName', header: 'Display Name' }, + { field: 'employeeId', header: 'Employee ID' }, + { field: 'status', header: 'Status' }, + { field: 'updatedAt', header: 'Last Updated At' }, + { field: 'updatedUser', header: 'Last Updated By' } + ]; + + this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field })); + + this.searchSubject.pipe(debounceTime(300)).subscribe(query => { + if (query && query.length >= 2) { + const requestPayload = { + data: { + searchBy: 'NAME', + searchValue: query + }, + compressed: true, + target: 'models.commons.Search' + }; + this.companyService.searchEmployees(requestPayload).subscribe({ + next: (employees) => { + this.suggestions = employees.map(emp => ({ + ...emp, + display: `${emp.employeeId} - ${emp.fullName}` + })); + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Error searching employees', err); + this.suggestions = []; + } + }); + } else { + this.suggestions = []; + } + }); + } + + openNew() { + this.user = undefined; + this.userForm.reset(); + this.userRoles = []; + this.submitted = false; + this.userDialog = true; + } + + editUser(user: UserDTO) { + this.userForm.reset(); + this.user = { ...user }; + this.userRoles = [...(user.userRoles || [])]; + this.userForm.patchValue(user); + this.userDialog = true; + console.log(this.userForm.getRawValue()); + } + + hideDialog() { + this.userDialog = false; + this.submitted = false; + } + + toggleActive(user: UserDTO) { + const isActivating = !user.active; + this.confirmationService.confirm({ + message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + user.displayName + '?', + header: 'Confirm', + icon: 'pi pi-exclamation-triangle', + rejectButtonStyleClass: 'p-button-text p-button-secondary', + acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger', + accept: () => { + this.userService.activateDeactivateUser(user.id!, isActivating).subscribe({ + next: (updatedUser) => { + user.active = updatedUser.active; + this.users = [...this.users]; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: `User ${isActivating ? 'Activated' : 'Deactivated'}`, + life: 3000 + }); + }, + error: (err) => { + console.error('Error toggling user active status', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to update user status', + life: 3000 + }); + } + }); + } + }); + } + + getSeverity(status: string) { + switch (status) { + case 'Active': + return 'success'; + case 'Inactive': + return 'warning'; + } + return 'info'; + } + + getErrorMessage(fieldName: string): string { + const control = this.userForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isFieldInvalid(fieldName: string): boolean { + const control = this.userForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submitted)); + } + + addRole() { + // for now, do nothing, since selects empty + } + + toggleRoleActive(role: UserRoleDTO) { + role.active = role.active === false ? true : false; + } + + onSelectEmployee(event: any) { + const emp = event.value; + const patch: any = { + fkEmployeeId: emp.id, + employeeId: emp.employeeId, + employeeName: emp.fullName, + fatherName: emp.fatherName, + department: emp.department, + designation: emp.designation + }; + if (!this.userForm.get('displayName')!.value) { + patch.displayName = emp.fullName; + } + this.userForm.patchValue(patch); + console.log(this.userForm); + } + + searchEmployees(event: any) { + this.searchSubject.next(event.query); + } + + + saveUser() { + this.submitted = true; + + if (this.userForm.invalid) { + return; + } + + const userData = this.userForm.getRawValue() as UserDTO; + userData.userRoles = this.userRoles; + + // Normalize null → empty array + const users = this.users ?? []; + + // Duplicate check (case-insensitive) + const existing = users.some(u => u.id !== userData.id && ( + u.loginId?.trim().toLowerCase() === userData.loginId?.trim().toLowerCase() + )); + + if (existing) { + this.messageService.add({ + severity: 'error', + summary: 'Validation Error', + detail: 'User with same login ID already exists', + life: 3000 + }); + return; + } + + this.userService.saveUser(userData).subscribe({ + next: (savedUser) => { + + const index = userData.id + ? users.findIndex(u => u.id === userData.id) + : -1; + + if (index !== -1) { + // UPDATE + users[index] = savedUser; + } else { + // CREATE + users.push(savedUser); + } + + // Reassign once (change detection + null safety) + this.users = [...users]; + + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: index !== -1 + ? 'User Updated' + : 'User Created', + life: 3000 + }); + + this.userDialog = false; + this.user = undefined; + this.userForm.reset(); + this.submitted = false; + }, + error: (err) => { + console.error('Error saving user', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to save user', + life: 3000 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/dashboard/dashboard.component.ts b/frontend/src/app/pages/dashboard/dashboard.component.ts new file mode 100644 index 0000000..8e6f11d --- /dev/null +++ b/frontend/src/app/pages/dashboard/dashboard.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { MenuComponent } from "../../fragments/menu/menu.component"; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-dashboard', + imports: [MenuComponent, RouterOutlet], + template: `
+ +
+ +
+
` +}) +export class DashboardComponent { + constructor(){} +} diff --git a/frontend/src/app/pages/session/auth/authorize.component.css b/frontend/src/app/pages/session/auth/authorize.component.css new file mode 100644 index 0000000..0372f99 --- /dev/null +++ b/frontend/src/app/pages/session/auth/authorize.component.css @@ -0,0 +1,38 @@ +.login-page { + height: 99vh; +} + +::ng-deep .p-card { + position: relative; + border-radius: 24px !important; + background: white; + padding: 1rem; + overflow: hidden; +} + +/* Gradient border */ +::ng-deep .p-card::before { + content: ""; + position: absolute; + inset: 0; + padding: 3px; /* border thickness */ + border-radius: 24px; + filter: drop-shadow(0 0 12px rgba(91, 185, 138, 0.35)); + background: linear-gradient( + 180deg, + #ffb455 0%, + rgba(255, 180, 85, 0.6) 40%, + rgba(255, 180, 85, 0.15) 70%, + transparent 100% + ); + + /* Mask trick = border only */ + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + + pointer-events: none; +} + diff --git a/frontend/src/app/pages/session/auth/authorize.component.html b/frontend/src/app/pages/session/auth/authorize.component.html new file mode 100644 index 0000000..6d37b8a --- /dev/null +++ b/frontend/src/app/pages/session/auth/authorize.component.html @@ -0,0 +1,38 @@ + diff --git a/frontend/src/app/pages/session/auth/authorize.component.ts b/frontend/src/app/pages/session/auth/authorize.component.ts new file mode 100644 index 0000000..11bc2af --- /dev/null +++ b/frontend/src/app/pages/session/auth/authorize.component.ts @@ -0,0 +1,106 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { CardModule } from 'primeng/card'; +import { InputGroupModule } from 'primeng/inputgroup'; +import { InputGroupAddonModule } from 'primeng/inputgroupaddon'; +import { PasswordModule } from 'primeng/password'; +import { KeyFilterModule } from 'primeng/keyfilter'; +import { DropdownModule } from 'primeng/dropdown'; +import { MessageModule } from 'primeng/message'; +import { Router } from '@angular/router'; +import { SessionService } from './../../../services/commons/session.service'; +import { HttpService } from '../../../services/http.service'; +import { environment } from '../../../../environments/environment'; +import { Company, Branch } from '../../../models/session.model'; +import { Request } from '../../../models/request.model'; +import { ResponseDto } from '../../../models/response.dto'; + +@Component({ + selector: 'app-authorize', + standalone: true, + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + CardModule, + ButtonModule, + InputTextModule, + PasswordModule, + DropdownModule, + KeyFilterModule, + MessageModule + ], + templateUrl: './authorize.component.html', + styleUrl: './authorize.component.css' +}) +export class AuthorizeComponent implements OnInit { + companies: Company[] = []; + branches: Branch[] = []; + isCompanyDisabled: boolean = false; + authForm: FormGroup; + message?: string; + + constructor(private fb: FormBuilder, private http: HttpService, private router: Router, private sessionService: SessionService) { + this.authForm = this.fb.group({ + companyId: ['', Validators.required], + branchId: ['', Validators.required] + }); + } + + ngOnInit(){ + const companyBranchRoles = this.sessionService.getItem('companies'); + + if (!companyBranchRoles) { + this.sessionService.logout(); + return; + } + sessionStorage.removeItem('companies'); + + try { + this.companies = companyBranchRoles; + } catch (e) { + this.sessionService.logout(); + } + + if (this.companies.length === 1) { + this.authForm.patchValue({ companyId: this.companies[0].id }); + this.isCompanyDisabled = true; + } + + this.updateBranches(); + this.authForm.get('companyId')?.valueChanges.subscribe(() => { + this.updateBranches(); + }); + } + + private updateBranches(): void { + const companyId = this.authForm.get('companyId')?.value; + const selectedCompany = this.companies.find(c => c.id === companyId); + this.branches = selectedCompany?.branches || []; + this.authForm.patchValue({ branchId: '' }); + } + + onAuthorize() { + this.message = ''; + if (this.authForm.valid) { + const requestPayload: Request = { + data: this.authForm.get('branchId')?.value + }; + + this.http.post(`${environment.authService}/3z4mkell5g5aset/authorize`, requestPayload).subscribe({ + next: (response) => { + this.router.navigate(['/dashboard']); + }, + error: (err) => { + this.message = err.message; + } + }); + } else { + alert('Please fill out the form correctly'); + } + } + +} diff --git a/frontend/src/app/pages/session/profile/profile.component.css b/frontend/src/app/pages/session/profile/profile.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/app/pages/session/profile/profile.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/pages/session/profile/profile.component.html b/frontend/src/app/pages/session/profile/profile.component.html new file mode 100644 index 0000000..ff14145 --- /dev/null +++ b/frontend/src/app/pages/session/profile/profile.component.html @@ -0,0 +1,167 @@ +
+
+ +
+ + User Details +
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + +
+ + Employment Details +
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+
+ diff --git a/frontend/src/app/pages/session/profile/profile.component.spec.ts b/frontend/src/app/pages/session/profile/profile.component.spec.ts new file mode 100644 index 0000000..658617a --- /dev/null +++ b/frontend/src/app/pages/session/profile/profile.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProfileComponent } from './profile.component'; + +describe('ProfileComponent', () => { + let component: ProfileComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfileComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(ProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/session/profile/profile.component.ts b/frontend/src/app/pages/session/profile/profile.component.ts new file mode 100644 index 0000000..ae00c5c --- /dev/null +++ b/frontend/src/app/pages/session/profile/profile.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { InputTextModule } from 'primeng/inputtext'; +import { DividerModule } from 'primeng/divider'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SessionService } from '../../../services/commons/session.service'; +import { CardModule } from "primeng/card"; +import { UserProfile } from '../../../models/session.model'; + +@Component({ + selector: 'app-profile', + templateUrl: './profile.component.html', + styleUrl: './profile.component.css', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, FormsModule, InputTextModule, DividerModule, CardModule] +}) +export class ProfileComponent implements OnInit { + userProfile: UserProfile; + profileForm: FormGroup; + constructor(private fb: FormBuilder, private sessionService: SessionService){ + this.userProfile = {}; + this.profileForm = this.fb.group({ + username: [{ value: null, disabled: true }], + roleName: [{ value: null, disabled: true }], + employeeId: [{ value: null, disabled: true }], + joiningDate: [{ value: null, disabled: true }], + displayName: [null], + department: [{ value: null, disabled: true }], + designation: [{ value: null, disabled: true }], + name: [{ value: null, disabled: true }], + fatherName: [{ value: null, disabled: true }], + gender: [{ value: null, disabled: true }], + dob: [{ value: null, disabled: true }], + contactNo: [{ value: null, disabled: true }], + alternateContactNo: [null], + emailId: [null] + }); + } + + ngOnInit(): void { + const userDetails = this.sessionService.getItem('userDetails'); + const companies = this.sessionService.getItem("companies") ? JSON.parse(this.sessionService.getItem("companies")) : ''; + this.userProfile = userDetails ? JSON.parse(userDetails) : {}; + //One liner approach commented out for later user + //this.profileForm.patchValue(this.userProfile as Partial); + this.profileForm.patchValue({ + ...this.userProfile, + roleName: companies ? companies[0].branches[0].roles[0].groupName : '', + joiningdate: this.userProfile.joiningDate + ? new Date(this.userProfile.joiningDate) + : null, + dob: this.userProfile.dob + ? new Date(this.userProfile.dob) + : null + }); + } + + onSave() : void{ + + } +} diff --git a/frontend/src/app/services/account/company/company.service.ts b/frontend/src/app/services/account/company/company.service.ts new file mode 100644 index 0000000..24b9d6c --- /dev/null +++ b/frontend/src/app/services/account/company/company.service.ts @@ -0,0 +1,159 @@ +import { Request } from './../../../models/request.model'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from '../../../../environments/environment'; +import { HttpService } from '../../http.service'; + +import { SubsidiaryDTO, DepartmentDTO, DesignationDTO, EmployeeDTO } from '../../../models/account.model'; +import { ResponseDto } from '../../../models/response.dto'; + +@Injectable({ + providedIn: 'root' +}) +export class CompanyService { + + constructor(private http: HttpService) {} + + // Subsidiaries + getAllSubsidiaries(): Observable { + return this.http.get(`${environment.accountService}/company/subsidiaries`).pipe( + map((response: ResponseDto) => response.data as SubsidiaryDTO[]) + ); + } + + saveSubsidiary(subsidiary: SubsidiaryDTO): Observable { + const requestPayload: Request = { + data: subsidiary, + compressed: true, + target: 'cygnus.models.account.Subsidiary' + }; + return this.http.post(`${environment.accountService}/company/subsidiaries`, requestPayload).pipe( + map((response: ResponseDto) => response.data as SubsidiaryDTO) + ); + } + + activateDeactivateSubsidiary(subsidiaryId: string, active: boolean): Observable { + const subsidiary: SubsidiaryDTO = ({ + id: subsidiaryId, + active: active + }); + const requestPayload: Request = { + scopes: [active ? "ACTIVATE" : "DEACTIVATE"], + data: subsidiary, + compressed: true, + target: 'cygnus.models.account.Subsidiary' + }; + return this.http.post(`${environment.accountService}/company/subsidiaries`, requestPayload).pipe( + map((response: ResponseDto) => response.data as SubsidiaryDTO) + ); + } + + // Departments + getAllDepartments(): Observable { + return this.http.get(`${environment.accountService}/company/departments`).pipe( + map((response: ResponseDto) => response.data as DepartmentDTO[]) + ); + } + + saveDepartment(department: DepartmentDTO): Observable { + const requestPayload: Request = { + data: department, + compressed: true, + target: 'cygnus.models.account.Department' + }; + return this.http.post(`${environment.accountService}/company/departments`, requestPayload).pipe( + map((response: ResponseDto) => response.data as DepartmentDTO) + ); + } + + activateDeactivateDepartment(departmentId: string, active: boolean): Observable { + const department: DepartmentDTO = ({ + id: departmentId, + active: active + } as DepartmentDTO); + const requestPayload: Request = { + scopes: [active ? "ACTIVATE" : "DEACTIVATE"], + data: department, + compressed: true, + target: 'cygnus.models.account.Department' + }; + return this.http.post(`${environment.accountService}/company/departments`, requestPayload).pipe( + map((response: ResponseDto) => response.data as DepartmentDTO) + ); + } + + // Designations + getAllDesignations(): Observable { + return this.http.get(`${environment.accountService}/company/designations`).pipe( + map((response: ResponseDto) => response.data as DesignationDTO[]) + ); + } + + saveDesignation(designation: DesignationDTO): Observable { + const requestPayload: Request = { + data: designation, + compressed: true, + target: 'cygnus.models.account.Designation' + }; + return this.http.post(`${environment.accountService}/company/designations`, requestPayload).pipe( + map((response: ResponseDto) => response.data as DesignationDTO) + ); + } + + activateDeactivateDesignation(designationId: string, active: boolean): Observable { + const designation: DesignationDTO = ({ + id: designationId, + active: active + } as DesignationDTO); + const requestPayload: Request = { + scopes: [active ? "ACTIVATE" : "DEACTIVATE"], + data: designation, + compressed: true, + target: 'cygnus.models.account.Designation' + }; + return this.http.post(`${environment.accountService}/company/designations`, requestPayload).pipe( + map((response: ResponseDto) => response.data as DesignationDTO) + ); + } + + // Employees + getAllEmployees(): Observable { + return this.http.get(`${environment.accountService}/company/employees`).pipe( + map((response: ResponseDto) => response.data as EmployeeDTO[]) + ); + } + + saveEmployee(employee: EmployeeDTO): Observable { + const requestPayload: Request = { + data: employee, + compressed: true, + target: 'cygnus.models.account.Employee' + }; + return this.http.post(`${environment.accountService}/company/employees`, requestPayload).pipe( + map((response: ResponseDto) => response.data as EmployeeDTO) + ); + } + + activateDeactivateEmployee(employeeId: string, active: boolean): Observable { + const employee: EmployeeDTO = ({ + id: employeeId, + active: active + } as EmployeeDTO); + const requestPayload: Request = { + scopes: [active ? "ACTIVATE" : "DEACTIVATE"], + data: employee, + compressed: true, + target: 'cygnus.models.account.Employee' + }; + return this.http.post(`${environment.accountService}/company/employees`, requestPayload).pipe( + map((response: ResponseDto) => response.data as EmployeeDTO) + ); + } + + searchEmployees(payload: Request): Observable { + return this.http.post(`${environment.accountService}/company/employees/search`, payload).pipe( + map((response: ResponseDto) => response.data as any[]) + ); + } +} \ No newline at end of file diff --git a/frontend/src/app/services/account/user/user.service.ts b/frontend/src/app/services/account/user/user.service.ts new file mode 100644 index 0000000..d38b554 --- /dev/null +++ b/frontend/src/app/services/account/user/user.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from '../../../../environments/environment'; +import { HttpService } from '../../http.service'; +import { UserDTO } from '../../../models/user.model'; +import { ResponseDto } from '../../../models/response.dto'; +import { Request } from '../../../models/request.model'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + constructor(private http: HttpService) { } + + getAllUsers(): Observable { + return this.http.get(`${environment.userService}/users`).pipe( + map((response: ResponseDto) => response.data as UserDTO[]) + ); + } + + saveUser(user: UserDTO): Observable { + const requestPayload: Request = { + data: user, + compressed: true, + target: 'cygnus.models.account.User' + }; + return this.http.post(`${environment.userService}/users`, requestPayload).pipe( + map((response: ResponseDto) => response.data as UserDTO) + ); + } + + activateDeactivateUser(userId: string, active: boolean): Observable { + const user: UserDTO = { + id: userId, + active: active + }; + const requestPayload: Request = { + scopes: [active ? "ACTIVATE" : "DEACTIVATE"], + data: user, + compressed: true, + target: 'cygnus.models.account.User' + }; + return this.http.post(`${environment.userService}/users`, requestPayload).pipe( + map((response: ResponseDto) => response.data as UserDTO) + ); + } +} \ No newline at end of file diff --git a/frontend/src/app/services/commons/session.service.ts b/frontend/src/app/services/commons/session.service.ts new file mode 100644 index 0000000..7a4ef2b --- /dev/null +++ b/frontend/src/app/services/commons/session.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class SessionService { + + constructor(private router: Router) { } + + setItem(key: string, value: T): void { + try { + sessionStorage.setItem(key, JSON.stringify(value)); + } catch (e) { + console.error(`Error saving ${key} to sessionStorage`, e); + } + } + + getDetails(key: string): T | null { + const details = this.getItem('details'); + if (details === null) return null; + const value = details[key]; + if (value === null) return null; + else return value; + } + + getItem(key: string): any | null { + const json = sessionStorage.getItem(key); + if (json === null) return null; + + try { + return JSON.parse(json); + } catch (e) { + console.warn(`Error parsing JSON for key ${key}`, e); + return null; + } + } + + removeItem(key: string): void { + sessionStorage.removeItem(key); + } + + clear(): void { + sessionStorage.clear(); + } + + logout(): void { + sessionStorage.clear(); + this.router.navigate(['/']); + } +} diff --git a/frontend/src/app/services/http.service.ts b/frontend/src/app/services/http.service.ts new file mode 100644 index 0000000..5b4f125 --- /dev/null +++ b/frontend/src/app/services/http.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { ResponseDto } from '../models/response.dto'; + +@Injectable({ + providedIn: 'root' +}) +export class HttpService { + + constructor(private http: HttpClient) { + } + + get(url: string, headers?: HttpHeaders): Observable { + const options = { + headers, + withCredentials: true + }; + return this.http.get(url, options).pipe( + catchError(this.handleError) + ); + } + + post(url: string, body: any, headers?: HttpHeaders): Observable { + const options = { + headers, + withCredentials: true + }; + return this.http.post(url, body, options).pipe( + catchError(this.handleError) + ); + } + + put(url: string, body: any, headers?: HttpHeaders): Observable { + return this.http.put(url, body, { headers }).pipe( + catchError(this.handleError) + ); + } + + delete(url: string, headers?: HttpHeaders): Observable { + return this.http.delete(url, { headers }).pipe( + catchError(this.handleError) + ); + } + + private handleError(error: HttpErrorResponse) { + return throwError(() => error.error); + } + + setHeaders(additionalHeaders: { [key: string]: string }): HttpHeaders { + return new HttpHeaders({ + ...additionalHeaders + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/services/masters/master.service.ts b/frontend/src/app/services/masters/master.service.ts new file mode 100644 index 0000000..f9f283f --- /dev/null +++ b/frontend/src/app/services/masters/master.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { HttpService } from '../http.service'; +import { CityDTO } from '../../models/masters/masters'; +import { map, Observable } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { ResponseDto } from '../../models/response.dto'; +import { Request } from '../../models/request.model'; + +@Injectable({ + providedIn: 'root' +}) +export class MasterService { + private readonly baseUrl = '/master'; + + constructor(private http: HttpService) {} + + searchCityStates(payload: Request): Observable { + return this.http.post(`${environment.masterService}/cities-states/search`, payload).pipe( + map((response: ResponseDto) => response.data as CityDTO[]) + ); + } + + +} diff --git a/frontend/src/app/services/utilities/encryption.service.ts b/frontend/src/app/services/utilities/encryption.service.ts new file mode 100644 index 0000000..ecae54e --- /dev/null +++ b/frontend/src/app/services/utilities/encryption.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { environment } from '../../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class EncryptionService { + + constructor() {} + + async encrypt(payload: any): Promise { + const encoder = new TextEncoder(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const jsonString = JSON.stringify(payload); + + const keyBytes = Uint8Array.from(atob(environment.encryptionKey), c => c.charCodeAt(0)); + const cryptoKey = await crypto.subtle.importKey( + "raw", + keyBytes, + { name: "AES-GCM" }, + false, + ["encrypt"] + ); + + const encrypted = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + cryptoKey, + encoder.encode(jsonString) + ); + + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(encrypted), iv.length); + + return btoa(String.fromCharCode(...combined)); + } + + async decrypt(base64Cipher: string): Promise { + const data = Uint8Array.from(atob(base64Cipher), c => c.charCodeAt(0)); + const iv = data.slice(0, 12); + const ciphertext = data.slice(12); + + const keyBytes = Uint8Array.from(atob(environment.encryptionKey), c => c.charCodeAt(0)); + const cryptoKey = await crypto.subtle.importKey( + "raw", + keyBytes, + { name: "AES-GCM" }, + false, + ["decrypt"] + ); + + const decrypted = await crypto.subtle.decrypt( + { name: "AES-GCM", iv }, + cryptoKey, + ciphertext + ); + + const decoder = new TextDecoder(); + return JSON.parse(decoder.decode(decrypted)); + } +} \ No newline at end of file diff --git a/frontend/src/app/services/utilities/rsa.service.ts b/frontend/src/app/services/utilities/rsa.service.ts new file mode 100644 index 0000000..a88ff21 --- /dev/null +++ b/frontend/src/app/services/utilities/rsa.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { environment } from '../../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RsaService { + constructor() {} + + private async importPublicKey(pemKey: string): Promise { + const pem = pemKey + .replace('-----BEGIN PUBLIC KEY-----', '') + .replace('-----END PUBLIC KEY-----', '') + .replace(/\s+/g, ''); + const binaryDer = Uint8Array.from(atob(pem), c => c.charCodeAt(0)); + + return crypto.subtle.importKey( + 'spki', + binaryDer.buffer, + { + name: 'RSA-OAEP', + hash: 'SHA-256', + }, + false, + ['encrypt'] + ); + } + + async encrypt(plaintext: string): Promise { + const key = await this.importPublicKey(environment.rsaPublicKey); + const encoded = new TextEncoder().encode(plaintext); + + const encrypted = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, encoded); + + return btoa(String.fromCharCode(...new Uint8Array(encrypted))); + } +} diff --git a/frontend/src/app/services/utilities/validation.service.ts b/frontend/src/app/services/utilities/validation.service.ts new file mode 100644 index 0000000..b8dea27 --- /dev/null +++ b/frontend/src/app/services/utilities/validation.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@angular/core'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +@Injectable({ + providedIn: 'root' +}) +export class ValidationService { + + // Regex patterns + static readonly PATTERNS = { + EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + INDIAN_MOBILE: /^[6-9]\d{9}$/, + PINCODE: /^\d{6}$/, + PAN: /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/, + CIN: /^[UL][0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$/, + MSME: /^UDYAM-[A-Z]{2}-\d{7}$/, + ALPHANUMERIC: /^[a-zA-Z0-9]+$/, + ALPHABET_WITH_SPACES: /^[a-zA-Z0-9\s.&'-]+$/, + CODE: /^[A-Z0-9]{2,10}$/ + }; + + // Error messages + static readonly ERROR_MESSAGES = { + REQUIRED: 'This field is required', + EMAIL_INVALID: 'Please enter a valid email address', + MOBILE_INVALID: 'Please enter a valid 10-digit mobile number starting with 6-9', + PINCODE_INVALID: 'Please enter a valid 6-digit pincode', + PAN_INVALID: 'Please enter a valid PAN number (e.g., ABCDE1234F)', + CIN_INVALID: 'Please enter a valid CIN number', + MSME_INVALID: 'Please enter a valid MSME number (e.g., UDYAM-XX-XXXXXXX)', + CODE_INVALID: 'Code must be 2-10 alphanumeric characters', + NAME_INVALID: 'Name must be at least 2 characters and contain only letters, numbers, spaces, dots, ampersands, apostrophes, and hyphens', + CONTACT_PERSON_INVALID: 'Contact person name must be at least 2 characters', + MIN_LENGTH: (min: number) => `Minimum ${min} characters required`, + MAX_LENGTH: (max: number) => `Maximum ${max} characters allowed` + }; + + // Custom validators + static emailValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + const isValid = this.PATTERNS.EMAIL.test(control.value); + return isValid ? null : { invalidEmail: true }; + }; + } + + static mobileValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + const isValid = this.PATTERNS.INDIAN_MOBILE.test(control.value); + return isValid ? null : { invalidMobile: true }; + }; + } + + static pincodeValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + const isValid = this.PATTERNS.PINCODE.test(control.value); + return isValid ? null : { invalidPincode: true }; + }; + } + + static panValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + const isValid = this.PATTERNS.PAN.test(control.value); + return isValid ? null : { invalidPan: true }; + }; + } + + static cinValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + const isValid = this.PATTERNS.CIN.test(control.value); + return isValid ? null : { invalidCin: true }; + }; + } + + static msmeValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + const isValid = this.PATTERNS.MSME.test(control.value); + return isValid ? null : { invalidMsme: true }; + }; + } + + static codeValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + const isValid = this.PATTERNS.CODE.test(control.value); + return isValid ? null : { invalidCode: true }; + }; + } + + static nameValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + if (control.value.length < 2) return { minLength: { requiredLength: 2, actualLength: control.value.length } }; + const isValid = this.PATTERNS.ALPHABET_WITH_SPACES.test(control.value); + return isValid ? null : { invalidName: true }; + }; + } + + static contactPersonValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + return control.value.length >= 2 ? null : { minLength: { requiredLength: 2, actualLength: control.value.length } }; + }; + } + + // Get error message for a control + static getErrorMessage(control: AbstractControl, fieldName: string): string { + if (!control.errors) return ''; + + if (control.errors['required']) { + return this.ERROR_MESSAGES.REQUIRED; + } + + if (control.errors['invalidEmail']) { + return this.ERROR_MESSAGES.EMAIL_INVALID; + } + + if (control.errors['invalidMobile']) { + return this.ERROR_MESSAGES.MOBILE_INVALID; + } + + if (control.errors['invalidPincode']) { + return this.ERROR_MESSAGES.PINCODE_INVALID; + } + + if (control.errors['invalidPan']) { + return this.ERROR_MESSAGES.PAN_INVALID; + } + + if (control.errors['invalidCin']) { + return this.ERROR_MESSAGES.CIN_INVALID; + } + + if (control.errors['invalidMsme']) { + return this.ERROR_MESSAGES.MSME_INVALID; + } + + if (control.errors['invalidCode']) { + return this.ERROR_MESSAGES.CODE_INVALID; + } + + if (control.errors['invalidName']) { + return this.ERROR_MESSAGES.NAME_INVALID; + } + + if (control.errors['minlength']) { + return this.ERROR_MESSAGES.MIN_LENGTH(control.errors['minlength'].requiredLength); + } + + if (control.errors['maxlength']) { + return this.ERROR_MESSAGES.MAX_LENGTH(control.errors['maxlength'].requiredLength); + } + + return 'Invalid input'; + } +} diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts new file mode 100644 index 0000000..a4206e1 --- /dev/null +++ b/frontend/src/environments/environment.prod.ts @@ -0,0 +1,6 @@ +export const environment = { + production: true, + encryptionKey: btoa('1234567890123456'), + baseUrl: 'https://society.nsbinfotech.com/app/api/v1', + rsaPublicKey: `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3RFV/f6ybsOF2m7NBLPUTMBq9b0frJG1HdIDYmrD9Wr1/aGBxTSJwq8IHFlatNpBF3OlJv9uEOybWMM1vXli4IgsuPPmcTOZsQ/O/9UGyBSL6apevNCw6pC1oa0MVLaN6COMAhDr+ri/PYiPQUcYsjDqghmAghMk99umHGUihz/oY/qgxzO+Q9cqePmjpH5c5RaXGBrOQxKoPlm7Uj6MqAfBhLC360VbcMot4XDoV+VeQXMzH0o6e870jdClsLOq1VsCA27jVvafj+HwaJ15ny9UWWilDuS/X8Sd7v+Rmd+qNezi6ROcglyaisXwKfeTWM8/o7HiUco2fL230+jEQIDAQAB` +}; \ No newline at end of file diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts new file mode 100644 index 0000000..631208a --- /dev/null +++ b/frontend/src/environments/environment.ts @@ -0,0 +1,9 @@ +export const environment = { + production: false, + encryptionKey: btoa('1234567890123456'), + authService: 'http://192.168.0.111:2001/cygnus/app/api/v1', + accountService: 'http://localhost:1701/cygnus/app/api/v1/account', + userService: 'http://localhost:1701/cygnus/app/api/v1/user', + masterService: 'http://localhost:1702/cygnus/app/api/v1/master', + rsaPublicKey: `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3RFV/f6ybsOF2m7NBLPUTMBq9b0frJG1HdIDYmrD9Wr1/aGBxTSJwq8IHFlatNpBF3OlJv9uEOybWMM1vXli4IgsuPPmcTOZsQ/O/9UGyBSL6apevNCw6pC1oa0MVLaN6COMAhDr+ri/PYiPQUcYsjDqghmAghMk99umHGUihz/oY/qgxzO+Q9cqePmjpH5c5RaXGBrOQxKoPlm7Uj6MqAfBhLC360VbcMot4XDoV+VeQXMzH0o6e870jdClsLOq1VsCA27jVvafj+HwaJ15ny9UWWilDuS/X8Sd7v+Rmd+qNezi6ROcglyaisXwKfeTWM8/o7HiUco2fL230+jEQIDAQAB` +}; \ No newline at end of file diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index ed3cf92..a82a241 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,17 +1,240 @@ -/* @import "primeng/resources/themes/aura-light-noir/theme.css"; */ -/* @import "primeng/resources/primeng.min.css"; */ -/* PrimeNG v18+ handles themes differently (often via Tailwind or presets). - For this prototype, we will rely on default component styles or add a CDN link if needed for quick styling. - However, PrimeIcons is still valid. */ -/* Quill Editor Styles */ +/* Global Styles from Cygnus-UI */ +@import "primeicons/primeicons.css"; +/* PrimeFlex is loaded via angular.json */ +@import "primeng/resources/themes/aura-light-green/theme.css"; +@import "primeng/resources/primeng.css"; @import "quill/dist/quill.core.css"; @import "quill/dist/quill.snow.css"; -@import "primeicons/primeicons.css"; +@layer primeng, primeng-overrides; -html, body { - margin: 0; - font-family: var(--font-family); - background-color: var(--surface-ground); - height: 100%; +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-Thin.ttf') format('truetype'); + font-weight: 100; + font-style: normal; } + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-ThinItalic.ttf') format('truetype'); + font-weight: 100; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-Light.ttf') format('truetype'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-LightItalic.ttf') format('truetype'); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-Italic.ttf') format('truetype'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-MediumItalic.ttf') format('truetype'); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-SemiBold.ttf') format('truetype'); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-SemiBoldItalic.ttf') format('truetype'); + font-weight: 600; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-BoldItalic.ttf') format('truetype'); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-ExtraBold.ttf') format('truetype'); + font-weight: 800; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-ExtraBoldItalic.ttf') format('truetype'); + font-weight: 800; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-Black.ttf') format('truetype'); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/assets/fonts/roboto/Roboto-BlackItalic.ttf') format('truetype'); + font-weight: 900; + font-style: italic; +} + +body { + font-family: 'Roboto', sans-serif; + background-color: rgba(226,232,240,0.4); + margin: 0px 2px; +} + +.fs-07{ font-size: 0.7rem; } +.fs-08{ font-size: 0.8rem; } +.fs-09{ font-size: 0.9rem; } +.fs-1{ font-size: 1rem; } +.fs-11{ font-size: 1.1rem; } +.fs-12{ font-size: 1.2rem; } +.fs-13{ font-size: 1.3rem; } +.fs-14{ font-size: 1.4rem; } +.fs-15{ font-size: 1.5rem; } + + +/* Overrides */ +.p-message .p-message-text { + font-size: 0.8rem; +} +.p-message-content { + justify-content: center; +} + +.p-float-label label{ + font-size: 0.9rem; + font-weight: normal !important; +} +@layer primeng-overrides { + /* Compact table styles */ + .p-datatable .p-datatable-tbody > tr > td { + font-size: 0.875rem; + padding: 0.4rem 0.5rem !important; + } + + .p-datatable .p-datatable-thead > tr > th { + font-size: 0.875rem; + padding: 0.75rem 0.75rem !important; + background-color: rgba(241, 245, 249, 1); + } + + .p-autocomplete-item { + font-size: 0.875rem; + } +} + +.p-fieldset .p-fieldset-legend { + /*background: var(--p-orange-500);*/ + background: rgba(245,115,22,0.9); + color: white; +} + +/* Fluid Support for compatibility with Cygnus-UI (PrimeNG v19 syntax) */ +input[fluid], textarea[fluid] { + width: 100%; +} + +p-dropdown[fluid] { + width: 100%; + display: block; +} +p-dropdown[fluid] .p-dropdown { + width: 100%; +} + +p-autoComplete[fluid] { + width: 100%; + display: block; +} +p-autoComplete[fluid] .p-autocomplete { + width: 100%; +} +p-autoComplete[fluid] .p-autocomplete-multiple-container { + width: 100%; +} + +p-calendar[fluid] { + width: 100%; + display: block; +} +p-calendar[fluid] .p-calendar { + width: 100%; +} +p-calendar[fluid] .p-inputtext { + width: 100%; +} + +p-inputNumber[fluid] { + width: 100%; + display: block; +} +p-inputNumber[fluid] .p-inputnumber { + width: 100%; +} +p-inputNumber[fluid] .p-inputtext { + width: 100%; +} + +p-password[fluid] { + width: 100%; + display: block; +} +p-password[fluid] .p-password { + width: 100%; +} +p-password[fluid] .p-inputtext { + width: 100%; +} + +p-multiSelect[fluid] { + width: 100%; + display: block; +} +p-multiSelect[fluid] .p-multiselect { + width: 100%; +} + diff --git a/ocr-session-api-dev.env b/ocr-session-api-dev.env new file mode 100644 index 0000000..f998f24 --- /dev/null +++ b/ocr-session-api-dev.env @@ -0,0 +1,15 @@ +SERVER_PORT=1700 +DB_URL=jdbc:postgresql://postgres-db:5432/ocr_subscribers +DB_USER=postgres +DB_PASSWORD="M@tr!x#149@dm!N" +REDIS_HOST=172.18.0.5 +REDIS_PORT=6379 +REDIS_PASSWORD=R3di5@2025 +MAX_REQUEST_SIZE=40MB +MULTIPART_REQUEST_SIZE=20MB +MULTIPART_FILE_SIZE=5MB +MINIO_URL=http://103.83.146.41:9800 +MINIO_KEY=admin +MINIO_SECRET=admin@123 +MINIO_BUCKET=cygnus-dev +MINIO_ENABLED=true \ No newline at end of file