526 Commits

Author SHA1 Message Date
Johnny
c650764eb5 Merge pull request #1271 from itflow-org/develop
Develop to Master
2026-03-14 13:03:05 -04:00
johnnyq
43dbc69358 Remove custom tables from db.sql 2026-03-14 12:25:15 -04:00
johnnyq
5c24813d36 Move client column back to the end in networks 2026-03-10 20:43:01 -04:00
johnnyq
c8dd7f2970 Network: Fix Assignable IPs field not posting 2026-03-10 20:35:37 -04:00
johnnyq
d13104bb5a Update changelog order 2026-03-10 14:06:34 -04:00
johnnyq
5d85810e58 Update Changelog 2026-03-10 14:01:59 -04:00
johnnyq
b00014771d tickets: Add Tooltip to Approve task 2026-03-10 13:58:39 -04:00
johnnyq
b133f63736 Ticket: Fix Task Approval 2026-03-10 13:54:54 -04:00
johnnyq
e45563ad66 Update Changelog 2026-03-10 13:25:19 -04:00
johnnyq
cf639cbf98 Update Changelog 2026-03-10 13:22:22 -04:00
johnnyq
b365b7c810 Rename unarchive to restore in assets 2026-03-09 15:33:32 -04:00
johnnyq
b394f484d3 Fix Vars add empty value placeholder for Ticket Frequency 2026-03-09 15:29:22 -04:00
johnnyq
ada336ceea Update var names 2026-03-09 15:16:29 -04:00
johnnyq
26ebac450f Setup: Fix config_base_url if not using the script so /setup is not appended 2026-03-09 12:04:21 -04:00
johnnyq
6e33c071ef Hide New Invoice, Recurring Invoices, Tickets, revenues, products for appropriate permissions 2026-03-09 11:44:45 -04:00
johnnyq
5bd21a2bd4 add .unifi to the allowed extensions on file uploads 2026-03-09 11:22:46 -04:00
johnnyq
e2547122a6 Update Var naming schemes to match the post var name 2026-03-08 14:25:12 -04:00
johnnyq
edad83cc37 Fix Regression of chnage post name from client to client_id Add / Edit Expense 2026-03-08 13:43:46 -04:00
johnnyq
b0190f462c Invoice: Fix Regression caused by renaming post var clien to client_id 2026-03-08 13:33:36 -04:00
johnnyq
c936823912 Update Changelog 2026-03-08 12:54:58 -04:00
johnnyq
7acaf55331 Bump App Version and Update Changelog 2026-03-08 12:47:02 -04:00
johnnyq
6122efc870 Updated all Queries related to invoice items to reflect their new tables quote, invoice, recurring invoice items 2026-03-07 19:35:44 -05:00
johnnyq
7f180eb6d9 Migrate Invoice Quote Recurring Invoice Items to their new respecting tables 2026-03-07 18:47:28 -05:00
johnnyq
72091670fd Bump DB to to include tables for quote items and recurring invoice item for future splitting of line items 2026-03-07 18:23:19 -05:00
johnnyq
2b6ebca4c3 Fix Project templates listing to not open edit modal when project is clicked 2026-03-07 18:00:32 -05:00
johnnyq
e44d571331 Allow Project template ticket templates to be Drag and drop sorted instead of manually typing in the order 2026-03-07 17:56:39 -05:00
johnnyq
526fa1aff5 Bump stripe-php from 19.0.0 to 19.4.1 2026-03-07 17:19:48 -05:00
johnnyq
11ba077726 Bump fullcalendar from 6.1.19 to 6.1.20 2026-03-07 17:09:01 -05:00
johnnyq
ffb97006ec Bump TCPDF from 6.10.1 to 6.11.2 2026-03-07 17:01:53 -05:00
johnnyq
8dbbef37bb Fix file upload missing client_id 2026-03-07 16:50:11 -05:00
johnnyq
92b98b95ee Better field arrangedment for better workflow for rack edit add 2026-03-07 16:39:07 -05:00
johnnyq
083262fedb Dont require gateway on network edit also make vlan field numeric 2026-03-07 16:31:39 -05:00
johnnyq
fe9bc05937 Rearrange fields so they make a better workflow in software edit and add 2026-03-07 16:17:34 -05:00
johnnyq
726b1cd42b Fix Stripe Add Payment in client portal regression on CSRF check 2026-03-07 13:17:24 -05:00
johnnyq
8de7b20ba1 Add CSRF Checks in the client portal 2026-03-07 13:11:08 -05:00
johnnyq
6d2cb0aea3 Split out item edits into seperate posts and modals for editing invoice, quote and recurring invoice items 2026-03-07 12:12:27 -05:00
johnnyq
c8273b2052 Client related modal and post move client_id get after post 2026-03-07 11:48:56 -05:00
johnnyq
60b6c245ef Change expense client to client_id post var 2026-03-06 18:40:01 -05:00
johnnyq
fcf098f494 use client_id as post var name not client 2026-03-06 18:33:25 -05:00
johnnyq
74ce8f4e98 Trips: enforceClientAccess only if client is referenced 2026-03-06 18:26:14 -05:00
johnnyq
6e3a36d8c4 payments: enforceClientAccess 2026-03-06 18:20:58 -05:00
johnnyq
6f0a2a5a73 quotes: enforceClientAccess 2026-03-06 18:11:35 -05:00
johnnyq
111a30f13c recurring invoices: enforceClientAccess 2026-03-06 18:01:20 -05:00
johnnyq
202f55c2ff invoices: remove client_id post from edit and link modals as it should get the client_id in post, enforceClientAccess 2026-03-06 17:53:44 -05:00
johnnyq
8ad8fd07b3 files, folders, documents: remove client_id post from edit and link modals as it should get the client_id in post, enforceClientAccess 2026-03-06 17:16:04 -05:00
johnnyq
a1931f59f8 services: remove client_id post from edit service modal as it should get the client_id in post, enforceClientAccess 2026-03-06 17:01:34 -05:00
johnnyq
e7b70c7992 domains: remove client_id post from edit domain modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:59:15 -05:00
johnnyq
7563148182 certificates: remove client_id post from edit certificate modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:53:20 -05:00
johnnyq
a252ff717e Racks: remove client_id post from edit rack and add rack device modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:43:56 -05:00
johnnyq
7d41782013 Credential: remove client_id post from edit credential modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:34:01 -05:00
johnnyq
91976cc415 Software: remove client_id post from edit software modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:23:53 -05:00
johnnyq
8afa2dbf75 Assets: remove client_id from edit asset modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:13:56 -05:00
johnnyq
3983c45cac Calendar Events: use client_id instead of client in POST and enforceClientAccess if client is assigned to an event 2026-03-06 15:59:18 -05:00
johnnyq
df5c350765 Vendors: enforceClientAccess in POST only if vendor is assigned a client, as vendor_client_id 0 is for global vendors 2026-03-06 15:43:44 -05:00
johnnyq
de8b9df4da Projects: enforceClientAccess in POST only if a client is assigned to the project 2026-03-06 15:25:30 -05:00
johnnyq
3be815c749 Recurring tickets: enforceClientAccess in POST 2026-03-06 14:35:19 -05:00
johnnyq
71b19e125e Locations and tickets enforceClientAccess in POST fix a few missing client_id vars which were bale to be used for logging too 2026-03-06 14:09:51 -05:00
johnnyq
8fc3dfed1f New Function enforceClientAccess() and added to contact post and client inc all This enforces user client access if set at post and in other places easily 2026-03-06 13:05:11 -05:00
johnnyq
30357b9cf7 Add CSRF Checks to notifications and ensure the user dismissing the notification is their own notification 2026-03-05 17:51:20 -05:00
johnnyq
a81edc122d Add CSRF Checks on Agent ajax endpoints that would update / delete or add something to the db 2026-03-05 17:12:44 -05:00
johnnyq
994526e2c8 Asset details Modal: Reduce top bar Icon Size, Add IPv6 to interfaces, rearrange interfaces to match full page asset details and cleanup code 2026-03-04 19:48:51 -05:00
johnnyq
203b161e82 Add Billing Time Increment Option in Client Ticket Time Detail Report, this option will later be available globally 2026-03-04 18:33:08 -05:00
johnnyq
47b8ec6f96 Simple Calendar naming 2026-03-04 17:18:54 -05:00
johnnyq
302914c789 Add New Calendar Hint 2026-03-04 17:13:02 -05:00
johnnyq
f77bd4f0c6 Calendar: Add Delete Calendar, rename System calendar to Built-in calendar with builtin calendar names, moved calendar select on top for both edit / add event 2026-03-04 17:09:13 -05:00
johnnyq
af4327ff27 Show Full Event title in calendar on hover 2026-03-04 16:27:22 -05:00
johnnyq
5cbed128e6 Use new custom CSS Class for Column Checkboxes so they are a static 40px 2026-03-04 11:56:20 -05:00
johnnyq
dd5fde4069 Client Overview: Cleaned up UI shrunk table sizes and card headers 2026-03-03 16:11:52 -05:00
johnnyq
220e1d4e24 Make new ticket first in client top head 2026-03-03 15:51:50 -05:00
johnnyq
3481431eb5 Add New ticket button in client top header button, spruce up credential favorites on overview 2026-03-03 15:50:14 -05:00
johnnyq
b94604ae71 Assets List: Major UI / UX Update removed some columns to reduce space and add them as a secondary row instead 2026-03-03 15:20:11 -05:00
johnnyq
fe406487de UI Update for Role Listing 2026-03-03 11:44:22 -05:00
johnnyq
99218d02ad Document Templates: UI Enhancement / Cleanup Document templates 2026-03-03 11:16:36 -05:00
johnnyq
8cd1668ff2 Cleanup Ticket Template Details header 2026-03-03 09:50:27 -05:00
johnnyq
c0f26204c2 Swapped Location and Contact Columns in Clients Listing for better scannabilty 2026-03-02 23:59:17 -05:00
johnnyq
ef3a7d0490 Rearrange columns for network interfaces make better logicl sense 2026-03-02 23:52:01 -05:00
johnnyq
f9558d4701 Assets: Added IPv6 Address to assets list and interface list, Updated UI of add / edit network interfaces updated icons wording and bettr arrangement 2026-03-02 23:40:53 -05:00
johnnyq
9d9ebe7b9e Added Bulk Net Terms editing in clients 2026-03-02 22:43:14 -05:00
johnnyq
918b40afbe Add missing CSRF Checks in admin area and settings 2026-03-02 22:15:36 -05:00
johnnyq
6da8821f2c Add hidden form field to folder modals as well 2026-03-02 20:51:12 -05:00
johnnyq
90171064a8 document and files and folders: Add missing CSRF checks, add missing permission checks 2026-03-02 20:49:35 -05:00
johnnyq
6bbe887f8b services: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:34:55 -05:00
johnnyq
b5fb14ec96 domains: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:32:20 -05:00
johnnyq
dd2ac00aa2 certificates: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:26:43 -05:00
johnnyq
e7b5e7120a racks: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:22:36 -05:00
johnnyq
8bac4f9e53 networks: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:17:26 -05:00
johnnyq
023cb4ff11 credentials: Add missing CSRF checks 2026-03-02 20:11:56 -05:00
johnnyq
1740599b61 software licenses: Add missing CSRF checks, add missing permission checks 2026-03-02 19:37:27 -05:00
johnnyq
24b244b612 assets: Add missing CSRF checks, add missing permission checks, renamed unarchive to restore 2026-03-02 19:32:45 -05:00
johnnyq
c71d1f190e locations: Add missing CSRF checks, add missing permission checks, renamed unarchive to restore 2026-03-02 19:15:52 -05:00
johnnyq
18e180eca5 Vendors: Check if client_id is set to enforce the right permission check. If client vendor then check client module otherwise check financial module 2026-03-02 18:40:07 -05:00
johnnyq
d936339f07 Contacts: Add missing CSRF checks, add missing permission checks, renamed unarchive to restore 2026-03-02 18:28:53 -05:00
johnnyq
ad16e92763 Use true Decimal minute reporting in Client Time Audit Detail Report 2026-03-02 18:06:54 -05:00
johnnyq
8a1335174d Trips: Add missing CSRF checks, we may need another permission module check for trips for now only admin and financial lv3 can delete a trip 2026-03-02 17:50:04 -05:00
johnnyq
7b438e2889 Transfers: Add missing CSRF checks 2026-03-02 17:38:20 -05:00
johnnyq
550980719e Accounts: Add missing CSRF checks 2026-03-02 17:35:18 -05:00
johnnyq
3d80d1519e Recurring Expenses: Add missing CSRF, Add missing permission checks 2026-03-02 17:32:18 -05:00
johnnyq
1d5fceeecd Expenses: Add missing CSRF, Add missing perms 2026-03-02 17:27:56 -05:00
johnnyq
5b49908438 Vendors: Add missing CSRF, need to update the permissions as a vendor can be client or global and permissions need to be set based off if the referal url has client_id or not 2026-03-02 17:20:00 -05:00
johnnyq
7e515afb79 Payments: Add missing CSRF and additonal perm check 2026-03-02 16:38:17 -05:00
wrongecho
dee5085f4a Allow associating a quote with a ticket 2026-03-02 10:54:44 +00:00
wrongecho
795144b288 Fix documents => files redirect after deletion (since we merged these) 2026-03-02 08:12:07 +00:00
wrongecho
5c3ff91f21 Lower autoclose minimum allowed value to 24 hours (this still works off cron so is not exact) 2026-03-02 08:07:43 +00:00
johnnyq
36ac006438 Products: Add missing CSRF checks change wording from unarchive to restore 2026-03-01 22:50:04 -05:00
johnnyq
af1ebfea41 Revenues: Add missing CSRF checks 2026-03-01 22:42:37 -05:00
johnnyq
f653752026 Recurring Invoice: Add missing CSRF checks and missing permissions in POST 2026-03-01 22:37:51 -05:00
johnnyq
4440581f14 Invoice: Add missing CSRF checks and missing permissions 2026-03-01 22:22:21 -05:00
johnnyq
2c47001b19 Quote: Add missing CSRF checks and missing permission on export quote pdf 2026-03-01 22:02:43 -05:00
johnnyq
e1daa14087 Calendar: Add missing CSRF checks 2026-03-01 21:52:28 -05:00
johnnyq
308dc6e550 Ticket Tasks: Add missing CSRF checks and other CSRF checks missed in he previous commits 2026-03-01 21:45:26 -05:00
johnnyq
54638428e3 rename unarchive project to restore project 2026-03-01 21:32:24 -05:00
johnnyq
1508e6b055 Project: Add missing CSRF checks 2026-03-01 21:30:41 -05:00
johnnyq
9213249f7b Recurring Ticket: Add missing CSRF checks 2026-03-01 21:21:37 -05:00
johnnyq
63f6faf1e8 Ticket: Add missing CSRF checks 2026-03-01 21:14:58 -05:00
johnnyq
c2cbfc5558 Add Hidden Inpout csrf token to client download pdf 2026-03-01 20:43:44 -05:00
johnnyq
ef1b65de09 Client: Add missing CSRF checks, and missing enforcements 2026-03-01 20:42:18 -05:00
johnnyq
ae81092112 Updated Placeholders to set better formal expectations 2026-02-28 20:52:53 -05:00
johnnyq
f09be24188 Rearrange add/edit Asset field Order, updated icons, minor ui work 2026-02-28 20:12:51 -05:00
johnnyq
10c37946b1 UI: Update icoms for description inputs 2026-02-28 19:14:00 -05:00
johnnyq
adb43703ed Update Networks, remove subnet mask in favor of CIDR, Global View show clients in first column, allow for IPv6 and provide IPv6 placeholder examples, gateway no longer required reword DHCP Range to IP Range moved DNS to the end, chnaged out some icons 2026-02-28 18:41:03 -05:00
johnnyq
92fba49a91 Revert "Add new optional beta email parser thats based on ImapEngine instead of Webklex"
This reverts commit 9cb1ff7330.
2026-02-26 16:44:49 -05:00
johnnyq
ac4bb32081 Revert "Beta Mail Parser Add inline images as attachments keeps the DB clean"
This reverts commit 87fd23f443.
2026-02-26 16:44:39 -05:00
johnnyq
7927312f4f Revert "Revert "Beta Mail Parser Add inline images as attachments keeps the DB clean""
This reverts commit 04a74b5a34.
2026-02-26 16:41:50 -05:00
johnnyq
04a74b5a34 Revert "Beta Mail Parser Add inline images as attachments keeps the DB clean"
This reverts commit 87fd23f443.
2026-02-26 16:41:03 -05:00
johnnyq
87fd23f443 Beta Mail Parser Add inline images as attachments keeps the DB clean 2026-02-26 16:30:29 -05:00
johnnyq
9cb1ff7330 Add new optional beta email parser thats based on ImapEngine instead of Webklex 2026-02-26 16:11:49 -05:00
wrongecho
1ba19cc249 New Ticket Parsing - Anyone CC'ed onto the original email that created the ticket is added as a ticket watcher 2026-02-26 12:49:32 +00:00
wrongecho
702d9689d6 clarify PR stance 2026-02-26 10:24:44 +00:00
johnnyq
8efa354fdb Fix Task Sorting in ticket templates 2026-02-21 14:49:46 -05:00
johnnyq
129e95fd3a Added Net 45 Days, removed 14 Days as this isnt typically used anywhere 2026-02-19 12:34:30 -05:00
johnnyq
ef7794c411 Added non payment invoice reminder / Late fee if enabled after 1 day an invoice is overdue. currently it was jumping right to 30,60,90... now its 1,30,90... 2026-02-19 12:20:26 -05:00
Johnny
20e2f22a27 Merge pull request #1268 from itflow-org/develop
Develop to Master for 26.02.1 Maint Release
2026-02-14 15:54:13 -05:00
johnnyq
12b3775041 Update Changelog 2026-02-14 15:50:27 -05:00
johnnyq
52b509fdc5 Update Changelog 2026-02-14 15:49:40 -05:00
johnnyq
f9d0a8bf43 Ticket: Dont display Updated at if null, Move Subject from top ticket bar to header in ticket details, add additional comments about the client_permission_overide 2026-02-14 15:48:40 -05:00
johnnyq
2349ef33d1 Remove 0 in access_permission_query 2026-02-14 15:07:42 -05:00
johnnyq
ccaf15268c Add a 0 IN ticket_client_id for no client tickets so agents that are restricted to certain clients and still view no clients in tickets list by using a , also restrict change clients on tickets for restricted agents 2026-02-14 15:06:17 -05:00
johnnyq
616635f951 client edit modal: Restrict Access to client restricted agents 2026-02-14 13:17:15 -05:00
johnnyq
ce7daaf1cf ticket edit modal: Restrict Access to client restricted agents 2026-02-14 12:55:34 -05:00
johnnyq
fb9f5d986d ticket Details: Restrict Access to ticket details by restricted agents 2026-02-14 12:51:58 -05:00
johnnyq
3148906cfe ticket Details: Restrict Access to ticket details by restricted Agents 2026-02-14 12:30:49 -05:00
johnnyq
6c057f4fd0 Restrict Tickets presented in ticket listing to client restricted Agents 2026-02-14 12:16:22 -05:00
johnnyq
3014bba66d Update Changelog, Bump App Release 2026-02-14 12:01:49 -05:00
johnnyq
97958d5e22 Missing Generate Password js code in Create Credential modal 2026-02-13 14:43:34 -05:00
johnnyq
1a7452cc27 Restrict Agents that are restricted to view certain clients from calendar events. 2026-02-13 14:28:45 -05:00
johnnyq
34ba5d0570 Properly get the new ticket number for logging in ticket merge 2026-02-13 14:14:01 -05:00
johnnyq
c0fe9813dc Fix Ticket Merging regressed from ticket select now use ticket_id instead of ticket_number 2026-02-13 14:09:05 -05:00
johnnyq
fd29eb7c15 Fix Transfer Asset to client due to field change from asset_important to asset_favorite 2026-02-13 13:39:41 -05:00
johnnyq
1bee085b33 Add Report for Client Detail Time Auditing 2026-02-13 13:35:02 -05:00
johnnyq
1d06e6d9c8 Revert to old ajax-modal js code for now, Fix Assets not lising in create ticket. 2026-02-11 13:33:28 -05:00
wrongecho
bdf8038148 Merge pull request #1265 from MydsiIversen/Time-API
feat: Add API endpoint to retrieve time worked by technicians
2026-02-09 14:23:02 +00:00
Mads Iversen
62fb2c91a1 feat: Add API endpoint to retrieve time worked by technicians on tickets with filtering by date and technician. 2026-02-09 12:09:56 +00:00
Johnny
376285ef62 Merge pull request #1263 from itflow-org/develop
Develop to Master for 26.02 Release Take 2
2026-02-08 12:02:43 -05:00
johnnyq
d1eeba67fc Add bulk Fav / unfav creds and added fav creds to the client overview page 2026-02-07 18:42:53 -05:00
johnnyq
2ca8112daf Revert Git change test to ticket_email_parser.php 2026-02-06 15:14:37 -05:00
johnnyq
546f10bc82 Testing Git changes on ticket_email_parser.php 2026-02-06 15:13:54 -05:00
johnnyq
ec7f1d561d Fix Version 2026-02-06 13:12:34 -05:00
johnnyq
8d54bc3a2f Another change 2026-02-06 13:03:07 -05:00
johnnyq
1d8f77f799 Update Changelog for 26.2 Release 2026-02-06 13:01:37 -05:00
johnnyq
1c6795dd55 Bump app version 2026-02-06 12:09:32 -05:00
Johnny
4d895a56e4 Merge pull request #1261 from cs2000/develop
Add full Microsoft 365 and Google OAuth mail support
2026-02-05 14:04:22 -05:00
johnnyq
bdd56c91f7 Update Credential Important to Favorite 2026-02-05 12:24:21 -05:00
johnnyq
be172b5bd9 Add star-toggle class to add asset 2026-02-05 12:15:41 -05:00
johnnyq
d224d71f59 Bump Datatables from 2.3.4 to 2.3.7 2026-02-05 11:50:33 -05:00
johnnyq
065630b975 Bump PHPMailer from 7.0.1 to 7.0.2 2026-02-05 11:46:28 -05:00
johnnyq
decea604ab Bump TinyMCE 8.2.2 to 8.3.2 2026-02-05 11:38:22 -05:00
johnnyq
004b3b2bdc Draft of ticket_by_client report v2 not yet in the side menu of reports 2026-02-05 11:31:40 -05:00
cs2000
dca0cc34e9 Revert to original file from dev branch
Accidentally comitted an older file.
2026-02-05 16:08:04 +00:00
cs2000
9afb165fc4 SonarCube Suggestion
Remove duplicates of https://login.microsoftonline.com/ and make them use a constant.
2026-02-05 10:26:51 +00:00
cs2000
6a6eb4a714 SonarCube Suggestion
Define a constant "MICROSOFT_OAUTH_BASE_URL" instead of duplicating "https://login.microsoftonline.com/" multiple times in the same file.
2026-02-05 10:22:06 +00:00
cs2000
629f4e0c81 SonarCube Suggestion
Defined a constant $settings_mail_path at the top of the file instead of duplicating "/admin/settings_mail.php" in the code multiple times.
2026-02-05 10:19:13 +00:00
cs2000
7cbe9bf7fc SonarCube Suggestion
Removed two instances of unecessary double parentheses.
2026-02-05 10:17:28 +00:00
cs2000
fc33312e79 SonarCube Suggestion
Rename multiple functions to conform to project naming rules (^[a-z][a-zA-Z0-9]*$). This is a non-functional refactor for style/CI compliance and consistency.
2026-02-05 10:15:52 +00:00
cs2000
563c0ea9c4 Changes for M365 oAuth using dev code
My bad, my changes were made previously using my installed version and not the latest changes in the dev branch, i have pulled the dev branch and merged my changes with the latest codebase.
2026-02-05 09:38:09 +00:00
cs2000
10d1a902d9 Changes for M365 oAuth now using latest dev code
My bad, my changes were made previously using my installed version and not the latest changes in the dev branch, i have pulled the dev branch and merged my changes with the latest codebase.
2026-02-05 09:37:22 +00:00
johnnyq
4079257739 Update UI/UX for Adding and editing roles, Permissions can be set upon role add nice blocker style radios buttons instead of select boxes 2026-02-04 13:12:24 -05:00
johnnyq
91aba13c0d Fix Empty col in client overview if no fabvorite assets, fix creds page rename var from important to favorite 2026-02-04 11:11:12 -05:00
cs2000
2cca4f2f0e Changes for M365 oAuth
- Added null-safe guard around folder path logging during message move failure to prevent property_exists() fatal when folder object is null.
2026-02-04 13:26:52 +00:00
cs2000
d2e8dc1439 Changes for M365 oAuth
- Fixed email queue gating for OAuth SMTP setups by treating configured config_smtp_provider as mail-enabled, even when config_smtp_host is blank.
- Restores queueing for public ticket reply emails (including “Public Comment & Email”) and related ticket notification paths.
2026-02-04 13:26:17 +00:00
cs2000
f3f9d0dd71 Changes for M365 oAuth
- Added OAuth token lifecycle helpers (expiry check, refresh, persistence).
- Updated SMTP XOAUTH2 send path to automatically refresh expired/missing access tokens for Microsoft/Google providers before sending queued mail.
2026-02-04 13:25:32 +00:00
cs2000
f6845a046f Changes for M365 oAuth
- New callback endpoint to complete Microsoft OAuth web flow.
- Validates admin session + OAuth state, exchanges authorization code for tokens, stores refresh/access tokens and expiry, and redirects with success/error feedback.
2026-02-04 13:24:50 +00:00
cs2000
a50a4f274f Changes for M365 oAuth
- Added handler to start Microsoft OAuth Authorization Code flow (oauth_connect_microsoft_mail) with state generation/validation prep.
- Added handler to test OAuth token refresh from admin UI and persist refreshed tokens/expiry.
- Updated IMAP test handler to support OAuth token refresh + XOAUTH2 authentication (in addition to legacy LOGIN).
2026-02-04 13:23:53 +00:00
cs2000
6b6d847756 Changes for M365 oAuth
- Added web-based Microsoft OAuth onboarding UI in Mail settings, including a Connect Microsoft 365 button and auto-generated callback URI display.
- Added Test OAuth Token Refresh UI section.
- Updated visibility logic so Test Email Sending and Test IMAP Connection show correctly for OAuth-based configs (not only host/password configs).
2026-02-04 13:23:03 +00:00
johnnyq
65d1f59e9b Feature: Favorites added to assets, Bulk Fav/unfav, adds favs to client overview 2026-02-03 22:23:20 -05:00
johnnyq
f39e6ccbc9 Remove old ticket merge code, rename kanban_v2 to to just kanban 2026-02-03 17:20:19 -05:00
johnnyq
747b62d78c Remove extra double quote 2026-02-03 17:09:07 -05:00
johnnyq
af0f3ac25f Update ticket Merge bulk with a dropdown select box to select a ticket instead of entering a number manually 2026-02-03 16:35:20 -05:00
johnnyq
6aefe99c2f Update ticket Merge with a dropdown select box to select a ticket instead of entering a number manually 2026-02-03 16:08:33 -05:00
johnnyq
61c5595a68 Fix Get type on add asset 2026-02-02 16:05:38 -05:00
johnnyq
3d11611699 Attempt to Fix duplicate data entries by fast clicking submit in modals using ajax_modal.js include 2026-01-31 15:14:01 -05:00
johnnyq
38b4ed4b96 Tidy check_login for client portal 2026-01-30 14:24:34 -05:00
johnnyq
34308a5f9a check if archived and active for client login as well along with loading extended client user session vars in /client/includes/check_login.php 2026-01-30 14:10:59 -05:00
johnnyq
118cc10804 During load user session check if user is archived and active, prevents users from making further actions if they are disabled or archived but are still logged in 2026-01-30 13:36:18 -05:00
johnnyq
ae3386f2d5 Fix Vendor phone formatting in Ticket details 2026-01-29 16:36:10 -05:00
johnnyq
bf1ccc62f5 Fix Collected Tax Report, was not adding totals OCT-Dec was misaligned and Grand Total wasnt displaying 2026-01-29 14:02:50 -05:00
johnnyq
1ce4dd932e Updated PL report - changed Gross Profit to Gross Revenue as is more accurate for taxation 2026-01-29 13:55:09 -05:00
johnnyq
ff0bb49926 Tidy up Ticket to Invoice Description 2026-01-29 13:13:24 -05:00
johnnyq
018642cbb8 Fix Ticket View / filtering 2026-01-28 23:33:12 -05:00
johnnyq
bf1d0655c4 Update Ticket list ui smaller badge for ticket and smaller task progress bar and more natural dates 2026-01-28 22:52:56 -05:00
johnnyq
874b07f4d7 More UI/UX updated to ticket details shrink padding on right cards, update wordings, changed header paddings, made unassigned a clickable badge no longer red, swapped trash cans for times icons, much cleaner overall 2026-01-28 22:06:50 -05:00
johnnyq
19fa210743 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-28 18:16:54 -05:00
johnnyq
c8a6513d1c Fix new times on new date time displays in ticket details 2026-01-28 18:16:38 -05:00
wrongecho
01288c8452 Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2026-01-28 23:10:54 +00:00
johnnyq
a60d3bb3a0 More Ticket Details UI/UX Sprucing better label naming etc 2026-01-28 17:41:14 -05:00
johnnyq
0470159c55 Added function formatDuration to format times like worked etc, updated Time worked in Ticket details to use the new function and some other minor ui/ux cleanups in ticket details 2026-01-28 16:08:39 -05:00
wrongecho
e1a93035fd Undo client scoping uses = instead of LIKE in tickets resolve.php --- see forum thread 2667 2026-01-28 14:57:28 +00:00
wrongecho
c0b7a26905 API bugfixes before release:
- apiEncryptLoginEntry should be apiEncryptCredentialEntry
- Client scoping uses = instead of LIKE in tickets resolve.php
2026-01-28 14:48:41 +00:00
johnnyq
704d770ec2 Fix Icon to Chevron down if Expand all folders is clicked 2026-01-27 16:32:17 -05:00
johnnyq
d42d2d99b1 Remember folders expanded by Passing folders_expanded var in breadcrumb links and search in files 2026-01-27 16:27:04 -05:00
johnnyq
ebfcc15927 Feature: File Folders are collapsed by default with a global option to exapand all folders or expand individual folder by click 2026-01-27 16:15:18 -05:00
johnnyq
4693aa3c93 Render root in display_folders function in files instead of it being seperate 2026-01-27 15:25:20 -05:00
wrongecho
07df657848 Bugfix: newline char showing in parsed emails 2026-01-27 17:25:53 +00:00
wrongecho
ef9436a1fb Bugfix: Ticket edit modal not setting assigned agent 2026-01-27 12:32:41 +00:00
johnnyq
9318f42ce0 Remove Folder option in edit document modal as this is reserved for move to folder modal 2026-01-24 01:37:00 -05:00
johnnyq
85c37d78a8 Do not update document_updated_at field if document is moved, toggle visibity, renamed, archived or restored same with bulk 2026-01-24 01:27:50 -05:00
johnnyq
ff02ab0cea Show Document Updated at instead of created at. This fixes where it should show the initial document creation date instead of the most updated date, also renamed uploaded column to updated 2026-01-23 13:06:06 -05:00
johnnyq
bd8bf026f0 Allow svg email attachemment for mail parser 2026-01-23 11:24:26 -05:00
johnnyq
644dc95380 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-21 12:40:43 -05:00
johnnyq
f4935efed3 Remove deprecated quick adds to the client overview 2026-01-21 12:40:30 -05:00
wrongecho
1712d34846 Kanban - move to v2 (rewritten style) 2026-01-21 13:15:23 +00:00
johnnyq
f0f1134be6 Fix phone number formatting is both asset details page and modal 2026-01-19 17:26:40 -05:00
johnnyq
e0f7460e08 If not client_id then do not include &client_id= in ticket uris as this would cause ticket viewing to break if client_id is 0. 2026-01-19 17:01:38 -05:00
johnnyq
ae1d71dcd7 append client_id to the uri for agent ticket links so that the when clicked will take them to the client section and show client header along with the bread crumbs link of client tickets 2026-01-19 16:24:31 -05:00
johnnyq
3cfe38c948 updated UI for contact details so menu is on the left instead of top. Also load the first tab based off the count 2026-01-17 17:35:05 -05:00
johnnyq
c1f0b63101 Fix login not passing master key if agent is client and agent and if MFA is enabled 2026-01-17 17:07:22 -05:00
johnnyq
0a658d7cab Fix Document Update API 2026-01-17 13:17:17 -05:00
johnnyq
3ed2582a9b Remove TaxID from packing slip 2026-01-15 14:09:03 -05:00
johnnyq
2ea68776f6 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-15 13:13:25 -05:00
johnnyq
c1ff22298f Remove xcustom from .gitignore as its been superceded by custom 2026-01-15 13:13:15 -05:00
wrongecho
e94d2f93ea Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2026-01-15 17:35:35 +00:00
wrongecho
e7f6f8a4c7 Mail Parser - NDR Parsing 2026-01-15 17:35:13 +00:00
johnnyq
4ffe75683b Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-15 12:28:34 -05:00
johnnyq
687920743d Fix user_id not being set in audit logs for agent and client logins, also fix issie where user_id wasnt being set in client portal. 2026-01-15 12:28:20 -05:00
wrongecho
1a93149643 Basic asset history tracking (transfers) - See https://tasks.dev.itflow.org/task_details.php?task_id=19 2026-01-15 14:58:23 +00:00
wrongecho
11d6654763 Basic asset history tracking - See https://tasks.dev.itflow.org/task_details.php?task_id=19 2026-01-15 14:49:41 +00:00
wrongecho
512d65c17a Keep asset PO/ref when copying 2026-01-15 14:22:22 +00:00
wrongecho
5a274061f3 Invoice - add a basic picking/packaging slip 2026-01-15 13:58:53 +00:00
wrongecho
a398ac7a8d Mail Parser - Support flowed text 2026-01-15 11:54:37 +00:00
wrongecho
41df4c4b9f API Keys - Revoke then Delete 2026-01-15 11:37:17 +00:00
wrongecho
7e7909cec1 Mail Parser - Do not automatically send new ticket notifications to noreply/donotreply addresses 2026-01-15 10:38:25 +00:00
wrongecho
7322bd66df Invoices - Show agent names in history 2026-01-15 10:19:56 +00:00
wrongecho
39affa5528 Show trips on main nav for everyone if accounting enabled 2026-01-15 10:04:48 +00:00
wrongecho
374111c88d Add digiBandit as sponsor 2026-01-15 09:28:52 +00:00
wrongecho
72fc0015bf Add digiBandit as sponsor 2026-01-15 09:28:03 +00:00
johnnyq
7ab406b3f5 Convert mysqli_fetch_assoc to mysqli_fetch_row in setup for table import 2026-01-14 18:02:11 -05:00
johnnyq
df280cd574 Fix missing isset in view in tickets.php 2026-01-14 17:36:06 -05:00
johnnyq
0a30300bde replace all instances of mysqli_fetch_array with mysqli_fetch_assoc for better performance and memory usage 2026-01-14 17:30:23 -05:00
johnnyq
cb8b99d6ae up the permissions to added edit Payment 2026-01-12 18:12:20 -05:00
johnnyq
1d3f29d385 added edit Payment 2026-01-12 18:11:41 -05:00
johnnyq
c154113474 Do not add Signature on ticket comment if Public only when reply is emailed 2026-01-12 15:18:22 -05:00
johnnyq
e02a6fc5e6 Use mysqli_fetch_assoc in login 2026-01-12 12:30:12 -05:00
johnnyq
78040573d1 Fix Remove Powered by ITFlow on Login Screen when WhiteLabel is enabled 2026-01-12 12:27:44 -05:00
wrongecho
981f9ace04 TinyMCE - add all options to mobile (we may regret this) 2026-01-12 16:12:06 +00:00
wrongecho
208f7ac8f0 Tickets Kanban
- Keep the selected view (kanban/list) when applying other filters
- Allow filtering tickets by project (main and kanban v2)
2026-01-12 16:11:36 +00:00
wrongecho
661f8db10b Tickets Kanban
- Keep the selected view (kanban/list) when applying other filters
- Allow filtering tickets by project (main and kanban v2)
2026-01-12 16:04:02 +00:00
wrongecho
b48168ffec Rewrite kanban in more procedural code to match existing codebase (as a v2, delete old and rename once we're happy) 2026-01-12 15:36:06 +00:00
wrongecho
908ebb46d9 Merge pull request #1258 from itflow-org/ticket-task-approvals
Ticket task approvals
2026-01-12 12:27:24 +00:00
wrongecho
58bcb38617 Add task approval system (tidy) 2026-01-12 12:21:10 +00:00
johnnyq
1de023f9df Fix Role Archiving, Roles can only be archived if no users are assigned to the role 2026-01-11 16:46:53 -05:00
johnnyq
5815ef2f75 Added bulk delete, archive and restore and invidual file/document restore functions to files 2026-01-10 20:38:04 -05:00
johnnyq
e5dab8b1ca Update files to use mysqli_fetch_assoc for better performance and memory optimization. Add title for List View and Grid View 2026-01-10 19:12:22 -05:00
johnnyq
56c4b7fbe6 Add show archived logic to files 2026-01-10 19:03:28 -05:00
johnnyq
365b65e5b2 fix Bulk Delete Documents and Files 2026-01-10 16:27:12 -05:00
johnnyq
2193cd8d3e switched from mysqli_fetch_array to mysqli_fetch_assoc in client modals 2026-01-10 16:09:03 -05:00
johnnyq
8b221bc055 switched from mysqli_fetch_array to mysqli_fetch_assoc in clients listing page. This will have a major query speed and memory optimization impact 2026-01-10 15:54:19 -05:00
wrongecho
77e4d2b566 Add task approval system 2026-01-09 17:14:44 +00:00
wrongecho
88a29b7599 Bugfix: Mail queue loop not sending invoices to all billing contacts 2026-01-09 16:56:11 +00:00
wrongecho
64525750b6 Fix readme demo creds 2026-01-09 13:56:29 +00:00
wrongecho
30499123f1 Bugfix: Portal not showing contact user id in session 2026-01-09 13:50:46 +00:00
johnnyq
79703042ff Update client table responsiveness viewpoint to fix issue when window is a certain width on the desktop 2025-12-30 16:58:44 -05:00
johnnyq
ccd5605d97 Remove unused code 2025-12-29 18:03:53 -05:00
johnnyq
908277065b Fix Ticket Template auto filling for v1 ticket creation 2025-12-29 17:47:24 -05:00
johnnyq
f2d4eb0486 Fix Ticket Template auto filling 2025-12-29 17:46:23 -05:00
johnnyq
f784b659e8 Remove extra agent client wording 2025-12-20 15:05:47 -05:00
johnnyq
e60a7a59f9 Fix Login flow where user agent and client exists and agent has MFA but will not let them continue, also update some wording and button colors. Also dont show email password fields again after success and login as agent and client is shown. 2025-12-20 14:30:57 -05:00
johnnyq
cab81ca170 Fix Billable sort 2025-12-18 20:39:47 -05:00
johnnyq
a82e2c7ea1 Billable and non billable status use icons check and minus 2025-12-18 20:38:15 -05:00
johnnyq
a277380441 Set API key back to 32 Chars 2025-12-18 20:03:33 -05:00
johnnyq
ad5710b1d8 Fix Invoice CSV Exporting 2025-12-18 20:00:56 -05:00
johnnyq
3e3531a6ce Set API Key to 32 Chars 2025-12-18 14:28:24 -05:00
johnnyq
a79ce23ae5 Fix randomString() to generate cryptographically secure URL-safe tokens, reduced url keys to 32 Characters for performance and easy copy and paste and compatibility while still mainitaining ubreakable cryptographic keys 2025-12-18 14:24:53 -05:00
Johnny
163aa3062e Merge pull request #1254 from itflow-org/develop
Develop to Master for 25.12.1 Maint Release
2025-12-14 14:03:56 -05:00
johnnyq
32f996d034 If login key is set and it is not provided show Client Email instead of just Email for placeholder 2025-12-14 13:42:38 -05:00
johnnyq
312eb4dffc Allow use of login key only for agents 2025-12-14 13:16:54 -05:00
johnnyq
1916456c84 Fix White Label not displaying on the login page 2025-12-14 13:04:53 -05:00
johnnyq
9b8d37b577 Updated changelog 2025-12-13 15:47:28 -05:00
johnnyq
05018e5f17 Added Reset favicon 2025-12-13 15:45:02 -05:00
johnnyq
72ef918452 Update changelog and bump app version to 25.12.1 2025-12-12 16:51:34 -05:00
johnnyq
27fde82aff Fixed Adding Payment provider not adding an account, now adding you can customize the income/expense account, expense category, and Expense vendor. Moved Saved Payment Provider Methods into Payment Providers as a link instead of on the admin side nav. Same with AI Provider and AI Models. 2025-12-12 16:42:09 -05:00
johnnyq
b27ffe6635 Refine DB Helpers 2025-12-10 18:32:46 -05:00
johnnyq
84cc4a094a Add DB helpers to make MySQLi Prepared statements less bloated and require less code 2025-12-10 17:09:34 -05:00
johnnyq
e75600ee05 Fix New ticket button in contact details in the related tickets section 2025-12-10 13:18:41 -05:00
johnnyq
871ad2ea7e Update legacy client login links to use the new unified login method 2025-12-10 12:21:12 -05:00
wrongecho
8b5f2e0f3f Update + credit 2025-12-10 08:54:24 +00:00
johnnyq
58d6ab7342 Unify Agent and Client login, if same user exists as a client and an agent then offer a selection of client portal or agent portal 2025-12-09 13:39:16 -05:00
Johnny
03570ecd04 Merge pull request #1250 from itflow-org/develop
Develop to Master for 25.12 release
2025-12-06 14:36:09 -05:00
johnnyq
ca5fb2e010 refined php-xml ext notice 2025-12-06 13:46:09 -05:00
johnnyq
da561b296e Added php-xml as a requirement 2025-12-06 13:38:40 -05:00
johnnyq
523da0dea0 Added a few more things to the changelog 2025-12-06 12:35:20 -05:00
johnnyq
0e4cc76a84 Bump Version for 25.12 updated changelog 2025-12-06 12:18:11 -05:00
johnnyq
7e39a7ed89 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2025-12-05 12:46:52 -05:00
johnnyq
4a26ea7ed9 Hide Permission Modules on sidenav admin menu as this is not ready for custom modules 2025-12-05 12:46:44 -05:00
wrongecho
7c83ba15b9 Mail queue - minor comment syntax error 2025-12-03 15:43:55 +00:00
wrongecho
10bfbed4bb Mail queue - introduce a --no-mx-validation flag to bypass recipient domain MX validation 2025-12-03 15:30:26 +00:00
wrongecho
81550bd7a8 Ticket merge input - strip text 2025-12-03 15:13:52 +00:00
johnnyq
a430bb917e Add CSV Escaping to the Sample Export CSV Files 2025-12-02 15:18:20 -05:00
johnnyq
e1a579387f Convert all Import / Export Modals to Ajax and a few other lingering modals that were not converted yet 2025-12-02 15:04:05 -05:00
johnnyq
fe8df66c67 Migrate Import/export clients to ajax-modals 2025-12-01 20:33:01 -05:00
johnnyq
5bb410f80c Converted all Document Link Modals to the new ajax-modal, also switched trash icons for unlink icons in document details 2025-12-01 17:16:59 -05:00
johnnyq
29b79b9d4e Bump TinyMCE from 8.2.0 to 8.2.2 2025-11-28 17:37:38 -05:00
johnnyq
0f8a8d1464 Bump TCPDF from 6.10.0 to 6.10.1 2025-11-28 17:34:16 -05:00
johnnyq
cc92a4b7ee Bump stripe-php from 18.1.0 to 19.0.0 2025-11-28 17:31:04 -05:00
johnnyq
3ffef6df51 Remove library phpMimeParser as its no longer needed and php-imap webklex is not doing this function 2025-11-28 17:27:05 -05:00
johnnyq
78e4787b99 Bump PHPMailer from 7.0.0 to 7.0.1 2025-11-28 17:24:12 -05:00
johnnyq
540512a156 remove folder location vars as no longer needed as files and documents have been merged 2025-11-28 15:26:26 -05:00
johnnyq
7737dbc65d Migrate Invoice to Recurring Invoice to an ajax modal 2025-11-28 15:15:01 -05:00
johnnyq
faa94d888d Convert Quote to Invoice to use ajax-modal 2025-11-28 14:44:47 -05:00
johnnyq
99e2487d2b Fix dupe race condition with ticket, invoice, quote, project, recurring ticket numbering when being created in parallel Atomically update and get the next ticket number in one SQL query everywhere. 2025-11-28 14:14:46 -05:00
johnnyq
f09d8ffe05 Fix dupe ticket numbering when being created in parallel Atomically update and get the next ticket number in one SQL query. 2025-11-28 13:07:56 -05:00
johnnyq
c486e3fe62 Conver the what should be the last bulk asset interfaces modals to the new ajax 2025-11-27 14:13:33 -05:00
johnnyq
ba2d6b6709 Convert Bulk Edit Product Category modal to ajax 2025-11-27 13:41:53 -05:00
johnnyq
a388a279bc Convert Bulk Edit Product Category modal to ajax 2025-11-27 13:40:09 -05:00
johnnyq
5a64b19a06 Convert Document from Template modal to ajax 2025-11-27 13:17:14 -05:00
johnnyq
53178b8d20 Updated bulk action js to pass the checkboxe names into the get array this allows the use of multiple checkbox name arrays to be passed at once instead of just selected_ids had to update each bulk model from selected_ids to to the array that was passed. This was important so we could mix files and documents together 2025-11-27 12:48:59 -05:00
wrongecho
0347382a34 Invoices - Allow specifying discount during creation 2025-11-27 09:53:35 +00:00
johnnyq
840460afe7 Update Bulk Action JS to accept and pass multiple custom name selector arrays but default to selected_ids if data-bulk-names is not specified 2025-11-26 16:12:19 -05:00
johnnyq
c851e54e1d Fix Decimal not showing on the iphone for specific fields associated to price cost percentage etc 2025-11-26 13:54:24 -05:00
johnnyq
5ef53b569c Create upload folders for recurring tickets and ticket_templates 2025-11-24 13:09:01 -05:00
johnnyq
698b4166e8 Add back deleted client edit in post 2025-11-24 11:27:01 -05:00
johnnyq
1a9a36829b Process base64 Images for document creation and editing for the API and Client Portal 2025-11-24 00:14:27 -05:00
johnnyq
155b8598ff Introduce cleanupUnusedImages function to delete referenced files that have been deleted when editing content which as been added to document template edit. Did not do this for documents as they are versioned and images will remain until the document is fully deleted 2025-11-23 15:36:11 -05:00
johnnyq
4153c91f84 Add function copyDirectory so when creating a document from a template copy the document_template folder to documents folder and update links 2025-11-23 15:26:29 -05:00
johnnyq
a99b19a1b5 Update add and edit Document template to extract base64 images and place them into files instead also delete the document_template/document_template_id folder when deleting a document template 2025-11-23 15:09:03 -05:00
johnnyq
18429fda2c Remove all side nav Quick adds 2025-11-23 14:53:39 -05:00
johnnyq
435da991ec Add custom folder to uploads for custom uploading for custom modules 2025-11-23 14:32:57 -05:00
johnnyq
ebd9aae924 Add Document Templates to uploads dir 2025-11-23 14:30:50 -05:00
johnnyq
414a84d5ec Focus on Author and Date values 2025-11-23 14:24:20 -05:00
johnnyq
a3b2517603 Fix up UI on document details header 2025-11-23 14:10:45 -05:00
johnnyq
43535082f6 Cleanup UI for document details title header 2025-11-23 14:02:01 -05:00
johnnyq
e73af9980e Also Delete Documents/Document_id folder during bulk delete 2025-11-23 13:43:12 -05:00
johnnyq
0bdd5784ee use saveBase64Image function for edit document as well and when document is deleted, delete the corresponding folder uploads/documents/document_id 2025-11-23 13:35:45 -05:00
johnnyq
48719ce29c Add Exclude uploads/documents to gitignore 2025-11-23 13:05:56 -05:00
johnnyq
29839d3b23 Implemented saveBase64Images() to convert base64 <img> tags into real files stored under /uploads/<module>/<id>/ with secure filenames. Added wrapper functions and updated document creation to use processed image paths. 2025-11-23 13:03:03 -05:00
johnnyq
185ea7d6ac Fix 'Email from at' On tickets that come from emails, was cuasing dup html head body tags causing htmlPurifier to strip it in ticket viewing, so we removed html body and head tags also remove orginal reply on reply ticket emails this removed ##- Please type your reply above this line -## and anything after it. Also removed the custom CSS that was embedded for all email sent out from ITFlow 2025-11-22 17:40:44 -05:00
johnnyq
ac7623d4f5 Update Add Client to use prepared statments 2025-11-21 20:53:04 -05:00
johnnyq
3d119261cc Add cleanInput function without mysqli_escape_string and converted add and edit payment method to Procedural mysqli Prepared Statments 2025-11-21 19:54:50 -05:00
wrongecho
169619c9b9 Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2025-11-18 13:08:17 +00:00
wrongecho
b991f787a2 Introduce subject-based automatic ticket merging/reply detection where e-mail is sent from a known contact or domain and the subject is a 95% match for a ticket opened in the last 7 days for that client 2025-11-18 13:08:02 +00:00
johnnyq
215fc6803e Fix Bulk Ticket Merging due to missing modal footer 2025-11-17 15:27:23 -05:00
johnnyq
a79c1c8246 Remove Duplicate get_query_strings code as this is all handled in the the filter header 2025-11-17 14:55:05 -05:00
johnnyq
1aa6419b1b Fix Broken Updating asset noted in asset details modal 2025-11-17 13:51:19 -05:00
Johnny
c7ef3627ce Merge pull request #1247 from itflow-org/develop
Merge Develop into Master for v25.11.1 release
2025-11-17 12:22:24 -05:00
johnnyq
02694f6720 Fix Broken Links in Email to Agent via Client Portal Ticket Update, update changelog 2025-11-17 12:10:10 -05:00
johnnyq
f50aabb570 Update Changelog 2025-11-17 11:59:43 -05:00
johnnyq
19b8d09bfd Update Ticket Detail card to use the adminlte's Card Collapse 2025-11-17 11:55:03 -05:00
johnnyq
66fb999a8c Update Test IMAP to use a Raw connection instead of depending on the Deprecated php-imap extension 2025-11-17 11:29:05 -05:00
johnnyq
0c5883b61b Use btn-tool for side references in Ticket details and some cleanup 2025-11-16 20:56:30 -05:00
johnnyq
ef66d5172c Move Ticket edit from top Right Menu to Ticket Details Section 2025-11-16 20:27:58 -05:00
johnnyq
118f9a34d8 Update Changelog 2025-11-16 20:02:26 -05:00
johnnyq
b61dfac569 Ticket Details Checks, Dont display Add/edit relations if no cliet in selected, dont show relations in ticket edit if no client assigned to ticket, also dont display public and email response type if no contact_email exists 2025-11-16 19:56:59 -05:00
johnnyq
79160f9b5c Dont show Client Tickets in Ticket Details Breadcrumbs if not client is assigned a ticket 2025-11-16 19:35:42 -05:00
johnnyq
d2523cff4a Add Tag Displays in edit and listing, remove unused type column 2025-11-16 18:41:30 -05:00
johnnyq
1839599769 Added Tag Type Filter Nav to tags 2025-11-16 18:21:01 -05:00
johnnyq
29e1b56e78 Hide contract side nav as its not yet complete 2025-11-16 17:58:07 -05:00
johnnyq
47e647c712 Update Changelog and bunp App Version 2025-11-16 17:55:13 -05:00
johnnyq
a87b0b0447 Fix regression in dashboard has client 2025-11-16 17:40:06 -05:00
johnnyq
96b8fcad3a Fix Pay With a Saved Card in Invoice Listing if Saved Cards are on files for that client 2025-11-16 17:34:37 -05:00
johnnyq
cf0fa0024c Update Wording on delete provider 2025-11-16 17:16:46 -05:00
johnnyq
aba5ed9271 Add Back Delete Payment Provider, the db will cascade delete all related recurring payments, related saved cards and client payment provider relation 2025-11-16 17:12:02 -05:00
johnnyq
63141f3578 Composer updates 2025-11-16 16:00:57 -05:00
johnnyq
612041635d Updated symfony/http-foundation from 7.3.3 to 7.3.7 2025-11-16 15:49:11 -05:00
johnnyq
efcc0fd5cb Add Where clause to only accept saved payment by logged in session_client_id in Client Portal 2025-11-16 15:33:45 -05:00
johnnyq
b0724f5b66 Add TOTP Secret to Export / Offboard Client Documentation PDF 2025-11-15 19:17:03 -05:00
johnnyq
66a2b4b6d2 Afer Ticket Merge Redirect to new ticket Details 2025-11-15 17:09:46 -05:00
johnnyq
77b4dfa50a Add UserID: on hover in users 2025-11-15 16:59:39 -05:00
johnnyq
1e6e7fd6d8 If ticket doesn't have a client dont show client section 2025-11-15 16:51:31 -05:00
johnnyq
46a1b673ba Fix Add Ticket Watcher 2025-11-13 00:01:35 -05:00
johnnyq
7230325e62 Migrate Ticket Template add to ajax-modal, add category type option if not defined, add product type if not defined 2025-11-12 17:04:53 -05:00
johnnyq
af8e733cfb Added Quick Add Links to the majority of Side bars navs 2025-11-12 16:27:03 -05:00
johnnyq
26ab43c57f Fix Mail Queue link when sending a test email, updated the quote send email wording to Quote sent 2025-11-12 14:50:05 -05:00
johnnyq
15ed4ef1ce Fix unable to delete Vendor Templates 2025-11-12 14:35:06 -05:00
johnnyq
0ac76766bd Add Asset Tags Display in Contact Details, asset details, along with their detail modals 2025-11-11 21:05:59 -05:00
johnnyq
abb97ad99f [Feature] Added Asset Tags 2025-11-11 19:57:51 -05:00
johnnyq
6cdc26b55b Fix broken edit payment methods due to missing hidden field 2025-11-09 12:24:04 -05:00
Johnny
d1dcc5fb7e Merge pull request #1246 from itflow-org/develop
Develop to Master for Release
2025-11-08 13:47:43 -05:00
johnnyq
9f19fd3c75 Fix Folder in Document API End point 2025-11-08 13:10:35 -05:00
johnnyq
61dedb7e7b Removed old Cron Files from /scripts/ removed old sendmail function along with PHPMailer requirments in functions.php, removed debug php ext check for php-mime-mail-parser and php-imap 2025-11-08 12:56:16 -05:00
johnnyq
65d2b8b2cb UJpdate App Version and Changelog for release 2025-11-08 12:44:58 -05:00
johnnyq
1d3f206660 Addec Contract Templates Listing add modal and add post code 2025-11-07 17:55:19 -05:00
johnnyq
ab46899e72 [FEATURE] Client Contracts, initial DB Schema, rates and net terms along with SLA Reponse / Resolution times will eventually be moved over to this 2025-11-07 16:49:35 -05:00
johnnyq
723a423b06 After ticket Creation Redirect to ticket details with Client Side bar and Top bar, Remove Currency code from Client Add / Edit defaults to company currency for now 2025-11-07 14:27:22 -05:00
johnnyq
a837b97870 Migrated the last of the bulk modals to the new ajax-modal 2025-11-06 22:49:44 -05:00
johnnyq
8be0789f25 Converted even more bulk modals to the new ajax-modal 2025-11-06 20:36:30 -05:00
wrongecho
99d017144d Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2025-11-06 16:58:35 +00:00
wrongecho
891f71006b Mail queue - Recipient domain MX validation before sending 2025-11-06 16:58:18 +00:00
wrongecho
d25017216a substr to conform to db columns for logs 2025-11-06 16:57:14 +00:00
wrongecho
83b7c7b054 substr to conform to db columns 2025-11-06 16:49:14 +00:00
wrongecho
283c2a17df sanitize after substr 2025-11-06 16:44:34 +00:00
johnnyq
44de049f3b Remove image optimization on upload for now as this will later be added a cron task 2025-11-06 11:44:22 -05:00
wrongecho
920d08f039 Note 2025-11-06 16:27:46 +00:00
johnnyq
0cf1e338c2 Added data-bulk to the ajax modal to allow for bulk GET collection of selected ids that have a class of bulk-select, converted expense and client bulk modals to use the the new ajax-modal 2025-11-06 11:26:08 -05:00
wrongecho
293a2b800e Merge pull request #1244 from Flos/fix-timer-sleeping-chrome-tab-and-initialise-on-reload
Fix ticket timer, to initialise on reload and after tab sleeping
2025-11-06 16:18:46 +00:00
wrongecho
650a099e19 Contacts API - Prevent more than 1 primary contact being set per client 2025-11-06 16:00:21 +00:00
wrongecho
46c2c8616e Clients API - Add - abbreviation support 2025-11-06 15:35:43 +00:00
wrongecho
6295a5c878 Clients API - Update endpoint 2025-11-06 15:34:19 +00:00
wrongecho
39d8e19e16 Clients API - Archiving & Un-archiving 2025-11-06 15:21:45 +00:00
wrongecho
9d3a44d110 Contacts API - Archiving & Un-archiving 2025-11-06 15:10:25 +00:00
wrongecho
54d46719c2 Certificates API - Create endpoint 2025-11-06 14:46:08 +00:00
wrongecho
dbed2c17db Certificates - Bugfix missing js to fetch details 2025-11-06 14:44:24 +00:00
wrongecho
f772ef2efd Certificates - Better empty date handling in agent ui 2025-11-06 14:44:03 +00:00
wrongecho
2f28f96f8d Log the API endpoint/URL path for API auth failures to assist with debugging 2025-11-06 14:43:32 +00:00
wrongecho
1f2bcf7c34 Domains - show a dash instead of blank where there is no expiry 2025-11-06 12:29:07 +00:00
wrongecho
a9a5850fd4 Domains - only show time to expire when there is an expiry date set (otherwise it shows epoch 56 years ago) 2025-11-06 12:19:28 +00:00
wrongecho
09f3bfd8f4 When fetching domain records, quit if no SOA record exists (prevents most sub-domains) 2025-11-06 11:57:08 +00:00
wrongecho
3813fbf8f2 When adding a domain, flag if no SOA record exists (prevents most sub-domains) 2025-11-06 10:41:52 +00:00
wrongecho
16001f8d4e When adding a contact, flag duplicate or invalid e-mail addresses 2025-11-05 22:22:28 +00:00
wrongecho
49d3dbad9a Fix client delete 2025-11-05 10:24:39 +00:00
johnnyq
56f32a4da2 Finished migrating the last of the admin add modals to ajax-modal 2025-11-05 01:54:49 -05:00
johnnyq
a297b8d6d8 Migrate many admin Add modals to the new ajax-modal 2025-11-05 01:39:22 -05:00
johnnyq
d365f48192 Minor ticket details sprucing little more padding on top ticket details and move subject to top with Ticket number 2025-11-04 19:25:07 -05:00
johnnyq
df6d955261 Migrate Schedule Ticket and Merge Ticket to ajax modal also spruce up Schedule Inteface and cleanup code 2025-11-04 18:37:43 -05:00
johnnyq
9fcaf9f5cc [Feature] Updated Invoice Ticket to include more details in the description, predefined along with optional Invoice Title, helper texts below qty and price to show how it gets its information, fixed ticket number not showing in ticket reply when adding ticket to an existing invoice. Add to Existing Invoice is default if open invoices exist, migrated many more ticket related modals to use ajax-modal 2025-11-04 17:48:21 -05:00
johnnyq
43a7b7faa5 [Feature] Allow searching quote via Global Search 2025-11-03 17:08:55 -05:00
johnnyq
69253385c5 Update Invoice, Quote, Recurring Invoice Hyperlinks to take you directly to the client view instead of keeping you in global view. We also did this with tickets in the previous commit 2025-11-03 16:29:41 -05:00
johnnyq
cea7d61481 Updated Bread crumbs in ticket, quote invoice to show All as the first breadcrumb 2025-11-03 15:43:56 -05:00
johnnyq
41f9a2e6e2 Migrated add revenue, trip, quote, invoice, recurring invoice, product, document, folder, file, project, and asset interface over to the new ajax-modal 2025-11-03 14:06:44 -05:00
johnnyq
31d3659098 Migrated product add to the new ajax-modal 2025-11-02 23:20:42 -05:00
johnnyq
c12bfb157e Migrate Add Ticket, Recurring Ticket, Project and Vendor to the new ajax-modal 2025-11-02 22:54:48 -05:00
johnnyq
a55dabb1cd Migrate credemtial, software, network, certificate, domain, service Add modals to the new ajax-modal 2025-11-02 19:39:34 -05:00
johnnyq
06fec3c280 Migrate add locaiton to new ajax-modal 2025-11-02 18:07:39 -05:00
johnnyq
f733a27ad7 Bump DataTable from 2.3.3 to 2.3.4, TinyMCE 8.0.2 to 8.2.0, Stripe-PHP 17.6.0 to 18.1.0, PHPMailer from 6.10.0 to 7.0.0, chartjs from 4.5.0 to 4.5.1 2025-11-02 16:44:59 -05:00
johnnyq
7ea39eb545 Fix non existent record in contact details, document details, document template, ticket template also add limit 1 2025-11-02 13:32:44 -05:00
johnnyq
a85f898ef5 Fix No records exist if client_id in the uri is non existent 2025-11-02 13:13:51 -05:00
johnnyq
519975f3cf Fix Include footer link in project details when no record exists 2025-11-02 12:49:24 -05:00
Johnny
0e9a071e96 Merge pull request #1245 from itflow-org/undefined-asset-handling
Better error handling for undefined/non-existent asset IDs
2025-11-02 12:42:25 -05:00
wrongecho
3917e66fd8 Better error handling for undefined/non-existent asset IDs 2025-11-02 17:17:37 +00:00
wrongecho
9f48e2d9f0 Better error handling for undefined/non-existent asset IDs 2025-11-02 17:14:01 +00:00
wrongecho
215eadcf2b Better error handling for undefined/non-existent asset IDs 2025-11-02 17:12:03 +00:00
Flos
b09e4938b7 Fix ticket timer, to initialise on reload and after tab sleeping
When the tab was reloaded, the timer was not initialized again.
When the tab was in background and the tab was sleeping, the timer showed the wrong time
2025-11-01 23:23:25 +01:00
johnnyq
d3d706ea68 Added ticket details to the search query in global search tickets 2025-11-01 16:28:23 -04:00
johnnyq
8268761ef4 Add Recurring Invoice Reference along with a link in Invoices 2025-11-01 16:17:11 -04:00
wrongecho
2850c35bdc Flag duplicate clients/leads when using the client_add modal 2025-11-01 18:48:23 +00:00
wrongecho
24d8635dac Invoice product autocomplete - search product code as well as name 2025-11-01 17:59:12 +00:00
johnnyq
8314a115bb Migrate Add Asset to the new ajax-modal 2025-10-30 19:05:37 -04:00
johnnyq
b8e2423dbd Mugrated Payment Modals from invoice folder to payment modal folder 2025-10-30 14:46:49 -04:00
johnnyq
52c67f4139 Remove unused budget code 2025-10-30 14:29:51 -04:00
johnnyq
e895156d03 Fix TinyMCE not working on bulk create tickets and sort Categories ASC and fix assign to 2025-10-30 14:06:09 -04:00
johnnyq
89abc18465 Migrated Contact Add to the ajax-modal 2025-10-30 13:58:10 -04:00
johnnyq
355dfbbb25 [Feature] Create Recurring Ticket - Add Asset Type Optgroups on asset selection along with make, model and assigned contact 2025-10-29 19:14:21 -04:00
johnnyq
6d15640ae4 [Feature] Recurring Ticket - Add Three Day and Biweekly to the Frequency options 2025-10-29 18:15:21 -04:00
johnnyq
ad4ab5a54c Fix in old add ticket 2025-10-29 17:50:37 -04:00
johnnyq
3c5c86c4c5 Fix Issue with user not showing up in add ticket and recurring ticket, Also fixed the sort from DESC to ASC in some areas with user select 2025-10-29 17:49:39 -04:00
johnnyq
09b91c8826 [Feature] Recurring Tickets - Add Category, Assigned Agent and Billable Status filters 2025-10-29 17:13:52 -04:00
johnnyq
13ea48bff8 Seperate out recurring expenses and expenses into their own post file 2025-10-29 16:21:56 -04:00
johnnyq
26bb430d6e Seperate out posts into payment, invoice and recurring invoice instead of them all being under invoice post file 2025-10-29 16:18:32 -04:00
johnnyq
82da54740f Moved recurring ticket posts to its own post file 2025-10-29 15:58:04 -04:00
johnnyq
e02b10d12a [Feature] Added Billable column in recurring ticket list view along with bulk actions to set priority, agent, billable status, category and next run date 2025-10-29 15:51:14 -04:00
johnnyq
1573045157 Sort recurring tickets by Next Run Date instead of subject, and Swap Client column with Netrun column so Client column is last and next run date is first 2025-10-29 12:25:02 -04:00
johnnyq
bf31c333a6 Migrate Add Client Modal to the new Ajax Modal 2025-10-28 16:56:27 -04:00
johnnyq
4229bca978 Major UI Work on Contact Details Modal Always Display common details at the top with the nav underneath. 2025-10-25 18:16:56 -04:00
johnnyq
13bd929755 Tidy up the contact details modal 2025-10-25 14:44:20 -04:00
johnnyq
7f6c0346af Add https:// prefix to vendor website field in vendor details modal 2025-10-25 13:09:25 -04:00
johnnyq
0387e66066 Added Sortable Task Count in Ticket Templates Listing view 2025-10-25 12:54:58 -04:00
johnnyq
04bae8dc37 Add Archive and Delete buttons to document details along with button titles to state what the button does, also added a from var to fix redirect behavior if deleting from document details go back to documents listing 2025-10-24 14:46:09 -04:00
johnnyq
559506fc90 Added Access Modules to view current modules and to allow custom modules for the future for use in custom code directories 2025-10-23 15:55:54 -04:00
johnnyq
f2b6d481a1 Feature: Add new date range picker to admin area mail queue audit log, app log 2025-10-23 13:30:22 -04:00
johnnyq
c66aa92365 Update All Side Nav Links to be absolute so the side bar includes can be navigatable when navs are included in custom code 2025-10-23 13:07:02 -04:00
johnnyq
e24ef68d8d Fix Deleting Recurring Ticket from asset details page due to missing CSRF Check token 2025-10-22 17:11:26 -04:00
johnnyq
0cacf83ae5 Fix Sending Email when Forcing a Recurring Invoice into an Invoice 2025-10-22 16:28:53 -04:00
johnnyq
2dc66b329b Fix Ajax Modal Link to referral category in Add Bulk Referral 2025-10-22 15:26:15 -04:00
johnnyq
10dc8ea2bf Wording update for client bulk modals 2025-10-22 14:53:43 -04:00
johnnyq
303f9174c9 Added Bulk Create Tickets for Clients 2025-10-22 14:50:50 -04:00
johnnyq
c5dd5f2b6f Add Clause to not collapse advanced filter on all time aka if date from is set to the default 1970-01-01 2025-10-20 18:16:39 -04:00
johnnyq
ab77705ca2 Feature: Replace old date range to Date Range Picker JS for better date from/to handling 2025-10-20 18:04:00 -04:00
johnnyq
10c89ebf73 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2025-10-16 11:43:37 -04:00
johnnyq
ecce994921 Used status var unstead of get status var for checkall 2025-10-16 11:43:26 -04:00
wrongecho
5dd4f5ea62 New mail parser:
- bugfix .eml not being generated
- include the message when notifying the tech of a reply
2025-10-16 16:32:37 +01:00
wrongecho
93bb5db019 typo 2025-10-15 21:56:21 +01:00
wrongecho
65ff008ccf Bugfix - Email not including ticket guest key 2025-10-15 15:36:07 +01:00
wrongecho
f0c48d23fe Add html code plugin + button to tinymceticket 2025-10-15 15:27:56 +01:00
wrongecho
975b52a43d Time tracking - show H/M/S placeholders if timer auto-start is disabled 2025-10-15 12:29:26 +01:00
wrongecho
079b0d5024 Asset import - allow importing notes 2025-10-15 10:32:16 +01:00
wrongecho
99ccb12b8c Allow importing TOTP credential info 2025-10-15 10:31:59 +01:00
wrongecho
0bb7d24e07 Allow importing TOTP credential info 2025-10-15 10:18:44 +01:00
wrongecho
b7a9f9ea38 When exporting credential info, include the TOTP secret 2025-10-15 10:12:14 +01:00
wrongecho
21aee98f9f Fix checkAll ticket box not showing when status wasn't set - should only be hidden for the closed view 2025-10-15 09:57:32 +01:00
wrongecho
9a5a4be64a When archiving a client, cancel recurring invoices 2025-10-15 09:20:08 +01:00
wrongecho
db7f8501d0 When archiving a client, cancel recurring invoices 2025-10-15 09:18:53 +01:00
johnnyq
61d15cbf9e Remove non existent seatch column recurring ticket prefix 2025-10-14 16:07:08 -04:00
johnnyq
39c9c695f1 Allow searching tickets with ticketprefix and number combo in Global search 2025-10-14 15:59:29 -04:00
johnnyq
d97654581b Add 30 Day wording to Expiring Domain and Certificates in dashboard 2025-10-12 13:34:27 -04:00
johnnyq
2ee70fd3a8 Update .htaccess 2025-10-09 19:23:48 -04:00
johnnyq
b336ec4188 Revert setup restore to a saner version 2025-10-09 19:14:31 -04:00
johnnyq
c77e1be1c3 Try to fix uploads 2025-10-09 19:00:02 -04:00
johnnyq
986f688468 another Attempt at restore 2025-10-09 18:49:54 -04:00
johnnyq
1d9429b762 Another attempt at restore 2025-10-09 18:27:35 -04:00
johnnyq
d122d90a47 Remove CSRF check 2025-10-09 18:11:16 -04:00
johnnyq
2c534d4d20 Attempt to fix uploads and writing to config file during setup 2025-10-09 18:10:21 -04:00
johnnyq
b7e0e5c5eb Fix setup complete flag 2025-10-09 13:00:00 -04:00
johnnyq
2915b12181 Remove temp CSRF check on setup 2025-10-09 12:43:28 -04:00
johnnyq
ed589ef65b Update Backup / Restore, now streams backup and restore to disk instead of memory causing memory to run out, sets timeout limit to unlimited, checks backup file contents for anything bad, use php instead shell exec for import of db, added .htaccess for apache to prevent php execution in /uploads/ directory as this is intended for file download only 2025-10-09 12:28:38 -04:00
Johnny
0d5bfdafdf Merge pull request #1242 from itflow-org/develop
Develop to Master
2025-10-08 17:39:41 -04:00
johnnyq
fbf3346052 Update Changelog 2025-10-08 17:30:19 -04:00
johnnyq
3ff206f84d Add .htaccess in cron 2025-10-08 17:25:03 -04:00
johnnyq
a3b0fce961 Fix login_microsoft 2025-10-08 17:19:17 -04:00
johnnyq
8130280b35 Fix edit from contact details modal 2025-10-08 17:12:23 -04:00
johnnyq
fea3020d9a Add powered by ITFlow in Guest Section 2025-10-08 15:29:06 -04:00
johnnyq
1eb9d163fa Updated Changelog 2025-10-08 14:50:34 -04:00
johnnyq
e3e7c2e38b Add Signature in ticket reply POST dont add a signature if Internal 2025-10-08 14:48:13 -04:00
johnnyq
27e1d6a9cd remove net terms in quote add jquery to guest header 2025-10-08 01:18:55 -04:00
johnnyq
2ec4cdc4fb Ceated inc_all_guest.php and modulaized the guest header.php removed guest footer and used the global footer.php as they were very similar 2025-10-08 01:00:48 -04:00
johnnyq
35a7506c26 Copy crons from /scripts to /cron, added custom directories for api/v1/, /setup, /cron and /scripts 2025-10-07 13:55:54 -04:00
johnnyq
16242be74e Update Client Nav to use Absolute links and updated more of the inc_alls to use Document Server Root 2025-10-07 13:44:08 -04:00
johnnyq
3fcbe440d3 Fix Missing Missing Country code in Guest View Ticket 2025-10-07 12:34:26 -04:00
johnnyq
4ef0755039 Update Guest header and footer to use Server Document Root var 2025-10-06 17:07:19 -04:00
johnnyq
a4ed906dd1 Update modal footer and header to user SERVER Document root with absolute path intead of relative 2025-10-06 15:13:52 -04:00
johnnyq
416a8d9a94 Fix to properly redirect to the setup page if config_enable_setup is not set or is 1 2025-10-06 14:19:49 -04:00
wrongecho
d8803aaac2 prevent open redirects upon agent login 2025-10-06 16:32:42 +01:00
wrongecho
01f6615ca0 rm test 2025-10-06 16:13:08 +01:00
johnnyq
fd93ee3263 Allow HTML for signatures 2025-10-04 19:20:34 -04:00
johnnyq
32bfd298a1 Added Project Edit Function for Ticket 2025-10-03 17:37:25 -04:00
johnnyq
5de2e7a3bd If imap Encryption is blank then use notls in the ticket mail parser 2025-10-03 11:36:27 -04:00
johnnyq
6e8c133a99 Fix Regressions in Vendor Templates updated path from ../user/post to ../agent/post 2025-10-03 11:25:48 -04:00
johnnyq
956f18430b Fix Microsoft SSO Login in Client Portal, fix ticket templates due to regression from changing from user to agent 2025-10-03 11:12:48 -04:00
johnnyq
76c9933baf Update imap and smtp providers to allow empty string if empty do not execute mail queue 2025-10-02 14:28:43 -04:00
johnnyq
6c6a988c2b Fix custom Favicon 2025-10-02 11:42:30 -04:00
1144 changed files with 47254 additions and 33199 deletions

23
.gitignore vendored
View File

@@ -4,8 +4,16 @@ config.php
uploads/favicon.ico uploads/favicon.ico
uploads/clients/* uploads/clients/*
!uploads/clients/index.php !uploads/clients/index.php
uploads/custom/*
!uploads/custom/index.php
uploads/documents/*
!uploads/documents/index.php
uploads/document_templates/*
!uploads/document_templates/index.php
uploads/expenses/* uploads/expenses/*
!uploads/expenses/index.php !uploads/expenses/index.php
uploads/recurring_tickets/*
!uploads/recurring_tickets/index.php
uploads/settings/* uploads/settings/*
!uploads/settings/index.php !uploads/settings/index.php
uploads/users/* uploads/users/*
@@ -14,6 +22,8 @@ uploads/tmp/*
!uploads/tmp/index.php !uploads/tmp/index.php
uploads/tickets/* uploads/tickets/*
!uploads/tickets/index.php !uploads/tickets/index.php
uploads/ticket_templates/*
!uploads/ticket_templates/index.php
.idea/* .idea/*
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/* plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/*
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/.gitkeep !plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/.gitkeep
@@ -22,10 +32,6 @@ plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/URI/*
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/* plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/*
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/.gitkeep !plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/.gitkeep
.vscode/settings.json .vscode/settings.json
xcustom/*
!xcustom/readme.php
post/xcustom
!post/xcustom/readme.php
admin/custom/* admin/custom/*
!admin/custom/readme.php !admin/custom/readme.php
agent/custom/* agent/custom/*
@@ -34,5 +40,12 @@ client/custom/*
!client/custom/readme.php !client/custom/readme.php
guest/custom/* guest/custom/*
!guest/custom/readme.php !guest/custom/readme.php
cron/custom/*
!cron/custom/readme.php
scripts/custom/*
!scripts/custom/readme.php
setup/custom/*
!setup/custom/readme.php
api/v1/custom/*
!api/v1/custom/readme.php
.zed .zed

View File

@@ -2,6 +2,311 @@
This file documents all notable changes made to ITFlow. This file documents all notable changes made to ITFlow.
## [26.03] Stable Release
### Bug Fixes
- Ticket Templates: Fix Task Sorting.
- Ticket: Lower autoclose setting minimum value from 48 to 24 Hours.
- Ticket: Fix Task Approval.
- Recurring Ticket: add empty value placeholder for Ticket Frequency.
- Documents/Files: Fix redirect after File Upload to redirect to files instead of the non existent documents.
- Setup: Fix base url tacking on /setup when not installing via script.
### New Features & Updates
- Clients: Net Terms: Added common 45 and 15 Days, removed 14 Days not as common.
- Clients: Bulk Action Set Net Terms Added.
- Clients: Swapped location and contact column, add PopOver with Details such as created, abbreviation, DB ID instead of taking up space underneath client, rounded tag pills and increased padding, removed info badges and added one info badge that displays a popover with details.
- Clients: Added New Ticket to Client Top Header Menu.
- Clients: Client Overview: UI Sprucing.
- Invoice: Send reminder 1 day after due date.
- Invoices/Quotes/Recurring Invoices: Split Items tables into their own POST logic and Modal UIs and tables (quote_items, invoice_items, recurring_items).
- Tickets: New Ticket Parsing - Anyone CC'ed onto the original email that created the ticket is added as a ticket watcher.
- Ticket/Quotes: Quotes can now be associated with a ticket.
- Networks: Removed Subnet Mask Field, Use CIDR instead.
- Networks: Rearranged fields, Updated placeholders, Add/Edit/list for better flow.
- Networks: Renamed DHCP to IP Range to allow for you use of both DHCP and or Usable IPs.
- Assets: Rearranged fields, Updated placeholders, Add/Edit/list for better flow.
- Assets: Added IPv6 if available under IP, Make and Model are now one line with Serial Underneath. Added OS under Type. use pill for status.
- Calendar: Event thats are cut off can now be viewed as a tooltip on hover.
- Calendar: Renamed System Calendars to built-in calendars and added the names and color dot for reference.
- Calendar: You can now delete a custom calendar.
- Report: Client Ticket Time Detail Audit: Selectable Billing Time Increment, will later be avauilable globally.
- Roles/Permissions: Now complete and is out of beta all permission roles are strictly enforced, except for in Trips and Calendar, new enforce modules will be added for these at a later date.
- Project Templates: Ticket Template order can now be dragged and dropped.
- Global: Introduced new checkbox class to all Checkbox select columns to keep consistency and reduce space and enhance ui.
- Global: CSRF Checks everywhere instead of just deletion calls.
- Global: Renamed the rest of the unarchive post and label calls to restore.
- Files: Allow upload of .unifi extension.
- Bump Libraries:
- stripe-php from 19.0.0 to 19.4.1.
- fullcalendar from 6.1.19 to 6.1.20
- TCPDF from 6.10.1 to 6.11.2
## [26.02.1] Maint Release
### Bug Fixes
- Credentials: Fix Password Generator.
- Calendar: Restrict Events for client restricted agents.
- Ticket Merge: Fix.
- Asset Transfer: Fix.
- Ticket Listing: Restrict Tickets presented in ticket list view from client restricted agents.
- Ticket Details: Deny access to client restricted agents to view tickets without client_id in uri.
- Tickets: Allow agents with restricted client access to view and edit tickets without a client.
- Ticket Change client: Limit selection for agents with restricted client access.
- Ticket Details: Don't display updated at when null.
### New Features & Updates
- Report: Added Client Detail Auditing.
- API: Added Endpoint to retrieve time worked by agent.
- ajax-modal: Revert to previous JS implementation before 26.02 release.
- Ticket: Move Subject from Ticket main ticket header to ticket details card header.
## [26.02] Stable Release
### Bug Fixes
- Mail Parser - Do not automatically send new ticket notifications to noreply/donotreply addresses.
- Ticket: removed newline \n on Parsed emails.
- Show Trips for everyone if accounting module is enabled.
- Fix Invoice Exporting.
- Fix Billable Column not sorting correctly in tickets.
- Fix Login flow where user agent and client user exists and agent has MFA but will not let them continue.
- Fix passing missing user_id var in client portal.
- Fix Ticket Templates not auto filling when selected.
- Fix Invoices not being sent to all billings contacts when manaully sent.
- Fix Documents and Files not able to be bulk deleted.
- Fix Role Archiving, can be archived as long as no users are assigned to the role.
- Fix showing Powered By ITFlow visibility on the login screen when Whitelabel is enabled.
- Missing username in audit log on successful login due to missing passed user_id to logging.
- API: Fix updating all documents instead of the intended document.
- Documents: Fix Document created at not showing the correct creation date of the master document.
- Ticket: Fixed Using edit ticket modal agent was not able to be set.
- Always check if a user is archived and or disabled instead of just during login.
- Report: Fix Collected tax report not totalling all tax categories.
### New Features & Updates
- Task Approval System for ticket tasks: Once an approval is requested, the task cannot be marked as complete until approved. Internal Approvals Any other technician, or Specific technician, Client Approvals Anyone (usually the requestor) Tech contacts Billing contacts.
- Printable Invoice Packing Slips now available.
- Drastic Performance Bump: Up to 50% faster queries accross the board and reduced server memory usage by 40% by switching Database Query method from mysqli_fetch_array to mysqli_fetch_assoc.
- Added Connect to Microsoft 365 Button to mail settings.
- OAUTH2 support for Microsoft 365 and Google Workspaces is now considered stable and working.
- Favorites: Assets and Credentials now can be favorited singly or by Bulk action. Favorited items appear in the client overview now.
- Files/Documents: Collapsable folders feature, collapsed by default with a button to expand all.
- URL Keys and such are now set to a more manageable 32 Characters by default.
- Various UI/UX Updates throughout the app, with focus oin ticket details, contact details modal etc.
- Added Show Archived files and documents to the files section.
- Added Bulk Archive and restore options to files and documents.
- Rewrite of the Kanban Ticket view to match our procedural style of coding.
- All options are available in TinyMCE now in Mobile mode.
- Agent names appear now in Invoice History section.
- Mail Parser: Support flowed text.
- Assets: Keep Purchase reference when copying.
- Assets: Add basic tracking history: Archiving, restoring, name changes, transferimg to new clients.
- Mail Parser: NDR Parsing.
- Allow SVG files in mail attachments.
- Tickets: Use a more friendly time worked instead of 02:41:00 translates to 2h 41m.
- Update wording on ticket to invoice item details.
- Merge Tickets: Now wth a ticket merge dropdown list of tickets instead of a text field.
- Role Permissions can now be set during role creation, update Permission UI to use radio buttons instead of select boxes.
- Bump TinyMCE 8.2.2 to 8.3.2.
- Bump PHPMailer from 7.0.1 to 7.0.2.
- Bump Datatables from 2.3.4 to 2.3.7.
## [25.12.1] Maint Release
### Major Changes
- Unified the Client/Agent Login and process (Note only Client Users can Reset passwords from the login page, does not apply to agent users).
### Bug Fixes
- Fix Payment Provider not adding an account.
- Fix New ticket button in contact details in the related tickets section.
### New Features & Updates
- You can now Set Payment Provider income/expense account, expense vendor and expense category upond creation or editing.
- Moved Saved Payment Provider Methods away from admin side nav to the count link within Payment Providers page.
- Moved AI Models from the admin side nav to the model count link within AI Providers.
- Add Favicon Reset.
## [25.12] Stable Release
### Breaking Changes ###
- For Existing installs: **php-xml** extension needs to be installed for document creation and editing, new install script does this for you as of Dec 6th 2025. To install php-xml: `sudo apt install php-xml`
### Major Changes
- Consolidated "Files" and "Documents" into a single section called **Files**.
### Bug Fixes
- Resolved issue with updating asset notes in asset details.
- Fixed problem with bulk ticket merging.
- Corrected issue where decimal inputs (e.g., price, cost) werent displaying on iPhones in certain forms.
- Added CSV escaping to the sample export data in areas where a sample CSV template is provided.
- Fix a race condition where dupe tickets, invoices, recurring invoices, recurring tickets, quotes will be created using the same number if created in parallel espcecially when using the API.
### New Features & Updates
- Introduced automatic subject-based ticket merging/reply detection. Now, if an email comes from a known contact or domain and the subject matches 95% of a ticket opened in the last 7 days, it will be merged automatically.
- Added `cleanInput` function to sanitize data before inserting it into the database when using MySQLi prepared statements.
- Migrated client post functionality to use MySQLi prepared statements.
- Updated payment method post functionality to use MySQLi prepared statements.
- Implemented `saveBase64Images()` to convert base64-encoded `<img>` tags into actual image files stored under `/uploads/<module>/<id>/` with secure filenames. Added wrapper functions, and updated document creation to use processed image paths.
- For new documents and document templates, images are now stored in `/uploads/documents/$document_id` instead of being stored as base64 in the database, using the `saveBase64Images()` function.
- UI/UX improvements made to the document details page.
- Removed sidebar quick-add options.
- Created new folders in the uploads directory: `documents`, `document_templates`, and `recurring_tickets`.
- Reworked the bulk action function to pass the name arrays, instead of a generic `selected_ids` array. This allows multiple bulk name arrays to be passed at once, currently used for the new file-document merge.
- Big task: Converted the remaining modals to use the new `ajax-modal` system, enabling more flexible flow expansion going forward.
- Mail queue: Added a `--no-mx-validation` flag to bypass recipient domain MX validation.
- Bump PHPMailer from 7.0.0 to 7.0.1.
- Bump stripe-php from 18.1.0 to 19.0.0.
- Bump TCPDF from 6.10.0 to 6.10.1.
- Bump TinyMCE from 8.2.0 to 8.2.2.
## [25.11.1] Maint Release
### Fixes
- Fix broken edit Payment Method.
- Fix unable to delete Vendor Template.
- Fix Mail Queue link in flash alert for testing email and sending a quote.
- Add Show Category Type select if not defined.
- Add Show Product Type select if not defined.
- Fix add ticket watcher.
- Fix if Client isn't assigned to a ticket dont show client view.
- Fix missing session client id check when paying an invoice from client portal.
- Update Composer Webklex-IMAP library dependency symfony/http-foundation from 7.3.3 to 7.3.7 to fix security related issues.
- Add back delete Payment provider the database will handle cascade deletes to saved cards, recurring payments and client payment provider reference.
- Don't show Client Tickets Breadcrumb if no client is assigned to a ticket.
- Don't Show Contact or Assignment Tab in edit ticket if no Client is Assigned.
- Don't Show add contact, asset, vendor, watcher if not client is assigned to a ticket.
- Don't Show Public Comment & Email if contact email doesn't exist.
- Fixed IMAP Test whicn now uses RAW TCP Connection instead of the depracated php-imap extension.
- Fix Broken Link in Ticket Updates via Client Portal to agent.
### Added / Changed
- [Feature] Added Asset Tags.
- [Feature] Added Quick Add Links to most side bar navs example quickly add a client from sidebar.
- Migrate ticket template add to ajax modal.
- Add TOTP secret to Client Export PDF in Credential section.
- Add UserID on hover in users listing.
- Merge ticket now redirects to the new ticket details page.
- [Feature] Add Pay via saved card under invoice Listings.
- Ticket Related Side Items UI Cleanup to use btn-tool class.
## [25.11] Stable
### Deprecation Notice:
- **Outdated CRON Scripts**: The following scripts are removed.
- `/scripts/cron_mail_queue.php`
- `/scripts/cron_ticket_email_parser.php`
- `/scripts/cron.php`
- `/scripts/cron_domain_refresher.php`
- `/scripts/cron_certificate_refresher.php`
**Action Required**: Transition to the new versions:
- `/cron/mail_queue.php`
- `/cron/ticket_email_parser.php`
- `/cron/cron.php`
- `/cron/domain_refresher.php`
- `/cron/certificate_refresher.php`
- PHP Extensions php-imap and php-mime-mail-parser are no longer required.
---
### Fixes
- **Ticket Listing**: Resolved issue where the “Check All” checkbox was visible even when ticket status wasnt set. Now hidden for closed tickets only.
- **Timer Auto-Start**: Show H/M/S placeholders when timer auto-start is disabled.
- **Ticket Guest URL**: Fixed email not including the ticket guest URL key.
- **EML Generation**: Resolved issue with EML not being generated in the new ticket parser.
- **New Ticket Mail Notification**: Included message when notifying the tech of a reply in the new ticket mail parser.
- **Advanced Filter Collapse**: Added clause to prevent collapse of advanced filters when the “from” date is set to the default (1970-01-01).
- **Recurring Invoice**: Fixed issue where email was marked as sent but not actually sent when forcing a recurring invoice to an invoice.
- **CSRF Token**: Fixed issue with deleting recurring ticket from asset details page due to missing CSRF check token.
- **Vendor Website Link**: Fixed missing `https://` prefix in the vendor website link on the vendor details modal.
- **Agent Select Box**: Resolved issue where agents sometimes didnt appear in the agent select boxes.
- **TinyMCE**: Fixed TinyMCE editor issue on Bulk Create Ticket in Assets.
- **Ticket Timer**: Fixed ticket timer initialization after reload and when the tab is put to sleep (background tab).
- **Client Deletion**: Fixed issue with client deletion.
- **Domain Records**: Added flag for missing SOA record when adding a domain (prevents subdomain creation).
- **Domain Fetching**: Quits domain record fetching if no SOA record exists (prevents subdomains).
- **Domain Expiry**: Only show time to expiry when theres an expiry date set; otherwise, display a dash.
- **Certificates**: Improved handling of empty date in the agent UI.
- **Certificates API**: Fixed bug with missing JS to fetch certificate details.
- **API Updates**:
- Clients API: Added support for archiving/un-archiving clients, updating client data, and abbreviation support.
- Contacts API: Added archiving/un-archiving and restriction to only allow one primary contact per client.
- Mail Queue: Added recipient domain MX validation before sending emails.
---
### Added / Changed
- **Backup / Restore**: Improved backup and restore by streaming data to disk (to prevent memory issues), setting unlimited timeouts, checking for bad backup contents, and using PHP for DB import instead of shell exec. Added `.htaccess` to prevent PHP execution in `/uploads/` directory.
- **Ajax Modals**: Migrated all Add and Bulk modals to the new Ajax Modal for improved performance.
- **Recurring Ticket Sorting**: Default sorting of recurring tickets by `RunDate` instead of subject.
- **Recurring Ticket Enhancements**:
- Added Billable column.
- Added bulk actions for setting priority, agent, billable status, and next run dates.
- Added filters for category, assigned agent, and billable status.
- Added new frequency options: 3-day and biweekly.
- **Asset Select**: Updated asset select dropdown to separate asset types using opt groups (planned for wider use).
- **Expiring Domains & Certificates**: Added "30 Day" warning for expiring domains and certificates in the dashboard.
- **Ticket Search**: Allowed search using both ticket prefix and number.
- **Recurring Invoice**: Cancel recurring invoices when the associated client is archived.
- **Credentials Import/Export**: Now includes TOTP secrets when importing/exporting credentials.
- **Asset Notes Import**: Allowed importing of asset notes.
- **Ticket View**: Added a "View HTML Code" button in all ticket views for TinyMCE.
- **Date Range Picker**: Updated all date filters to use the improved DateRangePicker JS.
- **Bulk Ticket Creation**: Added bulk ticket creation for clients.
- **Sidebar Updates**: Updated all sidebars to use absolute paths for easier integration with custom code.
- **Document Actions**: Added Archive and Delete buttons to the Document Details view with improved redirect behavior.
- **Ticket Template Sorting**: Allowed sorting by task count in ticket templates.
- **Contact Modal UI**: Updated contact details modal to display contact information at the top.
- **API & Code Updates**:
- Separated out post files for recurring tickets, invoices, expenses, and payments.
- Removed unused budget code.
- **Invoice Product Autocomplete**: Now allows searching for product codes as well as names.
- **Client Duplicate Check**: Flags duplicate clients or leads when using the client add modal.
- **Recurring Invoice Reference**: Added a column to invoices indicating if they were created from a recurring invoice.
- **Global Search Enhancements**:
- Allowed ticket details to be searchable in global search.
- Allowed searching for quotes in global search.
- **UI/UX Improvements**:
- Spruced up the ticket details page UI.
- Added contact email validation to flag duplicates or invalid addresses.
- **API Debugging**: Log API endpoint/URL path for authentication failures to aid in debugging.
- **Image Upload Optimization**: Removed image optimization from uploads (this will be handled by a cron job in the future).
- **View Behavior Change**: Updated ticket/invoice/quote views to always be in the Client section, showing client-side navigation and top info bar.
---
### Library Updates:
- **DataTable**: Bumped from 2.3.3 to 2.3.4.
- **TinyMCE**: Bumped from 8.0.2 to 8.2.0.
- **Stripe-PHP**: Bumped from 17.6.0 to 18.1.0.
- **PHPMailer**: Bumped from 6.10.0 to 7.0.0.
- **Chart.js**: Bumped from 4.5.0 to 4.5.1.
## [25.10.1]
- Deprecation Notice: `/scripts/cron_mail_queue.php` , `/scripts/cron_ticket_email_parser.php` , `/scripts/cron.php` `/scripts/cron_domain_refresher.php`, `/scripts/cron_certificate_refresher.php` are being phased out. Please transition to `/cron/mail_queue.php` , `/cron/ticket_email_parser.php`, `/cron/cron.php`, `/cron/domain_refresher.php`, `/cron/certificate_refresher.php` These older scripts will be removed in the November 25.11 release—update accordingly. 25.10.1 installs have the script already configured.
### Fixes
- Fix regression missing custom Favicon.
- Update SMTP and IMAP provider to allow for empty strings, empty means disabled.
- Fix Client portal Microsoft SSO Logins.
- Fix regression in Vendor Templates.
- Fix refression in some broken links from user to agent.
- Fix Project edit.
- Prevent open redirects upon agent login.
- Fix regression on switching to Webklex IMAP to allow for no SSL/TLS in IMAP.
- Fix Setup Redirect not behaving properly when setup hasnt been performed.
- Added Server Document Root Var to several includes, headers, footers files to allow includes from deeper directory strutures such as the new custom directories.
- Fix edit contact in contact details.
- Add .htaccess to /cron/.
### Added / Changed
- Support for HTML Signatures.
- Add Edit Project Functionality in a ticket.
- Added more custom locations: /cron/custom/, /scripts/custom/, /api/v1/custom/, /setup/custom/.
- Copied `/scripts/cron.php` `/scripts/cron_domain_refresher.php`, `/scripts/cron_certificate_refresher.php` to `/cron/cron.php`, `/cron/domain_refresher.php`, `/cron/certificate_refresher.php`. See Above!
- Signatures is now handled in post ticket reply on Public Comments only.
## [25.10] ## [25.10]
### Breaking Changes ### Breaking Changes
@@ -136,7 +441,7 @@ We will provide example code with directory structure for each custom directory
--- ---
### Fixed ### Fixed
- Several security vulnerabilities patched. - Several security vulnerabilities patched (with thanks to www.helx.io).
- Ticket status is no longer updated when scheduling. - Ticket status is no longer updated when scheduling.
- Client Portal: Tech contacts can no longer edit their own details. - Client Portal: Tech contacts can no longer edit their own details.
- Fixed overlapping logo issue in Invoice/Quote PDF exports. - Fixed overlapping logo issue in Invoice/Quote PDF exports.

View File

@@ -16,7 +16,7 @@
<br /> <br />
<a href="https://demo.itflow.org"><strong>View demo</strong></a> <a href="https://demo.itflow.org"><strong>View demo</strong></a>
<br /> <br />
Username: <b>demo@demo</b> | Password: <b>demo</b> Username: <b>demo@demo.com</b> | Password: <b>demo</b>
<br /> <br />
<br /> <br />
<a href="https://itflow.org/#about">About</a> <a href="https://itflow.org/#about">About</a>
@@ -82,7 +82,7 @@ We track the implementation of confirmed features and bugs via [TaskFlow](https:
For help using ITFlow, bugs, feature requests, and general ideas / discussions please use the community [forum](https://forum.itflow.org). For help using ITFlow, bugs, feature requests, and general ideas / discussions please use the community [forum](https://forum.itflow.org).
### Contributing ### Contributing
If you want to improve ITFlow, feel free to fork the repo and create a pull reques. Make sure to discuss significant changes or new features with fellow contributors on the forum first. This helps ensure that your contributions are aligned with project goals, and saves time for everyone. All contributions should follow our [code standards](https://docs.itflow.org/code_standards). See the [contributing guide](https://docs.itflow.org/contribute). We have temporarily paused PRs from the community.
#### Contributors #### Contributors
<a href="https://github.com/itflow-org/itflow/graphs/contributors"> <a href="https://github.com/itflow-org/itflow/graphs/contributors">
@@ -93,6 +93,7 @@ If you want to improve ITFlow, feel free to fork the repo and create a pull requ
Were incredibly grateful to the organizations and individuals who support the project - a big thank you to: Were incredibly grateful to the organizations and individuals who support the project - a big thank you to:
- CompuMatter - CompuMatter
- F1 for HELP - F1 for HELP
- digiBandit
- JetBrains (PhpStorm) - JetBrains (PhpStorm)
## License ## License

View File

@@ -13,7 +13,7 @@ We operate a rolling release model. Any bug fixes will be released into latest v
| Version | Supported | | Version | Supported |
|---------| ------------------ | |---------| ------------------ |
| 25.05 | :white_check_mark: | | 25.12 | :white_check_mark: |
## Reporting a Vulnerability via GitHub Security Advisories ## Reporting a Vulnerability via GitHub Security Advisories

View File

@@ -12,11 +12,21 @@ $num_rows = mysqli_num_rows($sql);
?> ?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="/admin">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="ai_provider.php">AI Providers</a>
</li>
<li class="breadcrumb-item active">AI Models</li>
</ol>
<div class="card card-dark"> <div class="card card-dark">
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Models</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Models</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addAIModelModal"><i class="fas fa-plus mr-2"></i>Add Model</button> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_model_add.php"><i class="fas fa-plus mr-2"></i>Add Model</button>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -48,7 +58,7 @@ $num_rows = mysqli_num_rows($sql);
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$provider_id = intval($row['ai_provider_id']); $provider_id = intval($row['ai_provider_id']);
$provider_name = nullable_htmlentities($row['ai_provider_name']); $provider_name = nullable_htmlentities($row['ai_provider_name']);
$model_id = intval($row['ai_model_id']); $model_id = intval($row['ai_model_id']);
@@ -104,5 +114,4 @@ $num_rows = mysqli_num_rows($sql);
</div> </div>
<?php <?php
require_once "modals/ai/ai_model_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -16,7 +16,7 @@ $num_rows = mysqli_num_rows($sql);
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Providers</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Providers</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addAIProviderModal"><i class="fas fa-plus mr-2"></i>Add Provider</button> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_provider_add.php"><i class="fas fa-plus mr-2"></i>Add Provider</button>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -39,7 +39,7 @@ $num_rows = mysqli_num_rows($sql);
Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?> Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?>
</a> </a>
</th> </th>
<th> <th class="text-center">
<a class="text-dark">Models</a> <a class="text-dark">Models</a>
</th> </th>
<th class="text-center">Action</th> <th class="text-center">Action</th>
@@ -48,7 +48,7 @@ $num_rows = mysqli_num_rows($sql);
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$provider_id = intval($row['ai_provider_id']); $provider_id = intval($row['ai_provider_id']);
$provider_name = nullable_htmlentities($row['ai_provider_name']); $provider_name = nullable_htmlentities($row['ai_provider_name']);
$url = nullable_htmlentities($row['ai_provider_api_url']); $url = nullable_htmlentities($row['ai_provider_api_url']);
@@ -67,7 +67,8 @@ $num_rows = mysqli_num_rows($sql);
</td> </td>
<td><?php echo $url; ?></td> <td><?php echo $url; ?></td>
<td><?php echo $key; ?></td> <td><?php echo $key; ?></td>
<td><?php echo $ai_model_count; ?></td> <td class="text-center">
<a class="badge badge-dark badge-pill p-2" href="ai_model.php"><?= $ai_model_count ?></a>
<td> <td>
<div class="dropdown dropleft text-center"> <div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown"> <button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
@@ -105,5 +106,4 @@ $num_rows = mysqli_num_rows($sql);
</div> </div>
<?php <?php
require_once "modals/ai/ai_provider_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -22,7 +22,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-key mr-2"></i>API Keys</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-key mr-2"></i>API Keys</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addApiKeyModal"><i class="fas fa-plus mr-2"></i>Create</button> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/api/api_key_add.php"><i class="fas fa-plus mr-2"></i>New API Key</button>
</div> </div>
</div> </div>
@@ -49,7 +49,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="dropdown-menu"> <div class="dropdown-menu">
<button class="dropdown-item text-danger text-bold" <button class="dropdown-item text-danger text-bold"
type="submit" form="bulkActions" name="bulk_delete_api_keys"> type="submit" form="bulkActions" name="bulk_delete_api_keys">
<i class="fas fa-fw fa-trash mr-2"></i>Revoke <i class="fas fa-fw fa-trash mr-2"></i>Delete
</button> </button>
</div> </div>
</div> </div>
@@ -105,7 +105,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$api_key_id = intval($row['api_key_id']); $api_key_id = intval($row['api_key_id']);
$api_key_name = nullable_htmlentities($row['api_key_name']); $api_key_name = nullable_htmlentities($row['api_key_name']);
$api_key_secret = nullable_htmlentities("************" . substr($row['api_key_secret'], -4)); $api_key_secret = nullable_htmlentities("************" . substr($row['api_key_secret'], -4));
@@ -128,26 +128,27 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?php echo $api_key_id ?>"> <input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?php echo $api_key_id ?>">
</div> </div>
</td> </td>
<td class="text-bold"><?php echo $api_key_name; ?></td> <td class="text-bold"><?php echo $api_key_name; ?></td>
<td><?php echo $api_key_client; ?></td> <td><?php echo $api_key_client; ?></td>
<td><?php echo $api_key_secret; ?></td> <td><?php echo $api_key_secret; ?></td>
<td><?php echo $api_key_created_at; ?></td> <td><?php echo $api_key_created_at; ?></td>
<td><?php echo $api_key_expire; ?></td> <td><?php echo $api_key_expire; ?></td>
<td> <td>
<div class="dropdown dropleft text-center"> <div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown"> <button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i> <i class="fas fa-ellipsis-h"></i>
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_api_key=<?php echo $api_key_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"> <?php if ($api_key_expire > date("Y-m-d H:i:s")) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?revoke_api_key=<?php echo $api_key_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-times mr-2"></i>Revoke <i class="fas fa-fw fa-times mr-2"></i>Revoke
</a> </a>
<?php } ?>
<?php if ($api_key_expire < date("Y-m-d H:i:s")) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_api_key=<?php echo $api_key_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-times mr-2"></i>Delete
</a>
<?php } ?>
</div> </div>
</div> </div>
</td> </td>
@@ -162,15 +163,11 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</form> </form>
</div> </div>
<?php require_once "../includes/filter_footer.php"; <?php require_once "../includes/filter_footer.php"; ?>
?>
</div> </div>
</div> </div>
<script src="../js/bulk_actions.js"></script> <script src="../js/bulk_actions.js"></script>
<?php <?php
require_once "modals/api/api_key_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -66,7 +66,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_type FROM app_logs ORDER BY app_log_type ASC"); $sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_type FROM app_logs ORDER BY app_log_type ASC");
while ($row = mysqli_fetch_array($sql_types_filter)) { while ($row = mysqli_fetch_assoc($sql_types_filter)) {
$log_type = nullable_htmlentities($row['app_log_type']); $log_type = nullable_htmlentities($row['app_log_type']);
?> ?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option> <option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option>
@@ -85,7 +85,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
$sql_categories_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_category FROM app_logs ORDER BY app_log_category ASC"); $sql_categories_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_category FROM app_logs ORDER BY app_log_category ASC");
while ($row = mysqli_fetch_array($sql_categories_filter)) { while ($row = mysqli_fetch_assoc($sql_categories_filter)) {
$log_category = nullable_htmlentities($row['app_log_category']); $log_category = nullable_htmlentities($row['app_log_category']);
?> ?>
<option <?php if ($category_filter == $log_category) { echo "selected"; } ?>><?php echo $log_category; ?></option> <option <?php if ($category_filter == $log_category) { echo "selected"; } ?>><?php echo $log_category; ?></option>
@@ -97,34 +97,15 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div> </div>
</div> </div>
</div> </div>
<div class="collapse mt-3 <?php if (!empty($_GET['dtf']) || $_GET['canned_date'] !== "custom" ) { echo "show"; } ?>" id="advancedFilter"> <div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-3">
<div class="form-group"> <div class="form-group">
<label>Canned Date</label> <label>Date range</label>
<select onchange="this.form.submit()" class="form-control select2" name="canned_date"> <input type="text" id="dateFilter" class="form-control" autocomplete="off">
<option <?php if ($_GET['canned_date'] == "custom") { echo "selected"; } ?> value="">Custom</option> <input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<option <?php if ($_GET['canned_date'] == "today") { echo "selected"; } ?> value="today">Today</option> <input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<option <?php if ($_GET['canned_date'] == "yesterday") { echo "selected"; } ?> value="yesterday">Yesterday</option> <input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
<option <?php if ($_GET['canned_date'] == "thisweek") { echo "selected"; } ?> value="thisweek">This Week</option>
<option <?php if ($_GET['canned_date'] == "lastweek") { echo "selected"; } ?> value="lastweek">Last Week</option>
<option <?php if ($_GET['canned_date'] == "thismonth") { echo "selected"; } ?> value="thismonth">This Month</option>
<option <?php if ($_GET['canned_date'] == "lastmonth") { echo "selected"; } ?> value="lastmonth">Last Month</option>
<option <?php if ($_GET['canned_date'] == "thisyear") { echo "selected"; } ?> value="thisyear">This Year</option>
<option <?php if ($_GET['canned_date'] == "lastyear") { echo "selected"; } ?> value="lastyear">Last Year</option>
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label>Date From</label>
<input onchange="this.form.submit()" type="date" class="form-control" name="dtf" max="2999-12-31" value="<?php echo nullable_htmlentities($dtf); ?>">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label>Date To</label>
<input onchange="this.form.submit()" type="date" class="form-control" name="dtt" max="2999-12-31" value="<?php echo nullable_htmlentities($dtt); ?>">
</div> </div>
</div> </div>
</div> </div>
@@ -160,7 +141,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$log_id = intval($row['app_log_id']); $log_id = intval($row['app_log_id']);
$log_type = nullable_htmlentities($row['app_log_type']); $log_type = nullable_htmlentities($row['app_log_type']);
$log_category = nullable_htmlentities($row['app_log_category']); $log_category = nullable_htmlentities($row['app_log_category']);

View File

@@ -88,7 +88,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
$sql_clients_filter = mysqli_query($mysqli, "SELECT * FROM clients ORDER BY client_name ASC"); $sql_clients_filter = mysqli_query($mysqli, "SELECT * FROM clients ORDER BY client_name ASC");
while ($row = mysqli_fetch_array($sql_clients_filter)) { while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
$client_id = intval($row['client_id']); $client_id = intval($row['client_id']);
$client_name = nullable_htmlentities($row['client_name']); $client_name = nullable_htmlentities($row['client_name']);
?> ?>
@@ -108,7 +108,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
$sql_users_filter = mysqli_query($mysqli, "SELECT * FROM users ORDER BY user_name ASC"); $sql_users_filter = mysqli_query($mysqli, "SELECT * FROM users ORDER BY user_name ASC");
while ($row = mysqli_fetch_array($sql_users_filter)) { while ($row = mysqli_fetch_assoc($sql_users_filter)) {
$user_id = intval($row['user_id']); $user_id = intval($row['user_id']);
$user_name = nullable_htmlentities($row['user_name']); $user_name = nullable_htmlentities($row['user_name']);
?> ?>
@@ -128,7 +128,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT log_type FROM logs ORDER BY log_type ASC"); $sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT log_type FROM logs ORDER BY log_type ASC");
while ($row = mysqli_fetch_array($sql_types_filter)) { while ($row = mysqli_fetch_assoc($sql_types_filter)) {
$log_type = nullable_htmlentities($row['log_type']); $log_type = nullable_htmlentities($row['log_type']);
?> ?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option> <option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option>
@@ -147,7 +147,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
$sql_actions_filter = mysqli_query($mysqli, "SELECT DISTINCT log_action FROM logs ORDER BY log_action ASC"); $sql_actions_filter = mysqli_query($mysqli, "SELECT DISTINCT log_action FROM logs ORDER BY log_action ASC");
while ($row = mysqli_fetch_array($sql_actions_filter)) { while ($row = mysqli_fetch_assoc($sql_actions_filter)) {
$log_action = nullable_htmlentities($row['log_action']); $log_action = nullable_htmlentities($row['log_action']);
?> ?>
<option <?php if ($action_filter == $log_action) { echo "selected"; } ?>><?php echo $log_action; ?></option> <option <?php if ($action_filter == $log_action) { echo "selected"; } ?>><?php echo $log_action; ?></option>
@@ -159,34 +159,15 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div> </div>
</div> </div>
</div> </div>
<div class="collapse mt-3 <?php if (!empty($_GET['dtf']) || $_GET['canned_date'] !== "custom" ) { echo "show"; } ?>" id="advancedFilter"> <div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-3">
<div class="form-group"> <div class="form-group">
<label>Canned Date</label> <label>Date range</label>
<select onchange="this.form.submit()" class="form-control select2" name="canned_date"> <input type="text" id="dateFilter" class="form-control" autocomplete="off">
<option <?php if ($_GET['canned_date'] == "custom") { echo "selected"; } ?> value="">Custom</option> <input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<option <?php if ($_GET['canned_date'] == "today") { echo "selected"; } ?> value="today">Today</option> <input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<option <?php if ($_GET['canned_date'] == "yesterday") { echo "selected"; } ?> value="yesterday">Yesterday</option> <input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
<option <?php if ($_GET['canned_date'] == "thisweek") { echo "selected"; } ?> value="thisweek">This Week</option>
<option <?php if ($_GET['canned_date'] == "lastweek") { echo "selected"; } ?> value="lastweek">Last Week</option>
<option <?php if ($_GET['canned_date'] == "thismonth") { echo "selected"; } ?> value="thismonth">This Month</option>
<option <?php if ($_GET['canned_date'] == "lastmonth") { echo "selected"; } ?> value="lastmonth">Last Month</option>
<option <?php if ($_GET['canned_date'] == "thisyear") { echo "selected"; } ?> value="thisyear">This Year</option>
<option <?php if ($_GET['canned_date'] == "lastyear") { echo "selected"; } ?> value="lastyear">Last Year</option>
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label>Date From</label>
<input onchange="this.form.submit()" type="date" class="form-control" name="dtf" max="2999-12-31" value="<?php echo nullable_htmlentities($dtf); ?>">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label>Date To</label>
<input onchange="this.form.submit()" type="date" class="form-control" name="dtt" max="2999-12-31" value="<?php echo nullable_htmlentities($dtt); ?>">
</div> </div>
</div> </div>
</div> </div>
@@ -244,7 +225,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$log_id = intval($row['log_id']); $log_id = intval($row['log_id']);
$log_type = nullable_htmlentities($row['log_type']); $log_type = nullable_htmlentities($row['log_type']);
$log_action = nullable_htmlentities($row['log_action']); $log_action = nullable_htmlentities($row['log_action']);
@@ -299,4 +280,3 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -111,7 +111,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$category_id = intval($row['category_id']); $category_id = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']); $category_name = nullable_htmlentities($row['category_name']);
$category_color = nullable_htmlentities($row['category_color']); $category_color = nullable_htmlentities($row['category_color']);
@@ -135,11 +135,11 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
if ($archived) { if ($archived) {
?> ?>
<a class="dropdown-item text-success confirm-link" <a class="dropdown-item text-success confirm-link"
href="post.php?unarchive_category=<?php echo $category_id; ?>"> href="post.php?restore_category=<?php echo $category_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Unarchive <i class="fas fa-fw fa-archive mr-2"></i>Restore
</a> </a>
<a class="dropdown-item text-danger confirm-link" <a class="dropdown-item text-danger confirm-link"
href="post.php?delete_category=<?php echo $category_id; ?>"> href="post.php?delete_category=<?php echo $category_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete <i class="fas fa-fw fa-trash mr-2"></i>Delete
</a> </a>
<?php <?php
@@ -150,7 +150,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<i class="fas fa-fw fa-edit mr-2"></i>Edit <i class="fas fa-fw fa-edit mr-2"></i>Edit
</a> </a>
<a class="dropdown-item text-danger confirm-link" <a class="dropdown-item text-danger confirm-link"
href="post.php?archive_category=<?php echo $category_id; ?>"> href="post.php?archive_category=<?php echo $category_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Archive <i class="fas fa-fw fa-archive mr-2"></i>Archive
</a> </a>
<?php <?php

125
admin/contract_template.php Normal file
View File

@@ -0,0 +1,125 @@
<?php
// Default Column Sort by Filter
$sort = "contract_template_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
// Search query
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM contract_templates
WHERE contract_template_name LIKE '%$q%' OR contract_template_type LIKE '%$q%' OR contract_template_name LIKE '%$q%'
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file-contract mr-2"></i>Contract Templates</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/contract_template/contract_template_add.php" data-modal-size="lg">
<i class="fas fa-plus mr-2"></i>New Template
</button>
</div>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search templates">
<div class="input-group-append">
<button class="btn btn-secondary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>Template Name</th>
<th>Type</th>
<th>Update Frequency</th>
<th>SLA (L/M/H Response)</th>
<th>SLA (L/M/H Resolution)</th>
<th>Hourly Rate</th>
<th>After Hours Rate</th>
<th>Support Hours</th>
<th>Net Terms</th>
<th>Created</th>
<th>Updated</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$id = intval($row['contract_template_id']);
$name = nullable_htmlentities($row['contract_template_name']);
$type = nullable_htmlentities($row['contract_template_type']);
$freq = nullable_htmlentities($row['contract_template_update_frequency']);
$sla_low_resp = nullable_htmlentities($row['sla_low_response_time']);
$sla_med_resp = nullable_htmlentities($row['sla_medium_response_time']);
$sla_high_resp = nullable_htmlentities($row['sla_high_response_time']);
$sla_low_res = nullable_htmlentities($row['sla_low_resolution_time']);
$sla_med_res = nullable_htmlentities($row['sla_medium_resolution_time']);
$sla_high_res = nullable_htmlentities($row['sla_high_resolution_time']);
$hourly_rate = nullable_htmlentities($row['contract_template_hourly_rate']);
$after_hours = nullable_htmlentities($row['contract_template_after_hours_hourly_rate']);
$support_hours = nullable_htmlentities($row['contract_template_support_hours']);
$net_terms = nullable_htmlentities($row['contract_template_net_terms']);
$created = nullable_htmlentities($row['contract_template_created_at']);
$updated = nullable_htmlentities($row['contract_template_updated_at']);
?>
<tr>
<td>
<a class="text-bold" href="contract_template_details.php?contract_template_id=<?php echo $id; ?>">
<i class="fas fa-fw fa-file-alt text-dark"></i> <?php echo $name; ?>
</a>
<div class="mt-1 text-secondary"><?php echo nullable_htmlentities($row['contract_template_description']); ?></div>
</td>
<td><?php echo $type; ?></td>
<td><?php echo $freq; ?></td>
<td><?php echo "$sla_low_resp / $sla_med_resp / $sla_high_resp"; ?></td>
<td><?php echo "$sla_low_res / $sla_med_res / $sla_high_res"; ?></td>
<td><?php echo $hourly_rate; ?></td>
<td><?php echo $after_hours; ?></td>
<td><?php echo $support_hours; ?></td>
<td><?php echo $net_terms; ?></td>
<td><?php echo $created; ?></td>
<td><?php echo $updated; ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-size="xl"
data-modal-url="modals/contract_template/contract_template_edit.php?id=<?= $id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold" href="post.php?delete_contract_template=<?php echo $id; ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<br>
</div>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>
<?php require_once "../includes/footer.php"; ?>

View File

@@ -21,7 +21,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-external-link-alt mr-2"></i>Custom Links</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-external-link-alt mr-2"></i>Custom Links</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addLinkModal"><i class="fas fa-plus mr-2"></i>New Link</button> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/custom_link/custom_link_add.php"><i class="fas fa-plus mr-2"></i>New Link</button>
</div> </div>
</div> </div>
@@ -72,7 +72,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$custom_link_id = intval($row['custom_link_id']); $custom_link_id = intval($row['custom_link_id']);
$custom_link_name = nullable_htmlentities($row['custom_link_name']); $custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = nullable_htmlentities($row['custom_link_uri']); $custom_link_uri = nullable_htmlentities($row['custom_link_uri']);
@@ -123,7 +123,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<i class="fas fa-fw fa-edit mr-2"></i>Edit <i class="fas fa-fw fa-edit mr-2"></i>Edit
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_custom_link=<?php echo $custom_link_id; ?>"> <a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_custom_link=<?php echo $custom_link_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete <i class="fas fa-fw fa-trash mr-2"></i>Delete
</a> </a>
</div> </div>
@@ -145,5 +145,4 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div> </div>
<?php <?php
require_once "modals/custom_link/custom_link_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -791,7 +791,7 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
// Copy primary_location and primary_contact to their new vars in their own respecting tables // Copy primary_location and primary_contact to their new vars in their own respecting tables
$sql = mysqli_query($mysqli, "SELECT * FROM clients"); $sql = mysqli_query($mysqli, "SELECT * FROM clients");
while($row = mysqli_fetch_array($sql)) { while($row = mysqli_fetch_assoc($sql)) {
$primary_contact = $row['primary_contact']; $primary_contact = $row['primary_contact'];
$primary_location = $row['primary_location']; $primary_location = $row['primary_location'];
@@ -1666,7 +1666,7 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
if (CURRENT_DATABASE_VERSION == '1.3.9') { if (CURRENT_DATABASE_VERSION == '1.3.9') {
// Migrate all Network Info from Assets to Interface Table and make it primary interface // Migrate all Network Info from Assets to Interface Table and make it primary interface
$sql = mysqli_query($mysqli, "SELECT * FROM assets"); $sql = mysqli_query($mysqli, "SELECT * FROM assets");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$asset_id = intval($row['asset_id']); $asset_id = intval($row['asset_id']);
$mac = sanitizeInput($row['asset_mac']); $mac = sanitizeInput($row['asset_mac']);
$ip = sanitizeInput($row['asset_ip']); $ip = sanitizeInput($row['asset_ip']);
@@ -1945,7 +1945,7 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
if (CURRENT_DATABASE_VERSION == '1.5.7') { if (CURRENT_DATABASE_VERSION == '1.5.7') {
// Create Users for contacts that have logins enabled and that are not archived // Create Users for contacts that have logins enabled and that are not archived
$contacts_sql = mysqli_query($mysqli, "SELECT * FROM `contacts` WHERE contact_archived_at IS NULL AND (contact_auth_method = 'local' OR contact_auth_method = 'azure')"); $contacts_sql = mysqli_query($mysqli, "SELECT * FROM `contacts` WHERE contact_archived_at IS NULL AND (contact_auth_method = 'local' OR contact_auth_method = 'azure')");
while($row = mysqli_fetch_array($contacts_sql)) { while($row = mysqli_fetch_assoc($contacts_sql)) {
$contact_id = intval($row['contact_id']); $contact_id = intval($row['contact_id']);
$contact_name = mysqli_real_escape_string($mysqli, $row['contact_name']); $contact_name = mysqli_real_escape_string($mysqli, $row['contact_name']);
$contact_email = mysqli_real_escape_string($mysqli, $row['contact_email']); $contact_email = mysqli_real_escape_string($mysqli, $row['contact_email']);
@@ -3853,7 +3853,7 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
// Get Current Stripe Settings // Get Current Stripe Settings
$sql_stripe_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1"); $sql_stripe_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1");
$row = mysqli_fetch_array($sql_stripe_settings); $row = mysqli_fetch_assoc($sql_stripe_settings);
$config_stripe_enable = intval($row['config_stripe_enable']); $config_stripe_enable = intval($row['config_stripe_enable']);
if ($config_stripe_enable === 1) { if ($config_stripe_enable === 1) {
$config_stripe_publishable = mysqli_real_escape_string($mysqli, $row['config_stripe_publishable']); $config_stripe_publishable = mysqli_real_escape_string($mysqli, $row['config_stripe_publishable']);
@@ -3879,7 +3879,7 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
// Migrate Clients and Payment Method over // Migrate Clients and Payment Method over
$sql_stripe_clients = mysqli_query($mysqli, "SELECT * FROM client_stripe WHERE stripe_pm IS NOT NULL AND stripe_pm != ''"); $sql_stripe_clients = mysqli_query($mysqli, "SELECT * FROM client_stripe WHERE stripe_pm IS NOT NULL AND stripe_pm != ''");
while ($row = mysqli_fetch_array($sql_stripe_clients)) { while ($row = mysqli_fetch_assoc($sql_stripe_clients)) {
$client_id = intval($row['client_id']); $client_id = intval($row['client_id']);
$stripe_id = mysqli_real_escape_string($mysqli, $row['stripe_id']); $stripe_id = mysqli_real_escape_string($mysqli, $row['stripe_id']);
$stripe_pm = mysqli_real_escape_string($mysqli, $row['stripe_pm']); $stripe_pm = mysqli_real_escape_string($mysqli, $row['stripe_pm']);
@@ -3932,7 +3932,7 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
// Migrate Payment Methods from Categories Table to new payment_methods table // Migrate Payment Methods from Categories Table to new payment_methods table
$sql_categories = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_type = 'Payment Method' AND category_name != 'Stripe' AND category_archived_at IS NULL"); $sql_categories = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_type = 'Payment Method' AND category_name != 'Stripe' AND category_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_categories)) { while ($row = mysqli_fetch_assoc($sql_categories)) {
$category_name = sanitizeInput($row['category_name']); $category_name = sanitizeInput($row['category_name']);
mysqli_query($mysqli,"INSERT INTO payment_methods SET payment_method_name = '$category_name'"); mysqli_query($mysqli,"INSERT INTO payment_methods SET payment_method_name = '$category_name'");
@@ -4027,10 +4027,318 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.5'"); mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.5'");
} }
// if (CURRENT_DATABASE_VERSION == '2.3.4') { if (CURRENT_DATABASE_VERSION == '2.3.5') {
// // Insert queries here required to update to DB version 2.3.4 mysqli_query($mysqli, "ALTER TABLE `settings` CHANGE `config_smtp_provider` `config_smtp_provider` VARCHAR(200) DEFAULT NULL");
mysqli_query($mysqli, "ALTER TABLE `settings` CHANGE `config_imap_provider` `config_imap_provider` VARCHAR(200) DEFAULT NULL");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.6'");
}
if (CURRENT_DATABASE_VERSION == '2.3.6') {
// Create New Contract Templates Table
mysqli_query($mysqli, "CREATE TABLE `contract_templates` (
`contract_template_id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`contract_template_name` VARCHAR(255) NOT NULL,
`contract_template_description` TEXT NULL DEFAULT NULL,
`contract_template_type` VARCHAR(50) NULL DEFAULT NULL,
`contract_template_sla_low_response_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_low_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_medium_response_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_medium_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_high_response_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_high_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_template_rate_standard` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_template_rate_after_hours` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_template_net_terms` VARCHAR(50) NULL DEFAULT NULL,
`contract_template_support_hours` VARCHAR(100) NULL DEFAULT NULL,
`contract_template_renewal_frequency` VARCHAR(50) NULL DEFAULT NULL,
`contract_template_details` TEXT NULL DEFAULT NULL,
`contract_template_created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`contract_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`contract_template_archived_at` DATETIME NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
// Create New Contracts Table
mysqli_query($mysqli, "CREATE TABLE `contracts` (
`contract_id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`contract_name` VARCHAR(255) NOT NULL,
`contract_status` VARCHAR(50) NOT NULL,
`contract_type` VARCHAR(50) NOT NULL,
`contract_sla_low_response_time` INT(11) NULL DEFAULT NULL,
`contract_sla_low_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_sla_medium_response_time` INT(11) NULL DEFAULT NULL,
`contract_sla_medium_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_sla_high_response_time` INT(11) NULL DEFAULT NULL,
`contract_sla_high_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_details` TEXT NULL DEFAULT NULL,
`contract_client_id` INT(11) NULL DEFAULT NULL,
`contract_client_name` VARCHAR(255) NULL DEFAULT NULL,
`contract_client_address` TEXT NULL DEFAULT NULL,
`contract_client_email` VARCHAR(255) NULL DEFAULT NULL,
`contract_client_phone` VARCHAR(100) NULL DEFAULT NULL,
`contract_contact_name` VARCHAR(255) NULL DEFAULT NULL,
`contract_contact_signature` TEXT NULL DEFAULT NULL,
`contract_contact_signature_date` DATETIME NULL DEFAULT NULL,
`contract_agent_name` VARCHAR(255) NULL DEFAULT NULL,
`contract_agent_signature` TEXT NULL DEFAULT NULL,
`contract_agent_signature_date` DATETIME NULL DEFAULT NULL,
`contract_rate_standard` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_rate_after_hours` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_net_terms` VARCHAR(50) NULL DEFAULT NULL,
`contract_support_hours` VARCHAR(100) NULL DEFAULT NULL,
`contract_start_date` DATE NULL DEFAULT NULL,
`contract_end_date` DATE NULL DEFAULT NULL,
`contract_renewal_frequency` VARCHAR(50) NULL DEFAULT NULL,
`contract_created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`contract_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`contract_archived_at` DATETIME NULL DEFAULT NULL,
FOREIGN KEY (`contract_client_id`) REFERENCES `clients`(`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.7'");
}
if (CURRENT_DATABASE_VERSION == '2.3.7') {
mysqli_query($mysqli, "
CREATE TABLE `asset_tags` (
`asset_tag_asset_id` INT(11) NOT NULL,
`asset_tag_tag_id` INT(11) NOT NULL,
PRIMARY KEY (`asset_tag_asset_id`, `asset_tag_tag_id`),
CONSTRAINT `fk_asset`
FOREIGN KEY (`asset_tag_asset_id`)
REFERENCES `assets`(`asset_id`)
ON DELETE CASCADE,
CONSTRAINT `fk_tag`
FOREIGN KEY (`asset_tag_tag_id`)
REFERENCES `tags`(`tag_id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.8'");
}
if (CURRENT_DATABASE_VERSION == '2.3.8') {
mysqli_query($mysqli, "
CREATE TABLE `task_approvals` (
`approval_id` int(11) NOT NULL AUTO_INCREMENT,
`approval_scope` enum('client','internal') NOT NULL,
`approval_type` enum('any','technical','billing','specific') NOT NULL,
`approval_required_user_id` int(11) DEFAULT NULL,
`approval_status` enum('pending','approved','declined') NOT NULL,
`approval_created_by` int(11) NOT NULL,
`approval_approved_by` varchar(255) DEFAULT NULL,
`approval_url_key` varchar(200) NOT NULL,
`approval_task_id` int(11) NOT NULL,
PRIMARY KEY (`approval_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.9'");
}
if (CURRENT_DATABASE_VERSION == '2.3.9') {
mysqli_query($mysqli, "ALTER TABLE `clients` ADD `client_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `client_notes`");
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `location_notes`");
mysqli_query($mysqli, "ALTER TABLE `vendors` ADD `vendor_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `vendor_notes`");
mysqli_query($mysqli, "ALTER TABLE `software` ADD `software_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `software_notes`");
mysqli_query(
$mysqli,
"ALTER TABLE `credentials`
CHANGE `credential_important` `credential_favorite`
TINYINT(1) NOT NULL DEFAULT 0
AFTER `credential_note`"
);
mysqli_query($mysqli, "ALTER TABLE `assets` DROP `asset_important`");
mysqli_query($mysqli, "ALTER TABLE `assets` ADD `asset_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `asset_notes`");
mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_important`");
mysqli_query($mysqli, "ALTER TABLE `documents` ADD `document_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `document_client_visible`");
mysqli_query($mysqli, "ALTER TABLE `racks` ADD `rack_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `rack_notes`");
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_important`");
mysqli_query($mysqli, "ALTER TABLE `files` ADD `file_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `file_mime_type`");
mysqli_query($mysqli, "ALTER TABLE `networks` ADD `network_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `network_notes`");
mysqli_query($mysqli, "ALTER TABLE `domains` ADD `domain_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `domain_notes`");
mysqli_query($mysqli, "ALTER TABLE `certificates` ADD `certificate_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `certificate_notes`");
mysqli_query($mysqli, "ALTER TABLE `services` ADD `service_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `service_notes`");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.0'");
}
if (CURRENT_DATABASE_VERSION == '2.4.0') {
mysqli_query($mysqli, "
CREATE TABLE `quote_items` (
`item_id` int(11) NOT NULL AUTO_INCREMENT,
`item_name` varchar(200) NOT NULL,
`item_description` text DEFAULT NULL,
`item_quantity` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_price` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_subtotal` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_tax` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_total` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_order` int(11) NOT NULL DEFAULT 0,
`item_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`item_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
`item_archived_at` datetime DEFAULT NULL,
`item_tax_id` int(11) NOT NULL DEFAULT 0,
`item_product_id` int(11) NOT NULL DEFAULT 0,
`item_quote_id` int(11) NOT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
mysqli_query($mysqli, "
CREATE TABLE `recurring_invoice_items` (
`item_id` int(11) NOT NULL AUTO_INCREMENT,
`item_name` varchar(200) NOT NULL,
`item_description` text DEFAULT NULL,
`item_quantity` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_price` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_subtotal` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_tax` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_total` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_order` int(11) NOT NULL DEFAULT 0,
`item_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`item_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
`item_archived_at` datetime DEFAULT NULL,
`item_tax_id` int(11) NOT NULL DEFAULT 0,
`item_product_id` int(11) NOT NULL DEFAULT 0,
`item_recurring_invoice_id` int(11) NOT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.1'");
}
if (CURRENT_DATABASE_VERSION == '2.4.1') {
// Migrate Items
mysqli_query($mysqli, "
INSERT INTO `recurring_invoice_items` (
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_recurring_invoice_id`
)
SELECT
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_recurring_invoice_id`
FROM `invoice_items`
WHERE `item_recurring_invoice_id` != 0
");
mysqli_query($mysqli, "
INSERT INTO `quote_items` (
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_quote_id`
)
SELECT
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_quote_id`
FROM `invoice_items`
WHERE `item_quote_id` != 0
");
mysqli_query($mysqli, "
DELETE FROM `invoice_items`
WHERE `item_recurring_invoice_id` != 0
");
mysqli_query($mysqli, "
DELETE FROM `invoice_items`
WHERE `item_quote_id` != 0
");
mysqli_query($mysqli, "
ALTER TABLE `invoice_items`
DROP COLUMN `item_quote_id`,
DROP COLUMN `item_recurring_invoice_id`
");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.2'");
}
//
// // if (CURRENT_DATABASE_VERSION == '2.4.2') {
// // Insert queries here required to update to DB version 2.4.3
// // Then, update the database to the next sequential version // // Then, update the database to the next sequential version
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.5'"); // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.3'");
// } // }
} else { } else {

View File

@@ -46,14 +46,13 @@ $systemInfo[] = [
// Section: PHP Extensions // Section: PHP Extensions
$phpExtensions = []; $phpExtensions = [];
$extensions = [ $extensions = [
'php-mailparse' => 'mailparse',
'php-imap' => 'imap',
'php-mysqli' => 'mysqli', 'php-mysqli' => 'mysqli',
'php-intl' => 'intl', 'php-intl' => 'intl',
'php-curl' => 'curl', 'php-curl' => 'curl',
'php-mbstring' => 'mbstring', 'php-mbstring' => 'mbstring',
'php-gd' => 'gd', 'php-gd' => 'gd',
'php-zip' => 'zip', 'php-zip' => 'zip',
'php-xml' => 'xml',
]; ];
foreach ($extensions as $name => $ext) { foreach ($extensions as $name => $ext) {
@@ -767,4 +766,3 @@ $mysqli->close();
<?php <?php
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -20,9 +20,9 @@
<div class="card card-dark"> <div class="card card-dark">
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file mr-2"></i>Document Templates</h3> <h3 class="card-title mt-2"><i class="fa fa-fw fa-file-alt mr-2"></i>Document Templates</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addDocumentTemplateModal"> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/document_template/document_template_add.php" data-modal-size="xl">
<i class="fas fa-plus mr-2"></i>New Template <i class="fas fa-plus mr-2"></i>New Template
</button> </button>
</div> </div>
@@ -66,21 +66,30 @@
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$document_template_id = intval($row['document_template_id']); $document_template_id = intval($row['document_template_id']);
$document_template_name = nullable_htmlentities($row['document_template_name']); $document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']); $document_template_description = nullable_htmlentities($row['document_template_description']);
$document_template_content = nullable_htmlentities($row['document_template_content']); $document_template_content = nullable_htmlentities($row['document_template_content']);
$document_template_created_by_name = nullable_htmlentities($row['user_name']); $document_template_created_by_name = nullable_htmlentities($row['user_name']);
$document_template_created_at = nullable_htmlentities($row['document_template_created_at']); $document_template_created_at = nullable_htmlentities($row['document_template_created_at']);
$document_template_updated_at = nullable_htmlentities($row['document_template_updated_at']); $document_template_updated_at = nullable_htmlentities(getFallback($row['document_template_updated_at']));
?> ?>
<tr> <tr>
<td> <td>
<a class="text-bold" href="document_template_details.php?document_template_id=<?php echo $document_template_id; ?>"><i class="fas fa-fw fa-file-alt text-dark"></i> <?php echo $document_template_name; ?></a> <a class="text-dark ajax-modal" href="#"
<div class="mt-1 text-secondary"><?php echo $document_template_description; ?></div> data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<div class="media">
<i class="fas fa-fw fa-2x fa-file-alt mr-2"></i>
<div class="media-body">
<div><?= $document_template_name ?></div>
<div><small class="text-secondary"><?= $document_template_description ?></small></div>
</div>
</div>
</a>
</td> </td>
<td> <td>
<?php echo $document_template_created_at; ?> <?php echo $document_template_created_at; ?>
@@ -93,13 +102,17 @@
<i class="fas fa-ellipsis-h"></i> <i class="fas fa-ellipsis-h"></i>
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a class="dropdown-item" href="document_template_details.php?document_template_id=<?= $document_template_id ?>">
<i class="fas fa-fw fa-eye mr-2"></i>View
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#" <a class="dropdown-item ajax-modal" href="#"
data-modal-size="xl" data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>"> data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit <i class="fas fa-fw fa-edit mr-2"></i>Edit
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold" href="post.php?delete_document_template=<?php echo $document_template_id; ?>"> <a class="dropdown-item text-danger text-bold" href="post.php?delete_document_template=<?php echo $document_template_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete <i class="fas fa-fw fa-trash mr-2"></i>Delete
</a> </a>
</div> </div>
@@ -121,38 +134,4 @@
</div> </div>
</div> </div>
<?php require_once "modals/document_template/document_template_add.php"; ?> <?php require_once "../includes/footer.php";
<?php require_once "../includes/footer.php"; ?>
<script>
$(document).ready(function(){
$('#generateAIContent').on('click', function(){
var prompt = $('#aiPrompt').val().trim();
if(prompt === '') {
alert('Please enter a prompt.');
return;
}
$('#generateAIContent').prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Generating...');
$.ajax({
url: 'post.php?ai_create_document_template', // The PHP script that calls the OpenAI API
method: 'POST',
data: { prompt: prompt },
dataType: 'html',
success: function(response) {
// Assuming you have exactly one TinyMCE instance on the page
// and it's targeting the .tinymce textarea:
tinymce.activeEditor.setContent(response);
},
error: function() {
alert('Error generating content. Please try again.');
},
complete: function() {
$('#generateAIContent').prop('disabled', false).html('<i class="fa fa-fw fa-magic mr-1"></i>Generate with AI');
}
});
});
});
</script>

View File

@@ -15,9 +15,15 @@ if (isset($_GET['document_template_id'])) {
$document_template_id = intval($_GET['document_template_id']); $document_template_id = intval($_GET['document_template_id']);
} }
$sql_document = mysqli_query($mysqli, "SELECT * FROM document_templates WHERE document_template_id = $document_template_id"); $sql_document = mysqli_query($mysqli, "SELECT * FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1");
$row = mysqli_fetch_array($sql_document); if (mysqli_num_rows($sql_document) == 0) {
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
require_once "../includes/footer.php";
exit();
}
$row = mysqli_fetch_assoc($sql_document);
$document_template_name = nullable_htmlentities($row['document_template_name']); $document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']); $document_template_description = nullable_htmlentities($row['document_template_description']);
@@ -37,19 +43,19 @@ $document_template_updated_at = nullable_htmlentities($row['document_template_up
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a href="document_template.php">Document Templates</a> <a href="document_template.php">Document Templates</a>
</li> </li>
<li class="breadcrumb-item active"><i class="fas fa-file mr-2"></i><?php echo $document_template_name; ?></li> <li class="breadcrumb-item active"><i class="fas fa-file-alt mr-2"></i><?php echo $document_template_name; ?></li>
</ol> </ol>
<div class="card card-dark"> <div class="card card-dark">
<div class="card-header py-2"> <div class="card-header">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file mr-2"></i><?php echo $document_template_name; ?></h3> <h3 class="card-title mt-1"><i class="fa fa-fw fa-file-alt mr-2"></i><?php echo $document_template_name; ?></h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" <button type="button" class="btn btn-tool ajax-modal"
data-modal-size="xl" data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>"> data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<i class="fas fa-edit mr-2"></i>Edit <i class="fas fa-edit mr-2"></i>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,16 +1,16 @@
<?php <?php
require_once "../config.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/config.php';
require_once "../functions.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/functions.php';
require_once "../includes/check_login.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/check_login.php';
require_once "../includes/page_title.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/page_title.php';
if (!isset($session_is_admin) || !$session_is_admin) { if (!isset($session_is_admin) || !$session_is_admin) {
exit(WORDING_ROLECHECK_FAILED . "<br>Tell your admin: Your role does not have admin access."); exit(WORDING_ROLECHECK_FAILED . "<br>Tell your admin: Your role does not have admin access.");
} }
require_once "../includes/header.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php';
require_once "../includes/top_nav.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/top_nav.php';
require_once "includes/side_nav.php"; require_once 'includes/side_nav.php';
require_once "../includes/inc_wrapper.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/inc_wrapper.php';
require_once "../includes/inc_alert_feedback.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/inc_alert_feedback.php';
require_once "../includes/filter_header.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/filter_header.php';
require_once "../includes/app_version.php"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/app_version.php';

View File

@@ -1,6 +1,6 @@
<!-- Main Sidebar Container --> <!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-dark-<?php echo nullable_htmlentities($config_theme); ?> d-print-none"> <aside class="main-sidebar sidebar-dark-<?php echo nullable_htmlentities($config_theme); ?> d-print-none">
<a class="brand-link pb-1 mt-1" href="../agent/<?php echo $config_start_page ?>"> <a class="brand-link pb-1 mt-1" href="/agent/<?php echo $config_start_page ?>">
<p class="h6"> <p class="h6">
<i class="nav-icon fas fa-arrow-left ml-3 mr-2"></i> <i class="nav-icon fas fa-arrow-left ml-3 mr-2"></i>
<span class="brand-text"> <span class="brand-text">
@@ -14,21 +14,29 @@
<!-- Sidebar Menu --> <!-- Sidebar Menu -->
<nav> <nav>
<ul class="nav nav-pills nav-sidebar flex-column mt-2" data-widget="treeview" data-accordion="false"> <ul class="nav nav-pills nav-sidebar flex-column mt-2" data-widget="treeview" data-accordion="false">
<!-- ACCESS Section --> <li class="nav-header">ACCESS</li>
<li class="nav-item"> <li class="nav-item">
<a href="users.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "users.php") {echo "active";} ?>"> <a href="/admin/users.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "users.php") {echo "active";} ?>">
<i class="nav-icon fas fa-users"></i> <i class="nav-icon fas fa-users"></i>
<p>Users</p> <p>Users</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="roles.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "roles.php") {echo "active";} ?>"> <a href="/admin/roles.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "roles.php") {echo "active";} ?>">
<i class="nav-icon fas fa-user-shield"></i> <i class="nav-icon fas fa-user-shield"></i>
<p>Roles</p> <p>Roles</p>
</a> </a>
</li> </li>
<!-- 2025-12-05 JQ - Hide Permission Modules currently just shows modules
<li class="nav-item"> <li class="nav-item">
<a href="api_keys.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "api_keys.php") {echo "active";} ?>"> <a href="/admin/modules.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "modules.php") {echo "active";} ?>">
<i class="nav-icon fas fa-puzzle-piece"></i>
<p>Modules</p>
</a>
</li>
-->
<li class="nav-item">
<a href="/admin/api_keys.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "api_keys.php") {echo "active";} ?>">
<i class="nav-icon fas fa-key"></i> <i class="nav-icon fas fa-key"></i>
<p>API Keys</p> <p>API Keys</p>
</a> </a>
@@ -36,66 +44,56 @@
<li class="nav-header">TAGS & CATEGORIES</li> <li class="nav-header">TAGS & CATEGORIES</li>
<li class="nav-item"> <li class="nav-item">
<a href="tag.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'tag.php' ? 'active' : ''); ?>"> <a href="/admin/tag.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'tag.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-tags"></i> <i class="nav-icon fas fa-tags"></i>
<p>Tags</p> <p>Tags</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="category.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'category.php' ? 'active' : ''); ?>"> <a href="/admin/category.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'category.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-list-ul"></i> <i class="nav-icon fas fa-list-ul"></i>
<p>Categories</p> <p>Categories</p>
</a> </a>
</li> </li>
<?php if ($config_module_enable_accounting) { ?> <?php if ($config_module_enable_accounting) { ?>
<li class="nav-item"> <li class="nav-item">
<a href="tax.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'tax.php' ? 'active' : ''); ?>"> <a href="/admin/tax.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'tax.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-balance-scale"></i> <i class="nav-icon fas fa-balance-scale"></i>
<p>Taxes</p> <p>Taxes</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="payment_method.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'payment_method.php' ? 'active' : ''); ?>"> <a href="/admin/payment_method.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'payment_method.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-hand-holding-usd"></i> <i class="nav-icon fas fa-hand-holding-usd"></i>
<p>Payment Methods</p> <p>Payment Methods</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="payment_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'payment_provider.php' ? 'active' : ''); ?>"> <a href="/admin/payment_provider.php"
class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['payment_provider.php', 'saved_payment_method.php']) ? 'active' : ''); ?>">
<i class="nav-icon far fa-credit-card"></i> <i class="nav-icon far fa-credit-card"></i>
<p>Payment Providers</p> <p>Payment Providers</p>
</a> </a>
</li> </li>
<li class="nav-item">
<a href="saved_payment_method.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'saved_payment_method.php' ? 'active' : ''); ?>">
<i class="nav-icon far fa-credit-card"></i>
<p>Saved Payments</p>
</a>
</li>
<?php } ?> <?php } ?>
<li class="nav-item"> <li class="nav-item">
<a href="ai_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ai_provider.php' ? 'active' : ''); ?>"> <a href="/admin/ai_provider.php"
class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['ai_provider.php', 'ai_model.php']) ? 'active' : ''); ?>">
<i class="nav-icon fas fa-robot"></i> <i class="nav-icon fas fa-robot"></i>
<p>AI Providers</p> <p>AI Providers</p>
</a> </a>
</li> </li>
<li class="nav-item">
<a href="ai_model.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ai_model.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-robot"></i>
<p>AI Models</p>
</a>
</li>
<?php if ($config_module_enable_ticketing) { ?> <?php if ($config_module_enable_ticketing) { ?>
<li class="nav-item"> <li class="nav-item">
<a href="ticket_status.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ticket_status.php' ? 'active' : ''); ?>"> <a href="/admin/ticket_status.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ticket_status.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-info-circle"></i> <i class="nav-icon fas fa-info-circle"></i>
<p>Ticket Statuses</p> <p>Ticket Statuses</p>
</a> </a>
</li> </li>
<?php } ?> <?php } ?>
<li class="nav-item"> <li class="nav-item">
<a href="custom_link.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'custom_link.php' ? 'active' : ''); ?>"> <a href="/admin/custom_link.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'custom_link.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-external-link-alt"></i> <i class="nav-icon fas fa-external-link-alt"></i>
<p>Custom Links</p> <p>Custom Links</p>
</a> </a>
@@ -104,33 +102,44 @@
<?php if ($config_module_enable_itdoc) { ?> <?php if ($config_module_enable_itdoc) { ?>
<li class="nav-header">TEMPLATES</li> <li class="nav-header">TEMPLATES</li>
<!-- 2025-11-16 JQ - Hide Contracts not yet ready
<li class="nav-item"> <li class="nav-item">
<a href="project_template.php" class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['project_template.php', 'project_template_details.php']) ? 'active' : ''); ?>"> <a href="/admin/contract_template.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'contract_template.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-file-contract"></i>
<p>
<span href="#" class="fas fa-plus-circle right ajax-modal" data-modal-url="/admin/modals/contract_template/contract_template_add.php" data-modal-size="lg"></span>
Contract Templates
</p>
</a>
</li>
-->
<li class="nav-item">
<a href="/admin/project_template.php" class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['project_template.php', 'project_template_details.php']) ? 'active' : ''); ?>">
<i class="nav-icon fas fa-project-diagram"></i> <i class="nav-icon fas fa-project-diagram"></i>
<p>Project Templates</p> <p>Project Templates</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="ticket_template.php" class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['ticket_template.php', 'ticket_template_details.php']) ? 'active' : ''); ?>"> <a href="/admin/ticket_template.php" class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['ticket_template.php', 'ticket_template_details.php']) ? 'active' : ''); ?>">
<i class="nav-icon fas fa-life-ring"></i> <i class="nav-icon fas fa-life-ring"></i>
<p>Ticket Templates</p> <p>Ticket Templates</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="vendor_template.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'vendor_template.php' ? 'active' : ''); ?>"> <a href="/admin/vendor_template.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'vendor_template.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-building"></i> <i class="nav-icon fas fa-building"></i>
<p>Vendor Templates</p> <p>Vendor Templates</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="software_template.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'software_template.php' ? 'active' : ''); ?>"> <a href="/admin/software_template.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'software_template.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-rocket"></i> <i class="nav-icon fas fa-rocket"></i>
<p>License Templates</p> <p>License Templates</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="document_template.php" class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['document_template.php', 'document_template_details.php']) ? 'active' : ''); ?>"> <a href="/admin/document_template.php" class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['document_template.php', 'document_template_details.php']) ? 'active' : ''); ?>">
<i class="nav-icon fas fa-file"></i> <i class="nav-icon fas fa-file-alt"></i>
<p>Document Templates</p> <p>Document Templates</p>
</a> </a>
</li> </li>
@@ -139,37 +148,37 @@
<li class="nav-header">MAINTENANCE</li> <li class="nav-header">MAINTENANCE</li>
<li class="nav-item"> <li class="nav-item">
<a href="mail_queue.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'mail_queue.php' ? 'active' : ''); ?>"> <a href="/admin/mail_queue.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'mail_queue.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-mail-bulk"></i> <i class="nav-icon fas fa-mail-bulk"></i>
<p>Mail Queue</p> <p>Mail Queue</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="audit_log.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'audit_log.php' ? 'active' : ''); ?>"> <a href="/admin/audit_log.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'audit_log.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-history"></i> <i class="nav-icon fas fa-history"></i>
<p>Audit Logs</p> <p>Audit Logs</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="app_log.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'app_log.php' ? 'active' : ''); ?>"> <a href="/admin/app_log.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'app_log.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-history"></i> <i class="nav-icon fas fa-history"></i>
<p>App Logs</p> <p>App Logs</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="backup.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'backup.php' ? 'active' : ''); ?>"> <a href="/admin/backup.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'backup.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-cloud-upload-alt"></i> <i class="nav-icon fas fa-cloud-upload-alt"></i>
<p>Backup</p> <p>Backup</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="debug.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'debug.php' ? 'active' : ''); ?>"> <a href="/admin/debug.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'debug.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-bug"></i> <i class="nav-icon fas fa-bug"></i>
<p>Debug</p> <p>Debug</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="update.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'update.php' ? 'active' : ''); ?>"> <a href="/admin/update.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'update.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-download"></i> <i class="nav-icon fas fa-download"></i>
<p>Update</p> <p>Update</p>
</a> </a>
@@ -185,56 +194,56 @@
</a> </a>
<ul class="nav nav-treeview"> <ul class="nav nav-treeview">
<li class="nav-item"> <li class="nav-item">
<a href="settings_company.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_company.php' ? 'active' : ''); ?>"> <a href="/admin/settings_company.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_company.php' ? 'active' : ''); ?>">
<i class="nav-icon fa fa-briefcase"></i> <i class="nav-icon fa fa-briefcase"></i>
<p>Company Details</p> <p>Company Details</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_localization.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_localization.php' ? 'active' : ''); ?>"> <a href="/admin/settings_localization.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_localization.php' ? 'active' : ''); ?>">
<i class="nav-icon fa fa-globe"></i> <i class="nav-icon fa fa-globe"></i>
<p>Localization</p> <p>Localization</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_theme.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_theme.php' ? 'active' : ''); ?>"> <a href="/admin/settings_theme.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_theme.php' ? 'active' : ''); ?>">
<i class="nav-icon fa fa-paint-brush"></i> <i class="nav-icon fa fa-paint-brush"></i>
<p>Theme</p> <p>Theme</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_security.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_security.php' ? 'active' : ''); ?>"> <a href="/admin/settings_security.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_security.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-shield-alt"></i> <i class="nav-icon fas fa-shield-alt"></i>
<p>Security</p> <p>Security</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_mail.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_mail.php' ? 'active' : ''); ?>"> <a href="/admin/settings_mail.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_mail.php' ? 'active' : ''); ?>">
<i class="nav-icon far fa-envelope"></i> <i class="nav-icon far fa-envelope"></i>
<p>Mail</p> <p>Mail</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_notification.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_notification.php' ? 'active' : ''); ?>"> <a href="/admin/settings_notification.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_notification.php' ? 'active' : ''); ?>">
<i class="nav-icon far fa-bell"></i> <i class="nav-icon far fa-bell"></i>
<p>Notifications</p> <p>Notifications</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_default.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_default.php' ? 'active' : ''); ?>"> <a href="/admin/settings_default.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_default.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-cogs"></i> <i class="nav-icon fas fa-cogs"></i>
<p>Defaults</p> <p>Defaults</p>
</a> </a>
</li> </li>
<?php if ($config_module_enable_accounting) { ?> <?php if ($config_module_enable_accounting) { ?>
<li class="nav-item"> <li class="nav-item">
<a href="settings_invoice.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_invoice.php' ? 'active' : ''); ?>"> <a href="/admin/settings_invoice.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_invoice.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-file-invoice"></i> <i class="nav-icon fas fa-file-invoice"></i>
<p>Invoice</p> <p>Invoice</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_quote.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_quote.php' ? 'active' : ''); ?>"> <a href="/admin/settings_quote.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_quote.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-comment-dollar"></i> <i class="nav-icon fas fa-comment-dollar"></i>
<p>Quote</p> <p>Quote</p>
</a> </a>
@@ -242,13 +251,13 @@
<?php } ?> <?php } ?>
<?php if ($config_module_enable_ticketing) { ?> <?php if ($config_module_enable_ticketing) { ?>
<li class="nav-item"> <li class="nav-item">
<a href="settings_project.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_project.php' ? 'active' : ''); ?>"> <a href="/admin/settings_project.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_project.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-project-diagram"></i> <i class="nav-icon fas fa-project-diagram"></i>
<p>Project</p> <p>Project</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_ticket.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_ticket.php' ? 'active' : ''); ?>"> <a href="/admin/settings_ticket.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_ticket.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-life-ring"></i> <i class="nav-icon fas fa-life-ring"></i>
<p>Ticket</p> <p>Ticket</p>
</a> </a>
@@ -257,20 +266,20 @@
<!-- Currently the only integration is the client portal SSO --> <!-- Currently the only integration is the client portal SSO -->
<?php if ($config_client_portal_enable) { ?> <?php if ($config_client_portal_enable) { ?>
<li class="nav-item"> <li class="nav-item">
<a href="identity_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'identity_provider.php' ? 'active' : ''); ?>"> <a href="/admin/identity_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'identity_provider.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-fingerprint"></i> <i class="nav-icon fas fa-fingerprint"></i>
<p>Identity Provider</p> <p>Identity Provider</p>
</a> </a>
</li> </li>
<?php } ?> <?php } ?>
<li class="nav-item"> <li class="nav-item">
<a href="settings_telemetry.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_telemetry.php' ? 'active' : ''); ?>"> <a href="/admin/settings_telemetry.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_telemetry.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-satellite-dish"></i> <i class="nav-icon fas fa-satellite-dish"></i>
<p>Telemetry</p> <p>Telemetry</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="settings_module.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_module.php' ? 'active' : ''); ?>"> <a href="/admin/settings_module.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'settings_module.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-cube"></i> <i class="nav-icon fas fa-cube"></i>
<p>Modules</p> <p>Modules</p>
</a> </a>
@@ -284,7 +293,7 @@
ORDER BY custom_link_order ASC, custom_link_name ASC" ORDER BY custom_link_order ASC, custom_link_name ASC"
); );
while ($row = mysqli_fetch_array($sql_custom_links)) { while ($row = mysqli_fetch_assoc($sql_custom_links)) {
$custom_link_name = nullable_htmlentities($row['custom_link_name']); $custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = sanitize_url($row['custom_link_uri']); $custom_link_uri = sanitize_url($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']); $custom_link_icon = nullable_htmlentities($row['custom_link_icon']);

View File

@@ -53,34 +53,15 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div> </div>
</div> </div>
</div> </div>
<div class="collapse mt-3 <?php if (!empty($_GET['dtf']) || $_GET['canned_date'] !== "custom" ) { echo "show"; } ?>" id="advancedFilter"> <div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-3">
<div class="form-group"> <div class="form-group">
<label>Canned Date</label> <label>Date range</label>
<select onchange="this.form.submit()" class="form-control select2" name="canned_date"> <input type="text" id="dateFilter" class="form-control" autocomplete="off">
<option <?php if ($_GET['canned_date'] == "custom") { echo "selected"; } ?> value="">Custom</option> <input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<option <?php if ($_GET['canned_date'] == "today") { echo "selected"; } ?> value="today">Today</option> <input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<option <?php if ($_GET['canned_date'] == "yesterday") { echo "selected"; } ?> value="yesterday">Yesterday</option> <input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
<option <?php if ($_GET['canned_date'] == "thisweek") { echo "selected"; } ?> value="thisweek">This Week</option>
<option <?php if ($_GET['canned_date'] == "lastweek") { echo "selected"; } ?> value="lastweek">Last Week</option>
<option <?php if ($_GET['canned_date'] == "thismonth") { echo "selected"; } ?> value="thismonth">This Month</option>
<option <?php if ($_GET['canned_date'] == "lastmonth") { echo "selected"; } ?> value="lastmonth">Last Month</option>
<option <?php if ($_GET['canned_date'] == "thisyear") { echo "selected"; } ?> value="thisyear">This Year</option>
<option <?php if ($_GET['canned_date'] == "lastyear") { echo "selected"; } ?> value="lastyear">Last Year</option>
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label>Date From</label>
<input onchange="this.form.submit()" type="date" class="form-control" name="dtf" max="2999-12-31" value="<?php echo nullable_htmlentities($dtf); ?>">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label>Date To</label>
<input onchange="this.form.submit()" type="date" class="form-control" name="dtt" max="2999-12-31" value="<?php echo nullable_htmlentities($dtt); ?>">
</div> </div>
</div> </div>
</div> </div>
@@ -135,7 +116,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$email_id = intval($row['email_id']); $email_id = intval($row['email_id']);
$email_from = nullable_htmlentities($row['email_from']); $email_from = nullable_htmlentities($row['email_from']);
$email_from_name = nullable_htmlentities($row['email_from_name']); $email_from_name = nullable_htmlentities($row['email_from_name']);
@@ -182,12 +163,12 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<!-- Show force resend if all retries have failed --> <!-- Show force resend if all retries have failed -->
<?php if ($email_status == 2 && $email_attempts > 3) { ?> <?php if ($email_status == 2 && $email_attempts > 3) { ?>
<a class="btn btn-sm btn-success" href="post.php?send_failed_mail=<?php echo $email_id; ?>"><i class="fas fa-fw fa-paper-plane"></i></a> <a class="btn btn-sm btn-success" href="post.php?send_failed_mail=<?php echo $email_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-paper-plane"></i></a>
<?php } ?> <?php } ?>
<!-- Allow cancelling a message if it hasn't yet been picked up (e.g. stuck/bugged) --> <!-- Allow cancelling a message if it hasn't yet been picked up (e.g. stuck/bugged) -->
<?php if ($email_status !== 3) { ?> <?php if ($email_status !== 3) { ?>
<a class="btn btn-sm btn-danger confirm-link" href="post.php?cancel_mail=<?php echo $email_id; ?>"><i class="fas fa-fw fa-trash"></i></a> <a class="btn btn-sm btn-danger confirm-link" href="post.php?cancel_mail=<?php echo $email_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-trash"></i></a>
<?php } ?> <?php } ?>
</td> </td>

View File

@@ -1,7 +1,11 @@
<div class="form-group"> <?php
<div class="modal" id="addAIModelModal" tabindex="-1">
<div class="modal-dialog"> require_once '../../../includes/modal_header.php';
<div class="modal-content">
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-robot mr-2"></i>Add AI Model</h5> <h5 class="modal-title"><i class="fa fa-fw fa-robot mr-2"></i>Add AI Model</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -23,7 +27,7 @@
<option value="">- Select an AI Provider -</option> <option value="">- Select an AI Provider -</option>
<?php <?php
$sql_ai_providers = mysqli_query($mysqli, "SELECT * FROM ai_providers"); $sql_ai_providers = mysqli_query($mysqli, "SELECT * FROM ai_providers");
while ($row = mysqli_fetch_array($sql_ai_providers)) { while ($row = mysqli_fetch_assoc($sql_ai_providers)) {
$ai_provider_id = intval($row['ai_provider_id']); $ai_provider_id = intval($row['ai_provider_id']);
$ai_provider_name = nullable_htmlentities($row['ai_provider_name']); $ai_provider_name = nullable_htmlentities($row['ai_provider_name']);
@@ -68,6 +72,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -6,7 +6,7 @@ $model_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM ai_models WHERE ai_model_id = $model_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM ai_models WHERE ai_model_id = $model_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$ai_model_ai_provider_id = intval($row['ai_model_ai_provider_id']); $ai_model_ai_provider_id = intval($row['ai_model_ai_provider_id']);
$model_id = intval($row['ai_model_id']); $model_id = intval($row['ai_model_id']);
$model_name = nullable_htmlentities($row['ai_model_name']); $model_name = nullable_htmlentities($row['ai_model_name']);
@@ -39,7 +39,7 @@ ob_start();
<option value="">- Select an AI Provider -</option> <option value="">- Select an AI Provider -</option>
<?php <?php
$sql_ai_providers = mysqli_query($mysqli, "SELECT * FROM ai_providers"); $sql_ai_providers = mysqli_query($mysqli, "SELECT * FROM ai_providers");
while ($row = mysqli_fetch_array($sql_ai_providers)) { while ($row = mysqli_fetch_assoc($sql_ai_providers)) {
$ai_provider_id = intval($row['ai_provider_id']); $ai_provider_id = intval($row['ai_provider_id']);
$ai_provider_name = nullable_htmlentities($row['ai_provider_name']); $ai_provider_name = nullable_htmlentities($row['ai_provider_name']);

View File

@@ -1,7 +1,11 @@
<div class="form-group"> <?php
<div class="modal" id="addAIProviderModal" tabindex="-1">
<div class="modal-dialog"> require_once '../../../includes/modal_header.php';
<div class="modal-content">
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-robot mr-2"></i>New AI Provider</h5> <h5 class="modal-title"><i class="fa fa-fw fa-robot mr-2"></i>New AI Provider</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -49,6 +53,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -6,7 +6,7 @@ $provider_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM ai_providers WHERE ai_provider_id = $provider_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM ai_providers WHERE ai_provider_id = $provider_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$provider_name = nullable_htmlentities($row['ai_provider_name']); $provider_name = nullable_htmlentities($row['ai_provider_name']);
$url = nullable_htmlentities($row['ai_provider_api_url']); $url = nullable_htmlentities($row['ai_provider_api_url']);
$key = nullable_htmlentities($row['ai_provider_api_key']); $key = nullable_htmlentities($row['ai_provider_api_key']);

View File

@@ -1,10 +1,13 @@
<?php <?php
$key = randomString(156);
$decryptPW = randomString(160); require_once '../../../includes/modal_header.php';
$key = randomString(32);
$decryptPW = randomString(32);
ob_start();
?> ?>
<div class="modal" id="addApiKeyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-key mr-2"></i>New Key</h5> <h5 class="modal-title"><i class="fas fa-fw fa-key mr-2"></i>New Key</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -61,7 +64,7 @@ $decryptPW = randomString(160);
<option value="0"> ALL CLIENTS </option> <option value="0"> ALL CLIENTS </option>
<?php <?php
$sql = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC"); $sql = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$client_id = intval($row['client_id']); $client_id = intval($row['client_id']);
$client_name = nullable_htmlentities($row['client_name']); ?> $client_name = nullable_htmlentities($row['client_name']); ?>
<option value="<?php echo $client_id; ?>"><?php echo "$client_name (Client ID: $client_id)"; ?></option> <option value="<?php echo $client_id; ?>"><?php echo "$client_name (Client ID: $client_id)"; ?></option>
@@ -116,6 +119,6 @@ $decryptPW = randomString(160);
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -2,7 +2,9 @@
require_once '../../../includes/modal_header.php'; require_once '../../../includes/modal_header.php';
$category = nullable_htmlentities($_GET['category']); $category = nullable_htmlentities($_GET['category'] ?? '');
$category_types_array = ['Expense', 'Income', 'Referral', 'Ticket'];
?> ?>
@@ -13,10 +15,30 @@ $category = nullable_htmlentities($_GET['category']);
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="type" value="<?php echo ($category); ?>">
<div class="modal-body"> <div class="modal-body">
<?php if ($category) { ?>
<input type="hidden" name="type" value="<?= $category ?>">
<?php } else { ?>
<div class="form-group">
<label>Type <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Select Type -</option>
<?php foreach ($category_types_array as $type_select) { ?>
<option><?= $type_select ?></option>
<?php } ?>
</select>
</div>
</div>
<?php } ?>
<div class="form-group"> <div class="form-group">
<label>Name <strong class="text-danger">*</strong></label> <label>Name <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
@@ -39,7 +61,7 @@ $category = nullable_htmlentities($_GET['category']);
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="add_category" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</button> <button type="submit" name="add_category" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create Category</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>

View File

@@ -6,7 +6,7 @@ $category_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_id = $category_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_id = $category_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$category_name = nullable_htmlentities($row['category_name']); $category_name = nullable_htmlentities($row['category_name']);
$category_color = nullable_htmlentities($row['category_color']); $category_color = nullable_htmlentities($row['category_color']);
$category_type = nullable_htmlentities($row['category_type']); $category_type = nullable_htmlentities($row['category_type']);

View File

@@ -0,0 +1,221 @@
<?php
require_once '../../../includes/modal_header.php';
$contract_types_array = ['Fully Managed', 'Partialy Managed', 'Break/Fix'];
$renewal_frequency_array = ['Manual', 'Annually', '2 Year', '3 Year', '5 Year', '7 Year'];
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-file-contract mr-2"></i>New Contract Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"><span>&times;</span></button>
</div>
<!-- Tabs Navigation -->
<ul class="modal-header nav nav-pills nav-justified">
<li class="nav-item">
<a class="nav-link active" id="general-tab" data-toggle="tab" href="#general" role="tab">General Info</a>
</li>
<li class="nav-item">
<a class="nav-link" id="sla-tab" data-toggle="tab" href="#sla" role="tab">SLA</a>
</li>
<li class="nav-item">
<a class="nav-link" id="rates-tab" data-toggle="tab" href="#rates" role="tab">Rates & Support</a>
</li>
<li class="nav-item">
<a class="nav-link" id="details-tab" data-toggle="tab" href="#details" role="tab">Details</a>
</li>
</ul>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
<div class="tab-content" id="contractTemplateTabContent">
<!-- General Info Tab -->
<div class="tab-pane fade show active" id="general" role="tabpanel">
<div class="form-group">
<label>Template Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-file-contract"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Contract Template Name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Template Description <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
</div>
<input type="text" class="form-control" name="description"
placeholder="Contract Template Description" maxlength="200" required>
</div>
</div>
<div class="form-group">
<label>Contract Type <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Select Type -</option>
<?php foreach ($contract_types_array as $type) { ?>
<option><?= $type ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>Renewal Frequency</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-sync-alt"></i></span>
</div>
<select class="form-control select2" name="renewal_frequency">
<option value="">- Select Frequency -</option>
<?php foreach ($renewal_frequency_array as $renewal_frequency) { ?>
<option><?= $renewal_frequency ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<!-- SLA Tab -->
<div class="tab-pane fade" id="sla" role="tabpanel">
<div class="form-row">
<div class="form-group col-md-6">
<label>Low Priority Response (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="number" class="form-control" name="sla_low_response_time" placeholder="e.g., 24">
</div>
</div>
<div class="form-group col-md-6">
<label>Low Priority Resolution (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-hourglass-half"></i></span>
</div>
<input type="number" class="form-control" name="sla_low_resolution_time" placeholder="e.g., 48">
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label>Medium Priority Response (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="number" class="form-control" name="sla_medium_response_time" placeholder="e.g., 12">
</div>
</div>
<div class="form-group col-md-6">
<label>Medium Priority Resolution (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-hourglass-half"></i></span>
</div>
<input type="number" class="form-control" name="sla_medium_resolution_time" placeholder="e.g., 24">
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label>High Priority Response (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-bolt"></i></span>
</div>
<input type="number" class="form-control" name="sla_high_response_time" placeholder="e.g., 1">
</div>
</div>
<div class="form-group col-md-6">
<label>High Priority Resolution (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
</div>
<input type="number" class="form-control" name="sla_high_resolution_time" placeholder="e.g., 4">
</div>
</div>
</div>
</div>
<!-- Rates & Support Tab -->
<div class="tab-pane fade" id="rates" role="tabpanel">
<div class="form-group">
<label>Standard Hourly Rate</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-dollar-sign"></i></span>
</div>
<input type="text" class="form-control" name="rate_standard" placeholder="e.g., 100">
</div>
</div>
<div class="form-group">
<label>After Hours Hourly Rate</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-moon"></i></span>
</div>
<input type="text" class="form-control" name="rate_after_hours" placeholder="e.g., 150">
</div>
</div>
<div class="form-group">
<label>Support Hours</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
</div>
<input type="text" class="form-control" name="support_hours" placeholder="e.g., Mon-Fri 9am-5pm">
</div>
</div>
<div class="form-group">
<label>Net Terms</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-file-invoice-dollar"></i></span>
</div>
<input type="text" class="form-control" name="net_terms" placeholder="e.g., Net 30">
</div>
</div>
</div>
<!-- Details Tab -->
<div class="tab-pane fade" id="details" role="tabpanel">
<div class="form-group">
<textarea class="form-control tinymce" rows="6" name="details" placeholder="Enter Contract Details"></textarea>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_contract_template" class="btn btn-primary text-bold">
<i class="fa fa-check mr-2"></i>Create Template
</button>
<button type="button" class="btn btn-light" data-dismiss="modal">
<i class="fa fa-times mr-2"></i>Cancel
</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';
?>

View File

@@ -0,0 +1,265 @@
<?php
require_once '../../../includes/modal_header.php';
$contract_template_id = intval($_GET['id']);
$contract_types_array = ['Fully Managed', 'Partialy Managed', 'Break/Fix'];
$update_frequency_array = ['Manual', 'Annually', '2 Year', '3 Year', '5 Year', '7 Year'];
// Fetch existing template
$sql = mysqli_query($mysqli, "SELECT * FROM contract_templates WHERE contract_template_id = $contract_template_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
// Assign locals
$name = nullable_htmlentities($row['contract_template_name']);
$description = nullable_htmlentities($row['contract_template_description']);
$type = nullable_htmlentities($row['contract_template_type']);
$renewal_frequency = nullable_htmlentities($row['contract_template_renewal_frequency']);
$sla_low_resp = intval($row['contract_template_sla_low_response_time']);
$sla_med_resp = intval($row['contract_template_sla_medium_response_time']);
$sla_high_resp = intval($row['contract_template_sla_high_response_time']);
$sla_low_res = intval($row['contract_template_sla_low_resolution_time']);
$sla_med_res = intval($row['contract_template_sla_medium_resolution_time']);
$sla_high_res = intval($row['contract_template_sla_high_resolution_time']);
$hourly_rate = intval($row['contract_template_rate_standard']);
$after_hours = intval($row['contract_template_rate_after_hours']);
$support_hours = nullable_htmlentities($row['contract_template_support_hours']);
$net_terms = intval($row['contract_template_net_terms']);
$details = nullable_htmlentities($row['contract_template_details']);
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-file-contract mr-2"></i>Edit Contract Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"><span>&times;</span></button>
</div>
<!-- Tabs Navigation -->
<ul class="modal-header nav nav-pills nav-justified">
<li class="nav-item">
<a class="nav-link active" id="general-tab" data-toggle="tab" href="#general" role="tab">General Info</a>
</li>
<li class="nav-item">
<a class="nav-link" id="sla-tab" data-toggle="tab" href="#sla" role="tab">SLA</a>
</li>
<li class="nav-item">
<a class="nav-link" id="rates-tab" data-toggle="tab" href="#rates" role="tab">Rates & Support</a>
</li>
<li class="nav-item">
<a class="nav-link" id="details-tab" data-toggle="tab" href="#details" role="tab">Details</a>
</li>
</ul>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="contract_template_id" value="<?php echo $contract_template_id; ?>">
<div class="modal-body">
<div class="tab-content" id="contractTemplateTabContent">
<!-- General Info Tab -->
<div class="tab-pane fade show active" id="general" role="tabpanel">
<div class="form-group">
<label>Template Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-file-contract"></i></span>
</div>
<input type="text" class="form-control" name="name"
placeholder="Contract Template Name" maxlength="200" required autofocus
value="<?= $name ?>">
</div>
</div>
<div class="form-group">
<label>Template Description <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
</div>
<input type="text" class="form-control" name="description"
placeholder="Contract Template Description" maxlength="200" required
value="<?= $description ?>">
</div>
</div>
<div class="form-group">
<label>Contract Type <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Select Type -</option>
<?php foreach ($contract_types_array as $type_select) { ?>
<option <?php if ($type == $type_select) { echo "selected"; } ?>><?= $type_select ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>Renewal Frequency</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-sync-alt"></i></span>
</div>
<select class="form-control select2" name="renewal_frequency">
<option value="">- Select Frequency -</option>
<?php foreach ($renewal_frequency_array as $renewal_frequency_select) { ?>
<option <?php if ($renewal_frequency == $renewal_frequency_select) { echo "selected"; } ?>><?= $renewal_frequency_select ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<!-- SLA Tab -->
<div class="tab-pane fade" id="sla" role="tabpanel">
<div class="form-row">
<div class="form-group col-md-6">
<label>Low Priority Response (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="number" class="form-control" name="sla_low_response_time" placeholder="e.g., 24"
value="<?= $sla_low_resp ?>">
</div>
</div>
<div class="form-group col-md-6">
<label>Low Priority Resolution (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-hourglass-half"></i></span>
</div>
<input type="number" class="form-control" name="sla_low_resolution_time" placeholder="e.g., 48"
value="<?= $sla_low_res ?>">
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label>Medium Priority Response (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="number" class="form-control" name="sla_medium_response_time" placeholder="e.g., 12"
value="<?= $sla_med_resp ?>">
</div>
</div>
<div class="form-group col-md-6">
<label>Medium Priority Resolution (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-hourglass-half"></i></span>
</div>
<input type="number" class="form-control" name="sla_medium_resolution_time" placeholder="e.g., 24"
value="<?= $sla_med_res ?>">
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label>High Priority Response (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-bolt"></i></span>
</div>
<input type="number" class="form-control" name="sla_high_response_time" placeholder="e.g., 1"
value="<?= $sla_high_resp ?>">
</div>
</div>
<div class="form-group col-md-6">
<label>High Priority Resolution (hrs)</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
</div>
<input type="number" class="form-control" name="sla_high_resolution_time" placeholder="e.g., 4"
value="<?= $sla_high_res ?>">
</div>
</div>
</div>
</div>
<!-- Rates & Support Tab -->
<div class="tab-pane fade" id="rates" role="tabpanel">
<div class="form-group">
<label>Standard Hourly Rate</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-dollar-sign"></i></span>
</div>
<input type="text" class="form-control" name="rate_standard" placeholder="e.g., 100"
value="<?= $rate_standard ?>">
</div>
</div>
<div class="form-group">
<label>After Hours Hourly Rate</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-moon"></i></span>
</div>
<input type="text" class="form-control" name="rate_after_hours" placeholder="e.g., 150"
value="<?= $rate_after_hours ?>">
</div>
</div>
<div class="form-group">
<label>Support Hours</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
</div>
<input type="text" class="form-control" name="support_hours" placeholder="e.g., Mon-Fri 9am-5pm"
value="<?= $support_hours ?>">
</div>
</div>
<div class="form-group">
<label>Net Terms</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-file-invoice-dollar"></i></span>
</div>
<input type="text" class="form-control" name="net_terms" placeholder="e.g., Net 30"
value="<?= $net_terms ?>">
</div>
</div>
</div>
<!-- Details Tab -->
<div class="tab-pane fade" id="details" role="tabpanel">
<div class="form-group">
<label>Contract Details</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
</div>
<textarea class="form-control tinymce" rows="6" name="details"
placeholder="Enter Contract Details"><?= $details ?></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_contract_template" class="btn btn-primary text-bold">
<i class="fa fa-check mr-2"></i>Save Changes
</button>
<button type="button" class="btn btn-light" data-dismiss="modal">
<i class="fa fa-times mr-2"></i>Cancel
</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';
?>

View File

@@ -1,6 +1,11 @@
<div class="modal" id="addLinkModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-external-link-alt mr-2"></i>New Custom Link</h5> <h5 class="modal-title"><i class="fas fa-fw fa-external-link-alt mr-2"></i>New Custom Link</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,6 +13,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body"> <div class="modal-body">
@@ -78,6 +84,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -5,7 +5,7 @@ require_once '../../../includes/modal_header.php';
$custom_link_id = intval($_GET['id']); $custom_link_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM custom_links WHERE custom_link_id = $custom_link_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM custom_links WHERE custom_link_id = $custom_link_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$custom_link_name = nullable_htmlentities($row['custom_link_name']); $custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = nullable_htmlentities($row['custom_link_uri']); $custom_link_uri = nullable_htmlentities($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']); $custom_link_icon = nullable_htmlentities($row['custom_link_icon']);
@@ -24,7 +24,7 @@ ob_start();
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="custom_link_id" value="<?php echo $custom_link_id; ?>"> <input type="hidden" name="custom_link_id" value="<?php echo $custom_link_id; ?>">
<div class="modal-body"> <div class="modal-body">

View File

@@ -1,6 +1,11 @@
<div class="modal" id="addDocumentTemplateModal" tabindex="-1"> <?php
<div class="modal-dialog modal-xl">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-file-alt mr-2"></i>Creating Document Template</h5> <h5 class="modal-title"><i class="fa fa-fw fa-file-alt mr-2"></i>Creating Document Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,6 +13,8 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@@ -39,11 +46,44 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="add_document_template" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</button> <button type="submit" name="add_document_template" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save template</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <script>
</div> $(document).ready(function(){
$('#generateAIContent').on('click', function(){
var prompt = $('#aiPrompt').val().trim();
if(prompt === '') {
alert('Please enter a prompt.');
return;
}
$('#generateAIContent').prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Generating...');
$.ajax({
url: '/agent/ajax.php?ai_create_document_template', // The PHP script that calls the OpenAI API
method: 'POST',
data: { prompt: prompt },
dataType: 'html',
success: function(response) {
// Assuming you have exactly one TinyMCE instance on the page
// and it's targeting the .tinymce textarea:
tinymce.activeEditor.setContent(response);
},
error: function() {
alert('Error generating content. Please try again.');
},
complete: function() {
$('#generateAIContent').prop('disabled', false).html('<i class="fa fa-fw fa-magic mr-1"></i>Generate with AI');
}
});
});
});
</script>
<?php
require_once '../../../includes/modal_footer.php';

View File

@@ -5,12 +5,11 @@ require_once '../../../includes/modal_header.php';
$document_template_id = intval($_GET['id']); $document_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$document_template_name = nullable_htmlentities($row['document_template_name']); $document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']); $document_template_description = nullable_htmlentities($row['document_template_description']);
$document_template_content = nullable_htmlentities($row['document_template_content']); $document_template_content = nullable_htmlentities($row['document_template_content']);
// Generate the HTML form content using output buffering.
ob_start(); ob_start();
?> ?>
@@ -21,7 +20,9 @@ ob_start();
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="document_template_id" value="<?php echo $document_template_id; ?>"> <input type="hidden" name="document_template_id" value="<?php echo $document_template_id; ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@@ -38,7 +39,7 @@ ob_start();
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="edit_document_template" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button> <button type="submit" name="edit_document_template" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save template</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>

View File

@@ -17,7 +17,7 @@ $purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'htt
$purifier = new HTMLPurifier($purifier_config); $purifier = new HTMLPurifier($purifier_config);
$sql = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_id = $email_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_id = $email_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$email_from = nullable_htmlentities($row['email_from']); $email_from = nullable_htmlentities($row['email_from']);
$email_from_name = nullable_htmlentities($row['email_from_name']); $email_from_name = nullable_htmlentities($row['email_from_name']);

View File

@@ -1,6 +1,11 @@
<div class="modal" id="addPaymentMethodModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Creating: <strong>Payment Method</strong></h5> <h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Creating: <strong>Payment Method</strong></h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -32,6 +37,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -6,7 +6,7 @@ $payment_method_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM payment_methods WHERE payment_method_id = $payment_method_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM payment_methods WHERE payment_method_id = $payment_method_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$payment_method_id = intval($row['payment_method_id']); $payment_method_id = intval($row['payment_method_id']);
$payment_method_name = nullable_htmlentities($row['payment_method_name']); $payment_method_name = nullable_htmlentities($row['payment_method_name']);
$payment_method_description = nullable_htmlentities($row['payment_method_description']); $payment_method_description = nullable_htmlentities($row['payment_method_description']);
@@ -22,7 +22,7 @@ ob_start();
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>"> <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="payment_method_id" value="<?= $payment_method_id ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">

View File

@@ -1,7 +1,10 @@
<div class="form-group"> <?php
<div class="modal" id="addPaymentProviderModal" tabindex="-1">
<div class="modal-dialog"> require_once '../../../includes/modal_header.php';
<div class="modal-content">
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Add Payment Provider</h5> <h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Add Payment Provider</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -13,11 +16,26 @@
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-info"> <div class="alert alert-info text-center">
An income account named after the provider will always be created and used for income of paid invoices.<br> <h6>Before Adding a Payment Provider!</h6>
If "Enable Expense" option is enabled, a matching vendor will also be automatically created for expense tracking. Additionally, an expense category named "Payment Processing" will be created. We recommend you add an <strong>Account</strong> and <strong>Vendor</strong> based off the Provider name before continuing eg <strong>Stripe</strong>
</div> </div>
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#pills-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-expense">Expense</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group"> <div class="form-group">
<label>Provider <strong class="text-danger">*</strong></label> <label>Provider <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
@@ -50,18 +68,44 @@
</div> </div>
</div> </div>
<div class="form-group">
<label>Income / Expense Account <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
</div>
<select class="form-control select2" name="account" required>
<option value="">- Select an Account -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT account_id, account_name FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$account_id = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']);
?>
<option <?php if ($account_name === 'Stripe') { echo "selected"; } ?> value="<?= $account_id ?>"><?= $account_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label>Threshold</label> <label>Threshold</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div> </div>
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00"> <input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00">
</div> </div>
<small class="form-text text-muted">Will not show as an option at Checkout if invoice amount is above this number, 0 disables the threshold check.</small> <small class="form-text text-muted">Will not show as an option at Checkout if invoice amount is above this number, 0 disables the threshold check.</small>
</div> </div>
<hr> </div>
<div class="tab-pane fade" id="pills-expense">
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
@@ -70,13 +114,67 @@
</div> </div>
</div> </div>
<div class="form-group">
<label>Payment Provider Vendor <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
</div>
<select class="form-control select2" name="expense_vendor" required>
<option value="0">Expense Disabled</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = nullable_htmlentities($row['vendor_name']);
?>
<option <?php if ($vendor_name === 'Stripe') { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group">
<label>Expense Category <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
</div>
<select class="form-control select2" name="expense_category" required>
<option value="">- Select a Category -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Expense' AND category_archived_at IS NULL ORDER BY category_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$category_id = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($category_name === 'Processing Fee') { echo "selected"; } ?> value="<?= $category_id ?>"><?= $category_name ?></option>
<?php
}
?>
</select>
<div class="input-group-append">
<button class="btn btn-secondary ajax-modal" type="button"
data-modal-url="../admin/modals/category/category_add.php?category=Expense">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label>Percentage Fee to expense</label> <label>Percentage Fee to expense</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
</div> </div>
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" placeholder="Enter Percentage"> <input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" placeholder="Enter Percentage">
</div> </div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small> <small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div> </div>
@@ -87,17 +185,19 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div> </div>
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" placeholder="0.030"> <input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" placeholder="0.030">
</div> </div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small> <small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div> </div>
</div> </div>
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="add_payment_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Add</button> <button type="submit" name="add_payment_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Add</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -6,14 +6,14 @@ $provider_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers WHERE payment_provider_id = $provider_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM payment_providers WHERE payment_provider_id = $provider_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$provider_name = nullable_htmlentities($row['payment_provider_name']); $provider_name = nullable_htmlentities($row['payment_provider_name']);
$public_key = nullable_htmlentities($row['payment_provider_public_key']); $public_key = nullable_htmlentities($row['payment_provider_public_key']);
$private_key = nullable_htmlentities($row['payment_provider_private_key']); $private_key = nullable_htmlentities($row['payment_provider_private_key']);
$account_id = nullable_htmlentities($row['payment_provider_account']); $account_id = intval($row['payment_provider_account']);
$threshold = floatval($row['payment_provider_threshold']); $threshold = floatval($row['payment_provider_threshold']);
$vendor_id = nullable_htmlentities($row['payment_provider_expense_vendor']); $vendor_id = intval($row['payment_provider_expense_vendor']);
$category_id = nullable_htmlentities($row['payment_provider_expense_category']); $category_id = intval($row['payment_provider_expense_category']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100; $percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']); $flat_fee = floatval($row['payment_provider_expense_flat_fee']);
@@ -21,24 +21,39 @@ $flat_fee = floatval($row['payment_provider_expense_flat_fee']);
ob_start(); ob_start();
?> ?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Editing: <strong><?php echo $provider_name; ?></strong></h5> <h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Editing: <strong><?= $provider_name ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>"> <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="provider_id" value="<?php echo $provider_id; ?>"> <input type="hidden" name="provider_id" value="<?= $provider_id ?>">
<div class="modal-body"> <div class="modal-body">
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#pills-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-expense">Expense</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group"> <div class="form-group">
<label>Publishable key <strong class="text-danger">*</strong></label> <label>Publishable key <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div> </div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?php echo $public_key; ?>"> <input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?= $public_key ?>">
</div> </div>
</div> </div>
@@ -48,7 +63,31 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div> </div>
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)" value="<?php echo $private_key; ?>"> <input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)" value="<?= $private_key ?>">
</div>
</div>
<div class="form-group">
<label>Income / Expense Account <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
</div>
<select class="form-control select2" name="account" required>
<option value="">- Select an Account -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT account_id, account_name FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$account_id_select = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']);
?>
<option <?php if ($account_id === $account_id_select) { echo "selected"; } ?> value="<?= $account_id_select ?>"><?= $account_name ?></option>
<?php
}
?>
</select>
</div> </div>
</div> </div>
@@ -58,19 +97,69 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div> </div>
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00" value="<?php echo $threshold; ?>"> <input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00" value="<?php echo $threshold; ?>">
</div> </div>
<small class="form-text text-muted">Will not show as an option at Checkout if above this number</small> <small class="form-text text-muted">Will not show as an option at Checkout if above this number</small>
</div> </div>
<hr> </div>
<div class="tab-pane fade" id="pills-expense">
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <label>Payment Provider Vendor <strong class="text-danger">*</strong></label>
<input type="checkbox" class="custom-control-input" name="enable_expense" <?php if ($vendor_id) { echo "checked"; } ?> value="1" id="enableEditExpenseSwitch"> <div class="input-group">
<label class="custom-control-label" for="enableEditExpenseSwitch">Enable Expense</label> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
</div>
<select class="form-control select2" name="expense_vendor" required>
<option value="0">Expense Disabled</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id_select = intval($row['vendor_id']);
$vendor_name = nullable_htmlentities($row['vendor_name']);
?>
<option <?php if ($vendor_id === $vendor_id_select) { echo "selected"; } ?>
value="<?= $vendor_id_select ?>"><?= $vendor_name ?>
</option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group">
<label>Expense Category <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
</div>
<select class="form-control select2" name="expense_category" required>
<option value="">- Select a Category -</option>
<?php
$sql_category = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Expense' AND category_archived_at IS NULL ORDER BY category_name ASC");
while ($row = mysqli_fetch_assoc($sql_category)) {
$category_id_select = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($category_id === $category_id_select) { echo "selected"; } ?> value="<?= $category_id_select ?>"><?= $category_name ?></option>
<?php
}
?>
</select>
<div class="input-group-append">
<button class="btn btn-secondary ajax-modal" type="button"
data-modal-url="../admin/modals/category/category_add.php?category=Expense">
<i class="fas fa-plus"></i>
</button>
</div>
</div> </div>
<small>(Category: Payment Processing -- Vendor: <?php echo $provider_name; ?></small>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -79,7 +168,7 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
</div> </div>
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" value="<?php echo $percent_fee; ?>" placeholder="Enter Percentage"> <input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" value="<?php echo $percent_fee; ?>" placeholder="Enter Percentage">
</div> </div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small> <small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div> </div>
@@ -90,11 +179,12 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div> </div>
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" value="<?php echo $flat_fee; ?>" placeholder="0.030"> <input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" value="<?php echo $flat_fee; ?>" placeholder="0.030">
</div> </div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small> <small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div> </div>
</div>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="edit_payment_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button> <button type="submit" name="edit_payment_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>

View File

@@ -1,6 +1,10 @@
<div class="modal" id="addProjectTemplateModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-project-diagram mr-2"></i>Creating Project Template</h5> <h5 class="modal-title"><i class="fas fa-fw fa-project-diagram mr-2"></i>Creating Project Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,6 +12,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@@ -36,6 +41,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -1,6 +1,18 @@
<div class="modal" id="editProjectTemplateModal<?php echo $project_template_id; ?>" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
$project_template_id = intval($_GET['project_template_id']);
$sql = mysqli_query($mysqli, "SELECT * FROM project_templates WHERE project_template_id = $project_template_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$project_template_name = nullable_htmlentities($row['project_template_name']);
$project_template_description = nullable_htmlentities($row['project_template_description']);
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-project-diagram mr-2"></i>Editing Project Template: <strong><?php echo $project_template_name; ?></strong></h5> <h5 class="modal-title"><i class="fas fa-fw fa-project-diagram mr-2"></i>Editing Project Template: <strong><?php echo $project_template_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,6 +20,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>"> <input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>">
<div class="modal-body"> <div class="modal-body">
@@ -34,11 +47,11 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="edit_project_template" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Create</button> <button type="submit" name="edit_project_template" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -1,6 +1,13 @@
<div class="modal" id="addProjectTemplateTicketTemplateModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
$project_template_id = intval($_GET['project_template_id']);
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-life-ring mr-2"></i>Adding Ticket Template</h5> <h5 class="modal-title"><i class="fas fa-fw fa-life-ring mr-2"></i>Adding Ticket Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,7 +15,9 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>"> <input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@@ -29,7 +38,7 @@
AND ticket_template_archived_at IS NULL AND ticket_template_archived_at IS NULL
ORDER BY ticket_template_name ASC" ORDER BY ticket_template_name ASC"
); );
while ($row = mysqli_fetch_array($sql_ticket_templates_select)) { while ($row = mysqli_fetch_assoc($sql_ticket_templates_select)) {
$ticket_template_id_select = intval($row['ticket_template_id']); $ticket_template_id_select = intval($row['ticket_template_id']);
$ticket_template_name_select = nullable_htmlentities($row['ticket_template_name']); $ticket_template_name_select = nullable_htmlentities($row['ticket_template_name']);
?> ?>
@@ -59,6 +68,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -1,17 +1,38 @@
<div class="modal" id="addRoleModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-user-shield mr-2"></i>Add new role</h5> <h5 class="modal-title"><i class="fas fa-fw fa-user-shield mr-2"></i>New Role</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off"> <form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>"> <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body"> <div class="modal-body">
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#pills-role-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-role-permissions">Permissions</a>
</li>
</ul>
<hr>
<div class="tab-content"> <div class="tab-content">
<!-- DETAILS TAB -->
<div class="tab-pane fade show active" id="pills-role-details">
<div class="form-group"> <div class="form-group">
<label>Name <strong class="text-danger">*</strong></label> <label>Name <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
@@ -34,25 +55,159 @@
<div class="form-group"> <div class="form-group">
<label>Admin Access <strong class="text-danger">*</strong></label> <label>Admin Access <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend"> <div class="custom-control custom-radio mb-2">
<span class="input-group-text"><i class="fa fa-fw fa-tools"></i></span> <input type="radio" class="custom-control-input" id="admin_no" name="role_is_admin" value="0" checked required>
</div> <label class="custom-control-label" for="admin_no">
<select class="form-control select2" name="role_is_admin" required> No - use permissions on the next tab
<option value="0">No - edit after creation to set permissions</option> </label>
<option value="1">Yes - this role should have full admin access</option>
</select>
</div> </div>
<div class="custom-control custom-radio">
<input type="radio" class="custom-control-input" id="admin_yes" name="role_is_admin" value="1" required>
<label class="custom-control-label" for="admin_yes">
Yes - this role should have full admin access
</label>
</div> </div>
</div> </div>
</div> </div>
<!-- PERMISSIONS TAB -->
<div class="tab-pane fade" id="pills-role-permissions">
<?php
// Enumerate modules
$sql_modules = mysqli_query($mysqli, "SELECT * FROM modules");
while ($row_modules = mysqli_fetch_assoc($sql_modules)) {
$module_id = intval($row_modules['module_id']);
// raw for name, escaped for display
$module_name_raw = $row_modules['module_name'];
$module_name_display = ucfirst(str_replace("module_", "", $module_name_raw));
$module_name_display_safe = nullable_htmlentities($module_name_display);
$module_description = nullable_htmlentities($row_modules['module_description']);
// default for new role
$module_permission = 0;
$field_name = $module_id . "##" . $module_name_raw;
$group_id = "perm_group_$module_id";
?>
<div class="form-group">
<label><?= $module_name_display_safe ?> <strong class="text-danger">*</strong></label>
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons" role="group"
aria-label="Permissions for <?= $module_name_display_safe ?>">
<label class="btn btn-outline-secondary btn-sm active" title="No Access">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_0"
value="0"
autocomplete="off"
checked
required
>
None
</label>
<label class="btn btn-outline-primary btn-sm" title="Viewing Only">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_1"
value="1"
autocomplete="off"
>
<i class="fas fa-fw fa-eye mr-1"></i>Read
</label>
<label class="btn btn-outline-warning btn-sm" title="Read, Edit, Archive">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_2"
value="2"
autocomplete="off"
>
<i class="fas fa-fw fa-edit mr-1"></i>Modify
</label>
<label class="btn btn-outline-danger btn-sm" title="Read, Edit, Archive, Delete">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_3"
value="3"
autocomplete="off"
>
<i class="fas fa-fw fa-trash mr-1"></i>Full
</label>
</div>
<small class="form-text text-muted mt-2"><?= $module_description ?></small>
</div>
<?php } // end while ?>
</div>
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="add_role" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button> <button type="submit" name="add_role" class="btn btn-primary text-bold">
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <i class="fas fa-check mr-2"></i>Create
</button>
<button type="button" class="btn btn-light" data-dismiss="modal">
<i class="fas fa-times mr-2"></i>Cancel
</button>
</div> </div>
</form> </form>
</div>
</div> <script>
</div> // Optional: when Admin Yes is selected, disable permission radios + switch to Details tab
(function () {
function setPermissionsEnabled(enabled) {
const permsTab = document.getElementById('pills-role-permissions');
if (!permsTab) return;
permsTab.querySelectorAll('input[type="radio"]').forEach(function (el) {
el.disabled = !enabled;
});
// also visually dim the tab content
permsTab.style.opacity = enabled ? '1' : '0.5';
}
const adminYes = document.getElementById('admin_yes');
const adminNo = document.getElementById('admin_no');
function refresh() {
const isAdmin = adminYes && adminYes.checked;
setPermissionsEnabled(!isAdmin);
if (isAdmin) {
// move user back to Details tab (avoids confusion)
const detailsTab = document.querySelector('a[href="#pills-role-details"]');
if (detailsTab) detailsTab.click();
}
}
if (adminYes && adminNo) {
adminYes.addEventListener('change', refresh);
adminNo.addEventListener('change', refresh);
refresh();
}
})();
</script>
<?php
require_once '../../../includes/modal_footer.php';

View File

@@ -6,7 +6,7 @@ $role_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_id = $role_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_id = $role_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$role_name = nullable_htmlentities($row['role_name']); $role_name = nullable_htmlentities($row['role_name']);
$role_description = nullable_htmlentities($row['role_description']); $role_description = nullable_htmlentities($row['role_description']);
$role_admin = intval($row['role_is_admin']); $role_admin = intval($row['role_is_admin']);
@@ -31,36 +31,36 @@ if (empty($user_names_string)) {
$user_names_string = "-"; $user_names_string = "-";
} }
// Generate the HTML form content using output buffering.
ob_start(); ob_start();
?> ?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-user-shield mr-2"></i>Editing role: <h5 class="modal-title"><i class="fas fa-fw fa-user-shield mr-2"></i>Editing role:
<strong><?php echo $role_name; ?></strong></h5> <strong><?= $role_name ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off"> <form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>"> <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="role_id" value="<?php echo $role_id; ?>"> <input type="hidden" name="role_id" value="<?= $role_id ?>">
<div class="modal-body"> <div class="modal-body">
<ul class="nav nav-pills nav-justified mb-3"> <ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#pills-role-details<?php echo $role_id; ?>">Details</a> <a class="nav-link active" data-toggle="pill" href="#pills-role-details">Details</a>
</li> </li>
<?php if (!$role_admin) { ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-role-access<?php echo $role_id; ?>">Access</a> <a class="nav-link" data-toggle="pill" href="#pills-role-permissions">Permissions</a>
</li> </li>
<?php } ?>
</ul> </ul>
<hr> <hr>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane fade show active" id="pills-role-details<?php echo $role_id; ?>"> <div class="tab-pane fade show active" id="pills-role-details">
<div class="form-group"> <div class="form-group">
<label>Name <strong class="text-danger">*</strong></label> <label>Name <strong class="text-danger">*</strong></label>
@@ -68,7 +68,7 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user-shield"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-user-shield"></i></span>
</div> </div>
<input type="text" class="form-control" name="role_name" placeholder="Role Name" maxlength="200" value="<?php echo $role_name; ?>" required> <input type="text" class="form-control" name="role_name" placeholder="Role Name" maxlength="200" value="<?= $role_name ?>" required>
</div> </div>
</div> </div>
@@ -78,27 +78,33 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-chevron-right"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-chevron-right"></i></span>
</div> </div>
<input type="text" class="form-control" name="role_description" placeholder="Role Description" maxlength="200" value="<?php echo $role_description; ?>" required> <input type="text" class="form-control" name="role_description" placeholder="Role Description" maxlength="200" value="<?= $role_description ?>" required>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Admin Access <strong class="text-danger">*</strong></label> <label>Admin Access <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="custom-control custom-radio mb-2">
<div class="input-group-prepend"> <input type="radio" class="custom-control-input" id="admin_yes" name="role_is_admin" value="1"
<span class="input-group-text"><i class="fa fa-fw fa-tools"></i></span> <?php if ($role_admin) { echo 'checked'; } ?> required>
<label class="custom-control-label" for="admin_yes">
Yes - this role should have full admin access
</label>
</div> </div>
<select class="form-control select2" name="role_is_admin" required>
<option value="1" <?php if ($role_admin) { echo 'selected'; } ?> >Yes - this role should have full admin access</option> <div class="custom-control custom-radio">
<option value="0" <?php if (!$role_admin) { echo 'selected'; } ?>>No - use permissions on the next tab</option> <input type="radio" class="custom-control-input" id="admin_no" name="role_is_admin" value="0"
</select> <?php if (!$role_admin) { echo 'checked'; } ?> required>
<label class="custom-control-label" for="admin_no">
No - use permissions on the next tab
</label>
</div> </div>
</div> </div>
</div> </div>
<?php if (!$role_admin) { ?>
<div class="tab-pane fade" id="pills-role-access<?php echo $role_id; ?>"> <div class="tab-pane fade" id="pills-role-permissions">
<?php if ($role_admin) { ?> <?php if ($role_admin) { ?>
<div class="alert alert-warning"><strong>Module permissions do not apply to Admins.</strong></div> <div class="alert alert-warning"><strong>Module permissions do not apply to Admins.</strong></div>
@@ -108,14 +114,14 @@ ob_start();
// Enumerate modules // Enumerate modules
$sql_modules = mysqli_query($mysqli, "SELECT * FROM modules"); $sql_modules = mysqli_query($mysqli, "SELECT * FROM modules");
while ($row_modules = mysqli_fetch_array($sql_modules)) { while ($row_modules = mysqli_fetch_assoc($sql_modules)) {
$module_id = intval($row_modules['module_id']); $module_id = intval($row_modules['module_id']);
$module_name = nullable_htmlentities($row_modules['module_name']); $module_name = nullable_htmlentities($row_modules['module_name']);
$module_name_display = ucfirst(str_replace("module_","",$module_name)); $module_name_display = ucfirst(str_replace("module_","",$module_name));
$module_description = nullable_htmlentities($row_modules['module_description']); $module_description = nullable_htmlentities($row_modules['module_description']);
// Get permission level for module // Get permission level for module
$module_permission_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT user_role_permission_level FROM user_role_permissions WHERE module_id = $module_id AND user_role_id = $role_id LIMIT 1")); $module_permission_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_role_permission_level FROM user_role_permissions WHERE module_id = $module_id AND user_role_id = $role_id LIMIT 1"));
$module_permission = 0; $module_permission = 0;
if ($module_permission_row) { if ($module_permission_row) {
$module_permission = $module_permission_row['user_role_permission_level']; $module_permission = $module_permission_row['user_role_permission_level'];
@@ -123,22 +129,73 @@ ob_start();
?> ?>
<div class="form-group"> <div class="form-group">
<label> <?php echo $module_name_display ?> <strong class="text-danger">*</strong></label> <label> <?= $module_name_display ?> <strong class="text-danger">*</strong></label>
<div class="input-group"> <?php
<select class="form-control select2" name="<?php echo "$module_id##$module_name" ?>" required> $field_name = "$module_id##$module_name";
<option value="0" <?php if ($module_permission == 0) { echo 'selected'; } ?> >None</option> $group_id = "perm_group_$module_id";
<option value="1" <?php if ($module_permission == 1) { echo 'selected'; } ?> >Read</option> ?>
<option value="2" <?php if ($module_permission == 2) { echo 'selected'; } ?>>Modify (Read, Edit, Archive)</option>
<option value="3" <?php if ($module_permission == 3) { echo 'selected'; } ?>>Full (Read, Edit, Archive, Delete)</option> <div class="btn-group btn-group-toggle btn-block" data-toggle="buttons" role="group" aria-label="Permissions for <?= $module_name_display ?>">
</select>
<label class="btn btn-outline-secondary btn-sm <?php if ($module_permission == 0) { echo 'active'; } ?>" title="No Access">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_0"
value="0"
autocomplete="off"
<?php if ($module_permission == 0) { echo 'checked'; } ?>
required
>
None
</label>
<label class="btn btn-outline-primary btn-sm <?php if ($module_permission == 1) { echo 'active'; } ?>" title="Viewing Only">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_1"
value="1"
autocomplete="off"
<?php if ($module_permission == 1) { echo 'checked'; } ?>
>
<i class="fas fa-fw fa-eye mr-1"></i>Read
</label>
<label class="btn btn-outline-warning btn-sm <?php if ($module_permission == 2) { echo 'active'; } ?>" title="Read, Edit, Archive">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_2"
value="2"
autocomplete="off"
<?php if ($module_permission == 2) { echo 'checked'; } ?>
>
<i class="fas fa-fw fa-edit mr-1"></i>Modify
</label>
<label class="btn btn-outline-danger btn-sm <?php if ($module_permission == 3) { echo 'active'; } ?>" title="Read, Edit, Archive, Delete">
<input
type="radio"
name="<?= $field_name ?>"
id="<?= $group_id ?>_3"
value="3"
autocomplete="off"
<?php if ($module_permission == 3) { echo 'checked'; } ?>
>
<i class="fas fa-fw fa-trash mr-1"></i>Full
</label>
</div> </div>
<small class="form-text text-muted"><?php echo $module_description ?></small>
<small class="form-text text-muted mt-2"><?= $module_description ?></small>
</div> </div>
<?php } // End while ?> <?php } // End while ?>
</div> </div>
<?php } ?>
</div> </div>

View File

@@ -1,6 +1,11 @@
<div class="modal" id="addSoftwareTemplateModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-cube mr-2"></i>New License Template</h5> <h5 class="modal-title"><i class="fa fa-fw fa-cube mr-2"></i>New License Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,6 +13,8 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@@ -78,6 +85,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -5,7 +5,7 @@ require_once '../../../includes/modal_header.php';
$software_template_id = intval($_GET['id']); $software_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM software_templates WHERE software_template_id = $software_template_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM software_templates WHERE software_template_id = $software_template_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$software_name = nullable_htmlentities($row['software_template_name']); $software_name = nullable_htmlentities($row['software_template_name']);
$software_version = nullable_htmlentities($row['software_template_version']); $software_version = nullable_htmlentities($row['software_template_version']);
$software_description = nullable_htmlentities($row['software_template_description']); $software_description = nullable_htmlentities($row['software_template_description']);
@@ -24,7 +24,9 @@ ob_start();
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="software_template_id" value="<?php echo $software_template_id; ?>"> <input type="hidden" name="software_template_id" value="<?php echo $software_template_id; ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">

View File

@@ -15,8 +15,11 @@ if (isset($_GET['type'])) {
$type_display = "Contact"; $type_display = "Contact";
} elseif ($type === 4) { } elseif ($type === 4) {
$type_display = "Credential"; $type_display = "Credential";
} elseif ($type === 5) {
$type_display = "Asset";
} }
} }
ob_start();
?> ?>
@@ -27,7 +30,9 @@ if (isset($_GET['type'])) {
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="type" value="<?php echo $type; ?>"> <input type="hidden" name="type" value="<?php echo $type; ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label>Name <strong class="text-danger">*</strong></label> <label>Name <strong class="text-danger">*</strong></label>
@@ -57,6 +62,7 @@ if (isset($_GET['type'])) {
<option value="2">Location Tag</option> <option value="2">Location Tag</option>
<option value="3">Contact Tag</option> <option value="3">Contact Tag</option>
<option value="4">Credential Tag</option> <option value="4">Credential Tag</option>
<option value="5">Asset Tag</option>
</select> </select>
</div> </div>
</div> </div>
@@ -85,7 +91,7 @@ if (isset($_GET['type'])) {
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="add_tag" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Create</button> <button type="submit" name="add_tag" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Create Tag</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>

View File

@@ -6,23 +6,38 @@ $tag_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM tags WHERE tag_id = $tag_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM tags WHERE tag_id = $tag_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$tag_name = nullable_htmlentities($row['tag_name']); $tag_name = nullable_htmlentities($row['tag_name']);
$tag_type = intval($row['tag_type']); $tag_type = intval($row['tag_type']);
$tag_color = nullable_htmlentities($row['tag_color']); $tag_color = nullable_htmlentities($row['tag_color']);
$tag_icon = nullable_htmlentities($row['tag_icon']); $tag_icon = nullable_htmlentities($row['tag_icon']);
// Generate the HTML form content using output buffering. if ($tag_type == 1) {
$tag_type_display = "Client";
} elseif ( $tag_type == 2) {
$tag_type_display = "Location";
} elseif ( $tag_type == 3) {
$tag_type_display = "Contact";
} elseif ( $tag_type == 4) {
$tag_type_display = "Credential";
} elseif ( $tag_type == 5) {
$tag_type_display = "Asset";
} else {
$tag_type_display = "Unknown";
}
ob_start(); ob_start();
?> ?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-tag mr-2"></i>Editing tag: <strong><?php echo $tag_name; ?></strong></h5> <h5 class="modal-title"><i class="fas fa-fw fa-tag mr-2"></i><?= $tag_type_display ?> Tag: <strong><?php echo $tag_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tag_id" value="<?php echo $tag_id; ?>"> <input type="hidden" name="tag_id" value="<?php echo $tag_id; ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@@ -35,22 +50,6 @@ ob_start();
</div> </div>
</div> </div>
<div class="form-group">
<label>Type <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-th"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Type -</option>
<option value="1" <?php if ($tag_type == 1) { echo "selected"; } ?>>Client Tag</option>
<option value="2" <?php if ($tag_type == 2) { echo "selected"; } ?>>Location Tag</option>
<option value="3" <?php if ($tag_type == 3) { echo "selected"; } ?>>Contact Tag</option>
<option value="4" <?php if ($tag_type == 4) { echo "selected"; } ?>>Credential Tag</option>
</select>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label>Color <strong class="text-danger">*</strong></label> <label>Color <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
@@ -73,7 +72,7 @@ ob_start();
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="edit_tag" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button> <button type="submit" name="edit_tag" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save changes</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>

View File

@@ -1,6 +1,11 @@
<div class="modal" id="addTaxModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-balance-scale mr-2"></i>New Tax</h5> <h5 class="modal-title"><i class="fas fa-fw fa-balance-scale mr-2"></i>New Tax</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -25,6 +30,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -5,7 +5,7 @@ require_once '../../../includes/modal_header.php';
$tax_id = intval($_GET['id']); $tax_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM taxes WHERE tax_id = $tax_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM taxes WHERE tax_id = $tax_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$tax_name = nullable_htmlentities($row['tax_name']); $tax_name = nullable_htmlentities($row['tax_name']);
$tax_percent = floatval($row['tax_percent']); $tax_percent = floatval($row['tax_percent']);

View File

@@ -1,6 +1,8 @@
<div class="modal" id="addTicketStatusModal" tabindex="-1"> <?php
<div class="modal-dialog"> require_once '../../../includes/modal_header.php';
<div class="modal-content"> ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-info-circle mr-2"></i>New Ticket Status</h5> <h5 class="modal-title"><i class="fas fa-fw fa-info-circle mr-2"></i>New Ticket Status</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,9 +10,9 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label>Name <strong class="text-danger">*</strong></label> <label>Name <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
@@ -37,6 +39,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -5,7 +5,7 @@ require_once '../../../includes/modal_header.php';
$ticket_status_id = intval($_GET['id']); $ticket_status_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM ticket_statuses WHERE ticket_status_id = $ticket_status_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM ticket_statuses WHERE ticket_status_id = $ticket_status_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$ticket_status_name = nullable_htmlentities($row['ticket_status_name']); $ticket_status_name = nullable_htmlentities($row['ticket_status_name']);
$ticket_status_color = nullable_htmlentities($row['ticket_status_color']); $ticket_status_color = nullable_htmlentities($row['ticket_status_color']);
$ticket_status_order = intval($row['ticket_status_order']); $ticket_status_order = intval($row['ticket_status_order']);
@@ -22,7 +22,9 @@ ob_start();
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="ticket_status_id" value="<?php echo $ticket_status_id; ?>"> <input type="hidden" name="ticket_status_id" value="<?php echo $ticket_status_id; ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">

View File

@@ -1,13 +1,19 @@
<div class="modal" id="addTicketTemplateModal" tabindex="-1"> <?php
<div class="modal-dialog modal-lg">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-life-ring mr-2"></i>Creating Ticket Template</h5> <h5 class="modal-title"><i class="fa fa-fw fa-life-ring mr-2"></i>New Ticket Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@@ -55,7 +61,7 @@
<?php <?php
$sql_project_templates = mysqli_query($mysqli, "SELECT * FROM project_templates WHERE project_template_archived_at IS NULL ORDER BY project_template_name ASC"); $sql_project_templates = mysqli_query($mysqli, "SELECT * FROM project_templates WHERE project_template_archived_at IS NULL ORDER BY project_template_name ASC");
while ($row = mysqli_fetch_array($sql_project_templates)) { while ($row = mysqli_fetch_assoc($sql_project_templates)) {
$project_template_id_select = intval($row['project_template_id']); $project_template_id_select = intval($row['project_template_id']);
$project_template_name_select = nullable_htmlentities($row['project_template_name']); ?> $project_template_name_select = nullable_htmlentities($row['project_template_name']); ?>
<option value="<?php echo $project_template_id_select; ?>"><?php echo $project_template_name_select; ?></option> <option value="<?php echo $project_template_id_select; ?>"><?php echo $project_template_name_select; ?></option>
@@ -66,10 +72,10 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" name="add_ticket_template" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</button> <button type="submit" name="add_ticket_template" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create Template</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -9,7 +9,9 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="ticket_template_id" value="<?php echo $ticket_template_id; ?>"> <input type="hidden" name="ticket_template_id" value="<?php echo $ticket_template_id; ?>">
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">

View File

@@ -6,7 +6,7 @@ $task_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_id = $task_template_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_id = $task_template_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$task_template_name = nullable_htmlentities($row['task_template_name']); $task_template_name = nullable_htmlentities($row['task_template_name']);
$task_template_order = intval($row['task_template_order']); $task_template_order = intval($row['task_template_order']);
$task_template_completion_estimate = intval($row['task_template_completion_estimate']); $task_template_completion_estimate = intval($row['task_template_completion_estimate']);
@@ -24,6 +24,7 @@ ob_start();
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="task_template_id" value="<?php echo $task_template_id; ?>"> <input type="hidden" name="task_template_id" value="<?php echo $task_template_id; ?>">
<div class="modal-body"> <div class="modal-body">

View File

@@ -1,6 +1,10 @@
<div class="modal" id="addUserModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-user-plus mr-2"></i>New User</h5> <h5 class="modal-title"><i class="fas fa-fw fa-user-plus mr-2"></i>New User</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -72,7 +76,7 @@
<option value="">- Role -</option> <option value="">- Role -</option>
<?php <?php
$sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL"); $sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_user_roles)) { while ($row = mysqli_fetch_assoc($sql_user_roles)) {
$role_id = intval($row['role_id']); $role_id = intval($row['role_id']);
$role_name = nullable_htmlentities($row['role_name']); $role_name = nullable_htmlentities($row['role_name']);
@@ -125,7 +129,7 @@
<?php <?php
$sql_client_select = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC"); $sql_client_select = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
while ($row = mysqli_fetch_array($sql_client_select)) { while ($row = mysqli_fetch_assoc($sql_client_select)) {
$client_id = intval($row['client_id']); $client_id = intval($row['client_id']);
$client_name = nullable_htmlentities($row['client_name']); $client_name = nullable_htmlentities($row['client_name']);
@@ -151,6 +155,22 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <script>
</div>
function generatePassword() {
jQuery.get(
"/agent/ajax.php", {
get_readable_pass: 'true'
},
function(data) {
const password = JSON.parse(data);
document.getElementById("password").value = password;
}
);
}
</script>
<?php
require_once "../../../includes/modal_footer.php";

View File

@@ -1,6 +1,10 @@
<div class="modal" id="resetAllUserPassModal" tabindex="-1"> <?php
<div class="modal-dialog modal-lg">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-body"> <div class="modal-body">
<div class="mb-4" style="text-align: center;"> <div class="mb-4" style="text-align: center;">
<i class="far fas fa-10x fa-skull-crossbones text-danger mb-3 mt-3"></i> <i class="far fas fa-10x fa-skull-crossbones text-danger mb-3 mt-3"></i>
@@ -26,6 +30,6 @@
<button type="button" class="btn btn-outline-secondary btn-lg px-5 mr-4" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-outline-secondary btn-lg px-5 mr-4" data-dismiss="modal">Cancel</button>
</div> </div>
</div>
</div> <?php
</div> require_once "../../../includes/modal_footer.php";

View File

@@ -6,7 +6,7 @@ $user_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM users WHERE users.user_id = $user_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM users WHERE users.user_id = $user_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$user_name = nullable_htmlentities($row['user_name']); $user_name = nullable_htmlentities($row['user_name']);
$user_email = nullable_htmlentities($row['user_email']); $user_email = nullable_htmlentities($row['user_email']);
$user_avatar = nullable_htmlentities($row['user_avatar']); $user_avatar = nullable_htmlentities($row['user_avatar']);
@@ -60,7 +60,7 @@ ob_start();
<option value="0">No one</option> <option value="0">No one</option>
<?php <?php
$sql_users = mysqli_query($mysqli, "SELECT * FROM users WHERE user_type = 1 AND user_archived_at IS NULL"); $sql_users = mysqli_query($mysqli, "SELECT * FROM users WHERE user_type = 1 AND user_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_users)) { while ($row = mysqli_fetch_assoc($sql_users)) {
$user_id_select = intval($row['user_id']); $user_id_select = intval($row['user_id']);
$user_name_select = nullable_htmlentities($row['user_name']); $user_name_select = nullable_htmlentities($row['user_name']);

View File

@@ -9,7 +9,7 @@ $sql = mysqli_query($mysqli, "SELECT * FROM users
WHERE users.user_id = $user_id LIMIT 1" WHERE users.user_id = $user_id LIMIT 1"
); );
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$user_name = nullable_htmlentities($row['user_name']); $user_name = nullable_htmlentities($row['user_name']);
$user_email = nullable_htmlentities($row['user_email']); $user_email = nullable_htmlentities($row['user_email']);
$user_avatar = nullable_htmlentities($row['user_avatar']); $user_avatar = nullable_htmlentities($row['user_avatar']);
@@ -94,11 +94,14 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
</div> </div>
<input type="password" class="form-control" data-toggle="password" name="new_password" <input type="password" class="form-control" data-toggle="password" name="new_password" id="password"
placeholder="Leave Blank For No Password Change" autocomplete="new-password"> placeholder="Leave Blank For No Password Change" autocomplete="new-password">
<div class="input-group-append"> <div class="input-group-append">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div> </div>
<div class="input-group-append">
<span class="btn btn-default"><i class="fa fa-fw fa-question" onclick="generatePassword()"></i></span>
</div>
</div> </div>
</div> </div>
@@ -111,7 +114,7 @@ ob_start();
<select class="form-control select2" name="role" required> <select class="form-control select2" name="role" required>
<?php <?php
$sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL"); $sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_user_roles)) { while ($row = mysqli_fetch_assoc($sql_user_roles)) {
$role_id = intval($row['role_id']); $role_id = intval($row['role_id']);
$role_name = nullable_htmlentities($row['role_name']); $role_name = nullable_htmlentities($row['role_name']);
@@ -172,7 +175,7 @@ ob_start();
<?php <?php
$sql_client_select = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC"); $sql_client_select = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
while ($row = mysqli_fetch_array($sql_client_select)) { while ($row = mysqli_fetch_assoc($sql_client_select)) {
$client_id_select = intval($row['client_id']); $client_id_select = intval($row['client_id']);
$client_name_select = nullable_htmlentities($row['client_name']); $client_name_select = nullable_htmlentities($row['client_name']);
@@ -200,5 +203,23 @@ ob_start();
</div> </div>
</form> </form>
<script>
function generatePassword() {
// Send a GET request to ajax.php as ajax.php?get_readable_pass=true
jQuery.get(
"/agent/ajax.php", {
get_readable_pass: 'true'
},
function(data) {
//If we get a response from post.php, parse it as JSON
const password = JSON.parse(data);
document.getElementById("password").value = password;
}
);
}
</script>
<?php <?php
require_once "../../../includes/modal_footer.php"; require_once "../../../includes/modal_footer.php";

View File

@@ -1,6 +1,11 @@
<div class="modal" id="exportUserModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-download mr-2"></i>Export Users to CSV</h5> <h5 class="modal-title"><i class="fas fa-fw fa-download mr-2"></i>Export Users to CSV</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -16,6 +21,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -1,6 +1,10 @@
<div class="modal" id="userInviteModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-user-plus"></i>Invite User</h5> <h5 class="modal-title"><i class="fas fa-fw fa-user-plus"></i>Invite User</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -40,6 +44,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once "../../../includes/modal_footer.php";

View File

@@ -6,7 +6,7 @@ $user_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM users WHERE user_id = $user_id AND user_archived_at IS NOT NULL LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM users WHERE user_id = $user_id AND user_archived_at IS NOT NULL LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$user_name = str_replace(" (archived)", "", $row['user_name']); //Removed (archived) from user_name $user_name = str_replace(" (archived)", "", $row['user_name']); //Removed (archived) from user_name
$user_name = nullable_htmlentities($user_name); $user_name = nullable_htmlentities($user_name);
$user_email = nullable_htmlentities($row['user_email']); $user_email = nullable_htmlentities($row['user_email']);
@@ -64,7 +64,7 @@ ob_start();
<select class="form-control select2" name="role" required> <select class="form-control select2" name="role" required>
<?php <?php
$sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL"); $sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_user_roles)) { while ($row = mysqli_fetch_assoc($sql_user_roles)) {
$role_id = intval($row['role_id']); $role_id = intval($row['role_id']);
$role_name = nullable_htmlentities($row['role_name']); $role_name = nullable_htmlentities($row['role_name']);

View File

@@ -1,6 +1,11 @@
<div class="modal" id="addVendorTemplateModal" tabindex="-1"> <?php
<div class="modal-dialog">
<div class="modal-content"> require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-building mr-2"></i>New Vendor Template</h5> <h5 class="modal-title"><i class="fas fa-fw fa-building mr-2"></i>New Vendor Template</h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
@@ -8,8 +13,7 @@
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="client_id" value="<?php if (isset($_GET['client_id'])) { echo $client_id; } else { echo 0; } ?>">
<div class="modal-body"> <div class="modal-body">
@@ -163,6 +167,6 @@
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div> </div>
</form> </form>
</div>
</div> <?php
</div> require_once '../../../includes/modal_footer.php';

View File

@@ -5,7 +5,7 @@ require_once '../../../includes/modal_header.php';
$vendor_template_id = intval($_GET['id']); $vendor_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM vendor_templates WHERE vendor_template_id = $vendor_template_id LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM vendor_templates WHERE vendor_template_id = $vendor_template_id LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$vendor_name = nullable_htmlentities($row['vendor_template_name']); $vendor_name = nullable_htmlentities($row['vendor_template_name']);
$vendor_description = nullable_htmlentities($row['vendor_template_description']); $vendor_description = nullable_htmlentities($row['vendor_template_description']);
$vendor_account_number = nullable_htmlentities($row['vendor_template_account_number']); $vendor_account_number = nullable_htmlentities($row['vendor_template_account_number']);
@@ -31,7 +31,9 @@ ob_start();
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="vendor_template_id" value="<?php echo $vendor_template_id; ?>"> <input type="hidden" name="vendor_template_id" value="<?php echo $vendor_template_id; ?>">
<div class="modal-body"> <div class="modal-body">
<ul class="nav nav-pills nav-justified mb-3"> <ul class="nav nav-pills nav-justified mb-3">

113
admin/modules.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
// Default Column Sortby Filter
$sort = "module_name";
$order = "DESC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM modules
WHERE (module_name LIKE '%$q%' OR module_description LIKE '%$q%')
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-puzzle-piece mr-2"></i>Access Modules</h3>
<div class="card-tools">
<div class="btn-group">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/module/module_add.php">
<i class="fas fa-fw fa-plus mr-2"></i>New Module
</button>
</div>
</div>
</div>
<div class="card-body">
<form class="mb-4" autocomplete="off">
<div class="row">
<div class="col-md-4">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) {echo stripslashes(nullable_htmlentities($q));} ?>" placeholder="Search Modules">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=module_name&order=<?php echo $disp; ?>">
Module <?php if ($sort == 'module_name') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$module_id = intval($row['module_id']);
$module_name = nullable_htmlentities($row['module_name']);
$module_description = nullable_htmlentities($row['module_description']);
?>
<tr>
<td>
<a href="#" <?php if ($module_id > 6) { ?> class="ajax-modal" data-modal-url="modals/modules/module_edit.php?id=<?= $module_id ?>" <?php } ?>>
<strong class="text-dark"><?= $module_name ?></strong>
</a>
<div class="text-secondary"><?= $module_description ?></div>
</td>
<td>
<?php if ($module_id > 6) { ?>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/module/module_edit.php?id=<?= $module_id ?>">
<i class="fas fa-fw fa-user-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_module=<?= $module_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Delete
</a>
</div>
</div>
<?php } else { echo "<p class='text-center'>N/A Predefined</p>"; } ?>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -0,0 +1,103 @@
<?php
require_once "../config.php";
require_once "../functions.php";
require_once "../includes/check_login.php";
$settings_mail_path = '/admin/settings_mail.php';
if (!isset($session_is_admin) || !$session_is_admin) {
flash_alert("Admin access required.", 'error');
redirect($settings_mail_path);
}
$state = sanitizeInput($_GET['state'] ?? '');
$code = $_GET['code'] ?? '';
$error = sanitizeInput($_GET['error'] ?? '');
$error_description = sanitizeInput($_GET['error_description'] ?? '');
$session_state = $_SESSION['mail_oauth_state'] ?? '';
$session_state_expires = intval($_SESSION['mail_oauth_state_expires_at'] ?? 0);
unset($_SESSION['mail_oauth_state'], $_SESSION['mail_oauth_state_expires_at']);
if (!empty($error)) {
$msg = "Microsoft OAuth authorization failed: $error";
if (!empty($error_description)) {
$msg .= " ($error_description)";
}
flash_alert($msg, 'error');
redirect($settings_mail_path);
}
if (empty($state) || empty($code) || empty($session_state) || !hash_equals($session_state, $state) || time() > $session_state_expires) {
flash_alert("Microsoft OAuth callback validation failed. Please try connecting again.", 'error');
redirect($settings_mail_path);
}
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_tenant_id)) {
flash_alert("Microsoft OAuth settings are incomplete. Please fill Client ID, Client Secret, and Tenant ID.", 'error');
redirect($settings_mail_path);
}
if (defined('BASE_URL') && !empty(BASE_URL)) {
$base_url = rtrim((string) BASE_URL, '/');
} else {
$base_url = 'https://' . rtrim((string) $config_base_url, '/');
}
$redirect_uri = $base_url . '/admin/oauth_microsoft_mail_callback.php';
$token_url = 'https://login.microsoftonline.com/' . rawurlencode($config_mail_oauth_tenant_id) . '/oauth2/v2.0/token';
$scope = 'offline_access openid profile https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send';
$ch = curl_init($token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'client_id' => $config_mail_oauth_client_id,
'client_secret' => $config_mail_oauth_client_secret,
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $redirect_uri,
'scope' => $scope,
], '', '&'));
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$raw_body = curl_exec($ch);
$curl_err = curl_error($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($raw_body === false || $http_code < 200 || $http_code >= 300) {
$reason = !empty($curl_err) ? $curl_err : "HTTP $http_code";
flash_alert("Microsoft OAuth token exchange failed: $reason", 'error');
redirect($settings_mail_path);
}
$json = json_decode($raw_body, true);
if (!is_array($json) || empty($json['refresh_token']) || empty($json['access_token'])) {
flash_alert("Microsoft OAuth token exchange failed: refresh token or access token missing.", 'error');
redirect($settings_mail_path);
}
$refresh_token = (string) $json['refresh_token'];
$access_token = (string) $json['access_token'];
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
$refresh_token_esc = mysqli_real_escape_string($mysqli, $refresh_token);
$access_token_esc = mysqli_real_escape_string($mysqli, $access_token);
$expires_at_esc = mysqli_real_escape_string($mysqli, $expires_at);
mysqli_query($mysqli, "UPDATE settings SET
config_imap_provider = 'microsoft_oauth',
config_smtp_provider = 'microsoft_oauth',
config_mail_oauth_refresh_token = '$refresh_token_esc',
config_mail_oauth_access_token = '$access_token_esc',
config_mail_oauth_access_token_expires_at = '$expires_at_esc'
WHERE company_id = 1
");
logAction("Settings", "Edit", "$session_name completed Microsoft OAuth connect flow for mail settings");
flash_alert("Microsoft OAuth connected successfully. Token expires at $expires_at.");
redirect($settings_mail_path);

View File

@@ -16,7 +16,7 @@ $num_rows = mysqli_num_rows($sql);
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-credit-card mr-2"></i>Payment Methods</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-credit-card mr-2"></i>Payment Methods</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addPaymentMethodModal"><i class="fas fa-plus mr-2"></i>Add Payment Method</button> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/payment_method/payment_method_add.php"><i class="fas fa-plus mr-2"></i>Add Payment Method</button>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -45,7 +45,7 @@ $num_rows = mysqli_num_rows($sql);
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$payment_method_id = intval($row['payment_method_id']); $payment_method_id = intval($row['payment_method_id']);
$payment_method_name = nullable_htmlentities($row['payment_method_name']); $payment_method_name = nullable_htmlentities($row['payment_method_name']);
$payment_method_description = nullable_htmlentities($row['payment_method_description']); $payment_method_description = nullable_htmlentities($row['payment_method_description']);
@@ -98,5 +98,4 @@ $num_rows = mysqli_num_rows($sql);
</div> </div>
<?php <?php
require_once "modals/payment_method/payment_method_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -21,7 +21,7 @@ $num_rows = mysqli_num_rows($sql);
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-credit-card mr-2"></i>Payment Providers</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-credit-card mr-2"></i>Payment Providers</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addPaymentProviderModal"><i class="fas fa-plus mr-2"></i>Add Provider</button> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/payment_provider/payment_provider_add.php"><i class="fas fa-plus mr-2"></i>Add Provider</button>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -57,7 +57,7 @@ $num_rows = mysqli_num_rows($sql);
<th> <th>
<a class="text-dark">Expensed Fee</a> <a class="text-dark">Expensed Fee</a>
</th> </th>
<th> <th class="text-center">
<a class="text-dark">Saved Payment Methods</a> <a class="text-dark">Saved Payment Methods</a>
</th> </th>
<th class="text-center">Action</th> <th class="text-center">Action</th>
@@ -66,13 +66,13 @@ $num_rows = mysqli_num_rows($sql);
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$provider_id = intval($row['payment_provider_id']); $provider_id = intval($row['payment_provider_id']);
$provider_name = nullable_htmlentities($row['payment_provider_name']); $provider_name = nullable_htmlentities($row['payment_provider_name']);
$provider_description = nullable_htmlentities($row['payment_provider_description']); $provider_description = nullable_htmlentities($row['payment_provider_description']);
$account_name = nullable_htmlentities($row['account_name']); $account_name = nullable_htmlentities($row['account_name']);
$threshold = floatval($row['payment_provider_threshold']); $threshold = floatval($row['payment_provider_threshold']);
$vendor_name = nullable_htmlentities($row['vendor_name']); $vendor_name = nullable_htmlentities($row['vendor_name'] ?? "Expense Disabled");
$category = nullable_htmlentities($row['category_name']); $category = nullable_htmlentities($row['category_name']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100; $percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']); $flat_fee = floatval($row['payment_provider_expense_flat_fee']);
@@ -94,7 +94,9 @@ $num_rows = mysqli_num_rows($sql);
<td><?php echo $vendor_name; ?></td> <td><?php echo $vendor_name; ?></td>
<td><?php echo $category; ?></td> <td><?php echo $category; ?></td>
<td><?php echo $percent_fee; ?>% + <?php echo numfmt_format_currency($currency_format, $flat_fee, $session_company_currency); ?></td> <td><?php echo $percent_fee; ?>% + <?php echo numfmt_format_currency($currency_format, $flat_fee, $session_company_currency); ?></td>
<td><?php echo $saved_payment_count; ?></td> <td class="text-center">
<a class="badge badge-dark badge-pill p-2" href="saved_payment_method.php"><?= $saved_payment_count ?></a>
</td>
<td> <td>
<div class="dropdown dropleft text-center"> <div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown"> <button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
@@ -106,12 +108,14 @@ $num_rows = mysqli_num_rows($sql);
<i class="fas fa-fw fa-edit mr-2"></i>Edit <i class="fas fa-fw fa-edit mr-2"></i>Edit
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<!-- <a class="dropdown-item text-danger confirm-link" href="post.php?disable_payment_provider=--><?php //echo $provider_id; ?><!--&csrf_token=--><?php //echo $_SESSION['csrf_token'] ?><!--">--> <a class="dropdown-item text-danger confirm-link" href="post.php?delete_payment_provider=<?= $provider_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<!-- <i class="fas fa-fw fa-thumbs-down mr-2"></i>Disable--> <i class="fas fa-fw fa-trash mr-2"></i><strong>Delete Provider and</strong>
<!-- </a>--> <ul class="text-xs">
<!-- <a class="dropdown-item text-danger confirm-link" href="post.php?delete_payment_provider=--><?php //echo $provider_id; ?><!--&csrf_token=--><?php //echo $_SESSION['csrf_token'] ?><!--">--> <li>Related Recurring Payments</li>
<!-- <i class="fas fa-fw fa-trash mr-2"></i>Delete--> <li>Related Saved cards</li>
<!-- </a>--> <li>Client Provider Relations</li>
</ul>
</a>
</div> </div>
</div> </div>
</td> </td>
@@ -135,5 +139,4 @@ $num_rows = mysqli_num_rows($sql);
</div> </div>
<?php <?php
require_once "modals/payment_provider/payment_provider_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -36,7 +36,4 @@ if (isset($session_is_admin) && $session_is_admin) {
require_once "../post/logout.php"; require_once "../post/logout.php";
// TODO: Find a home for these // TODO: Find a home for these
require_once "../post/ai.php";
require_once "../post/misc.php"; require_once "../post/misc.php";

View File

@@ -31,6 +31,27 @@ if (isset($_POST['add_api_key'])) {
} }
if (isset($_GET['revoke_api_key'])) {
validateCSRFToken($_GET['csrf_token']);
$api_key_id = intval($_GET['revoke_api_key']);
// Get API Key Name
$row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT api_key_name, api_key_client_id FROM api_keys WHERE api_key_id = $api_key_id"));
$api_key_name = sanitizeInput($row['api_key_name']);
$client_id = intval($row['api_key_client_id']);
mysqli_query($mysqli,"UPDATE api_keys SET api_key_expire = NOW() WHERE api_key_id = $api_key_id");
logAction("API Key", "Revoke", "$session_name revoked API key $name", $client_id);
flash_alert("API Key <strong>$name</strong> revoked", 'error');
redirect();
}
if (isset($_GET['delete_api_key'])) { if (isset($_GET['delete_api_key'])) {
validateCSRFToken($_GET['csrf_token']); validateCSRFToken($_GET['csrf_token']);
@@ -38,7 +59,7 @@ if (isset($_GET['delete_api_key'])) {
$api_key_id = intval($_GET['delete_api_key']); $api_key_id = intval($_GET['delete_api_key']);
// Get API Key Name // Get API Key Name
$row = mysqli_fetch_array(mysqli_query($mysqli,"SELECT api_key_name, api_key_client_id FROM api_keys WHERE api_key_id = $api_key_id")); $row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT api_key_name, api_key_client_id FROM api_keys WHERE api_key_id = $api_key_id"));
$api_key_name = sanitizeInput($row['api_key_name']); $api_key_name = sanitizeInput($row['api_key_name']);
$client_id = intval($row['api_key_client_id']); $client_id = intval($row['api_key_client_id']);
@@ -66,7 +87,7 @@ if (isset($_POST['bulk_delete_api_keys'])) {
$api_key_id = intval($api_key_id); $api_key_id = intval($api_key_id);
// Get API Key Name // Get API Key Name
$row = mysqli_fetch_array(mysqli_query($mysqli,"SELECT api_key_name, api_key_client_id FROM api_keys WHERE api_key_id = $api_key_id")); $row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT api_key_name, api_key_client_id FROM api_keys WHERE api_key_id = $api_key_id"));
$api_key_name = sanitizeInput($row['api_key_name']); $api_key_name = sanitizeInput($row['api_key_name']);
$client_id = intval($row['api_key_client_id']); $client_id = intval($row['api_key_client_id']);

View File

@@ -2,36 +2,170 @@
/* /*
* ITFlow - GET/POST request handler for DB / master key backup * ITFlow - GET/POST request handler for DB / master key backup
* Rewritten with streaming SQL dump, component checksums, safer zipping, and better headers.
*/ */
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
require_once "../includes/app_version.php"; require_once "../includes/app_version.php";
if (isset($_GET['download_backup'])) { // --- Optional performance levers for big backups ---
@set_time_limit(0);
if (function_exists('ini_set')) {
@ini_set('memory_limit', '1024M');
}
validateCSRFToken($_GET['csrf_token']); /**
* Write a line to a file handle with newline.
*/
function fwrite_ln($fh, string $s): void {
fwrite($fh, $s);
fwrite($fh, PHP_EOL);
}
$timestamp = date('YmdHis'); /**
$baseName = "itflow_$timestamp"; * Stream a SQL dump of schema and data into $sqlFile.
* - Tables first (DROP + CREATE + INSERTs)
* - Views (DROP VIEW + CREATE VIEW)
* - Triggers (DROP TRIGGER + CREATE TRIGGER)
*
* NOTE: Routines/events are not dumped here. Add if needed.
*/
function dump_database_streaming(mysqli $mysqli, string $sqlFile): void {
$fh = fopen($sqlFile, 'wb');
if (!$fh) {
http_response_code(500);
exit("Cannot open dump file");
}
// === 0. Scoped cleanup === // Preamble
$cleanupFiles = []; fwrite_ln($fh, "-- UTF-8 + Foreign Key Safe Dump");
fwrite_ln($fh, "SET NAMES 'utf8mb4';");
fwrite_ln($fh, "SET FOREIGN_KEY_CHECKS = 0;");
fwrite_ln($fh, "SET UNIQUE_CHECKS = 0;");
fwrite_ln($fh, "SET AUTOCOMMIT = 0;");
fwrite_ln($fh, "");
$registerTempFileForCleanup = function ($file) use (&$cleanupFiles) { // Gather tables and views
$cleanupFiles[] = $file; $tables = [];
}; $views = [];
register_shutdown_function(function () use (&$cleanupFiles) { $res = $mysqli->query("SHOW FULL TABLES");
foreach ($cleanupFiles as $file) { if (!$res) {
if (is_file($file)) { fclose($fh);
@unlink($file); error_log("MySQL Error (SHOW FULL TABLES): " . $mysqli->error);
http_response_code(500);
exit("Error retrieving tables.");
}
while ($row = $res->fetch_array(MYSQLI_NUM)) {
$name = $row[0];
$type = strtoupper($row[1] ?? '');
if ($type === 'VIEW') {
$views[] = $name;
} else {
$tables[] = $name;
} }
} }
}); $res->close();
// === 1. Local helper function: zipFolder // --- TABLES: structure and data ---
$zipFolder = function ($folderPath, $zipFilePath) { foreach ($tables as $table) {
$createRes = $mysqli->query("SHOW CREATE TABLE `{$mysqli->real_escape_string($table)}`");
if (!$createRes) {
error_log("MySQL Error (SHOW CREATE TABLE $table): " . $mysqli->error);
// continue to next table
continue;
}
$createRow = $createRes->fetch_assoc();
$createSQL = array_values($createRow)[1] ?? '';
$createRes->close();
fwrite_ln($fh, "-- ----------------------------");
fwrite_ln($fh, "-- Table structure for `{$table}`");
fwrite_ln($fh, "-- ----------------------------");
fwrite_ln($fh, "DROP TABLE IF EXISTS `{$table}`;");
fwrite_ln($fh, $createSQL . ";");
fwrite_ln($fh, "");
// Dump data in a streaming fashion
$dataRes = $mysqli->query("SELECT * FROM `{$mysqli->real_escape_string($table)}`", MYSQLI_USE_RESULT);
if ($dataRes) {
$wroteHeader = false;
while ($row = $dataRes->fetch_assoc()) {
if (!$wroteHeader) {
fwrite_ln($fh, "-- Dumping data for table `{$table}`");
$wroteHeader = true;
}
$cols = array_map(fn($c) => '`' . $mysqli->real_escape_string($c) . '`', array_keys($row));
$vals = array_map(
function ($v) use ($mysqli) {
return is_null($v) ? "NULL" : "'" . $mysqli->real_escape_string($v) . "'";
},
array_values($row)
);
fwrite_ln($fh, "INSERT INTO `{$table}` (" . implode(", ", $cols) . ") VALUES (" . implode(", ", $vals) . ");");
}
$dataRes->close();
if ($wroteHeader) fwrite_ln($fh, "");
}
}
// --- VIEWS ---
foreach ($views as $view) {
$escView = $mysqli->real_escape_string($view);
$cRes = $mysqli->query("SHOW CREATE VIEW `{$escView}`");
if ($cRes) {
$row = $cRes->fetch_assoc();
$createView = $row['Create View'] ?? '';
$cRes->close();
fwrite_ln($fh, "-- ----------------------------");
fwrite_ln($fh, "-- View structure for `{$view}`");
fwrite_ln($fh, "-- ----------------------------");
fwrite_ln($fh, "DROP VIEW IF EXISTS `{$view}`;");
// Ensure statement ends with semicolon
if (!str_ends_with($createView, ';')) $createView .= ';';
fwrite_ln($fh, $createView);
fwrite_ln($fh, "");
}
}
// --- TRIGGERS ---
$tRes = $mysqli->query("SHOW TRIGGERS");
if ($tRes) {
while ($t = $tRes->fetch_assoc()) {
$triggerName = $t['Trigger'];
$escTrig = $mysqli->real_escape_string($triggerName);
$crt = $mysqli->query("SHOW CREATE TRIGGER `{$escTrig}`");
if ($crt) {
$row = $crt->fetch_assoc();
$createTrig = $row['SQL Original Statement'] ?? ($row['Create Trigger'] ?? '');
$crt->close();
fwrite_ln($fh, "-- ----------------------------");
fwrite_ln($fh, "-- Trigger for `{$triggerName}`");
fwrite_ln($fh, "-- ----------------------------");
fwrite_ln($fh, "DROP TRIGGER IF EXISTS `{$triggerName}`;");
if (!str_ends_with($createTrig, ';')) $createTrig .= ';';
fwrite_ln($fh, $createTrig);
fwrite_ln($fh, "");
}
}
$tRes->close();
}
// Postamble
fwrite_ln($fh, "SET FOREIGN_KEY_CHECKS = 1;");
fwrite_ln($fh, "SET UNIQUE_CHECKS = 1;");
fwrite_ln($fh, "COMMIT;");
fclose($fh);
}
/**
* Zip a folder to $zipFilePath, skipping symlinks and dot-entries.
*/
function zipFolderStrict(string $folderPath, string $zipFilePath): void {
$zip = new ZipArchive(); $zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) { if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
error_log("Failed to open zip file: $zipFilePath"); error_log("Failed to open zip file: $zipFilePath");
@@ -39,30 +173,57 @@ if (isset($_GET['download_backup'])) {
exit("Internal Server Error: Cannot open zip archive."); exit("Internal Server Error: Cannot open zip archive.");
} }
$folderPath = realpath($folderPath); $folderReal = realpath($folderPath);
if (!$folderPath) { if (!$folderReal || !is_dir($folderReal)) {
error_log("Invalid folder path: $folderPath"); // Create an empty archive if uploads folder doesn't exist yet
http_response_code(500); $zip->close();
exit("Internal Server Error: Invalid folder path."); return;
} }
$files = new RecursiveIteratorIterator( $files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($folderPath), new RecursiveDirectoryIterator($folderReal, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY RecursiveIteratorIterator::LEAVES_ONLY
); );
foreach ($files as $file) { foreach ($files as $file) {
if (!$file->isDir()) { /** @var SplFileInfo $file */
if ($file->isDir()) continue;
if ($file->isLink()) continue; // skip symlinks
$filePath = $file->getRealPath(); $filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($folderPath) + 1); if ($filePath === false) continue;
$zip->addFile($filePath, $relativePath);
// ensure path is inside the folder boundary
if (strpos($filePath, $folderReal . DIRECTORY_SEPARATOR) !== 0 && $filePath !== $folderReal) {
continue;
} }
$relativePath = substr($filePath, strlen($folderReal) + 1);
$zip->addFile($filePath, $relativePath);
} }
$zip->close(); $zip->close();
}; }
// === 2. Create all temp files if (isset($_GET['download_backup'])) {
validateCSRFToken($_GET['csrf_token']);
$timestamp = date('YmdHis');
$baseName = "itflow_{$timestamp}";
$downloadName = $baseName . ".zip";
// === Scoped cleanup of temp files ===
$cleanupFiles = [];
$registerTempFileForCleanup = function ($file) use (&$cleanupFiles) {
$cleanupFiles[] = $file;
};
register_shutdown_function(function () use (&$cleanupFiles) {
foreach ($cleanupFiles as $file) {
if (is_file($file)) { @unlink($file); }
}
});
// === Create temp files ===
$sqlFile = tempnam(sys_get_temp_dir(), $baseName . "_sql_"); $sqlFile = tempnam(sys_get_temp_dir(), $baseName . "_sql_");
$uploadsZip = tempnam(sys_get_temp_dir(), $baseName . "_uploads_"); $uploadsZip = tempnam(sys_get_temp_dir(), $baseName . "_uploads_");
$versionFile = tempnam(sys_get_temp_dir(), $baseName . "_version_"); $versionFile = tempnam(sys_get_temp_dir(), $baseName . "_version_");
@@ -70,117 +231,75 @@ if (isset($_GET['download_backup'])) {
foreach ([$sqlFile, $uploadsZip, $versionFile, $finalZip] as $f) { foreach ([$sqlFile, $uploadsZip, $versionFile, $finalZip] as $f) {
$registerTempFileForCleanup($f); $registerTempFileForCleanup($f);
chmod($f, 0600); @chmod($f, 0600);
} }
// === 3. Generate SQL Dump // === Generate SQL Dump (streaming) ===
$sqlContent = "-- UTF-8 + Foreign Key Safe Dump\n"; dump_database_streaming($mysqli, $sqlFile);
$sqlContent .= "SET NAMES 'utf8mb4';\n";
$sqlContent .= "SET foreign_key_checks = 0;\n\n";
$tables = []; // === Zip the uploads folder (strict) ===
$res = $mysqli->query("SHOW TABLES"); zipFolderStrict("../uploads", $uploadsZip);
if (!$res) {
error_log("MySQL Error: " . $mysqli->error);
exit("Error retrieving tables.");
}
while ($row = $res->fetch_row()) { // === Gather metadata & checksums ===
$tables[] = $row[0]; $commitHash = (function_exists('shell_exec') ? trim(shell_exec('git log -1 --format=%H 2>/dev/null')) : '') ?: 'N/A';
} $gitBranch = (function_exists('shell_exec') ? trim(shell_exec('git rev-parse --abbrev-ref HEAD 2>/dev/null')) : '') ?: 'N/A';
foreach ($tables as $table) { $dbSha = hash_file('sha256', $sqlFile) ?: 'N/A';
$createRes = $mysqli->query("SHOW CREATE TABLE `$table`"); $upSha = hash_file('sha256', $uploadsZip) ?: 'N/A';
if (!$createRes) {
error_log("MySQL Error: " . $mysqli->error);
continue;
}
$createRow = $createRes->fetch_assoc();
$createSQL = array_values($createRow)[1];
$sqlContent .= "\n-- ----------------------------\n";
$sqlContent .= "-- Table structure for `$table`\n";
$sqlContent .= "-- ----------------------------\n";
$sqlContent .= "DROP TABLE IF EXISTS `$table`;\n";
$sqlContent .= $createSQL . ";\n\n";
$dataRes = $mysqli->query("SELECT * FROM `$table`");
if ($dataRes && $dataRes->num_rows > 0) {
$sqlContent .= "-- Dumping data for table `$table`\n";
while ($row = $dataRes->fetch_assoc()) {
$columns = array_map(fn($col) => '`' . $mysqli->real_escape_string($col) . '`', array_keys($row));
$values = array_map(function ($val) use ($mysqli) {
return is_null($val) ? "NULL" : "'" . $mysqli->real_escape_string($val) . "'";
}, array_values($row));
$sqlContent .= "INSERT INTO `$table` (" . implode(", ", $columns) . ") VALUES (" . implode(", ", $values) . ");\n";
}
$sqlContent .= "\n";
}
}
$sqlContent .= "SET foreign_key_checks = 1;\n";
file_put_contents($sqlFile, $sqlContent);
// === 4. Zip the uploads folder
$zipFolder("../uploads", $uploadsZip);
// === 5. Create version.txt
$commitHash = trim(shell_exec('git log -1 --format=%H')) ?: 'N/A';
$gitBranch = trim(shell_exec('git rev-parse --abbrev-ref HEAD')) ?: 'N/A';
$versionContent = "ITFlow Backup Metadata\n"; $versionContent = "ITFlow Backup Metadata\n";
$versionContent .= "-----------------------------\n"; $versionContent .= "-----------------------------\n";
$versionContent .= "Generated: " . date('Y-m-d H:i:s') . "\n"; $versionContent .= "Generated: " . date('Y-m-d H:i:s') . "\n";
$versionContent .= "Backup File: " . basename($finalZip) . "\n"; $versionContent .= "Backup File: " . $downloadName . "\n";
$versionContent .= "Generated By: $session_name\n"; $versionContent .= "Generated By: " . ($session_name ?? 'Unknown User') . "\n";
$versionContent .= "Host: " . gethostname() . "\n"; $versionContent .= "Host: " . gethostname() . "\n";
$versionContent .= "Git Branch: $gitBranch\n"; $versionContent .= "Git Branch: $gitBranch\n";
$versionContent .= "Git Commit: $commitHash\n"; $versionContent .= "Git Commit: $commitHash\n";
$versionContent .= "ITFlow Version: " . (defined('APP_VERSION') ? APP_VERSION : 'Unknown') . "\n"; $versionContent .= "ITFlow Version: " . (defined('APP_VERSION') ? APP_VERSION : 'Unknown') . "\n";
$versionContent .= "Database Version: " . (defined('CURRENT_DATABASE_VERSION') ? CURRENT_DATABASE_VERSION : 'Unknown') . "\n"; $versionContent .= "Database Version: " . (defined('CURRENT_DATABASE_VERSION') ? CURRENT_DATABASE_VERSION : 'Unknown') . "\n";
$versionContent .= "Checksum (SHA256): \n"; $versionContent .= "Checksums (SHA256):\n";
$versionContent .= " db.sql: $dbSha\n";
$versionContent .= " uploads.zip: $upSha\n";
file_put_contents($versionFile, $versionContent); file_put_contents($versionFile, $versionContent);
@chmod($versionFile, 0600);
// === 6. Build final ZIP // === Build final ZIP ===
$final = new ZipArchive(); $final = new ZipArchive();
if ($final->open($finalZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) { if ($final->open($finalZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
error_log("Failed to create final zip: $finalZip"); error_log("Failed to create final zip: $finalZip");
http_response_code(500); http_response_code(500);
exit("Internal Server Error: Unable to create backup archive."); exit("Internal Server Error: Unable to create backup archive.");
} }
$final->addFile($sqlFile, "db.sql"); $final->addFile($sqlFile, "db.sql");
$final->addFile($uploadsZip, "uploads.zip"); $final->addFile($uploadsZip, "uploads.zip");
$final->addFile($versionFile, "version.txt"); $final->addFile($versionFile, "version.txt");
$final->close(); $final->close();
chmod($finalZip, 0600); @chmod($finalZip, 0600);
$checksum = hash_file('sha256', $finalZip); // === Serve final ZIP with a stable filename ===
file_put_contents($versionFile, $versionContent . "$checksum\n");
// === 7. Serve final ZIP
header('Content-Type: application/zip'); header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . basename($finalZip) . '"'); header('X-Content-Type-Options: nosniff');
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
header('Content-Length: ' . filesize($finalZip)); header('Content-Length: ' . filesize($finalZip));
header('Pragma: public'); header('Pragma: public');
header('Expires: 0'); header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Transfer-Encoding: binary'); header('Content-Transfer-Encoding: binary');
// Push file
flush(); flush();
$fp = fopen($finalZip, 'rb'); $fp = fopen($finalZip, 'rb');
fpassthru($fp); fpassthru($fp);
fclose($fp); fclose($fp);
logAction("System", "Backup Download", "$session_name downloaded full backup."); // Log + UX
logAction("System", "Backup Download", ($session_name ?? 'Unknown User') . " downloaded full backup.");
flash_alert("Full backup downloaded."); flash_alert("Full backup downloaded.");
exit; exit;
} }
if (isset($_POST['backup_master_key'])) { if (isset($_POST['backup_master_key'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
@@ -188,7 +307,7 @@ if (isset($_POST['backup_master_key'])) {
$password = $_POST['password']; $password = $_POST['password'];
$sql = mysqli_query($mysqli, "SELECT * FROM users WHERE user_id = $session_user_id"); $sql = mysqli_query($mysqli, "SELECT * FROM users WHERE user_id = $session_user_id");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
if (password_verify($password, $row['user_password'])) { if (password_verify($password, $row['user_password'])) {
$site_encryption_master_key = decryptUserSpecificKey($row['user_specific_encryption_ciphertext'], $password); $site_encryption_master_key = decryptUserSpecificKey($row['user_specific_encryption_ciphertext'], $password);
@@ -210,4 +329,3 @@ if (isset($_POST['backup_master_key'])) {
redirect(); redirect();
} }
} }

View File

@@ -8,6 +8,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_category'])) { if (isset($_POST['add_category'])) {
validateCSRFToken($_POST['csrf_token']);
require_once 'category_model.php'; require_once 'category_model.php';
mysqli_query($mysqli,"INSERT INTO categories SET category_name = '$name', category_type = '$type', category_color = '$color'"); mysqli_query($mysqli,"INSERT INTO categories SET category_name = '$name', category_type = '$type', category_color = '$color'");
@@ -24,6 +26,8 @@ if (isset($_POST['add_category'])) {
if (isset($_POST['edit_category'])) { if (isset($_POST['edit_category'])) {
validateCSRFToken($_POST['csrf_token']);
require_once 'category_model.php'; require_once 'category_model.php';
$category_id = intval($_POST['category_id']); $category_id = intval($_POST['category_id']);
@@ -40,11 +44,13 @@ if (isset($_POST['edit_category'])) {
if (isset($_GET['archive_category'])) { if (isset($_GET['archive_category'])) {
validateCSRFToken($_GET['csrf_token']);
$category_id = intval($_GET['archive_category']); $category_id = intval($_GET['archive_category']);
// Get Category Name and Type for logging // Get Category Name and Type for logging
$sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id"); $sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$category_name = sanitizeInput($row['category_name']); $category_name = sanitizeInput($row['category_name']);
$category_type = sanitizeInput($row['category_type']); $category_type = sanitizeInput($row['category_type']);
@@ -58,21 +64,23 @@ if (isset($_GET['archive_category'])) {
} }
if (isset($_GET['unarchive_category'])) { if (isset($_GET['restore_category'])) {
$category_id = intval($_GET['unarchive_category']); validateCSRFToken($_GET['csrf_token']);
$category_id = intval($_GET['retore_category']);
// Get Category Name and Type for logging // Get Category Name and Type for logging
$sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id"); $sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$category_name = sanitizeInput($row['category_name']); $category_name = sanitizeInput($row['category_name']);
$category_type = sanitizeInput($row['category_type']); $category_type = sanitizeInput($row['category_type']);
mysqli_query($mysqli,"UPDATE categories SET category_archived_at = NULL WHERE category_id = $category_id"); mysqli_query($mysqli,"UPDATE categories SET category_archived_at = NULL WHERE category_id = $category_id");
logAction("Category", "Unarchive", "$session_name unarchived category $category_type $category_name", 0, $category_id); logAction("Category", "Restore", "$session_name retored category $category_type $category_name", 0, $category_id);
flash_alert("Category $category_type <strong>$category_name</strong> unarchived"); flash_alert("Category $category_type <strong>$category_name</strong> restored");
redirect(); redirect();
@@ -80,11 +88,13 @@ if (isset($_GET['unarchive_category'])) {
if (isset($_GET['delete_category'])) { if (isset($_GET['delete_category'])) {
validateCSRFToken($_GET['csrf_token']);
$category_id = intval($_GET['delete_category']); $category_id = intval($_GET['delete_category']);
// Get Category Name and Type for logging // Get Category Name and Type for logging
$sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id"); $sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$category_name = sanitizeInput($row['category_name']); $category_name = sanitizeInput($row['category_name']);
$category_type = sanitizeInput($row['category_type']); $category_type = sanitizeInput($row['category_type']);

View File

@@ -0,0 +1,157 @@
<?php
/*
* ITFlow - GET/POST request handler for Contract Templates
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_contract_template'])) {
// Sanitize text inputs
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
$type = sanitizeInput($_POST['type']);
$renewal_frequency = sanitizeInput($_POST['renewal_frequency']);
$support_hours = sanitizeInput($_POST['support_hours']);
$details = mysqli_escape_string($mysqli, $_POST['details']);
// Numeric fields cast to integer
$sla_low_resp = intval($_POST['sla_low_response_time']);
$sla_med_resp = intval($_POST['sla_medium_response_time']);
$sla_high_resp = intval($_POST['sla_high_response_time']);
$sla_low_res = intval($_POST['sla_low_resolution_time']);
$sla_med_res = intval($_POST['sla_medium_resolution_time']);
$sla_high_res = intval($_POST['sla_high_resolution_time']);
$rate_standard = intval($_POST['rate_standard']);
$rate_after_hours = intval($_POST['hourly_rate_after_hours']);
$net_terms = intval($_POST['net_terms']);
// Insert into database (numbers not quoted)
mysqli_query($mysqli, "
INSERT INTO contract_templates SET
contract_template_name = '$name',
contract_template_description = '$description',
contract_template_details = '$details',
contract_template_type = '$type',
contract_template_renewal_frequency = '$renewal_frequency',
contract_template_sla_low_response_time = $sla_low_resp,
contract_template_sla_medium_response_time = $sla_med_resp,
contract_template_sla_high_response_time = $sla_high_resp,
contract_template_sla_low_resolution_time = $sla_low_res,
contract_template_sla_medium_resolution_time = $sla_med_res,
contract_template_sla_high_resolution_time = $sla_high_res,
contract_template_rate_standard = $rate_standard,
contract_template_rate_after_hours = $rate_after_hours,
contract_template_support_hours = '$support_hours',
contract_template_net_terms = $net_terms
");
$contract_template_id = mysqli_insert_id($mysqli);
// Log action
logAction("Contract Template", "Create", "$session_name created contract template $name", 0, $contract_template_id);
// Flash message
flash_alert("Contract Template <strong>$name</strong> created");
// Redirect back
redirect();
}
if (isset($_POST['edit_contract_template'])) {
$contract_template_id = intval($_POST['contract_template_id']);
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
$type = sanitizeInput($_POST['type']);
$renewal_frequency= sanitizeInput($_POST['renewal_frequency']);
$support_hours = sanitizeInput($_POST['support_hours']);
$details = mysqli_escape_string($mysqli, $_POST['details']);
$sla_low_resp = intval($_POST['sla_low_response_time']);
$sla_med_resp = intval($_POST['sla_medium_response_time']);
$sla_high_resp = intval($_POST['sla_high_response_time']);
$sla_low_res = intval($_POST['sla_low_resolution_time']);
$sla_med_res = intval($_POST['sla_medium_resolution_time']);
$sla_high_res = intval($_POST['sla_high_resolution_time']);
$rate_standard = intval($_POST['rate_standard']);
$rate_after_hours = intval($_POST['rate_after_hours']);
$net_terms = intval($_POST['net_terms']);
mysqli_query($mysqli, "
UPDATE contract_templates SET
contract_template_name = '$name',
contract_template_description = '$description',
contract_template_details = '$details',
contract_template_type = '$type',
contract_template_renewal_frequency = '$renewal_frequency',
contract_template_sla_low_response_time = $sla_low_resp,
contract_template_sla_medium_response_time = $sla_med_resp,
contract_template_sla_high_response_time = $sla_high_resp,
contract_template_sla_low_resolution_time = $sla_low_res,
contract_template_sla_medium_resolution_time = $sla_med_res,
contract_template_sla_high_resolution_time = $sla_high_res,
contract_template_rate_standard = $rate_standard,
contract_template_rate_after_hours = $rate_after_hours,
contract_template_support_hours = '$support_hours',
contract_template_net_terms = $net_terms
WHERE contract_template_id = $contract_template_id
");
// Log action
logAction("Contract Template", "Update", "$session_name updated contract template $name", 0, $contract_template_id);
// Flash + redirect
flash_alert("Contract Template <strong>$name</strong> updated");
redirect();
}
if (isset($_GET['archive_contract_template'])) {
$contract_template_id = intval($_GET['archive_contract_template']);
$name = getFieldById('contract_templates', $contract_template_id, 'contract_template_name');
mysqli_query($mysqli, "
UPDATE contract_templates SET contract_template_archived_at = NOW()
WHERE contract_template_id = $contract_template_id
LIMIT 1
");
logAction("Contract Template", "Archive", "$session_name archived contract template $name", 0, $contract_template_id);
flash_alert("Contract Template <strong>$name</strong> archived", "danger");
redirect();
}
if (isset($_GET['restore_contract_template'])) {
$contract_template_id = intval($_GET['restore_contract_template']);
$name = getFieldById('contract_templates', $contract_template_id, 'contract_template_name');
mysqli_query($mysqli, "
UPDATE contract_templates SET contract_template_archived_at = NULL
WHERE contract_template_id = $contract_template_id
LIMIT 1
");
logAction("Contract Template", "Restore", "$session_name restored contract template $name", 0, $contract_template_id);
flash_alert("Contract Template <strong>$name</strong> restored");
redirect();
}
if (isset($_GET['delete_contract_template'])) {
$contract_template_id = intval($_GET['delete_contract_template']);
$name = getFieldById('contract_templates', $contract_template_id, 'contract_template_name');
mysqli_query($mysqli, "
DELETE FROM contract_templates
WHERE contract_template_id = $contract_template_id
LIMIT 1
");
logAction("Contract Template", "Delete", "$session_name deleted contract template $name", 0, $contract_template_id);
flash_alert("Contract Template <strong>$name</strong> deleted", "danger");
redirect();
}
?>

View File

@@ -8,6 +8,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_custom_link'])) { if (isset($_POST['add_custom_link'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$uri = sanitizeInput($_POST['uri']); $uri = sanitizeInput($_POST['uri']);
$new_tab = intval($_POST['new_tab'] ?? 0); $new_tab = intval($_POST['new_tab'] ?? 0);
@@ -29,6 +31,8 @@ if (isset($_POST['add_custom_link'])) {
if (isset($_POST['edit_custom_link'])) { if (isset($_POST['edit_custom_link'])) {
validateCSRFToken($_POST['csrf_token']);
$custom_link_id = intval($_POST['custom_link_id']); $custom_link_id = intval($_POST['custom_link_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$uri = sanitizeInput($_POST['uri']); $uri = sanitizeInput($_POST['uri']);
@@ -49,11 +53,13 @@ if (isset($_POST['edit_custom_link'])) {
if (isset($_GET['delete_custom_link'])) { if (isset($_GET['delete_custom_link'])) {
validateCSRFToken($_GET['csrf_token']);
$custom_link_id = intval($_GET['delete_custom_link']); $custom_link_id = intval($_GET['delete_custom_link']);
// Get Custom Link name and uri for logging // Get Custom Link name and uri for logging
$sql = mysqli_query($mysqli,"SELECT custom_link_name, custom_link_uri FROM custom_links WHERE custom_link_id = $custom_link_id"); $sql = mysqli_query($mysqli,"SELECT custom_link_name, custom_link_uri FROM custom_links WHERE custom_link_id = $custom_link_id");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$custom_link_name = sanitizeInput($row['custom_link_name']); $custom_link_name = sanitizeInput($row['custom_link_name']);
$custom_link_uri = sanitizeInput($row['custom_link_uri']); $custom_link_uri = sanitizeInput($row['custom_link_uri']);

View File

@@ -6,14 +6,28 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_document_template'])) { if (isset($_POST['add_document_template'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
$content = mysqli_real_escape_string($mysqli,$_POST['content']);
mysqli_query($mysqli,"INSERT INTO document_templates SET document_template_name = '$name', document_template_description = '$description', document_template_content = '$content', document_template_created_by = $session_user_id"); mysqli_query($mysqli,"INSERT INTO document_templates SET document_template_name = '$name', document_template_description = '$description', document_template_content = '', document_template_created_by = $session_user_id");
$document_template_id = mysqli_insert_id($mysqli); $document_template_id = mysqli_insert_id($mysqli);
$processed_content = mysqli_escape_string(
$mysqli,
saveBase64Images(
$_POST['content'],
$_SERVER['DOCUMENT_ROOT'] . "/uploads/document_templates/",
"uploads/document_templates/",
$document_template_id
)
);
// Document template update content
mysqli_query($mysqli,"UPDATE document_templates SET document_template_content = '$processed_content' WHERE document_template_id = $document_template_id");
logAction("Document Template", "Create", "$session_name created document template $name", 0, $document_template_id); logAction("Document Template", "Create", "$session_name created document template $name", 0, $document_template_id);
flash_alert("Document template <strong>$name</strong> created"); flash_alert("Document template <strong>$name</strong> created");
@@ -24,13 +38,30 @@ if (isset($_POST['add_document_template'])) {
if (isset($_POST['edit_document_template'])) { if (isset($_POST['edit_document_template'])) {
validateCSRFToken($_POST['csrf_token']);
$document_template_id = intval($_POST['document_template_id']); $document_template_id = intval($_POST['document_template_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
$content = mysqli_real_escape_string($mysqli,$_POST['content']);
$processed_content = saveBase64Images(
$_POST['content'],
$_SERVER['DOCUMENT_ROOT'] . "/uploads/document_templates/",
"uploads/document_templates/",
$document_template_id
);
$processed_content_escaped = mysqli_escape_string($mysqli, $processed_content);
// CLEAN UP unused images
cleanupUnusedImages(
$processed_content,
$_SERVER['DOCUMENT_ROOT'] . "/uploads/document_templates/" . $document_template_id,
"/uploads/document_templates/" . $document_template_id
);
// Document edit query // Document edit query
mysqli_query($mysqli,"UPDATE document_templates SET document_template_name = '$name', document_template_description = '$description', document_template_content = '$content', document_template_updated_by = $session_user_id WHERE document_template_id = $document_template_id"); mysqli_query($mysqli,"UPDATE document_templates SET document_template_name = '$name', document_template_description = '$description', document_template_content = '$processed_content_escaped', document_template_updated_by = $session_user_id WHERE document_template_id = $document_template_id");
logAction("Document Template", "Edit", "$session_name edited document template $name", 0, $document_template_id); logAction("Document Template", "Edit", "$session_name edited document template $name", 0, $document_template_id);
@@ -42,12 +73,17 @@ if (isset($_POST['edit_document_template'])) {
if (isset($_GET['delete_document_template'])) { if (isset($_GET['delete_document_template'])) {
validateCSRFToken($_GET['csrf_token']);
$document_template_id = intval($_GET['delete_document_template']); $document_template_id = intval($_GET['delete_document_template']);
$document_template_name = sanitizeInput(getFieldById('document_templates', $document_template_id, 'document_template_name')); $document_template_name = sanitizeInput(getFieldById('document_templates', $document_template_id, 'document_template_name'));
mysqli_query($mysqli,"DELETE FROM document_templates WHERE document_template_id = $document_template_id"); mysqli_query($mysqli,"DELETE FROM document_templates WHERE document_template_id = $document_template_id");
// Delete uploads/document_templates/$document_template_id if exists
removeDirectory($_SERVER['DOCUMENT_ROOT'] . "/uploads/document_templates/" . $document_template_id);
logAction("Document Template", "Delete", "$session_name deleted document template $document_template_name"); logAction("Document Template", "Delete", "$session_name deleted document template $document_template_name");
flash_alert("Document Template <strong>$document_template_name</strong> deleted", 'error'); flash_alert("Document Template <strong>$document_template_name</strong> deleted", 'error');

View File

@@ -4,6 +4,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_GET['send_failed_mail'])) { if (isset($_GET['send_failed_mail'])) {
validateCSRFToken($_GET['csrf_token']);
$email_id = intval($_GET['send_failed_mail']); $email_id = intval($_GET['send_failed_mail']);
mysqli_query($mysqli,"UPDATE email_queue SET email_status = 0, email_attempts = 3 WHERE email_id = $email_id"); mysqli_query($mysqli,"UPDATE email_queue SET email_status = 0, email_attempts = 3 WHERE email_id = $email_id");
@@ -18,6 +20,8 @@ if (isset($_GET['send_failed_mail'])) {
if (isset($_GET['cancel_mail'])) { if (isset($_GET['cancel_mail'])) {
validateCSRFToken($_GET['csrf_token']);
$email_id = intval($_GET['cancel_mail']); $email_id = intval($_GET['cancel_mail']);
mysqli_query($mysqli,"UPDATE email_queue SET email_status = 2, email_attempts = 99, email_failed_at = NOW() WHERE email_id = $email_id"); mysqli_query($mysqli,"UPDATE email_queue SET email_status = 2, email_attempts = 99, email_failed_at = NOW() WHERE email_id = $email_id");

View File

@@ -10,10 +10,17 @@ if (isset($_POST['add_payment_method'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = cleanInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = cleanInput($_POST['description']);
mysqli_query($mysqli,"INSERT INTO payment_methods SET payment_method_name = '$name', payment_method_description = '$description'"); $query = mysqli_prepare(
$mysqli, "INSERT INTO payment_methods
SET payment_method_name = ?, payment_method_description = ?"
);
mysqli_stmt_bind_param($query, "ss", $name, $description);
mysqli_stmt_execute($query);
logAction("Payment Method", "Create", "$session_name created Payment Method $name"); logAction("Payment Method", "Create", "$session_name created Payment Method $name");
@@ -28,10 +35,19 @@ if (isset($_POST['edit_payment_method'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
$payment_method_id = intval($_POST['payment_method_id']); $payment_method_id = intval($_POST['payment_method_id']);
$name = sanitizeInput($_POST['name']); $name = cleanInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = cleanInput($_POST['description']);
mysqli_query($mysqli,"UPDATE payment_methods SET payment_method_name = '$name', payment_method_description = '$description' WHERE payment_method_id = $payment_method_id"); $query = mysqli_prepare(
$mysqli,
"UPDATE payment_methods
SET payment_method_name = ?, payment_method_description = ?
WHERE payment_method_id = ?"
);
mysqli_stmt_bind_param($query, "ssi", $name, $description, $payment_method_id);
mysqli_stmt_execute($query);
logAction("Payment Method", "Edit", "$session_name edited Payment Method $name"); logAction("Payment Method", "Edit", "$session_name edited Payment Method $name");
@@ -43,6 +59,8 @@ if (isset($_POST['edit_payment_method'])) {
if (isset($_GET['delete_payment_method'])) { if (isset($_GET['delete_payment_method'])) {
validateCSRFToken($_GET['csrf_token']);
$payment_method_id = intval($_GET['delete_payment_method']); $payment_method_id = intval($_GET['delete_payment_method']);
$payment_method_name = sanitizeInput(getFieldById('payment_methods', $payment_method_is, 'payment_method_name')); $payment_method_name = sanitizeInput(getFieldById('payment_methods', $payment_method_is, 'payment_method_name'));

View File

@@ -14,53 +14,20 @@ if (isset($_POST['add_payment_provider'])) {
$public_key = sanitizeInput($_POST['public_key']); $public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']); $private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']); $threshold = floatval($_POST['threshold']);
$enable_expense = intval($_POST['enable_expense'] ?? 0); $account = intval($_POST['account']);
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
$expense_category = intval($_POST['expense_category']) ?? 0;
$percentage_fee = floatval($_POST['percentage_fee']) / 100 ?? 0; $percentage_fee = floatval($_POST['percentage_fee']) / 100 ?? 0;
$flat_fee = floatval($_POST['flat_fee']) ?? 0; $flat_fee = floatval($_POST['flat_fee']) ?? 0;
// Check to ensure provider isn't added twice // Check to ensure provider isn't added twice
$sql = "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1"; $sql = mysqli_query($mysqli, "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1");
$result = mysqli_query($mysqli, $sql); if (mysqli_num_rows($sql) > 0) {
if (mysqli_num_rows($result) > 0) {
flash_alert("Payment Provider <strong>$provider</strong> already exists", 'error'); flash_alert("Payment Provider <strong>$provider</strong> already exists", 'error');
redirect(); redirect();
} }
// Check for Stripe Account, if not create it mysqli_query($mysqli,"INSERT INTO payment_providers SET payment_provider_name = '$provider', payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_account = $account, payment_provider_expense_vendor = $expense_vendor, payment_provider_expense_category = $expense_category, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee");
$sql_account = mysqli_query($mysqli,"SELECT account_id FROM accounts WHERE account_name = '$provider' AND account_archived_at IS NULL LIMIT 1");
if (mysqli_num_rows($sql_account) == 0) {
$account_id = mysqli_insert_id($mysqli);
} else {
$row = mysqli_fetch_array($sql_account);
$account_id = intval($row['account_id']);
}
// Expense defaults
$category_id = 0;
$vendor_id = 0;
if ($enable_expense) {
// Category
$sql_category = mysqli_query($mysqli,"SELECT category_id FROM categories WHERE category_name = 'Payment Processing' AND category_type = 'Expense' AND category_archived_at IS NULL LIMIT 1");
if (mysqli_num_rows($sql_category) == 0) {
mysqli_query($mysqli,"INSERT INTO categories SET category_name = 'Processing Fee', category_type = 'Payment Processing', category_color = 'gray'");
$category_id = mysqli_insert_id($mysqli);
} else {
$row = mysqli_fetch_array($sql_category);
$category_id = intval($row['category_id']);
}
// Vendor
$sql_vendor = mysqli_query($mysqli,"SELECT vendor_id FROM vendors WHERE vendor_name = '$provider' AND vendor_client_id = 0 AND vendor_archived_at IS NULL LIMIT 1");
if (mysqli_num_rows($sql_vendor) == 0) {
mysqli_query($mysqli,"INSERT INTO vendors SET vendor_name = '$provider', vendor_description = 'Payment Processor Provider', vendor_client_id = 0");
$vendor_id = mysqli_insert_id($mysqli);
} else {
$row = mysqli_fetch_array($sql_vendor);
$vendor_id = intval($row['vendor_id']);
}
}
mysqli_query($mysqli,"INSERT INTO payment_providers SET payment_provider_name = '$provider', payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_account = $account_id, payment_provider_expense_vendor = $vendor_id, payment_provider_expense_category = $category_id, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee");
$provider_id = mysqli_insert_id($mysqli); $provider_id = mysqli_insert_id($mysqli);
@@ -81,11 +48,13 @@ if (isset($_POST['edit_payment_provider'])) {
$public_key = sanitizeInput($_POST['public_key']); $public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']); $private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']); $threshold = floatval($_POST['threshold']);
$enable_expense = intval($_POST['enable_expense'] ?? 0); $account = intval($_POST['account']);
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
$expense_category = intval($_POST['expense_category']) ?? 0;
$percentage_fee = floatval($_POST['percentage_fee']) / 100; $percentage_fee = floatval($_POST['percentage_fee']) / 100;
$flat_fee = floatval($_POST['flat_fee']); $flat_fee = floatval($_POST['flat_fee']);
mysqli_query($mysqli,"UPDATE payment_providers SET payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee WHERE payment_provider_id = $provider_id"); mysqli_query($mysqli,"UPDATE payment_providers SET payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_account = $account, payment_provider_expense_vendor = $expense_vendor, payment_provider_expense_category = $expense_category, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee WHERE payment_provider_id = $provider_id");
logAction("Payment Provider", "Edit", "$session_name edited Payment Provider $provider"); logAction("Payment Provider", "Edit", "$session_name edited Payment Provider $provider");
@@ -101,6 +70,11 @@ if (isset($_GET['delete_payment_provider'])) {
$provider_id = intval($_GET['delete_payment_provider']); $provider_id = intval($_GET['delete_payment_provider']);
// When deleted it cascades deletes
// all Recurring paymentes related to payment provider
// Delete all Saved Cards related
// Delete Client Payment Provider Releation
$provider_name = sanitizeInput(getFieldById('payment_providers', $provider_id, 'provider_name')); $provider_name = sanitizeInput(getFieldById('payment_providers', $provider_id, 'provider_name'));
// Delete provider // Delete provider

View File

@@ -4,6 +4,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_project_template'])) { if (isset($_POST['add_project_template'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
@@ -21,6 +23,8 @@ if (isset($_POST['add_project_template'])) {
if (isset($_POST['edit_project_template'])) { if (isset($_POST['edit_project_template'])) {
validateCSRFToken($_POST['csrf_token']);
$project_template_id = intval($_POST['project_template_id']); $project_template_id = intval($_POST['project_template_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
@@ -37,6 +41,8 @@ if (isset($_POST['edit_project_template'])) {
if (isset($_POST['edit_ticket_template_order'])) { if (isset($_POST['edit_ticket_template_order'])) {
validateCSRFToken($_POST['csrf_token']);
$ticket_template_id = intval($_POST['ticket_template_id']); $ticket_template_id = intval($_POST['ticket_template_id']);
$project_template_id = intval($_POST['project_template_id']); $project_template_id = intval($_POST['project_template_id']);
$order = intval($_POST['order']); $order = intval($_POST['order']);
@@ -49,6 +55,8 @@ if (isset($_POST['edit_ticket_template_order'])) {
if (isset($_POST['add_ticket_template_to_project_template'])) { if (isset($_POST['add_ticket_template_to_project_template'])) {
validateCSRFToken($_POST['csrf_token']);
$project_template_id = intval($_POST['project_template_id']); $project_template_id = intval($_POST['project_template_id']);
$ticket_template_id = intval($_POST['ticket_template_id']); $ticket_template_id = intval($_POST['ticket_template_id']);
$order = intval($_POST['order']); $order = intval($_POST['order']);
@@ -65,7 +73,8 @@ if (isset($_POST['add_ticket_template_to_project_template'])) {
if (isset($_POST['remove_ticket_template_from_project_template'])) { if (isset($_POST['remove_ticket_template_from_project_template'])) {
validateTechRole(); validateCSRFToken($_POST['csrf_token']);
$ticket_template_id = intval($_POST['ticket_template_id']); $ticket_template_id = intval($_POST['ticket_template_id']);
$project_template_id = intval($_POST['project_template_id']); $project_template_id = intval($_POST['project_template_id']);
@@ -81,6 +90,8 @@ if (isset($_POST['remove_ticket_template_from_project_template'])) {
if (isset($_GET['delete_project_template'])) { if (isset($_GET['delete_project_template'])) {
validateCSRFToken($_GET['csrf_token']);
$project_template_id = intval($_GET['delete_project_template']); $project_template_id = intval($_GET['delete_project_template']);
$project_template_name = sanitizeInput(getFieldById('project_templates', $project_template_id, 'project_template_name')); $project_template_name = sanitizeInput(getFieldById('project_templates', $project_template_id, 'project_template_name'));

View File

@@ -18,9 +18,23 @@ if (isset($_POST['add_role'])) {
$role_id = mysqli_insert_id($mysqli); $role_id = mysqli_insert_id($mysqli);
// Insert role permissions (only if not admin)
if ($admin == 0) {
foreach ($_POST as $key => $value) {
if (str_contains($key, '##module_')) {
$module_id = intval(explode('##', $key)[0]);
$access_level = intval($value);
if ($access_level > 0) {
mysqli_query($mysqli, "INSERT INTO user_role_permissions SET user_role_id = $role_id, module_id = $module_id, user_role_permission_level = $access_level");
}
}
}
}
logAction("User Role", "Create", "$session_name created user role $name", 0, $role_id); logAction("User Role", "Create", "$session_name created user role $name", 0, $role_id);
flash_alert("User Role <strong$name</strong> created"); flash_alert("User Role <strong>$name</strong> created");
redirect(); redirect();
@@ -76,7 +90,7 @@ if (isset($_GET['archive_role'])) {
mysqli_query($mysqli, "UPDATE user_roles SET role_archived_at = NOW() WHERE role_id = $role_id"); mysqli_query($mysqli, "UPDATE user_roles SET role_archived_at = NOW() WHERE role_id = $role_id");
$role_name = sanitizeInput(getFieldById('roles', $role_id, 'role_name')); $role_name = sanitizeInput(getFieldById('user_roles', $role_id, 'role_name'));
logAction("User Role", "Archive", "$session_name archived user role $role_name", 0, $role_id); logAction("User Role", "Archive", "$session_name archived user role $role_name", 0, $role_id);

View File

@@ -27,7 +27,7 @@ if (isset($_GET['delete_saved_payment'])) {
WHERE client_saved_payment_methods.saved_payment_id = $saved_payment_id" WHERE client_saved_payment_methods.saved_payment_id = $saved_payment_id"
); );
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$client_id = intval($row['saved_payment_client_id']); $client_id = intval($row['saved_payment_client_id']);
$provider_id = intval($row['saved_payment_provider_id']); $provider_id = intval($row['saved_payment_provider_id']);
$payment_provider_name = nullable_htmlentities($row['payment_provider_name']); $payment_provider_name = nullable_htmlentities($row['payment_provider_name']);
@@ -42,7 +42,7 @@ if (isset($_GET['delete_saved_payment'])) {
try { try {
// Initialize stripe // Initialize stripe
require_once 'plugins/stripe-php/init.php'; require_once '../plugins/stripe-php/init.php';
$stripe = new \Stripe\StripeClient($private_key); $stripe = new \Stripe\StripeClient($private_key);
// Detach PM // Detach PM
@@ -56,7 +56,7 @@ if (isset($_GET['delete_saved_payment'])) {
} }
// Remove payment method from ITFlow // Remove payment method from ITFlow. This will also cascade delete related recurring payments setup
mysqli_query($mysqli, "DELETE FROM client_saved_payment_methods WHERE saved_payment_id = $saved_payment_id"); mysqli_query($mysqli, "DELETE FROM client_saved_payment_methods WHERE saved_payment_id = $saved_payment_id");
// SQL Cascade delete will Remove All Associated Auto Payment Methods on recurring invoices in the recurring payments table. // SQL Cascade delete will Remove All Associated Auto Payment Methods on recurring invoices in the recurring payments table.

View File

@@ -19,7 +19,7 @@ if (isset($_POST['edit_company'])) {
$tax_id = sanitizeInput($_POST['tax_id']); $tax_id = sanitizeInput($_POST['tax_id']);
$sql = mysqli_query($mysqli,"SELECT company_logo FROM companies WHERE company_id = 1"); $sql = mysqli_query($mysqli,"SELECT company_logo FROM companies WHERE company_id = 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$existing_file_name = sanitizeInput($row['company_logo']); $existing_file_name = sanitizeInput($row['company_logo']);
// Company logo // Company logo
@@ -54,8 +54,10 @@ if (isset($_POST['edit_company'])) {
if (isset($_GET['remove_company_logo'])) { if (isset($_GET['remove_company_logo'])) {
validateCSRFToken($_GET['csrf_token']);
$sql = mysqli_query($mysqli,"SELECT company_logo FROM companies"); $sql = mysqli_query($mysqli,"SELECT company_logo FROM companies");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$company_logo = $row['company_logo']; // FileSystem Operation Logo is already sanitized $company_logo = $row['company_logo']; // FileSystem Operation Logo is already sanitized
unlink("../uploads/settings/$company_logo"); unlink("../uploads/settings/$company_logo");

View File

@@ -2,11 +2,84 @@
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (!defined('MICROSOFT_OAUTH_BASE_URL')) {
define('MICROSOFT_OAUTH_BASE_URL', 'https://login.microsoftonline.com/');
}
if (isset($_POST['oauth_connect_microsoft_mail'])) {
validateCSRFToken($_POST['csrf_token']);
// Save current IMAP/OAuth form values first so auth flow always uses latest inputs.
$config_imap_provider = sanitizeInput($_POST['config_imap_provider'] ?? '');
$config_imap_username = sanitizeInput($_POST['config_imap_username'] ?? '');
$config_mail_oauth_client_id = sanitizeInput($_POST['config_mail_oauth_client_id'] ?? '');
$config_mail_oauth_client_secret = sanitizeInput($_POST['config_mail_oauth_client_secret'] ?? '');
$config_mail_oauth_tenant_id = sanitizeInput($_POST['config_mail_oauth_tenant_id'] ?? '');
$config_mail_oauth_refresh_token = sanitizeInput($_POST['config_mail_oauth_refresh_token'] ?? '');
$config_mail_oauth_access_token = sanitizeInput($_POST['config_mail_oauth_access_token'] ?? '');
mysqli_query($mysqli, "UPDATE settings SET
config_imap_provider = '$config_imap_provider',
config_imap_username = '$config_imap_username',
config_mail_oauth_client_id = '$config_mail_oauth_client_id',
config_mail_oauth_client_secret = '$config_mail_oauth_client_secret',
config_mail_oauth_tenant_id = '$config_mail_oauth_tenant_id',
config_mail_oauth_refresh_token = '$config_mail_oauth_refresh_token',
config_mail_oauth_access_token = '$config_mail_oauth_access_token'
WHERE company_id = 1
");
if ($config_imap_provider !== 'microsoft_oauth') {
flash_alert("Please set IMAP Provider to Microsoft 365 (OAuth) before connecting.", 'error');
redirect();
}
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_tenant_id)) {
flash_alert("Missing Microsoft OAuth settings. Please provide Client ID, Client Secret, and Tenant ID first.", 'error');
redirect();
}
if (defined('BASE_URL') && !empty(BASE_URL)) {
$base_url = rtrim((string) BASE_URL, '/');
} else {
$base_url = 'https://' . rtrim((string) $config_base_url, '/');
}
$redirect_uri = $base_url . '/admin/oauth_microsoft_mail_callback.php';
try {
$state = bin2hex(random_bytes(32));
} catch (Throwable $e) {
$state = sha1(uniqid((string) mt_rand(), true));
}
$_SESSION['mail_oauth_state'] = $state;
$_SESSION['mail_oauth_state_expires_at'] = time() + 600;
$scope = 'offline_access openid profile https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send';
$authorize_url = MICROSOFT_OAUTH_BASE_URL . rawurlencode($config_mail_oauth_tenant_id) . '/oauth2/v2.0/authorize?'
. http_build_query([
'client_id' => $config_mail_oauth_client_id,
'response_type' => 'code',
'redirect_uri' => $redirect_uri,
'response_mode' => 'query',
'scope' => $scope,
'state' => $state,
'prompt' => 'consent',
], '', '&', PHP_QUERY_RFC3986);
logAction("Settings", "Edit", "$session_name started Microsoft OAuth connect flow for mail settings");
redirect($authorize_url);
}
if (isset($_POST['edit_mail_smtp_settings'])) { if (isset($_POST['edit_mail_smtp_settings'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
$config_smtp_provider = sanitizeInput($_POST['config_smtp_provider'] ?? 'standard_smtp'); $config_smtp_provider = sanitizeInput($_POST['config_smtp_provider']);
$config_smtp_host = sanitizeInput($_POST['config_smtp_host']); $config_smtp_host = sanitizeInput($_POST['config_smtp_host']);
$config_smtp_port = intval($_POST['config_smtp_port'] ?? 0); $config_smtp_port = intval($_POST['config_smtp_port'] ?? 0);
$config_smtp_encryption = sanitizeInput($_POST['config_smtp_encryption']); $config_smtp_encryption = sanitizeInput($_POST['config_smtp_encryption']);
@@ -22,7 +95,7 @@ if (isset($_POST['edit_mail_smtp_settings'])) {
mysqli_query($mysqli, " mysqli_query($mysqli, "
UPDATE settings SET UPDATE settings SET
config_smtp_provider = " . ($config_smtp_provider === 'none' ? "NULL" : "'$config_smtp_provider'") . ", config_smtp_provider = '$config_smtp_provider',
config_smtp_host = '$config_smtp_host', config_smtp_host = '$config_smtp_host',
config_smtp_port = $config_smtp_port, config_smtp_port = $config_smtp_port,
config_smtp_encryption = '$config_smtp_encryption', config_smtp_encryption = '$config_smtp_encryption',
@@ -48,7 +121,7 @@ if (isset($_POST['edit_mail_imap_settings'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
$config_imap_provider = sanitizeInput($_POST['config_imap_provider'] ?? 'standard_imap'); $config_imap_provider = sanitizeInput($_POST['config_imap_provider']);
$config_imap_host = sanitizeInput($_POST['config_imap_host']); $config_imap_host = sanitizeInput($_POST['config_imap_host']);
$config_imap_port = intval($_POST['config_imap_port'] ?? 0); $config_imap_port = intval($_POST['config_imap_port'] ?? 0);
$config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']); $config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']);
@@ -64,7 +137,7 @@ if (isset($_POST['edit_mail_imap_settings'])) {
mysqli_query($mysqli, " mysqli_query($mysqli, "
UPDATE settings SET UPDATE settings SET
config_imap_provider = " . ($config_imap_provider === 'none' ? "NULL" : "'$config_imap_provider'") . ", config_imap_provider = '$config_imap_provider',
config_imap_host = '$config_imap_host', config_imap_host = '$config_imap_host',
config_imap_port = $config_imap_port, config_imap_port = $config_imap_port,
config_imap_encryption = '$config_imap_encryption', config_imap_encryption = '$config_imap_encryption',
@@ -150,7 +223,7 @@ if (isset($_POST['test_email_smtp'])) {
$mail = addToMailQueue($data); $mail = addToMailQueue($data);
if ($mail === true) { if ($mail === true) {
flash_alert("Test email queued! <a class='text-bold text-light' href='admin_mail_queue.php'>Check Admin > Mail queue</a>"); flash_alert("Test email queued! <a class='text-bold text-light' href='mail_queue.php'>Check Admin > Mail queue</a>");
} else { } else {
flash_alert("Failed to add test mail to queue", 'error'); flash_alert("Failed to add test mail to queue", 'error');
} }
@@ -163,24 +236,347 @@ if (isset($_POST['test_email_imap'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
// Setup your IMAP connection parameters $provider = sanitizeInput($config_imap_provider ?? '');
$hostname = "{" . $config_imap_host . ":" . $config_imap_port . "/" . $config_imap_encryption . "/novalidate-cert}INBOX";
$host = $config_imap_host;
$port = (int) $config_imap_port;
$encryption = strtolower(trim($config_imap_encryption)); // e.g. "ssl", "tls", "none"
$username = $config_imap_username; $username = $config_imap_username;
$password = $config_imap_password; $password = $config_imap_password;
try { // Shared OAuth fields
$inbox = @imap_open($hostname, $username, $password); $config_mail_oauth_client_id = $config_mail_oauth_client_id ?? '';
$config_mail_oauth_client_secret = $config_mail_oauth_client_secret ?? '';
$config_mail_oauth_tenant_id = $config_mail_oauth_tenant_id ?? '';
$config_mail_oauth_refresh_token = $config_mail_oauth_refresh_token ?? '';
$config_mail_oauth_access_token = $config_mail_oauth_access_token ?? '';
$config_mail_oauth_access_token_expires_at = $config_mail_oauth_access_token_expires_at ?? '';
if ($inbox) { $is_oauth = ($provider === 'google_oauth' || $provider === 'microsoft_oauth');
imap_close($inbox);
flash_alert("Connected successfully"); if ($provider === 'google_oauth') {
} else { if (empty($host)) {
throw new Exception(imap_last_error()); $host = 'imap.gmail.com';
} }
if (empty($port)) {
$port = 993;
}
if (empty($encryption)) {
$encryption = 'ssl';
}
} elseif ($provider === 'microsoft_oauth') {
if (empty($host)) {
$host = 'outlook.office365.com';
}
if (empty($port)) {
$port = 993;
}
if (empty($encryption)) {
$encryption = 'ssl';
}
}
if (empty($host) || empty($port) || empty($username)) {
flash_alert("<strong>IMAP connection failed:</strong> Missing host, port, or username.", 'error');
redirect();
}
$token_is_expired = function (?string $expires_at): bool {
if (empty($expires_at)) {
return true;
}
$ts = strtotime($expires_at);
if ($ts === false) {
return true;
}
return ($ts - 60) <= time();
};
$http_form_post = function (string $url, array $fields): array {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields, '', '&'));
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'ok' => ($raw !== false && $code >= 200 && $code < 300),
'body' => $raw,
'code' => $code,
'err' => $err,
];
};
if ($is_oauth) {
if (!empty($config_mail_oauth_access_token) && !$token_is_expired($config_mail_oauth_access_token_expires_at)) {
$password = $config_mail_oauth_access_token;
} else {
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token)) {
flash_alert("<strong>IMAP OAuth failed:</strong> Missing OAuth client credentials or refresh token.", 'error');
redirect();
}
if ($provider === 'google_oauth') {
$response = $http_form_post('https://oauth2.googleapis.com/token', [
'client_id' => $config_mail_oauth_client_id,
'client_secret' => $config_mail_oauth_client_secret,
'refresh_token' => $config_mail_oauth_refresh_token,
'grant_type' => 'refresh_token',
]);
} else {
if (empty($config_mail_oauth_tenant_id)) {
flash_alert("<strong>IMAP OAuth failed:</strong> Microsoft tenant ID is required.", 'error');
redirect();
}
$token_url = MICROSOFT_OAUTH_BASE_URL . rawurlencode($config_mail_oauth_tenant_id) . "/oauth2/v2.0/token";
$response = $http_form_post($token_url, [
'client_id' => $config_mail_oauth_client_id,
'client_secret' => $config_mail_oauth_client_secret,
'refresh_token' => $config_mail_oauth_refresh_token,
'grant_type' => 'refresh_token',
]);
}
if (!$response['ok']) {
flash_alert("<strong>IMAP OAuth failed:</strong> Could not refresh access token.", 'error');
redirect();
}
$json = json_decode($response['body'], true);
if (!is_array($json) || empty($json['access_token'])) {
flash_alert("<strong>IMAP OAuth failed:</strong> Token response did not include an access token.", 'error');
redirect();
}
$password = $json['access_token'];
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
$refresh_token_to_save = $json['refresh_token'] ?? null;
$token_esc = mysqli_real_escape_string($mysqli, $password);
$expires_at_esc = mysqli_real_escape_string($mysqli, $expires_at);
$refresh_sql = '';
if (!empty($refresh_token_to_save)) {
$refresh_token_esc = mysqli_real_escape_string($mysqli, $refresh_token_to_save);
$refresh_sql = ", config_mail_oauth_refresh_token = '{$refresh_token_esc}'";
}
mysqli_query($mysqli, "UPDATE settings SET config_mail_oauth_access_token = '{$token_esc}', config_mail_oauth_access_token_expires_at = '{$expires_at_esc}'{$refresh_sql} WHERE company_id = 1");
}
}
// Build remote socket (implicit SSL vs plain TCP)
$transport = 'tcp';
if ($encryption === 'ssl') {
$transport = 'ssl';
}
$remote_socket = $transport . '://' . $host . ':' . $port;
// Stream context (you can tighten these if you want strict validation)
$context_options = [];
if (in_array($encryption, ['ssl', 'tls'], true)) {
$context_options['ssl'] = [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
];
}
$context = stream_context_create($context_options);
try {
$errno = 0;
$errstr = '';
// 10-second timeout, adjust as needed
$fp = @stream_socket_client(
$remote_socket,
$errno,
$errstr,
10,
STREAM_CLIENT_CONNECT,
$context
);
if (!$fp) {
throw new Exception("Could not connect to IMAP server: [$errno] $errstr");
}
stream_set_timeout($fp, 10);
// Read server greeting (IMAP servers send something like: * OK Dovecot ready)
$greeting = fgets($fp, 1024);
if ($greeting === false || strpos($greeting, '* OK') !== 0) {
fclose($fp);
throw new Exception("Invalid IMAP greeting: " . trim((string) $greeting));
}
// If you really want STARTTLS for "tls" (port 143), you can do it here
if ($encryption === 'tls' && stripos($greeting, 'STARTTLS') !== false) {
fwrite($fp, "A0001 STARTTLS\r\n");
$line = fgets($fp, 1024);
if ($line === false || stripos($line, 'A0001 OK') !== 0) {
fclose($fp);
throw new Exception("STARTTLS failed: " . trim((string) $line));
}
if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
fclose($fp);
throw new Exception("Unable to enable TLS encryption on IMAP connection.");
}
}
$tag = 'A0002';
if ($is_oauth) {
$oauth_b64 = base64_encode("user={$username}\x01auth=Bearer {$password}\x01\x01");
$auth_cmd = sprintf("%s AUTHENTICATE XOAUTH2 %s\r\n", $tag, $oauth_b64);
fwrite($fp, $auth_cmd);
} else {
$login_cmd = sprintf(
"%s LOGIN \"%s\" \"%s\"\r\n",
$tag,
addcslashes($username, "\\\""),
addcslashes($password, "\\\"")
);
fwrite($fp, $login_cmd);
}
$success = false;
$error_line = '';
while (!feof($fp)) {
$line = fgets($fp, 2048);
if ($line === false) {
break;
}
if (strpos($line, $tag . ' ') === 0) {
if (stripos($line, $tag . ' OK') === 0) {
$success = true;
} else {
$error_line = trim($line);
}
break;
}
}
// Always logout / close
fwrite($fp, "A0003 LOGOUT\r\n");
fclose($fp);
if ($success) {
if ($is_oauth) {
flash_alert("Connected successfully using OAuth");
} else {
flash_alert("Connected successfully");
}
} else {
if (!$error_line) {
$error_line = 'Unknown IMAP authentication error';
}
throw new Exception($error_line);
}
} catch (Exception $e) { } catch (Exception $e) {
flash_alert("<strong>IMAP connection failed:</strong> " . $e->getMessage(), 'error'); flash_alert("<strong>IMAP connection failed:</strong> " . htmlspecialchars($e->getMessage()), 'error');
} }
redirect(); redirect();
}
if (isset($_POST['test_oauth_token_refresh'])) {
validateCSRFToken($_POST['csrf_token']);
$provider = sanitizeInput($_POST['oauth_provider'] ?? '');
if ($provider !== 'google_oauth' && $provider !== 'microsoft_oauth') {
flash_alert("OAuth token test failed: unsupported provider.", 'error');
redirect();
}
$oauth_client_id = sanitizeInput($config_mail_oauth_client_id ?? '');
$oauth_client_secret = sanitizeInput($config_mail_oauth_client_secret ?? '');
$oauth_tenant_id = sanitizeInput($config_mail_oauth_tenant_id ?? '');
$oauth_refresh_token = sanitizeInput($config_mail_oauth_refresh_token ?? '');
if (empty($oauth_client_id) || empty($oauth_client_secret) || empty($oauth_refresh_token)) {
flash_alert("OAuth token test failed: missing client ID, client secret, or refresh token.", 'error');
redirect();
}
if ($provider === 'microsoft_oauth' && empty($oauth_tenant_id)) {
flash_alert("OAuth token test failed: Microsoft tenant ID is required.", 'error');
redirect();
}
$token_url = 'https://oauth2.googleapis.com/token';
if ($provider === 'microsoft_oauth') {
$token_url = MICROSOFT_OAUTH_BASE_URL . rawurlencode($oauth_tenant_id) . "/oauth2/v2.0/token";
}
$post_fields = http_build_query([
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client_secret,
'refresh_token' => $oauth_refresh_token,
'grant_type' => 'refresh_token',
]);
$ch = curl_init($token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$raw_body = curl_exec($ch);
$curl_err = curl_error($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($raw_body === false || $http_code < 200 || $http_code >= 300) {
$err_msg = !empty($curl_err) ? $curl_err : "HTTP $http_code";
flash_alert("OAuth token test failed: $err_msg", 'error');
redirect();
}
$json = json_decode($raw_body, true);
if (!is_array($json) || empty($json['access_token'])) {
flash_alert("OAuth token test failed: access token missing in provider response.", 'error');
redirect();
}
$new_access_token = sanitizeInput($json['access_token']);
$new_expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
$new_refresh_token = !empty($json['refresh_token']) ? sanitizeInput($json['refresh_token']) : '';
$new_access_token_esc = mysqli_real_escape_string($mysqli, $new_access_token);
$new_expires_at_esc = mysqli_real_escape_string($mysqli, $new_expires_at);
$refresh_sql = '';
if (!empty($new_refresh_token)) {
$new_refresh_token_esc = mysqli_real_escape_string($mysqli, $new_refresh_token);
$refresh_sql = ", config_mail_oauth_refresh_token = '$new_refresh_token_esc'";
}
mysqli_query($mysqli, "UPDATE settings SET config_mail_oauth_access_token = '$new_access_token_esc', config_mail_oauth_access_token_expires_at = '$new_expires_at_esc'$refresh_sql WHERE company_id = 1");
$provider_label = $provider === 'microsoft_oauth' ? 'Microsoft 365' : 'Google Workspace';
logAction("Settings", "Edit", "$session_name tested OAuth token refresh for $provider_label mail settings");
flash_alert("OAuth token refresh successful for $provider_label. Access token expires at $new_expires_at.");
redirect();
} }

View File

@@ -4,6 +4,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_module_settings'])) { if (isset($_POST['edit_module_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_module_enable_itdoc = intval($_POST['config_module_enable_itdoc'] ?? 0); $config_module_enable_itdoc = intval($_POST['config_module_enable_itdoc'] ?? 0);
$config_module_enable_ticketing = intval($_POST['config_module_enable_ticketing'] ?? 0); $config_module_enable_ticketing = intval($_POST['config_module_enable_ticketing'] ?? 0);
$config_module_enable_accounting = intval($_POST['config_module_enable_accounting'] ?? 0); $config_module_enable_accounting = intval($_POST['config_module_enable_accounting'] ?? 0);

View File

@@ -34,7 +34,7 @@ if (isset($_GET['stripe_remove_pm'])) {
// Remove Auto Pay on recurring invoices that are stripe // Remove Auto Pay on recurring invoices that are stripe
$sql_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id FROM recurring_invoices WHERE recurring_invoice_client_id = $client_id"); $sql_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id FROM recurring_invoices WHERE recurring_invoice_client_id = $client_id");
while ($row = mysqli_fetch_array($sql_recurring_invoices)) { while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) {
$recurring_invoice_id = intval($row['recurring_invoice_id']); $recurring_invoice_id = intval($row['recurring_invoice_id']);
mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_method = 'Stripe' AND recurring_payment_recurring_invoice_id = $recurring_invoice_id"); mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_method = 'Stripe' AND recurring_payment_recurring_invoice_id = $recurring_invoice_id");
} }
@@ -59,7 +59,7 @@ if (isset($_GET['stripe_reset_customer'])) {
// Remove Auto Pay on recurring invoices that are stripe // Remove Auto Pay on recurring invoices that are stripe
$sql_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id FROM recurring_invoices WHERE recurring_invoice_client_id = $client_id"); $sql_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id FROM recurring_invoices WHERE recurring_invoice_client_id = $client_id");
while ($row = mysqli_fetch_array($sql_recurring_invoices)) { while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) {
$recurring_invoice_id = intval($row['recurring_invoice_id']); $recurring_invoice_id = intval($row['recurring_invoice_id']);
mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_method = 'Stripe' AND recurring_payment_recurring_invoice_id = $recurring_invoice_id"); mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_method = 'Stripe' AND recurring_payment_recurring_invoice_id = $recurring_invoice_id");
} }

View File

@@ -49,3 +49,19 @@ if (isset($_POST['edit_favicon_settings'])) {
redirect(); redirect();
} }
if (isset($_GET['reset_favicon'])) {
validateCSRFToken($_GET['csrf_token']);
if (file_exists("../uploads/favicon.ico")) {
unlink("../uploads/favicon.ico");
}
logAction("Settings", "Edit", "$session_name reset Favicon");
flash_alert("Favicon reset", 'error');
redirect();
}

View File

@@ -4,6 +4,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_ticket_settings'])) { if (isset($_POST['edit_ticket_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_ticket_prefix = sanitizeInput($_POST['config_ticket_prefix']); $config_ticket_prefix = sanitizeInput($_POST['config_ticket_prefix']);
$config_ticket_next_number = intval($_POST['config_ticket_next_number']); $config_ticket_next_number = intval($_POST['config_ticket_next_number']);
$config_ticket_email_parse = intval($_POST['config_ticket_email_parse'] ?? 0); $config_ticket_email_parse = intval($_POST['config_ticket_email_parse'] ?? 0);

View File

@@ -6,6 +6,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_software_template'])) { if (isset($_POST['add_software_template'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$version = sanitizeInput($_POST['version']); $version = sanitizeInput($_POST['version']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
@@ -27,6 +29,8 @@ if (isset($_POST['add_software_template'])) {
if (isset($_POST['edit_software_template'])) { if (isset($_POST['edit_software_template'])) {
validateCSRFToken($_POST['csrf_token']);
$software_template_id = intval($_POST['software_template_id']); $software_template_id = intval($_POST['software_template_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$version = sanitizeInput($_POST['version']); $version = sanitizeInput($_POST['version']);
@@ -47,11 +51,13 @@ if (isset($_POST['edit_software_template'])) {
if (isset($_GET['delete_software_template'])) { if (isset($_GET['delete_software_template'])) {
validateCSRFToken($_GET['csrf_token']);
$software_template_id = intval($_GET['delete_software_template']); $software_template_id = intval($_GET['delete_software_template']);
// Get Software Template Name for logging and alert message // Get Software Template Name for logging and alert message
$sql = mysqli_query($mysqli,"SELECT software_template_name FROM software_templates WHERE software_template_id = $software_template_id"); $sql = mysqli_query($mysqli,"SELECT software_template_name FROM software_templates WHERE software_template_id = $software_template_id");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$software_template_name = sanitizeInput($row['software_template_name']); $software_template_name = sanitizeInput($row['software_template_name']);
mysqli_query($mysqli,"DELETE FROM software_templates WHERE software_template_id = $software_template_id"); mysqli_query($mysqli,"DELETE FROM software_templates WHERE software_template_id = $software_template_id");

View File

@@ -8,6 +8,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_tag'])) { if (isset($_POST['add_tag'])) {
validateCSRFToken($_POST['csrf_token']);
require_once 'tag_model.php'; require_once 'tag_model.php';
mysqli_query($mysqli,"INSERT INTO tags SET tag_name = '$name', tag_type = $type, tag_color = '$color', tag_icon = '$icon'"); mysqli_query($mysqli,"INSERT INTO tags SET tag_name = '$name', tag_type = $type, tag_color = '$color', tag_icon = '$icon'");
@@ -24,11 +26,13 @@ if (isset($_POST['add_tag'])) {
if (isset($_POST['edit_tag'])) { if (isset($_POST['edit_tag'])) {
validateCSRFToken($_POST['csrf_token']);
require_once 'post/tag_model.php'; require_once 'post/tag_model.php';
$tag_id = intval($_POST['tag_id']); $tag_id = intval($_POST['tag_id']);
mysqli_query($mysqli,"UPDATE tags SET tag_name = '$name', tag_type = $type, tag_color = '$color', tag_icon = '$icon' WHERE tag_id = $tag_id"); mysqli_query($mysqli,"UPDATE tags SET tag_name = '$name', tag_color = '$color', tag_icon = '$icon' WHERE tag_id = $tag_id");
logAction("Tag", "Edit", "$session_name edited tag $name", 0, $tag_id); logAction("Tag", "Edit", "$session_name edited tag $name", 0, $tag_id);
@@ -40,6 +44,8 @@ if (isset($_POST['edit_tag'])) {
if (isset($_GET['delete_tag'])) { if (isset($_GET['delete_tag'])) {
validateCSRFToken($_GET['csrf_token']);
$tag_id = intval($_GET['delete_tag']); $tag_id = intval($_GET['delete_tag']);
$tag_name = sanitizeInput(getFieldById('tags', $tag_id, 'tag_name')); $tag_name = sanitizeInput(getFieldById('tags', $tag_id, 'tag_name'));

View File

@@ -9,6 +9,7 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_tax'])) { if (isset($_POST['add_tax'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$percent = floatval($_POST['percent']); $percent = floatval($_POST['percent']);
@@ -27,6 +28,7 @@ if (isset($_POST['add_tax'])) {
if (isset($_POST['edit_tax'])) { if (isset($_POST['edit_tax'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);
$tax_id = intval($_POST['tax_id']); $tax_id = intval($_POST['tax_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$percent = floatval($_POST['percent']); $percent = floatval($_POST['percent']);
@@ -44,6 +46,7 @@ if (isset($_POST['edit_tax'])) {
if (isset($_GET['archive_tax'])) { if (isset($_GET['archive_tax'])) {
validateCSRFToken($_GET['csrf_token']); validateCSRFToken($_GET['csrf_token']);
$tax_id = intval($_GET['archive_tax']); $tax_id = intval($_GET['archive_tax']);
$tax_name = sanitizeInput(getFieldById('taxes', $tax_id, 'tax_name')); $tax_name = sanitizeInput(getFieldById('taxes', $tax_id, 'tax_name'));
@@ -60,6 +63,8 @@ if (isset($_GET['archive_tax'])) {
if (isset($_GET['delete_tax'])) { if (isset($_GET['delete_tax'])) {
validateCSRFToken($_GET['csrf_token']);
$tax_id = intval($_GET['delete_tax']); $tax_id = intval($_GET['delete_tax']);
$tax_name = sanitizeInput(getFieldById('taxes', $tax_id, 'tax_name')); $tax_name = sanitizeInput(getFieldById('taxes', $tax_id, 'tax_name'));

View File

@@ -4,6 +4,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_ticket_status'])) { if (isset($_POST['add_ticket_status'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$color = sanitizeInput($_POST['color']); $color = sanitizeInput($_POST['color']);
@@ -21,6 +23,8 @@ if (isset($_POST['add_ticket_status'])) {
if (isset($_POST['edit_ticket_status'])) { if (isset($_POST['edit_ticket_status'])) {
validateCSRFToken($_POST['csrf_token']);
$ticket_status_id = intval($_POST['ticket_status_id']); $ticket_status_id = intval($_POST['ticket_status_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$color = sanitizeInput($_POST['color']); $color = sanitizeInput($_POST['color']);

View File

@@ -5,11 +5,13 @@
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
// Import shared code from user-side tickets/tasks as we reuse functions // Import shared code from user-side tickets/tasks as we reuse functions
require_once '../user/post/ticket.php'; require_once '../agent/post/ticket.php';
require_once '../user/post/task.php'; require_once '../agent/post/task.php';
if (isset($_POST['add_ticket_template'])) { if (isset($_POST['add_ticket_template'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
$subject = sanitizeInput($_POST['subject']); $subject = sanitizeInput($_POST['subject']);
@@ -34,6 +36,8 @@ if (isset($_POST['add_ticket_template'])) {
if (isset($_POST['edit_ticket_template'])) { if (isset($_POST['edit_ticket_template'])) {
validateCSRFToken($_POST['csrf_token']);
$ticket_template_id = intval($_POST['ticket_template_id']); $ticket_template_id = intval($_POST['ticket_template_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
@@ -52,6 +56,8 @@ if (isset($_POST['edit_ticket_template'])) {
if (isset($_GET['delete_ticket_template'])) { if (isset($_GET['delete_ticket_template'])) {
validateCSRFToken($_GET['csrf_token']);
$ticket_template_id = intval($_GET['delete_ticket_template']); $ticket_template_id = intval($_GET['delete_ticket_template']);
$ticket_template_name = sanitizeInput(getFieldById('ticket_templates', $ticket_template_id, 'ticket_template_name')); $ticket_template_name = sanitizeInput(getFieldById('ticket_templates', $ticket_template_id, 'ticket_template_name'));
@@ -72,6 +78,8 @@ if (isset($_GET['delete_ticket_template'])) {
if (isset($_POST['add_ticket_template_task'])) { if (isset($_POST['add_ticket_template_task'])) {
validateCSRFToken($_POST['csrf_token']);
$ticket_template_id = intval($_POST['ticket_template_id']); $ticket_template_id = intval($_POST['ticket_template_id']);
$task_name = sanitizeInput($_POST['task_name']); $task_name = sanitizeInput($_POST['task_name']);
@@ -89,6 +97,8 @@ if (isset($_POST['add_ticket_template_task'])) {
if (isset($_GET['delete_task_template'])) { if (isset($_GET['delete_task_template'])) {
validateCSRFToken($_GET['csrf_token']);
$task_template_id = intval($_GET['delete_task_template']); $task_template_id = intval($_GET['delete_task_template']);
$task_template_name = sanitizeInput(getFieldById('tags', $task_template_id, 'task_template_name')); $task_template_name = sanitizeInput(getFieldById('tags', $task_template_id, 'task_template_name'));

View File

@@ -21,7 +21,7 @@ if (isset($_GET['update'])) {
if ($config_telemetry > 0 OR $config_telemetry = 2) { if ($config_telemetry > 0 OR $config_telemetry = 2) {
$sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1"); $sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$company_name = sanitizeInput($row['company_name']); $company_name = sanitizeInput($row['company_name']);
$website = sanitizeInput($row['company_website']); $website = sanitizeInput($row['company_website']);

View File

@@ -53,7 +53,7 @@ if (isset($_POST['add_user'])) {
mysqli_query($mysqli, "INSERT INTO user_settings SET user_id = $user_id, user_config_force_mfa = $force_mfa"); mysqli_query($mysqli, "INSERT INTO user_settings SET user_id = $user_id, user_config_force_mfa = $force_mfa");
$sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1"); $sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$company_name = sanitizeInput($row['company_name']); $company_name = sanitizeInput($row['company_name']);
// Sanitize Config vars from load_global_settings.php // Sanitize Config vars from load_global_settings.php
@@ -118,7 +118,7 @@ if (isset($_POST['edit_user'])) {
// Get current Avatar // Get current Avatar
$sql = mysqli_query($mysqli, "SELECT user_avatar FROM users WHERE user_id = $user_id"); $sql = mysqli_query($mysqli, "SELECT user_avatar FROM users WHERE user_id = $user_id");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$existing_file_name = sanitizeInput($row['user_avatar']); $existing_file_name = sanitizeInput($row['user_avatar']);
$extended_log_description = ''; $extended_log_description = '';
@@ -353,7 +353,7 @@ if (isset($_POST['ir_reset_user_password'])) {
// Confirm logged-in user password, for security // Confirm logged-in user password, for security
$admin_password = $_POST['admin_password']; $admin_password = $_POST['admin_password'];
$sql = mysqli_query($mysqli, "SELECT * FROM users WHERE user_id = $session_user_id"); $sql = mysqli_query($mysqli, "SELECT * FROM users WHERE user_id = $session_user_id");
$userRow = mysqli_fetch_array($sql); $userRow = mysqli_fetch_assoc($sql);
if (!password_verify($admin_password, $userRow['user_password'])) { if (!password_verify($admin_password, $userRow['user_password'])) {
flash_alert("Incorrect password.", 'error'); flash_alert("Incorrect password.", 'error');
@@ -364,7 +364,7 @@ if (isset($_POST['ir_reset_user_password'])) {
$sql_users = mysqli_query($mysqli, "SELECT * FROM users WHERE (user_archived_at IS NULL AND user_id != $session_user_id)"); $sql_users = mysqli_query($mysqli, "SELECT * FROM users WHERE (user_archived_at IS NULL AND user_id != $session_user_id)");
// Reset passwords // Reset passwords
while ($row = mysqli_fetch_array($sql_users)) { while ($row = mysqli_fetch_assoc($sql_users)) {
$user_id = intval($row['user_id']); $user_id = intval($row['user_id']);
$user_email = sanitizeInput($row['user_email']); $user_email = sanitizeInput($row['user_email']);
$new_password = randomString(); $new_password = randomString();

View File

@@ -5,10 +5,12 @@
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
// Import shared code from user-side vendor management as we reuse functions // Import shared code from user-side vendor management as we reuse functions
require_once '../user/post/vendor.php'; require_once '../agent/post/vendor.php';
if (isset($_POST['add_vendor_template'])) { if (isset($_POST['add_vendor_template'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
$account_number = sanitizeInput($_POST['account_number']); $account_number = sanitizeInput($_POST['account_number']);
@@ -37,6 +39,8 @@ if (isset($_POST['add_vendor_template'])) {
if (isset($_POST['edit_vendor_template'])) { if (isset($_POST['edit_vendor_template'])) {
validateCSRFToken($_POST['csrf_token']);
$vendor_template_id = intval($_POST['vendor_template_id']); $vendor_template_id = intval($_POST['vendor_template_id']);
$name = sanitizeInput($_POST['name']); $name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']); $description = sanitizeInput($_POST['description']);
@@ -141,6 +145,8 @@ if (isset($_POST['edit_vendor_template'])) {
if (isset($_GET['delete_vendor_template'])) { if (isset($_GET['delete_vendor_template'])) {
validateCSRFToken($_GET['csrf_token']);
$vendor_template_id = intval($_GET['delete_vendor_template']); $vendor_template_id = intval($_GET['delete_vendor_template']);
$vendor_template_name = sanitizeInput(getFieldById('vendor_templates', $vendor_template_id, 'vendor_template_name')); $vendor_template_name = sanitizeInput(getFieldById('vendor_templates', $vendor_template_id, 'vendor_template_name'));

View File

@@ -22,7 +22,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-project-diagram mr-2"></i>Project Templates</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-project-diagram mr-2"></i>Project Templates</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addProjectTemplateModal"><i class="fas fa-plus mr-2"></i>New Project Template</button> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/project_template/project_template_add.php"><i class="fas fa-plus mr-2"></i>New Project Template</button>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -61,7 +61,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while($row = mysqli_fetch_array($sql)){ while($row = mysqli_fetch_assoc($sql)){
$project_template_id = intval($row['project_template_id']); $project_template_id = intval($row['project_template_id']);
$project_template_name = nullable_htmlentities($row['project_template_name']); $project_template_name = nullable_htmlentities($row['project_template_name']);
$project_template_description = nullable_htmlentities($row['project_template_description']); $project_template_description = nullable_htmlentities($row['project_template_description']);
@@ -87,16 +87,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?> ?>
<tr> <tr>
<td> <td>
<a class="text-dark" href="#" data-toggle="modal" data-target="#editProjectTemplateModal<?php echo $project_template_id; ?>"> <a class="text-dark" href="project_template_details.php?project_template_id=<?= $project_template_id ?>">
<div class="media"> <div class="media">
<i class="fa fa-fw fa-2x fa-project-diagram mr-3"></i> <i class="fa fa-fw fa-2x fa-project-diagram mr-3"></i>
<div class="media-body"> <div class="media-body">
<div> <div>
<a href="project_template_details.php?project_template_id=<?php echo $project_template_id; ?>"> <?= $project_template_name ?>
<?php echo $project_template_name; ?> </div>
</a> <div>
<small class="text-secondary"><?= $project_template_description ?></small>
</div> </div>
<div><small class="text-secondary"><?php echo $project_template_description; ?></small></div>
</div> </div>
</div> </div>
</a> </a>
@@ -109,12 +109,12 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<i class="fas fa-ellipsis-h"></i> <i class="fas fa-ellipsis-h"></i>
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#editProjectTemplateModal<?php echo $project_template_id; ?>"> <a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/project_template/project_template_edit.php?project_template_id=<?= $project_template_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit <i class="fas fa-fw fa-edit mr-2"></i>Edit
</a> </a>
<?php if($session_user_role == 3) { ?> <?php if($session_user_role == 3) { ?>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_project_template=<?php echo $project_template_id; ?>"> <a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_project_template=<?php echo $project_template_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete <i class="fas fa-fw fa-trash mr-2"></i>Delete
</a> </a>
<?php } ?> <?php } ?>
@@ -125,8 +125,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php <?php
require "modals/project_template/project_template_edit.php";
} }
?> ?>
@@ -134,12 +132,9 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</tbody> </tbody>
</table> </table>
</div> </div>
<?php require_once "../includes/filter_footer.php"; <?php require_once "../includes/filter_footer.php"; ?>
?>
</div> </div>
</div> </div>
<?php <?php
require_once "modals/project_template/project_template_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -13,13 +13,13 @@ if (isset($_GET['project_template_id'])) {
); );
if (mysqli_num_rows($sql_project_templates) == 0) { if (mysqli_num_rows($sql_project_templates) == 0) {
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='admin_project_template.php'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>"; echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
include_once "footer.php"; require_once "../includes/footer.php";
exit; exit;
} }
$row = mysqli_fetch_array($sql_project_templates); $row = mysqli_fetch_assoc($sql_project_templates);
$project_template_name = nullable_htmlentities($row['project_template_name']); $project_template_name = nullable_htmlentities($row['project_template_name']);
$project_template_description = nullable_htmlentities($row['project_template_description']); $project_template_description = nullable_htmlentities($row['project_template_description']);
@@ -91,7 +91,7 @@ if (isset($_GET['project_template_id'])) {
<div class="col-sm-2"> <div class="col-sm-2">
<div class="btn-group float-right"> <div class="btn-group float-right">
<button type="button" class="btn btn-primary btn-sm" href="#" data-toggle="modal" data-target="#addProjectTemplateTicketTemplateModal"> <button type="button" class="btn btn-primary btn-sm ajax-modal" href="#" data-modal-url="modals/project_template/project_template_ticket_template_add.php?project_template_id=<?= $project_template_id ?>">
<i class="fas fa-fw fa-plus mr-2"></i>Add Ticket Template <i class="fas fa-fw fa-plus mr-2"></i>Add Ticket Template
</button> </button>
<div class="dropdown dropleft text-center ml-3"> <div class="dropdown dropleft text-center ml-3">
@@ -99,18 +99,18 @@ if (isset($_GET['project_template_id'])) {
<i class="fas fa-fw fa-ellipsis-v"></i> <i class="fas fa-fw fa-ellipsis-v"></i>
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#editProjectTemplateModal<?php echo $project_template_id; ?>"> <a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/project_template/project_template_edit.php?project_template_id=<?= $project_template_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit Template <i class="fas fa-fw fa-edit mr-2"></i>Edit Template
</a> </a>
<?php if ($session_user_role == 3) { ?> <?php if ($session_user_role == 3) { ?>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?archive_project_template=<?php echo $project_template_id; ?>"> <a class="dropdown-item text-danger text-bold confirm-link" href="post.php?archive_project_template=<?php echo $project_template_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Archive (not yet implemented) <i class="fas fa-fw fa-archive mr-2"></i>Archive (not yet implemented)
</a> </a>
<?php } ?> <?php } ?>
<?php if ($session_user_role == 3) { ?> <?php if ($session_user_role == 3) { ?>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_project_template=<?php echo $project_template_id; ?>"> <a class="dropdown-item text-danger confirm-link" href="post.php?delete_project_template=<?php echo $project_template_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete <i class="fas fa-fw fa-trash mr-2"></i>Delete
</a> </a>
<?php } ?> <?php } ?>
@@ -130,10 +130,9 @@ if (isset($_GET['project_template_id'])) {
<h5 class="text-secondary"><i class="fa fa-fw fa-life-ring mr-2"></i>Project Ticket Templates</h5> <h5 class="text-secondary"><i class="fa fa-fw fa-life-ring mr-2"></i>Project Ticket Templates</h5>
<div class="table-responsive-sm"> <div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover"> <table class="table table-striped table-borderless table-hover" id="ticket_templates">
<thead class="text-dark"> <thead class="text-dark">
<tr> <tr>
<th>Order</th>
<th>Template Name</th> <th>Template Name</th>
<th>Description</th> <th>Description</th>
<th>Ticket Subject</th> <th>Ticket Subject</th>
@@ -143,7 +142,7 @@ if (isset($_GET['project_template_id'])) {
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql_ticket_templates)) { while ($row = mysqli_fetch_assoc($sql_ticket_templates)) {
$ticket_template_id = intval($row['ticket_template_id']); $ticket_template_id = intval($row['ticket_template_id']);
$ticket_template_order = intval($row['ticket_template_order']); $ticket_template_order = intval($row['ticket_template_order']);
$ticket_template_name = nullable_htmlentities($row['ticket_template_name']); $ticket_template_name = nullable_htmlentities($row['ticket_template_name']);
@@ -154,16 +153,9 @@ if (isset($_GET['project_template_id'])) {
?> ?>
<tr> <tr data-task-id="<?php echo $ticket_template_id; ?>">
<td class="pr-0">
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="edit_ticket_template_order">
<input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>">
<input type="hidden" name="ticket_template_id" value="<?php echo $ticket_template_id; ?>">
<input type="text" class="form-control pr-0" onchange="this.form.submit()" name="order" value="<?php echo $ticket_template_order; ?>">
</form>
</td>
<td> <td>
<a href="#" class="drag-handle"><i class="fas fa-bars text-muted mr-2"></i></a>
<a href="ticket_template_details.php?ticket_template_id=<?php echo $ticket_template_id; ?>"> <a href="ticket_template_details.php?ticket_template_id=<?php echo $ticket_template_id; ?>">
<?php echo $ticket_template_name; ?> <?php echo $ticket_template_name; ?>
</a> </a>
@@ -172,6 +164,7 @@ if (isset($_GET['project_template_id'])) {
<td><?php echo $ticket_template_subject; ?></td> <td><?php echo $ticket_template_subject; ?></td>
<td> <td>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>"> <input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>">
<input type="hidden" name="ticket_template_id" value="<?php echo $ticket_template_id; ?>"> <input type="hidden" name="ticket_template_id" value="<?php echo $ticket_template_id; ?>">
<button type="submit" class="btn btn-default btn-sm confirm-link" <button type="submit" class="btn btn-default btn-sm confirm-link"
@@ -199,7 +192,7 @@ if (isset($_GET['project_template_id'])) {
<h5 class="text-secondary"><i class="fas fa-fw fa-tasks mr-2"></i>Project Task Templates</h5> <h5 class="text-secondary"><i class="fas fa-fw fa-tasks mr-2"></i>Project Task Templates</h5>
<table class="table"> <table class="table">
<?php <?php
while($row = mysqli_fetch_array($sql_task_templates)){ while($row = mysqli_fetch_assoc($sql_task_templates)){
$task_template_id = intval($row['task_template_id']); $task_template_id = intval($row['task_template_id']);
$task_template_name = nullable_htmlentities($row['task_template_name']); $task_template_name = nullable_htmlentities($row['task_template_name']);
?> ?>
@@ -219,10 +212,29 @@ if (isset($_GET['project_template_id'])) {
</div> <!-- End row --> </div> <!-- End row -->
<?php <script src="../plugins/SortableJS/Sortable.min.js"></script>
<script>
new Sortable(document.querySelector('table#ticket_templates tbody'), {
handle: '.drag-handle',
animation: 150,
onEnd: function (evt) {
const rows = document.querySelectorAll('table#ticket_templates tbody tr');
const positions = Array.from(rows).map((row, index) => ({
id: row.dataset.taskId,
order: index
}));
require_once "modals/project_template/project_template_edit.php"; $.post('/agent/ajax.php', {
require_once "modals/project_template/project_template_ticket_template_add.php"; update_project_template_ticket_order: true,
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
project_template_id: <?php echo $project_template_id; ?>,
positions: positions
});
}
});
</script>
<?php
} }

View File

@@ -17,14 +17,13 @@ $sql = mysqli_query(
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?> ?>
<div class="alert alert-info text-center"><strong>Roles are still in development. Permissions may not be fully enforced.</strong></div>
<div class="card card-dark"> <div class="card card-dark">
<div class="card-header py-2"> <div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-user-shield mr-2"></i>Roles</h3> <h3 class="card-title mt-2"><i class="fas fa-fw fa-user-shield mr-2"></i>Roles</h3>
<div class="card-tools"> <div class="card-tools">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addRoleModal"> <button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/role/role_add.php">
<i class="fas fa-fw fa-user-plus mr-2"></i>New Role <i class="fas fa-fw fa-user-plus mr-2"></i>New Role
</button> </button>
</div> </div>
@@ -65,7 +64,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$role_id = intval($row['role_id']); $role_id = intval($row['role_id']);
$role_name = nullable_htmlentities($row['role_name']); $role_name = nullable_htmlentities($row['role_name']);
$role_description = nullable_htmlentities($row['role_description']); $role_description = nullable_htmlentities($row['role_description']);
@@ -95,10 +94,15 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?> ?>
<tr> <tr>
<td> <td>
<a href="#" <?php if ($role_id !== 3) { ?> class="ajax-modal" data-modal-url="modals/role/role_edit.php?id=<?= $role_id ?>" <?php } ?>> <a class="<?php if ($role_id !== 3) { ?> ajax-modal" data-modal-url="modals/role/role_edit.php?id=<?= $role_id ?>" <?php } ?> href="#">
<strong class="text-dark"><?php echo $role_name; ?></strong> <div class="media">
<i class="fas fa-fw fa-2x fa-user-shield text-dark mr-2"></i>
<div class="media-body">
<div><?= $role_name ?></div>
<div><small class="text-secondary"><?= $role_description ?></small></div>
</div>
</div>
</a> </a>
<div class="text-secondary"><?php echo $role_description; ?></div>
</td> </td>
<td><?php echo $user_names_string; ?></td> <td><?php echo $user_names_string; ?></td>
<td><?php echo $role_admin ? 'Yes' : 'No' ; ?></td> <td><?php echo $role_admin ? 'Yes' : 'No' ; ?></td>
@@ -143,6 +147,4 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div> </div>
<?php <?php
require_once "modals/role/role_add.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -33,6 +33,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?> ?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="/admin">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="payment_provider.php">Payment Providers</a>
</li>
<li class="breadcrumb-item active">Saved Payment Methods (Stripe)</li>
</ol>
<div class="card card-dark"> <div class="card card-dark">
<div class="card-header"> <div class="card-header">
<h3 class="card-title"><i class="fas fa-fw fa-credit-card mr-2"></i>Saved Payment Methods</h3> <h3 class="card-title"><i class="fas fa-fw fa-credit-card mr-2"></i>Saved Payment Methods</h3>
@@ -94,7 +104,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$saved_payment_id = intval($row['saved_payment_id']); $saved_payment_id = intval($row['saved_payment_id']);
$client_id = intval($row['saved_payment_client_id']); $client_id = intval($row['saved_payment_client_id']);
$client_name = nullable_htmlentities($row['client_name']); $client_name = nullable_htmlentities($row['client_name']);
@@ -107,8 +117,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?> ?>
<tr> <tr>
<td><?php echo $client_name; ?> (<?php echo $client_id; ?>)</td> <td>
<td><?php echo $provider_name; ?> (<?php echo $provider_id; ?>)</td> <?= $client_name ?>
<br>
<small class="text-secondary">ID: <?= $client_id ?></small>
</td>
<td>
<?= $provider_name ?>
<br>
<small class="text-secondary">ID: <?= $provider_id ?></small>
</td>
<td><?php echo $saved_payment_description; ?></td> <td><?php echo $saved_payment_description; ?></td>
<td><?php echo $provider_client; ?></td> <td><?php echo $provider_client; ?></td>
<td><?php echo $provider_payment_method; ?></td> <td><?php echo $provider_payment_method; ?></td>

View File

@@ -4,7 +4,7 @@ require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli,"SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $sql = mysqli_query($mysqli,"SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_assoc($sql);
$company_id = intval($row['company_id']); $company_id = intval($row['company_id']);
$company_name = nullable_htmlentities($row['company_name']); $company_name = nullable_htmlentities($row['company_name']);
$company_country = nullable_htmlentities($row['company_country']); $company_country = nullable_htmlentities($row['company_country']);
@@ -37,7 +37,7 @@ $company_initials = nullable_htmlentities(initials($company_name));
<div class="col-md-3 text-center"> <div class="col-md-3 text-center">
<?php if ($company_logo) { ?> <?php if ($company_logo) { ?>
<img class="img-thumbnail" src="<?php echo "../uploads/settings/$company_logo"; ?>"> <img class="img-thumbnail" src="<?php echo "../uploads/settings/$company_logo"; ?>">
<a href="post.php?remove_company_logo" class="btn btn-outline-danger btn-block">Remove Logo</a> <a href="post.php?remove_company_logo&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-outline-danger btn-block">Remove Logo</a>
<hr> <hr>
<?php } ?> <?php } ?>
<div class="form-group"> <div class="form-group">

View File

@@ -66,7 +66,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody> <tbody>
<?php <?php
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$custom_field_id = intval($row['custom_field_id']); $custom_field_id = intval($row['custom_field_id']);
$custom_field_label = nullable_htmlentities($row['custom_field_label']); $custom_field_label = nullable_htmlentities($row['custom_field_label']);
$custom_field_type = nullable_htmlentities($row['custom_field_type']); $custom_field_type = nullable_htmlentities($row['custom_field_type']);
@@ -118,4 +118,3 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
require_once "custom_field_create_modal.php"; require_once "custom_field_create_modal.php";
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -42,7 +42,7 @@ require_once "includes/inc_all_admin.php";
<?php <?php
$sql = mysqli_query($mysqli, "SELECT * FROM calendars ORDER BY calendar_name ASC"); $sql = mysqli_query($mysqli, "SELECT * FROM calendars ORDER BY calendar_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$calendar_id = intval($row['calendar_id']); $calendar_id = intval($row['calendar_id']);
$calendar_name = nullable_htmlentities($row['calendar_name']); ?> $calendar_name = nullable_htmlentities($row['calendar_name']); ?>
<option <?php if ($config_default_calendar == $calendar_id) { <option <?php if ($config_default_calendar == $calendar_id) {
@@ -65,7 +65,7 @@ require_once "includes/inc_all_admin.php";
<?php <?php
$sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC"); $sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$account_id = intval($row['account_id']); $account_id = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']); ?> $account_name = nullable_htmlentities($row['account_name']); ?>
<option <?php if ($config_default_transfer_from_account == $account_id) { <option <?php if ($config_default_transfer_from_account == $account_id) {
@@ -88,7 +88,7 @@ require_once "includes/inc_all_admin.php";
<?php <?php
$sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC"); $sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$account_id = intval($row['account_id']); $account_id = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']); ?> $account_name = nullable_htmlentities($row['account_name']); ?>
<option <?php if ($config_default_transfer_to_account == $account_id) { <option <?php if ($config_default_transfer_to_account == $account_id) {
@@ -111,7 +111,7 @@ require_once "includes/inc_all_admin.php";
<?php <?php
$sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC"); $sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$account_id = intval($row['account_id']); $account_id = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']); ?> $account_name = nullable_htmlentities($row['account_name']); ?>
<option <?php if ($config_default_payment_account == $account_id) { <option <?php if ($config_default_payment_account == $account_id) {
@@ -136,7 +136,7 @@ require_once "includes/inc_all_admin.php";
<?php <?php
$sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC"); $sql = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$account_id = intval($row['account_id']); $account_id = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']); ?> $account_name = nullable_htmlentities($row['account_name']); ?>
<option <?php if ($config_default_expense_account == $account_id) { <option <?php if ($config_default_expense_account == $account_id) {
@@ -159,7 +159,7 @@ require_once "includes/inc_all_admin.php";
<?php <?php
$sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_type = 'Payment Method' ORDER BY category_name ASC"); $sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_type = 'Payment Method' ORDER BY category_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$payment_method = nullable_htmlentities($row['category_name']); ?> $payment_method = nullable_htmlentities($row['category_name']); ?>
<option <?php if ($config_default_payment_method == $payment_method) { <option <?php if ($config_default_payment_method == $payment_method) {
echo "selected"; echo "selected";
@@ -181,7 +181,7 @@ require_once "includes/inc_all_admin.php";
<?php <?php
$sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_type = 'Payment Method' ORDER BY category_name ASC"); $sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_type = 'Payment Method' ORDER BY category_name ASC");
while ($row = mysqli_fetch_array($sql)) { while ($row = mysqli_fetch_assoc($sql)) {
$payment_method = nullable_htmlentities($row['category_name']); ?> $payment_method = nullable_htmlentities($row['category_name']); ?>
<option <?php if ($config_default_expense_payment_method == $payment_method) { <option <?php if ($config_default_expense_payment_method == $payment_method) {
echo "selected"; echo "selected";
@@ -214,7 +214,7 @@ require_once "includes/inc_all_admin.php";
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div> </div>
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*\.?[0-9]{0,2}" name="hourly_rate" value="<?php echo number_format($config_default_hourly_rate, 2, '.', ''); ?>" placeholder="0.00" required> <input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="hourly_rate" value="<?php echo number_format($config_default_hourly_rate, 2, '.', ''); ?>" placeholder="0.00" required>
</div> </div>
</div> </div>

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