ikmalsaid commited on
Commit
a200aad
·
verified ·
1 Parent(s): eb7619b

Updated src files

Browse files
Files changed (50) hide show
  1. .gitignore +171 -0
  2. LICENSE +661 -0
  3. app.py +2 -0
  4. main/__init__.py +156 -0
  5. main/controllers/admin.py +370 -0
  6. main/controllers/auth.py +127 -0
  7. main/controllers/finance.py +242 -0
  8. main/controllers/inventory.py +236 -0
  9. main/controllers/mosque.py +450 -0
  10. main/models/announcement.py +101 -0
  11. main/models/demo.py +246 -0
  12. main/models/finance.py +16 -0
  13. main/models/inventory.py +16 -0
  14. main/models/mosque.py +22 -0
  15. main/models/prayer_time.py +29 -0
  16. main/models/user.py +31 -0
  17. main/static/css/style.css +765 -0
  18. main/static/favicon/favicon.ico +0 -0
  19. main/static/js/main.js +1 -0
  20. main/templates/admin/add_admin.html +102 -0
  21. main/templates/admin/add_mosque.html +203 -0
  22. main/templates/admin/add_staff.html +102 -0
  23. main/templates/admin/dashboard.html +202 -0
  24. main/templates/admin/edit_mosque.html +181 -0
  25. main/templates/admin/mosque_details.html +285 -0
  26. main/templates/admin/reassign_mosque.html +53 -0
  27. main/templates/admin/reset_password.html +54 -0
  28. main/templates/auth/change_password.html +160 -0
  29. main/templates/auth/forgot_password.html +149 -0
  30. main/templates/auth/login.html +105 -0
  31. main/templates/auth/register.html +149 -0
  32. main/templates/auth/view_recovery_key.html +125 -0
  33. main/templates/base.html +505 -0
  34. main/templates/errors/error.html +59 -0
  35. main/templates/finance/add.html +111 -0
  36. main/templates/finance/edit.html +100 -0
  37. main/templates/finance/list.html +181 -0
  38. main/templates/home.html +262 -0
  39. main/templates/inventory/add.html +100 -0
  40. main/templates/inventory/edit.html +84 -0
  41. main/templates/inventory/list.html +179 -0
  42. main/templates/mosque/add_another_admin.html +93 -0
  43. main/templates/mosque/add_another_staff.html +93 -0
  44. main/templates/mosque/announcements.html +208 -0
  45. main/templates/mosque/dashboard.html +316 -0
  46. main/templates/mosque/edit_announcement.html +104 -0
  47. main/templates/mosque/mosque_info.html +162 -0
  48. main/templates/mosque/prayer_times.html +299 -0
  49. main/templates/mosque/register_mosque.html +207 -0
  50. requirements.txt +7 -0
.gitignore ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # PyPI configuration file
171
+ .pypirc
LICENSE ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU Affero General Public License is a free, copyleft license for
11
+ software and other kinds of works, specifically designed to ensure
12
+ cooperation with the community in the case of network server software.
13
+
14
+ The licenses for most software and other practical works are designed
15
+ to take away your freedom to share and change the works. By contrast,
16
+ our General Public Licenses are intended to guarantee your freedom to
17
+ share and change all versions of a program--to make sure it remains free
18
+ software for all its users.
19
+
20
+ When we speak of free software, we are referring to freedom, not
21
+ price. Our General Public Licenses are designed to make sure that you
22
+ have the freedom to distribute copies of free software (and charge for
23
+ them if you wish), that you receive source code or can get it if you
24
+ want it, that you can change the software or use pieces of it in new
25
+ free programs, and that you know you can do these things.
26
+
27
+ Developers that use our General Public Licenses protect your rights
28
+ with two steps: (1) assert copyright on the software, and (2) offer
29
+ you this License which gives you legal permission to copy, distribute
30
+ and/or modify the software.
31
+
32
+ A secondary benefit of defending all users' freedom is that
33
+ improvements made in alternate versions of the program, if they
34
+ receive widespread use, become available for other developers to
35
+ incorporate. Many developers of free software are heartened and
36
+ encouraged by the resulting cooperation. However, in the case of
37
+ software used on network servers, this result may fail to come about.
38
+ The GNU General Public License permits making a modified version and
39
+ letting the public access it on a server without ever releasing its
40
+ source code to the public.
41
+
42
+ The GNU Affero General Public License is designed specifically to
43
+ ensure that, in such cases, the modified source code becomes available
44
+ to the community. It requires the operator of a network server to
45
+ provide the source code of the modified version running there to the
46
+ users of that server. Therefore, public use of a modified version, on
47
+ a publicly accessible server, gives the public access to the source
48
+ code of the modified version.
49
+
50
+ An older license, called the Affero General Public License and
51
+ published by Affero, was designed to accomplish similar goals. This is
52
+ a different license, not a version of the Affero GPL, but Affero has
53
+ released a new version of the Affero GPL which permits relicensing under
54
+ this license.
55
+
56
+ The precise terms and conditions for copying, distribution and
57
+ modification follow.
58
+
59
+ TERMS AND CONDITIONS
60
+
61
+ 0. Definitions.
62
+
63
+ "This License" refers to version 3 of the GNU Affero General Public License.
64
+
65
+ "Copyright" also means copyright-like laws that apply to other kinds of
66
+ works, such as semiconductor masks.
67
+
68
+ "The Program" refers to any copyrightable work licensed under this
69
+ License. Each licensee is addressed as "you". "Licensees" and
70
+ "recipients" may be individuals or organizations.
71
+
72
+ To "modify" a work means to copy from or adapt all or part of the work
73
+ in a fashion requiring copyright permission, other than the making of an
74
+ exact copy. The resulting work is called a "modified version" of the
75
+ earlier work or a work "based on" the earlier work.
76
+
77
+ A "covered work" means either the unmodified Program or a work based
78
+ on the Program.
79
+
80
+ To "propagate" a work means to do anything with it that, without
81
+ permission, would make you directly or secondarily liable for
82
+ infringement under applicable copyright law, except executing it on a
83
+ computer or modifying a private copy. Propagation includes copying,
84
+ distribution (with or without modification), making available to the
85
+ public, and in some countries other activities as well.
86
+
87
+ To "convey" a work means any kind of propagation that enables other
88
+ parties to make or receive copies. Mere interaction with a user through
89
+ a computer network, with no transfer of a copy, is not conveying.
90
+
91
+ An interactive user interface displays "Appropriate Legal Notices"
92
+ to the extent that it includes a convenient and prominently visible
93
+ feature that (1) displays an appropriate copyright notice, and (2)
94
+ tells the user that there is no warranty for the work (except to the
95
+ extent that warranties are provided), that licensees may convey the
96
+ work under this License, and how to view a copy of this License. If
97
+ the interface presents a list of user commands or options, such as a
98
+ menu, a prominent item in the list meets this criterion.
99
+
100
+ 1. Source Code.
101
+
102
+ The "source code" for a work means the preferred form of the work
103
+ for making modifications to it. "Object code" means any non-source
104
+ form of a work.
105
+
106
+ A "Standard Interface" means an interface that either is an official
107
+ standard defined by a recognized standards body, or, in the case of
108
+ interfaces specified for a particular programming language, one that
109
+ is widely used among developers working in that language.
110
+
111
+ The "System Libraries" of an executable work include anything, other
112
+ than the work as a whole, that (a) is included in the normal form of
113
+ packaging a Major Component, but which is not part of that Major
114
+ Component, and (b) serves only to enable use of the work with that
115
+ Major Component, or to implement a Standard Interface for which an
116
+ implementation is available to the public in source code form. A
117
+ "Major Component", in this context, means a major essential component
118
+ (kernel, window system, and so on) of the specific operating system
119
+ (if any) on which the executable work runs, or a compiler used to
120
+ produce the work, or an object code interpreter used to run it.
121
+
122
+ The "Corresponding Source" for a work in object code form means all
123
+ the source code needed to generate, install, and (for an executable
124
+ work) run the object code and to modify the work, including scripts to
125
+ control those activities. However, it does not include the work's
126
+ System Libraries, or general-purpose tools or generally available free
127
+ programs which are used unmodified in performing those activities but
128
+ which are not part of the work. For example, Corresponding Source
129
+ includes interface definition files associated with source files for
130
+ the work, and the source code for shared libraries and dynamically
131
+ linked subprograms that the work is specifically designed to require,
132
+ such as by intimate data communication or control flow between those
133
+ subprograms and other parts of the work.
134
+
135
+ The Corresponding Source need not include anything that users
136
+ can regenerate automatically from other parts of the Corresponding
137
+ Source.
138
+
139
+ The Corresponding Source for a work in source code form is that
140
+ same work.
141
+
142
+ 2. Basic Permissions.
143
+
144
+ All rights granted under this License are granted for the term of
145
+ copyright on the Program, and are irrevocable provided the stated
146
+ conditions are met. This License explicitly affirms your unlimited
147
+ permission to run the unmodified Program. The output from running a
148
+ covered work is covered by this License only if the output, given its
149
+ content, constitutes a covered work. This License acknowledges your
150
+ rights of fair use or other equivalent, as provided by copyright law.
151
+
152
+ You may make, run and propagate covered works that you do not
153
+ convey, without conditions so long as your license otherwise remains
154
+ in force. You may convey covered works to others for the sole purpose
155
+ of having them make modifications exclusively for you, or provide you
156
+ with facilities for running those works, provided that you comply with
157
+ the terms of this License in conveying all material for which you do
158
+ not control copyright. Those thus making or running the covered works
159
+ for you must do so exclusively on your behalf, under your direction
160
+ and control, on terms that prohibit them from making any copies of
161
+ your copyrighted material outside their relationship with you.
162
+
163
+ Conveying under any other circumstances is permitted solely under
164
+ the conditions stated below. Sublicensing is not allowed; section 10
165
+ makes it unnecessary.
166
+
167
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168
+
169
+ No covered work shall be deemed part of an effective technological
170
+ measure under any applicable law fulfilling obligations under article
171
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172
+ similar laws prohibiting or restricting circumvention of such
173
+ measures.
174
+
175
+ When you convey a covered work, you waive any legal power to forbid
176
+ circumvention of technological measures to the extent such circumvention
177
+ is effected by exercising rights under this License with respect to
178
+ the covered work, and you disclaim any intention to limit operation or
179
+ modification of the work as a means of enforcing, against the work's
180
+ users, your or third parties' legal rights to forbid circumvention of
181
+ technological measures.
182
+
183
+ 4. Conveying Verbatim Copies.
184
+
185
+ You may convey verbatim copies of the Program's source code as you
186
+ receive it, in any medium, provided that you conspicuously and
187
+ appropriately publish on each copy an appropriate copyright notice;
188
+ keep intact all notices stating that this License and any
189
+ non-permissive terms added in accord with section 7 apply to the code;
190
+ keep intact all notices of the absence of any warranty; and give all
191
+ recipients a copy of this License along with the Program.
192
+
193
+ You may charge any price or no price for each copy that you convey,
194
+ and you may offer support or warranty protection for a fee.
195
+
196
+ 5. Conveying Modified Source Versions.
197
+
198
+ You may convey a work based on the Program, or the modifications to
199
+ produce it from the Program, in the form of source code under the
200
+ terms of section 4, provided that you also meet all of these conditions:
201
+
202
+ a) The work must carry prominent notices stating that you modified
203
+ it, and giving a relevant date.
204
+
205
+ b) The work must carry prominent notices stating that it is
206
+ released under this License and any conditions added under section
207
+ 7. This requirement modifies the requirement in section 4 to
208
+ "keep intact all notices".
209
+
210
+ c) You must license the entire work, as a whole, under this
211
+ License to anyone who comes into possession of a copy. This
212
+ License will therefore apply, along with any applicable section 7
213
+ additional terms, to the whole of the work, and all its parts,
214
+ regardless of how they are packaged. This License gives no
215
+ permission to license the work in any other way, but it does not
216
+ invalidate such permission if you have separately received it.
217
+
218
+ d) If the work has interactive user interfaces, each must display
219
+ Appropriate Legal Notices; however, if the Program has interactive
220
+ interfaces that do not display Appropriate Legal Notices, your
221
+ work need not make them do so.
222
+
223
+ A compilation of a covered work with other separate and independent
224
+ works, which are not by their nature extensions of the covered work,
225
+ and which are not combined with it such as to form a larger program,
226
+ in or on a volume of a storage or distribution medium, is called an
227
+ "aggregate" if the compilation and its resulting copyright are not
228
+ used to limit the access or legal rights of the compilation's users
229
+ beyond what the individual works permit. Inclusion of a covered work
230
+ in an aggregate does not cause this License to apply to the other
231
+ parts of the aggregate.
232
+
233
+ 6. Conveying Non-Source Forms.
234
+
235
+ You may convey a covered work in object code form under the terms
236
+ of sections 4 and 5, provided that you also convey the
237
+ machine-readable Corresponding Source under the terms of this License,
238
+ in one of these ways:
239
+
240
+ a) Convey the object code in, or embodied in, a physical product
241
+ (including a physical distribution medium), accompanied by the
242
+ Corresponding Source fixed on a durable physical medium
243
+ customarily used for software interchange.
244
+
245
+ b) Convey the object code in, or embodied in, a physical product
246
+ (including a physical distribution medium), accompanied by a
247
+ written offer, valid for at least three years and valid for as
248
+ long as you offer spare parts or customer support for that product
249
+ model, to give anyone who possesses the object code either (1) a
250
+ copy of the Corresponding Source for all the software in the
251
+ product that is covered by this License, on a durable physical
252
+ medium customarily used for software interchange, for a price no
253
+ more than your reasonable cost of physically performing this
254
+ conveying of source, or (2) access to copy the
255
+ Corresponding Source from a network server at no charge.
256
+
257
+ c) Convey individual copies of the object code with a copy of the
258
+ written offer to provide the Corresponding Source. This
259
+ alternative is allowed only occasionally and noncommercially, and
260
+ only if you received the object code with such an offer, in accord
261
+ with subsection 6b.
262
+
263
+ d) Convey the object code by offering access from a designated
264
+ place (gratis or for a charge), and offer equivalent access to the
265
+ Corresponding Source in the same way through the same place at no
266
+ further charge. You need not require recipients to copy the
267
+ Corresponding Source along with the object code. If the place to
268
+ copy the object code is a network server, the Corresponding Source
269
+ may be on a different server (operated by you or a third party)
270
+ that supports equivalent copying facilities, provided you maintain
271
+ clear directions next to the object code saying where to find the
272
+ Corresponding Source. Regardless of what server hosts the
273
+ Corresponding Source, you remain obligated to ensure that it is
274
+ available for as long as needed to satisfy these requirements.
275
+
276
+ e) Convey the object code using peer-to-peer transmission, provided
277
+ you inform other peers where the object code and Corresponding
278
+ Source of the work are being offered to the general public at no
279
+ charge under subsection 6d.
280
+
281
+ A separable portion of the object code, whose source code is excluded
282
+ from the Corresponding Source as a System Library, need not be
283
+ included in conveying the object code work.
284
+
285
+ A "User Product" is either (1) a "consumer product", which means any
286
+ tangible personal property which is normally used for personal, family,
287
+ or household purposes, or (2) anything designed or sold for incorporation
288
+ into a dwelling. In determining whether a product is a consumer product,
289
+ doubtful cases shall be resolved in favor of coverage. For a particular
290
+ product received by a particular user, "normally used" refers to a
291
+ typical or common use of that class of product, regardless of the status
292
+ of the particular user or of the way in which the particular user
293
+ actually uses, or expects or is expected to use, the product. A product
294
+ is a consumer product regardless of whether the product has substantial
295
+ commercial, industrial or non-consumer uses, unless such uses represent
296
+ the only significant mode of use of the product.
297
+
298
+ "Installation Information" for a User Product means any methods,
299
+ procedures, authorization keys, or other information required to install
300
+ and execute modified versions of a covered work in that User Product from
301
+ a modified version of its Corresponding Source. The information must
302
+ suffice to ensure that the continued functioning of the modified object
303
+ code is in no case prevented or interfered with solely because
304
+ modification has been made.
305
+
306
+ If you convey an object code work under this section in, or with, or
307
+ specifically for use in, a User Product, and the conveying occurs as
308
+ part of a transaction in which the right of possession and use of the
309
+ User Product is transferred to the recipient in perpetuity or for a
310
+ fixed term (regardless of how the transaction is characterized), the
311
+ Corresponding Source conveyed under this section must be accompanied
312
+ by the Installation Information. But this requirement does not apply
313
+ if neither you nor any third party retains the ability to install
314
+ modified object code on the User Product (for example, the work has
315
+ been installed in ROM).
316
+
317
+ The requirement to provide Installation Information does not include a
318
+ requirement to continue to provide support service, warranty, or updates
319
+ for a work that has been modified or installed by the recipient, or for
320
+ the User Product in which it has been modified or installed. Access to a
321
+ network may be denied when the modification itself materially and
322
+ adversely affects the operation of the network or violates the rules and
323
+ protocols for communication across the network.
324
+
325
+ Corresponding Source conveyed, and Installation Information provided,
326
+ in accord with this section must be in a format that is publicly
327
+ documented (and with an implementation available to the public in
328
+ source code form), and must require no special password or key for
329
+ unpacking, reading or copying.
330
+
331
+ 7. Additional Terms.
332
+
333
+ "Additional permissions" are terms that supplement the terms of this
334
+ License by making exceptions from one or more of its conditions.
335
+ Additional permissions that are applicable to the entire Program shall
336
+ be treated as though they were included in this License, to the extent
337
+ that they are valid under applicable law. If additional permissions
338
+ apply only to part of the Program, that part may be used separately
339
+ under those permissions, but the entire Program remains governed by
340
+ this License without regard to the additional permissions.
341
+
342
+ When you convey a copy of a covered work, you may at your option
343
+ remove any additional permissions from that copy, or from any part of
344
+ it. (Additional permissions may be written to require their own
345
+ removal in certain cases when you modify the work.) You may place
346
+ additional permissions on material, added by you to a covered work,
347
+ for which you have or can give appropriate copyright permission.
348
+
349
+ Notwithstanding any other provision of this License, for material you
350
+ add to a covered work, you may (if authorized by the copyright holders of
351
+ that material) supplement the terms of this License with terms:
352
+
353
+ a) Disclaiming warranty or limiting liability differently from the
354
+ terms of sections 15 and 16 of this License; or
355
+
356
+ b) Requiring preservation of specified reasonable legal notices or
357
+ author attributions in that material or in the Appropriate Legal
358
+ Notices displayed by works containing it; or
359
+
360
+ c) Prohibiting misrepresentation of the origin of that material, or
361
+ requiring that modified versions of such material be marked in
362
+ reasonable ways as different from the original version; or
363
+
364
+ d) Limiting the use for publicity purposes of names of licensors or
365
+ authors of the material; or
366
+
367
+ e) Declining to grant rights under trademark law for use of some
368
+ trade names, trademarks, or service marks; or
369
+
370
+ f) Requiring indemnification of licensors and authors of that
371
+ material by anyone who conveys the material (or modified versions of
372
+ it) with contractual assumptions of liability to the recipient, for
373
+ any liability that these contractual assumptions directly impose on
374
+ those licensors and authors.
375
+
376
+ All other non-permissive additional terms are considered "further
377
+ restrictions" within the meaning of section 10. If the Program as you
378
+ received it, or any part of it, contains a notice stating that it is
379
+ governed by this License along with a term that is a further
380
+ restriction, you may remove that term. If a license document contains
381
+ a further restriction but permits relicensing or conveying under this
382
+ License, you may add to a covered work material governed by the terms
383
+ of that license document, provided that the further restriction does
384
+ not survive such relicensing or conveying.
385
+
386
+ If you add terms to a covered work in accord with this section, you
387
+ must place, in the relevant source files, a statement of the
388
+ additional terms that apply to those files, or a notice indicating
389
+ where to find the applicable terms.
390
+
391
+ Additional terms, permissive or non-permissive, may be stated in the
392
+ form of a separately written license, or stated as exceptions;
393
+ the above requirements apply either way.
394
+
395
+ 8. Termination.
396
+
397
+ You may not propagate or modify a covered work except as expressly
398
+ provided under this License. Any attempt otherwise to propagate or
399
+ modify it is void, and will automatically terminate your rights under
400
+ this License (including any patent licenses granted under the third
401
+ paragraph of section 11).
402
+
403
+ However, if you cease all violation of this License, then your
404
+ license from a particular copyright holder is reinstated (a)
405
+ provisionally, unless and until the copyright holder explicitly and
406
+ finally terminates your license, and (b) permanently, if the copyright
407
+ holder fails to notify you of the violation by some reasonable means
408
+ prior to 60 days after the cessation.
409
+
410
+ Moreover, your license from a particular copyright holder is
411
+ reinstated permanently if the copyright holder notifies you of the
412
+ violation by some reasonable means, this is the first time you have
413
+ received notice of violation of this License (for any work) from that
414
+ copyright holder, and you cure the violation prior to 30 days after
415
+ your receipt of the notice.
416
+
417
+ Termination of your rights under this section does not terminate the
418
+ licenses of parties who have received copies or rights from you under
419
+ this License. If your rights have been terminated and not permanently
420
+ reinstated, you do not qualify to receive new licenses for the same
421
+ material under section 10.
422
+
423
+ 9. Acceptance Not Required for Having Copies.
424
+
425
+ You are not required to accept this License in order to receive or
426
+ run a copy of the Program. Ancillary propagation of a covered work
427
+ occurring solely as a consequence of using peer-to-peer transmission
428
+ to receive a copy likewise does not require acceptance. However,
429
+ nothing other than this License grants you permission to propagate or
430
+ modify any covered work. These actions infringe copyright if you do
431
+ not accept this License. Therefore, by modifying or propagating a
432
+ covered work, you indicate your acceptance of this License to do so.
433
+
434
+ 10. Automatic Licensing of Downstream Recipients.
435
+
436
+ Each time you convey a covered work, the recipient automatically
437
+ receives a license from the original licensors, to run, modify and
438
+ propagate that work, subject to this License. You are not responsible
439
+ for enforcing compliance by third parties with this License.
440
+
441
+ An "entity transaction" is a transaction transferring control of an
442
+ organization, or substantially all assets of one, or subdividing an
443
+ organization, or merging organizations. If propagation of a covered
444
+ work results from an entity transaction, each party to that
445
+ transaction who receives a copy of the work also receives whatever
446
+ licenses to the work the party's predecessor in interest had or could
447
+ give under the previous paragraph, plus a right to possession of the
448
+ Corresponding Source of the work from the predecessor in interest, if
449
+ the predecessor has it or can get it with reasonable efforts.
450
+
451
+ You may not impose any further restrictions on the exercise of the
452
+ rights granted or affirmed under this License. For example, you may
453
+ not impose a license fee, royalty, or other charge for exercise of
454
+ rights granted under this License, and you may not initiate litigation
455
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
456
+ any patent claim is infringed by making, using, selling, offering for
457
+ sale, or importing the Program or any portion of it.
458
+
459
+ 11. Patents.
460
+
461
+ A "contributor" is a copyright holder who authorizes use under this
462
+ License of the Program or a work on which the Program is based. The
463
+ work thus licensed is called the contributor's "contributor version".
464
+
465
+ A contributor's "essential patent claims" are all patent claims
466
+ owned or controlled by the contributor, whether already acquired or
467
+ hereafter acquired, that would be infringed by some manner, permitted
468
+ by this License, of making, using, or selling its contributor version,
469
+ but do not include claims that would be infringed only as a
470
+ consequence of further modification of the contributor version. For
471
+ purposes of this definition, "control" includes the right to grant
472
+ patent sublicenses in a manner consistent with the requirements of
473
+ this License.
474
+
475
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
476
+ patent license under the contributor's essential patent claims, to
477
+ make, use, sell, offer for sale, import and otherwise run, modify and
478
+ propagate the contents of its contributor version.
479
+
480
+ In the following three paragraphs, a "patent license" is any express
481
+ agreement or commitment, however denominated, not to enforce a patent
482
+ (such as an express permission to practice a patent or covenant not to
483
+ sue for patent infringement). To "grant" such a patent license to a
484
+ party means to make such an agreement or commitment not to enforce a
485
+ patent against the party.
486
+
487
+ If you convey a covered work, knowingly relying on a patent license,
488
+ and the Corresponding Source of the work is not available for anyone
489
+ to copy, free of charge and under the terms of this License, through a
490
+ publicly available network server or other readily accessible means,
491
+ then you must either (1) cause the Corresponding Source to be so
492
+ available, or (2) arrange to deprive yourself of the benefit of the
493
+ patent license for this particular work, or (3) arrange, in a manner
494
+ consistent with the requirements of this License, to extend the patent
495
+ license to downstream recipients. "Knowingly relying" means you have
496
+ actual knowledge that, but for the patent license, your conveying the
497
+ covered work in a country, or your recipient's use of the covered work
498
+ in a country, would infringe one or more identifiable patents in that
499
+ country that you have reason to believe are valid.
500
+
501
+ If, pursuant to or in connection with a single transaction or
502
+ arrangement, you convey, or propagate by procuring conveyance of, a
503
+ covered work, and grant a patent license to some of the parties
504
+ receiving the covered work authorizing them to use, propagate, modify
505
+ or convey a specific copy of the covered work, then the patent license
506
+ you grant is automatically extended to all recipients of the covered
507
+ work and works based on it.
508
+
509
+ A patent license is "discriminatory" if it does not include within
510
+ the scope of its coverage, prohibits the exercise of, or is
511
+ conditioned on the non-exercise of one or more of the rights that are
512
+ specifically granted under this License. You may not convey a covered
513
+ work if you are a party to an arrangement with a third party that is
514
+ in the business of distributing software, under which you make payment
515
+ to the third party based on the extent of your activity of conveying
516
+ the work, and under which the third party grants, to any of the
517
+ parties who would receive the covered work from you, a discriminatory
518
+ patent license (a) in connection with copies of the covered work
519
+ conveyed by you (or copies made from those copies), or (b) primarily
520
+ for and in connection with specific products or compilations that
521
+ contain the covered work, unless you entered into that arrangement,
522
+ or that patent license was granted, prior to 28 March 2007.
523
+
524
+ Nothing in this License shall be construed as excluding or limiting
525
+ any implied license or other defenses to infringement that may
526
+ otherwise be available to you under applicable patent law.
527
+
528
+ 12. No Surrender of Others' Freedom.
529
+
530
+ If conditions are imposed on you (whether by court order, agreement or
531
+ otherwise) that contradict the conditions of this License, they do not
532
+ excuse you from the conditions of this License. If you cannot convey a
533
+ covered work so as to satisfy simultaneously your obligations under this
534
+ License and any other pertinent obligations, then as a consequence you may
535
+ not convey it at all. For example, if you agree to terms that obligate you
536
+ to collect a royalty for further conveying from those to whom you convey
537
+ the Program, the only way you could satisfy both those terms and this
538
+ License would be to refrain entirely from conveying the Program.
539
+
540
+ 13. Remote Network Interaction; Use with the GNU General Public License.
541
+
542
+ Notwithstanding any other provision of this License, if you modify the
543
+ Program, your modified version must prominently offer all users
544
+ interacting with it remotely through a computer network (if your version
545
+ supports such interaction) an opportunity to receive the Corresponding
546
+ Source of your version by providing access to the Corresponding Source
547
+ from a network server at no charge, through some standard or customary
548
+ means of facilitating copying of software. This Corresponding Source
549
+ shall include the Corresponding Source for any work covered by version 3
550
+ of the GNU General Public License that is incorporated pursuant to the
551
+ following paragraph.
552
+
553
+ Notwithstanding any other provision of this License, you have
554
+ permission to link or combine any covered work with a work licensed
555
+ under version 3 of the GNU General Public License into a single
556
+ combined work, and to convey the resulting work. The terms of this
557
+ License will continue to apply to the part which is the covered work,
558
+ but the work with which it is combined will remain governed by version
559
+ 3 of the GNU General Public License.
560
+
561
+ 14. Revised Versions of this License.
562
+
563
+ The Free Software Foundation may publish revised and/or new versions of
564
+ the GNU Affero General Public License from time to time. Such new versions
565
+ will be similar in spirit to the present version, but may differ in detail to
566
+ address new problems or concerns.
567
+
568
+ Each version is given a distinguishing version number. If the
569
+ Program specifies that a certain numbered version of the GNU Affero General
570
+ Public License "or any later version" applies to it, you have the
571
+ option of following the terms and conditions either of that numbered
572
+ version or of any later version published by the Free Software
573
+ Foundation. If the Program does not specify a version number of the
574
+ GNU Affero General Public License, you may choose any version ever published
575
+ by the Free Software Foundation.
576
+
577
+ If the Program specifies that a proxy can decide which future
578
+ versions of the GNU Affero General Public License can be used, that proxy's
579
+ public statement of acceptance of a version permanently authorizes you
580
+ to choose that version for the Program.
581
+
582
+ Later license versions may give you additional or different
583
+ permissions. However, no additional obligations are imposed on any
584
+ author or copyright holder as a result of your choosing to follow a
585
+ later version.
586
+
587
+ 15. Disclaimer of Warranty.
588
+
589
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597
+
598
+ 16. Limitation of Liability.
599
+
600
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608
+ SUCH DAMAGES.
609
+
610
+ 17. Interpretation of Sections 15 and 16.
611
+
612
+ If the disclaimer of warranty and limitation of liability provided
613
+ above cannot be given local legal effect according to their terms,
614
+ reviewing courts shall apply local law that most closely approximates
615
+ an absolute waiver of all civil liability in connection with the
616
+ Program, unless a warranty or assumption of liability accompanies a
617
+ copy of the Program in return for a fee.
618
+
619
+ END OF TERMS AND CONDITIONS
620
+
621
+ How to Apply These Terms to Your New Programs
622
+
623
+ If you develop a new program, and you want it to be of the greatest
624
+ possible use to the public, the best way to achieve this is to make it
625
+ free software which everyone can redistribute and change under these terms.
626
+
627
+ To do so, attach the following notices to the program. It is safest
628
+ to attach them to the start of each source file to most effectively
629
+ state the exclusion of warranty; and each file should have at least
630
+ the "copyright" line and a pointer to where the full notice is found.
631
+
632
+ <one line to give the program's name and a brief idea of what it does.>
633
+ Copyright (C) <year> <name of author>
634
+
635
+ This program is free software: you can redistribute it and/or modify
636
+ it under the terms of the GNU Affero General Public License as published
637
+ by the Free Software Foundation, either version 3 of the License, or
638
+ (at your option) any later version.
639
+
640
+ This program is distributed in the hope that it will be useful,
641
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
642
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643
+ GNU Affero General Public License for more details.
644
+
645
+ You should have received a copy of the GNU Affero General Public License
646
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
647
+
648
+ Also add information on how to contact you by electronic and paper mail.
649
+
650
+ If your software can interact with users remotely through a computer
651
+ network, you should also make sure that it provides a way for users to
652
+ get its source. For example, if your program is a web application, its
653
+ interface could display a "Source" link that leads users to an archive
654
+ of the code. There are many ways you could offer source, and different
655
+ solutions will be better for different programs; see section 13 for the
656
+ specific requirements.
657
+
658
+ You should also get your employer (if you work as a programmer) or school,
659
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
660
+ For more information on this, and how to apply and follow the GNU AGPL, see
661
+ <https://www.gnu.org/licenses/>.
app.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from main import start_server
2
+ start_server(demo_mode=True, launch=True)
main/__init__.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ from flask_login import LoginManager, current_user
4
+ from colorpaws import ColorPaws
5
+ import os
6
+
7
+ logger = ColorPaws(__name__)
8
+ db = SQLAlchemy()
9
+
10
+ def get_db_name(demo_mode=False):
11
+ """Return the appropriate database name based on demo mode"""
12
+ return "mosqku_demo.db" if demo_mode else "mosqku_client.db"
13
+
14
+ def start_server(demo_mode=False, custom_error_handler=True, launch=True,
15
+ host='0.0.0.0', port=7860, debug=False):
16
+ """
17
+ Starts the Mosqku web service.
18
+
19
+ Parameters:
20
+ - demo_mode: Starts in demo mode with pre-defined database.
21
+ - custom_error_handler: Handles web errors with custom responses.
22
+ - launch: Launch the web server automatically.
23
+ - host: Specify host address.
24
+ - port: Specify port number.
25
+ - debug: Enable Flask debug mode.
26
+ """
27
+ logger.info(f"Starting Mosqku ({'Demo' if demo_mode else 'Client'} Mode)...")
28
+ logger.info(f'In loving memory of my beloved cat and kitten, Niddy and Nimi!')
29
+
30
+ app = Flask(__name__)
31
+ app.config['SECRET_KEY'] = os.getenv('MOSQKU_SECRET_KEY', 'mosqku-key-2025')
32
+ app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{get_db_name(demo_mode)}'
33
+ app.config['DEMO_MODE'] = demo_mode
34
+
35
+ if custom_error_handler:
36
+ app.config['DEBUG'] = False
37
+ app.config['PROPAGATE_EXCEPTIONS'] = False
38
+ app.config['PRESERVE_CONTEXT_ON_EXCEPTION'] = False
39
+
40
+ db.init_app(app)
41
+
42
+ from .models.user import User
43
+ from .models.mosque import Mosque
44
+ from .models.prayer_time import PrayerTime
45
+ from .models.announcement import Announcement
46
+
47
+ # Import blueprints
48
+ from .controllers.auth import auth as auth_blueprint
49
+ from .controllers.admin import admin as admin_blueprint
50
+ from .controllers.mosque import mosque as mosque_blueprint
51
+ from .controllers.inventory import inventory as inventory_blueprint
52
+ from .controllers.finance import finance as finance_blueprint
53
+
54
+ # Register blueprints - Note that mosque blueprint is registered first to handle root URL
55
+ app.register_blueprint(mosque_blueprint)
56
+ app.register_blueprint(auth_blueprint, url_prefix='/auth')
57
+ app.register_blueprint(admin_blueprint, url_prefix='/admin')
58
+ app.register_blueprint(inventory_blueprint)
59
+ app.register_blueprint(finance_blueprint)
60
+
61
+ # Initialize Login Manager
62
+ login_manager = LoginManager()
63
+ login_manager.login_view = 'auth.login'
64
+ login_manager.init_app(app)
65
+
66
+ @login_manager.user_loader
67
+ def load_user(id):
68
+ return User.query.get(int(id))
69
+
70
+ if custom_error_handler:
71
+ @app.errorhandler(404)
72
+ def page_not_found(e):
73
+ return render_template('errors/error.html',
74
+ error_code=404,
75
+ error_title="Page Not Found",
76
+ error_description="The page you're looking for doesn't exist.",
77
+ user=current_user,
78
+ hide_breadcrumbs=True), 404
79
+
80
+ @app.errorhandler(403)
81
+ def forbidden(e):
82
+ return render_template('errors/error.html',
83
+ error_code=403,
84
+ error_title="Access Forbidden",
85
+ error_description="You don't have permission to access this resource.",
86
+ user=current_user,
87
+ hide_breadcrumbs=True), 403
88
+
89
+ @app.errorhandler(500)
90
+ def internal_server_error(e):
91
+ return render_template('errors/error.html',
92
+ error_code=500,
93
+ error_title="Internal Server Error",
94
+ error_description="Something went wrong on our end. Please try again later.",
95
+ user=current_user,
96
+ hide_breadcrumbs=True), 500
97
+
98
+ @app.errorhandler(401)
99
+ def unauthorized(e):
100
+ return render_template('errors/error.html',
101
+ error_code=401,
102
+ error_title="Unauthorized Access",
103
+ error_description="Please log in to access this page.",
104
+ user=current_user,
105
+ hide_breadcrumbs=True), 401
106
+
107
+ # Handle all other exceptions
108
+ @app.errorhandler(Exception)
109
+ def handle_exception(e):
110
+ # Log the error here if you want
111
+ return render_template('errors/error.html',
112
+ error_code=500,
113
+ error_title="Internal Server Error",
114
+ error_description="Something went wrong on our end. Please try again later.",
115
+ user=current_user,
116
+ hide_breadcrumbs=True), 500
117
+
118
+ # Create database and optionally add demo data
119
+ create_database(app, demo_mode)
120
+
121
+ if launch:
122
+ app.run(host=host, port=port, debug=debug)
123
+ else:
124
+ return app
125
+
126
+ def create_database(app, demo_mode=False):
127
+ db_name = get_db_name(demo_mode)
128
+ if not os.path.exists('instance/' + db_name):
129
+ with app.app_context():
130
+ db.create_all()
131
+
132
+ # Create default superadmin account
133
+ from werkzeug.security import generate_password_hash
134
+ from .models.user import User
135
+
136
+ default_admin = User.query.filter_by(email='[email protected]').first()
137
+ if not default_admin:
138
+ recovery_key = User.generate_recovery_key()
139
+ default_admin = User(
140
+ email='[email protected]',
141
+ name='superadmin',
142
+ password=generate_password_hash('Admin@123', method='scrypt'),
143
+ role='superadmin',
144
+ recovery_key=recovery_key
145
+ )
146
+ db.session.add(default_admin)
147
+ db.session.commit()
148
+ logger.info(f'Created superadmin account: {default_admin.email}')
149
+ logger.info(f'Default superadmin recovery key: {recovery_key}')
150
+
151
+ # Generate demo data if requested
152
+ if demo_mode:
153
+ from .models.demo import generate_demo_data
154
+ generate_demo_data(logger=logger)
155
+
156
+ logger.info(f'Created database: {os.path.join(app.instance_path, db_name)}')
main/controllers/admin.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, flash, redirect, url_for
2
+ from flask_login import login_required, current_user
3
+ from ..models.user import User
4
+ from ..models.mosque import Mosque
5
+ from ..models.announcement import Announcement
6
+ from ..models.prayer_time import PrayerTime
7
+ from .. import db
8
+ from werkzeug.security import generate_password_hash
9
+ from datetime import datetime, date
10
+
11
+ admin = Blueprint('admin', __name__)
12
+
13
+ @admin.route('/dashboard')
14
+ @login_required
15
+ def dashboard():
16
+ if current_user.role != 'superadmin':
17
+ flash('Unauthorized access.', category='error')
18
+ return redirect(url_for('mosque.dashboard'))
19
+
20
+ mosques = Mosque.query.all()
21
+ users = User.query.filter(User.role.in_(['admin', 'staff'])).order_by(User.mosque_id.asc(), User.role.asc(), User.name.asc()).all()
22
+
23
+ # Get mosque names for all users in one query to avoid N+1 problem
24
+ mosque_dict = {m.id: m.name for m in Mosque.query.all()}
25
+
26
+ return render_template(
27
+ "admin/dashboard.html",
28
+ user=current_user,
29
+ mosques=mosques,
30
+ users=users,
31
+ mosque_dict=mosque_dict
32
+ )
33
+
34
+ @admin.route('/add_mosque', methods=['GET', 'POST'])
35
+ @login_required
36
+ def add_mosque():
37
+ if current_user.role != 'superadmin':
38
+ flash('Unauthorized access.', category='error')
39
+ return redirect(url_for('mosque.dashboard'))
40
+
41
+ if request.method == 'POST':
42
+ name = request.form.get('name')
43
+ address = request.form.get('address')
44
+ city = request.form.get('city')
45
+ state = request.form.get('state')
46
+ country = request.form.get('country')
47
+ phone = request.form.get('phone')
48
+ email = request.form.get('email')
49
+ capacity = request.form.get('capacity')
50
+ jakim_code = request.form.get('jakim_code')
51
+
52
+ try:
53
+ mosque = Mosque(
54
+ name=name,
55
+ address=address,
56
+ city=city,
57
+ state=state,
58
+ country=country,
59
+ phone=phone,
60
+ email=email,
61
+ capacity=capacity,
62
+ jakim_code=jakim_code
63
+ )
64
+ db.session.add(mosque)
65
+ db.session.commit()
66
+ flash('Mosque added successfully!', category='success')
67
+ return redirect(url_for('admin.dashboard'))
68
+ except Exception as e:
69
+ flash('Error adding mosque.', category='error')
70
+
71
+ return render_template("admin/add_mosque.html", user=current_user)
72
+
73
+ @admin.route('/add_admin', methods=['GET', 'POST'])
74
+ @login_required
75
+ def add_admin():
76
+ if current_user.role != 'superadmin':
77
+ flash('Unauthorized access.', category='error')
78
+ return redirect(url_for('mosque.dashboard'))
79
+
80
+ if request.method == 'POST':
81
+ email = request.form.get('email')
82
+ name = request.form.get('name')
83
+ password = request.form.get('password')
84
+ mosque_id = request.form.get('mosque_id')
85
+
86
+ user = User.query.filter_by(email=email).first()
87
+ if user:
88
+ flash('Email already exists.', category='error')
89
+ else:
90
+ try:
91
+ recovery_key = User.generate_recovery_key()
92
+ new_admin = User(
93
+ email=email,
94
+ name=name,
95
+ password=generate_password_hash(password, method='scrypt'),
96
+ role='admin',
97
+ mosque_id=mosque_id,
98
+ recovery_key=recovery_key
99
+ )
100
+ db.session.add(new_admin)
101
+ db.session.commit()
102
+ flash(f'{name} added successfully!', category='success')
103
+ return redirect(url_for('admin.dashboard'))
104
+ except Exception as e:
105
+ flash('Error adding admin.', category='error')
106
+
107
+ mosques = Mosque.query.all()
108
+ return render_template("admin/add_admin.html", user=current_user, mosques=mosques)
109
+
110
+ @admin.route('/edit_mosque/<int:id>', methods=['GET', 'POST'])
111
+ @login_required
112
+ def edit_mosque(id):
113
+ if current_user.role != 'superadmin':
114
+ flash('Unauthorized access.', category='error')
115
+ return redirect(url_for('mosque.dashboard'))
116
+
117
+ mosque = Mosque.query.get_or_404(id)
118
+
119
+ if request.method == 'POST':
120
+ mosque.name = request.form.get('name')
121
+ mosque.address = request.form.get('address')
122
+ mosque.city = request.form.get('city')
123
+ mosque.state = request.form.get('state')
124
+ mosque.country = request.form.get('country')
125
+ mosque.phone = request.form.get('phone')
126
+ mosque.email = request.form.get('email')
127
+ mosque.capacity = request.form.get('capacity')
128
+ mosque.jakim_code = request.form.get('jakim_code')
129
+
130
+ try:
131
+ db.session.commit()
132
+ flash('Mosque updated successfully!', category='success')
133
+ return redirect(url_for('admin.dashboard'))
134
+ except Exception as e:
135
+ flash('Error updating mosque.', category='error')
136
+
137
+ return render_template("admin/edit_mosque.html", user=current_user, mosque=mosque)
138
+
139
+ @admin.route('/mosque_announcements/<int:mosque_id>', methods=['GET', 'POST'])
140
+ @login_required
141
+ def mosque_announcements(mosque_id):
142
+ if current_user.role != 'superadmin':
143
+ flash('Unauthorized access.', category='error')
144
+ return redirect(url_for('mosque.dashboard'))
145
+
146
+ mosque = Mosque.query.get_or_404(mosque_id)
147
+
148
+ if request.method == 'POST':
149
+ title = request.form.get('title')
150
+ content = request.form.get('content')
151
+ start_date = datetime.strptime(request.form.get('start_date'), '%Y-%m-%d').date()
152
+ end_date = datetime.strptime(request.form.get('end_date'), '%Y-%m-%d').date()
153
+ start_time = datetime.strptime(request.form.get('start_time'), '%H:%M').time()
154
+ end_time = datetime.strptime(request.form.get('end_time'), '%H:%M').time()
155
+ is_urgent = request.form.get('is_urgent') == 'on'
156
+
157
+ if start_date > end_date or (start_date == end_date and start_time >= end_time):
158
+ flash('End date/time must be after start date/time.', 'error')
159
+ return redirect(url_for('admin.mosque_announcements', mosque_id=mosque_id))
160
+
161
+ announcement = Announcement(
162
+ mosque_id=mosque_id,
163
+ title=title,
164
+ content=content,
165
+ start_date=start_date,
166
+ end_date=end_date,
167
+ start_time=start_time,
168
+ end_time=end_time,
169
+ is_urgent=is_urgent
170
+ )
171
+ db.session.add(announcement)
172
+ db.session.commit()
173
+ flash('Announcement added successfully.', 'success')
174
+ return redirect(url_for('admin.mosque_announcements', mosque_id=mosque_id))
175
+
176
+ announcements = Announcement.query.filter_by(mosque_id=mosque_id).all()
177
+ return render_template('mosque/announcements.html',
178
+ announcements=announcements,
179
+ user=current_user,
180
+ mosque=mosque,
181
+ now=datetime.now())
182
+
183
+ @admin.route('/mosque_announcements/delete/<int:id>', methods=['POST'])
184
+ @login_required
185
+ def delete_mosque_announcement(id):
186
+ if current_user.role != 'superadmin':
187
+ flash('Unauthorized access.', category='error')
188
+ return redirect(url_for('mosque.dashboard'))
189
+
190
+ announcement = Announcement.query.get_or_404(id)
191
+ mosque_id = announcement.mosque_id
192
+
193
+ try:
194
+ db.session.delete(announcement)
195
+ db.session.commit()
196
+ flash('Announcement deleted successfully!', category='success')
197
+ except Exception as e:
198
+ flash('Error deleting announcement.', category='error')
199
+
200
+ return redirect(url_for('admin.mosque_announcements', mosque_id=mosque_id))
201
+
202
+ @admin.route('/reset_password/<int:admin_id>', methods=['GET', 'POST'])
203
+ @login_required
204
+ def reset_admin_password(admin_id):
205
+ if current_user.role != 'superadmin':
206
+ flash('Unauthorized access.', category='error')
207
+ return redirect(url_for('mosque.dashboard'))
208
+
209
+ admin = User.query.get_or_404(admin_id)
210
+
211
+ if request.method == 'POST':
212
+ new_password = request.form.get('new_password')
213
+ confirm_password = request.form.get('confirm_password')
214
+
215
+ if not new_password or not confirm_password:
216
+ flash('All fields are required.', category='error')
217
+ elif new_password != confirm_password:
218
+ flash('Passwords don\'t match.', category='error')
219
+ elif len(new_password) < 8:
220
+ flash('Password must be at least 8 characters.', category='error')
221
+ else:
222
+ admin.password = generate_password_hash(new_password, method='scrypt')
223
+ db.session.commit()
224
+ flash('Password reset successfully!', category='success')
225
+ return redirect(url_for('admin.dashboard'))
226
+
227
+ return render_template("admin/reset_password.html", user=current_user, admin=admin)
228
+
229
+ @admin.route('/delete_admin/<int:admin_id>', methods=['POST'])
230
+ @login_required
231
+ def delete_admin(admin_id):
232
+ if current_user.role != 'superadmin':
233
+ flash('Unauthorized access.', category='error')
234
+ return redirect(url_for('mosque.dashboard'))
235
+
236
+ admin = User.query.get_or_404(admin_id)
237
+ if admin.role == 'superadmin':
238
+ flash('Cannot delete a superadmin account.', category='error')
239
+ return redirect(url_for('admin.dashboard'))
240
+
241
+ try:
242
+ db.session.delete(admin)
243
+ db.session.commit()
244
+ flash('Administrator account deleted successfully!', category='success')
245
+ except Exception as e:
246
+ flash('Error deleting administrator account.', category='error')
247
+
248
+ return redirect(url_for('admin.dashboard'))
249
+
250
+ @admin.route('/reassign_mosque/<int:admin_id>', methods=['GET', 'POST'])
251
+ @login_required
252
+ def reassign_mosque(admin_id):
253
+ if current_user.role != 'superadmin':
254
+ flash('Unauthorized access.', category='error')
255
+ return redirect(url_for('mosque.dashboard'))
256
+
257
+ admin = User.query.get_or_404(admin_id)
258
+ mosques = Mosque.query.all()
259
+
260
+ if request.method == 'POST':
261
+ mosque_id = request.form.get('mosque_id')
262
+ if mosque_id:
263
+ admin.mosque_id = mosque_id
264
+ db.session.commit()
265
+ flash('Administrator reassigned successfully!', category='success')
266
+ return redirect(url_for('admin.dashboard'))
267
+ else:
268
+ flash('Please select a mosque.', category='error')
269
+
270
+ return render_template("admin/reassign_mosque.html", user=current_user, admin=admin, mosques=mosques)
271
+
272
+ @admin.route('/delete_mosque/<int:mosque_id>', methods=['POST'])
273
+ @login_required
274
+ def delete_mosque(mosque_id):
275
+ if current_user.role != 'superadmin':
276
+ flash('Unauthorized access.', category='error')
277
+ return redirect(url_for('mosque.dashboard'))
278
+
279
+ mosque = Mosque.query.get_or_404(mosque_id)
280
+
281
+ try:
282
+ # Get all admins of this mosque
283
+ admins = User.query.filter_by(mosque_id=mosque_id).all()
284
+
285
+ # Remove mosque_id from all admins
286
+ for admin in admins:
287
+ admin.mosque_id = None
288
+
289
+ # Delete all prayer times
290
+ PrayerTime.query.filter_by(mosque_id=mosque_id).delete()
291
+
292
+ # Delete all announcements
293
+ Announcement.query.filter_by(mosque_id=mosque_id).delete()
294
+
295
+ # Finally delete the mosque
296
+ db.session.delete(mosque)
297
+ db.session.commit()
298
+ flash('Mosque and all related data deleted successfully!', category='success')
299
+ except Exception as e:
300
+ flash('Error deleting mosque.', category='error')
301
+
302
+ return redirect(url_for('admin.dashboard'))
303
+
304
+ @admin.route('/mosque_details/<int:id>')
305
+ @login_required
306
+ def mosque_details(id):
307
+ if current_user.role != 'superadmin':
308
+ flash('Unauthorized access.', category='error')
309
+ return redirect(url_for('mosque.dashboard'))
310
+
311
+ mosque = Mosque.query.get_or_404(id)
312
+ all_announcements = Announcement.query.filter_by(mosque_id=id).all()
313
+ active_announcements = [a for a in all_announcements if a.is_active]
314
+ prayer_times = PrayerTime.query.filter_by(
315
+ mosque_id=id,
316
+ date=datetime.now().date()
317
+ ).all()
318
+
319
+ # Get assigned admins
320
+
321
+ assigned_admins = User.query.filter_by(mosque_id=id, role='admin').all()
322
+ staff_members = User.query.filter_by(mosque_id=id, role='staff').all()
323
+
324
+ return render_template(
325
+ "admin/mosque_details.html",
326
+ user=current_user,
327
+ mosque=mosque,
328
+ assigned_admins=assigned_admins,
329
+ staff_members=staff_members,
330
+ prayer_times=prayer_times,
331
+ announcements=active_announcements,
332
+ now=datetime.now()
333
+ )
334
+
335
+ @admin.route('/add_staff', methods=['GET', 'POST'])
336
+ @login_required
337
+ def add_staff():
338
+ if current_user.role != 'superadmin':
339
+ flash('Unauthorized access.', category='error')
340
+ return redirect(url_for('mosque.dashboard'))
341
+
342
+ if request.method == 'POST':
343
+ email = request.form.get('email')
344
+ name = request.form.get('name')
345
+ password = request.form.get('password')
346
+ mosque_id = request.form.get('mosque_id')
347
+
348
+ user = User.query.filter_by(email=email).first()
349
+ if user:
350
+ flash('Email already exists.', category='error')
351
+ else:
352
+ try:
353
+ recovery_key = User.generate_recovery_key()
354
+ new_staff = User(
355
+ email=email,
356
+ name=name,
357
+ password=generate_password_hash(password, method='scrypt'),
358
+ role='staff',
359
+ mosque_id=mosque_id,
360
+ recovery_key=recovery_key
361
+ )
362
+ db.session.add(new_staff)
363
+ db.session.commit()
364
+ flash(f'{name} added successfully!', category='success')
365
+ return redirect(url_for('admin.dashboard'))
366
+ except Exception as e:
367
+ flash('Error adding staff.', category='error')
368
+
369
+ mosques = Mosque.query.all()
370
+ return render_template("admin/add_staff.html", user=current_user, mosques=mosques)
main/controllers/auth.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, flash, redirect, url_for
2
+ from werkzeug.security import generate_password_hash, check_password_hash
3
+ from flask_login import login_user, login_required, logout_user, current_user
4
+ from ..models.user import User
5
+ from .. import db
6
+
7
+ auth = Blueprint('auth', __name__)
8
+
9
+ @auth.route('/login', methods=['GET', 'POST'])
10
+ def login():
11
+ if request.method == 'POST':
12
+ email = request.form.get('email')
13
+ password = request.form.get('password')
14
+
15
+ user = User.query.filter_by(email=email).first()
16
+ if user and check_password_hash(user.password, password):
17
+ flash('Logged in successfully!', category='success')
18
+ login_user(user, remember=True)
19
+ return redirect(url_for('mosque.dashboard'))
20
+ else:
21
+ flash('Username or password is incorrect.', category='error')
22
+
23
+ return render_template("auth/login.html", user=current_user)
24
+
25
+ @auth.route('/register', methods=['GET', 'POST'])
26
+ def register():
27
+ if request.method == 'POST':
28
+ email = request.form.get('email')
29
+ name = request.form.get('name')
30
+ password = request.form.get('password')
31
+ confirm_password = request.form.get('confirm_password')
32
+
33
+ user = User.query.filter_by(email=email).first()
34
+
35
+ if not email or not name or not password or not confirm_password:
36
+ flash('All fields are required.', category='error')
37
+ elif user:
38
+ flash('User already exists.', category='error')
39
+ elif len(email) < 4:
40
+ flash('Email must be greater than 3 characters.', category='error')
41
+ elif len(name) < 2:
42
+ flash('Name must be greater than 1 character.', category='error')
43
+ elif password != confirm_password:
44
+ flash('Passwords don\'t match.', category='error')
45
+ elif len(password) < 8:
46
+ flash('Password must be at least 8 characters.', category='error')
47
+ else:
48
+ recovery_key = User.generate_recovery_key()
49
+ new_user = User(
50
+ email=email,
51
+ name=name,
52
+ password=generate_password_hash(password, method='scrypt'),
53
+ role='user',
54
+ recovery_key=recovery_key
55
+ )
56
+ db.session.add(new_user)
57
+ db.session.commit()
58
+ login_user(new_user, remember=True)
59
+ flash(f'{name} account created!', category='success')
60
+ return redirect(url_for('mosque.dashboard'))
61
+
62
+ return render_template("auth/register.html", user=current_user)
63
+
64
+ @auth.route('/logout')
65
+ @login_required
66
+ def logout():
67
+ logout_user()
68
+ return redirect(url_for('auth.login'))
69
+
70
+ @auth.route('/change_password', methods=['GET', 'POST'])
71
+ @login_required
72
+ def change_password():
73
+ if request.method == 'POST':
74
+ current_password = request.form.get('current_password')
75
+ new_password = request.form.get('new_password')
76
+ confirm_password = request.form.get('confirm_password')
77
+
78
+ if not current_password or not new_password or not confirm_password:
79
+ flash('All fields are required.', category='error')
80
+ elif not check_password_hash(current_user.password, current_password):
81
+ flash('Current password is incorrect.', category='error')
82
+ elif new_password != confirm_password:
83
+ flash('Passwords don\'t match.', category='error')
84
+ elif len(new_password) < 8:
85
+ flash('Password must be at least 8 characters.', category='error')
86
+ else:
87
+ current_user.password = generate_password_hash(new_password, method='scrypt')
88
+ db.session.commit()
89
+ flash('Password updated successfully!', category='success')
90
+ return redirect(url_for('mosque.dashboard'))
91
+
92
+ return render_template("auth/change_password.html", user=current_user)
93
+
94
+ @auth.route('/view_recovery_key')
95
+ @login_required
96
+ def view_recovery_key():
97
+ return render_template("auth/view_recovery_key.html", user=current_user)
98
+
99
+ @auth.route('/forgot_password', methods=['GET', 'POST'])
100
+ def forgot_password():
101
+ if request.method == 'POST':
102
+ email = request.form.get('email')
103
+ recovery_key = request.form.get('recovery_key')
104
+ new_password = request.form.get('new_password')
105
+ confirm_password = request.form.get('confirm_password')
106
+
107
+ user = User.query.filter_by(email=email).first()
108
+
109
+ if not email or not recovery_key or not new_password or not confirm_password:
110
+ flash('All fields are required.', category='error')
111
+ elif not user:
112
+ flash('Email not found.', category='error')
113
+ elif recovery_key != user.recovery_key:
114
+ flash('Invalid recovery key.', category='error')
115
+ elif new_password != confirm_password:
116
+ flash('Passwords don\'t match.', category='error')
117
+ elif len(new_password) < 8:
118
+ flash('Password must be at least 8 characters.', category='error')
119
+ else:
120
+ user.password = generate_password_hash(new_password, method='scrypt')
121
+ # Generate a new recovery key after password reset
122
+ user.recovery_key = User.generate_recovery_key()
123
+ db.session.commit()
124
+ flash('Password reset successfully! You can now login with your new password.', category='success')
125
+ return redirect(url_for('auth.login'))
126
+
127
+ return render_template("auth/forgot_password.html", user=current_user)
main/controllers/finance.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, flash, redirect, url_for, send_file
2
+ from flask_login import login_required, current_user
3
+ from ..models.finance import Finance
4
+ from ..models.mosque import Mosque
5
+ from .. import db
6
+ from datetime import datetime
7
+ import csv
8
+ import io
9
+ import tempfile
10
+
11
+ finance = Blueprint('finance', __name__)
12
+
13
+ @finance.route('/finance')
14
+ @login_required
15
+ def finance_list():
16
+ if current_user.role == 'superadmin':
17
+ mosque_id = request.args.get('mosque_id', type=int)
18
+ if not mosque_id:
19
+ flash('Please select a mosque.', category='error')
20
+ return redirect(url_for('admin.dashboard'))
21
+ else:
22
+ mosque_id = current_user.mosque_id
23
+
24
+ mosque = Mosque.query.get_or_404(mosque_id)
25
+ items = Finance.query.filter_by(mosque_id=mosque_id).order_by(Finance.date_added.desc()).all()
26
+
27
+ # Calculate totals
28
+ total_income = sum(item.amount for item in items if item.amount > 0)
29
+ total_expenses = abs(sum(item.amount for item in items if item.amount < 0))
30
+
31
+ return render_template('finance/list.html',
32
+ items=items,
33
+ mosque=mosque,
34
+ user=current_user,
35
+ total_income=total_income,
36
+ total_expenses=total_expenses)
37
+
38
+ @finance.route('/finance/add', methods=['GET', 'POST'])
39
+ @login_required
40
+ def add_item():
41
+ if current_user.role == 'superadmin':
42
+ mosque_id = request.args.get('mosque_id', type=int)
43
+ if not mosque_id:
44
+ flash('Please select a mosque.', category='error')
45
+ return redirect(url_for('admin.dashboard'))
46
+ else:
47
+ mosque_id = current_user.mosque_id
48
+
49
+ mosque = Mosque.query.get_or_404(mosque_id)
50
+
51
+ if request.method == 'POST':
52
+ number = request.form.get('number')
53
+ transaction_name = request.form.get('transaction_name')
54
+ finance_category = request.form.get('finance_category')
55
+ description = request.form.get('description')
56
+ amount = request.form.get('amount', type=float)
57
+ remarks = request.form.get('remarks')
58
+
59
+ if not number or not transaction_name or not finance_category or amount is None:
60
+ flash('Required fields cannot be empty.', category='error')
61
+ else:
62
+ try:
63
+ new_item = Finance(
64
+ mosque_id=mosque_id,
65
+ number=number,
66
+ transaction_name=transaction_name,
67
+ finance_category=finance_category,
68
+ description=description,
69
+ amount=amount,
70
+ remarks=remarks
71
+ )
72
+ db.session.add(new_item)
73
+ db.session.commit()
74
+ flash('Transaction added successfully!', category='success')
75
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
76
+ except Exception as e:
77
+ flash('Error adding transaction.', category='error')
78
+
79
+ return render_template('finance/add.html', mosque=mosque, user=current_user)
80
+
81
+ @finance.route('/finance/edit/<int:id>', methods=['GET', 'POST'])
82
+ @login_required
83
+ def edit_item(id):
84
+ item = Finance.query.get_or_404(id)
85
+
86
+ if current_user.role == 'superadmin':
87
+ mosque_id = item.mosque_id
88
+ else:
89
+ if item.mosque_id != current_user.mosque_id:
90
+ flash('Unauthorized access.', category='error')
91
+ return redirect(url_for('mosque.dashboard'))
92
+ mosque_id = current_user.mosque_id
93
+
94
+ mosque = Mosque.query.get_or_404(mosque_id)
95
+
96
+ if request.method == 'POST':
97
+ item.number = request.form.get('number')
98
+ item.transaction_name = request.form.get('transaction_name')
99
+ item.finance_category = request.form.get('finance_category')
100
+ item.description = request.form.get('description')
101
+ item.amount = request.form.get('amount', type=float)
102
+ item.remarks = request.form.get('remarks')
103
+
104
+ if not item.number or not item.transaction_name or not item.finance_category or item.amount is None:
105
+ flash('Required fields cannot be empty.', category='error')
106
+ else:
107
+ try:
108
+ db.session.commit()
109
+ flash('Transaction updated successfully!', category='success')
110
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
111
+ except Exception as e:
112
+ flash('Error updating transaction.', category='error')
113
+
114
+ return render_template('finance/edit.html', item=item, mosque=mosque, user=current_user)
115
+
116
+ @finance.route('/finance/delete/<int:id>', methods=['POST'])
117
+ @login_required
118
+ def delete_item(id):
119
+ item = Finance.query.get_or_404(id)
120
+
121
+ if current_user.role == 'superadmin':
122
+ mosque_id = item.mosque_id
123
+ else:
124
+ if item.mosque_id != current_user.mosque_id:
125
+ flash('Unauthorized access.', category='error')
126
+ return redirect(url_for('mosque.dashboard'))
127
+ mosque_id = current_user.mosque_id
128
+
129
+ try:
130
+ db.session.delete(item)
131
+ db.session.commit()
132
+ flash('Transaction deleted successfully!', category='success')
133
+ except Exception as e:
134
+ flash('Error deleting transaction.', category='error')
135
+
136
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
137
+
138
+ @finance.route('/finance/export/<int:mosque_id>')
139
+ @login_required
140
+ def export_data(mosque_id):
141
+ # Check authorization
142
+ if current_user.role != 'superadmin' and current_user.mosque_id != mosque_id:
143
+ flash('Unauthorized access.', category='error')
144
+ return redirect(url_for('mosque.dashboard'))
145
+
146
+ mosque = Mosque.query.get_or_404(mosque_id)
147
+ items = Finance.query.filter_by(mosque_id=mosque_id).order_by(Finance.date_added.desc()).all()
148
+
149
+ # Create a temporary file
150
+ temp = tempfile.NamedTemporaryFile(delete=False, mode='w', newline='', suffix='.csv')
151
+
152
+ try:
153
+ # Write CSV data
154
+ fieldnames = ['number', 'transaction_name', 'finance_category', 'description', 'amount', 'remarks']
155
+ writer = csv.DictWriter(temp, fieldnames=fieldnames)
156
+ writer.writeheader()
157
+
158
+ for item in items:
159
+ writer.writerow({
160
+ 'number': item.number,
161
+ 'transaction_name': item.transaction_name,
162
+ 'finance_category': item.finance_category,
163
+ 'description': item.description,
164
+ 'amount': item.amount,
165
+ 'remarks': item.remarks
166
+ })
167
+
168
+ temp.close()
169
+
170
+ # Send the file
171
+ return send_file(
172
+ temp.name,
173
+ mimetype='text/csv',
174
+ as_attachment=True,
175
+ download_name=f'finance_data_{mosque.name}_{datetime.now().strftime("%Y%m%d")}.csv'
176
+ )
177
+
178
+ except Exception as e:
179
+ flash('Error exporting data.', category='error')
180
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
181
+
182
+ @finance.route('/finance/import/<int:mosque_id>', methods=['POST'])
183
+ @login_required
184
+ def import_data(mosque_id):
185
+ # Check authorization
186
+ if current_user.role != 'superadmin' and current_user.mosque_id != mosque_id:
187
+ flash('Unauthorized access.', category='error')
188
+ return redirect(url_for('mosque.dashboard'))
189
+
190
+ mosque = Mosque.query.get_or_404(mosque_id)
191
+
192
+ if 'file' not in request.files:
193
+ flash('No file uploaded.', category='error')
194
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
195
+
196
+ file = request.files['file']
197
+
198
+ if file.filename == '':
199
+ flash('No file selected.', category='error')
200
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
201
+
202
+ if not file.filename.endswith('.csv'):
203
+ flash('Please upload a CSV file.', category='error')
204
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
205
+
206
+ try:
207
+ # Read CSV file
208
+ stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None)
209
+ csv_reader = csv.DictReader(stream)
210
+
211
+ # Validate headers
212
+ required_fields = ['number', 'transaction_name', 'finance_category', 'description', 'amount']
213
+ if not all(field in csv_reader.fieldnames for field in required_fields):
214
+ flash('Invalid CSV format. Please check the required columns.', category='error')
215
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
216
+
217
+ # Process each row
218
+ for row in csv_reader:
219
+ try:
220
+ amount = float(row['amount'])
221
+ new_item = Finance(
222
+ mosque_id=mosque_id,
223
+ number=row['number'],
224
+ transaction_name=row['transaction_name'],
225
+ finance_category=row['finance_category'],
226
+ description=row['description'],
227
+ amount=amount,
228
+ remarks=row.get('remarks', '')
229
+ )
230
+ db.session.add(new_item)
231
+ except ValueError:
232
+ flash('Invalid amount format in CSV.', category='error')
233
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
234
+
235
+ db.session.commit()
236
+ flash('Data imported successfully!', category='success')
237
+
238
+ except Exception as e:
239
+ db.session.rollback()
240
+ flash('Error importing data.', category='error')
241
+
242
+ return redirect(url_for('finance.finance_list', mosque_id=mosque_id))
main/controllers/inventory.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, flash, redirect, url_for, send_file
2
+ from flask_login import login_required, current_user
3
+ from ..models.inventory import Inventory
4
+ from ..models.mosque import Mosque
5
+ from .. import db
6
+ from datetime import datetime
7
+ import csv
8
+ import io
9
+ import tempfile
10
+
11
+ inventory = Blueprint('inventory', __name__)
12
+
13
+ @inventory.route('/inventory')
14
+ @login_required
15
+ def inventory_list():
16
+ if current_user.role == 'superadmin':
17
+ mosque_id = request.args.get('mosque_id', type=int)
18
+ if not mosque_id:
19
+ flash('Please select a mosque.', category='error')
20
+ return redirect(url_for('admin.dashboard'))
21
+ else:
22
+ mosque_id = current_user.mosque_id
23
+
24
+ mosque = Mosque.query.get_or_404(mosque_id)
25
+ items = Inventory.query.filter_by(mosque_id=mosque_id).order_by(Inventory.date_added.desc()).all()
26
+
27
+ return render_template('inventory/list.html',
28
+ items=items,
29
+ mosque=mosque,
30
+ user=current_user)
31
+
32
+ @inventory.route('/inventory/add', methods=['GET', 'POST'])
33
+ @login_required
34
+ def add_item():
35
+ if current_user.role == 'superadmin':
36
+ mosque_id = request.args.get('mosque_id', type=int)
37
+ if not mosque_id:
38
+ flash('Please select a mosque.', category='error')
39
+ return redirect(url_for('admin.dashboard'))
40
+ else:
41
+ mosque_id = current_user.mosque_id
42
+
43
+ mosque = Mosque.query.get_or_404(mosque_id)
44
+
45
+ if request.method == 'POST':
46
+ number = request.form.get('number')
47
+ item_name = request.form.get('item_name')
48
+ item_category = request.form.get('item_category')
49
+ item_description = request.form.get('item_description')
50
+ quantity = request.form.get('quantity', type=int)
51
+ remarks = request.form.get('remarks')
52
+
53
+ if not number or not item_name or not item_category or quantity is None:
54
+ flash('Required fields cannot be empty.', category='error')
55
+ else:
56
+ try:
57
+ new_item = Inventory(
58
+ mosque_id=mosque_id,
59
+ number=number,
60
+ item_name=item_name,
61
+ item_category=item_category,
62
+ item_description=item_description,
63
+ quantity=quantity,
64
+ remarks=remarks
65
+ )
66
+ db.session.add(new_item)
67
+ db.session.commit()
68
+ flash('Item added successfully!', category='success')
69
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
70
+ except Exception as e:
71
+ flash('Error adding item.', category='error')
72
+
73
+ return render_template('inventory/add.html', mosque=mosque, user=current_user)
74
+
75
+ @inventory.route('/inventory/edit/<int:id>', methods=['GET', 'POST'])
76
+ @login_required
77
+ def edit_item(id):
78
+ item = Inventory.query.get_or_404(id)
79
+
80
+ if current_user.role == 'superadmin':
81
+ mosque_id = item.mosque_id
82
+ else:
83
+ if item.mosque_id != current_user.mosque_id:
84
+ flash('Unauthorized access.', category='error')
85
+ return redirect(url_for('mosque.dashboard'))
86
+ mosque_id = current_user.mosque_id
87
+
88
+ mosque = Mosque.query.get_or_404(mosque_id)
89
+
90
+ if request.method == 'POST':
91
+ item.number = request.form.get('number')
92
+ item.item_name = request.form.get('item_name')
93
+ item.item_category = request.form.get('item_category')
94
+ item.item_description = request.form.get('item_description')
95
+ item.quantity = request.form.get('quantity', type=int)
96
+ item.remarks = request.form.get('remarks')
97
+
98
+ if not item.number or not item.item_name or not item.item_category or item.quantity is None:
99
+ flash('Required fields cannot be empty.', category='error')
100
+ else:
101
+ try:
102
+ db.session.commit()
103
+ flash('Item updated successfully!', category='success')
104
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
105
+ except Exception as e:
106
+ flash('Error updating item.', category='error')
107
+
108
+ return render_template('inventory/edit.html', item=item, mosque=mosque, user=current_user)
109
+
110
+ @inventory.route('/inventory/delete/<int:id>', methods=['POST'])
111
+ @login_required
112
+ def delete_item(id):
113
+ item = Inventory.query.get_or_404(id)
114
+
115
+ if current_user.role == 'superadmin':
116
+ mosque_id = item.mosque_id
117
+ else:
118
+ if item.mosque_id != current_user.mosque_id:
119
+ flash('Unauthorized access.', category='error')
120
+ return redirect(url_for('mosque.dashboard'))
121
+ mosque_id = current_user.mosque_id
122
+
123
+ try:
124
+ db.session.delete(item)
125
+ db.session.commit()
126
+ flash('Item deleted successfully!', category='success')
127
+ except Exception as e:
128
+ flash('Error deleting item.', category='error')
129
+
130
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
131
+
132
+ @inventory.route('/inventory/export/<int:mosque_id>')
133
+ @login_required
134
+ def export_data(mosque_id):
135
+ # Check authorization
136
+ if current_user.role != 'superadmin' and current_user.mosque_id != mosque_id:
137
+ flash('Unauthorized access.', category='error')
138
+ return redirect(url_for('mosque.dashboard'))
139
+
140
+ mosque = Mosque.query.get_or_404(mosque_id)
141
+ items = Inventory.query.filter_by(mosque_id=mosque_id).order_by(Inventory.date_added.desc()).all()
142
+
143
+ # Create a temporary file
144
+ temp = tempfile.NamedTemporaryFile(delete=False, mode='w', newline='', suffix='.csv')
145
+
146
+ try:
147
+ # Write CSV data
148
+ fieldnames = ['number', 'item_name', 'item_category', 'item_description', 'quantity', 'remarks']
149
+ writer = csv.DictWriter(temp, fieldnames=fieldnames)
150
+ writer.writeheader()
151
+
152
+ for item in items:
153
+ writer.writerow({
154
+ 'number': item.number,
155
+ 'item_name': item.item_name,
156
+ 'item_category': item.item_category,
157
+ 'item_description': item.item_description,
158
+ 'quantity': item.quantity,
159
+ 'remarks': item.remarks
160
+ })
161
+
162
+ temp.close()
163
+
164
+ # Send the file
165
+ return send_file(
166
+ temp.name,
167
+ mimetype='text/csv',
168
+ as_attachment=True,
169
+ download_name=f'inventory_data_{mosque.name}_{datetime.now().strftime("%Y%m%d")}.csv'
170
+ )
171
+
172
+ except Exception as e:
173
+ flash('Error exporting data.', category='error')
174
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
175
+
176
+ @inventory.route('/inventory/import/<int:mosque_id>', methods=['POST'])
177
+ @login_required
178
+ def import_data(mosque_id):
179
+ # Check authorization
180
+ if current_user.role != 'superadmin' and current_user.mosque_id != mosque_id:
181
+ flash('Unauthorized access.', category='error')
182
+ return redirect(url_for('mosque.dashboard'))
183
+
184
+ mosque = Mosque.query.get_or_404(mosque_id)
185
+
186
+ if 'file' not in request.files:
187
+ flash('No file uploaded.', category='error')
188
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
189
+
190
+ file = request.files['file']
191
+
192
+ if file.filename == '':
193
+ flash('No file selected.', category='error')
194
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
195
+
196
+ if not file.filename.endswith('.csv'):
197
+ flash('Please upload a CSV file.', category='error')
198
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
199
+
200
+ try:
201
+ # Read CSV file
202
+ stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None)
203
+ csv_reader = csv.DictReader(stream)
204
+
205
+ # Validate headers
206
+ required_fields = ['number', 'item_name', 'item_category', 'item_description', 'quantity']
207
+ if not all(field in csv_reader.fieldnames for field in required_fields):
208
+ flash('Invalid CSV format. Please check the required columns.', category='error')
209
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
210
+
211
+ # Process each row
212
+ for row in csv_reader:
213
+ try:
214
+ quantity = int(row['quantity'])
215
+ new_item = Inventory(
216
+ mosque_id=mosque_id,
217
+ number=row['number'],
218
+ item_name=row['item_name'],
219
+ item_category=row['item_category'],
220
+ item_description=row['item_description'],
221
+ quantity=quantity,
222
+ remarks=row.get('remarks', '')
223
+ )
224
+ db.session.add(new_item)
225
+ except ValueError:
226
+ flash('Invalid quantity format in CSV.', category='error')
227
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
228
+
229
+ db.session.commit()
230
+ flash('Data imported successfully!', category='success')
231
+
232
+ except Exception as e:
233
+ db.session.rollback()
234
+ flash('Error importing data.', category='error')
235
+
236
+ return redirect(url_for('inventory.inventory_list', mosque_id=mosque_id))
main/controllers/mosque.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
2
+ from flask_login import login_required, current_user
3
+ from ..models.mosque import Mosque
4
+ from ..models.prayer_time import PrayerTime
5
+ from ..models.announcement import Announcement
6
+ from ..models.user import User
7
+ from .. import db
8
+ from datetime import datetime, date
9
+ from werkzeug.security import generate_password_hash
10
+
11
+ mosque = Blueprint('mosque', __name__)
12
+
13
+ @mosque.route('/')
14
+ def root():
15
+ return redirect(url_for('mosque.home'))
16
+
17
+ @mosque.route('/home')
18
+ def home():
19
+ mosques = Mosque.query.all()
20
+ return render_template("home.html", user=current_user, mosques=mosques)
21
+
22
+ @mosque.route('/mosque_info/<int:id>')
23
+ def mosque_info(id):
24
+ mosque = Mosque.query.get_or_404(id)
25
+ prayer_times = PrayerTime.query.filter_by(
26
+ mosque_id=id,
27
+ date=date.today()
28
+ ).all()
29
+
30
+ # Get active announcements
31
+ all_announcements = Announcement.query.filter_by(mosque_id=id).all()
32
+ active_announcements = [a for a in all_announcements if a.is_active]
33
+
34
+ return render_template(
35
+ "mosque/mosque_info.html",
36
+ user=current_user,
37
+ mosque=mosque,
38
+ prayer_times=prayer_times,
39
+ active_announcements=active_announcements,
40
+ now=datetime.now()
41
+ )
42
+
43
+ @mosque.route('/register_mosque', methods=['GET', 'POST'])
44
+ @login_required
45
+ def register_mosque():
46
+ # Prevent users who already have a mosque from registering another one
47
+ if current_user.mosque_id:
48
+ flash('You are already an administrator of a mosque.', category='error')
49
+ return redirect(url_for('mosque.dashboard'))
50
+
51
+ # Prevent superadmins from registering mosques (they should use add_mosque)
52
+ if current_user.role == 'superadmin':
53
+ flash('Superadmins should use the admin dashboard to add mosques.', category='error')
54
+ return redirect(url_for('admin.dashboard'))
55
+
56
+ if request.method == 'POST':
57
+ name = request.form.get('name')
58
+ address = request.form.get('address')
59
+ city = request.form.get('city')
60
+ state = request.form.get('state')
61
+ country = request.form.get('country')
62
+ phone = request.form.get('phone')
63
+ email = request.form.get('email')
64
+ capacity = request.form.get('capacity')
65
+ jakim_code = request.form.get('jakim_code')
66
+
67
+ try:
68
+ mosque = Mosque(
69
+ name=name,
70
+ address=address,
71
+ city=city,
72
+ state=state,
73
+ country=country,
74
+ phone=phone,
75
+ email=email,
76
+ capacity=capacity,
77
+ jakim_code=jakim_code
78
+ )
79
+ db.session.add(mosque)
80
+ db.session.commit()
81
+
82
+ # Update the current user to be an admin of this mosque
83
+ current_user.role = 'admin'
84
+ current_user.mosque_id = mosque.id
85
+ db.session.commit()
86
+
87
+ flash('Mosque registered successfully! You are now the administrator.', category='success')
88
+ return redirect(url_for('mosque.dashboard'))
89
+ except Exception as e:
90
+ flash('Error registering mosque.', category='error')
91
+
92
+ return render_template("mosque/register_mosque.html", user=current_user)
93
+
94
+ @mosque.route('/dashboard')
95
+ @login_required
96
+ def dashboard():
97
+ # Redirect superadmins to admin dashboard
98
+ if current_user.role == 'superadmin':
99
+ return redirect(url_for('admin.dashboard'))
100
+
101
+ if current_user.mosque_id:
102
+ mosque = Mosque.query.get(current_user.mosque_id)
103
+ prayer_times = PrayerTime.query.filter_by(
104
+ mosque_id=current_user.mosque_id,
105
+ date=date.today()
106
+ ).all()
107
+
108
+ # Get all announcements and filter active ones using the property
109
+ all_announcements = Announcement.query.filter_by(mosque_id=current_user.mosque_id).all()
110
+ active_announcements = [a for a in all_announcements if a.is_active]
111
+
112
+ # Get assigned admins for this mosque
113
+ assigned_admins = User.query.filter_by(mosque_id=current_user.mosque_id, role='admin').all()
114
+
115
+ # Get staff members for this mosque
116
+ staff_members = User.query.filter_by(mosque_id=current_user.mosque_id, role='staff').all()
117
+
118
+ return render_template(
119
+ "mosque/dashboard.html",
120
+ user=current_user,
121
+ mosque=mosque,
122
+ prayer_times=prayer_times,
123
+ active_announcements=active_announcements,
124
+ assigned_admins=assigned_admins,
125
+ staff_members=staff_members,
126
+ now=datetime.now()
127
+ )
128
+ return render_template("mosque/dashboard.html", user=current_user)
129
+
130
+ @mosque.route('/prayer_times/<int:mosque_id>', methods=['GET', 'POST'])
131
+ @login_required
132
+ def prayer_times(mosque_id):
133
+ if current_user.mosque_id != mosque_id and current_user.role != 'superadmin':
134
+ flash('Unauthorized access.', category='error')
135
+ return redirect(url_for('mosque.dashboard'))
136
+
137
+ mosque = Mosque.query.get_or_404(mosque_id)
138
+
139
+ if request.method == 'POST':
140
+ prayer_name = request.form.get('prayer_name')
141
+ time = request.form.get('time')
142
+ date_str = request.form.get('date')
143
+
144
+ try:
145
+ prayer_time = PrayerTime(
146
+ prayer_name=prayer_name,
147
+ time=datetime.strptime(time, '%H:%M').time(),
148
+ date=datetime.strptime(date_str, '%Y-%m-%d').date(),
149
+ mosque_id=mosque_id
150
+ )
151
+ db.session.add(prayer_time)
152
+ db.session.commit()
153
+ flash('Prayer time added successfully!', category='success')
154
+ except Exception as e:
155
+ flash('Error adding prayer time.', category='error')
156
+
157
+ prayer_times = PrayerTime.query.filter_by(mosque_id=mosque_id).all()
158
+ return render_template(
159
+ "mosque/prayer_times.html",
160
+ user=current_user,
161
+ prayer_times=prayer_times,
162
+ mosque=mosque
163
+ )
164
+
165
+ @mosque.route('/prayer_times/delete/<int:id>', methods=['POST'])
166
+ @login_required
167
+ def delete_prayer_time(id):
168
+ prayer_time = PrayerTime.query.get_or_404(id)
169
+
170
+ # Check if user has permission to delete this prayer time
171
+ if current_user.mosque_id != prayer_time.mosque_id and current_user.role != 'superadmin':
172
+ flash('Unauthorized access.', category='error')
173
+ return redirect(url_for('mosque.dashboard'))
174
+
175
+ try:
176
+ mosque_id = prayer_time.mosque_id # Store mosque_id before deletion
177
+ db.session.delete(prayer_time)
178
+ db.session.commit()
179
+ flash('Prayer time deleted successfully!', category='success')
180
+ except Exception as e:
181
+ flash('Error deleting prayer time.', category='error')
182
+
183
+ return redirect(url_for('mosque.prayer_times', mosque_id=mosque_id))
184
+
185
+ @mosque.route('/prayer_times/<int:mosque_id>/import', methods=['POST'])
186
+ @login_required
187
+ def import_prayer_times(mosque_id):
188
+ if current_user.mosque_id != mosque_id and current_user.role != 'superadmin':
189
+ return jsonify({'success': False, 'message': 'Unauthorized access.'}), 403
190
+
191
+ mosque = Mosque.query.get_or_404(mosque_id)
192
+
193
+ try:
194
+ data = request.get_json()
195
+ prayer_times = data.get('prayer_times', [])
196
+
197
+ # Begin transaction
198
+ duplicates = 0
199
+ added = 0
200
+
201
+ for prayer_data in prayer_times:
202
+ # Check if prayer time already exists
203
+ existing = PrayerTime.query.filter_by(
204
+ mosque_id=mosque_id,
205
+ prayer_name=prayer_data['prayer_name'],
206
+ date=datetime.strptime(prayer_data['date'], '%Y-%m-%d').date()
207
+ ).first()
208
+
209
+ if not existing:
210
+ prayer_time = PrayerTime(
211
+ prayer_name=prayer_data['prayer_name'],
212
+ time=datetime.strptime(prayer_data['time'], '%H:%M').time(),
213
+ date=datetime.strptime(prayer_data['date'], '%Y-%m-%d').date(),
214
+ mosque_id=mosque_id
215
+ )
216
+ db.session.add(prayer_time)
217
+ added += 1
218
+ else:
219
+ duplicates += 1
220
+
221
+ # Always commit if there are any additions
222
+ if added > 0:
223
+ db.session.commit()
224
+
225
+ # Return appropriate response
226
+ total = added + duplicates
227
+ if added == 0 and duplicates > 0:
228
+ return jsonify({
229
+ 'success': True,
230
+ 'status': 'no_changes',
231
+ 'message': f'All {duplicates} prayer times already exist for these dates.',
232
+ 'added': added,
233
+ 'duplicates': duplicates,
234
+ 'total': total
235
+ })
236
+ else:
237
+ return jsonify({
238
+ 'success': True,
239
+ 'status': 'imported',
240
+ 'message': f'Successfully imported {added} prayer times. {duplicates} duplicates were skipped.',
241
+ 'added': added,
242
+ 'duplicates': duplicates,
243
+ 'total': total
244
+ })
245
+
246
+ except Exception as e:
247
+ db.session.rollback()
248
+ print('Error importing prayer times:', str(e))
249
+ return jsonify({
250
+ 'success': False,
251
+ 'status': 'error',
252
+ 'message': 'Failed to import prayer times. Please try again.'
253
+ }), 500
254
+
255
+ @mosque.route('/mosque_announcements', methods=['GET', 'POST'])
256
+ @login_required
257
+ def announcements():
258
+ # If user is not a superadmin and has no mosque, redirect to register
259
+ if not current_user.role == 'superadmin' and not current_user.mosque_id:
260
+ flash('Please register a mosque first.', 'warning')
261
+ return redirect(url_for('mosque.register_mosque'))
262
+
263
+ # For superadmin, get mosque_id from query parameter
264
+ if current_user.role == 'superadmin':
265
+ mosque_id = request.args.get('mosque_id', type=int)
266
+ if not mosque_id:
267
+ flash('Please specify a mosque.', 'warning')
268
+ return redirect(url_for('admin.dashboard'))
269
+ else:
270
+ mosque_id = current_user.mosque_id
271
+
272
+ if request.method == 'POST':
273
+ title = request.form.get('title')
274
+ content = request.form.get('content')
275
+ start_date = datetime.strptime(request.form.get('start_date'), '%Y-%m-%d').date()
276
+ end_date = datetime.strptime(request.form.get('end_date'), '%Y-%m-%d').date()
277
+ start_time = datetime.strptime(request.form.get('start_time'), '%H:%M').time()
278
+ end_time = datetime.strptime(request.form.get('end_time'), '%H:%M').time()
279
+ is_urgent = request.form.get('is_urgent') == 'on'
280
+
281
+ if start_date > end_date or (start_date == end_date and start_time >= end_time):
282
+ flash('End date/time must be after start date/time.', 'error')
283
+ return redirect(url_for('mosque.announcements', mosque_id=mosque_id))
284
+
285
+ announcement = Announcement(
286
+ mosque_id=mosque_id,
287
+ title=title,
288
+ content=content,
289
+ start_date=start_date,
290
+ end_date=end_date,
291
+ start_time=start_time,
292
+ end_time=end_time,
293
+ is_urgent=is_urgent
294
+ )
295
+ db.session.add(announcement)
296
+ db.session.commit()
297
+ flash('Announcement added successfully.', 'success')
298
+ if current_user.role == 'superadmin':
299
+ return redirect(url_for('admin.mosque_announcements', mosque_id=mosque_id))
300
+ return redirect(url_for('mosque.announcements'))
301
+
302
+ announcements = Announcement.query.filter_by(mosque_id=mosque_id).all()
303
+ mosque = Mosque.query.get_or_404(mosque_id)
304
+ return render_template('mosque/announcements.html',
305
+ announcements=announcements,
306
+ user=current_user,
307
+ mosque=mosque,
308
+ now=datetime.now())
309
+
310
+ @mosque.route('/mosque_announcements/delete/<int:id>', methods=['POST'])
311
+ @login_required
312
+ def delete_announcement(id):
313
+ announcement = Announcement.query.get_or_404(id)
314
+
315
+ # Check if user has permission to delete this announcement
316
+ if current_user.mosque_id != announcement.mosque_id and current_user.role != 'superadmin':
317
+ flash('Unauthorized access.', category='error')
318
+ return redirect(url_for('mosque.dashboard'))
319
+
320
+ try:
321
+ mosque_id = announcement.mosque_id # Store mosque_id before deletion
322
+ db.session.delete(announcement)
323
+ db.session.commit()
324
+ flash('Announcement deleted successfully!', category='success')
325
+ except Exception as e:
326
+ flash('Error deleting announcement.', category='error')
327
+
328
+ return redirect(url_for('mosque.announcements', mosque_id=mosque_id))
329
+
330
+ @mosque.route('/mosque_announcements/edit/<int:id>', methods=['GET', 'POST'])
331
+ @login_required
332
+ def edit_announcement(id):
333
+ announcement = Announcement.query.get_or_404(id)
334
+
335
+ # Check if user has permission to edit this announcement
336
+ if current_user.mosque_id != announcement.mosque_id and current_user.role != 'superadmin':
337
+ flash('Unauthorized access.', category='error')
338
+ return redirect(url_for('mosque.dashboard'))
339
+
340
+ if request.method == 'POST':
341
+ title = request.form.get('title')
342
+ content = request.form.get('content')
343
+ start_date = datetime.strptime(request.form.get('start_date'), '%Y-%m-%d').date()
344
+ end_date = datetime.strptime(request.form.get('end_date'), '%Y-%m-%d').date()
345
+ start_time = datetime.strptime(request.form.get('start_time'), '%H:%M').time()
346
+ end_time = datetime.strptime(request.form.get('end_time'), '%H:%M').time()
347
+ is_urgent = request.form.get('is_urgent') == 'on'
348
+
349
+ if start_date > end_date or (start_date == end_date and start_time >= end_time):
350
+ flash('End date/time must be after start date/time.', 'error')
351
+ return redirect(url_for('mosque.edit_announcement', id=id))
352
+
353
+ try:
354
+ announcement.title = title
355
+ announcement.content = content
356
+ announcement.start_date = start_date
357
+ announcement.end_date = end_date
358
+ announcement.start_time = start_time
359
+ announcement.end_time = end_time
360
+ announcement.is_urgent = is_urgent
361
+ db.session.commit()
362
+ flash('Announcement updated successfully.', 'success')
363
+ if current_user.role == 'superadmin':
364
+ return redirect(url_for('admin.mosque_announcements', mosque_id=announcement.mosque_id))
365
+ return redirect(url_for('mosque.announcements'))
366
+ except Exception as e:
367
+ flash('Error updating announcement.', 'error')
368
+ return redirect(url_for('mosque.edit_announcement', id=id))
369
+
370
+ return render_template('mosque/edit_announcement.html',
371
+ user=current_user,
372
+ announcement=announcement,
373
+ mosque=Mosque.query.get(announcement.mosque_id),
374
+ now=datetime.now())
375
+
376
+ @mosque.route('/add_another_admin', methods=['GET', 'POST'])
377
+ @login_required
378
+ def add_another_admin():
379
+ # Only allow mosque admins to access this route
380
+ if current_user.role != 'admin':
381
+ flash('Unauthorized access.', category='error')
382
+ return redirect(url_for('mosque.dashboard'))
383
+
384
+ mosque = Mosque.query.get_or_404(current_user.mosque_id)
385
+
386
+ if request.method == 'POST':
387
+ email = request.form.get('email')
388
+ name = request.form.get('name')
389
+ password = request.form.get('password')
390
+
391
+ user = User.query.filter_by(email=email).first()
392
+ if user:
393
+ flash('Email already exists.', category='error')
394
+ else:
395
+ try:
396
+ recovery_key = User.generate_recovery_key()
397
+ new_admin = User(
398
+ email=email,
399
+ name=name,
400
+ password=generate_password_hash(password, method='scrypt'),
401
+ role='admin',
402
+ mosque_id=current_user.mosque_id,
403
+ recovery_key=recovery_key
404
+ )
405
+ db.session.add(new_admin)
406
+ db.session.commit()
407
+ flash(f'{name} added successfully!', category='success')
408
+ return redirect(url_for('mosque.dashboard'))
409
+ except Exception as e:
410
+ flash('Error adding admin.', category='error')
411
+
412
+ return render_template("mosque/add_another_admin.html", user=current_user, mosque=mosque)
413
+
414
+ @mosque.route('/add_another_staff', methods=['GET', 'POST'])
415
+ @login_required
416
+ def add_another_staff():
417
+ # Only allow mosque admins to access this route
418
+ if current_user.role != 'admin':
419
+ flash('Unauthorized access.', category='error')
420
+ return redirect(url_for('mosque.dashboard'))
421
+
422
+ mosque = Mosque.query.get_or_404(current_user.mosque_id)
423
+
424
+ if request.method == 'POST':
425
+ email = request.form.get('email')
426
+ name = request.form.get('name')
427
+ password = request.form.get('password')
428
+
429
+ user = User.query.filter_by(email=email).first()
430
+ if user:
431
+ flash('Email already exists.', category='error')
432
+ else:
433
+ try:
434
+ recovery_key = User.generate_recovery_key()
435
+ new_staff = User(
436
+ email=email,
437
+ name=name,
438
+ password=generate_password_hash(password, method='scrypt'),
439
+ role='staff',
440
+ mosque_id=current_user.mosque_id,
441
+ recovery_key=recovery_key
442
+ )
443
+ db.session.add(new_staff)
444
+ db.session.commit()
445
+ flash(f'{name} added successfully!', category='success')
446
+ return redirect(url_for('mosque.dashboard'))
447
+ except Exception as e:
448
+ flash('Error adding staff.', category='error')
449
+
450
+ return render_template("mosque/add_another_staff.html", user=current_user, mosque=mosque)
main/models/announcement.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .. import db
2
+ from datetime import datetime
3
+
4
+ class Announcement(db.Model):
5
+ id = db.Column(db.Integer, primary_key=True)
6
+ title = db.Column(db.String(200), nullable=False)
7
+ content = db.Column(db.Text, nullable=False)
8
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
9
+ start_date = db.Column(db.Date, nullable=False)
10
+ end_date = db.Column(db.Date, nullable=False)
11
+ start_time = db.Column(db.Time, nullable=False)
12
+ end_time = db.Column(db.Time, nullable=False)
13
+ mosque_id = db.Column(db.Integer, db.ForeignKey('mosque.id'), nullable=False)
14
+ is_urgent = db.Column(db.Boolean, nullable=False, default=False)
15
+
16
+ def __repr__(self):
17
+ return f'<Announcement {self.title}>'
18
+
19
+ def __init__(self, id=None, mosque_id=None, title=None, content=None, start_date=None, end_date=None, start_time=None, end_time=None, is_urgent=False):
20
+ self.id = id
21
+ self.mosque_id = mosque_id
22
+ self.title = title
23
+ self.content = content
24
+ self.start_date = start_date
25
+ self.end_date = end_date
26
+ self.start_time = start_time
27
+ self.end_time = end_time
28
+ self.is_urgent = is_urgent
29
+
30
+ @staticmethod
31
+ def create_table():
32
+ db.execute('''
33
+ CREATE TABLE IF NOT EXISTS announcements (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ mosque_id INTEGER NOT NULL,
36
+ title TEXT NOT NULL,
37
+ content TEXT NOT NULL,
38
+ start_date DATE NOT NULL,
39
+ end_date DATE NOT NULL,
40
+ start_time TIME NOT NULL,
41
+ end_time TIME NOT NULL,
42
+ is_urgent BOOLEAN NOT NULL DEFAULT 0,
43
+ FOREIGN KEY (mosque_id) REFERENCES mosques (id)
44
+ )
45
+ ''')
46
+ db.commit()
47
+
48
+ @staticmethod
49
+ def get_all(mosque_id):
50
+ announcements = db.execute(
51
+ 'SELECT * FROM announcements WHERE mosque_id = ? ORDER BY start_date DESC, start_time DESC',
52
+ (mosque_id,)
53
+ ).fetchall()
54
+ return [Announcement(
55
+ id=row[0],
56
+ mosque_id=row[1],
57
+ title=row[2],
58
+ content=row[3],
59
+ start_date=datetime.strptime(row[4], '%Y-%m-%d').date(),
60
+ end_date=datetime.strptime(row[5], '%Y-%m-%d').date(),
61
+ start_time=datetime.strptime(row[6], '%H:%M').time(),
62
+ end_time=datetime.strptime(row[7], '%H:%M').time(),
63
+ is_urgent=bool(row[8])
64
+ ) for row in announcements]
65
+
66
+ def save(self):
67
+ if self.id is None:
68
+ db.execute(
69
+ '''INSERT INTO announcements (mosque_id, title, content, start_date, end_date, start_time, end_time, is_urgent)
70
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
71
+ (self.mosque_id, self.title, self.content, self.start_date, self.end_date, self.start_time, self.end_time, self.is_urgent)
72
+ )
73
+ else:
74
+ db.execute(
75
+ '''UPDATE announcements
76
+ SET title = ?, content = ?, start_date = ?, end_date = ?, start_time = ?, end_time = ?, is_urgent = ?
77
+ WHERE id = ? AND mosque_id = ?''',
78
+ (self.title, self.content, self.start_date, self.end_date, self.start_time, self.end_time, self.is_urgent, self.id, self.mosque_id)
79
+ )
80
+ db.commit()
81
+
82
+ def delete(self):
83
+ if self.id:
84
+ db.execute('DELETE FROM announcements WHERE id = ? AND mosque_id = ?', (self.id, self.mosque_id))
85
+ db.commit()
86
+
87
+ @property
88
+ def is_active(self):
89
+ now = datetime.now()
90
+ current_date = now.date()
91
+ current_time = now.time()
92
+
93
+ if current_date < self.start_date:
94
+ return False
95
+ if current_date > self.end_date:
96
+ return False
97
+ if current_date == self.start_date and current_time < self.start_time:
98
+ return False
99
+ if current_date == self.end_date and current_time > self.end_time:
100
+ return False
101
+ return True
main/models/demo.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta, time
2
+ from werkzeug.security import generate_password_hash
3
+ from .user import User
4
+ from .mosque import Mosque
5
+ from .prayer_time import PrayerTime
6
+ from .inventory import Inventory
7
+ from .finance import Finance
8
+ from .announcement import Announcement
9
+ from .. import db
10
+ import random
11
+
12
+ def generate_mosque_data(mosque_info, logger):
13
+ """Generate data for a specific mosque"""
14
+ mosque = Mosque(
15
+ name=mosque_info['name'],
16
+ address=mosque_info['address'],
17
+ city=mosque_info['city'],
18
+ state=mosque_info['state'],
19
+ country=mosque_info['country'],
20
+ jakim_code=mosque_info['jakim_code'],
21
+ phone=mosque_info['phone'],
22
+ email=mosque_info['email'],
23
+ capacity=mosque_info['capacity']
24
+ )
25
+ db.session.add(mosque)
26
+ db.session.commit()
27
+
28
+ # Create mosque admins and staff
29
+ for user_data in mosque_info['users']:
30
+ user = User(
31
+ email=user_data["email"],
32
+ name=user_data["name"],
33
+ password=generate_password_hash("Demo@123", method='scrypt'),
34
+ role=user_data["role"],
35
+ mosque_id=mosque.id,
36
+ recovery_key=User.generate_recovery_key()
37
+ )
38
+ db.session.add(user)
39
+ db.session.commit()
40
+
41
+ # Generate prayer times with mosque-specific variations
42
+ prayer_names = ['Imsak', 'Subuh', 'Syuruk', 'Zuhur', 'Asar', 'Maghrib', 'Isyak']
43
+ base_times = {
44
+ 'Imsak': time(5, 30),
45
+ 'Subuh': time(5, 45),
46
+ 'Syuruk': time(7, 0),
47
+ 'Zuhur': time(13, 15),
48
+ 'Asar': time(16, 30),
49
+ 'Maghrib': time(19, 20),
50
+ 'Isyak': time(20, 35)
51
+ }
52
+
53
+ # Add mosque-specific offset (each mosque slightly different timing)
54
+ mosque_offset = random.randint(-10, 10)
55
+
56
+ for i in range(7):
57
+ date = datetime.now().date() + timedelta(days=i)
58
+ for prayer_name in prayer_names:
59
+ base_time = base_times[prayer_name]
60
+ # Add mosque-specific variations to prayer times
61
+ varied_time = (datetime.combine(date, base_time) +
62
+ timedelta(minutes=mosque_offset + random.randint(-2, 2))).time()
63
+
64
+ prayer_time = PrayerTime(
65
+ prayer_name=prayer_name,
66
+ time=varied_time,
67
+ date=date,
68
+ mosque_id=mosque.id
69
+ )
70
+ db.session.add(prayer_time)
71
+ db.session.commit()
72
+
73
+ # Generate inventory with mosque-specific quantities
74
+ inventory_items = [
75
+ ('Prayer Mats', 'Prayer Items', random.randint(300, 700)),
76
+ ('Quran Copies', 'Prayer Items', random.randint(100, 300)),
77
+ ('Cleaning Supplies', 'Cleaning', random.randint(30, 70)),
78
+ ('Vacuum Cleaners', 'Cleaning', random.randint(3, 8)),
79
+ ('Chairs', 'Office', random.randint(20, 50)),
80
+ ('Tables', 'Office', random.randint(5, 15)),
81
+ ('Water Dispensers', 'Kitchen', random.randint(5, 12)),
82
+ ('Light Bulbs', 'Maintenance', random.randint(50, 150))
83
+ ]
84
+
85
+ for i, (item_name, category, qty) in enumerate(inventory_items, 1):
86
+ inventory = Inventory(
87
+ mosque_id=mosque.id,
88
+ number=f'{mosque_info["code"]}-INV-{i:03d}',
89
+ item_name=item_name,
90
+ item_category=category,
91
+ item_description=f'{item_name} for {mosque.name}',
92
+ quantity=qty,
93
+ remarks=f'Inventory item for {mosque.name}'
94
+ )
95
+ db.session.add(inventory)
96
+ db.session.commit()
97
+
98
+ # Generate finance records with mosque-specific amounts
99
+ finance_scale = random.uniform(0.8, 1.5) # Each mosque has different financial scale
100
+ finance_records = [
101
+ ('Monthly Donation', 'Donation', 5000.00 * finance_scale),
102
+ ('Electricity Bill', 'Utility', -800.00 * finance_scale),
103
+ ('Water Bill', 'Utility', -200.00 * finance_scale),
104
+ ('Building Maintenance', 'Maintenance', -1500.00 * finance_scale),
105
+ ('Staff Salary', 'Salary', -2000.00 * finance_scale),
106
+ ('Ramadan Event', 'Event', -1000.00 * finance_scale),
107
+ ('Friday Collection', 'Donation', 3000.00 * finance_scale)
108
+ ]
109
+
110
+ for i, (name, category, amount) in enumerate(finance_records, 1):
111
+ date = datetime.now() - timedelta(days=random.randint(0, 30))
112
+ finance = Finance(
113
+ mosque_id=mosque.id,
114
+ number=f'{mosque_info["code"]}-FIN-{i:03d}',
115
+ transaction_name=name,
116
+ finance_category=category,
117
+ description=f'{name} for {mosque.name}',
118
+ amount=round(amount, 2),
119
+ date_added=date,
120
+ remarks=f'Finance record for {mosque.name}'
121
+ )
122
+ db.session.add(finance)
123
+ db.session.commit()
124
+
125
+ # Generate mosque-specific announcements
126
+ announcements = [
127
+ {
128
+ 'title': f'Weekly Activities at {mosque.name}',
129
+ 'content': f'Join our weekly activities at {mosque.name}. We offer Quran classes, Islamic lectures, and community events.',
130
+ 'is_urgent': False,
131
+ 'days_duration': 30
132
+ },
133
+ {
134
+ 'title': f'Urgent: {mosque.name} Maintenance Day',
135
+ 'content': f'We need volunteers for mosque maintenance this weekend at {mosque.name}. Your help is greatly appreciated.',
136
+ 'is_urgent': True,
137
+ 'days_duration': 7
138
+ },
139
+ {
140
+ 'title': mosque_info['special_announcement']['title'],
141
+ 'content': mosque_info['special_announcement']['content'],
142
+ 'is_urgent': mosque_info['special_announcement']['is_urgent'],
143
+ 'days_duration': mosque_info['special_announcement']['days_duration']
144
+ }
145
+ ]
146
+
147
+ current_date = datetime.now().date()
148
+ for i, announcement_data in enumerate(announcements):
149
+ start_date = current_date + timedelta(days=i)
150
+ end_date = start_date + timedelta(days=announcement_data['days_duration'])
151
+
152
+ announcement = Announcement(
153
+ mosque_id=mosque.id,
154
+ title=announcement_data['title'],
155
+ content=announcement_data['content'],
156
+ start_date=start_date,
157
+ end_date=end_date,
158
+ start_time=time(8, 0),
159
+ end_time=time(22, 0),
160
+ is_urgent=announcement_data['is_urgent']
161
+ )
162
+ db.session.add(announcement)
163
+ db.session.commit()
164
+
165
+ def generate_demo_data(logger):
166
+ """Generate demo data for multiple mosques"""
167
+ logger.info("Generating demo data...")
168
+
169
+ # Define multiple mosques with their specific data
170
+ mosques_info = [
171
+ {
172
+ 'name': "Masjid As-Salam",
173
+ 'address': "Jalan Cempaka",
174
+ 'city': "Shah Alam",
175
+ 'state': "Selangor",
176
+ 'country': "Malaysia",
177
+ 'jakim_code': "SGR01",
178
+ 'phone': "+60123456789",
179
+ 'email': "[email protected]",
180
+ 'capacity': 1000,
181
+ 'code': 'MAS',
182
+ 'users': [
183
+ {"email": "[email protected]", "name": "Salam Admin", "role": "admin"},
184
+ {"email": "[email protected]", "name": "Salam Staff 1", "role": "staff"},
185
+ {"email": "[email protected]", "name": "Salam Staff 2", "role": "staff"}
186
+ ],
187
+ 'special_announcement': {
188
+ 'title': 'Quran Competition Registration Open',
189
+ 'content': 'Register now for our annual Quran recitation competition. Open for all age groups.',
190
+ 'is_urgent': True,
191
+ 'days_duration': 14
192
+ }
193
+ },
194
+ {
195
+ 'name': "Masjid An-Nur",
196
+ 'address': "Jalan Makmur",
197
+ 'city': "Petaling Jaya",
198
+ 'state': "Selangor",
199
+ 'country': "Malaysia",
200
+ 'jakim_code': "SGR01",
201
+ 'phone': "+60123456790",
202
+ 'email': "[email protected]",
203
+ 'capacity': 1500,
204
+ 'code': 'MAN',
205
+ 'users': [
206
+ {"email": "[email protected]", "name": "Nur Admin", "role": "admin"},
207
+ {"email": "[email protected]", "name": "Nur Staff 1", "role": "staff"},
208
+ {"email": "[email protected]", "name": "Nur Staff 2", "role": "staff"}
209
+ ],
210
+ 'special_announcement': {
211
+ 'title': 'Youth Islamic Camp',
212
+ 'content': 'Join our weekend Islamic camp for youth aged 15-25. Activities include Islamic lectures, team building, and outdoor activities.',
213
+ 'is_urgent': False,
214
+ 'days_duration': 21
215
+ }
216
+ },
217
+ {
218
+ 'name': "Masjid Al-Hidayah",
219
+ 'address': "Jalan Semarak",
220
+ 'city': "Subang Jaya",
221
+ 'state': "Selangor",
222
+ 'country': "Malaysia",
223
+ 'jakim_code': "SGR01",
224
+ 'phone': "+60123456791",
225
+ 'email': "[email protected]",
226
+ 'capacity': 800,
227
+ 'code': 'MAH',
228
+ 'users': [
229
+ {"email": "[email protected]", "name": "Hidayah Admin", "role": "admin"},
230
+ {"email": "[email protected]", "name": "Hidayah Staff 1", "role": "staff"},
231
+ {"email": "[email protected]", "name": "Hidayah Staff 2", "role": "staff"}
232
+ ],
233
+ 'special_announcement': {
234
+ 'title': 'Community Iftar Program',
235
+ 'content': 'Join us for our weekly community iftar program every Saturday. All are welcome to join.',
236
+ 'is_urgent': True,
237
+ 'days_duration': 10
238
+ }
239
+ }
240
+ ]
241
+
242
+ # Generate data for each mosque
243
+ for mosque_info in mosques_info:
244
+ generate_mosque_data(mosque_info, logger)
245
+
246
+ logger.info("Demo data generated successfully!")
main/models/finance.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .. import db
2
+ from datetime import datetime
3
+
4
+ class Finance(db.Model):
5
+ id = db.Column(db.Integer, primary_key=True)
6
+ mosque_id = db.Column(db.Integer, db.ForeignKey('mosque.id'), nullable=True)
7
+ number = db.Column(db.String(50), nullable=False)
8
+ transaction_name = db.Column(db.String(100), nullable=False)
9
+ finance_category = db.Column(db.String(50), nullable=False)
10
+ description = db.Column(db.Text)
11
+ amount = db.Column(db.Float, nullable=False, default=0.0)
12
+ date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
13
+ remarks = db.Column(db.Text)
14
+
15
+ # Relationship with Mosque model (optional)
16
+ mosque = db.relationship('Mosque', backref=db.backref('finance_items', lazy=True))
main/models/inventory.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .. import db
2
+ from datetime import datetime
3
+
4
+ class Inventory(db.Model):
5
+ id = db.Column(db.Integer, primary_key=True)
6
+ mosque_id = db.Column(db.Integer, db.ForeignKey('mosque.id'), nullable=True)
7
+ number = db.Column(db.String(50), nullable=False)
8
+ item_name = db.Column(db.String(100), nullable=False)
9
+ item_category = db.Column(db.String(50), nullable=False)
10
+ item_description = db.Column(db.Text)
11
+ quantity = db.Column(db.Integer, nullable=False, default=0)
12
+ date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
13
+ remarks = db.Column(db.Text)
14
+
15
+ # Relationship with Mosque model (optional)
16
+ mosque = db.relationship('Mosque', backref=db.backref('inventory_items', lazy=True))
main/models/mosque.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .. import db
2
+ from datetime import datetime
3
+
4
+ class Mosque(db.Model):
5
+ id = db.Column(db.Integer, primary_key=True)
6
+ name = db.Column(db.String(150), nullable=False)
7
+ address = db.Column(db.String(200), nullable=False)
8
+ city = db.Column(db.String(100), nullable=False)
9
+ state = db.Column(db.String(100), nullable=False)
10
+ country = db.Column(db.String(100), nullable=False)
11
+ jakim_code = db.Column(db.String(10), nullable=False)
12
+ phone = db.Column(db.String(20))
13
+ email = db.Column(db.String(150))
14
+ capacity = db.Column(db.Integer)
15
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
16
+
17
+ # Relationships
18
+ prayer_times = db.relationship('PrayerTime', backref='mosque', lazy=True)
19
+ announcements = db.relationship('Announcement', backref='mosque', lazy=True)
20
+
21
+ def __repr__(self):
22
+ return f'<Mosque {self.name}>'
main/models/prayer_time.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .. import db
2
+ from datetime import datetime
3
+
4
+ # Prayer order dictionary for sorting
5
+ PRAYER_ORDER = {
6
+ 'Imsak': 1,
7
+ 'Subuh': 2,
8
+ 'Syuruk': 3,
9
+ 'Zuhur': 4,
10
+ 'Asar': 5,
11
+ 'Maghrib': 6,
12
+ 'Isyak': 7
13
+ }
14
+
15
+ class PrayerTime(db.Model):
16
+ id = db.Column(db.Integer, primary_key=True)
17
+ prayer_name = db.Column(db.String(20), nullable=False) # Fajr, Dhuhr, Asr, Maghrib, Isha
18
+ time = db.Column(db.Time, nullable=False)
19
+ date = db.Column(db.Date, nullable=False)
20
+ mosque_id = db.Column(db.Integer, db.ForeignKey('mosque.id'), nullable=False)
21
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
22
+
23
+ @property
24
+ def order(self):
25
+ """Get the order of this prayer for sorting"""
26
+ return PRAYER_ORDER.get(self.prayer_name, 999) # Unknown prayers go to the end
27
+
28
+ def __repr__(self):
29
+ return f'<PrayerTime {self.prayer_name} at {self.time}>'
main/models/user.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .. import db
2
+ from flask_login import UserMixin
3
+ from datetime import datetime
4
+ import secrets
5
+ import string
6
+
7
+ class User(db.Model, UserMixin):
8
+ id = db.Column(db.Integer, primary_key=True)
9
+ email = db.Column(db.String(150), unique=True, nullable=False)
10
+ password = db.Column(db.String(150), nullable=False)
11
+ name = db.Column(db.String(150), nullable=False)
12
+ role = db.Column(db.String(20), nullable=False, default='user') # 'user', 'admin', 'staff', 'superadmin'
13
+ recovery_key = db.Column(db.String(16), unique=True, nullable=False)
14
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
15
+
16
+ # Relationships
17
+ mosque_id = db.Column(db.Integer, db.ForeignKey('mosque.id'))
18
+ mosque = db.relationship('Mosque', backref='admins')
19
+
20
+ def __repr__(self):
21
+ return f'<User {self.email}>'
22
+
23
+ @staticmethod
24
+ def generate_recovery_key():
25
+ """Generate a 16-character recovery key using uppercase letters and numbers"""
26
+ alphabet = string.ascii_uppercase + string.digits
27
+ while True:
28
+ key = ''.join(secrets.choice(alphabet) for i in range(16))
29
+ # Check if key already exists
30
+ if not User.query.filter_by(recovery_key=key).first():
31
+ return key
main/static/css/style.css ADDED
@@ -0,0 +1,765 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles for Mosqku */
2
+
3
+ :root {
4
+ --primary-color: #006400;
5
+ --primary-dark: #004d00;
6
+ --primary-light: #008000;
7
+ --accent-color: #90EE90;
8
+ --error-color: #dc3545;
9
+ --error-dark: #bd2130;
10
+ --warning-color: #ffc107;
11
+ --warning-dark: #d39e00;
12
+ }
13
+
14
+ body {
15
+ min-height: 100vh;
16
+ display: flex;
17
+ flex-direction: column;
18
+ background-color: #f8f9fa;
19
+ margin: 0;
20
+ padding: 0;
21
+ position: relative;
22
+ }
23
+
24
+ .container.mt-4 {
25
+ flex: 1 0 auto;
26
+ padding-bottom: 80px; /* Ensure content doesn't overlap with footer */
27
+ }
28
+
29
+ .footer {
30
+ position: absolute;
31
+ bottom: 0;
32
+ width: 100%;
33
+ background-color: var(--primary-dark) !important;
34
+ padding: 1rem 0;
35
+ }
36
+
37
+ .navbar {
38
+ box-shadow: 0 2px 4px rgba(0,0,0,.1);
39
+ }
40
+
41
+ .navbar-dark.bg-success {
42
+ background-color: var(--primary-color) !important;
43
+ }
44
+
45
+ .navbar-dark .navbar-nav .nav-link {
46
+ color: rgba(255, 255, 255, 0.95) !important;
47
+ font-weight: 500;
48
+ }
49
+
50
+ .navbar-dark .navbar-nav .nav-link:hover {
51
+ color: #ffffff !important;
52
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
53
+ }
54
+
55
+ .navbar-dark .navbar-brand {
56
+ color: #ffffff !important;
57
+ font-weight: 600;
58
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
59
+ }
60
+
61
+ .card {
62
+ box-shadow: 0 4px 6px rgba(0,0,0,.1);
63
+ border: none;
64
+ margin-bottom: 20px;
65
+ border-color: var(--primary-color);
66
+ }
67
+
68
+ .card-header {
69
+ background-color: var(--primary-color);
70
+ color: white;
71
+ border-bottom: 1px solid rgba(0,0,0,.125);
72
+ }
73
+
74
+ .btn-primary {
75
+ background-color: var(--primary-color);
76
+ border-color: var(--primary-color);
77
+ }
78
+
79
+ .btn-primary:hover {
80
+ background-color: var(--primary-dark);
81
+ border-color: var(--primary-dark);
82
+ }
83
+
84
+ .btn-outline-primary {
85
+ color: var(--primary-color);
86
+ border-color: var(--primary-color);
87
+ }
88
+
89
+ .btn-outline-primary:hover {
90
+ background-color: var(--primary-color);
91
+ border-color: var(--primary-color);
92
+ }
93
+
94
+ .btn:focus,
95
+ .btn.focus,
96
+ .btn-primary:focus,
97
+ .btn-primary.focus,
98
+ .btn-outline-primary:focus,
99
+ .btn-outline-primary.focus {
100
+ box-shadow: none !important;
101
+ }
102
+
103
+ .btn-primary.dropdown-toggle:focus,
104
+ .btn-outline-primary.dropdown-toggle:focus,
105
+ .btn-primary.dropdown-toggle.show,
106
+ .btn-outline-primary.dropdown-toggle.show {
107
+ box-shadow: none !important;
108
+ }
109
+
110
+ .alert {
111
+ border-radius: 4px;
112
+ margin-bottom: 20px;
113
+ padding: 1rem;
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 0.5rem;
117
+ }
118
+
119
+ .alert i {
120
+ font-size: 1.25rem;
121
+ margin-right: 0.5rem;
122
+ }
123
+
124
+ .alert-success {
125
+ background-color: var(--accent-color);
126
+ border-color: var(--primary-light);
127
+ color: var(--primary-dark);
128
+ }
129
+
130
+ .alert-success i {
131
+ color: var(--primary-dark);
132
+ }
133
+
134
+ .alert-danger, .alert-error {
135
+ background-color: #ffe6e6;
136
+ border-color: var(--error-color);
137
+ color: var(--error-dark);
138
+ }
139
+
140
+ .alert-danger i, .alert-error i {
141
+ color: var(--error-color);
142
+ }
143
+
144
+ .alert-warning {
145
+ background-color: #fff3cd;
146
+ border-color: var(--warning-color);
147
+ color: var(--warning-dark);
148
+ }
149
+
150
+ .alert-warning i {
151
+ color: var(--warning-color);
152
+ }
153
+
154
+ .alert-info {
155
+ background-color: #cce5ff;
156
+ border-color: #b8daff;
157
+ color: #004085;
158
+ }
159
+
160
+ .alert-info i {
161
+ color: #004085;
162
+ }
163
+
164
+ .prayer-time-card {
165
+ background-color: #fff;
166
+ border-radius: 8px;
167
+ padding: 15px;
168
+ margin-bottom: 15px;
169
+ }
170
+
171
+ .prayer-time-name {
172
+ font-weight: bold;
173
+ color: #2c3e50;
174
+ }
175
+
176
+ .prayer-time {
177
+ font-size: 1.2em;
178
+ color: #34495e;
179
+ }
180
+
181
+ .announcement-card {
182
+ background-color: #fff;
183
+ border-radius: 8px;
184
+ padding: 20px;
185
+ margin-bottom: 20px;
186
+ border: 1px solid var(--primary-light);
187
+ }
188
+
189
+ .announcement-title {
190
+ color: var(--primary-color);
191
+ font-size: 1.2em;
192
+ margin-bottom: 10px;
193
+ }
194
+
195
+ .announcement-date {
196
+ color: #6c757d;
197
+ font-size: 0.875rem;
198
+ margin-top: 0.5rem;
199
+ line-height: 1.5;
200
+ }
201
+
202
+ .announcement-date i {
203
+ width: 1.2rem;
204
+ color: var(--primary-color);
205
+ }
206
+
207
+ .mosque-card {
208
+ transition: transform 0.2s;
209
+ }
210
+
211
+ .mosque-card:hover {
212
+ transform: translateY(-5px);
213
+ }
214
+
215
+ .dashboard-stats {
216
+ background-color: #fff;
217
+ border-radius: 8px;
218
+ padding: 20px;
219
+ margin-bottom: 20px;
220
+ text-align: center;
221
+ }
222
+
223
+ .stats-number {
224
+ font-size: 2em;
225
+ font-weight: bold;
226
+ color: #2c3e50;
227
+ }
228
+
229
+ .stats-label {
230
+ color: #7f8c8d;
231
+ text-transform: uppercase;
232
+ font-size: 0.9em;
233
+ }
234
+
235
+ /* Form control focus */
236
+ .form-control:focus {
237
+ border-color: var(--primary-color);
238
+ box-shadow: 0 0 0 0.25rem rgba(0, 100, 0, 0.25);
239
+ }
240
+
241
+ .form-select:focus {
242
+ border-color: var(--primary-color);
243
+ box-shadow: 0 0 0 0.25rem rgba(0, 100, 0, 0.25);
244
+ }
245
+
246
+ /* Icon spacing */
247
+ .fas, .far, .fab {
248
+ margin-right: 0.4rem;
249
+ margin-left: 0.4rem;
250
+ }
251
+
252
+ /* Hero Section */
253
+ .hero-section {
254
+ background: linear-gradient(rgba(0, 100, 0, 0.9), rgba(0, 100, 0, 0.8));
255
+ background-size: cover;
256
+ background-position: center;
257
+ color: #ffffff;
258
+ padding: 6rem 1rem;
259
+ margin-top: 0.5rem;
260
+ margin-bottom: 4rem;
261
+ border-radius: 10px;
262
+ }
263
+
264
+ /* Optional background image */
265
+ .hero-section.with-bg-image {
266
+ background: linear-gradient(rgba(0, 100, 0, 0.9), rgba(0, 100, 0, 0.8)), url('/static/img/mosque-bg.jpg');
267
+ background-size: cover;
268
+ background-position: center;
269
+ }
270
+
271
+ .hero-content {
272
+ max-width: 800px;
273
+ margin: 0 auto;
274
+ }
275
+
276
+ .hero-title {
277
+ font-size: 3.5rem;
278
+ font-weight: 700;
279
+ margin-bottom: 1.5rem;
280
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
281
+ color: #ffffff;
282
+ }
283
+
284
+ .hero-subtitle {
285
+ font-size: 1.5rem;
286
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
287
+ color: #ffffff;
288
+ }
289
+
290
+ .hero-cta {
291
+ margin-top: 2rem;
292
+ display: flex;
293
+ gap: 1rem;
294
+ justify-content: center;
295
+ flex-wrap: wrap;
296
+ }
297
+
298
+ .hero-cta .btn {
299
+ padding: 0.8rem 2rem;
300
+ font-weight: 600;
301
+ text-transform: uppercase;
302
+ letter-spacing: 0.5px;
303
+ min-width: 200px;
304
+ }
305
+
306
+ .hero-cta .btn-outline-primary {
307
+ color: #ffffff;
308
+ border-color: #ffffff;
309
+ background-color: transparent;
310
+ }
311
+
312
+ .hero-cta .btn-outline-primary:hover {
313
+ background-color: #ffffff;
314
+ color: var(--primary-color);
315
+ border-color: #ffffff;
316
+ }
317
+
318
+ /* Features Section */
319
+ .features-section {
320
+ padding: 5rem 0;
321
+ border-radius: 10px;
322
+ }
323
+
324
+ .section-title {
325
+ font-size: 2.5rem;
326
+ font-weight: 600;
327
+ color: #333333;
328
+ margin-bottom: 3rem;
329
+ }
330
+
331
+ .feature-card {
332
+ background: white;
333
+ padding: 2rem;
334
+ border-radius: 10px;
335
+ text-align: center;
336
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
337
+ height: 100%;
338
+ border: 1px solid rgba(0, 100, 0, 0.1);
339
+ }
340
+
341
+ .feature-card:hover {
342
+ transform: translateY(-10px);
343
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
344
+ }
345
+
346
+ .feature-icon {
347
+ color: var(--primary-color);
348
+ margin-bottom: 1.5rem;
349
+ }
350
+
351
+ .feature-icon i {
352
+ color: var(--primary-color);
353
+ }
354
+
355
+ .feature-card h3 {
356
+ font-size: 1.5rem;
357
+ margin-bottom: 1rem;
358
+ color: #333333;
359
+ font-weight: 600;
360
+ }
361
+
362
+ .feature-card p {
363
+ color: #4a4a4a;
364
+ line-height: 1.6;
365
+ font-size: 1rem;
366
+ }
367
+
368
+ /* Mosques Section */
369
+ .mosques-section {
370
+ padding: 5rem 0;
371
+ }
372
+
373
+ .mosque-card {
374
+ background: white;
375
+ border-radius: 15px;
376
+ overflow: hidden;
377
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
378
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
379
+ border: 1px solid rgba(0, 100, 0, 0.1);
380
+ }
381
+
382
+ .mosque-card:hover {
383
+ transform: translateY(-10px);
384
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
385
+ }
386
+
387
+ .mosque-card-header {
388
+ background-color: var(--primary-color);
389
+ color: white;
390
+ padding: 2rem;
391
+ text-align: center;
392
+ }
393
+
394
+ .mosque-card-header i {
395
+ font-size: 3rem;
396
+ }
397
+
398
+ .mosque-card-body {
399
+ padding: 2rem;
400
+ }
401
+
402
+ .mosque-name {
403
+ font-size: 1rem;
404
+ color: var(--primary-dark);
405
+ margin-bottom: 1rem;
406
+ }
407
+
408
+ .mosque-info {
409
+ margin-bottom: 1.5rem;
410
+ }
411
+
412
+ .mosque-info p {
413
+ margin-bottom: 0.5rem;
414
+ color: #4a4a4a;
415
+ font-size: 1rem;
416
+ }
417
+
418
+ .mosque-info i {
419
+ width: 1.5rem;
420
+ color: var(--primary-color);
421
+ }
422
+
423
+ .empty-state {
424
+ background: white;
425
+ border-radius: 15px;
426
+ padding: 3rem;
427
+ }
428
+
429
+ .empty-state i {
430
+ opacity: 1;
431
+ }
432
+
433
+ .empty-state h3 {
434
+ color: #333333;
435
+ margin: 1rem 0;
436
+ font-weight: 600;
437
+ }
438
+
439
+ .empty-state p {
440
+ color: #4a4a4a;
441
+ }
442
+
443
+ .empty-state .btn-primary {
444
+ color: #ffffff;
445
+ }
446
+
447
+ .empty-state .btn-primary i {
448
+ color: #ffffff;
449
+ }
450
+
451
+ .admin-actions {
452
+ background-color: #f8f9fa;
453
+ padding: 2rem 0;
454
+ margin-top: -5rem;
455
+ }
456
+
457
+ /* Responsive adjustments */
458
+ @media (max-width: 768px) {
459
+ .hero-title {
460
+ font-size: 2.5rem;
461
+ }
462
+
463
+ .hero-subtitle {
464
+ font-size: 1.2rem;
465
+ }
466
+
467
+ .hero-cta {
468
+ flex-direction: column;
469
+ align-items: stretch;
470
+ padding: 0 1rem;
471
+ }
472
+
473
+ .hero-cta .btn {
474
+ width: 100%;
475
+ margin: 0;
476
+ }
477
+
478
+ .section-title {
479
+ font-size: 2rem;
480
+ }
481
+
482
+ .features-section,
483
+ .mosques-section {
484
+ padding: 3rem 0;
485
+ }
486
+ }
487
+
488
+ /* Dashboard Styles */
489
+ .dashboard-header {
490
+ background-color: white;
491
+ padding: 1.5rem;
492
+ border-radius: 10px;
493
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
494
+ margin-bottom: 2rem;
495
+ }
496
+
497
+ .dashboard-header h2 {
498
+ color: var(--primary-color);
499
+ margin-bottom: 0.5rem;
500
+ }
501
+
502
+ .dashboard-header h2 i {
503
+ margin-right: 0.5rem;
504
+ }
505
+
506
+ .dashboard-card {
507
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
508
+ }
509
+
510
+ .dashboard-card:hover {
511
+ transform: translateY(-5px);
512
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
513
+ }
514
+
515
+ .dashboard-card .card-header {
516
+ background-color: white;
517
+ border-bottom: 2px solid rgba(0, 100, 0, 0.1);
518
+ }
519
+
520
+ .dashboard-card .card-header h4 {
521
+ color: var(--primary-color);
522
+ font-size: 1.2rem;
523
+ margin: 0;
524
+ }
525
+
526
+ .dashboard-card .card-header h4 i {
527
+ margin-right: 0.5rem;
528
+ }
529
+
530
+ /* Mosque Info List */
531
+ .mosque-info-list {
532
+ display: flex;
533
+ flex-direction: column;
534
+ gap: 1rem;
535
+ }
536
+
537
+ .info-item {
538
+ display: flex;
539
+ align-items: flex-start;
540
+ gap: 1rem;
541
+ }
542
+
543
+ .info-item i {
544
+ color: var(--primary-color);
545
+ font-size: 1.2rem;
546
+ width: 1.5rem;
547
+ margin-top: 0.25rem;
548
+ }
549
+
550
+ .info-item div {
551
+ flex: 1;
552
+ }
553
+
554
+ .info-item label {
555
+ display: block;
556
+ font-size: 0.875rem;
557
+ color: #666;
558
+ margin-bottom: 0.25rem;
559
+ }
560
+
561
+ .info-item span {
562
+ color: #333;
563
+ font-weight: 500;
564
+ }
565
+
566
+ /* Prayer Times List */
567
+ .prayer-times-list {
568
+ display: flex;
569
+ flex-direction: column;
570
+ gap: 0.75rem;
571
+ }
572
+
573
+ .prayer-time-item {
574
+ display: flex;
575
+ justify-content: space-between;
576
+ align-items: center;
577
+ padding: 0.75rem 1rem;
578
+ background-color: #f8f9fa;
579
+ border-radius: 8px;
580
+ transition: background-color 0.2s ease;
581
+ }
582
+
583
+ .prayer-time-item:hover {
584
+ background-color: #e9ecef;
585
+ }
586
+
587
+ .prayer-name {
588
+ font-weight: 500;
589
+ color: var(--primary-color);
590
+ display: flex;
591
+ align-items: center;
592
+ gap: 0.5rem;
593
+ }
594
+
595
+ .prayer-name i {
596
+ color: var(--primary-color);
597
+ width: 1.5rem;
598
+ text-align: center;
599
+ }
600
+
601
+ .prayer-time {
602
+ color: #666;
603
+ font-weight: 500;
604
+ display: flex;
605
+ align-items: center;
606
+ gap: 0.5rem;
607
+ }
608
+
609
+ .prayer-time i {
610
+ color: var(--primary-color);
611
+ }
612
+
613
+ /* Announcements List */
614
+ .announcements-list {
615
+ display: flex;
616
+ flex-direction: column;
617
+ gap: 0.75rem;
618
+ }
619
+
620
+ .announcement-item {
621
+ padding: 1rem;
622
+ background-color: #f8f9fa;
623
+ border-radius: 8px;
624
+ border-left: 4px solid var(--primary-color);
625
+ }
626
+
627
+ .announcement-item h5 {
628
+ color: var(--primary-color);
629
+ margin-bottom: 0.5rem;
630
+ font-size: 1.1rem;
631
+ }
632
+
633
+ .announcement-item p {
634
+ color: #666;
635
+ margin-bottom: 0.5rem;
636
+ font-size: 0.95rem;
637
+ }
638
+
639
+ .announcement-meta {
640
+ font-size: 0.875rem;
641
+ color: #666;
642
+ }
643
+
644
+ .announcement-meta i {
645
+ margin-right: 0.5rem;
646
+ color: var(--primary-color);
647
+ }
648
+
649
+ /* Empty State Indicators */
650
+ .empty-indicator {
651
+ text-align: center;
652
+ padding: 2rem 1rem;
653
+ color: #666;
654
+ }
655
+
656
+ .empty-indicator i {
657
+ margin-bottom: 1rem;
658
+ }
659
+
660
+ .empty-indicator p {
661
+ margin: 0;
662
+ font-size: 0.95rem;
663
+ }
664
+
665
+ /* Quick Actions Dropdown */
666
+ .dropdown-menu {
667
+ border: none;
668
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
669
+ border-radius: 8px;
670
+ }
671
+
672
+ .dropdown-item {
673
+ padding: 0.75rem 1rem;
674
+ color: #333;
675
+ }
676
+
677
+ .dropdown-item:hover {
678
+ background-color: #f8f9fa;
679
+ color: var(--primary-color);
680
+ }
681
+
682
+ .dropdown-item i {
683
+ margin-right: 0.5rem;
684
+ width: 1.5rem;
685
+ color: var(--primary-color);
686
+ }
687
+
688
+ /* Quick Actions Button */
689
+ .btn-primary.dropdown-toggle.show,
690
+ .btn-primary.dropdown-toggle:focus {
691
+ background-color: var(--primary-color) !important;
692
+ border-color: var(--primary-color) !important;
693
+ color: white !important;
694
+ }
695
+
696
+ .btn-outline-primary.dropdown-toggle.show,
697
+ .btn-outline-primary.dropdown-toggle:focus {
698
+ background-color: transparent !important;
699
+ border-color: var(--primary-color) !important;
700
+ color: var(--primary-color) !important;
701
+ }
702
+
703
+ .btn-outline-primary.dropdown-toggle.show:hover {
704
+ background-color: var(--primary-color) !important;
705
+ color: white !important;
706
+ }
707
+
708
+ /* Admin Dashboard Styles */
709
+ .mosque-list,
710
+ .admin-list {
711
+ display: flex;
712
+ flex-direction: column;
713
+ gap: 0.75rem;
714
+ }
715
+
716
+ .mosque-item,
717
+ .admin-item {
718
+ background-color: #f8f9fa;
719
+ border-radius: 8px;
720
+ padding: 1.25rem;
721
+ border-left: 4px solid var(--primary-color);
722
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
723
+ }
724
+
725
+ .mosque-item:hover,
726
+ .admin-item:hover {
727
+ transform: translateX(5px);
728
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
729
+ }
730
+
731
+ .admin-name {
732
+ color: var(--primary-color);
733
+ font-size: 1rem;
734
+ }
735
+
736
+ .mosque-actions {
737
+ display: flex;
738
+ flex-direction: column;
739
+ gap: 0.5rem;
740
+ }
741
+
742
+ .mosque-actions .btn,
743
+ .admin-actions .btn {
744
+ min-width: 100px;
745
+ }
746
+
747
+ .admin-item .badge {
748
+ background-color: var(--primary-color) !important;
749
+ padding: 0.5rem 0.75rem;
750
+ font-weight: 500;
751
+ }
752
+
753
+ .btn {
754
+ display: inline;
755
+ }
756
+
757
+ .card-body {
758
+ flex: 1 1 auto;
759
+ padding: 1rem;
760
+ }
761
+
762
+ .badge {
763
+ font-weight: normal !important;
764
+ font-size: 0.8rem !important;
765
+ }
main/static/favicon/favicon.ico ADDED
main/static/js/main.js ADDED
@@ -0,0 +1 @@
 
 
1
+ // main.js
main/templates/admin/add_admin.html ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add New Admin{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Add New Admin</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-user-plus"></i> Add New Admin</h2>
15
+ <p class="text-muted mb-0">Create a new admin account and assign to a mosque</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-6">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-user-shield"></i> Administrator Details</h4>
25
+ </div>
26
+ <div class="card-body p-4">
27
+ <form method="POST" id="adminForm" onsubmit="return validatePassword()">
28
+ <div class="mb-3">
29
+ <label for="name" class="form-label">
30
+ <i class="fas fa-user"></i> Full Name
31
+ </label>
32
+ <input type="text" class="form-control" id="name" name="name" required>
33
+ </div>
34
+
35
+ <div class="mb-3">
36
+ <label for="email" class="form-label">
37
+ <i class="fas fa-envelope"></i> Email Address
38
+ </label>
39
+ <input type="email" class="form-control" id="email" name="email" required>
40
+ </div>
41
+
42
+ <div class="mb-3">
43
+ <label for="password" class="form-label">
44
+ <i class="fas fa-lock"></i> Password
45
+ </label>
46
+ <input type="password" class="form-control" id="password" name="password"
47
+ pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
48
+ title="Password must be at least 8 characters long and include at least one number and one special character"
49
+ required>
50
+ <div class="form-text">
51
+ Password requirements:
52
+ <ul class="mb-0">
53
+ <li>At least 8 characters long</li>
54
+ <li>Must contain at least one number</li>
55
+ <li>Must contain at least one special character (@$!%*#?&)</li>
56
+ </ul>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="mb-4">
61
+ <label for="mosque_id" class="form-label">
62
+ <i class="fas fa-mosque"></i> Assign to Mosque
63
+ </label>
64
+ <select class="form-select" id="mosque_id" name="mosque_id" required>
65
+ <option value="" disabled selected>Select a mosque</option>
66
+ {% for mosque in mosques %}
67
+ <option value="{{ mosque.id }}">{{ mosque.name }} ({{ mosque.city }}, {{ mosque.state }})</option>
68
+ {% endfor %}
69
+ </select>
70
+ </div>
71
+
72
+ <div class="d-grid gap-2">
73
+ <button type="submit" class="btn btn-primary">
74
+ <i class="fas fa-plus"></i> Create Administrator
75
+ </button>
76
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
77
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
78
+ </a>
79
+ </div>
80
+ </form>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <script>
87
+ function validatePassword() {
88
+ const password = document.getElementById('password');
89
+ const pattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
90
+
91
+ if (!pattern.test(password.value)) {
92
+ password.setCustomValidity('Password must meet all requirements');
93
+ return false;
94
+ } else {
95
+ password.setCustomValidity('');
96
+ return true;
97
+ }
98
+ }
99
+
100
+ document.getElementById('password').addEventListener('input', validatePassword);
101
+ </script>
102
+ {% endblock %}
main/templates/admin/add_mosque.html ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add Mosque{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Add Mosque</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-mosque"></i> Add New Mosque</h2>
15
+ <p class="text-muted mb-0">Register a new mosque in the system</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-8">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-info-circle"></i> Mosque Information</h4>
25
+ </div>
26
+ <div class="card-body p-4">
27
+ <form method="POST">
28
+ <div class="row">
29
+ <div class="col-md-6">
30
+ <div class="mb-3">
31
+ <label for="name" class="form-label">
32
+ <i class="fas fa-building"></i> Mosque Name
33
+ </label>
34
+ <input type="text" class="form-control" id="name" name="name" required>
35
+ </div>
36
+
37
+ <div class="mb-3">
38
+ <label for="address" class="form-label">
39
+ <i class="fas fa-map-marker-alt"></i> Address
40
+ </label>
41
+ <input type="text" class="form-control" id="address" name="address" required>
42
+ </div>
43
+
44
+ <div class="mb-3">
45
+ <label for="city" class="form-label">
46
+ <i class="fas fa-city"></i> City
47
+ </label>
48
+ <input type="text" class="form-control" id="city" name="city" required>
49
+ </div>
50
+
51
+ <div class="mb-3">
52
+ <label for="state" class="form-label">
53
+ <i class="fas fa-map"></i> State/Province
54
+ </label>
55
+ <select class="form-select" id="state" name="state" required>
56
+ <option value="" disabled selected>Select state</option>
57
+ {% for state in ['Johor', 'Kedah', 'Kelantan', 'Melaka', 'Negeri Sembilan', 'Pahang', 'Perak', 'Perlis', 'Pulau Pinang', 'Sabah', 'Sarawak', 'Selangor', 'Terengganu', 'Kuala Lumpur', 'Labuan', 'Putrajaya'] %}
58
+ <option value="{{ state }}">{{ state }}</option>
59
+ {% endfor %}
60
+ </select>
61
+ </div>
62
+
63
+ <div class="mb-3">
64
+ <label for="jakim_code" class="form-label">
65
+ <i class="fas fa-clock"></i> JAKIM Prayer Time Zone
66
+ </label>
67
+ <select class="form-select" id="jakim_code" name="jakim_code" required>
68
+ <option value="" disabled selected>Select prayer time zone</option>
69
+ <optgroup label="Johor">
70
+ <option value="JHR01">JHR01 (Pulau Aur dan Pulau Pemanggil)</option>
71
+ <option value="JHR02">JHR02 (Johor Bahru, Kota Tinggi, Mersing, Kulai)</option>
72
+ <option value="JHR03">JHR03 (Kluang, Pontian)</option>
73
+ <option value="JHR04">JHR04 (Batu Pahat, Muar, Segamat, Gemas Johor, Tangkak)</option>
74
+ </optgroup>
75
+ <optgroup label="Kedah">
76
+ <option value="KDH01">KDH01 (Kota Setar, Kubang Pasu, Pokok Sena)</option>
77
+ <option value="KDH02">KDH02 (Kuala Muda, Yan, Pendang)</option>
78
+ <option value="KDH03">KDH03 (Padang Terap, Sik)</option>
79
+ <option value="KDH04">KDH04 (Baling)</option>
80
+ <option value="KDH05">KDH05 (Bandar Baharu, Kulim)</option>
81
+ <option value="KDH06">KDH06 (Langkawi)</option>
82
+ <option value="KDH07">KDH07 (Puncak Gunung Jerai)</option>
83
+ </optgroup>
84
+ <optgroup label="Kelantan">
85
+ <option value="KTN01">KTN01 (Bachok, Kota Bharu, Machang, Pasir Mas, dan lain-lain)</option>
86
+ <option value="KTN02">KTN02 (Gua Musang, Jeli, Lojing)</option>
87
+ </optgroup>
88
+ <optgroup label="Melaka">
89
+ <option value="MLK01">MLK01 (Melaka)</option>
90
+ </optgroup>
91
+ <optgroup label="Negeri Sembilan">
92
+ <option value="NGS01">NGS01 (Tampin, Jempol)</option>
93
+ <option value="NGS02">NGS02 (Jelebu, Kuala Pilah, Rembau)</option>
94
+ <option value="NGS03">NGS03 (Port Dickson, Seremban)</option>
95
+ </optgroup>
96
+ <optgroup label="Pahang">
97
+ <option value="PHG01">PHG01 (Pulau Tioman)</option>
98
+ <option value="PHG02">PHG02 (Kuantan, Pekan, Muadzam Shah)</option>
99
+ <option value="PHG03">PHG03 (Jerantut, Temerloh, Maran, dan lain-lain)</option>
100
+ <option value="PHG04">PHG04 (Bentong, Lipis, Raub)</option>
101
+ <option value="PHG05">PHG05 (Genting Sempah, Janda Baik, Bukit Tinggi)</option>
102
+ <option value="PHG06">PHG06 (Cameron Highlands, Genting Highlands, Bukit Fraser)</option>
103
+ <option value="PHG07">PHG07 (Rompin, Endau, Pontian)</option>
104
+ </optgroup>
105
+ <optgroup label="Perak">
106
+ <option value="PRK01">PRK01 (Tapah, Slim River, Tanjung Malim)</option>
107
+ <option value="PRK02">PRK02 (Kuala Kangsar, Sg. Siput, Ipoh, dan lain-lain)</option>
108
+ <option value="PRK03">PRK03 (Lenggong, Pengkalan Hulu, Grik)</option>
109
+ <option value="PRK04">PRK04 (Temengor, Belum)</option>
110
+ <option value="PRK05">PRK05 (Kg Gajah, Teluk Intan, Bagan Datuk, dan lain-lain)</option>
111
+ <option value="PRK06">PRK06 (Selama, Taiping, Bagan Serai, Parit Buntar)</option>
112
+ <option value="PRK07">PRK07 (Bukit Larut)</option>
113
+ </optgroup>
114
+ <optgroup label="Perlis">
115
+ <option value="PLS01">PLS01 (Perlis)</option>
116
+ </optgroup>
117
+ <optgroup label="Pulau Pinang">
118
+ <option value="PNG01">PNG01 (Pulau Pinang)</option>
119
+ </optgroup>
120
+ <optgroup label="Sabah">
121
+ <option value="SBH01">SBH01 (Sandakan)</option>
122
+ <option value="SBH02">SBH02 (Beluran)</option>
123
+ <option value="SBH03">SBH03 (Lahad Datu)</option>
124
+ <option value="SBH04">SBH04 (Bandar Tawau)</option>
125
+ <option value="SBH05">SBH05 (Kudat)</option>
126
+ <option value="SBH06">SBH06 (Gunung Kinabalu)</option>
127
+ <option value="SBH07">SBH07 (Kota Kinabalu)</option>
128
+ <option value="SBH08">SBH08 (Pensiangan)</option>
129
+ <option value="SBH09">SBH09 (Beaufort)</option>
130
+ </optgroup>
131
+ <optgroup label="Sarawak">
132
+ <option value="SWK01">SWK01 (Limbang)</option>
133
+ <option value="SWK02">SWK02 (Miri)</option>
134
+ <option value="SWK03">SWK03 (Pandan)</option>
135
+ <option value="SWK04">SWK04 (Sibu)</option>
136
+ <option value="SWK05">SWK05 (Sarikei)</option>
137
+ <option value="SWK06">SWK06 (Lubok Antu)</option>
138
+ <option value="SWK07">SWK07 (Serian)</option>
139
+ <option value="SWK08">SWK08 (Kuching)</option>
140
+ <option value="SWK09">SWK09 (Other)</option>
141
+ </optgroup>
142
+ <optgroup label="Selangor">
143
+ <option value="SGR01">SGR01 (Gombak)</option>
144
+ <option value="SGR02">SGR02 (Kuala Selangor)</option>
145
+ <option value="SGR03">SGR03 (Klang)</option>
146
+ </optgroup>
147
+ <optgroup label="Terengganu">
148
+ <option value="TRG01">TRG01 (Kuala Terengganu)</option>
149
+ <option value="TRG02">TRG02 (Besut)</option>
150
+ <option value="TRG03">TRG03 (Hulu Terengganu)</option>
151
+ <option value="TRG04">TRG04 (Dungun)</option>
152
+ </optgroup>
153
+ <optgroup label="Wilayah Persekutuan">
154
+ <option value="WLY01">WLY01 (Kuala Lumpur)</option>
155
+ <option value="WLY02">WLY02 (Labuan)</option>
156
+ </optgroup>
157
+ </select>
158
+ </div>
159
+ </div>
160
+
161
+ <div class="col-md-6">
162
+ <input type="hidden" id="country" name="country" value="Malaysia">
163
+
164
+ <div class="mb-3">
165
+ <label for="phone" class="form-label">
166
+ <i class="fas fa-phone"></i> Phone Number
167
+ </label>
168
+ <input type="tel" class="form-control" id="phone" name="phone">
169
+ <div class="form-text">Optional</div>
170
+ </div>
171
+
172
+ <div class="mb-3">
173
+ <label for="email" class="form-label">
174
+ <i class="fas fa-envelope"></i> Email Address
175
+ </label>
176
+ <input type="email" class="form-control" id="email" name="email">
177
+ <div class="form-text">Optional</div>
178
+ </div>
179
+
180
+ <div class="mb-3">
181
+ <label for="capacity" class="form-label">
182
+ <i class="fas fa-users"></i> Capacity
183
+ </label>
184
+ <input type="number" class="form-control" id="capacity" name="capacity" min="1">
185
+ <div class="form-text">Optional - Enter the maximum capacity of the mosque</div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="d-grid gap-2 mt-4">
191
+ <button type="submit" class="btn btn-primary">
192
+ <i class="fas fa-plus"></i> Add Mosque
193
+ </button>
194
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
195
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
196
+ </a>
197
+ </div>
198
+ </form>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ {% endblock %}
main/templates/admin/add_staff.html ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add New Staff{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Add New Staff</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-user-plus"></i> Add New Staff</h2>
15
+ <p class="text-muted mb-0">Create a new staff account and assign to a mosque</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-6">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-user-tie"></i> Staff Details</h4>
25
+ </div>
26
+ <div class="card-body p-4">
27
+ <form method="POST" id="staffForm" onsubmit="return validatePassword()">
28
+ <div class="mb-3">
29
+ <label for="name" class="form-label">
30
+ <i class="fas fa-user"></i> Full Name
31
+ </label>
32
+ <input type="text" class="form-control" id="name" name="name" required>
33
+ </div>
34
+
35
+ <div class="mb-3">
36
+ <label for="email" class="form-label">
37
+ <i class="fas fa-envelope"></i> Email Address
38
+ </label>
39
+ <input type="email" class="form-control" id="email" name="email" required>
40
+ </div>
41
+
42
+ <div class="mb-3">
43
+ <label for="password" class="form-label">
44
+ <i class="fas fa-lock"></i> Password
45
+ </label>
46
+ <input type="password" class="form-control" id="password" name="password"
47
+ pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
48
+ title="Password must be at least 8 characters long and include at least one number and one special character"
49
+ required>
50
+ <div class="form-text">
51
+ Password requirements:
52
+ <ul class="mb-0">
53
+ <li>At least 8 characters long</li>
54
+ <li>Must contain at least one number</li>
55
+ <li>Must contain at least one special character (@$!%*#?&)</li>
56
+ </ul>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="mb-4">
61
+ <label for="mosque_id" class="form-label">
62
+ <i class="fas fa-mosque"></i> Assign to Mosque
63
+ </label>
64
+ <select class="form-select" id="mosque_id" name="mosque_id" required>
65
+ <option value="" disabled selected>Select a mosque</option>
66
+ {% for mosque in mosques %}
67
+ <option value="{{ mosque.id }}">{{ mosque.name }} ({{ mosque.city }}, {{ mosque.state }})</option>
68
+ {% endfor %}
69
+ </select>
70
+ </div>
71
+
72
+ <div class="d-grid gap-2">
73
+ <button type="submit" class="btn btn-primary">
74
+ <i class="fas fa-plus"></i> Create Staff Account
75
+ </button>
76
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
77
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
78
+ </a>
79
+ </div>
80
+ </form>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <script>
87
+ function validatePassword() {
88
+ const password = document.getElementById('password');
89
+ const pattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
90
+
91
+ if (!pattern.test(password.value)) {
92
+ password.setCustomValidity('Password must meet all requirements');
93
+ return false;
94
+ } else {
95
+ password.setCustomValidity('');
96
+ return true;
97
+ }
98
+ }
99
+
100
+ document.getElementById('password').addEventListener('input', validatePassword);
101
+ </script>
102
+ {% endblock %}
main/templates/admin/dashboard.html ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Admin Dashboard{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item active">Admin Dashboard</li>
7
+ {% endblock %}
8
+
9
+ {% block content %}
10
+ <div class="dashboard-header mb-4">
11
+ <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
12
+ <div>
13
+ <h2 class="mb-1"><i class="fas fa-user-shield text-success"></i> Admin Dashboard</h2>
14
+ <p class="text-muted mb-md-0">Super Administrator Control Panel</p>
15
+ </div>
16
+ <div class="dropdown">
17
+ <button class="btn btn-primary dropdown-toggle w-md-auto" type="button" id="actionDropdown" data-bs-toggle="dropdown" aria-expanded="false">
18
+ <i class="fas fa-cog"></i> Quick Actions
19
+ </button>
20
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="actionDropdown">
21
+ <li>
22
+ <a class="dropdown-item" href="{{ url_for('admin.add_mosque') }}">
23
+ <i class="fas fa-mosque"></i> Add New Mosque
24
+ </a>
25
+ </li>
26
+ <li>
27
+ <a class="dropdown-item" href="{{ url_for('admin.add_admin') }}">
28
+ <i class="fas fa-user-shield"></i> Add New Admin
29
+ </a>
30
+ </li>
31
+ <li>
32
+ <a class="dropdown-item" href="{{ url_for('admin.add_staff') }}">
33
+ <i class="fas fa-user-tie"></i> Add New Staff
34
+ </a>
35
+ </li>
36
+ </ul>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="row g-4">
42
+ <!-- Mosques Card -->
43
+ <div class="col-md-6">
44
+ <div class="card dashboard-card border-success border-opacity-25">
45
+ <div class="card-header bg-success bg-opacity-10">
46
+ <div class="d-flex justify-content-between align-items-center">
47
+ <h4 class="mb-0"><i class="fas fa-mosque text-success"></i> Mosques</h4>
48
+ <span class="badge bg-success">{{ mosques|length }}</span>
49
+ </div>
50
+ </div>
51
+ <div class="card-body">
52
+ {% if mosques %}
53
+ <div class="mosque-list">
54
+ {% for mosque in mosques %}
55
+ <div class="mosque-item p-3 border-bottom">
56
+ <div class="row">
57
+ <div class="col-12 col-md-8 mb-3 mb-md-0">
58
+ <h6 class="mosque-name mb-2 text-success">{{ mosque.name }}</h6>
59
+ <div class="mosque-details">
60
+ <p class="text-muted mb-2">
61
+ <i class="fas fa-map-marker-alt"></i> {{ mosque.city }}, {{ mosque.state }}
62
+ </p>
63
+ {% if mosque.phone %}
64
+ <p class="text-muted mb-1 small">
65
+ <i class="fas fa-phone"></i> {{ mosque.phone }}
66
+ </p>
67
+ {% endif %}
68
+ {% if mosque.email %}
69
+ <p class="text-muted mb-1 small">
70
+ <i class="fas fa-envelope"></i> {{ mosque.email }}
71
+ </p>
72
+ {% endif %}
73
+ </div>
74
+ </div>
75
+ <div class="col-12 col-md-4">
76
+ <div class="d-flex gap-2 justify-content-start justify-content-md-end">
77
+ <a href="{{ url_for('admin.mosque_details', id=mosque.id) }}"
78
+ class="btn btn-outline-success btn-sm"
79
+ title="View Details">
80
+ <i class="fas fa-eye"></i>
81
+ </a>
82
+ <a href="{{ url_for('admin.edit_mosque', id=mosque.id) }}"
83
+ class="btn btn-outline-success btn-sm"
84
+ title="Edit Mosque">
85
+ <i class="fas fa-edit"></i>
86
+ </a>
87
+ <form action="{{ url_for('admin.delete_mosque', mosque_id=mosque.id) }}"
88
+ method="POST"
89
+ class="d-inline"
90
+ onsubmit="return confirm('Are you sure you want to delete this mosque? This will remove all prayer times, announcements, and unassign all administrators.');">
91
+ <button type="submit"
92
+ class="btn btn-outline-danger btn-sm"
93
+ title="Delete Mosque">
94
+ <i class="fas fa-trash"></i>
95
+ </button>
96
+ </form>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ {% endfor %}
102
+ </div>
103
+ {% else %}
104
+ <div class="empty-indicator text-center py-4">
105
+ <i class="fas fa-mosque fa-2x mb-3 text-success"></i>
106
+ <p class="text-muted">No mosques available.</p>
107
+ <div class="mt-3">
108
+ <a href="{{ url_for('admin.add_mosque') }}" class="btn btn-primary">
109
+ <i class="fas fa-plus me-1"></i> Add Mosque
110
+ </a>
111
+ </div>
112
+ </div>
113
+ {% endif %}
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Users Card -->
119
+ <div class="col-md-6">
120
+ <div class="card dashboard-card border-success border-opacity-25">
121
+ <div class="card-header bg-success bg-opacity-10">
122
+ <div class="d-flex justify-content-between align-items-center">
123
+ <h4 class="mb-0"><i class="fas fa-users text-success"></i> Users</h4>
124
+ <span class="badge bg-success">{{ users|length }}</span>
125
+ </div>
126
+ </div>
127
+ <div class="card-body">
128
+ {% if users %}
129
+ <div class="user-list">
130
+ {% for user_item in users %}
131
+ <div class="user-item p-3 border-bottom">
132
+ <div class="row">
133
+ <div class="col-12 col-md-8 mb-3 mb-md-0">
134
+ <div class="d-flex align-items-center gap-2 mb-2">
135
+ <h6 class="user-name mb-0 text-success">{{ user_item.name }}</h6>
136
+ <span class="badge {% if user_item.role == 'admin' %}bg-success{% else %}bg-primary{% endif %}">
137
+ {{ user_item.role }}
138
+ </span>
139
+ </div>
140
+ <div class="user-details">
141
+ <p class="text-muted mb-1">
142
+ <i class="fas fa-envelope"></i> {{ user_item.email }}
143
+ </p>
144
+ <p class="text-muted mb-0 small">
145
+ <i class="fas fa-mosque"></i>
146
+ {% if user_item.mosque_id %}
147
+ {{ mosque_dict[user_item.mosque_id] }}
148
+ {% else %}
149
+ No assigned mosque
150
+ {% endif %}
151
+ </p>
152
+ </div>
153
+ </div>
154
+ <div class="col-12 col-md-4">
155
+ <div class="d-flex gap-2 justify-content-start justify-content-md-end">
156
+ <a href="{{ url_for('admin.reset_admin_password', admin_id=user_item.id) }}"
157
+ class="btn btn-outline-success btn-sm"
158
+ title="Reset Password">
159
+ <i class="fas fa-key"></i>
160
+ </a>
161
+ <a href="{{ url_for('admin.reassign_mosque', admin_id=user_item.id) }}"
162
+ class="btn btn-outline-success btn-sm"
163
+ title="Reassign Mosque">
164
+ <i class="fas fa-exchange-alt"></i>
165
+ </a>
166
+ <form action="{{ url_for('admin.delete_admin', admin_id=user_item.id) }}"
167
+ method="POST"
168
+ class="d-inline"
169
+ onsubmit="return confirm('Are you sure you want to delete this user?');">
170
+ <button type="submit"
171
+ class="btn btn-outline-danger btn-sm"
172
+ title="Delete User">
173
+ <i class="fas fa-trash"></i>
174
+ </button>
175
+ </form>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ {% endfor %}
181
+ </div>
182
+ {% else %}
183
+ <div class="empty-indicator text-center py-4">
184
+ <i class="fas fa-users fa-2x mb-3 text-success"></i>
185
+ <p class="text-muted">No users available.</p>
186
+ <div class="mt-3">
187
+ <div class="d-flex gap-2">
188
+ <a href="{{ url_for('admin.add_admin') }}" class="btn btn-outline-success">
189
+ <i class="fas fa-user-shield"></i> Add New Admin
190
+ </a>
191
+ <a href="{{ url_for('admin.add_staff') }}" class="btn btn-outline-primary">
192
+ <i class="fas fa-user-tie"></i> Add New Staff
193
+ </a>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ {% endif %}
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ {% endblock %}
main/templates/admin/edit_mosque.html ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Edit Mosque{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Edit Mosque</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-edit"></i> Edit Mosque</h2>
15
+ <p class="text-muted mb-0">Update mosque information</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-8">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-info-circle"></i> Mosque Information</h4>
25
+ </div>
26
+ <div class="card-body">
27
+ <form method="POST">
28
+ <div class="row g-3">
29
+ <input type="hidden" id="country" name="country" value="Malaysia">
30
+ <div class="col-md-6">
31
+ <label for="name" class="form-label">Mosque Name</label>
32
+ <input type="text" class="form-control" id="name" name="name" value="{{ mosque.name }}" required>
33
+ </div>
34
+
35
+ <div class="col-md-6">
36
+ <label for="phone" class="form-label">Phone Number</label>
37
+ <input type="tel" class="form-control" id="phone" name="phone" value="{{ mosque.phone }}">
38
+ </div>
39
+
40
+ <div class="col-md-6">
41
+ <label for="email" class="form-label">Email</label>
42
+ <input type="email" class="form-control" id="email" name="email" value="{{ mosque.email }}">
43
+ </div>
44
+
45
+ <div class="col-md-6">
46
+ <label for="capacity" class="form-label">Capacity</label>
47
+ <input type="number" class="form-control" id="capacity" name="capacity" min="0" value="{{ mosque.capacity }}">
48
+ </div>
49
+
50
+ <div class="col-md-6">
51
+ <label for="state" class="form-label">State</label>
52
+ <select class="form-select" id="state" name="state" required>
53
+ <option value="" disabled>Select state</option>
54
+ {% for state in ['Johor', 'Kedah', 'Kelantan', 'Melaka', 'Negeri Sembilan', 'Pahang', 'Perak', 'Perlis', 'Pulau Pinang', 'Sabah', 'Sarawak', 'Selangor', 'Terengganu', 'Kuala Lumpur', 'Labuan', 'Putrajaya'] %}
55
+ <option value="{{ state }}" {% if mosque.state == state %}selected{% endif %}>{{ state }}</option>
56
+ {% endfor %}
57
+ </select>
58
+ </div>
59
+
60
+ <div class="col-md-6">
61
+ <label for="jakim_code" class="form-label">
62
+ JAKIM Prayer Time Zone
63
+ </label>
64
+ <select class="form-select" id="jakim_code" name="jakim_code" required>
65
+ <option value="" disabled>Select prayer time zone</option>
66
+ <optgroup label="Johor">
67
+ <option value="JHR01" {% if mosque.jakim_code == 'JHR01' %}selected{% endif %}>JHR01 (Pulau Aur dan Pulau Pemanggil)</option>
68
+ <option value="JHR02" {% if mosque.jakim_code == 'JHR02' %}selected{% endif %}>JHR02 (Johor Bahru, Kota Tinggi, Mersing, Kulai)</option>
69
+ <option value="JHR03" {% if mosque.jakim_code == 'JHR03' %}selected{% endif %}>JHR03 (Kluang, Pontian)</option>
70
+ <option value="JHR04" {% if mosque.jakim_code == 'JHR04' %}selected{% endif %}>JHR04 (Batu Pahat, Muar, Segamat, Gemas Johor, Tangkak)</option>
71
+ </optgroup>
72
+ <optgroup label="Kedah">
73
+ <option value="KDH01" {% if mosque.jakim_code == 'KDH01' %}selected{% endif %}>KDH01 (Kota Setar, Kubang Pasu, Pokok Sena)</option>
74
+ <option value="KDH02" {% if mosque.jakim_code == 'KDH02' %}selected{% endif %}>KDH02 (Kuala Muda, Yan, Pendang)</option>
75
+ <option value="KDH03" {% if mosque.jakim_code == 'KDH03' %}selected{% endif %}>KDH03 (Padang Terap, Sik)</option>
76
+ <option value="KDH04" {% if mosque.jakim_code == 'KDH04' %}selected{% endif %}>KDH04 (Baling)</option>
77
+ <option value="KDH05" {% if mosque.jakim_code == 'KDH05' %}selected{% endif %}>KDH05 (Bandar Baharu, Kulim)</option>
78
+ <option value="KDH06" {% if mosque.jakim_code == 'KDH06' %}selected{% endif %}>KDH06 (Langkawi)</option>
79
+ <option value="KDH07" {% if mosque.jakim_code == 'KDH07' %}selected{% endif %}>KDH07 (Puncak Gunung Jerai)</option>
80
+ </optgroup>
81
+ <optgroup label="Kelantan">
82
+ <option value="KTN01" {% if mosque.jakim_code == 'KTN01' %}selected{% endif %}>KTN01 (Bachok, Kota Bharu, Machang, Pasir Mas, dan lain-lain)</option>
83
+ <option value="KTN02" {% if mosque.jakim_code == 'KTN02' %}selected{% endif %}>KTN02 (Gua Musang, Jeli, Lojing)</option>
84
+ </optgroup>
85
+ <optgroup label="Melaka">
86
+ <option value="MLK01" {% if mosque.jakim_code == 'MLK01' %}selected{% endif %}>MLK01 (Melaka)</option>
87
+ </optgroup>
88
+ <optgroup label="Negeri Sembilan">
89
+ <option value="NGS01" {% if mosque.jakim_code == 'NGS01' %}selected{% endif %}>NGS01 (Tampin, Jempol)</option>
90
+ <option value="NGS02" {% if mosque.jakim_code == 'NGS02' %}selected{% endif %}>NGS02 (Jelebu, Kuala Pilah, Rembau)</option>
91
+ <option value="NGS03" {% if mosque.jakim_code == 'NGS03' %}selected{% endif %}>NGS03 (Port Dickson, Seremban)</option>
92
+ </optgroup>
93
+ <optgroup label="Pahang">
94
+ <option value="PHG01" {% if mosque.jakim_code == 'PHG01' %}selected{% endif %}>PHG01 (Pulau Tioman)</option>
95
+ <option value="PHG02" {% if mosque.jakim_code == 'PHG02' %}selected{% endif %}>PHG02 (Kuantan, Pekan, Muadzam Shah)</option>
96
+ <option value="PHG03" {% if mosque.jakim_code == 'PHG03' %}selected{% endif %}>PHG03 (Jerantut, Temerloh, Maran, dan lain-lain)</option>
97
+ <option value="PHG04" {% if mosque.jakim_code == 'PHG04' %}selected{% endif %}>PHG04 (Bentong, Lipis, Raub)</option>
98
+ <option value="PHG05" {% if mosque.jakim_code == 'PHG05' %}selected{% endif %}>PHG05 (Genting Sempah, Janda Baik, Bukit Tinggi)</option>
99
+ <option value="PHG06" {% if mosque.jakim_code == 'PHG06' %}selected{% endif %}>PHG06 (Cameron Highlands, Genting Highlands, Bukit Fraser)</option>
100
+ <option value="PHG07" {% if mosque.jakim_code == 'PHG07' %}selected{% endif %}>PHG07 (Rompin, Endau, Pontian)</option>
101
+ </optgroup>
102
+ <optgroup label="Perak">
103
+ <option value="PRK01" {% if mosque.jakim_code == 'PRK01' %}selected{% endif %}>PRK01 (Tapah, Slim River, Tanjung Malim)</option>
104
+ <option value="PRK02" {% if mosque.jakim_code == 'PRK02' %}selected{% endif %}>PRK02 (Kuala Kangsar, Sg. Siput, Ipoh, dan lain-lain)</option>
105
+ <option value="PRK03" {% if mosque.jakim_code == 'PRK03' %}selected{% endif %}>PRK03 (Lenggong, Pengkalan Hulu, Grik)</option>
106
+ <option value="PRK04" {% if mosque.jakim_code == 'PRK04' %}selected{% endif %}>PRK04 (Temengor, Belum)</option>
107
+ <option value="PRK05" {% if mosque.jakim_code == 'PRK05' %}selected{% endif %}>PRK05 (Kg Gajah, Teluk Intan, Bagan Datuk, dan lain-lain)</option>
108
+ <option value="PRK06" {% if mosque.jakim_code == 'PRK06' %}selected{% endif %}>PRK06 (Selama, Taiping, Bagan Serai, Parit Buntar)</option>
109
+ <option value="PRK07" {% if mosque.jakim_code == 'PRK07' %}selected{% endif %}>PRK07 (Bukit Larut)</option>
110
+ </optgroup>
111
+ <optgroup label="Perlis">
112
+ <option value="PLS01" {% if mosque.jakim_code == 'PLS01' %}selected{% endif %}>PLS01 (Perlis)</option>
113
+ </optgroup>
114
+ <optgroup label="Pulau Pinang">
115
+ <option value="PNG01" {% if mosque.jakim_code == 'PNG01' %}selected{% endif %}>PNG01 (Pulau Pinang)</option>
116
+ </optgroup>
117
+ <optgroup label="Sabah">
118
+ <option value="SBH01" {% if mosque.jakim_code == 'SBH01' %}selected{% endif %}>SBH01 (Sandakan)</option>
119
+ <option value="SBH02" {% if mosque.jakim_code == 'SBH02' %}selected{% endif %}>SBH02 (Beluran)</option>
120
+ <option value="SBH03" {% if mosque.jakim_code == 'SBH03' %}selected{% endif %}>SBH03 (Lahad Datu)</option>
121
+ <option value="SBH04" {% if mosque.jakim_code == 'SBH04' %}selected{% endif %}>SBH04 (Bandar Tawau)</option>
122
+ <option value="SBH05" {% if mosque.jakim_code == 'SBH05' %}selected{% endif %}>SBH05 (Kudat)</option>
123
+ <option value="SBH06" {% if mosque.jakim_code == 'SBH06' %}selected{% endif %}>SBH06 (Gunung Kinabalu)</option>
124
+ <option value="SBH07" {% if mosque.jakim_code == 'SBH07' %}selected{% endif %}>SBH07 (Kota Kinabalu)</option>
125
+ <option value="SBH08" {% if mosque.jakim_code == 'SBH08' %}selected{% endif %}>SBH08 (Pensiangan)</option>
126
+ <option value="SBH09" {% if mosque.jakim_code == 'SBH09' %}selected{% endif %}>SBH09 (Beaufort)</option>
127
+ </optgroup>
128
+ <optgroup label="Sarawak">
129
+ <option value="SWK01" {% if mosque.jakim_code == 'SWK01' %}selected{% endif %}>SWK01 (Limbang)</option>
130
+ <option value="SWK02" {% if mosque.jakim_code == 'SWK02' %}selected{% endif %}>SWK02 (Miri)</option>
131
+ <option value="SWK03" {% if mosque.jakim_code == 'SWK03' %}selected{% endif %}>SWK03 (Pandan)</option>
132
+ <option value="SWK04" {% if mosque.jakim_code == 'SWK04' %}selected{% endif %}>SWK04 (Sibu)</option>
133
+ <option value="SWK05" {% if mosque.jakim_code == 'SWK05' %}selected{% endif %}>SWK05 (Sarikei)</option>
134
+ <option value="SWK06" {% if mosque.jakim_code == 'SWK06' %}selected{% endif %}>SWK06 (Lubok Antu)</option>
135
+ <option value="SWK07" {% if mosque.jakim_code == 'SWK07' %}selected{% endif %}>SWK07 (Serian)</option>
136
+ <option value="SWK08" {% if mosque.jakim_code == 'SWK08' %}selected{% endif %}>SWK08 (Kuching)</option>
137
+ <option value="SWK09" {% if mosque.jakim_code == 'SWK09' %}selected{% endif %}>SWK09 (Other)</option>
138
+ </optgroup>
139
+ <optgroup label="Selangor">
140
+ <option value="SGR01" {% if mosque.jakim_code == 'SGR01' %}selected{% endif %}>SGR01 (Gombak)</option>
141
+ <option value="SGR02" {% if mosque.jakim_code == 'SGR02' %}selected{% endif %}>SGR02 (Kuala Selangor)</option>
142
+ <option value="SGR03" {% if mosque.jakim_code == 'SGR03' %}selected{% endif %}>SGR03 (Klang)</option>
143
+ </optgroup>
144
+ <optgroup label="Terengganu">
145
+ <option value="TRG01" {% if mosque.jakim_code == 'TRG01' %}selected{% endif %}>TRG01 (Kuala Terengganu)</option>
146
+ <option value="TRG02" {% if mosque.jakim_code == 'TRG02' %}selected{% endif %}>TRG02 (Besut)</option>
147
+ <option value="TRG03" {% if mosque.jakim_code == 'TRG03' %}selected{% endif %}>TRG03 (Hulu Terengganu)</option>
148
+ <option value="TRG04" {% if mosque.jakim_code == 'TRG04' %}selected{% endif %}>TRG04 (Dungun)</option>
149
+ </optgroup>
150
+ <optgroup label="Wilayah Persekutuan">
151
+ <option value="WLY01" {% if mosque.jakim_code == 'WLY01' %}selected{% endif %}>WLY01 (Kuala Lumpur)</option>
152
+ <option value="WLY02" {% if mosque.jakim_code == 'WLY02' %}selected{% endif %}>WLY02 (Labuan)</option>
153
+ </optgroup>
154
+ </select>
155
+ </div>
156
+
157
+ <div class="col-md-6">
158
+ <label for="city" class="form-label">City</label>
159
+ <input type="text" class="form-control" id="city" name="city" value="{{ mosque.city }}" required>
160
+ </div>
161
+
162
+ <div class="col-12">
163
+ <label for="address" class="form-label">Address</label>
164
+ <textarea class="form-control" id="address" name="address" rows="3" required>{{ mosque.address }}</textarea>
165
+ </div>
166
+ </div>
167
+
168
+ <div class="d-grid gap-2 mt-4">
169
+ <button type="submit" class="btn btn-primary">
170
+ <i class="fas fa-save"></i> Update Mosque
171
+ </button>
172
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
173
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
174
+ </a>
175
+ </div>
176
+ </form>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ {% endblock %}
main/templates/admin/mosque_details.html ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Mosque Details{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ {% else %}
9
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
10
+ {% endif %}
11
+ <li class="breadcrumb-item active">Mosque Details</li>
12
+ {% endblock %}
13
+
14
+ {% block content %}
15
+ <div class="dashboard-header">
16
+ <div class="row align-items-center">
17
+ <div class="col">
18
+ <h2><i class="fas fa-mosque"></i> {{ mosque.name }}</h2>
19
+ <p class="text-muted mb-0">
20
+ <i class="fas fa-map-marker-alt"></i> {{ mosque.city }}, {{ mosque.state }}, {{ mosque.country }}
21
+ </p>
22
+ </div>
23
+ {% if user.is_authenticated and (user.mosque_id == mosque.id or user.role == 'superadmin') %}
24
+ <div class="col-md-4 text-end mt-3 mt-md-0">
25
+ <div class="dropdown">
26
+ <button class="btn btn-primary dropdown-toggle w-md-auto" type="button" id="actionDropdown" data-bs-toggle="dropdown" aria-expanded="false">
27
+ <i class="fas fa-cog"></i> Manage Mosque
28
+ </button>
29
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="actionDropdown">
30
+ <li>
31
+ <a class="dropdown-item" href="{{ url_for('mosque.prayer_times', mosque_id=mosque.id) }}">
32
+ <i class="fas fa-clock"></i> Manage Prayer Times
33
+ </a>
34
+ </li>
35
+ <li>
36
+ {% if user.role == 'superadmin' %}
37
+ <a class="dropdown-item" href="{{ url_for('admin.mosque_announcements', mosque_id=mosque.id) }}">
38
+ {% else %}
39
+ <a class="dropdown-item" href="{{ url_for('mosque.announcements') }}">
40
+ {% endif %}
41
+ <i class="fas fa-bullhorn"></i> Manage Announcements
42
+ </a>
43
+ </li>
44
+ <li>
45
+ <a class="dropdown-item" href="{{ url_for('inventory.inventory_list', mosque_id=mosque.id) }}">
46
+ <i class="fas fa-boxes"></i> Manage Inventory
47
+ </a>
48
+ </li>
49
+ <li>
50
+ <a class="dropdown-item" href="{{ url_for('finance.finance_list', mosque_id=mosque.id) }}">
51
+ <i class="fas fa-money-bill-wave"></i> Manage Finance
52
+ </a>
53
+ </li>
54
+ </ul>
55
+ </div>
56
+ </div>
57
+ {% endif %}
58
+ </div>
59
+ </div>
60
+
61
+ <div class="row g-4">
62
+ <!-- Mosque Information -->
63
+ <div class="col-md-4">
64
+ <div class="card dashboard-card border-success border-opacity-25">
65
+ <div class="card-header bg-success bg-opacity-10">
66
+ <div class="d-flex justify-content-between align-items-center">
67
+ <h4 class="mb-0"><i class="fas fa-info-circle text-success"></i> Mosque Information</h4>
68
+ </div>
69
+ </div>
70
+ <div class="card-body">
71
+ <div class="mosque-info-list">
72
+ <div class="info-item">
73
+ <i class="fas fa-map-marked-alt"></i>
74
+ <div>
75
+ <label>Address</label>
76
+ <span>{{ mosque.address }}</span>
77
+ </div>
78
+ </div>
79
+ {% if mosque.phone %}
80
+ <div class="info-item">
81
+ <i class="fas fa-phone"></i>
82
+ <div>
83
+ <label>Phone</label>
84
+ <span>{{ mosque.phone }}</span>
85
+ </div>
86
+ </div>
87
+ {% endif %}
88
+ {% if mosque.email %}
89
+ <div class="info-item">
90
+ <i class="fas fa-envelope"></i>
91
+ <div>
92
+ <label>Email</label>
93
+ <span>{{ mosque.email }}</span>
94
+ </div>
95
+ </div>
96
+ {% endif %}
97
+ {% if mosque.capacity %}
98
+ <div class="info-item">
99
+ <i class="fas fa-users"></i>
100
+ <div>
101
+ <label>Capacity</label>
102
+ <span>{{ mosque.capacity }} people</span>
103
+ </div>
104
+ </div>
105
+ {% endif %}
106
+ {% if user.is_authenticated and (user.role == 'superadmin' or user.mosque_id == mosque.id) %}
107
+ <hr>
108
+ <div class="info-item">
109
+ <i class="fas fa-user-shield"></i>
110
+ <div>
111
+ <label>Assigned Admins ({{ assigned_admins|length }})</label>
112
+ {% if assigned_admins %}
113
+ <div class="assigned-admins-list mt-2">
114
+ {% for admin in assigned_admins|sort(attribute='id', reverse=true) %}
115
+ <div class="assigned-admin-item mb-2">
116
+ <span class="admin-name small">{{ admin.name }}</span>
117
+ <br>
118
+ <small class="text-muted">{{ admin.email }}</small>
119
+ </div>
120
+ {% endfor %}
121
+ </div>
122
+ {% else %}
123
+ <span class="text-muted">No administrators assigned</span>
124
+ {% endif %}
125
+ </div>
126
+ </div>
127
+
128
+ <div class="info-item mt-3">
129
+ <i class="fas fa-user-tie"></i>
130
+ <div>
131
+ <label>Staff Members ({{ staff_members|length }})</label>
132
+ {% if staff_members %}
133
+ <div class="assigned-admins-list mt-2">
134
+ {% for staff in staff_members|sort(attribute='id', reverse=true) %}
135
+ <div class="assigned-admin-item mb-2">
136
+ <span class="admin-name">{{ staff.name }}</span>
137
+ <br>
138
+ <small class="text-muted">{{ staff.email }}</small>
139
+ </div>
140
+ {% endfor %}
141
+ </div>
142
+ {% else %}
143
+ <span class="text-muted">No staff members assigned</span>
144
+ {% endif %}
145
+ </div>
146
+ </div>
147
+ {% endif %}
148
+ </div>
149
+ </div>
150
+ </div>
151
+ <div class="mt-3">
152
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
153
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
154
+ </a>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Prayer Times Card -->
159
+ <div class="col-md-4">
160
+ <div class="card dashboard-card border-success border-opacity-25">
161
+ <div class="card-header bg-success bg-opacity-10">
162
+ <div class="d-flex justify-content-between align-items-center">
163
+ <h4 class="mb-0">
164
+ <i class="fas fa-clock text-success"></i> Prayer Times for <span id="todayDate">Today</span>
165
+ </h4>
166
+ <span class="badge bg-success">{{ prayer_times|length }}</span>
167
+ </div>
168
+ </div>
169
+ <div class="card-body">
170
+ {% if prayer_times %}
171
+ <div class="prayer-times-list">
172
+ {% for prayer in prayer_times|sort(attribute='order') %}
173
+ <div class="prayer-time-item">
174
+ <div class="prayer-name">{{ prayer.prayer_name }}</div>
175
+ <div class="prayer-time">{{ prayer.time.strftime('%I:%M %p') }}</div>
176
+ </div>
177
+ {% endfor %}
178
+ </div>
179
+ {% else %}
180
+ <div class="empty-indicator">
181
+ <i class="fas fa-calendar-day fa-2x"></i>
182
+ <p>No prayer times scheduled for today</p>
183
+ </div>
184
+ {% endif %}
185
+ <div class="text-end mt-3">
186
+ <a href="{{ url_for('mosque.prayer_times', mosque_id=mosque.id) }}" class="text-success text-decoration-none">
187
+ View Prayer Times Schedule <i class="fas fa-chevron-right"></i>
188
+ </a>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Announcements -->
195
+ <div class="col-md-4">
196
+ <div class="card dashboard-card border-success border-opacity-25">
197
+ <div class="card-header bg-success bg-opacity-10">
198
+ <div class="d-flex justify-content-between align-items-center">
199
+ <h4 class="mb-0"><i class="fas fa-bullhorn text-success"></i> Active Announcements</h4>
200
+ <span class="badge bg-success">{{ announcements|length }}</span>
201
+ </div>
202
+ </div>
203
+ <div class="card-body">
204
+ {% if announcements %}
205
+ <div class="announcements-list">
206
+ {# First show non-expired announcements #}
207
+ {% for announcement in announcements|sort(attribute='end_time', reverse=true)|sort(attribute='end_date', reverse=true)|sort(attribute='is_urgent', reverse=true) %}
208
+ {% set is_expired = announcement.end_date < now.date() or (announcement.end_date == now.date() and announcement.end_time < now.time()) %}
209
+ {% if not is_expired %}
210
+ <div class="announcement-item mb-2">
211
+ <div class="d-flex justify-content-between align-items-start">
212
+ <div>
213
+ <h5 class="announcement-title mb-1">
214
+ {% if announcement.is_urgent %}
215
+ <i class="fas fa-exclamation-circle text-danger" title="Urgent Announcement"></i>
216
+ {% endif %}
217
+ {{ announcement.title }}
218
+ </h5>
219
+ <p class="announcement-content mb-2">{{ announcement.content }}</p>
220
+ <p class="text-muted small mb-0">
221
+ <i class="fas fa-calendar"></i>
222
+ {{ announcement.start_date.strftime('%d %b %Y') }} {{ announcement.start_time.strftime('%I:%M %p') }} -
223
+ {{ announcement.end_date.strftime('%d %b %Y') }} {{ announcement.end_time.strftime('%I:%M %p') }}
224
+ </p>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ {% endif %}
229
+ {% endfor %}
230
+
231
+ {# Then show expired announcements #}
232
+ {% for announcement in announcements|sort(attribute='end_time', reverse=true)|sort(attribute='end_date', reverse=true)|sort(attribute='is_urgent', reverse=true) %}
233
+ {% set is_expired = announcement.end_date < now.date() or (announcement.end_date == now.date() and announcement.end_time < now.time()) %}
234
+ {% if is_expired %}
235
+ <div class="announcement-item mb-2 opacity-75">
236
+ <div class="d-flex justify-content-between align-items-start">
237
+ <div>
238
+ <h5 class="announcement-title mb-1">
239
+ {% if announcement.is_urgent %}
240
+ <i class="fas fa-exclamation-circle text-danger" title="Urgent Announcement"></i>
241
+ {% endif %}
242
+ {{ announcement.title }}
243
+ <span class="badge bg-secondary" title="Past Announcement">Expired</span>
244
+ </h5>
245
+ <p class="announcement-content mb-2">{{ announcement.content }}</p>
246
+ <p class="text-muted small mb-0">
247
+ <i class="fas fa-calendar"></i>
248
+ {{ announcement.start_date.strftime('%d %b %Y') }} {{ announcement.start_time.strftime('%I:%M %p') }} -
249
+ {{ announcement.end_date.strftime('%d %b %Y') }} {{ announcement.end_time.strftime('%I:%M %p') }}
250
+ </p>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ {% endif %}
255
+ {% endfor %}
256
+ </div>
257
+ {% else %}
258
+ <div class="empty-indicator">
259
+ <i class="fas fa-bullhorn fa-2x mb-3"></i>
260
+ <p>No announcements available</p>
261
+ </div>
262
+ {% endif %}
263
+ <div class="text-end mt-3">
264
+ {% if user.role == 'superadmin' %}
265
+ <a href="{{ url_for('admin.mosque_announcements', mosque_id=mosque.id) }}" class="text-success text-decoration-none">
266
+ {% else %}
267
+ <a href="{{ url_for('mosque.announcements') }}" class="text-success text-decoration-none">
268
+ {% endif %}
269
+ View All Announcements <i class="fas fa-chevron-right"></i>
270
+ </a>
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ {% endblock %}
277
+
278
+ {% block scripts %}
279
+ <script>
280
+ // Format today's date
281
+ const today = new Date();
282
+ const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
283
+ document.getElementById('todayDate').textContent = today.toLocaleDateString('en-US', options);
284
+ </script>
285
+ {% endblock %}
main/templates/admin/reassign_mosque.html ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Reassign Mosque{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Reassign Mosque</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-exchange-alt"></i> Reassign Mosque</h2>
15
+ <p class="text-muted mb-0">Reassign {{ admin.name }} to a different mosque</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-6">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-mosque"></i> Select Mosque</h4>
25
+ </div>
26
+ <div class="card-body">
27
+ <form method="POST">
28
+ <div class="mb-4">
29
+ <label for="mosque_id" class="form-label">Mosque</label>
30
+ <select class="form-select" id="mosque_id" name="mosque_id" required>
31
+ <option value="" disabled selected>Select a mosque</option>
32
+ {% for mosque in mosques %}
33
+ <option value="{{ mosque.id }}" {% if admin.mosque_id == mosque.id %}selected{% endif %}>
34
+ {{ mosque.name }} ({{ mosque.city }}, {{ mosque.state }})
35
+ </option>
36
+ {% endfor %}
37
+ </select>
38
+ </div>
39
+
40
+ <div class="d-grid gap-2">
41
+ <button type="submit" class="btn btn-primary">
42
+ <i class="fas fa-save"></i> Save Assignment
43
+ </button>
44
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
45
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
46
+ </a>
47
+ </div>
48
+ </form>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ {% endblock %}
main/templates/admin/reset_password.html ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Reset Admin Password{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Reset Admin Password</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-key"></i> Reset Administrator Password</h2>
15
+ <p class="text-muted mb-0">Reset password for {{ admin.name }}</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-6">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-lock"></i> New Password</h4>
25
+ </div>
26
+ <div class="card-body">
27
+ <form method="POST">
28
+ <div class="mb-3">
29
+ <label for="new_password" class="form-label">New Password</label>
30
+ <input type="password" class="form-control" id="new_password" name="new_password" required>
31
+ <div class="form-text">
32
+ Password must be at least 8 characters long
33
+ </div>
34
+ </div>
35
+
36
+ <div class="mb-4">
37
+ <label for="confirm_password" class="form-label">Confirm Password</label>
38
+ <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
39
+ </div>
40
+
41
+ <div class="d-grid gap-2">
42
+ <button type="submit" class="btn btn-primary">
43
+ <i class="fas fa-save"></i> Reset Password
44
+ </button>
45
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
46
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
47
+ </a>
48
+ </div>
49
+ </form>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ {% endblock %}
main/templates/auth/change_password.html ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Change Password{% endblock %}
4
+
5
+ {% block styles %}
6
+ <style>
7
+ .password-field {
8
+ position: relative;
9
+ }
10
+ .password-toggle {
11
+ position: absolute;
12
+ right: 10px;
13
+ top: 50%;
14
+ transform: translateY(-50%);
15
+ border: none;
16
+ background: none;
17
+ color: #6c757d;
18
+ cursor: pointer;
19
+ padding: 0;
20
+ }
21
+ .password-toggle:hover {
22
+ color: #198754;
23
+ }
24
+ </style>
25
+ {% endblock %}
26
+
27
+ {% block breadcrumbs %}
28
+ {% if user.role == 'superadmin' %}
29
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
30
+ {% else %}
31
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
32
+ {% endif %}
33
+ <li class="breadcrumb-item active">Change Password</li>
34
+ {% endblock %}
35
+
36
+ {% block content %}
37
+ <div class="dashboard-header">
38
+ <div class="row align-items-center">
39
+ <div class="col">
40
+ <h2><i class="fas fa-lock"></i> Change Password</h2>
41
+ <p class="text-muted mb-0">Update your account password</p>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="row justify-content-center">
47
+ <div class="col-md-6">
48
+ <div class="card dashboard-card">
49
+ <div class="card-header">
50
+ <h4><i class="fas fa-key"></i> Password Details</h4>
51
+ </div>
52
+ <div class="card-body p-4">
53
+ <form method="POST" id="changePasswordForm" onsubmit="return validatePassword()">
54
+ <div class="mb-3">
55
+ <label for="current_password" class="form-label">
56
+ <i class="fas fa-lock"></i> Current Password
57
+ </label>
58
+ <div class="password-field">
59
+ <input type="password" class="form-control" id="current_password" name="current_password" required>
60
+ <button type="button" class="password-toggle" onclick="togglePassword('current_password')" tabindex="-1">
61
+ <i class="fas fa-eye"></i>
62
+ </button>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="mb-3">
67
+ <label for="new_password" class="form-label">
68
+ <i class="fas fa-key"></i> New Password
69
+ </label>
70
+ <div class="password-field">
71
+ <input type="password" class="form-control" id="new_password" name="new_password"
72
+ pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
73
+ title="Password must be at least 8 characters long and include at least one number and one special character"
74
+ required>
75
+ <button type="button" class="password-toggle" onclick="togglePassword('new_password')" tabindex="-1">
76
+ <i class="fas fa-eye"></i>
77
+ </button>
78
+ </div>
79
+ <div class="form-text">
80
+ Password requirements:
81
+ <ul class="mb-0">
82
+ <li>At least 8 characters long</li>
83
+ <li>Must contain at least one number</li>
84
+ <li>Must contain at least one special character (@$!%*#?&)</li>
85
+ </ul>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="mb-4">
90
+ <label for="confirm_password" class="form-label">
91
+ <i class="fas fa-lock"></i> Confirm New Password
92
+ </label>
93
+ <div class="password-field">
94
+ <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
95
+ <button type="button" class="password-toggle" onclick="togglePassword('confirm_password')" tabindex="-1">
96
+ <i class="fas fa-eye"></i>
97
+ </button>
98
+ </div>
99
+ <div id="password-match" class="form-text text-danger d-none">
100
+ Passwords do not match
101
+ </div>
102
+ </div>
103
+
104
+ <div class="d-grid gap-2">
105
+ <button type="submit" class="btn btn-primary">
106
+ <i class="fas fa-save"></i> Update Password
107
+ </button>
108
+ {% if user.role == 'superadmin' %}
109
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
110
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
111
+ </a>
112
+ {% else %}
113
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
114
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
115
+ </a>
116
+ {% endif %}
117
+ </div>
118
+ </form>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <script>
125
+ function togglePassword(fieldId) {
126
+ const field = document.getElementById(fieldId);
127
+ const button = field.nextElementSibling;
128
+ const icon = button.querySelector('i');
129
+
130
+ if (field.type === 'password') {
131
+ field.type = 'text';
132
+ icon.classList.remove('fa-eye');
133
+ icon.classList.add('fa-eye-slash');
134
+ } else {
135
+ field.type = 'password';
136
+ icon.classList.remove('fa-eye-slash');
137
+ icon.classList.add('fa-eye');
138
+ }
139
+ }
140
+
141
+ function validatePassword() {
142
+ const newPassword = document.getElementById('new_password');
143
+ const confirmPassword = document.getElementById('confirm_password');
144
+ const passwordMatch = document.getElementById('password-match');
145
+
146
+ if (newPassword.value !== confirmPassword.value) {
147
+ passwordMatch.classList.remove('d-none');
148
+ confirmPassword.setCustomValidity('Passwords do not match');
149
+ return false;
150
+ } else {
151
+ passwordMatch.classList.add('d-none');
152
+ confirmPassword.setCustomValidity('');
153
+ return true;
154
+ }
155
+ }
156
+
157
+ document.getElementById('confirm_password').addEventListener('input', validatePassword);
158
+ document.getElementById('new_password').addEventListener('input', validatePassword);
159
+ </script>
160
+ {% endblock %}
main/templates/auth/forgot_password.html ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Reset Password{% endblock %}
4
+
5
+ {% set hide_breadcrumbs = true %}
6
+
7
+ {% block styles %}
8
+ <style>
9
+ .password-field {
10
+ position: relative;
11
+ }
12
+ .password-toggle {
13
+ position: absolute;
14
+ right: 10px;
15
+ top: 50%;
16
+ transform: translateY(-50%);
17
+ border: none;
18
+ background: none;
19
+ color: #6c757d;
20
+ cursor: pointer;
21
+ padding: 0;
22
+ }
23
+ .password-toggle:hover {
24
+ color: #198754;
25
+ }
26
+ </style>
27
+ {% endblock %}
28
+
29
+ {% block content %}
30
+ <div class="container">
31
+ <div class="row justify-content-center align-items-center min-vh-75">
32
+ <div class="col-md-6 col-lg-5">
33
+ <div class="text-center mb-4">
34
+ <h1 class="display-6 fw-bold" style="color: var(--primary-color)">
35
+ <i class="fas fa-mosque"></i> Mosqku
36
+ </h1>
37
+ <p class="text-muted">Reset your password using your recovery key</p>
38
+ </div>
39
+
40
+ <div class="card dashboard-card">
41
+ <div class="card-body p-4">
42
+ <form method="POST" id="resetPasswordForm" onsubmit="return validatePassword()">
43
+ <div class="mb-3">
44
+ <label for="email" class="form-label">
45
+ <i class="fas fa-envelope"></i> Email Address
46
+ </label>
47
+ <input type="email" class="form-control" id="email" name="email" required>
48
+ </div>
49
+
50
+ <div class="mb-3">
51
+ <label for="recovery_key" class="form-label">
52
+ <i class="fas fa-shield-alt"></i> Recovery Key
53
+ </label>
54
+ <input type="text" class="form-control" id="recovery_key" name="recovery_key"
55
+ pattern="[A-Z0-9]{16}"
56
+ title="Recovery key should be 16 characters long and contain only uppercase letters and numbers"
57
+ required>
58
+ </div>
59
+
60
+ <div class="mb-3">
61
+ <label for="new_password" class="form-label">
62
+ <i class="fas fa-lock"></i> New Password
63
+ </label>
64
+ <div class="password-field">
65
+ <input type="password" class="form-control" id="new_password" name="new_password"
66
+ pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
67
+ title="Password must be at least 8 characters long and include at least one number and one special character"
68
+ required>
69
+ <button type="button" class="password-toggle" onclick="togglePassword('new_password')" tabindex="-1">
70
+ <i class="fas fa-eye"></i>
71
+ </button>
72
+ </div>
73
+ <div class="form-text">
74
+ Password requirements:
75
+ <ul class="mb-0">
76
+ <li>At least 8 characters long</li>
77
+ <li>Must contain at least one number</li>
78
+ <li>Must contain at least one special character (@$!%*#?&)</li>
79
+ </ul>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="mb-4">
84
+ <label for="confirm_password" class="form-label">
85
+ <i class="fas fa-lock"></i> Confirm New Password
86
+ </label>
87
+ <div class="password-field">
88
+ <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
89
+ <button type="button" class="password-toggle" onclick="togglePassword('confirm_password')" tabindex="-1">
90
+ <i class="fas fa-eye"></i>
91
+ </button>
92
+ </div>
93
+ <div id="password-match" class="form-text text-danger d-none">
94
+ Passwords do not match
95
+ </div>
96
+ </div>
97
+
98
+ <div class="d-grid gap-2">
99
+ <button type="submit" class="btn btn-primary">
100
+ <i class="fas fa-key"></i> Reset Password
101
+ </button>
102
+ <a href="{{ url_for('auth.login') }}" class="btn btn-outline-secondary">
103
+ <i class="fas fa-sign-in-alt"></i> Back to Login
104
+ </a>
105
+ </div>
106
+ </form>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <script>
114
+ function togglePassword(fieldId) {
115
+ const field = document.getElementById(fieldId);
116
+ const button = field.nextElementSibling;
117
+ const icon = button.querySelector('i');
118
+
119
+ if (field.type === 'password') {
120
+ field.type = 'text';
121
+ icon.classList.remove('fa-eye');
122
+ icon.classList.add('fa-eye-slash');
123
+ } else {
124
+ field.type = 'password';
125
+ icon.classList.remove('fa-eye-slash');
126
+ icon.classList.add('fa-eye');
127
+ }
128
+ }
129
+
130
+ function validatePassword() {
131
+ const password = document.getElementById('new_password');
132
+ const confirmPassword = document.getElementById('confirm_password');
133
+ const passwordMatch = document.getElementById('password-match');
134
+
135
+ if (password.value !== confirmPassword.value) {
136
+ passwordMatch.classList.remove('d-none');
137
+ confirmPassword.setCustomValidity('Passwords do not match');
138
+ return false;
139
+ } else {
140
+ passwordMatch.classList.add('d-none');
141
+ confirmPassword.setCustomValidity('');
142
+ return true;
143
+ }
144
+ }
145
+
146
+ document.getElementById('confirm_password').addEventListener('input', validatePassword);
147
+ document.getElementById('new_password').addEventListener('input', validatePassword);
148
+ </script>
149
+ {% endblock %}
main/templates/auth/login.html ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Login{% endblock %}
4
+
5
+ {% set hide_breadcrumbs = true %}
6
+
7
+ {% block styles %}
8
+ <style>
9
+ .password-field {
10
+ position: relative;
11
+ }
12
+ .password-toggle {
13
+ position: absolute;
14
+ right: 10px;
15
+ top: 50%;
16
+ transform: translateY(-50%);
17
+ border: none;
18
+ background: none;
19
+ color: #6c757d;
20
+ cursor: pointer;
21
+ padding: 0;
22
+ }
23
+ .password-toggle:hover {
24
+ color: #198754;
25
+ }
26
+ </style>
27
+ {% endblock %}
28
+
29
+ {% block content %}
30
+ <div class="container">
31
+ <div class="row justify-content-center align-items-center min-vh-75">
32
+ <div class="col-md-6 col-lg-5">
33
+ <div class="text-center mb-4">
34
+ <h1 class="display-6 fw-bold" style="color: var(--primary-color)">
35
+ <i class="fas fa-mosque"></i> Mosqku
36
+ </h1>
37
+ <p class="text-muted">Welcome back! Please login to continue.</p>
38
+ </div>
39
+
40
+ <div class="card dashboard-card">
41
+ <div class="card-body p-4">
42
+ <form method="POST">
43
+ <div class="mb-3">
44
+ <label for="email" class="form-label">
45
+ <i class="fas fa-envelope"></i> Email Address
46
+ </label>
47
+ <input type="email" class="form-control" id="email" name="email" required>
48
+ </div>
49
+
50
+ <div class="mb-4">
51
+ <label for="password" class="form-label">
52
+ <i class="fas fa-lock"></i> Password
53
+ </label>
54
+ <div class="password-field">
55
+ <input type="password" class="form-control" id="password" name="password" required>
56
+ <button type="button" class="password-toggle" onclick="togglePassword('password')" tabindex="-1">
57
+ <i class="fas fa-eye"></i>
58
+ </button>
59
+ </div>
60
+ <div class="text-end mt-1">
61
+ <a href="{{ url_for('auth.forgot_password') }}" class="text-success text-decoration-none small">
62
+ <i class="fas fa-key"></i> Forgot Password?
63
+ </a>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="d-grid gap-2">
68
+ <button type="submit" class="btn btn-primary">
69
+ <i class="fas fa-sign-in-alt"></i> Login
70
+ </button>
71
+ <a href="{{ url_for('auth.register') }}" class="btn btn-outline-secondary">
72
+ <i class="fas fa-user-plus"></i> Create Account
73
+ </a>
74
+ </div>
75
+ </form>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="text-center mt-4">
80
+ <a href="{{ url_for('mosque.home') }}" class="text-muted text-decoration-none">
81
+ <i class="fas fa-arrow-left"></i> Back to Home
82
+ </a>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <script>
89
+ function togglePassword(fieldId) {
90
+ const field = document.getElementById(fieldId);
91
+ const button = field.nextElementSibling;
92
+ const icon = button.querySelector('i');
93
+
94
+ if (field.type === 'password') {
95
+ field.type = 'text';
96
+ icon.classList.remove('fa-eye');
97
+ icon.classList.add('fa-eye-slash');
98
+ } else {
99
+ field.type = 'password';
100
+ icon.classList.remove('fa-eye-slash');
101
+ icon.classList.add('fa-eye');
102
+ }
103
+ }
104
+ </script>
105
+ {% endblock %}
main/templates/auth/register.html ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Register{% endblock %}
4
+
5
+ {% block styles %}
6
+ <style>
7
+ .password-field {
8
+ position: relative;
9
+ }
10
+ .password-toggle {
11
+ position: absolute;
12
+ right: 10px;
13
+ top: 50%;
14
+ transform: translateY(-50%);
15
+ border: none;
16
+ background: none;
17
+ color: #6c757d;
18
+ cursor: pointer;
19
+ padding: 0;
20
+ }
21
+ .password-toggle:hover {
22
+ color: #198754;
23
+ }
24
+ </style>
25
+ {% endblock %}
26
+
27
+ {% block content %}
28
+ <div class="container">
29
+ <div class="row justify-content-center align-items-center min-vh-75">
30
+ <div class="col-md-6 col-lg-5">
31
+ <div class="text-center mb-4">
32
+ <h1 class="display-6 fw-bold" style="color: var(--primary-color)">
33
+ <i class="fas fa-mosque"></i> Mosqku
34
+ </h1>
35
+ <p class="text-muted">Create your account to get started.</p>
36
+ </div>
37
+
38
+ <div class="card dashboard-card">
39
+ <div class="card-body p-4">
40
+ <form method="POST" id="registerForm" onsubmit="return validatePassword()">
41
+ <div class="mb-3">
42
+ <label for="name" class="form-label">
43
+ <i class="fas fa-user"></i> Full Name
44
+ </label>
45
+ <input type="text" class="form-control" id="name" name="name" required>
46
+ </div>
47
+
48
+ <div class="mb-3">
49
+ <label for="email" class="form-label">
50
+ <i class="fas fa-envelope"></i> Email Address
51
+ </label>
52
+ <input type="email" class="form-control" id="email" name="email" required>
53
+ </div>
54
+
55
+ <div class="mb-3">
56
+ <label for="password" class="form-label">
57
+ <i class="fas fa-lock"></i> Password
58
+ </label>
59
+ <div class="password-field">
60
+ <input type="password" class="form-control" id="password" name="password"
61
+ pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
62
+ title="Password must be at least 8 characters long and include at least one number and one special character"
63
+ required>
64
+ <button type="button" class="password-toggle" onclick="togglePassword('password')" tabindex="-1">
65
+ <i class="fas fa-eye"></i>
66
+ </button>
67
+ </div>
68
+ <div class="form-text">
69
+ Password requirements:
70
+ <ul class="mb-0">
71
+ <li>At least 8 characters long</li>
72
+ <li>Must contain at least one number</li>
73
+ <li>Must contain at least one special character (@$!%*#?&)</li>
74
+ </ul>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="mb-4">
79
+ <label for="confirm_password" class="form-label">
80
+ <i class="fas fa-lock"></i> Confirm Password
81
+ </label>
82
+ <div class="password-field">
83
+ <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
84
+ <button type="button" class="password-toggle" onclick="togglePassword('confirm_password')" tabindex="-1">
85
+ <i class="fas fa-eye"></i>
86
+ </button>
87
+ </div>
88
+ <div id="password-match" class="form-text text-danger d-none">
89
+ Passwords do not match
90
+ </div>
91
+ </div>
92
+
93
+ <div class="d-grid gap-2">
94
+ <button type="submit" class="btn btn-primary">
95
+ <i class="fas fa-user-plus"></i> Create Account
96
+ </button>
97
+ <a href="{{ url_for('auth.login') }}" class="btn btn-outline-secondary">
98
+ <i class="fas fa-sign-in-alt"></i> Login Instead
99
+ </a>
100
+ </div>
101
+ </form>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="text-center mt-4">
106
+ <a href="{{ url_for('mosque.home') }}" class="text-muted text-decoration-none">
107
+ <i class="fas fa-arrow-left"></i> Back to Home
108
+ </a>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <script>
115
+ function togglePassword(fieldId) {
116
+ const field = document.getElementById(fieldId);
117
+ const button = field.nextElementSibling;
118
+ const icon = button.querySelector('i');
119
+
120
+ if (field.type === 'password') {
121
+ field.type = 'text';
122
+ icon.classList.remove('fa-eye');
123
+ icon.classList.add('fa-eye-slash');
124
+ } else {
125
+ field.type = 'password';
126
+ icon.classList.remove('fa-eye-slash');
127
+ icon.classList.add('fa-eye');
128
+ }
129
+ }
130
+
131
+ function validatePassword() {
132
+ const password = document.getElementById('password');
133
+ const confirmPassword = document.getElementById('confirm_password');
134
+ const passwordMatch = document.getElementById('password-match');
135
+
136
+ if (password.value !== confirmPassword.value) {
137
+ passwordMatch.classList.remove('d-none');
138
+ confirmPassword.setCustomValidity('Passwords do not match');
139
+ return false;
140
+ } else {
141
+ passwordMatch.classList.add('d-none');
142
+ confirmPassword.setCustomValidity('');
143
+ return true;
144
+ }
145
+ }
146
+
147
+ document.getElementById('confirm_password').addEventListener('input', validatePassword);
148
+ </script>
149
+ {% endblock %}
main/templates/auth/view_recovery_key.html ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}View Recovery Key{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ {% elif user.role in ['admin', 'staff'] %}
9
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
10
+ {% endif %}
11
+ <li class="breadcrumb-item active">View Recovery Key</li>
12
+ {% endblock %}
13
+
14
+ {% block styles %}
15
+ <style>
16
+ .recovery-key-display {
17
+ margin: 1.5rem 0;
18
+ }
19
+ .recovery-key-display .input-group {
20
+ max-width: 100%;
21
+ }
22
+ .recovery-key-display input {
23
+ font-family: monospace;
24
+ letter-spacing: 1px;
25
+ font-size: 1.1rem;
26
+ }
27
+ .alert {
28
+ margin-bottom: 1.5rem;
29
+ }
30
+ .alert i {
31
+ margin-right: 0.5rem;
32
+ }
33
+ @media (max-width: 576px) {
34
+ .recovery-key-display input {
35
+ font-size: 0.9rem;
36
+ letter-spacing: 0.5px;
37
+ }
38
+ .card-body {
39
+ padding: 1rem !important;
40
+ }
41
+ .alert {
42
+ padding: 0.75rem;
43
+ font-size: 0.9rem;
44
+ }
45
+ }
46
+ </style>
47
+ {% endblock %}
48
+
49
+ {% block content %}
50
+ <div class="dashboard-header">
51
+ <div class="row align-items-center">
52
+ <div class="col">
53
+ <h2><i class="fas fa-shield-alt"></i> View Recovery Key</h2>
54
+ <p class="text-muted mb-0">Access your account recovery key</p>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="container">
60
+ <div class="row justify-content-center">
61
+ <div class="col-md-8 col-lg-6">
62
+ <div class="card dashboard-card">
63
+ <div class="card-header">
64
+ <h4 class="mb-0"><i class="fas fa-key"></i> Your Recovery Key</h4>
65
+ </div>
66
+ <div class="card-body">
67
+ <div class="alert alert-warning">
68
+ <i class="fas fa-exclamation-triangle"></i>
69
+ <strong>Important:</strong> Keep this key safe and secure. You'll need it to reset your password if you ever lose access to your account.
70
+ </div>
71
+
72
+ <div class="recovery-key-display">
73
+ <label class="form-label mb-2">Your Recovery Key:</label>
74
+ <div class="input-group">
75
+ <input type="text" class="form-control text-center"
76
+ value="{{ user.recovery_key }}" id="recoveryKey" readonly>
77
+ <button class="btn btn-outline-primary" type="button" onclick="copyRecoveryKey()">
78
+ <i class="fas fa-copy"></i> <span class="d-none d-sm-inline">Copy</span>
79
+ </button>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="alert alert-info">
84
+ <i class="fas fa-info-circle"></i>
85
+ <strong>Note:</strong> This key will change if you reset your password.
86
+ </div>
87
+
88
+ <div class="d-grid">
89
+ {% if user.role == 'superadmin' %}
90
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
91
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
92
+ </a>
93
+ {% elif user.role in ['admin', 'staff'] %}
94
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
95
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
96
+ </a>
97
+ {% endif %}
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <script>
106
+ function copyRecoveryKey() {
107
+ const recoveryKey = document.getElementById('recoveryKey');
108
+ recoveryKey.select();
109
+ document.execCommand('copy');
110
+
111
+ // Show feedback
112
+ const button = event.currentTarget;
113
+ const originalText = button.innerHTML;
114
+ button.innerHTML = '<i class="fas fa-check"></i> <span class="d-none d-sm-inline">Copied!</span>';
115
+ button.classList.remove('btn-outline-primary');
116
+ button.classList.add('btn-primary');
117
+
118
+ setTimeout(() => {
119
+ button.innerHTML = originalText;
120
+ button.classList.remove('btn-primary');
121
+ button.classList.add('btn-outline-primary');
122
+ }, 2000);
123
+ }
124
+ </script>
125
+ {% endblock %}
main/templates/base.html ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-100">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}{% endblock %} - Mosqku</title>
7
+ <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon/favicon.ico') }}">
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
11
+ <style>
12
+ @import url('https://db.onlinewebfonts.com/c/91365a119c448bf9da6d8f710e3bdda6?family=Nokia+Sans+S60+Regular');
13
+
14
+ @font-face {
15
+ font-family: "Nokia Sans S60 Regular";
16
+ src: url('https://db.onlinewebfonts.com/c/91365a119c448bf9da6d8f710e3bdda6?family=Nokia+Sans+S60+Regular') format('woff2');
17
+ }
18
+
19
+ * {
20
+ font-family: "Nokia Sans S60 Regular";
21
+ }
22
+
23
+ body {
24
+ display: flex;
25
+ flex-direction: column;
26
+ min-height: 100vh;
27
+ margin: 0;
28
+ padding-top: 56px; /* Default - Height of navbar */
29
+ position: relative;
30
+ overflow-x: hidden;
31
+ }
32
+
33
+ /* Background Mosque Silhouette */
34
+ body::before {
35
+ content: "\f678"; /* Font Awesome mosque icon unicode */
36
+ font-family: "Font Awesome 6 Free";
37
+ font-weight: 900;
38
+ position: fixed;
39
+ font-size: 80vh;
40
+ color: rgba(108, 117, 125, 0.03); /* Very light gray */
41
+ right: -10vh;
42
+ bottom: -10vh;
43
+ z-index: -1;
44
+ transform: rotate(-10deg);
45
+ pointer-events: none;
46
+ }
47
+
48
+ body::after {
49
+ content: "\f678";
50
+ font-family: "Font Awesome 6 Free";
51
+ font-weight: 900;
52
+ position: fixed;
53
+ font-size: 60vh;
54
+ color: rgba(108, 117, 125, 0.02);
55
+ left: -5vh;
56
+ top: 15vh;
57
+ z-index: -1;
58
+ transform: rotate(15deg);
59
+ pointer-events: none;
60
+ }
61
+
62
+ body.demo-mode {
63
+ padding-top: 96px; /* Height of navbar + demo banner */
64
+ }
65
+ .navbar {
66
+ position: fixed;
67
+ top: 0;
68
+ right: 0;
69
+ left: 0;
70
+ z-index: 1030;
71
+ }
72
+ body.demo-mode .navbar {
73
+ top: 40px; /* Height of demo banner */
74
+ }
75
+ .demo-banner {
76
+ position: fixed;
77
+ top: 0;
78
+ left: 0;
79
+ right: 0;
80
+ z-index: 1031;
81
+ height: 40px;
82
+ background-color: #2e7d32;
83
+ color: white;
84
+ cursor: pointer;
85
+ transition: background-color 0.3s;
86
+ }
87
+ .demo-banner:hover {
88
+ background-color: #1b5e20;
89
+ }
90
+ .demo-modal code {
91
+ background-color: #e8f5e9;
92
+ color: #2e7d32;
93
+ padding: 2px 4px;
94
+ border-radius: 4px;
95
+ }
96
+ .demo-modal .modal-header {
97
+ background-color: #2e7d32;
98
+ color: white;
99
+ }
100
+ .demo-modal .modal-body {
101
+ padding: 20px;
102
+ }
103
+ .demo-account {
104
+ background-color: #f5f5f5;
105
+ border-radius: 8px;
106
+ padding: 15px;
107
+ margin-bottom: 15px;
108
+ }
109
+ .demo-account h5 {
110
+ color: #2e7d32;
111
+ margin-bottom: 10px;
112
+ }
113
+ .main-content {
114
+ flex: 1 0 auto;
115
+ overflow-y: auto;
116
+ padding-bottom: 60px; /* Height of footer */
117
+ }
118
+ .footer {
119
+ position: fixed;
120
+ bottom: 0;
121
+ width: 100%;
122
+ height: 60px;
123
+ background-color: #343a40;
124
+ z-index: 1030;
125
+ }
126
+ .footer .container {
127
+ height: 100%;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ }
132
+ /* Flash message container */
133
+ .flash-container {
134
+ position: relative;
135
+ transition: all 0.5s ease;
136
+ overflow: hidden;
137
+ }
138
+
139
+ /* Individual flash message */
140
+ .alert {
141
+ margin-bottom: 1rem;
142
+ transition: all 0.5s ease;
143
+ opacity: 1;
144
+ }
145
+
146
+ /* Fading out state */
147
+ .alert.fade {
148
+ max-height: 0;
149
+ opacity: 0;
150
+ margin: 0;
151
+ padding-top: 0;
152
+ padding-bottom: 0;
153
+ }
154
+
155
+ /* Container collapse */
156
+ .flash-container:empty {
157
+ margin: 0;
158
+ padding: 0;
159
+ }
160
+ </style>
161
+ {% block styles %}{% endblock %}
162
+ </head>
163
+ <body {% if config.get('DEMO_MODE', False) %}class="demo-mode"{% endif %}>
164
+ {% if config.get('DEMO_MODE', False) %}
165
+ <!-- Demo Banner -->
166
+ <div class="demo-banner d-flex align-items-center justify-content-center" data-bs-toggle="modal" data-bs-target="#demoModal">
167
+ <i class="fas fa-info-circle me-2"></i>
168
+ Demo Mode Active - Read More
169
+ <i class="fas fa-chevron-right ms-2"></i>
170
+ </div>
171
+
172
+ <!-- Demo Modal -->
173
+ <div class="modal fade demo-modal" id="demoModal" tabindex="-1">
174
+ <div class="modal-dialog modal-lg">
175
+ <div class="modal-content">
176
+ <div class="modal-header">
177
+ <h5 class="modal-title">
178
+ <i class="fas fa-info-circle me-2"></i>
179
+ Demo Mode - Test Accounts
180
+ </h5>
181
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
182
+ </div>
183
+ <div class="modal-body">
184
+ <!-- Superadmin Section -->
185
+ <div class="demo-account">
186
+ <div class="d-flex align-items-center mb-3">
187
+ <i class="fas fa-user-shield fa-2x me-3 text-success"></i>
188
+ <div>
189
+ <h5 class="mb-1">Superadmin Account</h5>
190
+ <p class="mb-0 text-muted small">Full system access and management of all mosques</p>
191
+ </div>
192
+ </div>
193
+ <div class="bg-light p-3 rounded">
194
+ <div class="row">
195
+ <div class="col-md-6">
196
+ <p class="mb-2"><strong>Login Details:</strong></p>
197
+ <ul class="list-unstyled">
198
+ <li>Email: <code>[email protected]</code></li>
199
+ <li>Password: <code>Admin@123</code></li>
200
+ </ul>
201
+ </div>
202
+ <div class="col-md-6">
203
+ <p class="mb-2"><strong>Capabilities:</strong></p>
204
+ <ul class="small">
205
+ <li>Manage all mosques</li>
206
+ <li>Create/edit mosque admins</li>
207
+ <li>Full system configuration</li>
208
+ </ul>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <!-- Masjid Al-Salam Section -->
215
+ <div class="demo-account">
216
+ <div class="d-flex align-items-center mb-3">
217
+ <i class="fas fa-mosque fa-2x me-3 text-success"></i>
218
+ <div>
219
+ <h5 class="mb-1">Masjid As-Salam Accounts</h5>
220
+ <p class="mb-0 text-muted small">Management team for Masjid As-Salam</p>
221
+ </div>
222
+ </div>
223
+ <div class="bg-light p-3 rounded">
224
+ <div class="row">
225
+ <div class="col-md-6">
226
+ <p class="mb-2"><strong>Admin Account:</strong></p>
227
+ <ul class="list-unstyled mb-3">
228
+ <li>Email: <code>[email protected]</code></li>
229
+ <li>Password: <code>Demo@123</code></li>
230
+ </ul>
231
+ <p class="mb-2"><strong>Staff Accounts:</strong></p>
232
+ <ul class="list-unstyled">
233
+ <li>Staff 1: <code>[email protected]</code></li>
234
+ <li>Staff 2: <code>[email protected]</code></li>
235
+ <li>Password (all staff): <code>Demo@123</code></li>
236
+ </ul>
237
+ </div>
238
+ <div class="col-md-6">
239
+ <p class="mb-2"><strong>Capabilities:</strong></p>
240
+ <ul class="small">
241
+ <li>Manage mosque details</li>
242
+ <li>Control prayer times</li>
243
+ <li>Post announcements</li>
244
+ <li>Manage inventory & finance</li>
245
+ </ul>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <!-- Masjid An-Nur Section -->
252
+ <div class="demo-account">
253
+ <div class="d-flex align-items-center mb-3">
254
+ <i class="fas fa-mosque fa-2x me-3 text-success"></i>
255
+ <div>
256
+ <h5 class="mb-1">Masjid An-Nur Accounts</h5>
257
+ <p class="mb-0 text-muted small">Management team for Masjid An-Nur</p>
258
+ </div>
259
+ </div>
260
+ <div class="bg-light p-3 rounded">
261
+ <div class="row">
262
+ <div class="col-md-6">
263
+ <p class="mb-2"><strong>Admin Account:</strong></p>
264
+ <ul class="list-unstyled mb-3">
265
+ <li>Email: <code>[email protected]</code></li>
266
+ <li>Password: <code>Demo@123</code></li>
267
+ </ul>
268
+ <p class="mb-2"><strong>Staff Accounts:</strong></p>
269
+ <ul class="list-unstyled">
270
+ <li>Staff 1: <code>[email protected]</code></li>
271
+ <li>Staff 2: <code>[email protected]</code></li>
272
+ <li>Password (all staff): <code>Demo@123</code></li>
273
+ </ul>
274
+ </div>
275
+ <div class="col-md-6">
276
+ <p class="mb-2"><strong>Capabilities:</strong></p>
277
+ <ul class="small">
278
+ <li>Manage mosque details</li>
279
+ <li>Control prayer times</li>
280
+ <li>Post announcements</li>
281
+ <li>Manage inventory & finance</li>
282
+ </ul>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ <!-- Masjid Al-Hidayah Section -->
289
+ <div class="demo-account">
290
+ <div class="d-flex align-items-center mb-3">
291
+ <i class="fas fa-mosque fa-2x me-3 text-success"></i>
292
+ <div>
293
+ <h5 class="mb-1">Masjid Al-Hidayah Accounts</h5>
294
+ <p class="mb-0 text-muted small">Management team for Masjid Al-Hidayah</p>
295
+ </div>
296
+ </div>
297
+ <div class="bg-light p-3 rounded">
298
+ <div class="row">
299
+ <div class="col-md-6">
300
+ <p class="mb-2"><strong>Admin Account:</strong></p>
301
+ <ul class="list-unstyled mb-3">
302
+ <li>Email: <code>[email protected]</code></li>
303
+ <li>Password: <code>Demo@123</code></li>
304
+ </ul>
305
+ <p class="mb-2"><strong>Staff Accounts:</strong></p>
306
+ <ul class="list-unstyled">
307
+ <li>Staff 1: <code>[email protected]</code></li>
308
+ <li>Staff 2: <code>[email protected]</code></li>
309
+ <li>Password (all staff): <code>Demo@123</code></li>
310
+ </ul>
311
+ </div>
312
+ <div class="col-md-6">
313
+ <p class="mb-2"><strong>Capabilities:</strong></p>
314
+ <ul class="small">
315
+ <li>Manage mosque details</li>
316
+ <li>Control prayer times</li>
317
+ <li>Post announcements</li>
318
+ <li>Manage inventory & finance</li>
319
+ </ul>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+ <div class="alert alert-success mb-0">
326
+ <div class="d-flex">
327
+ <i class="fas fa-lightbulb fa-2x me-3"></i>
328
+ <div>
329
+ <h6 class="mb-1">Getting Started Tips</h6>
330
+ <ol class="mb-0">
331
+ <li>Start with the superadmin account to explore system-wide features</li>
332
+ <li>Try mosque admin accounts to manage specific mosques</li>
333
+ <li>Use staff accounts to test role-based permissions</li>
334
+ </ol>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ {% endif %}
343
+ <nav class="navbar navbar-expand-lg navbar-dark bg-success">
344
+ <div class="container">
345
+ <a class="navbar-brand" href="{{ url_for('mosque.home') }}">
346
+ <i class="fas fa-mosque"></i> Mosqku
347
+ </a>
348
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
349
+ <span class="navbar-toggler-icon"></span>
350
+ </button>
351
+ <div class="collapse navbar-collapse" id="navbarNav">
352
+ <ul class="navbar-nav me-auto">
353
+ {% if user is defined and user.is_authenticated %}
354
+ {% if user.role == 'superadmin' %}
355
+ <li class="nav-item">
356
+ <a class="nav-link" href="{{ url_for('admin.dashboard') }}">
357
+ <i class="fas fa-user-shield"></i> Admin Dashboard
358
+ </a>
359
+ </li>
360
+ {% endif %}
361
+ {% if user.role != 'superadmin' %}
362
+ <li class="nav-item">
363
+ <a class="nav-link" href="{{ url_for('mosque.dashboard') }}">
364
+ <i class="fas fa-tachometer-alt"></i> Mosque Dashboard
365
+ </a>
366
+ </li>
367
+ {% endif %}
368
+ {% endif %}
369
+ </ul>
370
+ <ul class="navbar-nav">
371
+ {% if user is defined and user.is_authenticated %}
372
+ <li class="nav-item dropdown">
373
+ <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
374
+ <i class="fas fa-user"></i> {{ user.name }}
375
+ </a>
376
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
377
+ <li>
378
+ <a class="dropdown-item" href="{{ url_for('auth.change_password') }}">
379
+ <i class="fas fa-key"></i> Change Password
380
+ </a>
381
+ </li>
382
+ <li>
383
+ <a class="dropdown-item" href="{{ url_for('auth.view_recovery_key') }}">
384
+ <i class="fas fa-shield-alt"></i> View Recovery Key
385
+ </a>
386
+ </li>
387
+ <li><hr class="dropdown-divider"></li>
388
+ <li>
389
+ <a class="dropdown-item" href="{{ url_for('auth.logout') }}">
390
+ <i class="fas fa-sign-out-alt"></i> Logout
391
+ </a>
392
+ </li>
393
+ </ul>
394
+ </li>
395
+ {% else %}
396
+ <li class="nav-item">
397
+ <a class="nav-link" href="{{ url_for('auth.login') }}">
398
+ <i class="fas fa-sign-in-alt"></i> Login
399
+ </a>
400
+ </li>
401
+ <li class="nav-item">
402
+ <a class="nav-link" href="{{ url_for('auth.register') }}">
403
+ <i class="fas fa-user-plus"></i> Register
404
+ </a>
405
+ </li>
406
+ {% endif %}
407
+ </ul>
408
+ </div>
409
+ </div>
410
+ </nav>
411
+
412
+ <div class="main-content">
413
+ <div class="container py-4">
414
+ {% if user.is_authenticated and not hide_breadcrumbs|default(false) %}
415
+ <nav aria-label="breadcrumb">
416
+ <ol class="breadcrumb">
417
+ {% block breadcrumbs %}
418
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
419
+ {% endblock %}
420
+ </ol>
421
+ </nav>
422
+ {% endif %}
423
+ <!-- Flash Messages -->
424
+ <div class="flash-container">
425
+ {% with messages = get_flashed_messages(with_categories=true) %}
426
+ {% if messages %}
427
+ {% for category, message in messages %}
428
+ <div class="alert alert-{{ category }} alert-dismissible auto-dismiss show" role="alert">
429
+ {% if category in ['danger', 'error'] %}
430
+ <i class="fas fa-exclamation-triangle"></i>
431
+ {% elif category == 'warning' %}
432
+ <i class="fas fa-exclamation-circle"></i>
433
+ {% elif category == 'success' %}
434
+ <i class="fas fa-check-circle"></i>
435
+ {% else %}
436
+ <i class="fas fa-info-circle"></i>
437
+ {% endif %}
438
+ {{ message }}
439
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
440
+ </div>
441
+ {% endfor %}
442
+ {% endif %}
443
+ {% endwith %}
444
+ </div>
445
+
446
+ {% block content %}
447
+ {% endblock %}
448
+ </div>
449
+ </div>
450
+
451
+ <footer class="footer py-3 bg-dark text-light">
452
+ <div class="container text-center">
453
+ <span>Copyright &copy; 2025 Ikmal Said. All rights reserved</span>
454
+ </div>
455
+ </footer>
456
+
457
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
458
+ <script>
459
+ // Auto-dismiss alerts after 5 seconds
460
+ document.addEventListener('DOMContentLoaded', function() {
461
+ const alerts = document.querySelectorAll('.alert.auto-dismiss');
462
+ alerts.forEach(function(alert) {
463
+ // Start fade out after 4.5 seconds
464
+ setTimeout(function() {
465
+ alert.classList.remove('show');
466
+ alert.classList.add('fade');
467
+ }, 4500);
468
+
469
+ // Remove the alert after fade out (5 seconds total)
470
+ setTimeout(function() {
471
+ alert.remove();
472
+ // Check if flash container is empty and add a class if it is
473
+ const flashContainer = document.querySelector('.flash-container');
474
+ if (flashContainer && !flashContainer.hasChildNodes()) {
475
+ flashContainer.style.margin = '0';
476
+ flashContainer.style.padding = '0';
477
+ }
478
+ }, 5000);
479
+ });
480
+
481
+ // Handle manual dismissal
482
+ const closeButtons = document.querySelectorAll('.alert .btn-close');
483
+ closeButtons.forEach(function(button) {
484
+ button.addEventListener('click', function() {
485
+ const alert = this.closest('.alert');
486
+ alert.classList.remove('show');
487
+ alert.classList.add('fade');
488
+
489
+ // Remove after transition
490
+ setTimeout(function() {
491
+ alert.remove();
492
+ // Check if flash container is empty and add a class if it is
493
+ const flashContainer = document.querySelector('.flash-container');
494
+ if (flashContainer && !flashContainer.hasChildNodes()) {
495
+ flashContainer.style.margin = '0';
496
+ flashContainer.style.padding = '0';
497
+ }
498
+ }, 500);
499
+ });
500
+ });
501
+ });
502
+ </script>
503
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
504
+ </body>
505
+ </html>
main/templates/errors/error.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{{ error_code }} - {{ error_title }}{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container py-5">
7
+ <div class="row justify-content-center">
8
+ <div class="col-md-8 text-center">
9
+ <div class="error-page">
10
+ <div class="error-code text-success mb-4">
11
+ <h1 class="display-1 fw-bold">{{ error_code }}</h1>
12
+ </div>
13
+ <h2 class="mb-4">{{ error_title }}</h2>
14
+ <p class="text-muted mb-4">{{ error_description }}</p>
15
+ <div class="d-grid gap-3 d-sm-flex justify-content-sm-center">
16
+ <a href="{{ url_for('mosque.home') }}" class="btn btn-primary">
17
+ <i class="fas fa-home"></i> Back to Home
18
+ </a>
19
+ <button onclick="history.back()" class="btn btn-outline-success">
20
+ <i class="fas fa-arrow-left"></i> Go Back
21
+ </button>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <style>
29
+ .error-page {
30
+ padding: 40px;
31
+ background: #fff;
32
+ border-radius: 10px;
33
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
34
+ }
35
+
36
+ .error-code {
37
+ position: relative;
38
+ display: inline-block;
39
+ }
40
+
41
+ .error-code i {
42
+ position: absolute;
43
+ top: -15px;
44
+ right: -15px;
45
+ color: #198754;
46
+ opacity: 0.2;
47
+ transform: rotate(15deg);
48
+ }
49
+
50
+ .error-code h1 {
51
+ font-size: 120px;
52
+ background: linear-gradient(45deg, #198754, #20c997);
53
+ -webkit-background-clip: text;
54
+ -webkit-text-fill-color: transparent;
55
+ margin: 0;
56
+ line-height: 1;
57
+ }
58
+ </style>
59
+ {% endblock %}
main/templates/finance/add.html ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add Transaction{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item"><a href="{{ url_for('finance.finance_list', mosque_id=mosque.id) }}" class="text-success">Manage Finance</a></li>
13
+ <li class="breadcrumb-item active">Add Transaction</li>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ <div class="dashboard-header">
18
+ <div class="row align-items-center">
19
+ <div class="col">
20
+ <h2><i class="fas fa-plus-circle"></i> Add New Transaction</h2>
21
+ <p class="text-muted mb-0">Add a new transaction for {{ mosque.name }}</p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="row justify-content-center">
27
+ <div class="col-md-8">
28
+ <div class="card dashboard-card">
29
+ <div class="card-header">
30
+ <h4><i class="fas fa-money-bill-wave"></i> Transaction Details</h4>
31
+ </div>
32
+ <div class="card-body">
33
+ <form method="POST" id="transactionForm">
34
+ <div class="row g-3">
35
+ <div class="col-md-6">
36
+ <label for="number" class="form-label">Transaction Number</label>
37
+ <input type="text" class="form-control" id="number" name="number" required>
38
+ </div>
39
+
40
+ <div class="col-md-6">
41
+ <label for="transaction_name" class="form-label">Transaction Name</label>
42
+ <input type="text" class="form-control" id="transaction_name" name="transaction_name" required>
43
+ </div>
44
+
45
+ <div class="col-md-6">
46
+ <label for="finance_category" class="form-label">Category</label>
47
+ <select class="form-select" id="finance_category" name="finance_category" required>
48
+ <option value="" disabled selected>Select category</option>
49
+ <optgroup label="Income">
50
+ <option value="Donation">Donation</option>
51
+ <option value="Government Grant">Government Grant</option>
52
+ <option value="Zakat">Zakat</option>
53
+ <option value="Rental">Rental</option>
54
+ <option value="Other Income">Other Income</option>
55
+ </optgroup>
56
+ <optgroup label="Expenses">
57
+ <option value="Utilities">Utilities</option>
58
+ <option value="Maintenance">Maintenance</option>
59
+ <option value="Staff Salary">Staff Salary</option>
60
+ <option value="Events">Events</option>
61
+ <option value="Supplies">Supplies</option>
62
+ <option value="Other Expenses">Other Expenses</option>
63
+ </optgroup>
64
+ </select>
65
+ </div>
66
+
67
+ <div class="col-md-6">
68
+ <label for="amount" class="form-label">Amount (RM)</label>
69
+ <div class="input-group">
70
+ <span class="input-group-text">RM</span>
71
+ <input type="number" step="0.01" class="form-control" id="amount" name="amount" required>
72
+ </div>
73
+ <div class="form-text text-muted">Use negative value for expenses (e.g., -100.00)</div>
74
+ </div>
75
+
76
+ <div class="col-12">
77
+ <label for="description" class="form-label">Description</label>
78
+ <textarea class="form-control" id="description" name="description" rows="3"></textarea>
79
+ </div>
80
+
81
+ <div class="col-12">
82
+ <label for="remarks" class="form-label">Remarks</label>
83
+ <textarea class="form-control" id="remarks" name="remarks" rows="2"></textarea>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="d-grid gap-2 mt-4">
88
+ <button type="submit" class="btn btn-primary">
89
+ <i class="fas fa-plus"></i> Add Transaction
90
+ </button>
91
+ <a href="{{ url_for('finance.finance_list', mosque_id=mosque.id) }}" class="btn btn-outline-secondary">
92
+ <i class="fas fa-arrow-left"></i> Back to Finance List
93
+ </a>
94
+ </div>
95
+ </form>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <script>
102
+ // Reset form after successful submission
103
+ document.getElementById('transactionForm').addEventListener('submit', function() {
104
+ if (this.checkValidity()) {
105
+ setTimeout(() => {
106
+ this.reset();
107
+ }, 100);
108
+ }
109
+ });
110
+ </script>
111
+ {% endblock %}
main/templates/finance/edit.html ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Edit Transaction{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item"><a href="{{ url_for('finance.finance_list', mosque_id=mosque.id) }}" class="text-success">Manage Finance</a></li>
13
+ <li class="breadcrumb-item active">Edit Transaction</li>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ <div class="dashboard-header">
18
+ <div class="row align-items-center">
19
+ <div class="col">
20
+ <h2><i class="fas fa-edit"></i> Edit Transaction</h2>
21
+ <p class="text-muted mb-0">Edit existing transaction for {{ mosque.name }}</p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="row justify-content-center">
27
+ <div class="col-md-8">
28
+ <div class="card dashboard-card">
29
+ <div class="card-header">
30
+ <h4><i class="fas fa-money-bill-wave"></i> Transaction Details</h4>
31
+ </div>
32
+ <div class="card-body">
33
+ <form method="POST">
34
+ <div class="row g-3">
35
+ <div class="col-md-6">
36
+ <label for="number" class="form-label">Transaction Number</label>
37
+ <input type="text" class="form-control" id="number" name="number" value="{{ item.number }}" required>
38
+ </div>
39
+
40
+ <div class="col-md-6">
41
+ <label for="transaction_name" class="form-label">Transaction Name</label>
42
+ <input type="text" class="form-control" id="transaction_name" name="transaction_name" value="{{ item.transaction_name }}" required>
43
+ </div>
44
+
45
+ <div class="col-md-6">
46
+ <label for="finance_category" class="form-label">Category</label>
47
+ <select class="form-select" id="finance_category" name="finance_category" required>
48
+ <option value="" disabled>Select category</option>
49
+ <optgroup label="Income">
50
+ <option value="Donation" {{ 'selected' if item.finance_category == 'Donation' }}>Donation</option>
51
+ <option value="Government Grant" {{ 'selected' if item.finance_category == 'Government Grant' }}>Government Grant</option>
52
+ <option value="Zakat" {{ 'selected' if item.finance_category == 'Zakat' }}>Zakat</option>
53
+ <option value="Rental" {{ 'selected' if item.finance_category == 'Rental' }}>Rental</option>
54
+ <option value="Other Income" {{ 'selected' if item.finance_category == 'Other Income' }}>Other Income</option>
55
+ </optgroup>
56
+ <optgroup label="Expenses">
57
+ <option value="Utilities" {{ 'selected' if item.finance_category == 'Utilities' }}>Utilities</option>
58
+ <option value="Maintenance" {{ 'selected' if item.finance_category == 'Maintenance' }}>Maintenance</option>
59
+ <option value="Staff Salary" {{ 'selected' if item.finance_category == 'Staff Salary' }}>Staff Salary</option>
60
+ <option value="Events" {{ 'selected' if item.finance_category == 'Events' }}>Events</option>
61
+ <option value="Supplies" {{ 'selected' if item.finance_category == 'Supplies' }}>Supplies</option>
62
+ <option value="Other Expenses" {{ 'selected' if item.finance_category == 'Other Expenses' }}>Other Expenses</option>
63
+ </optgroup>
64
+ </select>
65
+ </div>
66
+
67
+ <div class="col-md-6">
68
+ <label for="amount" class="form-label">Amount (RM)</label>
69
+ <div class="input-group">
70
+ <span class="input-group-text">RM</span>
71
+ <input type="number" step="0.01" class="form-control" id="amount" name="amount" value="{{ item.amount }}" required>
72
+ </div>
73
+ <div class="form-text text-muted">Use negative value for expenses (e.g., -100.00)</div>
74
+ </div>
75
+
76
+ <div class="col-12">
77
+ <label for="description" class="form-label">Description</label>
78
+ <textarea class="form-control" id="description" name="description" rows="3">{{ item.description }}</textarea>
79
+ </div>
80
+
81
+ <div class="col-12">
82
+ <label for="remarks" class="form-label">Remarks</label>
83
+ <textarea class="form-control" id="remarks" name="remarks" rows="2">{{ item.remarks }}</textarea>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="d-grid gap-2 mt-4">
88
+ <button type="submit" class="btn btn-primary">
89
+ <i class="fas fa-save"></i> Update Transaction
90
+ </button>
91
+ <a href="{{ url_for('finance.finance_list', mosque_id=mosque.id) }}" class="btn btn-outline-secondary">
92
+ <i class="fas fa-arrow-left"></i> Back to Finance List
93
+ </a>
94
+ </div>
95
+ </form>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ {% endblock %}
main/templates/finance/list.html ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Finance Management{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item active">Manage Finance</li>
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+ <div class="dashboard-header">
17
+ <div class="row align-items-center">
18
+ <div class="col">
19
+ <h2><i class="fas fa-money-bill-wave"></i> Finance Management</h2>
20
+ <p class="text-muted mb-0">Manage financial income and expenses for {{ mosque.name }}</p>
21
+ <p class="text-muted small mb-0">
22
+ <span><i class="fas fa-hand-holding-usd text-muted"></i> Total Income: RM {{ "%.2f"|format(total_income) }}</span>
23
+ <span class="ms-3"><i class="fas fa-money-bill-wave-alt text-muted"></i> Total Expenses: RM {{ "%.2f"|format(total_expenses) }}</span>
24
+ {% if items %}
25
+ <span class="ms-3"><i class="fas fa-clock"></i> Last Updated: {{ items[0].date_added.strftime('%d/%m/%Y %H:%M') }}</span>
26
+ {% endif %}
27
+ </p>
28
+ </div>
29
+ <div class="col-md-4 text-end mt-3 mt-md-0">
30
+ <div class="btn-group">
31
+ <a href="{{ url_for('finance.add_item', mosque_id=mosque.id) }}" class="btn btn-primary">
32
+ <i class="fas fa-plus"></i> Add New Transaction
33
+ </a>
34
+ <div class="btn-group ms-2">
35
+ <button type="button" class="btn btn-outline-primary" data-bs-toggle="dropdown" aria-expanded="false">
36
+ <i class="fas fa-ellipsis-v"></i> More
37
+ </button>
38
+ <ul class="dropdown-menu dropdown-menu-end">
39
+ <li>
40
+ <a class="dropdown-item" href="{{ url_for('finance.export_data', mosque_id=mosque.id) }}">
41
+ <i class="fas fa-file-export"></i> Export Data (CSV)
42
+ </a>
43
+ </li>
44
+ <li>
45
+ <a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importModal">
46
+ <i class="fas fa-file-import"></i> Import Data (CSV)
47
+ </a>
48
+ </li>
49
+ </ul>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="card mt-4">
57
+ <div class="card-body p-0">
58
+ <div class="table-responsive" style="min-height: 400px;">
59
+ {% if items %}
60
+ <table class="table table-hover mb-0">
61
+ <thead class="table-light">
62
+ <tr>
63
+ <th style="width: 10%">Number</th>
64
+ <th style="width: 15%">Transaction</th>
65
+ <th style="width: 10%">Category</th>
66
+ <th style="width: 20%">Description</th>
67
+ <th style="width: 10%">Amount (RM)</th>
68
+ <th style="width: 12%">Date Added</th>
69
+ <th style="width: 13%">Remarks</th>
70
+ <th style="width: 10%">Actions</th>
71
+ </tr>
72
+ </thead>
73
+ <tbody>
74
+ {% for item in items %}
75
+ <tr>
76
+ <td>{{ item.number }}</td>
77
+ <td>{{ item.transaction_name }}</td>
78
+ <td>{{ item.finance_category }}</td>
79
+ <td>{{ item.description }}</td>
80
+ <td class="{{ 'text-success' if item.amount > 0 else 'text-danger' }}">
81
+ {{ "%.2f"|format(item.amount) }}
82
+ </td>
83
+ <td>{{ item.date_added.strftime('%d/%m/%Y %H:%M') }}</td>
84
+ <td>{{ item.remarks }}</td>
85
+ <td>
86
+ <div class="btn-group">
87
+ <a href="{{ url_for('finance.edit_item', id=item.id) }}" class="btn btn-sm btn-outline-success">
88
+ <i class="fas fa-edit"></i>
89
+ </a>
90
+ <button type="button" class="btn btn-sm btn-outline-success" data-bs-toggle="modal" data-bs-target="#deleteModal{{ item.id }}">
91
+ <i class="fas fa-trash"></i>
92
+ </button>
93
+ </div>
94
+
95
+ <!-- Delete Modal -->
96
+ <div class="modal fade" id="deleteModal{{ item.id }}" tabindex="-1" aria-hidden="true">
97
+ <div class="modal-dialog">
98
+ <div class="modal-content">
99
+ <div class="modal-header">
100
+ <h5 class="modal-title">Delete Transaction</h5>
101
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
102
+ </div>
103
+ <div class="modal-body">
104
+ Are you sure you want to delete "{{ item.transaction_name }}"?
105
+ </div>
106
+ <div class="modal-footer">
107
+ <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
108
+ <form action="{{ url_for('finance.delete_item', id=item.id) }}" method="POST" class="d-inline">
109
+ <button type="submit" class="btn btn-outline-success">Delete</button>
110
+ </form>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </td>
116
+ </tr>
117
+ {% endfor %}
118
+ </tbody>
119
+ </table>
120
+ {% else %}
121
+ <div class="text-center py-4">
122
+ <div class="empty-state">
123
+ <i class="fas fa-money-bill-wave fa-3x mb-3 text-muted"></i>
124
+ <h3>No Transactions Found</h3>
125
+ <p class="text-muted">Start by adding your first financial transaction.</p>
126
+ <a href="{{ url_for('finance.add_item', mosque_id=mosque.id) }}" class="btn btn-primary mt-3">
127
+ <i class="fas fa-plus"></i> Add First Transaction
128
+ </a>
129
+ </div>
130
+ </div>
131
+ {% endif %}
132
+ </div>
133
+ </div>
134
+ </div>
135
+
136
+ <div class="mt-3">
137
+ {% if user.role == 'superadmin' %}
138
+ <a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="btn btn-outline-secondary">
139
+ {% else %}
140
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
141
+ {% endif %}
142
+ <i class="fas fa-arrow-left"></i> Back
143
+ </a>
144
+ </div>
145
+
146
+ <!-- Import Modal -->
147
+ <div class="modal fade" id="importModal" tabindex="-1" aria-hidden="true">
148
+ <div class="modal-dialog">
149
+ <div class="modal-content">
150
+ <div class="modal-header">
151
+ <h5 class="modal-title">Import Financial Data</h5>
152
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
153
+ </div>
154
+ <form action="{{ url_for('finance.import_data', mosque_id=mosque.id) }}" method="POST" enctype="multipart/form-data">
155
+ <div class="modal-body">
156
+ <div class="alert alert-info">
157
+ <i class="fas fa-info-circle"></i> Please upload a CSV file with the following columns:
158
+ <small>
159
+ <ul class="mb-0 mt-2">
160
+ <li>transaction_name</li>
161
+ <li>finance_category</li>
162
+ <li>description</li>
163
+ <li>amount</li>
164
+ <li>remarks (optional)</li>
165
+ </ul>
166
+ </small>
167
+ </div>
168
+ <div class="mb-3">
169
+ <label for="csvFile" class="form-label">Select CSV File</label>
170
+ <input type="file" class="form-control" id="csvFile" name="file" accept=".csv" required>
171
+ </div>
172
+ </div>
173
+ <div class="modal-footer">
174
+ <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
175
+ <button type="submit" class="btn btn-primary">Import Data</button>
176
+ </div>
177
+ </form>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ {% endblock %}
main/templates/home.html ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Home{% endblock %}
4
+
5
+ {% set hide_breadcrumbs = true %}
6
+
7
+ {% block styles %}
8
+ <style>
9
+ .feature-list {
10
+ list-style: none;
11
+ padding-left: 0;
12
+ margin-top: 1rem;
13
+ }
14
+ .feature-list li {
15
+ margin-bottom: 0.5rem;
16
+ display: flex;
17
+ align-items: center;
18
+ }
19
+ .feature-list li i {
20
+ margin-right: 0.5rem;
21
+ min-width: 1rem;
22
+ }
23
+ </style>
24
+ {% endblock %}
25
+
26
+ {% block content %}
27
+ <!-- Hero Section -->
28
+ <div class="hero-section text-center {% if mosque_bg_exists %}with-bg-image{% endif %}">
29
+ <div class="hero-content">
30
+ <h1 class="hero-title">Welcome to Mosqku</h1>
31
+ <p class="hero-subtitle">Connecting Communities Through Modern Mosque Management</p>
32
+ {% if not user.is_authenticated %}
33
+ <div class="hero-cta">
34
+ <a href="{{ url_for('auth.register') }}" class="btn btn-primary btn-lg">
35
+ <i class="fas fa-user-plus"></i> Get Started
36
+ </a>
37
+ <a href="#features" class="btn btn-outline-primary btn-lg">
38
+ <i class="fas fa-info-circle"></i> Learn More
39
+ </a>
40
+ </div>
41
+ {% elif not user.mosque_id and user.role != 'superadmin' %}
42
+ <div class="hero-cta">
43
+ <a href="{{ url_for('mosque.register_mosque') }}" class="btn btn-primary btn-lg">
44
+ <i class="fas fa-mosque"></i> Register Your Mosque
45
+ </a>
46
+ </div>
47
+ {% else %}
48
+ <div class="hero-cta">
49
+ {% if user.role == 'superadmin' %}
50
+ <a href="{{ url_for('admin.dashboard') }}" class="btn btn-primary btn-lg">
51
+ <i class="fas fa-gauge-high"></i> Go to Admin Dashboard
52
+ </a>
53
+ {% else %}
54
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-primary btn-lg">
55
+ <i class="fas fa-tachometer-alt"></i> Go to Mosque Dashboard
56
+ </a>
57
+ {% endif %}
58
+ </div>
59
+ {% endif %}
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Features Section -->
64
+ <section id="features" class="features-section py-5">
65
+ <div class="container">
66
+ <h2 class="section-title text-center mb-5">Why Choose Mosqku?</h2>
67
+ <div class="row g-4">
68
+ <div class="col-md-4">
69
+ <div class="feature-card">
70
+ <div class="feature-icon">
71
+ <i class="fas fa-clock fa-2x"></i>
72
+ </div>
73
+ <h3>Prayer Time Management</h3>
74
+ <p>Easily manage and display prayer times for your mosque. Keep your community informed and synchronized with accurate prayer schedules.</p>
75
+ </div>
76
+ </div>
77
+ <div class="col-md-4">
78
+ <div class="feature-card">
79
+ <div class="feature-icon">
80
+ <i class="fas fa-bullhorn fa-2x"></i>
81
+ </div>
82
+ <h3>Announcements</h3>
83
+ <p>Share important updates and events with your community in real-time. Never miss an important announcement.</p>
84
+ </div>
85
+ </div>
86
+ <div class="col-md-4">
87
+ <div class="feature-card">
88
+ <div class="feature-icon">
89
+ <i class="fas fa-users fa-2x"></i>
90
+ </div>
91
+ <h3>Community Building</h3>
92
+ <p>Foster stronger connections within your community through effective mosque management and communication.</p>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ <div class="row g-4 mt-4">
97
+ <div class="col-md-4">
98
+ <div class="feature-card">
99
+ <div class="feature-icon">
100
+ <i class="fas fa-boxes fa-2x"></i>
101
+ </div>
102
+ <h3>Inventory Management</h3>
103
+ <p>Keep track of mosque assets and supplies with our comprehensive inventory system.</p>
104
+ </div>
105
+ </div>
106
+ <div class="col-md-4">
107
+ <div class="feature-card">
108
+ <div class="feature-icon">
109
+ <i class="fas fa-money-bill-wave fa-2x"></i>
110
+ </div>
111
+ <h3>Financial Management</h3>
112
+ <p>Streamline mosque finances with our secure and transparent financial management system.</p>
113
+ </div>
114
+ </div>
115
+ <div class="col-md-4">
116
+ <div class="feature-card">
117
+ <div class="feature-icon">
118
+ <i class="fas fa-shield-alt fa-2x"></i>
119
+ </div>
120
+ <h3>Secure Access</h3>
121
+ <p>Protect your mosque's data with our robust security features and role-based access control.</p>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </section>
127
+
128
+ <!-- Benefits Section -->
129
+ <section class="benefits-section py-5">
130
+ <div class="container">
131
+ <h2 class="section-title text-center mb-5">Benefits of Using Mosqku</h2>
132
+ <div class="row g-4">
133
+ <div class="col-md-6">
134
+ <div class="benefit-card">
135
+ <h4><i class="fas fa-chart-line text-success"></i> Increased Efficiency</h4>
136
+ <p>Save time and resources with automated management tools and streamlined processes.</p>
137
+ </div>
138
+ </div>
139
+ <div class="col-md-6">
140
+ <div class="benefit-card">
141
+ <h4><i class="fas fa-handshake text-success"></i> Better Community Engagement</h4>
142
+ <p>Keep your community informed and involved with real-time updates and easy communication.</p>
143
+ </div>
144
+ </div>
145
+ <div class="col-md-6">
146
+ <div class="benefit-card">
147
+ <h4><i class="fas fa-tasks text-success"></i> Simplified Administration</h4>
148
+ <p>Manage all aspects of your mosque operations from a single, user-friendly platform.</p>
149
+ </div>
150
+ </div>
151
+ <div class="col-md-6">
152
+ <div class="benefit-card">
153
+ <h4><i class="fas fa-mobile-alt text-success"></i> Mobile Accessibility</h4>
154
+ <p>Access your mosque management system anytime, anywhere, from any device.</p>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </section>
160
+
161
+ <!-- Testimonials Section -->
162
+ <section class="testimonials-section py-5">
163
+ <div class="container">
164
+ <h2 class="section-title text-center mb-5">What Mosques Say About Us</h2>
165
+ <div class="row g-4">
166
+ <div class="col-md-4">
167
+ <div class="testimonial-card">
168
+ <div class="testimonial-content">
169
+ <i class="fas fa-quote-left fa-2x text-success mb-3"></i>
170
+ <p>"Mosqku has revolutionized how we manage our mosque. The prayer time management and announcement features have made it so much easier to keep our community informed and engaged."</p>
171
+ </div>
172
+ <div class="testimonial-author">
173
+ <h5>Masjid Al-Hidayah</h5>
174
+ <p class="text-muted">Kuala Lumpur</p>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ <div class="col-md-4">
179
+ <div class="testimonial-card">
180
+ <div class="testimonial-content">
181
+ <i class="fas fa-quote-left fa-2x text-success mb-3"></i>
182
+ <p>"The inventory and financial management features have brought transparency and efficiency to our operations. We can now focus more on serving our community rather than administrative tasks."</p>
183
+ </div>
184
+ <div class="testimonial-author">
185
+ <h5>Masjid As-Salam</h5>
186
+ <p class="text-muted">Johor Bahru</p>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ <div class="col-md-4">
191
+ <div class="testimonial-card">
192
+ <div class="testimonial-content">
193
+ <i class="fas fa-quote-left fa-2x text-success mb-3"></i>
194
+ <p>"Since implementing Mosqku, we've seen a significant increase in community participation and engagement. The mobile accessibility makes it convenient for everyone to stay connected."</p>
195
+ </div>
196
+ <div class="testimonial-author">
197
+ <h5>Masjid An-Nur</h5>
198
+ <p class="text-muted">Penang</p>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </section>
205
+
206
+ <!-- Mosques Section -->
207
+ <section class="mosques-section">
208
+ <div class="container">
209
+ <div class="section-header text-center mb-4">
210
+ <h2 class="section-title">Find Mosques Near You</h2>
211
+ </div>
212
+
213
+ {% if mosques %}
214
+ <div class="row g-4 justify-content-center">
215
+ {% for mosque in mosques %}
216
+ <div class="col-md-4">
217
+ <div class="mosque-card h-100">
218
+ <div class="mosque-card-header text-center">
219
+ <i class="fas fa-mosque"></i>
220
+ </div>
221
+ <div class="mosque-card-body">
222
+ <h3 class="mosque-name text-center">{{ mosque.name }}</h3>
223
+ <div class="mosque-info text-start">
224
+ <p><i class="fas fa-map-marker-alt"></i> {{ mosque.city }}, {{ mosque.state }}</p>
225
+ {% if mosque.phone %}
226
+ <p><i class="fas fa-phone"></i> {{ mosque.phone }}</p>
227
+ {% endif %}
228
+ {% if mosque.email %}
229
+ <p><i class="fas fa-envelope"></i> {{ mosque.email }}</p>
230
+ {% endif %}
231
+ </div>
232
+ <a href="{{ url_for('mosque.mosque_info', id=mosque.id) }}" class="btn btn-outline-primary mt-3 w-100">
233
+ <i class="fas fa-info-circle"></i> View Details
234
+ </a>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ {% endfor %}
239
+ </div>
240
+ {% else %}
241
+ <div class="empty-state text-center py-5">
242
+ <i class="fas fa-mosque fa-3x mb-3"></i>
243
+ <h3>No Mosques Registered Yet</h3>
244
+ <p class="text-muted">Be the first to register your mosque and join our growing community.</p>
245
+ {% if not user.is_authenticated %}
246
+ <a href="{{ url_for('auth.register') }}" class="btn btn-primary mt-3">
247
+ <i class="fas fa-user-plus"></i> Sign Up to Register
248
+ </a>
249
+ {% elif user.role == 'superadmin' %}
250
+ <a href="{{ url_for('admin.add_mosque') }}" class="btn btn-primary mt-3">
251
+ <i class="fas fa-plus"></i> Add New Mosque
252
+ </a>
253
+ {% else %}
254
+ <a href="{{ url_for('mosque.register_mosque') }}" class="btn btn-primary mt-3">
255
+ <i class="fas fa-plus"></i> Register Your Mosque
256
+ </a>
257
+ {% endif %}
258
+ </div>
259
+ {% endif %}
260
+ </div>
261
+ </section>
262
+ {% endblock %}
main/templates/inventory/add.html ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add Inventory Item{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item"><a href="{{ url_for('inventory.inventory_list', mosque_id=mosque.id) }}" class="text-success">Manage Inventory</a></li>
13
+ <li class="breadcrumb-item active">Add Item</li>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ <div class="dashboard-header">
18
+ <div class="row align-items-center">
19
+ <div class="col">
20
+ <h2><i class="fas fa-plus-circle"></i> Add New Item</h2>
21
+ <p class="text-muted mb-0">Add new inventory item for {{ mosque.name }}</p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="row justify-content-center">
27
+ <div class="col-md-8">
28
+ <div class="card dashboard-card">
29
+ <div class="card-header">
30
+ <h4><i class="fas fa-boxes"></i> Item Details</h4>
31
+ </div>
32
+ <div class="card-body">
33
+ <form method="POST" id="inventoryForm">
34
+ <div class="row g-3">
35
+ <div class="col-md-6">
36
+ <label for="number" class="form-label">Item Number</label>
37
+ <input type="text" class="form-control" id="number" name="number" required>
38
+ </div>
39
+
40
+ <div class="col-md-6">
41
+ <label for="item_name" class="form-label">Item Name</label>
42
+ <input type="text" class="form-control" id="item_name" name="item_name" required>
43
+ </div>
44
+
45
+ <div class="col-md-6">
46
+ <label for="item_category" class="form-label">Category</label>
47
+ <select class="form-select" id="item_category" name="item_category" required>
48
+ <option value="" disabled selected>Select category</option>
49
+ <option value="Furniture">Furniture</option>
50
+ <option value="Electronics">Electronics</option>
51
+ <option value="Books">Books</option>
52
+ <option value="Prayer Items">Prayer Items</option>
53
+ <option value="Cleaning Supplies">Cleaning Supplies</option>
54
+ <option value="Office Supplies">Office Supplies</option>
55
+ <option value="Kitchen">Kitchen</option>
56
+ <option value="Other">Other</option>
57
+ </select>
58
+ </div>
59
+
60
+ <div class="col-md-6">
61
+ <label for="quantity" class="form-label">Quantity</label>
62
+ <input type="number" class="form-control" id="quantity" name="quantity" min="0" required>
63
+ </div>
64
+
65
+ <div class="col-12">
66
+ <label for="item_description" class="form-label">Description</label>
67
+ <textarea class="form-control" id="item_description" name="item_description" rows="3"></textarea>
68
+ </div>
69
+
70
+ <div class="col-12">
71
+ <label for="remarks" class="form-label">Remarks</label>
72
+ <textarea class="form-control" id="remarks" name="remarks" rows="2"></textarea>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="d-grid gap-2 mt-4">
77
+ <button type="submit" class="btn btn-primary">
78
+ <i class="fas fa-plus"></i> Add Item
79
+ </button>
80
+ <a href="{{ url_for('inventory.inventory_list', mosque_id=mosque.id) }}" class="btn btn-outline-secondary">
81
+ <i class="fas fa-arrow-left"></i> Back to Inventory List
82
+ </a>
83
+ </div>
84
+ </form>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <script>
91
+ // Reset form after successful submission
92
+ document.getElementById('inventoryForm').addEventListener('submit', function() {
93
+ if (this.checkValidity()) {
94
+ setTimeout(() => {
95
+ this.reset();
96
+ }, 100);
97
+ }
98
+ });
99
+ </script>
100
+ {% endblock %}
main/templates/inventory/edit.html ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Edit Inventory Item{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item"><a href="{{ url_for('inventory.inventory_list', mosque_id=mosque.id) }}" class="text-success">Manage Inventory</a></li>
13
+ <li class="breadcrumb-item active">Edit Item</li>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ <div class="dashboard-header">
18
+ <div class="row align-items-center">
19
+ <div class="col">
20
+ <h2><i class="fas fa-edit"></i> Edit Item</h2>
21
+ <p class="text-muted mb-0">Edit existing inventory item for {{ mosque.name }}</p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="row justify-content-center">
27
+ <div class="col-md-8">
28
+ <div class="card dashboard-card">
29
+ <div class="card-header">
30
+ <h4><i class="fas fa-boxes"></i> Item Details</h4>
31
+ </div>
32
+ <div class="card-body">
33
+ <form method="POST">
34
+ <div class="row g-3">
35
+ <div class="col-md-6">
36
+ <label for="number" class="form-label">Item Number</label>
37
+ <input type="text" class="form-control" id="number" name="number" value="{{ item.number }}" required>
38
+ </div>
39
+
40
+ <div class="col-md-6">
41
+ <label for="item_name" class="form-label">Item Name</label>
42
+ <input type="text" class="form-control" id="item_name" name="item_name" value="{{ item.item_name }}" required>
43
+ </div>
44
+
45
+ <div class="col-md-6">
46
+ <label for="item_category" class="form-label">Category</label>
47
+ <select class="form-select" id="item_category" name="item_category" required>
48
+ <option value="" disabled>Select category</option>
49
+ {% for category in ['Furniture', 'Electronics', 'Books', 'Prayer Items', 'Cleaning Supplies', 'Office Supplies', 'Kitchen', 'Other'] %}
50
+ <option value="{{ category }}" {% if item.item_category == category %}selected{% endif %}>{{ category }}</option>
51
+ {% endfor %}
52
+ </select>
53
+ </div>
54
+
55
+ <div class="col-md-6">
56
+ <label for="quantity" class="form-label">Quantity</label>
57
+ <input type="number" class="form-control" id="quantity" name="quantity" min="0" value="{{ item.quantity }}" required>
58
+ </div>
59
+
60
+ <div class="col-12">
61
+ <label for="item_description" class="form-label">Description</label>
62
+ <textarea class="form-control" id="item_description" name="item_description" rows="3">{{ item.item_description }}</textarea>
63
+ </div>
64
+
65
+ <div class="col-12">
66
+ <label for="remarks" class="form-label">Remarks</label>
67
+ <textarea class="form-control" id="remarks" name="remarks" rows="2">{{ item.remarks }}</textarea>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="d-grid gap-2 mt-4">
72
+ <button type="submit" class="btn btn-primary">
73
+ <i class="fas fa-save"></i> Update Item
74
+ </button>
75
+ <a href="{{ url_for('inventory.inventory_list', mosque_id=mosque.id) }}" class="btn btn-outline-secondary">
76
+ <i class="fas fa-arrow-left"></i> Back to Inventory List
77
+ </a>
78
+ </div>
79
+ </form>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ {% endblock %}
main/templates/inventory/list.html ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Inventory Management{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item active">Manage Inventory</li>
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+ <div class="dashboard-header">
17
+ <div class="row align-items-center">
18
+ <div class="col">
19
+ <h2><i class="fas fa-boxes"></i> Inventory Management</h2>
20
+ <p class="text-muted mb-0">Manage inventory items for {{ mosque.name }}</p>
21
+ <p class="text-muted small mb-0">
22
+ <span><i class="fas fa-layer-group"></i> Total Items: {{ items|length }}</span>
23
+ {% if items %}
24
+ <span class="ms-3"><i class="fas fa-clock"></i> Last Updated: {{ items[0].date_added.strftime('%d/%m/%Y %H:%M') }}</span>
25
+ {% endif %}
26
+ </p>
27
+ </div>
28
+ <div class="col-md-4 text-end mt-3 mt-md-0">
29
+ <div class="btn-group">
30
+ <a href="{{ url_for('inventory.add_item', mosque_id=mosque.id) }}" class="btn btn-primary">
31
+ <i class="fas fa-plus"></i> Add New Item
32
+ </a>
33
+ <div class="btn-group ms-2">
34
+ <button type="button" class="btn btn-outline-primary" data-bs-toggle="dropdown" aria-expanded="false">
35
+ <i class="fas fa-ellipsis-v"></i> More
36
+ </button>
37
+ <ul class="dropdown-menu dropdown-menu-end">
38
+ <li>
39
+ <a class="dropdown-item" href="{{ url_for('inventory.export_data', mosque_id=mosque.id) }}">
40
+ <i class="fas fa-file-export"></i> Export Data (CSV)
41
+ </a>
42
+ </li>
43
+ <li>
44
+ <a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importModal">
45
+ <i class="fas fa-file-import"></i> Import Data (CSV)
46
+ </a>
47
+ </li>
48
+ </ul>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="card mt-4">
56
+ <div class="card-body p-0">
57
+ <div class="table-responsive" style="min-height: 400px;">
58
+ {% if items %}
59
+ <table class="table table-hover mb-0">
60
+ <thead class="table-light">
61
+ <tr>
62
+ <th style="width: 10%">Number</th>
63
+ <th style="width: 15%">Item Name</th>
64
+ <th style="width: 10%">Category</th>
65
+ <th style="width: 20%">Description</th>
66
+ <th style="width: 8%">Quantity</th>
67
+ <th style="width: 12%">Date Added</th>
68
+ <th style="width: 15%">Remarks</th>
69
+ <th style="width: 10%">Actions</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ {% for item in items %}
74
+ <tr>
75
+ <td>{{ item.number }}</td>
76
+ <td>{{ item.item_name }}</td>
77
+ <td>{{ item.item_category }}</td>
78
+ <td>{{ item.item_description }}</td>
79
+ <td>{{ item.quantity }}</td>
80
+ <td>{{ item.date_added.strftime('%d/%m/%Y %H:%M') }}</td>
81
+ <td>{{ item.remarks }}</td>
82
+ <td>
83
+ <div class="btn-group">
84
+ <a href="{{ url_for('inventory.edit_item', id=item.id) }}" class="btn btn-sm btn-outline-success">
85
+ <i class="fas fa-edit"></i>
86
+ </a>
87
+ <button type="button" class="btn btn-sm btn-outline-success" data-bs-toggle="modal" data-bs-target="#deleteModal{{ item.id }}">
88
+ <i class="fas fa-trash"></i>
89
+ </button>
90
+ </div>
91
+
92
+ <!-- Delete Modal -->
93
+ <div class="modal fade" id="deleteModal{{ item.id }}" tabindex="-1" aria-hidden="true">
94
+ <div class="modal-dialog">
95
+ <div class="modal-content">
96
+ <div class="modal-header">
97
+ <h5 class="modal-title">Delete Item</h5>
98
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
99
+ </div>
100
+ <div class="modal-body">
101
+ Are you sure you want to delete "{{ item.item_name }}"?
102
+ </div>
103
+ <div class="modal-footer">
104
+ <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
105
+ <form action="{{ url_for('inventory.delete_item', id=item.id) }}" method="POST" class="d-inline">
106
+ <button type="submit" class="btn btn-outline-success">Delete</button>
107
+ </form>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </td>
113
+ </tr>
114
+ {% endfor %}
115
+ </tbody>
116
+ </table>
117
+ {% else %}
118
+ <div class="text-center py-4">
119
+ <div class="empty-state">
120
+ <i class="fas fa-boxes fa-3x mb-3 text-muted"></i>
121
+ <h3>No Items Found</h3>
122
+ <p class="text-muted">Start by adding your first inventory item.</p>
123
+ <a href="{{ url_for('inventory.add_item', mosque_id=mosque.id) }}" class="btn btn-primary mt-3">
124
+ <i class="fas fa-plus"></i> Add First Item
125
+ </a>
126
+ </div>
127
+ </div>
128
+ {% endif %}
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <div class="mt-3">
134
+ {% if user.role == 'superadmin' %}
135
+ <a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="btn btn-outline-secondary">
136
+ {% else %}
137
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
138
+ {% endif %}
139
+ <i class="fas fa-arrow-left"></i> Back
140
+ </a>
141
+ </div>
142
+
143
+ <!-- Import Modal -->
144
+ <div class="modal fade" id="importModal" tabindex="-1" aria-hidden="true">
145
+ <div class="modal-dialog">
146
+ <div class="modal-content">
147
+ <div class="modal-header">
148
+ <h5 class="modal-title">Import Inventory Data</h5>
149
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
150
+ </div>
151
+ <form action="{{ url_for('inventory.import_data', mosque_id=mosque.id) }}" method="POST" enctype="multipart/form-data">
152
+ <div class="modal-body">
153
+ <div class="alert alert-info">
154
+ <i class="fas fa-info-circle"></i> Please upload a CSV file with the following columns:
155
+ <small>
156
+ <ul class="mb-0 mt-2">
157
+ <li>item_name</li>
158
+ <li>item_category</li>
159
+ <li>item_description</li>
160
+ <li>quantity</li>
161
+ <li>remarks (optional)</li>
162
+ </ul>
163
+ </small>
164
+ </div>
165
+ <div class="mb-3">
166
+ <label for="csvFile" class="form-label">Select CSV File</label>
167
+ <input type="file" class="form-control" id="csvFile" name="file" accept=".csv" required>
168
+ </div>
169
+ </div>
170
+ <div class="modal-footer">
171
+ <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
172
+ <button type="submit" class="btn btn-primary">Import Data</button>
173
+ </div>
174
+ </form>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ {% endblock %}
main/templates/mosque/add_another_admin.html ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add Another Admin{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Add Another Admin</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-user-plus"></i> Add Another Admin</h2>
15
+ <p class="text-muted mb-0">Create a new admin account for your mosque</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-6">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-user-shield"></i> Administrator Details</h4>
25
+ </div>
26
+ <div class="card-body p-4">
27
+ <form method="POST" id="adminForm" onsubmit="return validatePassword()">
28
+ <div class="mb-3">
29
+ <label for="name" class="form-label">
30
+ <i class="fas fa-user"></i> Full Name
31
+ </label>
32
+ <input type="text" class="form-control" id="name" name="name" required>
33
+ </div>
34
+
35
+ <div class="mb-3">
36
+ <label for="email" class="form-label">
37
+ <i class="fas fa-envelope"></i> Email Address
38
+ </label>
39
+ <input type="email" class="form-control" id="email" name="email" required>
40
+ </div>
41
+
42
+ <div class="mb-3">
43
+ <label for="password" class="form-label">
44
+ <i class="fas fa-lock"></i> Password
45
+ </label>
46
+ <input type="password" class="form-control" id="password" name="password"
47
+ pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
48
+ title="Password must be at least 8 characters long and include at least one number and one special character"
49
+ required>
50
+ <div class="form-text">
51
+ Password requirements:
52
+ <ul class="mb-0">
53
+ <li>At least 8 characters long</li>
54
+ <li>Must contain at least one number</li>
55
+ <li>Must contain at least one special character (@$!%*#?&)</li>
56
+ </ul>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Hidden field for mosque_id -->
61
+ <input type="hidden" name="mosque_id" value="{{ mosque.id }}">
62
+
63
+ <div class="d-grid gap-2">
64
+ <button type="submit" class="btn btn-primary">
65
+ <i class="fas fa-plus"></i> Create Administrator
66
+ </button>
67
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
68
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
69
+ </a>
70
+ </div>
71
+ </form>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <script>
78
+ function validatePassword() {
79
+ const password = document.getElementById('password');
80
+ const pattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
81
+
82
+ if (!pattern.test(password.value)) {
83
+ password.setCustomValidity('Password must meet all requirements');
84
+ return false;
85
+ } else {
86
+ password.setCustomValidity('');
87
+ return true;
88
+ }
89
+ }
90
+
91
+ document.getElementById('password').addEventListener('input', validatePassword);
92
+ </script>
93
+ {% endblock %}
main/templates/mosque/add_another_staff.html ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add New Staff{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Add New Staff</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-user-plus"></i> Add New Staff</h2>
15
+ <p class="text-muted mb-0">Create a new staff account for your mosque</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-6">
22
+ <div class="card dashboard-card">
23
+ <div class="card-header">
24
+ <h4><i class="fas fa-user-tie"></i> Staff Details</h4>
25
+ </div>
26
+ <div class="card-body p-4">
27
+ <form method="POST" id="staffForm" onsubmit="return validatePassword()">
28
+ <div class="mb-3">
29
+ <label for="name" class="form-label">
30
+ <i class="fas fa-user"></i> Full Name
31
+ </label>
32
+ <input type="text" class="form-control" id="name" name="name" required>
33
+ </div>
34
+
35
+ <div class="mb-3">
36
+ <label for="email" class="form-label">
37
+ <i class="fas fa-envelope"></i> Email Address
38
+ </label>
39
+ <input type="email" class="form-control" id="email" name="email" required>
40
+ </div>
41
+
42
+ <div class="mb-3">
43
+ <label for="password" class="form-label">
44
+ <i class="fas fa-lock"></i> Password
45
+ </label>
46
+ <input type="password" class="form-control" id="password" name="password"
47
+ pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
48
+ title="Password must be at least 8 characters long and include at least one number and one special character"
49
+ required>
50
+ <div class="form-text">
51
+ Password requirements:
52
+ <ul class="mb-0">
53
+ <li>At least 8 characters long</li>
54
+ <li>Must contain at least one number</li>
55
+ <li>Must contain at least one special character (@$!%*#?&)</li>
56
+ </ul>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Hidden field for mosque_id -->
61
+ <input type="hidden" name="mosque_id" value="{{ mosque.id }}">
62
+
63
+ <div class="d-grid gap-2">
64
+ <button type="submit" class="btn btn-primary">
65
+ <i class="fas fa-plus"></i> Create Staff Account
66
+ </button>
67
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
68
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
69
+ </a>
70
+ </div>
71
+ </form>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <script>
78
+ function validatePassword() {
79
+ const password = document.getElementById('password');
80
+ const pattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
81
+
82
+ if (!pattern.test(password.value)) {
83
+ password.setCustomValidity('Password must meet all requirements');
84
+ return false;
85
+ } else {
86
+ password.setCustomValidity('');
87
+ return true;
88
+ }
89
+ }
90
+
91
+ document.getElementById('password').addEventListener('input', validatePassword);
92
+ </script>
93
+ {% endblock %}
main/templates/mosque/announcements.html ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Manage Announcements{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item active">Manage Announcements</li>
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+ <div class="dashboard-header">
17
+ <div class="row align-items-center">
18
+ <div class="col">
19
+ <h2><i class="fas fa-bullhorn"></i> Announcements Management</h2>
20
+ <p class="text-muted mb-0">Create and manage mosque announcements for {{ mosque.name }}</p>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="row g-4">
26
+ <!-- Add Announcement Form -->
27
+ <div class="col-md-4">
28
+ <div class="card dashboard-card">
29
+ <div class="card-header">
30
+ <h4><i class="fas fa-plus-circle"></i> Add Announcement</h4>
31
+ </div>
32
+ <div class="card-body">
33
+ <form method="POST" id="announcementForm">
34
+ <div class="mb-3">
35
+ <label for="title" class="form-label">Title</label>
36
+ <input type="text" class="form-control" id="title" name="title" required>
37
+ </div>
38
+
39
+ <div class="mb-3">
40
+ <label for="content" class="form-label">Content</label>
41
+ <textarea class="form-control" id="content" name="content" rows="4" required></textarea>
42
+ </div>
43
+
44
+ <div class="mb-3">
45
+ <label for="start_date" class="form-label">Start Date</label>
46
+ <input type="date" class="form-control" id="start_date" name="start_date" required>
47
+ </div>
48
+
49
+ <div class="mb-3">
50
+ <label for="start_time" class="form-label">Start Time</label>
51
+ <input type="time" class="form-control" id="start_time" name="start_time" required>
52
+ </div>
53
+
54
+ <div class="mb-3">
55
+ <label for="end_date" class="form-label">End Date</label>
56
+ <input type="date" class="form-control" id="end_date" name="end_date" required>
57
+ </div>
58
+
59
+ <div class="mb-3">
60
+ <label for="end_time" class="form-label">End Time</label>
61
+ <input type="time" class="form-control" id="end_time" name="end_time" required>
62
+ </div>
63
+
64
+ <div class="mb-4">
65
+ <div class="form-check">
66
+ <input type="checkbox" class="form-check-input" id="is_urgent" name="is_urgent" style="border-color: #198754; background-color: transparent;">
67
+ <label class="form-check-label" for="is_urgent">
68
+ <i class="fas fa-exclamation-circle text-success"></i> Mark as Urgent
69
+ </label>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="d-grid">
74
+ <button type="submit" class="btn btn-primary">
75
+ <i class="fas fa-plus"></i> Add Announcement
76
+ </button>
77
+ </div>
78
+ </form>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="mt-4">
83
+ {% if user.role == 'superadmin' %}
84
+ <a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="btn btn-outline-secondary">
85
+ {% else %}
86
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
87
+ {% endif %}
88
+ <i class="fas fa-arrow-left"></i> Back
89
+ </a>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- Announcements List -->
94
+ <div class="col-md-8">
95
+ <div class="card dashboard-card">
96
+ <div class="card-header d-flex justify-content-between align-items-center">
97
+ <h4><i class="fas fa-list-alt"></i> Announcements ({{ announcements|length }})</h4>
98
+ </div>
99
+ <div class="card-body">
100
+ {% if announcements %}
101
+ <div class="announcement-list">
102
+ {# First show non-expired announcements #}
103
+ {% for announcement in announcements|sort(attribute='end_time', reverse=true)|sort(attribute='end_date', reverse=true)|sort(attribute='is_urgent', reverse=true) %}
104
+ {% set is_expired = announcement.end_date < now.date() or (announcement.end_date == now.date() and announcement.end_time < now.time()) %}
105
+ {% if not is_expired %}
106
+ <div class="announcement-item mb-2">
107
+ <div class="d-flex justify-content-between align-items-start">
108
+ <div>
109
+ <h5 class="announcement-title mb-1">
110
+ {% if announcement.is_urgent %}
111
+ <i class="fas fa-exclamation-circle text-danger" title="Urgent Announcement"></i>
112
+ {% endif %}
113
+ {{ announcement.title }}
114
+ </h5>
115
+ <p class="announcement-content mb-2">{{ announcement.content }}</p>
116
+ <p class="text-muted small mb-0">
117
+ <i class="fas fa-calendar"></i>
118
+ {{ announcement.start_date.strftime('%d %b %Y') }} {{ announcement.start_time.strftime('%I:%M %p') }} -
119
+ {{ announcement.end_date.strftime('%d %b %Y') }} {{ announcement.end_time.strftime('%I:%M %p') }}
120
+ </p>
121
+ </div>
122
+ <div class="btn-group">
123
+ <a href="{{ url_for('mosque.edit_announcement', id=announcement.id) }}" class="btn btn-outline-success btn-sm">
124
+ <i class="fas fa-edit"></i>
125
+ </a>
126
+ <form action="{{ url_for('mosque.delete_announcement', id=announcement.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this announcement?');">
127
+ <button type="submit" class="btn btn-outline-success btn-sm">
128
+ <i class="fas fa-trash"></i>
129
+ </button>
130
+ </form>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ {% endif %}
135
+ {% endfor %}
136
+
137
+ {# Then show expired announcements #}
138
+ {% for announcement in announcements|sort(attribute='end_time', reverse=true)|sort(attribute='end_date', reverse=true)|sort(attribute='is_urgent', reverse=true) %}
139
+ {% set is_expired = announcement.end_date < now.date() or (announcement.end_date == now.date() and announcement.end_time < now.time()) %}
140
+ {% if is_expired %}
141
+ <div class="announcement-item mb-2 opacity-75">
142
+ <div class="d-flex justify-content-between align-items-start">
143
+ <div>
144
+ <h5 class="d-flex align-items-center gap-2 mb-2">
145
+ {% if announcement.is_urgent %}
146
+ <i class="fas fa-exclamation-circle text-danger" title="Urgent Announcement"></i>
147
+ {% endif %}
148
+ {{ announcement.title }}
149
+ <span class="badge bg-secondary" title="Past Announcement">Expired</span>
150
+ </h5>
151
+ <p class="announcement-content mb-2">{{ announcement.content }}</p>
152
+ <p class="text-muted small mb-0">
153
+ <i class="fas fa-calendar"></i>
154
+ {{ announcement.start_date.strftime('%d %b %Y') }} {{ announcement.start_time.strftime('%I:%M %p') }} -
155
+ {{ announcement.end_date.strftime('%d %b %Y') }} {{ announcement.end_time.strftime('%I:%M %p') }}
156
+ </p>
157
+ </div>
158
+ <div class="btn-group">
159
+ <a href="{{ url_for('mosque.edit_announcement', id=announcement.id) }}" class="btn btn-outline-success btn-sm">
160
+ <i class="fas fa-edit"></i>
161
+ </a>
162
+ <form action="{{ url_for('mosque.delete_announcement', id=announcement.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this announcement?');">
163
+ <button type="submit" class="btn btn-outline-success btn-sm">
164
+ <i class="fas fa-trash"></i>
165
+ </button>
166
+ </form>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ {% endif %}
171
+ {% endfor %}
172
+ </div>
173
+ {% else %}
174
+ <div class="empty-indicator">
175
+ <i class="fas fa-bullhorn fa-2x mb-3"></i>
176
+ <p>No announcements available</p>
177
+ </div>
178
+ {% endif %}
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <script>
185
+ document.addEventListener('DOMContentLoaded', function() {
186
+ // Style the urgent checkbox when checked
187
+ const urgentCheckbox = document.getElementById('is_urgent');
188
+ urgentCheckbox.addEventListener('change', function() {
189
+ if (this.checked) {
190
+ this.style.backgroundColor = '#198754';
191
+ this.style.borderColor = '#198754';
192
+ } else {
193
+ this.style.backgroundColor = 'transparent';
194
+ this.style.borderColor = '#198754';
195
+ }
196
+ });
197
+
198
+ // Reset form after successful submission
199
+ const messages = {{ get_flashed_messages(with_categories=true)|tojson }};
200
+ messages.forEach(function([category, message]) {
201
+ if (category === 'success' && message.includes('added successfully')) {
202
+ document.getElementById('announcementForm').reset();
203
+ document.getElementById('is_urgent').style.backgroundColor = 'transparent';
204
+ }
205
+ });
206
+ });
207
+ </script>
208
+ {% endblock %}
main/templates/mosque/dashboard.html ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Mosque Dashboard{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item active">Mosque Dashboard</li>
7
+ {% endblock %}
8
+
9
+ {% block content %}
10
+ {% if not user.mosque_id %}
11
+ <div class="text-center py-5">
12
+ <div class="empty-state">
13
+ <i class="fas fa-mosque fa-3x mb-3"></i>
14
+ <h3>Welcome to Your Dashboard</h3>
15
+ <p class="text-muted">You haven't registered a mosque yet. Register your mosque to start managing it.</p>
16
+ <a href="{{ url_for('mosque.register_mosque') }}" class="btn btn-primary mt-3">
17
+ <i class="fas fa-plus"></i> Register Your Mosque
18
+ </a>
19
+ </div>
20
+ </div>
21
+ {% else %}
22
+ <!-- Dashboard Header -->
23
+ <div class="dashboard-header mb-4">
24
+ <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
25
+ <div>
26
+ <h2 class="mb-1"><i class="fas fa-mosque"></i> {{ mosque.name }}</h2>
27
+ <p class="text-muted mb-md-0">
28
+ <i class="fas fa-map-marker-alt"></i> {{ mosque.city }}, {{ mosque.state }}, {{ mosque.country }}
29
+ </p>
30
+ </div>
31
+ {% if user.role in ['admin', 'staff'] %}
32
+ <div class="dropdown">
33
+ <button class="btn btn-primary dropdown-toggle w-md-auto" type="button" id="actionDropdown" data-bs-toggle="dropdown" aria-expanded="false">
34
+ <i class="fas fa-cog"></i> Quick Actions
35
+ </button>
36
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="actionDropdown">
37
+ {% if user.role == 'admin' %}
38
+ <li>
39
+ <a class="dropdown-item" href="{{ url_for('mosque.prayer_times', mosque_id=mosque.id) }}">
40
+ <i class="fas fa-clock"></i> Manage Prayer Times
41
+ </a>
42
+ </li>
43
+ <li>
44
+ <a class="dropdown-item" href="{{ url_for('mosque.announcements') }}">
45
+ <i class="fas fa-bullhorn"></i> Manage Announcements
46
+ </a>
47
+ </li>
48
+ <li>
49
+ <a class="dropdown-item" href="{{ url_for('inventory.inventory_list', mosque_id=mosque.id) }}">
50
+ <i class="fas fa-boxes"></i> Manage Inventory
51
+ </a>
52
+ </li>
53
+ <li>
54
+ <a class="dropdown-item" href="{{ url_for('finance.finance_list', mosque_id=mosque.id) }}">
55
+ <i class="fas fa-money-bill-wave"></i> Manage Finance
56
+ </a>
57
+ </li>
58
+ <li><hr class="dropdown-divider"></li>
59
+ <li>
60
+ <a class="dropdown-item" href="{{ url_for('mosque.add_another_admin') }}">
61
+ <i class="fas fa-user-shield"></i> Add Another Admin
62
+ </a>
63
+ </li>
64
+ <li>
65
+ <a class="dropdown-item" href="{{ url_for('mosque.add_another_staff') }}">
66
+ <i class="fas fa-user-tie"></i> Add Another Staff
67
+ </a>
68
+ </li>
69
+ {% elif user.role == 'staff' %}
70
+ <li>
71
+ <a class="dropdown-item" href="{{ url_for('mosque.prayer_times', mosque_id=mosque.id) }}">
72
+ <i class="fas fa-clock"></i> Manage Prayer Times
73
+ </a>
74
+ </li>
75
+ <li>
76
+ <a class="dropdown-item" href="{{ url_for('mosque.announcements') }}">
77
+ <i class="fas fa-bullhorn"></i> Manage Announcements
78
+ </a>
79
+ </li>
80
+ {% endif %}
81
+ </ul>
82
+ </div>
83
+ {% endif %}
84
+ </div>
85
+ </div>
86
+
87
+ <div class="row g-4">
88
+ <!-- Mosque Information Card -->
89
+ <div class="col-md-4">
90
+ <div class="card dashboard-card border-success border-opacity-25">
91
+ <div class="card-header bg-success bg-opacity-10">
92
+ <div class="d-flex justify-content-between align-items-center">
93
+ <h4 class="mb-0"><i class="fas fa-info-circle text-success"></i> Mosque Details</h4>
94
+ </div>
95
+ </div>
96
+ <div class="card-body">
97
+ <div class="mosque-info-list">
98
+ <div class="info-item">
99
+ <i class="fas fa-map-marked-alt"></i>
100
+ <div>
101
+ <label>Address</label>
102
+ <span>{{ mosque.address }}</span>
103
+ </div>
104
+ </div>
105
+ {% if mosque.phone %}
106
+ <div class="info-item">
107
+ <i class="fas fa-phone"></i>
108
+ <div>
109
+ <label>Phone</label>
110
+ <span>{{ mosque.phone }}</span>
111
+ </div>
112
+ </div>
113
+ {% endif %}
114
+ {% if mosque.email %}
115
+ <div class="info-item">
116
+ <i class="fas fa-envelope"></i>
117
+ <div>
118
+ <label>Email</label>
119
+ <span>{{ mosque.email }}</span>
120
+ </div>
121
+ </div>
122
+ {% endif %}
123
+ {% if mosque.capacity %}
124
+ <div class="info-item">
125
+ <i class="fas fa-users"></i>
126
+ <div>
127
+ <label>Capacity</label>
128
+ <span>{{ mosque.capacity }} people</span>
129
+ </div>
130
+ </div>
131
+ {% endif %}
132
+ <hr>
133
+ <div class="info-item">
134
+ <i class="fas fa-user-shield"></i>
135
+ <div>
136
+ <label>Assigned Admins ({{ assigned_admins|length }})</label>
137
+ {% if assigned_admins %}
138
+ <div class="assigned-admins-list mt-2">
139
+ {% for admin in assigned_admins|sort(attribute='id', reverse=true) %}
140
+ {% if admin.id == user.id %}
141
+ <div class="assigned-admin-item mb-2">
142
+ <span class="admin-name small">{{ admin.name }} <span class="text-success">(You)</span></span>
143
+ <br>
144
+ <small class="text-muted">{{ admin.email }}</small>
145
+ </div>
146
+ {% endif %}
147
+ {% endfor %}
148
+ {% for admin in assigned_admins|sort(attribute='id', reverse=true) %}
149
+ {% if admin.id != user.id %}
150
+ <div class="assigned-admin-item mb-2">
151
+ <span class="admin-name small">{{ admin.name }}</span>
152
+ <br>
153
+ <small class="text-muted">{{ admin.email }}</small>
154
+ </div>
155
+ {% endif %}
156
+ {% endfor %}
157
+ </div>
158
+ {% else %}
159
+ <span class="text-muted">No other administrators assigned</span>
160
+ {% endif %}
161
+ </div>
162
+ </div>
163
+
164
+ <div class="info-item">
165
+ <i class="fas fa-user-tie"></i>
166
+ <div>
167
+ <label>Staff Members ({{ staff_members|length }})</label>
168
+ {% if staff_members %}
169
+ <div class="assigned-admins-list mt-2">
170
+ {% for staff in staff_members|sort(attribute='id', reverse=true) %}
171
+ <div class="assigned-admin-item mb-2">
172
+ <span class="admin-name small">{{ staff.name }}
173
+ {% if staff.id == user.id %}
174
+ <span class="text-success">(You)</span>
175
+ {% endif %}
176
+ </span>
177
+ <br>
178
+ <small class="text-muted">{{ staff.email }}</small>
179
+ </div>
180
+ {% endfor %}
181
+ </div>
182
+ {% else %}
183
+ <span class="text-muted">No staff members assigned</span>
184
+ {% endif %}
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <!-- Prayer Times Card -->
193
+ <div class="col-md-4">
194
+ <div class="card dashboard-card border-success border-opacity-25">
195
+ <div class="card-header bg-success bg-opacity-10">
196
+ <div class="d-flex justify-content-between align-items-center">
197
+ <h4 class="mb-0">
198
+ <i class="fas fa-clock text-success"></i> Prayer Times for <span id="todayDate">Today</span>
199
+ </h4>
200
+ <span class="badge bg-success">{{ prayer_times|length }}</span>
201
+ </div>
202
+ </div>
203
+ <div class="card-body">
204
+ {% if prayer_times %}
205
+ <div class="prayer-times-list">
206
+ {% for prayer in prayer_times|sort(attribute='order') %}
207
+ <div class="prayer-time-item">
208
+ <div class="prayer-name">{{ prayer.prayer_name }}</div>
209
+ <div class="prayer-time">{{ prayer.time.strftime('%I:%M %p') }}</div>
210
+ </div>
211
+ {% endfor %}
212
+ </div>
213
+ {% else %}
214
+ <div class="empty-indicator">
215
+ <i class="fas fa-calendar-day fa-2x"></i>
216
+ <p>No prayer times scheduled for today</p>
217
+ </div>
218
+ {% endif %}
219
+ <div class="text-end mt-3">
220
+ <a href="{{ url_for('mosque.prayer_times', mosque_id=mosque.id) }}" class="text-success text-decoration-none">
221
+ View Prayer Times Schedule <i class="fas fa-chevron-right"></i>
222
+ </a>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Announcements Card -->
229
+ <div class="col-md-4">
230
+ <div class="card dashboard-card border-success border-opacity-25">
231
+ <div class="card-header bg-success bg-opacity-10">
232
+ <div class="d-flex justify-content-between align-items-center">
233
+ <h4 class="mb-0"><i class="fas fa-bullhorn text-success"></i> Current Announcements</h4>
234
+ <span class="badge bg-success">{{ active_announcements|length }}</span>
235
+ </div>
236
+ </div>
237
+ <div class="card-body">
238
+ {% if active_announcements %}
239
+ <div class="announcements-list">
240
+ {# First show non-expired announcements #}
241
+ {% for announcement in active_announcements|sort(attribute='end_time', reverse=true)|sort(attribute='end_date', reverse=true)|sort(attribute='is_urgent', reverse=true) %}
242
+ {% set is_expired = announcement.end_date < now.date() or (announcement.end_date == now.date() and announcement.end_time < now.time()) %}
243
+ {% if not is_expired %}
244
+ <div class="announcement-item mb-2">
245
+ <div class="d-flex justify-content-between align-items-start">
246
+ <div>
247
+ <h5 class="announcement-title mb-1">
248
+ {% if announcement.is_urgent %}
249
+ <i class="fas fa-exclamation-circle text-danger" title="Urgent Announcement"></i>
250
+ {% endif %}
251
+ {{ announcement.title }}
252
+ </h5>
253
+ <p class="announcement-content mb-2">{{ announcement.content }}</p>
254
+ <p class="text-muted small mb-0">
255
+ <i class="fas fa-calendar"></i>
256
+ {{ announcement.start_date.strftime('%d %b %Y') }} {{ announcement.start_time.strftime('%I:%M %p') }} -
257
+ {{ announcement.end_date.strftime('%d %b %Y') }} {{ announcement.end_time.strftime('%I:%M %p') }}
258
+ </p>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ {% endif %}
263
+ {% endfor %}
264
+
265
+ {# Then show expired announcements #}
266
+ {% for announcement in active_announcements|sort(attribute='end_time', reverse=true)|sort(attribute='end_date', reverse=true)|sort(attribute='is_urgent', reverse=true) %}
267
+ {% set is_expired = announcement.end_date < now.date() or (announcement.end_date == now.date() and announcement.end_time < now.time()) %}
268
+ {% if is_expired %}
269
+ <div class="announcement-item mb-2 opacity-75">
270
+ <div class="d-flex justify-content-between align-items-start">
271
+ <div>
272
+ <h5 class="announcement-title mb-1">
273
+ {% if announcement.is_urgent %}
274
+ <i class="fas fa-exclamation-circle text-danger" title="Urgent Announcement"></i>
275
+ {% endif %}
276
+ {{ announcement.title }}
277
+ <span class="badge bg-secondary" title="Past Announcement">Expired</span>
278
+ </h5>
279
+ <p class="announcement-content mb-2">{{ announcement.content }}</p>
280
+ <p class="text-muted small mb-0">
281
+ <i class="fas fa-calendar"></i>
282
+ {{ announcement.start_date.strftime('%d %b %Y') }} {{ announcement.start_time.strftime('%I:%M %p') }} -
283
+ {{ announcement.end_date.strftime('%d %b %Y') }} {{ announcement.end_time.strftime('%I:%M %p') }}
284
+ </p>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ {% endif %}
289
+ {% endfor %}
290
+ </div>
291
+ {% else %}
292
+ <div class="empty-indicator">
293
+ <i class="fas fa-bullhorn fa-2x mb-3"></i>
294
+ <p>No announcements available</p>
295
+ </div>
296
+ {% endif %}
297
+ <div class="text-end mt-3">
298
+ <a href="{{ url_for('mosque.announcements') }}" class="text-success text-decoration-none">
299
+ View All Announcements <i class="fas fa-chevron-right"></i>
300
+ </a>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ {% endif %}
307
+ {% endblock %}
308
+
309
+ {% block scripts %}
310
+ <script>
311
+ // Format today's date
312
+ const today = new Date();
313
+ const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
314
+ document.getElementById('todayDate').textContent = today.toLocaleDateString('en-US', options);
315
+ </script>
316
+ {% endblock %}
main/templates/mosque/edit_announcement.html ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Edit Announcement{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.announcements') }}" class="text-success">Announcements</a></li>
13
+ <li class="breadcrumb-item active">Edit Announcement</li>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ <div class="dashboard-header">
18
+ <div class="row align-items-center">
19
+ <div class="col">
20
+ <h2><i class="fas fa-edit"></i> Edit Announcement</h2>
21
+ <p class="text-muted mb-0">Update announcement details</p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="row justify-content-center">
27
+ <div class="col-md-8">
28
+ <div class="card dashboard-card">
29
+ <div class="card-header">
30
+ <h4><i class="fas fa-bullhorn"></i> Announcement Details</h4>
31
+ </div>
32
+ <div class="card-body">
33
+ <form method="POST">
34
+ <div class="mb-3">
35
+ <label for="title" class="form-label">Title</label>
36
+ <input type="text" class="form-control" id="title" name="title" value="{{ announcement.title }}" required>
37
+ </div>
38
+
39
+ <div class="mb-3">
40
+ <label for="content" class="form-label">Content</label>
41
+ <textarea class="form-control" id="content" name="content" rows="4" required>{{ announcement.content }}</textarea>
42
+ </div>
43
+
44
+ <div class="row">
45
+ <div class="col-md-6">
46
+ <div class="mb-3">
47
+ <label for="start_date" class="form-label">Start Date</label>
48
+ <input type="date" class="form-control" id="start_date" name="start_date" value="{{ announcement.start_date.strftime('%Y-%m-%d') }}" required>
49
+ </div>
50
+ </div>
51
+ <div class="col-md-6">
52
+ <div class="mb-3">
53
+ <label for="start_time" class="form-label">Start Time</label>
54
+ <input type="time" class="form-control" id="start_time" name="start_time" value="{{ announcement.start_time.strftime('%H:%M') }}" required>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="row">
60
+ <div class="col-md-6">
61
+ <div class="mb-3">
62
+ <label for="end_date" class="form-label">End Date</label>
63
+ <input type="date" class="form-control" id="end_date" name="end_date" value="{{ announcement.end_date.strftime('%Y-%m-%d') }}" required>
64
+ </div>
65
+ </div>
66
+ <div class="col-md-6">
67
+ <div class="mb-3">
68
+ <label for="end_time" class="form-label">End Time</label>
69
+ <input type="time" class="form-control" id="end_time" name="end_time" value="{{ announcement.end_time.strftime('%H:%M') }}" required>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="mb-4">
75
+ <div class="form-check">
76
+ <input type="checkbox" class="form-check-input" id="is_urgent" name="is_urgent"
77
+ style="border-color: #198754; background-color: {% if announcement.is_urgent %}#198754{% else %}transparent{% endif %};"
78
+ {% if announcement.is_urgent %}checked{% endif %}>
79
+ <label class="form-check-label" for="is_urgent">
80
+ <i class="fas fa-exclamation-circle text-success"></i> Mark as Urgent
81
+ </label>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="d-grid gap-2">
86
+ <button type="submit" class="btn btn-primary">
87
+ <i class="fas fa-save"></i> Update Announcement
88
+ </button>
89
+ <div class="mt-3">
90
+ {% if user.role == 'superadmin' %}
91
+ <a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="btn btn-outline-secondary">
92
+ {% else %}
93
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
94
+ {% endif %}
95
+ <i class="fas fa-arrow-left"></i> Back
96
+ </a>
97
+ </div>
98
+ </div>
99
+ </form>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ {% endblock %}
main/templates/mosque/mosque_info.html ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{{ mosque.name }} - Info{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.home') }}" class="text-success">Home</a></li>
7
+ <li class="breadcrumb-item active">Mosque Info</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-mosque"></i> {{ mosque.name }}</h2>
15
+ <p class="text-muted mb-0">
16
+ <i class="fas fa-map-marker-alt"></i> {{ mosque.city }}, {{ mosque.state }}, {{ mosque.country }}
17
+ </p>
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="row g-4">
23
+ <!-- Mosque Information -->
24
+ <div class="col-md-4">
25
+ <div class="card dashboard-card border-success border-opacity-25">
26
+ <div class="card-header bg-success bg-opacity-10">
27
+ <div class="d-flex justify-content-between align-items-center">
28
+ <h4 class="mb-0"><i class="fas fa-info-circle text-success"></i> Mosque Information</h4>
29
+ </div>
30
+ </div>
31
+ <div class="card-body">
32
+ <div class="mosque-info-list">
33
+ <div class="info-item">
34
+ <i class="fas fa-map-marked-alt"></i>
35
+ <div>
36
+ <label>Address</label>
37
+ <span>{{ mosque.address }}</span>
38
+ </div>
39
+ </div>
40
+ {% if mosque.phone %}
41
+ <div class="info-item">
42
+ <i class="fas fa-phone"></i>
43
+ <div>
44
+ <label>Phone</label>
45
+ <span>{{ mosque.phone }}</span>
46
+ </div>
47
+ </div>
48
+ {% endif %}
49
+ {% if mosque.email %}
50
+ <div class="info-item">
51
+ <i class="fas fa-envelope"></i>
52
+ <div>
53
+ <label>Email</label>
54
+ <span>{{ mosque.email }}</span>
55
+ </div>
56
+ </div>
57
+ {% endif %}
58
+ {% if mosque.capacity %}
59
+ <div class="info-item">
60
+ <i class="fas fa-users"></i>
61
+ <div>
62
+ <label>Capacity</label>
63
+ <span>{{ mosque.capacity }} people</span>
64
+ </div>
65
+ </div>
66
+ {% endif %}
67
+ </div>
68
+ </div>
69
+ </div>
70
+ <div class="mt-3">
71
+ <a href="{{ url_for('mosque.home') }}" class="btn btn-outline-secondary">
72
+ <i class="fas fa-arrow-left"></i> Back to Home
73
+ </a>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Prayer Times Card -->
78
+ <div class="col-md-4">
79
+ <div class="card dashboard-card border-success border-opacity-25">
80
+ <div class="card-header bg-success bg-opacity-10">
81
+ <div class="d-flex justify-content-between align-items-center">
82
+ <h4 class="mb-0">
83
+ <i class="fas fa-clock text-success"></i> Prayer Times for <span id="todayDate">Today</span>
84
+ </h4>
85
+ <span class="badge bg-success">{{ prayer_times|length }}</span>
86
+ </div>
87
+ </div>
88
+ <div class="card-body">
89
+ {% if prayer_times %}
90
+ <div class="prayer-times-list">
91
+ {% for prayer in prayer_times|sort(attribute='order') %}
92
+ <div class="prayer-time-item">
93
+ <div class="prayer-name">{{ prayer.prayer_name }}</div>
94
+ <div class="prayer-time">{{ prayer.time.strftime('%I:%M %p') }}</div>
95
+ </div>
96
+ {% endfor %}
97
+ </div>
98
+ {% else %}
99
+ <div class="empty-indicator">
100
+ <i class="fas fa-calendar-day fa-2x"></i>
101
+ <p>No prayer times scheduled for today</p>
102
+ </div>
103
+ {% endif %}
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Announcements -->
109
+ <div class="col-md-4">
110
+ <div class="card dashboard-card border-success border-opacity-25">
111
+ <div class="card-header bg-success bg-opacity-10">
112
+ <div class="d-flex justify-content-between align-items-center">
113
+ <h4 class="mb-0"><i class="fas fa-bullhorn text-success"></i> Active Announcements</h4>
114
+ <span class="badge bg-success">{{ active_announcements|length }}</span>
115
+ </div>
116
+ </div>
117
+ <div class="card-body">
118
+ {% if active_announcements %}
119
+ <div class="announcements-list">
120
+ {% for announcement in active_announcements|sort(attribute='end_time', reverse=true)|sort(attribute='end_date', reverse=true)|sort(attribute='is_urgent', reverse=true) %}
121
+ <div class="announcement-item mb-2">
122
+ <div class="d-flex justify-content-between align-items-start">
123
+ <div>
124
+ <h5 class="announcement-title mb-1">
125
+ {% if announcement.is_urgent %}
126
+ <i class="fas fa-exclamation-circle text-danger" title="Urgent Announcement"></i>
127
+ {% endif %}
128
+ {{ announcement.title }}
129
+ </h5>
130
+ <p class="announcement-content mb-2">{{ announcement.content }}</p>
131
+ <p class="text-muted small mb-0">
132
+ <i class="fas fa-calendar"></i>
133
+ {{ announcement.start_date.strftime('%d %b %Y') }} {{ announcement.start_time.strftime('%I:%M %p') }} -
134
+ {{ announcement.end_date.strftime('%d %b %Y') }} {{ announcement.end_time.strftime('%I:%M %p') }}
135
+ </p>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ {% endfor %}
140
+ </div>
141
+ {% else %}
142
+ <div class="empty-indicator">
143
+ <i class="fas fa-bullhorn fa-2x mb-3"></i>
144
+ <p>No announcements available</p>
145
+ </div>
146
+ {% endif %}
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ {% endblock %}
152
+
153
+ {% block scripts %}
154
+ <script>
155
+ document.addEventListener('DOMContentLoaded', function() {
156
+ // Format and display today's date
157
+ const today = new Date();
158
+ const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
159
+ document.getElementById('todayDate').textContent = today.toLocaleDateString('en-US', options);
160
+ });
161
+ </script>
162
+ {% endblock %}
main/templates/mosque/prayer_times.html ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Manage Prayer Times{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ {% if user.role == 'superadmin' %}
7
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}" class="text-success">Admin Dashboard</a></li>
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="text-success">Mosque Details</a></li>
9
+ {% else %}
10
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
11
+ {% endif %}
12
+ <li class="breadcrumb-item active">Manage Prayer Times</li>
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+ <div class="dashboard-header">
17
+ <div class="row align-items-center">
18
+ <div class="col">
19
+ <h2><i class="fas fa-clock"></i> Prayer Times Management</h2>
20
+ <p class="text-muted mb-0">Manage daily prayer schedules for {{ mosque.name }}</p>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="row g-4">
26
+ <!-- Add Prayer Time Form -->
27
+ <div class="col-md-4">
28
+ <div class="card dashboard-card">
29
+ <div class="card-header">
30
+ <h4><i class="fas fa-plus-circle"></i> Add Prayer Time</h4>
31
+ </div>
32
+ <div class="card-body">
33
+ <div class="mb-3">
34
+ <button type="button" class="btn btn-outline-success w-100" id="autoFillBtn" onclick="fetchJakimPrayerTimes()">
35
+ <i class="fas fa-cloud-download-alt"></i> Auto-fill Prayer Times from JAKIM
36
+ </button>
37
+ </div>
38
+ <div id="importProgress" class="d-none">
39
+ <div class="progress mb-2">
40
+ <div class="progress-bar bg-success progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
41
+ </div>
42
+ <p class="text-center text-muted small">Importing prayer times...</p>
43
+ </div>
44
+ <form method="POST" id="prayerTimeForm">
45
+ <div class="mb-3">
46
+ <label for="prayer_name" class="form-label">Prayer Name</label>
47
+ <select class="form-select" id="prayer_name" name="prayer_name" required>
48
+ <option value="" disabled selected>Select prayer</option>
49
+ <option value="Imsak">Imsak</option>
50
+ <option value="Subuh">Subuh</option>
51
+ <option value="Syuruk">Syuruk</option>
52
+ <option value="Zuhur">Zuhur</option>
53
+ <option value="Asar">Asar</option>
54
+ <option value="Maghrib">Maghrib</option>
55
+ <option value="Isyak">Isyak</option>
56
+ </select>
57
+ </div>
58
+
59
+ <div class="mb-3">
60
+ <label for="time" class="form-label">Prayer Time</label>
61
+ <input type="time" class="form-control" id="time" name="time" required>
62
+ </div>
63
+
64
+ <div class="mb-4">
65
+ <label for="date" class="form-label">Date</label>
66
+ <input type="date" class="form-control" id="date" name="date" required>
67
+ </div>
68
+
69
+ <div class="d-grid">
70
+ <button type="submit" class="btn btn-primary">
71
+ <i class="fas fa-plus"></i> Add Prayer Time
72
+ </button>
73
+ </div>
74
+ </form>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="mt-4">
79
+ {% if user.role == 'superadmin' %}
80
+ <a href="{{ url_for('admin.mosque_details', id=mosque.id) }}" class="btn btn-outline-secondary">
81
+ {% else %}
82
+ <a href="{{ url_for('mosque.dashboard') }}" class="btn btn-outline-secondary">
83
+ {% endif %}
84
+ <i class="fas fa-arrow-left"></i> Back
85
+ </a>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Prayer Times List -->
90
+ <div class="col-md-8">
91
+ <div class="card dashboard-card">
92
+ <div class="card-header d-flex justify-content-between align-items-center">
93
+ <h4><i class="fas fa-list"></i> Scheduled Prayer Times ({{ prayer_times|length }})</h4>
94
+ </div>
95
+ <div class="card-body p-0">
96
+ {% if prayer_times %}
97
+ <div class="table-responsive">
98
+ <table class="table table-hover mb-0">
99
+ <thead class="table-light">
100
+ <tr>
101
+ <th>Prayer Name</th>
102
+ <th>Date</th>
103
+ <th>Time</th>
104
+ <th class="text-end">Actions</th>
105
+ </tr>
106
+ </thead>
107
+ <tbody>
108
+ {% for prayer in prayer_times|sort(attribute='date,order,time') %}
109
+ <tr{% if ((loop.index0 // 7) % 2) == 1 %} class="table-light"{% endif %}>
110
+ <td>{{ prayer.prayer_name }}</td>
111
+ <td>{{ prayer.date.strftime('%d %B %Y') }}</td>
112
+ <td>{{ prayer.time.strftime('%I:%M %p') }}</td>
113
+ <td class="text-end">
114
+ <form method="POST" action="{{ url_for('mosque.delete_prayer_time', id=prayer.id) }}" class="d-inline">
115
+ <button type="submit" class="btn btn-sm btn-outline-success" onclick="return confirm('Are you sure you want to delete this prayer time?')">
116
+ <i class="fas fa-trash"></i>
117
+ </button>
118
+ </form>
119
+ </td>
120
+ </tr>
121
+ {% endfor %}
122
+ </tbody>
123
+ </table>
124
+ </div>
125
+ {% else %}
126
+ <div class="text-center py-4">
127
+ <div class="empty-indicator">
128
+ <i class="fas fa-clock fa-2x mb-3 text-muted"></i>
129
+ <p class="text-muted">No prayer times scheduled yet</p>
130
+ <p class="text-muted small">Add your first prayer time using the form on the left</p>
131
+ </div>
132
+ </div>
133
+ {% endif %}
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <script>
140
+ // Reset form after successful submission
141
+ document.getElementById('prayerTimeForm').addEventListener('submit', function() {
142
+ if (this.checkValidity()) {
143
+ setTimeout(() => {
144
+ this.reset();
145
+ }, 100);
146
+ }
147
+ });
148
+
149
+ // Convert Unix timestamp to HH:mm format
150
+ function formatTime(timestamp) {
151
+ const date = new Date(timestamp * 1000);
152
+ return date.toTimeString().slice(0, 5);
153
+ }
154
+
155
+ // Calculate Imsak time (10 minutes before Subuh)
156
+ function calculateImsakTime(subuhTime) {
157
+ const [hours, minutes] = subuhTime.split(':').map(Number);
158
+ let totalMinutes = hours * 60 + minutes - 10;
159
+
160
+ // Handle case where Imsak time goes to previous day
161
+ if (totalMinutes < 0) {
162
+ totalMinutes += 24 * 60;
163
+ }
164
+
165
+ const imsakHours = Math.floor(totalMinutes / 60);
166
+ const imsakMinutes = totalMinutes % 60;
167
+
168
+ return `${imsakHours.toString().padStart(2, '0')}:${imsakMinutes.toString().padStart(2, '0')}`;
169
+ }
170
+
171
+ // Convert prayer name from API to match our format
172
+ function convertPrayerName(apiName) {
173
+ const mapping = {
174
+ 'fajr': 'Subuh',
175
+ 'syuruk': 'Syuruk',
176
+ 'dhuhr': 'Zuhur',
177
+ 'asr': 'Asar',
178
+ 'maghrib': 'Maghrib',
179
+ 'isha': 'Isyak'
180
+ };
181
+ return mapping[apiName] || apiName;
182
+ }
183
+
184
+ // Format date as YYYY-MM-DD for input field
185
+ function formatDate(year, month, day) {
186
+ const monthNum = new Date(Date.parse(month + " 1, 2000")).getMonth() + 1;
187
+ return `${year}-${monthNum.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
188
+ }
189
+
190
+ // Show/hide loading indicator
191
+ function toggleLoading(show) {
192
+ const btn = document.getElementById('autoFillBtn');
193
+ const progress = document.getElementById('importProgress');
194
+
195
+ if (show) {
196
+ btn.disabled = true;
197
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Importing...';
198
+ progress.classList.remove('d-none');
199
+ } else {
200
+ btn.disabled = false;
201
+ btn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> Auto-fill Prayer Times from JAKIM';
202
+ progress.classList.add('d-none');
203
+ }
204
+ }
205
+
206
+ // Fetch and process prayer times from JAKIM API
207
+ async function fetchJakimPrayerTimes() {
208
+ try {
209
+ const jakimCode = '{{ mosque.jakim_code }}';
210
+ if (!jakimCode) {
211
+ alert('No JAKIM zone code set for this mosque.');
212
+ return;
213
+ }
214
+
215
+ toggleLoading(true);
216
+
217
+ const response = await fetch(`https://api.waktusolat.app/v2/solat/${jakimCode}`);
218
+ if (!response.ok) {
219
+ throw new Error('Failed to fetch prayer times');
220
+ }
221
+
222
+ const data = await response.json();
223
+ const prayers = data.prayers;
224
+
225
+ // Prepare batch data
226
+ const batchData = [];
227
+
228
+ // Process each day's prayer times
229
+ for (const prayer of prayers) {
230
+ const date = formatDate(data.year, data.month, prayer.day);
231
+
232
+ // Get Subuh time first to calculate Imsak
233
+ const subuhTime = formatTime(prayer.fajr);
234
+ const imsakTime = calculateImsakTime(subuhTime);
235
+
236
+ const prayerTimes = {
237
+ 'fajr': prayer.fajr,
238
+ 'syuruk': prayer.syuruk,
239
+ 'dhuhr': prayer.dhuhr,
240
+ 'asr': prayer.asr,
241
+ 'maghrib': prayer.maghrib,
242
+ 'isha': prayer.isha
243
+ };
244
+
245
+ // Add Imsak time first (it should appear first in the list)
246
+ batchData.push({
247
+ prayer_name: 'Imsak',
248
+ time: imsakTime,
249
+ date: date
250
+ });
251
+
252
+ // Add other prayer times
253
+ for (const [apiName, timestamp] of Object.entries(prayerTimes)) {
254
+ batchData.push({
255
+ prayer_name: convertPrayerName(apiName),
256
+ time: formatTime(timestamp),
257
+ date: date
258
+ });
259
+ }
260
+ }
261
+
262
+ // Send batch import request
263
+ const importResponse = await fetch('{{ url_for("mosque.import_prayer_times", mosque_id=mosque.id) }}', {
264
+ method: 'POST',
265
+ headers: {
266
+ 'Content-Type': 'application/json',
267
+ },
268
+ body: JSON.stringify({ prayer_times: batchData })
269
+ });
270
+
271
+ if (!importResponse.ok) {
272
+ throw new Error('Failed to import prayer times');
273
+ }
274
+
275
+ const result = await importResponse.json();
276
+
277
+ if (result.success) {
278
+ // Show appropriate message based on status
279
+ if (result.status === 'no_changes') {
280
+ // All prayer times already exist
281
+ alert(result.message);
282
+ } else {
283
+ // Show success message with details
284
+ alert(result.message);
285
+ }
286
+ // Always refresh to ensure view is up to date
287
+ window.location.reload();
288
+ } else {
289
+ throw new Error(result.message || 'Failed to import prayer times');
290
+ }
291
+ } catch (error) {
292
+ console.error('Error importing prayer times:', error);
293
+ alert('Failed to import prayer times. Please try again later.');
294
+ } finally {
295
+ toggleLoading(false);
296
+ }
297
+ }
298
+ </script>
299
+ {% endblock %}
main/templates/mosque/register_mosque.html ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Register Mosque{% endblock %}
4
+
5
+ {% block breadcrumbs %}
6
+ <li class="breadcrumb-item"><a href="{{ url_for('mosque.dashboard') }}" class="text-success">Mosque Dashboard</a></li>
7
+ <li class="breadcrumb-item active">Register Mosque</li>
8
+ {% endblock %}
9
+
10
+ {% block content %}
11
+ <div class="dashboard-header">
12
+ <div class="row align-items-center">
13
+ <div class="col">
14
+ <h2><i class="fas fa-mosque"></i> Register Your Mosque</h2>
15
+ <p class="text-muted mb-0">Register your mosque and become its administrator</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row justify-content-center">
21
+ <div class="col-md-8">
22
+ <div class="alert alert-info">
23
+ <i class="fas fa-info-circle"></i> Registering a mosque will automatically make you its administrator.
24
+ </div>
25
+
26
+ <div class="card dashboard-card">
27
+ <div class="card-header">
28
+ <h4><i class="fas fa-info-circle"></i> Mosque Information</h4>
29
+ </div>
30
+ <div class="card-body p-4">
31
+ <form method="POST">
32
+ <div class="row">
33
+ <div class="col-md-6">
34
+ <div class="mb-3">
35
+ <label for="name" class="form-label">
36
+ <i class="fas fa-building"></i> Mosque Name
37
+ </label>
38
+ <input type="text" class="form-control" id="name" name="name" required>
39
+ </div>
40
+
41
+ <div class="mb-3">
42
+ <label for="address" class="form-label">
43
+ <i class="fas fa-map-marker-alt"></i> Address
44
+ </label>
45
+ <input type="text" class="form-control" id="address" name="address" required>
46
+ </div>
47
+
48
+ <div class="mb-3">
49
+ <label for="city" class="form-label">
50
+ <i class="fas fa-city"></i> City
51
+ </label>
52
+ <input type="text" class="form-control" id="city" name="city" required>
53
+ </div>
54
+
55
+ <div class="mb-3">
56
+ <label for="state" class="form-label">
57
+ <i class="fas fa-map"></i> State/Province
58
+ </label>
59
+ <select class="form-select" id="state" name="state" required>
60
+ <option value="" disabled selected>Select state</option>
61
+ {% for state in ['Johor', 'Kedah', 'Kelantan', 'Melaka', 'Negeri Sembilan', 'Pahang', 'Perak', 'Perlis', 'Pulau Pinang', 'Sabah', 'Sarawak', 'Selangor', 'Terengganu', 'Kuala Lumpur', 'Labuan', 'Putrajaya'] %}
62
+ <option value="{{ state }}">{{ state }}</option>
63
+ {% endfor %}
64
+ </select>
65
+ </div>
66
+
67
+ <div class="mb-3">
68
+ <label for="jakim_code" class="form-label">
69
+ <i class="fas fa-clock"></i> JAKIM Prayer Time Zone
70
+ </label>
71
+ <select class="form-select" id="jakim_code" name="jakim_code" required>
72
+ <option value="" disabled selected>Select prayer time zone</option>
73
+ <optgroup label="Johor">
74
+ <option value="JHR01">JHR01 (Pulau Aur dan Pulau Pemanggil)</option>
75
+ <option value="JHR02">JHR02 (Johor Bahru, Kota Tinggi, Mersing, Kulai)</option>
76
+ <option value="JHR03">JHR03 (Kluang, Pontian)</option>
77
+ <option value="JHR04">JHR04 (Batu Pahat, Muar, Segamat, Gemas Johor, Tangkak)</option>
78
+ </optgroup>
79
+ <optgroup label="Kedah">
80
+ <option value="KDH01">KDH01 (Kota Setar, Kubang Pasu, Pokok Sena)</option>
81
+ <option value="KDH02">KDH02 (Kuala Muda, Yan, Pendang)</option>
82
+ <option value="KDH03">KDH03 (Padang Terap, Sik)</option>
83
+ <option value="KDH04">KDH04 (Baling)</option>
84
+ <option value="KDH05">KDH05 (Bandar Baharu, Kulim)</option>
85
+ <option value="KDH06">KDH06 (Langkawi)</option>
86
+ <option value="KDH07">KDH07 (Puncak Gunung Jerai)</option>
87
+ </optgroup>
88
+ <optgroup label="Kelantan">
89
+ <option value="KTN01">KTN01 (Bachok, Kota Bharu, Machang, Pasir Mas, dan lain-lain)</option>
90
+ <option value="KTN02">KTN02 (Gua Musang, Jeli, Lojing)</option>
91
+ </optgroup>
92
+ <optgroup label="Melaka">
93
+ <option value="MLK01">MLK01 (Melaka)</option>
94
+ </optgroup>
95
+ <optgroup label="Negeri Sembilan">
96
+ <option value="NGS01">NGS01 (Tampin, Jempol)</option>
97
+ <option value="NGS02">NGS02 (Jelebu, Kuala Pilah, Rembau)</option>
98
+ <option value="NGS03">NGS03 (Port Dickson, Seremban)</option>
99
+ </optgroup>
100
+ <optgroup label="Pahang">
101
+ <option value="PHG01">PHG01 (Pulau Tioman)</option>
102
+ <option value="PHG02">PHG02 (Kuantan, Pekan, Muadzam Shah)</option>
103
+ <option value="PHG03">PHG03 (Jerantut, Temerloh, Maran, dan lain-lain)</option>
104
+ <option value="PHG04">PHG04 (Bentong, Lipis, Raub)</option>
105
+ <option value="PHG05">PHG05 (Genting Sempah, Janda Baik, Bukit Tinggi)</option>
106
+ <option value="PHG06">PHG06 (Cameron Highlands, Genting Highlands, Bukit Fraser)</option>
107
+ <option value="PHG07">PHG07 (Rompin, Endau, Pontian)</option>
108
+ </optgroup>
109
+ <optgroup label="Perak">
110
+ <option value="PRK01">PRK01 (Tapah, Slim River, Tanjung Malim)</option>
111
+ <option value="PRK02">PRK02 (Kuala Kangsar, Sg. Siput, Ipoh, dan lain-lain)</option>
112
+ <option value="PRK03">PRK03 (Lenggong, Pengkalan Hulu, Grik)</option>
113
+ <option value="PRK04">PRK04 (Temengor, Belum)</option>
114
+ <option value="PRK05">PRK05 (Kg Gajah, Teluk Intan, Bagan Datuk, dan lain-lain)</option>
115
+ <option value="PRK06">PRK06 (Selama, Taiping, Bagan Serai, Parit Buntar)</option>
116
+ <option value="PRK07">PRK07 (Bukit Larut)</option>
117
+ </optgroup>
118
+ <optgroup label="Perlis">
119
+ <option value="PLS01">PLS01 (Perlis)</option>
120
+ </optgroup>
121
+ <optgroup label="Pulau Pinang">
122
+ <option value="PNG01">PNG01 (Pulau Pinang)</option>
123
+ </optgroup>
124
+ <optgroup label="Sabah">
125
+ <option value="SBH01">SBH01 (Sandakan)</option>
126
+ <option value="SBH02">SBH02 (Beluran)</option>
127
+ <option value="SBH03">SBH03 (Lahad Datu)</option>
128
+ <option value="SBH04">SBH04 (Bandar Tawau)</option>
129
+ <option value="SBH05">SBH05 (Kudat)</option>
130
+ <option value="SBH06">SBH06 (Gunung Kinabalu)</option>
131
+ <option value="SBH07">SBH07 (Kota Kinabalu)</option>
132
+ <option value="SBH08">SBH08 (Pensiangan)</option>
133
+ <option value="SBH09">SBH09 (Beaufort)</option>
134
+ </optgroup>
135
+ <optgroup label="Sarawak">
136
+ <option value="SWK01">SWK01 (Limbang)</option>
137
+ <option value="SWK02">SWK02 (Miri)</option>
138
+ <option value="SWK03">SWK03 (Pandan)</option>
139
+ <option value="SWK04">SWK04 (Sibu)</option>
140
+ <option value="SWK05">SWK05 (Sarikei)</option>
141
+ <option value="SWK06">SWK06 (Lubok Antu)</option>
142
+ <option value="SWK07">SWK07 (Serian)</option>
143
+ <option value="SWK08">SWK08 (Kuching)</option>
144
+ <option value="SWK09">SWK09 (Other)</option>
145
+ </optgroup>
146
+ <optgroup label="Selangor">
147
+ <option value="SGR01">SGR01 (Gombak)</option>
148
+ <option value="SGR02">SGR02 (Kuala Selangor)</option>
149
+ <option value="SGR03">SGR03 (Klang)</option>
150
+ </optgroup>
151
+ <optgroup label="Terengganu">
152
+ <option value="TRG01">TRG01 (Kuala Terengganu)</option>
153
+ <option value="TRG02">TRG02 (Besut)</option>
154
+ <option value="TRG03">TRG03 (Hulu Terengganu)</option>
155
+ <option value="TRG04">TRG04 (Dungun)</option>
156
+ </optgroup>
157
+ <optgroup label="Wilayah Persekutuan">
158
+ <option value="WLY01">WLY01 (Kuala Lumpur)</option>
159
+ <option value="WLY02">WLY02 (Labuan)</option>
160
+ </optgroup>
161
+ </select>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="col-md-6">
166
+ <input type="hidden" id="country" name="country" value="Malaysia">
167
+
168
+ <div class="mb-3">
169
+ <label for="phone" class="form-label">
170
+ <i class="fas fa-phone"></i> Phone Number
171
+ </label>
172
+ <input type="tel" class="form-control" id="phone" name="phone">
173
+ <div class="form-text">Optional</div>
174
+ </div>
175
+
176
+ <div class="mb-3">
177
+ <label for="email" class="form-label">
178
+ <i class="fas fa-envelope"></i> Email Address
179
+ </label>
180
+ <input type="email" class="form-control" id="email" name="email">
181
+ <div class="form-text">Optional</div>
182
+ </div>
183
+
184
+ <div class="mb-3">
185
+ <label for="capacity" class="form-label">
186
+ <i class="fas fa-users"></i> Capacity
187
+ </label>
188
+ <input type="number" class="form-control" id="capacity" name="capacity" min="1">
189
+ <div class="form-text">Optional - Enter the maximum capacity of the mosque</div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <div class="d-grid gap-2 mt-4">
195
+ <button type="submit" class="btn btn-primary">
196
+ <i class="fas fa-plus"></i> Register Mosque
197
+ </button>
198
+ <a href="{{ url_for('mosque.home') }}" class="btn btn-outline-secondary">
199
+ <i class="fas fa-arrow-left"></i> Back to Home
200
+ </a>
201
+ </div>
202
+ </form>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ {% endblock %}
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ colorpaws
2
+ flask
3
+ flask-login
4
+ flask-sqlalchemy
5
+ flask-wtf
6
+ werkzeug
7
+ email-validator