Compare commits

..

1154 Commits

Author SHA1 Message Date
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
Johnny d829e39b66
Merge pull request #1240 from itflow-org/develop
Develop to Master for 25.10 release
2025-10-01 15:28:06 -04:00
johnnyq 0e401df3c0 Setting 0 for the Payment Provider threshold disables the invoice amount check 2025-10-01 15:23:00 -04:00
johnnyq 9072c37e95 Set payment provider default Threshold to 9999.00 when adding a payment provider 2025-10-01 15:06:14 -04:00
johnnyq 2eff11efbf Added Custom Link option to Reports Nav 2025-10-01 14:35:18 -04:00
johnnyq 6a7a02d220 Add Custom Links Admin Nav option 2025-10-01 13:48:52 -04:00
johnnyq d6349bbc5c Mention new installs already configuring the new cron jobs 2025-10-01 11:37:13 -04:00
johnnyq 9a2f887db1 Update Changelog increase app version to 25.10 2025-10-01 11:34:43 -04:00
johnnyq 71d30ff95f Enforce Payment Provider Max Threshold for Stripe Paymented in Guest Invoice and Unpaid Invoices 2025-09-30 12:14:24 -04:00
johnnyq 3135247936 Add Gibraltar to the countries list array 2025-09-25 17:35:02 -04:00
johnnyq 0d629221fe Add Software keys and software key assignment for contacts and assets table to the database schema 2025-09-25 17:24:42 -04:00
johnnyq 181ea4b487 Remove dead router.php require 2025-09-25 13:43:47 -04:00
johnnyq fa769665df Update report links in dashboard 2025-09-25 13:38:51 -04:00
johnnyq 00f5198bed Update appNotidfy links to use Absolute paths, updated gitignore 2025-09-25 13:31:16 -04:00
johnnyq 785a291614 Remove unessesary vars in edit ticket asset modal 2025-09-24 14:56:50 -04:00
johnnyq 92209c7125 Updated edit ticket asset to new ajax-modal and add additonal assets field 2025-09-24 14:51:34 -04:00
johnnyq 690007be5c More Relative to Absolute web asset paths updates 2025-09-24 13:09:25 -04:00
johnnyq e6bcf0e12f Started updating href paths to absolute paths instead of relative paths as itflow should be installed in document root anyway and not a sub-directory 2025-09-24 12:56:01 -04:00
wrongecho ca6a903b8f Keepalive
- Fix directory path
- Remove duplicate file
- Add to client portal
2025-09-24 08:39:45 +01:00
johnnyq 50f790dd6c Fix MFA Enforcement 2025-09-23 20:30:25 -04:00
johnnyq ed6aa843b7 Fix Activity log link 2025-09-23 19:41:02 -04:00
johnnyq 52a27699f1 fix mfa model 2025-09-23 19:39:49 -04:00
johnnyq dba08714bf moved user preferences and user settings into /agent/user/ directory 2025-09-23 19:17:14 -04:00
johnnyq edabc5c33f rename /user/ to /agent/ and update links to use agent/ instead 2025-09-23 18:04:23 -04:00
johnnyq 6b6c70f1df added extra ../ to href for css and js in header and footer to allow code to be placed deeper within the directory structure moved reports to /user/reports, this should also fix the new custom directories 2025-09-23 17:05:12 -04:00
johnnyq 93061eb695 Add Assigned Agent column to Recurring Tickets 2025-09-23 15:43:45 -04:00
johnnyq 1f9133c188 Added Viewing Archived Users and the Ability to Restore alonf with reassigning tickets of that user during the archive process 2025-09-23 15:36:45 -04:00
wrongecho e7dcc6df3c Tickets API - Add ability to resolve ticket in post request with only a ticket ID and client ID via resolve.php endpoint 2025-09-23 09:28:27 +01:00
wrongecho fbd58b4723 Bugfix: Change working directory back after 2025-09-23 09:27:04 +01:00
wrongecho e992138456 Better logic for the index/root page:
- If app user, send to their start page
- If a client contact, send back to the client area
- If not logged in at all, make them
2025-09-23 08:45:06 +01:00
wrongecho 058f79d0a1 Fix file paths in cron notifs to new structure 2025-09-23 08:38:29 +01:00
johnnyq 5c448c05a9 Update .gitignore and add custom directories to /admin /client /guest /user Example Documentation coming soon 2025-09-22 19:33:40 -04:00
wrongecho e966cd3068 New tickets v2 - Exclude leads 2025-09-22 12:04:11 +01:00
wrongecho 6d3351b2f7 Payment providers bug-fixing 2025-09-22 11:53:49 +01:00
wrongecho 61a1d61901 Bugfix - remove capital in html field name - Threshold => threshold 2025-09-22 11:23:18 +01:00
johnnyq 4ff3231451 Fix Incorrect Asset Name in Logging and Flash Alert when editing an asset in a ticket, remove ACH and Add Bank Transfer to Setup 2025-09-20 15:56:50 -04:00
johnnyq ce832d2805 Fix Broken Restore from Backup on Setup Page 2025-09-19 16:02:55 -04:00
Johnny b11730303e
Merge pull request #1238 from itflow-org/develop
Develop
2025-09-19 14:00:02 -04:00
johnnyq 565f9ab314 Update Changelog and App Version 2025-09-19 13:47:36 -04:00
Johnny 9435434cf9
Merge pull request #1237 from git-kup/develop
Update invoice.php - removed extra unnecessary wording
2025-09-17 00:03:41 -04:00
git-kup a58ca6f66d
Update invoice.php - removed extra unnecessary wording
it always bugged me that the alert message is so bloated. i almost never go to the mail queue after sending each invoice it just doesn't make any sense
2025-09-16 19:36:25 -04:00
johnnyq c769bbc405 Created new Mail Queuer to take advantage of OAUTH2 for M365 and Google Workspaces not enabled by default 2025-09-16 15:43:54 -04:00
Johnny 0379143829
Merge pull request #1236 from cynicalgeek/develop
Update to use payment_methods table
2025-09-16 11:46:30 -04:00
cynicalgeek ee235cf231 Update to use payment_methods table 2025-09-16 01:38:11 -07:00
johnnyq 04b29d43df Update Agent ticket access path in emails to new /user/ path 2025-09-15 17:28:58 -04:00
johnnyq dc0715da57 Added SMTP Provider and the ability to share OAUTH keys with IMAP for M365 Mail Auth 2025-09-15 17:23:00 -04:00
johnnyq 902323a75b Fix Broken gloabl search when in Admin section 2025-09-12 17:32:44 -04:00
johnnyq 3a5b18f3dd Added Beta support for Microsoft IMAP OAUTH2 must use new mail parser for it to work cron/ticket_email_parser.php 2025-09-12 15:56:44 -04:00
johnnyq ce7d84aa2f Reintroduce Webklex IMAP for ticket processing as PHP-IMAP is no longer being developed. This is optional for now and considered beta can be found in cron/ticket_email_parser.php 2025-09-10 14:27:46 -04:00
johnnyq 981fb9585d Updated All Exports to include your company name if exporting all and if exporting just from a client prepend the client name to file, introduced a sanitize_filename function and used it for the exports to always get a clean file name that works on every OS 2025-09-10 12:50:10 -04:00
johnnyq 23b2dcba70 Renamed post file user back to users 2025-09-09 17:47:16 -04:00
johnnyq e4a437f54c added escape parameters to fputcsv to satisfy php 8.4 Depracations 2025-09-09 17:45:09 -04:00
johnnyq d4167f9595 Fix Missing first row on interface export, started adding escape parameters to fputcsv to satisfy php 8.4 Depracations and fixed export all quotes 2025-09-09 16:54:18 -04:00
johnnyq 88475a2b76 Fix Mispelling causing Adding new certificate to break 2025-09-09 12:50:27 -04:00
johnnyq c26ce4b7dc Spruced up UI and eliminated checkbox to grant consent on Stripe consent in client Portal instead button acts a consent 2025-09-08 17:36:09 -04:00
johnnyq 5960e7cbd9 Fix Broken Add Payment link in reccuring invoice in the client portal 2025-09-08 17:23:51 -04:00
johnnyq 68872ab9fb Merge branch 'develop' of github.com:itflow-org/itflow into develop 2025-09-08 13:06:49 -04:00
johnnyq 64f12b42b8 Fix Footer path in used edit causing edit user not to function, also removed the old ajax used edit modal 2025-09-08 13:06:33 -04:00
wrongecho 8c0d542d7d Better logic handling for the default page redirects 2025-09-08 15:40:59 +01:00
wrongecho c016b67c3a Fix typo in sql query field, matching develop back to master because we screwed up the flow 2025-09-08 15:26:57 +01:00
Johnny 49d127e957
Merge pull request #1235 from expxx/patch-1
fix: spelling error with `vendor_descripion` -> `vendor_description`
2025-09-07 21:04:14 -04:00
Cam e7353c4757
fix: spelling error with `vendor_descripion` 2025-09-07 18:26:00 -06:00
Johnny 3106685972
Merge pull request #1234 from itflow-org/develop
v25.09.1
2025-09-07 11:44:10 -04:00
johnnyq 2b7017fae2 Fix dark mode for AI Ticket Summary 2025-09-07 11:38:03 -04:00
johnnyq da0b01e23f Center Generating Summary in AI Summary 2025-09-07 11:30:43 -04:00
johnnyq d450ea4beb Fix Installer Database not installing due to bad file name 2025-09-07 11:06:26 -04:00
johnnyq 9210734911 Also take in consideration of the ticket source, category and priority 2025-09-06 19:41:16 -04:00
johnnyq ebae80bb7e Ticket Summeries are now in HTML form wit ha breakdown of the Main Issue, Actions Taken and Resolution or Next Steps. It also takes into consideration of the current ticket status and Who replyied for the ticket replys / comments 2025-09-06 19:31:44 -04:00
johnnyq dcade3a5c7 Update the AI Ticket Summary Prompt 2025-09-06 17:57:58 -04:00
johnnyq f51c3e9e3f Text Wrap client tags so table doesnt go off the page with clients with many tags 2025-09-05 19:29:21 -04:00
johnnyq b34298e45b Hide Add Credit button in Client Top Head Dropdown also hide credits in client listing if no credit balance 2025-09-05 19:17:02 -04:00
johnnyq 9fa78897bc Fix extra spacing between icon and Assigned Contact Name iin ticket details 2025-09-05 19:05:06 -04:00
johnnyq 9642babb7e Fix issue with AI Settings displaying in Admin Menu if the module invoice/accouting module is turned off 2025-09-05 19:00:17 -04:00
johnnyq d2d1aed393 Fix long Invoice/Quote notes overflowing into the invoice/quote footer by removing a rowspan class, adjusted the page margins from 15 to 10 and adjusted the invoice/quote notes to use font size 9 instead of 10 2025-09-05 18:48:35 -04:00
Johnny 58d43063ec
Merge pull request #1233 from itflow-org/develop
25.09 Release
2025-09-05 13:24:54 -04:00
johnnyq 6a5ce1dce3 Add note about updating from the webUI 2025-09-05 13:09:02 -04:00
johnnyq f3c7c0761b Add note about backing up before updating 2025-09-05 13:05:46 -04:00
johnnyq 27b3124f77 Update .gitignore to ignore the new custom/* 2025-09-05 12:57:19 -04:00
johnnyq aa516529e7 Another attempt at fix automatic recurring payments expense 2025-09-04 17:56:00 -04:00
johnnyq 2283ad0eb2 Fix Adding Expense on recurring stripe payments in cron 2025-09-04 17:44:34 -04:00
johnnyq cece9ad46c Another attempt to fix reccuring payment migration to match the correct client_id; 2025-09-04 16:29:55 -04:00
johnnyq d37da2e8ff Fix Recurring Payment Migration for stripe 2025-09-04 16:07:38 -04:00
johnnyq 9d9bfc65d5 Fix Active side nav for api keys 2025-09-03 18:46:01 -04:00
johnnyq d7d6e4ac6f Fix paths for db and uploads dir in debug 2025-09-03 18:42:32 -04:00
johnnyq 46d93f986e Further enhanced the Saved Payments Section by splitting cards and add card into 2 columns 2025-09-03 17:45:11 -04:00
johnnyq 2549a97a7e Added card icons to Saved Payments in Client Portal and spruced up ui 2025-09-03 17:40:09 -04:00
johnnyq fc2cca5fdb Added card icon 2025-09-03 17:14:53 -04:00
johnnyq ef1ec56270 Allow the Client to easily Pay an invoice from the client portal with a saved card 2025-09-03 17:09:17 -04:00
johnnyq cf63a2e7d1 Fix TinyMCE License in client portal 2025-09-02 14:57:09 -04:00
johnnyq ed9b99e2f0 Fix unpaid invoices on client portal but also comment it out until ready for next release 2025-09-02 14:52:38 -04:00
johnnyq 33340b80f9 Updated Changelog, Increased app version 2025-09-02 13:14:18 -04:00
johnnyq ec94bb3d2a Add back Pay Invoice with saved card, currently only pays the full amount, would like to integrate into add payment modal and add payment post eventually 2025-09-01 16:19:33 -04:00
johnnyq bc2fe2bec8 Fixed stripe Migration table name spelling 2025-08-30 11:19:28 -04:00
Marcus Hill a87a206c04 Add better error handling for dodgy whois results 2025-08-30 16:16:55 +01:00
Marcus Hill 45083e19b0 Fix if undefined 2025-08-30 15:58:19 +01:00
Marcus Hill de627c19c5 No csrf for client side, yet 2025-08-30 15:51:14 +01:00
Marcus Hill 595e57dcdd Rewording 2025-08-30 15:51:05 +01:00
Marcus Hill dadcc69900 Typo 2025-08-30 15:35:31 +01:00
Marcus Hill 2c160d1ac9 Take you back to dashboard rather than a redirect 2025-08-30 15:31:49 +01:00
johnnyq 292b074d8c Remove legacy Stripe Enabled from invoice Commented out Pay Via Saved card this will have to wait for next release 2025-08-29 13:44:22 -04:00
johnnyq 2beff45811 Removed some simple tinyMCE inits and combined AI button into one 2025-08-29 12:51:14 -04:00
johnnyq 469c36da46 Remove AI Enable References through the UI this will be handled differently 2025-08-29 12:26:56 -04:00
johnnyq 8afe2d52a0 Converted and combine ajax add for tags and categories, fixed quick add links 2025-08-28 16:57:08 -04:00
johnnyq 51273e9151 Remove unused / unfinished browser extension from user preferences 2025-08-28 15:39:00 -04:00
johnnyq 1517c96841 Remove Budget from reports as it totally unfinished 2025-08-28 14:38:49 -04:00
johnnyq b7afbd3500 Revert Bump down fontawesome-free 7.0.0 to 5.15.4, did not like the way the new icons looked with the app at all 2025-08-28 14:25:14 -04:00
johnnyq 39d6c42c71 Bump and Migrate logic chartjs 2.9.4 to 4.5.0, bump stripe-php from 17.2.1 to 17.6.0, fontawesome-free from 5.15.4 to 7.0.0, fullcalendar from 6.1.17 to 6.1.19, TinyMCE from 7.9.1 to 8.0.2, bootsatrap js bundle from 4.6.1 to 4.6.2, DataTables from 2.3.1 to 2.3.3 2025-08-28 13:57:42 -04:00
johnnyq 9f50c9355a Move reports out of the sub directory for now as it broke requires 2025-08-28 11:46:20 -04:00
johnnyq fd73f132bc Fixed a few table names in Stripe Migration, Removed legacy stripe and AI settings 2025-08-28 11:44:29 -04:00
johnnyq 1f99e592b7 Remove old Autop Pay Logic from recurring Invoices Listing 2025-08-27 20:09:12 -04:00
johnnyq a90c4eded8 Remove Old Auto Pay Modal for recurring Invoices 2025-08-27 20:05:47 -04:00
johnnyq 1638538fb4 Remove Old Auto Pay from recurring Invoice in favor of the saved cards. 2025-08-27 20:04:49 -04:00
johnnyq da0892f9e3 Remove Stripe check and add payment provider check in Client Portal Recurring Invoices to create auto payments 2025-08-27 18:23:58 -04:00
johnnyq 66cab6c1ab Fixed Guest Pay removed old stripe vars in place of new payment provider vars 2025-08-27 17:38:41 -04:00
johnnyq 535078b0ab Fix Bad vars in PAyment Providers listing, add check to see if payment provider already exists 2025-08-27 16:27:54 -04:00
johnnyq d584c6b1e8 Commented out Credit lines as its not ready for next release running into way to many pitfalls will focus more on next release 2025-08-27 16:02:00 -04:00
johnnyq 530a76a9a2 Fix undefined vars in export invoice / quote pdf fix regression company logo path in export invoice / quote 2025-08-27 11:53:58 -04:00
johnnyq 2a0181fd92 Convert Invoice and Quote Guest URL to Copy to Clipboard link instead of open links 2025-08-26 12:17:45 -04:00
johnnyq 86c5b9a55d Removed Budge from the side nav as this was never finished and doesnt really do anything, will work on it at a later date 2025-08-25 23:51:48 -04:00
johnnyq 0a6b890250 Fix broken php start tag 2025-08-25 17:33:40 -04:00
johnnyq af7f1f30ad Credentials: Fix broken page if no clients exist, which also fixes dont show share modal if GET client_id is not set 2025-08-25 17:32:15 -04:00
johnnyq f6c0f840fe Asset: Multiple Bug fixes, Fix broken asset list if no assets exist, fix bulk Asset Ticket creation to use the client_id of the associated asset, allow to assign to all open projects. Fix Transfer client where client_id is not defined in client overview section 2025-08-25 16:59:15 -04:00
johnnyq 0e54359db8 Assets: Fix Description not displaying, Fix issue with unitialized OS var, fix client asset uri using asset_uri_2 2025-08-25 14:07:55 -04:00
johnnyq 373f1574bd Update function path for customAction to use the new path /custom and not /xcustom 2025-08-25 12:58:57 -04:00
johnnyq 6f5ffef22f Link Fixes 2025-08-25 12:50:01 -04:00
johnnyq 451206525e Remove function code for old way ajax-modal, moved modals over to modal_header/footer and removed _new 2025-08-25 00:07:51 -04:00
johnnyq 41f957ea3b Migrated admin modals to modals/ENTITY removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-24 23:50:38 -04:00
johnnyq 91b0080428 Finished Consolidating the remaining user modals to modals/ENTITY removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-24 00:38:48 -04:00
johnnyq 170d6dd7e5 Consolidate alot of modals to modals/ENTITY removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-23 18:48:21 -04:00
johnnyq 15ba385398 Consolidate asset modals to modals/asset removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-22 21:54:26 -04:00
johnnyq 0b93d58a32 Consolidate account modals to modals/account removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-22 18:51:39 -04:00
johnnyq 67078c4552 Consolidate product modals to modals/product removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-22 18:22:04 -04:00
johnnyq ab17de7efd Consolidate client modals to modals/client and use new ajax-modal syntax 2025-08-22 18:06:37 -04:00
johnnyq 1145260619 Consolidate trip modals to modals/trip removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-22 17:49:46 -04:00
johnnyq 066952ed04 Consolidate vendor modals to modals/vendor removed ajax_ and _modal prefixes from file names and use new ajax-modal syntax 2025-08-22 17:36:07 -04:00
johnnyq 6ccf7a0fb8 Set Invoice Credit, Consolidated all expense modals to modals/expense removed ajax_ and _modal prefixes from file name create new modal header and footer to include another parent directory eventually will consolidate all, this was just an example 2025-08-22 16:59:31 -04:00
johnnyq 7f49ecefb1 Flash alert if no comment was entered in a ticket reply via the client portal 2025-08-21 18:35:16 -04:00
johnnyq 43324391ff Fix Regressed See More... in User Activies page 2025-08-21 18:15:28 -04:00
johnnyq 89af8216b8 Fix Ticket Kanban CSS Path and category query 2025-08-21 18:13:31 -04:00
johnnyq be9e30e6d8 Fix Show Add Payment dropdown if client has credits or saved payment methods 2025-08-21 18:04:49 -04:00
johnnyq 85cfd6fb7c Fix Regression Broken Quick add modals 2025-08-21 17:54:10 -04:00
johnnyq e694e20025 Use mysqli escape string instead of sanitizeInput on stripe to payment provider migration just incase 2025-08-20 14:18:34 -04:00
johnnyq 7618dd335a Update Stripe to Payment Provider Migration to not transfer over clients with a NULL or blank stripe payment method 2025-08-20 14:12:39 -04:00
johnnyq eca09253d3 Migrate over Payment Methods from Categories to its own table payment_methods 2025-08-20 13:53:38 -04:00
johnnyq 96c32b3275 Added Stripe Config Migration to a database Update 2025-08-20 13:30:46 -04:00
wrongecho 94a4fec3c6 Fix popping future tickets numbering 2025-08-20 16:43:45 +01:00
Marcus Hill a8386d3937 Allow reporting on average time to first response 2025-08-16 21:44:17 +01:00
Marcus Hill 4b7aa4bece Powered by 2025-08-16 16:37:24 +01:00
Marcus Hill 4c604f4009 Use HTML for the logo instead of pdf->image; scales much nicer 2025-08-16 16:25:51 +01:00
Marcus Hill 98cf2f7a5a Use freeserif font instead of helvetica to properly display foreign characters 2025-08-16 15:54:28 +01:00
Marcus Hill 804467b1ba Use freeserif font instead of helvetica to properly display foreign characters 2025-08-16 15:52:54 +01:00
Marcus Hill 9fbf775299 Fix file icon in file rename modal 2025-08-16 15:30:10 +01:00
Marcus Hill 5c93753b7e Rm extra unnecessary line in overdue invoice email 2025-08-16 15:17:48 +01:00
Marcus Hill 27016eaa28 Recurring tickets - add ability to bulk recurr 2025-08-16 14:31:36 +01:00
Marcus Hill 859281784a Recurring tickets - fix include path 2025-08-16 14:31:01 +01:00
Marcus Hill 7ae7b8904c Invoice.php - Fix undefined error 2025-08-16 14:30:37 +01:00
Marcus Hill 559e0c6350 Comment direct link to guest pay, we should not encourage agents to do this 2025-08-16 13:47:47 +01:00
Marcus Hill d942badec8 Fix primary contact text not showing 2025-08-16 13:42:29 +01:00
johnnyq f443ae9203 convert even more ajax modals to new syntax 2025-08-16 02:06:42 -04:00
johnnyq d37dc37c2d convert more ajax modals to new syntax 2025-08-16 01:40:40 -04:00
johnnyq 236d895498 Reworked the ajax-modal function to be more simplified and use less data attributes 2025-08-16 01:02:16 -04:00
johnnyq 3bb0ef6f39 Left Align Billing Header 2025-08-15 22:55:40 -04:00
johnnyq a0ced96d1d Use a WAY MORE Efficient query when filling out filter selection boxes by using EXIST instead of DISTINCT selections 2025-08-15 20:39:23 -04:00
johnnyq 4ef1e91c78 When Show Archived is selected in clients show Drop down filters for them as well in Idustry and Referral 2025-08-15 19:32:09 -04:00
johnnyq 2333fa2d9b Product Category Dropdown Filter only show categories thats are assigned to the products not all the categories 2025-08-15 19:26:42 -04:00
johnnyq 37bef2d824 Check Stock if product type is a product before adding the line item 2025-08-15 19:12:45 -04:00
johnnyq 6427235aec Show Name Description Tax Price and Stock in Product Auto Complete in Invoice 2025-08-15 18:25:23 -04:00
johnnyq 535c8e9f7b Remove Stock if Inventory item is selected and added to an invoice and return stock when Invoice Item related to a product is deleted 2025-08-15 17:32:44 -04:00
johnnyq 890e166d1c Thicken the left border on ticket replies for more prominent type of reply 2025-08-13 13:35:29 -04:00
johnnyq 337ceb7477 Fix Client Net terms displaying default Net terms instead of On Receipt if selected 2025-08-11 21:51:05 -04:00
johnnyq f82f3e9b8c Allow adding more than 9 stock 2025-08-11 21:40:42 -04:00
johnnyq 3c709955e3 Add UI elements for product location, used logic to seperate product from service, also added the ability to add stock for a product and linking stock to an expense with notation, still need to work on taking away from stock 2025-08-11 21:18:55 -04:00
johnnyq 595e6090c7 Added Stock Ledger to tables and additonal fields for product including type, code and location 2025-08-11 18:19:45 -04:00
johnnyq 1d98eb5160 Update Project Details Header to use card group class and update client ui to not show header button texts in mobile view, added no text wrap to location and contact column 2025-08-08 16:09:46 -04:00
johnnyq 7172c92f02 Update Database updates patch in update_cli 2025-08-08 15:09:03 -04:00
johnnyq 874d9ddbc3 Cleaned up ticket details UI used card Group for top, removed responses in ticket replies section, uncarded the ticket commend editor giving much more room, moved summarize to 3 dots 2025-08-07 20:38:23 -04:00
johnnyq 73b84a9690 Removed dynamic depth path_prefix due to potential security issues with it 2025-08-06 18:12:13 -04:00
johnnyq c78f50ab8c cap the path_prefix to 3 depth directories max 2025-08-06 18:03:51 -04:00
johnnyq 8407cd3ea5 updated router.php to automatically determine relative path based off directory depth and use var path_prefix instead of prepend_uri var these vars are only used in header and footer php files for now 2025-08-06 18:02:25 -04:00
johnnyq 9caa42975d rename get_settings.php to load_global_settings.php and update all requires 2025-08-06 17:48:23 -04:00
johnnyq 44fdb6c24f Broke up the check_login.php require into several files seperated by function and then required them in the check_login 2025-08-06 17:16:30 -04:00
Johnny c8984d1bc9
Merge pull request #1231 from itflow-org/ticket-fr
Implement ticket first response date/time tracking
2025-08-06 16:19:27 -04:00
johnnyq be66a1690f Fix setup redirect 2025-08-06 12:52:02 -04:00
wrongecho 13838ec8d6 Implement ticket first response date/time tracking 2025-08-06 10:45:28 +01:00
wrongecho 2d72a6fd12 Implement ticket first response date/time tracking 2025-08-06 10:38:52 +01:00
johnnyq e24ec04a75 Fix Post errors when repeat and Email Event is not selected 2025-08-05 18:39:24 -04:00
johnnyq 9a0aa9ce97 Fix Calendar not showing when no client is selected 2025-08-05 18:36:23 -04:00
johnnyq 643a4ce666 add router.php to inc_all_client.php 2025-08-05 18:18:46 -04:00
johnnyq 189098541e Remove unused top nav ticket modal 2025-08-05 18:17:14 -04:00
johnnyq e8a1d4c66c Update mfa enforcement page to use new asset pathes 2025-08-05 14:05:09 -04:00
johnnyq 08dd6147f2 Migrate Dark/light mode to a user settings instead of global setting, created new include called router.php to dynamically prepend ../../ to asset pathes based off currect directory depth 2025-08-05 13:32:19 -04:00
johnnyq 7e55808a05 add some redirect to login.php, fix ticket post 2025-08-04 18:16:13 -04:00
johnnyq 6b39c1a280 Finished moving to the redirect, flash_alert and get_field_by_id and maing sure validate_csrf is on top and some other cleanups for guest and client posts 2025-08-04 18:07:26 -04:00
johnnyq a38d0054df Finished moving to the redirect, flash_alert and get_field_by_id and maing sure validate_csrf is on top and some other cleanups for user posts 2025-08-04 17:41:32 -04:00
johnnyq e39072d3bc More work on moving to the redirect, flash_alert and get_field_by_id and maing sure validate_csrf is on top and some other cleanups, along with more work on credits 2025-08-04 17:01:19 -04:00
johnnyq b4e5e3fda7 Fix injection with role id affecting authenticated admin accounts only 2025-08-04 12:06:05 -04:00
johnnyq e8af173b4f Fix Client Billing column lweft align the label right align the amounts 2025-08-04 11:36:30 -04:00
johnnyq fef0d8761f More user post function migration and cleanup 2025-08-04 11:27:24 -04:00
wrongecho 76b48378ab Hide invite contact letter options for now 2025-08-04 11:45:54 +01:00
johnnyq a7e042b5f4 Removed logging comments as its implied in asset post also if returning 1 field for logging use getFieldById Function flipped it so it validate CSRF before role permission check, replaced header redirect with new redirect function and elimnasted the extra exit() afer redirect as its done in the function, also used the flash_alert function 2025-08-02 12:51:40 -04:00
johnnyq ab8f51622f Started Removing logging comments as its implied in user posts also if returning 1 field for logging use getFieldById Function flipped it so it validate CSRF before role permission check 2025-08-01 18:30:32 -04:00
johnnyq fae7e76721 Remove logging comments as its implied in admin also if returning 1 field for logging use getFieldById Function 2025-08-01 18:08:02 -04:00
johnnyq 012b0f1a77 Update Admin posts to use new functions flash_alert and getFieldById which greatly cleans up and reduces the code also removed logging comments as implied by the function logAction 2025-08-01 17:21:35 -04:00
johnnyq cc5b1c8040 Add new function called flash_alert() to repace the alerting in posts, updated accounts post to use new function 2025-08-01 15:51:29 -04:00
johnnyq 2cf932600e Fix broken link for edit role 2025-08-01 15:32:43 -04:00
johnnyq d2dac1915d Update admin, user, guest and client post to use new redirect function 2025-08-01 15:25:52 -04:00
johnnyq 5d52b6c4ec Update logic for Apply Credit on invoice Post 2025-08-01 15:17:50 -04:00
johnnyq 7cfba7f2ef Added redirect() function for page redirects and updated credit system fields to include credit_note, credit_invoice_id and type for better reporting 2025-08-01 13:37:23 -04:00
johnnyq 41a8e41463 Move reports back and rename them appropriately update broken links 2025-07-31 18:48:33 -04:00
johnnyq 07e2c41eb7 Fix client uri regression broken asset details page update echos to use php short hand for easier code reading 2025-07-31 16:10:33 -04:00
johnnyq 9fe0d8417b Feature: Added Client URI field to Assets which is exposed in the client portal can be used for remote desktop links etc 2025-07-31 15:54:07 -04:00
johnnyq 905908bcf1 broken link updates 2025-07-30 20:59:20 -04:00
johnnyq 8e87c25de1 Update more broken links from file diretory consolidation and started migrating some js and ajax and css from root to /user /admin etc that pertain to the section 2025-07-30 20:08:28 -04:00
johnnyq 5a76780cd9 Move db.sql back to fix develop platform 2025-07-29 21:40:50 -04:00
johnnyq 10fd911dcc Update links on the setup page as well 2025-07-29 18:59:42 -04:00
johnnyq 2333d81276 Update more broken links from file dirextory consolidation 2025-07-29 18:34:51 -04:00
johnnyq 9ee76213e1 Update more broken links from file dirextory consolidation 2025-07-29 14:32:43 -04:00
johnnyq 4ec1441189 Cleanup some broken links from file dirextory consolidation 2025-07-28 23:29:46 -04:00
johnnyq 38e2e50a29 More progress on consolidating files into user and admin 2025-07-28 22:48:44 -04:00
johnnyq 95950700d8 Moved user items to user directory 2025-07-28 17:57:06 -04:00
johnnyq 0494bfc1cf Moved admin_ to /admin, user_ to user report_ to /reports each have their own post includes modals directories created seperate headers and footer. Also did the same for xcustom, more work to me done 2025-07-28 13:32:28 -04:00
johnnyq 4906e06bf1 Initialized migrating modals into entity folders and modals out of ajax and start working on moving admin items to new admin directory 2025-07-27 21:24:14 -04:00
johnnyq 699546f531 In Client Import Clarify CSV as Primary Contact Name instead of just Contact Name same with Location name use Primary Location Name, makes it much clearer when importing clients 2025-07-25 14:21:55 -04:00
johnnyq d5ec601d75 add eg info@company.com for further clarification on company setup 2025-07-25 13:36:07 -04:00
johnnyq 5f26f68a04 Clarify Email during company setup, instead of just enter email enter company email for the placeholder https://youtu.be/Ul10HXaPwNk?t=2079 2025-07-25 13:34:05 -04:00
johnnyq 219fe59aa2 Use Badge Pill for rounded Badges in client side nav 2025-07-25 12:44:43 -04:00
johnnyq d115d57d1a Update sanitize_input Function to handle encoding issues such as Vietnamese. Instead of Blindly just converting to UTF-8 check the input to see if its already UTF-8 2025-07-25 12:35:13 -04:00
johnnyq 6307099bc1 Clarified Importing contacts when inporting clients in the client import modal window 2025-07-25 12:18:17 -04:00
johnnyq e0a5efdfaf Updated Contact Asset Counts to use more optimied query and added asset counts to client listing 2025-07-24 17:28:10 -04:00
johnnyq e014e516e6 Beta Support for Dark Mode has been added to the theme options 2025-07-24 14:58:06 -04:00
johnnyq 4d71eb5de0 Remove Credit once Applied, also get total credits applied 2025-07-24 13:24:59 -04:00
johnnyq 0db6853fde Updated Invoice, Quote Recurring Invoice UI to include a table border and added hover effects to line items 2025-07-24 13:02:57 -04:00
johnnyq 197dd9f299 Initial Work on Client Account Credit System, DB Structure, Credit Balance Calculation, added Apply Credit and add Credit with Expire dates, Added DB Structure for Discount Codes, UI Rewrite on client top header now using grouped cards, more dark mode work as well 2025-07-24 11:31:45 -04:00
johnnyq 99aaeefe8e Move the adminlte css to be the last css to fix select2 being white in dark-mode 2025-07-23 17:32:51 -04:00
johnnyq 712804d2a8 Preparing for Dark mode theme: Started Fixing models so they look better in dark mode 2025-07-23 17:02:20 -04:00
johnnyq a393284acd Remove Building Icon next to company name in main side bar nav 2025-07-23 13:24:48 -04:00
johnnyq 719a0e5efa Also Apply previous commit it editing recurring expenses 2025-07-23 13:20:06 -04:00
johnnyq 6221fd67e1 Fix recurring expense next run date logic for monthly and annual frequencies 2025-07-23 11:47:31 -04:00
johnnyq 56c7c76ac6 Add option to filter footer 500 records per page 2025-07-22 16:15:09 -04:00
johnnyq a5aae51b1a Fix issue in some of the client overview entities archived will not remain selected after query, fixed issue where clients would not show in overview if in archived 2025-07-22 16:13:04 -04:00
johnnyq 14691fa367 Fix Archived Query for domains to not show archived clients 2025-07-22 15:51:57 -04:00
johnnyq 38b2a94231 Client Overview do not count Archived Clients entities and in services do not show services of archived clients 2025-07-22 15:46:17 -04:00
johnnyq 4c031c8f83 Increase the width for totals on invoices and Quotes PDF Exports 2025-07-21 21:31:39 -04:00
johnnyq b100498dd7 Add Client Bulk Actions: Edit Tags, Referral, Industry, Set Hourly Rate, Send Bulk Email, Archive. Also added restore option to restore a client 2025-07-21 18:27:31 -04:00
johnnyq 50a8e67917 Commented out deleting old Payment Provider Settings from DB until next release 2025-07-21 13:29:29 -04:00
wrongecho dfe0a0b1c7 Add default netid for asset int csv import to prevent errors 2025-07-21 09:18:30 +01:00
wrongecho 52dac2252a Add default purchase date for csv import to prevent error when undefined 2025-07-21 08:11:57 +01:00
johnnyq 5aee84e84f Add Credit card icon to the pay button 2025-07-15 18:27:09 -04:00
johnnyq 3e7d7f3801 Make Client Portal Dashboard Cards Linkable, added upaid invoices with ui for option to enter card manually, pay with saved card or pay entire balance 2025-07-15 18:23:20 -04:00
johnnyq 184aba4de4 Finished UI work for AI Providers and Models, AI is set to use the new AI Provider and Models 2025-07-15 16:19:23 -04:00
johnnyq dd482b246f Use getFallBack function on MAC Address if no MAC is present show a - placeholder 2025-07-15 13:56:54 -04:00
johnnyq 717e6351e7 Add Option for Mac Address in Additonal Column and allow to filter by MAC 2025-07-15 13:54:25 -04:00
johnnyq c36b1cf50f Added Income Category Filter to Invoices 2025-07-15 13:34:05 -04:00
johnnyq 896da66caf Remove the Archive button from invoices as we dont archive invoices 2025-07-15 13:26:13 -04:00
johnnyq f72b7763c1 Added Bulk Actions to Invoices Assign Income Category 2025-07-15 13:24:28 -04:00
johnnyq 46c4a62046 Converted add payment to a dropdown under invoice to allow for the standard manaual add payment and to pay with a saved card or by entering a card manully 2025-07-14 17:36:03 -04:00
wrongecho da3a1d2ce4 Allow linking closed tickets to a project 2025-07-14 15:13:36 +01:00
johnnyq 35d6b51770 use table-sm for tickets to reduce padding and margin and remove ticket_compact_view 2025-07-10 16:11:02 -04:00
johnnyq 35cbfdd736 Rework Categories into a select box for tickets, which now makes it easier to see what category your viewing and one less click as the form auto submits upon selection 2025-07-10 16:04:52 -04:00
johnnyq a7b2522c87 Fix Invoice Link in ticket details when billed 2025-07-09 20:10:08 -04:00
johnnyq fe8baf2ca4 Fix pointer not switch to a finger when hovering over add contact in ticket details 2025-07-09 20:06:22 -04:00
johnnyq 300aef413c Fixed Email Notify not showing for recurring invoice 2025-07-09 19:32:24 -04:00
johnnyq 467c681117 Add Saved Payment Options to recurring invoices and details for agents 2025-07-09 19:23:12 -04:00
johnnyq 31a89c0641 Update Recurring Payment Auto pay in cron 2025-07-09 17:05:56 -04:00
johnnyq 5f94543d9b Add Easy Select Auto Pay option in Client Portal Recurring Invoices, fix guest pay 2025-07-09 15:58:13 -04:00
johnnyq 10fd74b4c4 Remove unused model 2025-07-09 01:46:56 -04:00
johnnyq 9bc705bfa1 Updated Guest pay and Client Add Delete Payments, adjustedsetting saved payment per recurring invoice 2025-07-09 01:45:56 -04:00
johnnyq 307d9892b2 No need for a sql delete on recurring payments when deleting a saved payment method as SQL cascadely deletes the recurring payment if saved payment method is deleted 2025-07-08 14:10:27 -04:00
johnnyq 93f4ea51fd Rework Payment Provider AI Provider Tables and logic add cascading deletes 2025-07-08 14:07:10 -04:00
johnnyq a6f83493f8 Fix Payment Method Additons in setup 2025-07-07 23:22:35 -04:00
johnnyq 7e17fdf851 Add new UI for Saved Payments for payment providers 2025-07-07 18:25:23 -04:00
johnnyq c76da10747 Add Payment Method to UI and switch Add Payment modals to use the new table 2025-07-07 16:37:51 -04:00
wrongecho 7c558ff842 Add location create endpoint 2025-07-07 13:47:17 +01:00
johnnyq a011dc4dea POST code for AI and Payment Providers 2025-07-06 17:01:28 -04:00
johnnyq fa9acef279 Fixed IMAP test button with error details 2025-07-06 12:47:26 -04:00
johnnyq ed0936059b Convert new lines to brs for contact notes 2025-07-06 11:50:12 -04:00
johnnyq 77597fd7a9 Update wording on Add Payment Processing, we plan on using the income/expense account to be named after provider name and category be processing fees and the vendor named after the provider name if payment processing expense is enabled 2025-07-04 18:07:59 -04:00
johnnyq 8d1bf3d245 UI add modal and listing work on Payment Providers 2025-07-04 17:48:54 -04:00
johnnyq e7503e8f55 Initial DB work to Generalize Payment Providers, allow multiple payment providers and client payment methods, also to move payment methods to its own table instead of in categories, started work on ai provider ui models 2025-07-04 16:52:30 -04:00
johnnyq 9b6be66623 Initial work on Adding Payment Methods for Online Payments in Client on Agent Side and initial work on AI Providers with multiple model support assigned to various sections and custom prompts 2025-07-04 15:42:26 -04:00
johnnyq 6bc7862232 UI Dashb oard resize some Dashboard cards 2025-07-03 18:14:20 -04:00
Johnny 69a8a9246d
Merge pull request #1226 from MydsiIversen/Document-enhancement-clientside
Enhanced the document function on the client side
2025-07-03 17:42:24 -04:00
johnnyq b87b801b04 Fix broken Table header Link buttons in contact details 2025-07-03 16:24:51 -04:00
johnnyq dba04a407a Fix Table Header Link Buttons in Asset Details 2025-07-03 16:19:55 -04:00
johnnyq e281a39512 Add title Icon for Bulk Set Status 2025-07-03 16:08:15 -04:00
johnnyq e0364183e2 Disable Certain Bulk Actions that are directly related to a client in client overview section instead are shown in the client section 2025-07-03 16:03:57 -04:00
Johnny 6af61cbb3c
Merge pull request #1225 from itflow-org/bulk-assign-phyloc
Add physical location modal - not working
2025-07-03 15:11:42 -04:00
johnnyq 67dc012caa Small wording hints on file uploads instead of upload File its just upload and added the title of the upload modal from upload files to upload file(s) 2025-07-03 14:44:30 -04:00
johnnyq 8753655c9c Do not allow client portal logins with Contact users of a client that is archived 2025-07-03 14:18:14 -04:00
wrongecho 873dc1f76d Fix contact location if not provided in api call 2025-07-01 11:50:20 +01:00
wrongecho d781130b49 Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2025-07-01 10:52:05 +01:00
wrongecho effbf241e6 Fix default rate and net terms if not provided in api call 2025-07-01 10:51:56 +01:00
Mads Iversen 396a67b198 - Enhanced the document retrieval process by including the document description in the SQL query.
- Implemented file upload capabilities, allowing users to attach files to documents with support for various formats (PDF, Word, text).
- Created modals for adding new documents and uploading files, improving user experience.
- Added functions for formatting file sizes and retrieving appropriate icons based on file extensions.
- Updated the document display logic to handle attached files, providing inline previews for PDFs and images.
2025-06-30 14:41:12 +02:00
Marcus Hill 1939e06a24 Start using the new modal when creating tickets. This allows us to select both the client and contact, along with a client-specific asset, location and vendor 2025-06-28 17:56:55 +01:00
Marcus Hill 3f21e73b2e Comment ticket collabs, still broken 2025-06-28 15:55:13 +01:00
Marcus Hill bc9529c488 Client Portal - Allow client choose asset during ticket creation 2025-06-28 15:49:17 +01:00
Marcus Hill 19af05ebee Client Portal - Add company logo to header banner 2025-06-28 14:28:25 +01:00
Marcus Hill eb11e5b2f8 Bugfix: Company logo not being removed properly from db 2025-06-28 14:27:51 +01:00
Marcus Hill b85137096d Add physical location modal - not working 2025-06-28 13:42:00 +01:00
Marcus Hill 77e9d72f92 Add physical location modal - not working 2025-06-28 13:37:44 +01:00
Marcus Hill ec344cbaa7 Allow entering multiple ticket watchers at once 2025-06-28 12:30:42 +01:00
johnnyq 4cb2f42d71 Added Bulk Action Set IP to DHCP in Asset Interface 2025-06-27 01:45:25 -04:00
johnnyq 98723b76cf Added Bulk Actions Set Network, Type and Delete to Asset Interfaces 2025-06-27 01:31:23 -04:00
johnnyq 45ac7f084b Add Bulk Delete Function Also when deletign a ticket delete the watchers and attachments and remove the ticket id from uploads/tickets/ticket_id 2025-06-26 23:16:08 -04:00
johnnyq 3626777bd1 Fix Item name and Description Alignment in PDF Exports 2025-06-26 19:12:25 -04:00
johnnyq 5fb1070567 Show your Assigned Assets on the Client Portal Home Page 2025-06-26 18:45:23 -04:00
johnnyq f0bcf975b6 Add Assets to the client Portal viewable by Primary and technical Contacts 2025-06-26 18:18:36 -04:00
johnnyq 0ca28dc630 Fix Document API to not insert a new document upon update as we changed the way documents and document versions are handled 2025-06-26 17:30:34 -04:00
johnnyq 8196706538 Ticket API: Allow HTML into ticket Content and allow inserting asset_id 2025-06-26 17:13:27 -04:00
johnnyq 411a6efeb5 Fix Searching Closed Tickets 2025-06-26 17:05:38 -04:00
johnnyq ffad0cdec9 Fix broken Documents in the Client Portal due to regression with splitting document templates 2025-06-26 16:45:00 -04:00
johnnyq ca519fe4e1 Fix Adding a vendor from a template and adding country code to vendor template 2025-06-26 16:27:11 -04:00
johnnyq 6b0855cff8 Fix Exports while in Client Overview some were failing due to missing client_id var and some were not logging the client_id if in the client section 2025-06-26 15:49:49 -04:00
wrongecho e8a3309822 Fix project search not filtering completed projects 2025-06-26 13:13:25 +01:00
wrongecho ce8c4dfc4c Add debug check for php-zip 2025-06-25 20:40:08 +01:00
wrongecho a1b99c46c7 Client Portal - Don't allow tech contacts to edit themselves 2025-06-25 09:44:57 +01:00
wrongecho 1ee50fdfde Ticket merging - Hyperlink the new parent ticket in the internal comment for quicker access 2025-06-25 09:23:02 +01:00
wrongecho 05850f61d2 Fix php debug message 2025-06-25 09:11:19 +01:00
johnnyq 12d8c8e959 Fix Overlapping logo with company information in PDF Invoice and Quote 2025-06-23 19:43:41 -04:00
johnnyq 2b4933bd5c Do not Update ticket status when scheduling or cancelling a tickets schedule 2025-06-23 18:15:05 -04:00
Marcus Hill 500e713764 clarify error message wording 2025-06-22 10:01:10 +01:00
Marcus Hill 2ca50776ec Ticket
- Move ticket metadata/details into their own card (source, category, time, closure details, etc)
- Start adding support for ticket_quote
2025-06-22 10:00:53 +01:00
Marcus Hill bd31513e5d Add db fields for ticket_quote 2025-06-22 09:59:23 +01:00
Johnny 6df04390bb
Merge pull request #1224 from itflow-org/develop
25.6.1 Release
2025-06-22 01:52:33 -04:00
johnnyq 49511276fd Bump Release to 25.06.1 - updated changelog 2025-06-21 18:42:19 -04:00
johnnyq c4e91df1ed Fix Regression that broke setup, set company currency default to USD if not set 2025-06-21 18:33:48 -04:00
Johnny ed8a576ac3
Merge pull request #1223 from itflow-org/develop
25.06 Release
2025-06-20 15:34:32 -04:00
johnnyq 5c6a7acb7d Fix Updated at date in Documents 2025-06-20 15:25:42 -04:00
johnnyq 850db54a3b Bump ITFlow version to 25.6 and updated changelog 2025-06-20 15:10:46 -04:00
johnnyq 985593b7c2 Add Confirm Modal to Accept Decline Quote 2025-06-19 16:39:22 -04:00
johnnyq 857b5d1bfd Remove redundant php open tag 2025-06-19 00:27:33 -04:00
johnnyq 498e86aa7a Refactor Restore ITFlow Backup code in setup to utilize Sys Temp Directory 2025-06-19 00:26:23 -04:00
johnnyq 79d0fd28b0 Fix Backup function 2025-06-19 00:17:52 -04:00
johnnyq 426ca90515 Add Function to cleanup temp backup files if backup times out or error out 2025-06-19 00:13:56 -04:00
johnnyq e7328f1be5 Use tmp directory to zip and backup itflow instance 2025-06-19 00:06:54 -04:00
johnnyq baccc8051e Fix Extra spacing before Contact Name in client listing 2025-06-18 23:13:06 -04:00
johnnyq f1c7690270 Also add contact_id to the compact ticket listing 2025-06-18 22:34:27 -04:00
johnnyq d700d1530b Add missing contact_id in ticket listing 2025-06-18 22:32:55 -04:00
johnnyq b60f44eede Allow the Contact to be clickable in ticket compact listing too 2025-06-18 18:06:59 -04:00
johnnyq 54ab788e93 Allow the Contact to be clickable in ticket listing and direct to the client contact details page 2025-06-18 18:05:28 -04:00
johnnyq ac2dd04959 Moved Has Base Vendor Template Icon to the the end of the Vendors Table listing 2025-06-18 16:56:40 -04:00
johnnyq 9a449c35ac Fix No content in Edit Recurring Invoice Note Modal 2025-06-18 16:18:01 -04:00
johnnyq 2ff5d2f87c Merge branch 'develop' of github.com:itflow-org/itflow into develop 2025-06-18 15:32:38 -04:00
johnnyq 8f704dade8 Recurring Payments now make the payment on the invoice Due Date instead of when the recurring invoice is generated 2025-06-18 15:32:22 -04:00
wrongecho 3b3e323ce5 Assets - Import CSV now takes purchase date too (MUST be in format YYYY-MM-DD, Excel is annoying and will silently reformat this at random) 2025-06-18 14:48:13 +01:00
wrongecho 43ec144bf0 Fix more debug errors 2025-06-18 14:47:21 +01:00
wrongecho d5f94819d0 Fix 'Passing null to parameter' debug message 2025-06-18 14:11:18 +01:00
johnnyq 98aa7e4993 Remove url_query_strings_sort var as this functionality has been moved to the filter header that all listing pages share 2025-06-18 01:42:21 -04:00
johnnyq 7c61911dba Added Document Template Delete function 2025-06-18 00:55:57 -04:00
johnnyq 2bb107a5d7 Add Functionality to mark all tasks as complete or incomplete in a ticket 2025-06-18 00:48:18 -04:00
johnnyq 0c4a2aedb7 Show Puzzle Piece Icon if a Vendor has a base vendor template assigned to it 2025-06-18 00:16:19 -04:00
johnnyq e7e7272002 Migrated Vendor Templates to its own table, lots of code modifications here 2025-06-17 22:44:54 -04:00
johnnyq f672991089 Remove Document Template field from a few more files 2025-06-17 19:09:43 -04:00
johnnyq 473fa2671d Migrate Document templates to its own table 2025-06-17 19:00:56 -04:00
johnnyq 85e1515080 Move Document Versions to a seperate table much more efficient and allow to reference same document using a link Note this update will delete previous document versions 2025-06-17 17:58:20 -04:00
johnnyq 84437a2732 Bump TCPDF from 6.9.4 to 6.10.0 and TinyMCE from 7.9.0 to 7.9.1 2025-06-17 14:44:06 -04:00
johnnyq 71911f418f Remove Unessesary Code 2025-06-17 14:32:35 -04:00
johnnyq 2741f78bd8 Fully Migrated Quote and Invoice to use TCPDF and elimiated PDFMake 2025-06-17 14:22:57 -04:00
johnnyq 012e54161a Converted PDF Invoice from PDFMake to TCPDF 2025-06-16 18:40:05 -04:00
johnnyq 409084c499 Update UI on Quote, Recurring Invoice and Guest views to match new optimized Invoice UI Layout 2025-06-16 15:49:38 -04:00
johnnyq 27595b2c4a UI Add Ticket to Invoice - Removed Warning about Draf Invoices increase modal size to large and move input to a row to reduce uneessary veritical space being occupied. 2025-06-16 13:13:48 -04:00
johnnyq 62b6535625 UI Invoice make footer text secondary color 2025-06-15 15:42:13 -04:00
johnnyq e593f438b3 UI Work on Client Overview Increase Quick Notes and Recent Activities width and added contact photos or initials to important contacts 2025-06-15 15:36:54 -04:00
johnnyq 44d46a2cc5 UI Overhaul to invoice Details Page 2025-06-15 15:06:53 -04:00
johnnyq 66e30dd1a8 Added Badge Counts in Client Overview Section 2025-06-13 16:05:49 -04:00
johnnyq 4c74351d21 Added Company Tax ID Field and Option to Show Tax ID on Invoices 2025-06-13 15:51:09 -04:00
johnnyq 5d2b14259c Added Ticket Due Date basic functionality to tickets 2025-06-12 17:13:17 -04:00
johnnyq 878d5444e1 Use Gray Accent color when in client view and use configured theme for Global view, this will help differentiate the view that you are in 2025-06-12 15:00:47 -04:00
johnnyq ec24ec60c6 Disable Ticket Auto Timer by default 2025-06-12 13:49:11 -04:00
johnnyq 9c096d1f65 Add Setting option to enable or disable ticket autotimer 2025-06-12 13:44:21 -04:00
johnnyq dba3e895da UI/UX update in ticket details switch to full card stacks with edit icons for the stackable reference items on the right like asset watchers contact etc 2025-06-11 22:28:38 -04:00
johnnyq 07726322df Project Details: Add Bulk Actions to tickets and allow tickets to be sorted 2025-06-11 20:47:27 -04:00
johnnyq bef18c0d72 Do not Resolved Tickets in Bulk that have Open tasks, display warning and count of ticket not resolved because of open tasks 2025-06-11 18:48:23 -04:00
johnnyq 7c3332570a Add Ticket Category UI for Recurring Tickets 2025-06-11 18:11:08 -04:00
johnnyq a3d4a52188 Add Mark Sent when invoice is draft to Action Dropdown for invoice listings 2025-06-11 17:29:38 -04:00
johnnyq 5530e89f41 Add one more Non-Billable Check in invoices 2025-06-11 17:18:28 -04:00
johnnyq a3554b3dfd Add one more Non-Billable Check in invoices 2025-06-11 17:16:03 -04:00
johnnyq 674da36cd1 Do Not calculate Non-Billable Invoices towards a clients balance Convert Add Payment Modal to AJAX and add it as an option from the invoice listing page 2025-06-11 17:12:37 -04:00
johnnyq 18ce12f60d Add Option if visiting a deleted invoice / recurring to return to the listing page and added delete to invoice details page 2025-06-11 15:57:52 -04:00
johnnyq a462ab36f8 Ticket add / edit / builk sort categories alphabeticaly 2025-06-11 14:56:23 -04:00
Johnny 8aa41edc3e
Merge pull request #1222 from itflow-org/functions-ip-addr-leftmost
Update how functions.php gets the remote IP address for logging
2025-06-11 14:01:12 -04:00
wrongecho 4e0252553a rm extra ; 2025-06-11 08:18:32 +01:00
johnnyq 8745d09890 Add sanitize the remaining uris that allow uri type:// refactored service details 2025-06-10 12:11:58 -04:00
wrongecho ac3a02baea Disallow turning on login key without a secret 2025-06-10 09:19:29 +01:00
wrongecho f2bbc170da Update how functions.php gets the remote IP address for logging
- Builds on PR #1210 to always get the leftmost IP address
- Cloudflare (HTTP_CF_CONNECTING_IP) must now be explicitly defined, otherwise people could add the HTTP_CF_CONNECTING_IP header to a non-Cloudflare host and spoof IPs
- Tidy up the if/else logic a little
2025-06-10 09:03:00 +01:00
johnnyq d79a17adb0 Added sanitize_url function to strip out unsupported URI Schemas schema:// if not on the allow list it will show unsupport://URL 2025-06-09 20:28:10 -04:00
johnnyq 680dbb04ce Fix UI Regression with Indenting Columns in Tickets listing when Open and Closed tickets are filters 2025-06-09 13:30:13 -04:00
johnnyq 6c8403fa09 Move MFA Modal out of the Password Reset Form 2025-06-09 12:52:07 -04:00
johnnyq fec8eaef70 Remove xml from the allowed upload list, if you must zip it up 2025-06-07 01:28:13 -04:00
johnnyq 5f007029b4 Fix Category 2025-06-07 00:58:56 -04:00
johnnyq 4e76ceaa0f Simplify Category filter logic in tickets catrgory is an int not a string duh 2025-06-07 00:46:11 -04:00
johnnyq 08e467baa9 Encode Page Title 2025-06-06 21:48:24 -04:00
Johnny f78a2250f1
Merge pull request #1221 from itflow-org/develop
Develop to Master for 25.05.1 Release
2025-06-02 14:39:50 -04:00
johnnyq 84e8a459c8 update Changelog 2025-06-02 14:24:52 -04:00
johnnyq b777f80249 Increment Minor Version and update Changelog 2025-06-02 14:19:04 -04:00
johnnyq 73da203dbb Added Cards for Account Balance, Recurring Monthly for Billing Contacts and Domains Expiring for Technical Contacts to Client Portal 2025-05-31 17:47:08 -04:00
johnnyq 55a31865d0 Updated Client Overview Entities to not show Archived client's Entities even though the entity may not be archived, also added Archive Searching to network and certificates also added unarchive capabilities to them as well 2025-05-30 16:15:12 -04:00
wrongecho 350697869b Fix invoice top banner not showing 2025-05-30 17:31:54 +01:00
Johnny 939b07422d
Merge pull request #1220 from itflow-org/develop
Merge Develop into Master for 25.05 Release
2025-05-29 14:30:25 -04:00
wrongecho 88369d480a Reword changelog 2025-05-29 18:42:25 +01:00
wrongecho cf083e94e6 Fix 2025-05-29 18:40:51 +01:00
wrongecho 11e8f5acfc Bump app version 2025-05-29 18:37:39 +01:00
wrongecho 77b3a89eb2 Reword changelog 2025-05-29 18:37:16 +01:00
johnnyq f572f4265a Updated Changelog 2025-05-29 13:31:40 -04:00
johnnyq 0b66c8e1be Better naming of Indetity Providers instead of Integrations to make way for more Indentity Provider like Google and custom SSO for the future 2025-05-29 13:29:27 -04:00
johnnyq a8328a3f56 Add more text-nowrap to more table headers 2025-05-29 11:45:45 -04:00
johnnyq 8b42b17121 Added more text no wrap to table headers 2025-05-29 00:18:37 -04:00
johnnyq db418ce662 Mobile UI/UX - Hide long New Button names and use plus icon only in mobile view, proper form group spacing in mobile as well 2025-05-28 23:39:44 -04:00
johnnyq 96fe566e08 Add location country even when client id uri is not set 2025-05-28 22:03:55 -04:00
johnnyq a00f26d8a4 UI/UX Draggables now switch to a hand on hover, updated the UI in invoice quote and reccuring invoice so buttons do not wrap and are grouped. Seperated the Dragable button using a button link, did the same for tasks in tickets 2025-05-28 21:22:21 -04:00
johnnyq eeef63d1c3 Updated Changelog 2025-05-28 19:36:52 -04:00
johnnyq 0b88ea85ae Display Country in Invoices, Quotes, Recurring Invoices, Clients, locations, client top head, and allow searching via country in Locations and Clients 2025-05-28 19:27:11 -04:00
johnnyq c564118156 Fix Dupe GET VARS in tickets Removed extra Rebuild URL as this has been done in the filter header for some time now 2025-05-28 18:34:50 -04:00
johnnyq 01a7dc2068 Allow both Client and Contact to be sorted in same column in ticket list 2025-05-28 18:29:02 -04:00
johnnyq bb44ecec3f Trips UI/UX - Move Client at the end column for consistency 2025-05-28 18:13:44 -04:00
johnnyq b7b24d7de6 UI/UX Expenses - Combined Category and Description Column with secondary text seperation and expanded Truncation of Description from 50 chars to 60 chars 2025-05-28 18:08:08 -04:00
johnnyq dcca93e34f Only show 8 notifications at a time instead of 10 2025-05-28 17:43:03 -04:00
johnnyq 4124188505 Ticket UI/UX allow the ticket toolbar to be a little more Mobile friendly 2025-05-28 17:39:32 -04:00
johnnyq eb5d59623b Don't show Checkbox columns when ticket is closed, compact ticket list now matches round pills for status and priority 2025-05-28 17:02:50 -04:00
Marcus Hill 8631c06731 Stripe - Remove the locally stored payment identifiers (expiry/last 4) from the database when the client removes the stripe pm 2025-05-27 19:29:03 +01:00
Marcus Hill 40eb40fd86 Cron mail queue - fix $config_smtp_encryption being set to None in IF statement 2025-05-27 14:46:50 +01:00
Marcus Hill c486682a0e Cron mail queue - fix app_log_type not being one of available enums 2025-05-27 14:05:09 +01:00
Marcus Hill 336da073f1 Admin mail settings
- Disable the IMAP test button as it doesn't work yet
- Remove word 'successfully' when testing SMTP as we're only adding the message to the queue, it wasn't sent yet
2025-05-27 14:00:25 +01:00
Marcus Hill d2e187a239 Update security supported versions 2025-05-26 20:34:01 +01:00
johnnyq f69de29353 Get a more accurate count of Tables rows in Debug using count instead of relying on show table status as this is not accurate all the time. 2025-05-24 13:45:27 -04:00
johnnyq 61de8bc792 Redirect to login when itflow restore completes 2025-05-24 13:30:49 -04:00
johnnyq d31f19707b DB Dump 2025-05-24 13:23:41 -04:00
johnnyq 811312466f If Restore from Backup is chosen show message must configure database first if DB and config dont exist 2025-05-24 13:09:16 -04:00
johnnyq 5ecfb3e962 Update setup to include welcome as an option and do not hide the side nav items if db is already created. 2025-05-24 12:40:33 -04:00
johnnyq 77be5af4e5 Update setup to include restore option but place it in its own nav section for seperation. Also if DB and config is configured skip and do not show checks or database in the side nav 2025-05-24 12:28:34 -04:00
johnnyq c512a716d2 Fix extra spacing in modal footer in notifications 2025-05-23 19:09:36 -04:00
johnnyq 14f5630caf Fix Invoice Header Button 2025-05-23 18:34:25 -04:00
johnnyq 8532bdc172 More UI updates to Guest Pay 2025-05-23 18:25:09 -04:00
johnnyq 9d74bf8e19 Use cards in Guest Pay Invoice 2025-05-23 17:45:06 -04:00
johnnyq f75445b4d0 Limit Stripe Payments to just Credit Cards 2025-05-23 17:18:22 -04:00
johnnyq e04fa1b696 Add stripe_pm_created_at 2025-05-23 17:13:05 -04:00
johnnyq e07dfb5f67 db structure 2025-05-23 15:42:47 -04:00
johnnyq 3d1af05fc2 Updated DB to store Payment details 2025-05-23 15:41:52 -04:00
johnnyq 0e38925d74 Update Changelog 2025-05-22 18:00:08 -04:00
johnnyq c0f3343412 Client Portal Add Recurrung Invoices with option to Enable or Disable Auto Pay per recurring Invoice if Stripe is enabled and Client has a payment method. Also when removing saved auto payment methods delete all recurring payments for that client that are Stripe method also added this to the admin settings 2025-05-22 17:58:51 -04:00
johnnyq 6a368840fa Bump stripe-php from 16.4.0 to 17.2.1 2025-05-22 12:37:35 -04:00
johnnyq 5361391b3b Update changelog with the bumps 2025-05-22 12:24:49 -04:00
johnnyq b80662bb24 Bump FullCalendar from 6.1.15 to 6.1.17 2025-05-22 12:16:16 -04:00
johnnyq 4c272b6b8d Bump DataTables from 2.2.2 to 2.3.1 2025-05-22 12:08:47 -04:00
johnnyq 96abdef3ad Bump TCPDF from 6.8.2 to 6.9.4 2025-05-22 12:04:48 -04:00
johnnyq 0b04bc79e9 Bump tinyMCE from 7.7.1 to 7.9.0 2025-05-22 11:57:54 -04:00
johnnyq cefbbdc3a8 Bump phpMailer from 6.9.2 to 6.10.0 2025-05-22 11:46:09 -04:00
johnnyq 83ffe05a99 Update Changelog 2025-05-22 11:38:24 -04:00
johnnyq b6f73083ef SMTP Option Encryption None now works as intended 2025-05-22 11:37:14 -04:00
johnnyq 693736023e Update changelog 2025-05-21 21:02:55 -04:00
johnnyq fed87c93ab Migrated contact link models to the new ajax models this fixes the issue of the overlapping var contact_name and improves page load and performance in contact details 2025-05-21 12:18:42 -04:00
johnnyq f53b77b556 Migrated asset link models to the new ajax models this fixes the issue of the overlapping var asset_name and improves page load and performance in asset details 2025-05-21 11:51:18 -04:00
wrongecho b858d82b0b Show archived categories properly 2025-05-20 14:51:12 +01:00
wrongecho ccb2af6d17 Fix category name/type logging when archiving/deleting a category 2025-05-20 14:50:27 +01:00
wrongecho 8d937ac8f5 Fix add asset modal icon not showing 2025-05-20 11:39:35 +01:00
wrongecho 2786fb65ed Don't show archived ticket categories in the tickets.php filter options 2025-05-20 11:39:09 +01:00
wrongecho 025532f579 Fix quote top navbar options not showing following perms work 2025-05-20 11:29:26 +01:00
wrongecho 5bd03be1ad Ticket tasks - set maxlength html attribute 2025-05-14 16:05:43 +01:00
wrongecho 40086f1ce0 Quotes / Invoicing - More role/perms enforcement 2025-05-14 11:07:25 +01:00
wrongecho be66ad9a4c Quotes / Invoicing
- Ability to manually mark a quote as invoiced (weird css fix for this, we can remove the custom css if we make the parent button just a dropdown, but don't want to introduce extra clicks)
- When converting a quote to an invoice, show the new invoice number in the quote history
- Quotes can now be sent from the main Send dropdown, instead of having to use the send button in the options menu / main quotes.php page
2025-05-14 10:41:32 +01:00
Marcus Hill 0df5c01bb7 Project - Require CSRF token to delete a project 2025-05-11 12:25:13 +01:00
Marcus Hill b85fa38b67 Project - Show client abbreviation in open ticket link modal 2025-05-11 12:19:34 +01:00
Marcus Hill 546246d7c5 Project - Allow editing client after creation 2025-05-11 12:14:20 +01:00
Marcus Hill d5536e78f4 Ajax contacts - Enforce client access restrictions when getting client contacts 2025-05-11 12:01:23 +01:00
Marcus Hill 908738b7ca Ajax active clients - enforce client access restrictions (e.g. when changing ticket client) 2025-05-11 11:57:16 +01:00
Marcus Hill 797e02bffa Hide Credentials in side nav if no perms to view 2025-05-11 11:48:47 +01:00
Marcus Hill d856685782 Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2025-05-11 11:46:00 +01:00
Marcus Hill 1400983d8c Projects/Tickets
- Hide new project button for users that only have support read access
- Hide new ticket button for users that only have support read access
- Enforce client access restrictions for viewing project details based off project client
- Prevent selecting the 'Select tickets' text when linking ticket
2025-05-11 11:45:47 +01:00
johnnyq 2a43c5d868 Remove DB Check 2025-05-08 16:46:25 -04:00
johnnyq a67675c649 Remove 500 Records per page option to reduce Resource strain, 100 records per page is max 2025-05-08 16:08:28 -04:00
johnnyq fc344ef636 add notification paging 2025-05-07 19:23:11 -04:00
johnnyq 2ffb2be083 Update the backup code to be a full backup zip file download of uploads and db dump along with version meta data file. Also allow to restore a single file in setup currently hidden 2025-05-07 15:37:57 -04:00
johnnyq 069772f27d Add Upload uploads.zip file to restore files as well 2025-05-07 14:54:25 -04:00
johnnyq 241ec50802 Add hidden option to restore dumped ITFlow Database during Setup 2025-05-07 14:32:51 -04:00
johnnyq b943c9cd89 Remove Influencer as a referral 2025-05-06 19:50:51 -04:00
johnnyq 80625f8c3f Remove Unused vars in setup, added jpeg to the allow extension for user avatars 2025-05-06 19:36:31 -04:00
johnnyq 58435d3460 Add Next Button if Database is already configured 2025-05-06 17:18:54 -04:00
johnnyq 7a7ac4a47f Fix Edit Blank Recurring ticket in Asset Details 2025-05-06 13:16:56 -04:00
Marcus Hill 429dfa5ca4 Allow file upload extensions: .bat, .stk 2025-05-04 21:38:11 +01:00
wrongecho e1f212d30d Start recording ticket source (API, Email, Portal, Agent) 2025-04-28 14:51:38 +01:00
wrongecho 670450bcfb Ticket statuses - Allow ordering from admin settings, this can replace the need to move the Kanban columns 2025-04-23 10:22:33 +01:00
Johnny 83e15e9e4a
Merge pull request #1216 from itflow-org/develop
Develop to Master 25.03.6
2025-04-21 17:28:14 -04:00
johnnyq b309081d75 Allow to search by project reference number 2025-04-21 17:16:35 -04:00
johnnyq f1a7b35aa6 Update Changelog and App Version fix date to 2025-04-21 17:00:32 -04:00
Marcus Hill 469c5ef06d Update client pdf export
- Fix HTML formatting for the cover div, other div styling is still broken
- Adjust layout of cover info and add MSP logo
- Add software purchase and expiry dates
2025-04-19 16:30:00 +01:00
Marcus Hill 07cbe561bd Add stupidly bigger update warning to update page. Add reminder note to check ITFlow backup (one in every ten page loads) 2025-04-19 15:14:40 +01:00
Johnny b69a70cfc3
Merge pull request #1213 from itflow-org/develop
Develop to Master - 25.03.5 Release
2025-04-18 19:08:18 -04:00
johnnyq 923001928c Update Changelog and version 2025-04-18 18:08:40 -04:00
johnnyq 75ed461c67 Asset and Contact Links now goto the details page instead of the details modal 2025-04-16 18:51:53 -04:00
johnnyq 691aebce91 Revert Fix 2025-04-13 15:15:18 -04:00
johnnyq 846947ff49 Change the button handlebar class 2025-04-13 15:11:37 -04:00
johnnyq 65e107d154 Totally remove Dragula in Favor of the modern SortableJS library, updated the Kanban 2025-04-13 15:01:52 -04:00
johnnyq 19b809b699 Added SortableJS Library, and updated Invoice, Quote and Recurring to use it. Added Grab Bar Icons next to action buttons. Will now sort in Mobile much more efficiently, update ajax vars for recurring invoice 2025-04-13 13:29:16 -04:00
johnnyq 60fe02bb47 Comment 2025-04-13 11:57:26 -04:00
johnnyq 3e708059c6 Fix not showing File folders instead of Document Folders when creating a document. 2025-04-13 11:55:14 -04:00
johnnyq 424104bb66 Default Date between max date to 9999-12-31 instead of current date for filtered listings, this fixes the issue if you post date an entity it would not show in the listing by default unless you selected a a great to date in the filter 2025-04-12 12:08:30 -04:00
johnnyq 62696b9ebe Fix Mobile Country Code in contact list 2025-04-12 11:58:32 -04:00
Johnny 87403e8c2d
Merge pull request #1209 from itflow-org/index-redirect
Redirect the blank index page to the start page
2025-04-12 11:54:05 -04:00
wrongecho 7a5a607ff6 Redirect the blank index page to the start page 2025-04-11 14:53:02 +01:00
wrongecho a195774726
Update README.md
Add JetBrains to sponsors, as they provide FOSS licenses of PhpStorm to us
2025-04-11 14:31:01 +01:00
johnnyq 8d0da7b55b Fix Entity Linking in Asset and contact details 2025-04-09 14:00:51 -04:00
Johnny 58c315cd09
Merge pull request #1207 from TamirSlo/fix-dashboard-db-update-1-9-7
Fix Dashboard following DB Update 1.9.7
2025-04-08 17:00:40 -04:00
Johnny dd6c4602db
Merge pull request #1194 from ssteeltm/develop
fix: missing kanban ticket settings
2025-04-08 16:59:07 -04:00
Tamir Slobodskoy d413e0c8ff
Fix Copy Trip Modal 2025-04-08 05:27:56 +01:00
Tamir Slobodskoy b356658635
Fix Dashboard following DB Update 1.9.7 2025-04-08 05:02:46 +01:00
Johnny d92f0fc49b
Merge pull request #1206 from itflow-org/develop
Develop to Master for 25.03.4 Release
2025-04-07 13:31:20 -04:00
johnnyq f206a28cf7 Update Changelog and App version to 25.03.4 2025-04-07 13:07:03 -04:00
johnnyq 70cb0ac635 Add the ability to remove additional assets from the ticket details screen 2025-04-07 12:58:55 -04:00
johnnyq a0ece18876 Allow to remove additional assets in a ticket 2025-04-07 11:59:56 -04:00
wrongecho 4a22b03952 Global search - assets
When global search returns an asset, include a hyperlink to the asset details as part of the asset name
2025-04-04 15:12:32 +01:00
wrongecho 5ebf797c90 rm asset_mac - no longer in assets table 2025-04-04 15:08:04 +01:00
wrongecho a20759f1f2 rm asset_mac - no longer in assets table 2025-04-04 15:07:47 +01:00
wrongecho c273cab36e Portal - View all ticket bugfix
View all tickets should display the ticket_number rather than the database ID
2025-04-04 14:50:36 +01:00
johnnyq 8306a04eda Add Purchase Reference to Copy Asset 2025-04-03 15:07:56 -04:00
johnnyq f078203136 Fix Database Export, was not properly exporting utf8, html data and such. Also disable foreign key contraint check in the export then renable it in the end. This fixes the issue with importing the database into phpmyadmin or using the mysql command 2025-04-03 15:01:47 -04:00
johnnyq 15e89c3c4e Fix Bulk Assign ticket to only show ITflow users and not client users 2025-04-01 17:23:44 -04:00
Johnny 595c4f1440
Merge pull request #1205 from itflow-org/develop
Develop to Master for 25.03.3 release
2025-04-01 11:45:41 -04:00
johnnyq 91a523dc23 Update Changelog 2025-04-01 11:40:24 -04:00
johnnyq 8567c97c09 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2025-04-01 11:33:16 -04:00
johnnyq 3621e99c61 Update Changelog and app version 2025-04-01 11:33:04 -04:00
Johnny d99b9cbe68
Merge pull request #1204 from itflow-org/fix-assign2
Ticket assign
2025-04-01 10:56:56 -04:00
Johnny e319768fd3
Merge pull request #1203 from itflow-org/fix-bulk-assign
Tickets - Fix bulk assign
2025-04-01 10:56:27 -04:00
Johnny c30ffcf096
Merge pull request #1202 from itflow-org/user-activity
User activity
2025-04-01 10:55:17 -04:00
wrongecho 7286248fef Ticket assign
Remove the role check altogether, its the old way of doing the roles anyway
2025-04-01 09:12:24 +01:00
wrongecho dc49f80cc3 Tickets - Fix bulk assign
Fix bulk assigning tickets to agents
2025-04-01 09:03:33 +01:00
wrongecho 1ae2da8054 User activity
Hide the See More button if the user can't actually access the logs due to not being an admin
2025-04-01 08:46:22 +01:00
johnnyq 090f4cb560 Fix adding location phone extension when addign a client 2025-03-31 19:33:07 -04:00
johnnyq 0914716b8e Allow user to redact client replied tickets 2025-03-31 18:42:56 -04:00
johnnyq ab463c1773 Tidy Phone Country code add + placeholder 2025-03-31 17:30:33 -04:00
johnnyq 36af4d11fc Few more phone fixes 2025-03-31 16:52:47 -04:00
johnnyq 14d8dc6fa6 Fix php errors thrown when formatPhone is blank 2025-03-31 16:35:36 -04:00
johnnyq 2032b48ad3 DB Update Set Country codes to default to NULL and not 1 Nullify all current country codes 2025-03-31 12:06:36 -04:00
Johnny 2af795f548
Merge pull request #1201 from itflow-org/fix-users
Fix users
2025-03-31 11:22:47 -04:00
Johnny 7b4edb2948
Merge pull request #1200 from itflow-org/recurring-invoices
Recurring invoices
2025-03-31 11:22:30 -04:00
wrongecho 17a906fd03 Users bugfixes
- Fix syntax error when adding user, thanks @fleetlognorge
- Fix old reference to scheduled_tickets
2025-03-31 08:35:42 +01:00
wrongecho af46a1fd96 Fix syntax error when adding user, thanks @fleetlognorge 2025-03-31 08:32:14 +01:00
wrongecho 393c0b8c11 Recurring invoices
- Fix the delete link
- Cron should only flag recurring invoices that with a next-run in the past if the recurring invoice is active
2025-03-31 08:28:22 +01:00
johnnyq e92f2f714d Fix Ticket Assign to 2025-03-30 20:48:12 -04:00
johnnyq 42606067c0 If no country code is entered display the number only no spaces hyphens or perenthesis 2025-03-30 12:22:43 -04:00
johnnyq 98bb65509d Fix setting country code in company details 2025-03-30 11:45:27 -04:00
johnnyq a2599e5d43 Fix network location edit 2025-03-30 02:14:06 -04:00
Johnny 0390b1bc2a
Merge pull request #1198 from itflow-org/develop
Develop to Master
2025-03-29 18:23:37 -04:00
johnnyq 531f3ec741 Update app version and changelog 2025-03-29 18:18:52 -04:00
johnnyq 127afdca0d DB.sql revert 2 2025-03-29 18:16:25 -04:00
johnnyq c4df5bf988 DB.sql revert 2025-03-29 18:12:32 -04:00
Johnny 30234e044d
Merge pull request #1197 from itflow-org/develop
Merge Develop into Master
2025-03-29 17:47:08 -04:00
johnnyq 1e98ee8916 Update app version and Changelog hotfix 2025-03-29 17:43:00 -04:00
johnnyq d5665c2577 Update db.sql to match the mediumtext of the updates 2025-03-29 17:35:53 -04:00
johnnyq 762ec51a19 Fix issue with missing phone numbers 2025-03-29 16:41:40 -04:00
johnnyq 309ad724ec Fix client export to only show licnesed software by the selected client only 2025-03-29 16:22:48 -04:00
Johnny 34397fe468
Merge pull request #1196 from itflow-org/develop
Merge Develop into Master
2025-03-29 15:51:47 -04:00
johnnyq 2f82647f5e One more chnagelog update 2025-03-29 15:35:40 -04:00
johnnyq 7d7854424c Refined and updated changelog 2025-03-28 13:15:33 -04:00
johnnyq eaeadbe933 Fix users list 2025-03-28 12:01:39 -04:00
johnnyq cf3f0cee6c Revert Users listing 2025-03-28 11:48:23 -04:00
ssteeltm a5f7b7fa9c fix: missing kanban ticket settings 2025-03-28 12:00:48 -03:00
johnnyq 9f7c289e94 Fix AI Promps 2025-03-27 22:25:01 -04:00
johnnyq c2bba7a919 Allow to close a project with resolved tickets 2025-03-27 14:21:56 -04:00
johnnyq 4bb37a7198 Removed absent task_description var from projects 2025-03-27 13:41:15 -04:00
johnnyq 231694aabe Fix Invoice Vars in ticket, return empty string instead of null for getFieldByID() 2025-03-27 13:38:22 -04:00
johnnyq 2fb75e6d67 Uncommented out temp comment check cli runtime 2025-03-27 12:40:43 -04:00
Johnny 810af638a3
Merge pull request #1193 from itflow-org/cron-recurring-notify
Recurring items with next-run dates in the past
2025-03-27 12:39:27 -04:00
johnnyq 9223b8cfb1 Fixed some renamed vars 2025-03-27 12:29:55 -04:00
wrongecho b7df21a663 Notify if a recurring ticket, invoice or expense has a next run date in the past - it needs to be manually adjusted for cron to pick it up again. Also, bugfix bulk recurring ticket delete. 2025-03-27 16:29:39 +00:00
johnnyq 7d47ed4dbd Merge branch 'develop' of github.com:itflow-org/itflow into develop 2025-03-27 12:01:24 -04:00
johnnyq 65eaf92862 Fixed a few debug errors where client was not checked to see if it was set in some of the create modals, replaced old url_query_strings_sb var to the correct new var url_query_strings_sort. 2025-03-27 12:01:10 -04:00
wrongecho 8a01bc0d7d Recurring ticket - remove rogue 's' from 'Contact' tab 2025-03-27 15:54:10 +00:00
Johnny 1a8a3781dc
Merge pull request #1192 from itflow-org/certs-exp-colours
Certificates page and sidebar - colours/badges
2025-03-27 11:39:49 -04:00
wrongecho eadfdc41c4 Tickets api - allow setting the billable status when creating a ticket 2025-03-27 14:45:44 +00:00
wrongecho 421abd4c5b Tickets api - bugfix 500 error 2025-03-27 14:34:36 +00:00
wrongecho ae1a0dcc73 Certificates page and sidebar - Only show yellow on certificates 7 days (blanket, not just for LE) 2025-03-27 12:27:08 +00:00
johnnyq 63b8804e2d Migrated client pdf export to TCPDF from pdfMake 2025-03-26 21:13:00 -04:00
johnnyq 634afcc089 Added TCPDF for PDFs and allow document export to PDF 2025-03-26 19:35:27 -04:00
johnnyq 90f5c8ad57 Simplify getFallBack function to just fallback on - instead N/A 2025-03-26 18:39:45 -04:00
johnnyq 12fd45c144 Dont include Archived Assets, contacts or software is the software license export report 2025-03-26 18:28:33 -04:00
johnnyq 382258a27c Add User and Asset Assigned Licenses to Client PDF Export 2025-03-26 18:17:45 -04:00
johnnyq b5fa8ab4de Added Search by client Abbreviation to client and global search 2025-03-26 17:56:40 -04:00
johnnyq bbb0db2f4f Update Changelog 2025-03-26 17:53:33 -04:00
johnnyq 5b89e3dbee Add Phone Country code in even more places 2025-03-26 14:04:47 -04:00
johnnyq 07b29a7bdc Add Phone Country code in more places for proper phone number formatting 2025-03-26 13:36:56 -04:00
johnnyq 3286343026 Update DB to remove phone_mask 2025-03-26 11:14:05 -04:00
johnnyq 6a26b611fa Remove Phone Masking option in favor of Country Codes 2025-03-26 11:10:51 -04:00
johnnyq 218fd2dcdc Update formatPhoneNumber php function to include other country formatting, updated phone inputs to incoude country code 2025-03-25 18:45:24 -04:00
johnnyq 4c85db5e49 Remove legacy redact function in favor of tinymce integration redact 2025-03-24 13:22:41 -04:00
johnnyq 85ae42190a Feature: Added User Signature preferences, currently appends signature to ticket replies / comments 2025-03-24 12:49:47 -04:00
johnnyq df8a755462 Wrap a wait before the DOM is loaded before loading anything calling anything in app.js 2025-03-23 18:08:14 -04:00
johnnyq 53713a0318 Add back batch payment when in client view for invoices 2025-03-22 21:01:55 -04:00
johnnyq 61aa477cbf Fix Tags 2025-03-22 20:49:37 -04:00
johnnyq c2adb92d28 Rework tag filter a bit to use array_map instead of looping through the get vars, update string wording to tag_filter and only show tags in the tag filter that are relatd to an entity and also include tags in the get var fixes 2025-03-22 18:32:56 -04:00
johnnyq 19b2b08eac Update locations filter to still include the a location that is not assigned to an entity if present in the url this fixes the issue where for example in client contact you select a location to filter only 1 contact is shown you goto edit the contacts location then submit it pushes you back with no results and all locations is present but in the uri the old location is still present 2025-03-22 15:49:35 -04:00
johnnyq 504346256f Only show locations if an entity references the locations. Previously, all locations were listed in the location filter, even if no entites were assigned to them. 2025-03-22 15:16:53 -04:00
johnnyq 34e92d2223 Update Tags filter to only show tags available 2025-03-22 14:55:13 -04:00
johnnyq 573953704c Limit Client Selection to clients that have an emtity in client filters 2025-03-22 14:11:27 -04:00
johnnyq 0b9f10985d Add Client Select Filters the remaining entities also when creating an entity auto select the client based off the the client selected in the client dropdown filter 2025-03-22 13:50:03 -04:00
johnnyq 8e3dd42a32 Allow start interface to be named 0 2025-03-21 21:54:55 -04:00
johnnyq 0647933df7 Added JS Library to handle Country based Phone formatting inputs intl-tel-input 2025-03-21 12:59:56 -04:00
johnnyq 93f4da3962 Added Phone Country Code fields defaulting to +1 for US will add logic in code later for phone number formatting 2025-03-21 12:48:00 -04:00
johnnyq f72351ea88 UI tidy 2025-03-21 11:58:16 -04:00
johnnyq d3c4c8c846 Comment Query 2025-03-20 18:18:33 -04:00
johnnyq cf047024a1 Add Projects to client side nav and seperated the logic between client and global view of projects 2025-03-20 18:17:26 -04:00
johnnyq ad3ed68932 Add the ability to Create a ticket inside a project then auto assign it to the project 2025-03-20 17:05:47 -04:00
Johnny 62b8ee9d30
Merge pull request #1190 from itflow-org/cron-certificate-notifs
Cron certificate notifs
2025-03-20 13:51:35 -04:00
johnnyq eedd92c894 Fix creating a ticket and selecting a template it would use template name instead of the subject for the ticket 2025-03-20 13:47:11 -04:00
johnnyq 622d5e5a44 Delete unused add asset ajax modal 2025-03-20 13:00:51 -04:00
johnnyq 475d653979 You can now upload and create documents, credentials in contact and asset details sections 2025-03-20 12:58:00 -04:00
johnnyq 19a6f8f422 Use nearest text area instead of a static ID for AI rewording, started adding entity creations in contact details and auto selecting the contact when in contact details page 2025-03-20 11:39:50 -04:00
wrongecho e69d69760d Cron - Only notify on certificates expiring in 45 days if they are valid for longer than 90 (i.e. not LE) 2025-03-20 13:53:53 +00:00
johnnyq 06de349fac Expanded the Redact tool into ticket details area, generalized it to allow redaction in other areas as well 2025-03-19 15:35:02 -04:00
johnnyq c3ec83f640 Updated changelog also added redact to open tickets 2025-03-19 13:20:55 -04:00
wrongecho 37c20e4e0d Hyperlink the report_time_by_tech report. Need to add a monthly version. 2025-03-19 14:07:07 +00:00
wrongecho 400ba5bb20 Started changelog 2025-03-19 14:02:30 +00:00
wrongecho a1ea5214a9 Sonarcloud fixes 2025-03-19 13:54:15 +00:00
wrongecho aae633c4ac
Merge pull request #1189 from itflow-org/ticket-redaction
Add ticket redaction feature
2025-03-19 13:53:15 +00:00
johnnyq ec8d7a36a8 Only store optimized WebP images, so only 1 image is stored and used for thumnails and viewing, removed fields has_preview and has_thumbnail as these are no longer needed, fixed issue when optimizing portrait images the bottom half would get cropped out 2025-03-18 16:48:05 -04:00
wrongecho 21dc26b06f
Merge branch 'develop' into ticket-redaction 2025-03-18 09:42:29 +00:00
wrongecho 6a8d2cf1d4 Add ticket redaction feature 2025-03-18 09:40:39 +00:00
johnnyq b803ba4c55 Use MD5 hashing instead of SHA256 on file uploads which is way faster and still provides a unique file reference 2025-03-17 15:10:39 -04:00
johnnyq 4378fc2719 Removed redundant hash field from files table as the reference already stores this value 2025-03-17 14:59:37 -04:00
johnnyq 244e1290b4 Convert service_domains to use InnoDB instead of MyISAM 2025-03-15 18:35:21 -04:00
johnnyq 5a64bd3a32 Ensure all Tables and fields use CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 2025-03-15 18:28:48 -04:00
johnnyq cc0b2126ba Removed most Associated deletions when deleting a client as most of this is performed through SQL now using cascade deleting 2025-03-14 19:45:59 -04:00
johnnyq 97f92e508e Remove Associated Deletions as these are handled by the database cascade deletions 2025-03-14 18:37:56 -04:00
johnnyq 09fbe4e4ad Update more multi to multi link tables to use cascading delete and foreign key relation 2025-03-14 18:18:44 -04:00
johnnyq 508af6a80f Fix some Database update logic 2025-03-14 17:15:10 -04:00
johnnyq 073f816dbd Do not allow cascade deletion for rack devices as this table is optional to reference an asset and will cause issues when adding a devoce without a selected asset 2025-03-14 15:33:00 -04:00
johnnyq bf327afd19 Bump DataTables from 2.2.1 to 2.2.2 2025-03-14 13:35:48 -04:00
johnnyq 8fb8ce319e Added copy to clipboard for contact and asset ajax modal credentials fixed issue with clipboard because jquery ui was declared last mved it up 2025-03-14 13:28:00 -04:00
johnnyq eabfef22be Turned caching back on for Ajax Modals Function Fix some PHP Error, update credential vars in assets and contacts 2025-03-14 12:31:18 -04:00
johnnyq 06c31e0808 Update Asset reference SQL Tables to use Foreign keys and cascading delete, and remove orphaned data from the foreign tables to prevent integrity errors during update 2025-03-13 21:35:39 -04:00
johnnyq 509fb5cfed Bump TinyMCE from 7.6.1 to 7.7.1 2025-03-13 19:41:21 -04:00
johnnyq f1aa66119a Update pointers to the includes folder 2025-03-13 18:12:18 -04:00
johnnyq 7150b1545a Moved files check_login.php, get_settings.php, settings_localization_array.php, inc_set_timezone.php into the includes folder, we also moved /client/check_login.php into client/includes/ 2025-03-13 17:55:31 -04:00
johnnyq 59bbbe4a8d Removed Patch panel tables as they share similarities with assets and assets interfaces, rename events to calendar events and event attendees to calendar even attendees 2025-03-13 16:34:38 -04:00
johnnyq f64641cfdd Fix regression OTP in guest view item 2025-03-12 23:03:28 -04:00
johnnyq f1783a6110 update credential model 2025-03-12 22:35:13 -04:00
johnnyq c8c1155c9c Update the API for the new cred var names 2025-03-12 22:33:06 -04:00
johnnyq 338a08da6c Fix credential vars in Services post 2025-03-12 22:10:48 -04:00
johnnyq 15aed891f4 Fix credential vars in Services 2025-03-12 22:04:55 -04:00
johnnyq c1c54780cb Update client export pdf for new table field names 2025-03-12 21:39:47 -04:00
johnnyq e93704bbdb Completely renamed everything login to credential including tables added cascading deletion to the multi to multi tables 2025-03-12 21:28:50 -04:00
johnnyq c0db914213 Added Recurring Ticket Reference to Tickets 2025-03-12 13:56:36 -04:00
johnnyq b78e31e518 Fix Recurring Income by Client Report with the updated recurring invoice vars 2025-03-12 11:36:51 -04:00
johnnyq 2533f0ced1 Fix Create Auto Payment after renaming recurring to recurring invoices 2025-03-11 22:54:44 -04:00
johnnyq c7f4e48a27 A Few more places to update recurring invoice vars 2025-03-11 22:38:13 -04:00
johnnyq 87c1a95b06 Few more places for recurring invoice vars 2025-03-11 22:26:52 -04:00
johnnyq 4efb39358a Missed recurring_invoice details and fixes up a few other places for recurring invoice vars 2025-03-11 22:24:19 -04:00
johnnyq 619b93a545 Renamed Recurring to recurring_invoices updated a large portion of code, and updated many other fields to reflect recurring_invoice and not just recurring 2025-03-11 22:08:37 -04:00
johnnyq a0598997d7 Update Recurring Tickets in a few more places 2025-03-11 19:55:08 -04:00
johnnyq d0b8095bba Feature: Added additional assets to Recurring Tickets as well 2025-03-11 19:36:39 -04:00
johnnyq daab1bca3d Rename the remain items from Scheduled Tickets to Recurring Tickets 2025-03-11 18:32:24 -04:00
johnnyq a0a2fddd90 Feature: Additonal Assets can now be assigned to a single Ticket 2025-03-11 17:41:40 -04:00
johnnyq f61c30bd5a Started work on additional assets per ticket, added table ticket_assets, Fixed Legacy Check User Role Functions 2025-03-11 15:10:23 -04:00
johnnyq 4921d1eb19 More Work on Client Users section, added User Icon Indicator in contact if a user account exists 2025-03-11 13:59:01 -04:00
johnnyq 28c8b1a6da Fix missing attachments on ticket replies via the ticket email parser 2025-03-10 19:17:40 -04:00
johnnyq 3f0a6df717 Organize color theme settings into similar color rows 2025-03-10 18:46:08 -04:00
johnnyq 07c8398e0d Update user_roles naming in setup and setup_cli 2025-03-10 18:06:47 -04:00
johnnyq 82c7a64a25 Added Bulk Delete Asset Functionality and also deleted more asset reference tables as we are not using cascade delete yet 2025-03-10 17:35:16 -04:00
johnnyq 164a6a5604 Started Standard user view and client user view in users 2025-03-10 16:51:10 -04:00
johnnyq 9b6ea851e7 Removed the prepended user_ from the fields in the user_roles table, moved user_role_id from user_settings directly to users table, rename table user_permissions to user_client_permissions, removed unused Sessions vars in login. This upedate will require to update using update_cli.php --db_update 2025-03-10 15:57:16 -04:00
johnnyq 3804e18e53 When cron or manually generating invoice from a Recurring Invoice insert referencing recurring Invoice ID into the invoice 2025-03-10 12:34:31 -04:00
johnnyq 244a47efad Added Recurring Invoice Reference ID to Invoices and Product ID reference to Invoice Items 2025-03-10 12:26:14 -04:00
Johnny 89e59b2448
Merge pull request #1184 from itflow-org/tck-time-by-tech
Add work/time by technician report
2025-03-09 12:31:34 -04:00
Johnny bf9c2f6de7
Merge pull request #1182 from itflow-org/tck-fr-at
Add database field for ticket_first_response_at.
2025-03-09 12:31:07 -04:00
Johnny ff80a3db3f
Merge pull request #1186 from itflow-org/develop
v25.02.4 - Stable Minor Release
2025-03-08 18:11:38 -05:00
johnnyq c7d00d7b0d Fixed Changelog 2025-03-08 17:53:20 -05:00
johnnyq 1c6e74b08e Update Changelog put back the missing parts for 25.02.3 2025-03-08 17:40:36 -05:00
johnnyq f8d054f8aa Bump minor version and update the Changelog 2025-03-08 17:25:10 -05:00
wrongecho e5dc50ec35 Add time by technician report 2025-03-07 22:57:18 +00:00
johnnyq e0dfaf2d22 Fixed a few var errors in ajax contact details 2025-03-07 16:58:52 -05:00
johnnyq 757a62c35b Fix Ticket Count under contact listing 2025-03-07 16:44:38 -05:00
johnnyq 52a62fc23c Fix Client URL in recurring Invoices 2025-03-07 14:47:47 -05:00
wrongecho 44b70d7161 Add database field for ticket_first_response_at. This can be used as part of metric reporting and also as part of SLAs in the future 2025-03-07 13:28:35 +00:00
johnnyq ad9e4b4fb4 Added new php function to retrieve and escape a single record from a specified table using an id getFieldById(table, id, field_to_retrieve, escape_method) escape method defaults to SQL but html and json and int can be specified 2025-03-04 13:45:21 -05:00
johnnyq 4fdd5ae769 Fix Bug adding software license if no vendor is selected 2025-03-04 00:13:21 -05:00
Johnny 9f2b9e3b3e
Merge pull request #1181 from itflow-org/develop
v25.02.3 - Stable Minor Release
2025-03-03 15:57:26 -05:00
johnnyq 2c074e9dc4 Spelling 2025-03-03 15:55:01 -05:00
johnnyq 0fad31d683 Update changelog new minor release 2025-03-03 15:53:58 -05:00
johnnyq b154930a4c Fix Notifications 2025-03-03 15:36:32 -05:00
Johnny 359b04e7d1
Merge pull request #1180 from itflow-org/develop
v25.02.2 Maint / Small Feature Release
2025-03-03 15:22:57 -05:00
johnnyq cc00e3bf75 Add Periods to the changelog 2025-03-03 15:19:18 -05:00
johnnyq 0454685039 Update Changelog 2025-03-03 15:09:40 -05:00
johnnyq b5eb325c5e Fix Dismissed Notification Search Filter 2025-03-03 15:00:33 -05:00
johnnyq ed6276a3e4 Add Active Inactive filter to recurring invoices 2025-03-03 14:56:04 -05:00
johnnyq 5da1310e34 Combine Notifications dismissed and notifications into 1 and add link to Notifications Ajax modal 2025-03-03 14:36:38 -05:00
wrongecho a69b09c9e6 Bugfix: When exporting to CSV, the first asset isn't shown 2025-03-03 09:42:45 +00:00
wrongecho 8da3bb15e9 Add physical location field to asset csv import/export 2025-03-03 09:29:28 +00:00
wrongecho 8488445bf4 Start March changelog 2025-03-03 09:06:07 +00:00
wrongecho 546d21adac Fix client notes on edit modal 2025-03-03 09:03:49 +00:00
johnnyq 580f50b187 Added Link button to WYSIWYG Document Creation and editing 2025-03-03 00:22:51 -05:00
johnnyq 4744276f2a Don't count Non-Billable Invoices in overdue count 2025-03-03 00:07:22 -05:00
johnnyq 6106b8aebb Fix broken client link for the report clients with a balance 2025-03-02 21:03:02 -05:00
Johnny dd2b203321
Merge pull request #1178 from itflow-org/43-ssl-history
Add SSL certificate history tracking
2025-03-02 11:22:34 -05:00
Marcus Hill 7994c9c7a8 Add SSL certificate history tracking 2025-03-02 10:15:26 +00:00
Marcus Hill ae59aa3326 Add SSL certificate history tracking 2025-03-02 10:12:47 +00:00
Marcus Hill 0ab9a1c97d Temp fix warnings on num_domains expiring / num_domains_expired vars not defined yet 2025-03-02 09:50:50 +00:00
Marcus Hill 2908568e2a Fix certificates not showing notes 2025-03-02 09:40:21 +00:00
Marcus Hill 2b673a1b6c Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2025-03-02 09:26:28 +00:00
Marcus Hill bece8abfe2 Prune db updates older than a year 2025-03-02 09:26:16 +00:00
johnnyq ac2b355399 updated Changelog and bumped app version to 25.02.2 2025-03-01 21:21:03 -05:00
Johnny 7e658ee1f2
Merge pull request #1177 from itflow-org/develop
Release v25.02.1
2025-03-01 12:29:14 -05:00
Johnny bbee81c3bb
Merge pull request #1176 from Whit3XLightning/Whit3XLightning-patch-2
Update folder_create_modal.php
2025-03-01 12:11:59 -05:00
johnnyq 45b61ba335 Updated Changelog 2025-03-01 12:09:43 -05:00
johnnyq 32e6345cbc Add Paid to the top of a pdf invoice if paid 2025-03-01 12:03:48 -05:00
Tallyn Morse afff46972a
Update folder_create_modal.php
$folder['folder_name']; did not exist in the scope of where folder_create_modal.php was being required in the client_files.php and client_documents.php leading to null array pointer exception. While dirty the new way will reliably retrieve the name of the current folder the user is browsing to correctly name the modal.
2025-02-28 23:26:39 -06:00
johnnyq d197995226 Updated Changelog, and Updated version to 25.02.1 2025-02-28 15:24:47 -05:00
johnnyq 09fa23519d Use Generated Thumbnail images if available in Gallery view for files, add options to download Original, Preview (Optimized) or Thumbnail 2025-02-28 14:43:03 -05:00
johnnyq 9da736daee Enhance the file picture upload optimization to read EXIF data when available and correctly adjust the image orientation for the optimized version. 2025-02-28 13:55:32 -05:00
johnnyq 410d1e0f86 Add Fade Animation to ajax modals 2025-02-28 13:34:26 -05:00
johnnyq 12ad7962c0 Wrap text in notifications 2025-02-28 13:31:53 -05:00
Johnny 397ebc5112
Merge pull request #1175 from ssteeltm/fix-kanban-dragging-touch
fix: kanban cards dragging when on touch devices
2025-02-27 23:25:58 -05:00
ssteeltm d48823925a fix: kanban cards dragging when on touch devices 2025-02-27 18:36:34 -03:00
johnnyq 6f49f16f6b Fix Links cannot contain directories that exist https://forum.itflow.org/d/1568-bulk-send-email-links-cannot-contain-directories-that-exist 2025-02-27 15:21:04 -05:00
johnnyq c2664a2888 Add .zed to the .gitignore 2025-02-27 11:46:29 -05:00
johnnyq 4529a56d7b Fix Sort Newest to oldest in notifications 2025-02-27 11:22:02 -05:00
johnnyq fded8177c5 Added contact and Asset Indicators with quick links to credentials page 2025-02-26 16:00:22 -05:00
johnnyq e670a9847a Added Indicator Counts for assets, credentials, licenses, tickets and documnent in Contacts Listing 2025-02-26 15:35:14 -05:00
johnnyq 8aada99f06 Added Edit to asset and contact details ajax modal for quick edits, added more links to the vendor details ajax modal 2025-02-26 13:46:04 -05:00
johnnyq 04e624cc14 Added Purchase Reference to Software Licneses 2025-02-26 12:59:03 -05:00
johnnyq 55ebe70808 Add Vendor Details Popup, works with Domain Vendors right now, Added fallBack function when no text is present use a placeholder 2025-02-25 18:16:03 -05:00
johnnyq f23afdd85c Added Quick Category/Tag Add throughout utilizes the new ajax modal 2025-02-25 17:05:34 -05:00
johnnyq bac76871c1 Added vendor UI select elements for software 2025-02-24 21:36:30 -05:00
johnnyq 56cbcf2921 Removed Single Link Vendor Login and single link software login. These are unused will be moved to multi to multi relationship, also added vendor_id to software to reference a vendor 2025-02-24 21:24:57 -05:00
johnnyq f2dc25aedb Added Link to and unlink Items in Asset Details 2025-02-24 19:08:00 -05:00
johnnyq 2367ca2255 Remove expire record limits and only include items expiring within 45 days instead of 90 in client overview 2025-02-24 17:24:43 -05:00
johnnyq f28c1ce398 In-App alerts are spawned 1,7 and 45 days instead of 1,7,14,30 and 90 2025-02-24 17:03:29 -05:00
johnnyq 6cc6e80f92 Fixed ticket link for agent email notifications on scheduling tickets 2025-02-24 16:19:22 -05:00
johnnyq 9aeda2ce53 Set Array instead of selecting Days to Expire in client overview 2025-02-24 13:18:32 -05:00
johnnyq a08393b4d4 Fix Redirect Link when scheduling tickets that conflict with other scheduled tickets 2025-02-24 12:52:35 -05:00
johnnyq 02c1446cb8 Fix Transfer Asset(s) to Client 2025-02-24 12:42:31 -05:00
johnnyq 8e66434ec4 Updated asset details Link in rack, fixed opening asset details modal when clicking on a connected asset in asset details page 2025-02-23 15:48:47 -05:00
johnnyq 66d43d8a95 Dashboard When clicking domains or certificates sort by expire date asc 2025-02-23 12:30:47 -05:00
johnnyq 6c7052ea0d Fix missing public key var in certificate edit 2025-02-23 12:23:13 -05:00
johnnyq a994bb7e8c Update Links on Project and and client listing 2025-02-22 17:37:35 -05:00
johnnyq 9b109c7abd Update links in project list 2025-02-22 17:29:19 -05:00
johnnyq 95855fc22e Update links in client overview 2025-02-22 17:27:16 -05:00
johnnyq 3218ea85b2 Bug Fix Contact Detail Modal not showing Credentials or Tickets 2025-02-22 17:15:41 -05:00
Johnny a1831c7406
Merge pull request #1174 from itflow-org/develop
25.02 Release
2025-02-22 16:40:45 -05:00
johnnyq c022e5fe32 Last fix document edit loading tonymce 2025-02-22 15:45:21 -05:00
johnnyq 2cddda5062 Updated Changelog 2025-02-22 15:36:57 -05:00
johnnyq daeb71abb3 Update the App Version to 25.02 for prep on release 2025-02-22 15:20:10 -05:00
johnnyq 915161d812 Fixed Revenue Edit category and account fields also removed currency selection there as its automatically determined by the company default 2025-02-22 15:04:11 -05:00
johnnyq 0e3959ce00 Updated Client Access Permissions to use the defined in check_login.php 2025-02-22 14:25:24 -05:00
johnnyq 8d05633d7d Auto set client abbreviation if not set in add or edit client 2025-02-22 14:02:31 -05:00
Johnny 2d39f21b51
Merge pull request #1173 from itflow-org/small-fix
Small perms fixes
2025-02-22 13:38:05 -05:00
johnnyq 69850f83d3 Add s 2025-02-22 13:31:27 -05:00
johnnyq 7e041d52df Move Client Overview to the bottom of the side nav 2025-02-22 13:22:53 -05:00
johnnyq 58a8f08bd2 Fix broken product edit due to wrong field being called 2025-02-22 13:15:15 -05:00
Marcus Hill 8eea19d03d Small permissions fix for client-specific access 2025-02-22 17:59:24 +00:00
johnnyq 5d18e85d62 Don't auto generate Client Abbreviation on the fly on the side bar. This is auto generated when you create or edit a client and leave abbreviation blank 2025-02-22 12:53:56 -05:00
johnnyq 8a13207327 Fixed dragula css and js to not prepend / 2025-02-22 12:47:26 -05:00
johnnyq 2a974c28b0 Remove old item ordering logic for invoice, recurring invoice and quote 2025-02-21 22:14:40 -05:00
johnnyq 9b5eb86cac Changelog Update, Allow to view unlimited notifications 2025-02-21 21:50:44 -05:00
johnnyq 86212ee088 Update Changelog 2025-02-21 21:20:14 -05:00
johnnyq e0a1e45928 Updated Asset Details AJAX modal to include its own details as well 2025-02-21 20:48:17 -05:00
johnnyq 5c3e0f0c31 Added Contact Info to the new contact details akax modal 2025-02-21 20:37:02 -05:00
johnnyq 25db6c6e03 Replaced more contact Links with the new ajax contact details modal, updated the modal UI as well to use bigger icons and have the icon on its own line 2025-02-21 20:14:36 -05:00
johnnyq 87779e5c10 Show Quick Asset details when clicking on a connected asset also when you click on asset in document details 2025-02-21 18:22:14 -05:00
Johnny 94ff910564
Merge pull request #1171 from ssteeltm/drag-and-drop-items
Drag and drop items
2025-02-21 14:56:34 -05:00
johnnyq 1b59eef9e2 Added Quick Details Modal for Assets 2025-02-21 14:55:22 -05:00
ssteeltm b10d757b77 drag drop for recurring invoices 2025-02-21 15:05:45 -03:00
ssteeltm 4dd55df7a8 Drag and Drop for Invoice Items 2025-02-21 14:44:24 -03:00
ssteeltm 554c4d99bb Drag and Drop Quote Items 2025-02-21 14:22:01 -03:00
ssteeltm 17920e3cda update admin_ticket_template_details.php 2025-02-21 13:51:57 -03:00
root 073713a6a2 Drag and Drop Ticket Template Tasks 2025-02-21 13:30:26 -03:00
Hugo Sampaio 43c8e0785a
Merge branch 'itflow-org:master' into drag-and-drop-items 2025-02-21 12:55:35 -03:00
johnnyq 22cd28de98 Added Connected Device Icon as well 2025-02-21 01:20:27 -05:00
johnnyq c1548ce83d Connected Asset Links are now clickable 2025-02-21 00:55:09 -05:00
johnnyq 64f3df6baf Add Some Client Permission Checks to the new Global View and some tidying 2025-02-21 00:28:36 -05:00
johnnyq ab3ffdf7fc Update more links rearrange Client Overview Side menu to match client side nav, define 0 for the post vars now defined in form. update unbilled tickets report 2025-02-20 21:07:53 -05:00
johnnyq 162ce8261a Hide client related form elements when Creating Entities in the client over view global section 2025-02-20 18:55:27 -05:00
johnnyq fe68a1f641 Services is now in global view 2025-02-20 17:20:15 -05:00
johnnyq 713bd0dab7 Added Network to Global View and updated links 2025-02-20 16:43:59 -05:00
johnnyq fc00b5e78f Added Location to Global View, updated links and fixed password pop over moving you to the top of the page when clicked in asset and conact details 2025-02-20 16:25:07 -05:00
johnnyq 2795b4e15e Added Global Views for Licenses, Credentials and Certificates 2025-02-20 14:58:25 -05:00
johnnyq 7628fc2643 Fixed Shared links not showing in the client overview until a user opens it 2025-02-20 11:39:01 -05:00
johnnyq 15d7875369 Fix Client Selection 2025-02-20 00:48:44 -05:00
johnnyq a5d69d47ee Show select Client when adding items in Client Overview / Global View 2025-02-20 00:44:22 -05:00
johnnyq d76d405d5a Make contacts global view compatible, remove bulk mail from admin as this is in contacts bulk actions, updated links, tidy 2025-02-20 00:38:40 -05:00
johnnyq 70495ef298 Created whole new section Client Overview to get a global view of all your clients things, merged domains and assets into one 2025-02-19 22:16:24 -05:00
johnnyq 7f5893a627 Moved Edit Recurring Ticket over to the new AJAX modal function 2025-02-19 19:10:21 -05:00
johnnyq 4c02a4e8c2 Moved Edit Network over to the new AJAX modal function 2025-02-19 18:05:16 -05:00
johnnyq afdb4c06da Converted edit Calendar to the new ajax modal function 2025-02-19 17:10:41 -05:00
johnnyq 2fe8adcfca Simplified Client URL Handling in global client header logic 2025-02-19 15:58:43 -05:00
johnnyq a2c77cf0c2 Merged Client and Global Calendar into one code base seperated them with some header logic 2025-02-19 15:05:37 -05:00
johnnyq eab441da06 No need to enforce client permission check as its already handled in inc_all_client.php 2025-02-19 14:23:32 -05:00
johnnyq 19daa289b2 Merged Files Vendors with Client Vendors seperated them with some header logic 2025-02-19 14:18:16 -05:00
johnnyq d228c30b03 Merged Global Tickets with Client Tickets seperated them with some header logic 2025-02-19 13:45:44 -05:00
johnnyq a5ff978a77 Merged Global Recurring Invoices with Client Recurring Invoices seperated them with some header logic 2025-02-19 12:10:03 -05:00
johnnyq 78cfda1dbc Merged Global Recurring Tickets with Client Recurring Tickets seperated them with some header logic 2025-02-19 11:56:49 -05:00
johnnyq 78ae44c334 Migrated domain and certificate edit to the new AJAX modal function 2025-02-19 01:22:31 -05:00
johnnyq e6e30dcd7c Migrated domain edit to the new AJAX Modal functon 2025-02-19 00:44:51 -05:00
johnnyq 17489e60c8 Merged Client Payments and Payments into one file using logic at the header to seperate between client and global section 2025-02-18 21:45:12 -05:00
johnnyq f723450d51 Merged Client Invoices and Invoices into one file using logic at the header to seperate between client and global section 2025-02-18 21:20:40 -05:00
johnnyq f8e4163c2f Merged Client Trip and Global Trips along with exports into 1 managable file 2025-02-18 20:58:47 -05:00
johnnyq 1dee1b4abb Add missing LIMIT 1 to ajax edit items 2025-02-18 20:29:36 -05:00
johnnyq 6f8cc40167 Changed header names from logins to credentials in asset and contact details, migrated more items to the new AJAX modal function 2025-02-18 20:25:02 -05:00
johnnyq 88ee9b60e7 Migrated Tax, custom link edit over to the new AJAX model function 2025-02-18 18:47:28 -05:00
johnnyq a9e3e4744d Added back option for 500 records per page as we have successfully moved most of the repeating row modals to ajax which will allow super improved performance 2025-02-18 18:10:26 -05:00
johnnyq b9f6871bae Migrated Edit Line Item to new AJAX modal function, adjusted the logic to determine line item type (invoice, quote, recurring) in the post instead of the form 2025-02-18 18:06:31 -05:00
johnnyq f920b8fac9 Fixed missing vendor name and category name for receipt filename in edit expense 2025-02-18 17:29:58 -05:00
johnnyq d8955f1f2d Added Asset Purchase reference to assets to reference an invoice or PO number 2025-02-18 17:22:22 -05:00
johnnyq 43b50c44b1 Combined client_quotes.php and quotes.php into one file quotes.php checks if get client_id var is set and which chooses to display client top header and client side nav or global nav 2025-02-18 17:04:49 -05:00
johnnyq f6e33d5892 Added Document Quick View to asset and contact details 2025-02-18 11:57:28 -05:00
johnnyq 206dfc2426 Show related documents in asset details 2025-02-18 11:47:51 -05:00
johnnyq 2467274f7b Ported users, roles, categories, tags edit to new AJAX modal function 2025-02-17 19:29:31 -05:00
johnnyq e02d45db3e More Migration to AJAX Modal 2025-02-17 16:44:29 -05:00
johnnyq 650b8754e8 Migrated Edit Recurring Invoice, Revenue over to the new AJAX modal 2025-02-17 11:57:10 -05:00
wrongecho 64a6b7dc39 Client files - allow uploading .key files 2025-02-17 12:01:36 +00:00
wrongecho 7aa773b336 Update changelog 2025-02-17 11:56:17 +00:00
wrongecho a8b63ed429 Quote - remove reference to old modal 2025-02-17 11:42:51 +00:00
wrongecho 08f2a307d3
Merge pull request #1166 from itflow-org/quote-upload
Ability to upload files to an approved quote
2025-02-17 11:31:46 +00:00
wrongecho 72a84af1a7
Merge branch 'develop' into quote-upload 2025-02-17 11:27:05 +00:00
wrongecho a8f682286a Add DB logic for quote file upload 2025-02-17 11:14:55 +00:00
johnnyq b0a79c1b6f remove legacy quote copy modal, dont show client selection in quote copy when copying quote from within client section 2025-02-16 23:55:59 -05:00
johnnyq ccec330ceb Migrated project, transfer and quote edit to the new AJAX modal function 2025-02-16 23:14:32 -05:00
johnnyq 093fd69415 Migrated Ticket Priority, Assign Agent, Assign Contact, Ticket Billable, Task edit, task_template edit to the the new AJAX Modal 2025-02-16 18:27:43 -05:00
johnnyq 3cf33afac4 Let AJAX handle the model header 2025-02-16 13:20:15 -05:00
johnnyq 5f7b297cf1 UI tidy work on header for Viewing Message in Queue 2025-02-15 22:21:15 -05:00
johnnyq b6ffa0595d migrated credentials and expenses edit modals to the new AJAX function 2025-02-15 21:47:43 -05:00
johnnyq 4a3154baca Remove the old asset edit modal in favor of the new AJAX function 2025-02-15 20:36:37 -05:00
johnnyq ca0b1a1fb7 Edit and Copy Assets is now using the new AJAX function 2025-02-15 20:35:35 -05:00
johnnyq 5f02bc07aa Delete the old asset Interface edit modal as we are using the new AJAX 2025-02-15 18:12:00 -05:00
johnnyq 7c070e3929 Migrated edit asset interface to use the new AJAX function 2025-02-15 18:11:01 -05:00
johnnyq ed0b192f5b Added data-modal-size to the new ajax generic modal functin to define modal size if left blank it will use standard md, also moved Email Message View to use the new ajax modal 2025-02-15 17:02:35 -05:00
johnnyq 36e1df93bb Moved edit client, notification, and create contact note to the new ajax system, also updated the notificaiton count to be more efficient 2025-02-15 16:19:35 -05:00
johnnyq 615604661c Seems like I have to readd some Javascript functions into the ajax footer to get certain function to work on the generic ajax modal 2025-02-15 15:01:24 -05:00
johnnyq 9458c1cc7a Added data-toggle='ajax-modal' to trigger the ajax modal instead, also added an ajax header and and footer to dedup code 2025-02-15 14:50:48 -05:00
johnnyq 298a584377 Remove Test Code that end up not using 2025-02-15 00:53:03 -05:00
johnnyq 74a1603943 Started work on AJAX with a different approach, this is WIP but edit contact currently uses it. with this approach your dont have to touch the js or modal template just the ajax 2025-02-15 00:50:52 -05:00
johnnyq 117861034a Enhanced the add/edit interface Modal UI into tabs 2025-02-14 13:42:15 -05:00
johnnyq 7452e8f08e Update some Asset Details Headings 2025-02-13 16:45:20 -05:00
johnnyq 5ed5473b36 Add Nat IP Field to Interfaces 2025-02-13 16:34:27 -05:00
johnnyq 2e9e49a203 Added the Ability Set which interface is primary under add / edit interface section 2025-02-13 16:26:21 -05:00
johnnyq c81b430318 Feature: Ability to import and export Asset Interfaces via CSV 2025-02-13 15:58:19 -05:00
johnnyq 33beae68e5 Tickets can not be categorized in bulk 2025-02-12 19:48:16 -05:00
johnnyq 1b8cd5630a Fix recurring Tickets priority var 2025-02-12 18:54:48 -05:00
johnnyq b8d17be015 Fix Ticket Priority Sorting, now sorts by prioty when clicked 2025-02-12 18:45:05 -05:00
johnnyq fe762ef926 Asset Interfaces: Renamed Port to Description, added Interface Type 2025-02-12 17:13:39 -05:00
johnnyq 8d8b0f4a48 Feature: Ability to create multiple interfaces/ports in one swoop with interface name and port prefix along with network assignment and notes 2025-02-12 15:27:31 -05:00
johnnyq a2e16f918c Removed Task Order Input from Task edit modal as this is done by dragging and dropping now 2025-02-11 17:29:53 -05:00
johnnyq 228a457518 Remove dup css styles 2025-02-11 17:25:36 -05:00
johnnyq f0cc9412bb Added custom css class grab-cursor so it turns the cursor into a grab hand instead of a finger, applied this to ticket tasks 2025-02-11 17:24:47 -05:00
johnnyq 7509301f5c Fix Edit Login under Contact and Asset Details, had to redo the SQL query as login_id was being used in tag as well and sometimes would get a login_id 0 2025-02-11 16:41:59 -05:00
johnnyq d97da07cb1 Restricted users by client now shows the correct client count, also moved the side nav count to a new include file called get side nav counts 2025-02-11 13:33:32 -05:00
johnnyq 2ec38606e0 Fixed URI links for credentials in asset details and contacts details 2025-02-11 13:17:56 -05:00
johnnyq 2c85922788 Clone all Interfaces of asset when transfering to a new client jut the name, port, primary, mac 2025-02-10 17:15:46 -05:00
johnnyq fc24bcb24f Moved Transfer Asset to Client to a Bulk Action and create Primary Interface when moving with MAC address 2025-02-10 16:58:39 -05:00
Johnny 08ebedef03
Merge pull request #1165 from ssteeltm/tasks_moves
ticket tasks moving order
2025-02-10 14:07:40 -05:00
Hugo Sampaio eac46d0da0 fix conflitct 2025-02-10 10:31:03 -03:00
johnnyq a69b60703b Dedup Notications code and move to a modal instead of a drop menu 2025-02-08 19:54:49 -05:00
johnnyq 4ed0767f1c Show record details in listing filter footer Showing X of X of X Records 2025-02-08 18:47:23 -05:00
johnnyq 72ae7843f6 Show record details in listing filter footer Showing X of X of X Records 2025-02-08 18:45:19 -05:00
Johnny d92b803526
Merge pull request #1158 from ssteeltm/kanban_tickets
tickets views
2025-02-08 18:22:35 -05:00
Johnny 506783f022
Merge pull request #1167 from itflow-org/portal-custom-links
Allow adding custom links to the client portal nav-bar
2025-02-06 10:33:17 -05:00
wrongecho 2fe7bf6870 Taking a different approach. Utilize the existing file upload functionality and just link the file to the quote. 2025-02-05 22:48:15 +00:00
wrongecho c36fb6ae12 rv 2025-02-05 15:44:33 +00:00
wrongecho 55c761b11e rv 2025-02-05 15:44:09 +00:00
wrongecho a769993fa8 swap donate link for sponsor services link 2025-02-05 15:15:28 +00:00
wrongecho 6df3a35766 Move reports perms to new role system 2025-02-05 14:51:53 +00:00
wrongecho abd985d0f4 Create report landing page from nav menu 2025-02-05 14:50:21 +00:00
wrongecho db5cfed96f Update to new perms system 2025-02-05 14:31:46 +00:00
wrongecho 9ce502c1b9 Comment old perms functions 2025-02-05 14:31:31 +00:00
wrongecho 0172895e3c Rename password rotation report to credential rotation report 2025-02-05 14:19:43 +00:00
wrongecho a29bfc3cb5 Only show tasks if ticket is open OR when ticket is resolved & there were tasks 2025-02-05 10:50:59 +00:00
wrongecho 341ff69c58 Only show tasks if ticket is open OR when ticket is resolved & there were tasks 2025-02-05 10:48:16 +00:00
wrongecho 66ab0d3f11 Update changelog 2025-02-05 10:24:31 +00:00
wrongecho b147bc46cf Client Portal Tickets - Show category field, tidy the perms check 2025-02-05 10:21:06 +00:00
Hugo Sampaio 11203f1ad2 notification when resolved 2025-02-04 17:27:41 -03:00
Hugo Sampaio ee2e4b671d ticket kanban settings 2025-02-04 17:07:42 -03:00
Hugo Sampaio 65bb1b4007 alter ticket_kanban to ticket_order 2025-02-04 13:35:28 -03:00
wrongecho cdc0422d31 Allow adding custom links to the client portal nav-bar 2025-02-04 16:04:44 +00:00
wrongecho 1bbf7c7662 rm old debugging message 2025-02-04 15:10:10 +00:00
wrongecho 1ca31662f8 Cron scripts should log using logApp 2025-02-04 14:24:41 +00:00
Hugo Sampaio 3de97fcd15 nullable_htmlentities 2025-02-04 10:47:23 -03:00
wrongecho 9218d828b0 Fix unbilled tickets report permissions - requires sales read rights (in addition to general reporting) 2025-02-04 13:39:42 +00:00
wrongecho 4ed9c5cfb8 Start new changelog 2025-02-04 13:30:22 +00:00
Hugo Sampaio 17cd1fd0c1 ajax cleanup 2025-02-04 10:16:52 -03:00
Hugo Sampaio c7b9c95d0d missing tickets_kanban.css 2025-02-04 09:59:45 -03:00
Hugo Sampaio c0360f4e68 kanban js/css files 2025-02-04 09:58:18 -03:00
wrongecho 571ed4818e rm desc field; we don't store the description, the file name is enough because we know it's already related to the quote 2025-02-03 22:32:25 +00:00
wrongecho 04226101af Add ability for client to upload attachments to approved invoices 2025-02-03 22:19:32 +00:00
wrongecho b50c2295a6 Add logic for showing quote attachments in the admin/tech side 2025-02-03 22:19:06 +00:00
wrongecho 75e002a054 Update git-ignore for new quotes dir 2025-02-03 22:18:22 +00:00
Hugo Sampaio e6c18970b3 fix: explicit default declaration 2025-02-03 18:04:49 -03:00
Hugo Sampaio 00a9c53fc4 dragula plugin 2025-02-03 11:59:18 -03:00
Hugo Sampaio 942f5bff52 ticket tasks moving order 2025-02-03 11:55:20 -03:00
Johnny 9ea41c6ae5
Merge pull request #1163 from itflow-org/develop
Release 25.01.3
2025-01-31 13:52:39 -05:00
Johnny c40d15a545
Merge pull request #1162 from itflow-org/tck-assign-modal
Bugfix: Amend SQL query for ticket assignment modal so contacts aren't shown
2025-01-31 13:48:08 -05:00
wrongecho 4c966f19e5 Bugfix: Amend SQL query for ticket assignment modal so client contacts aren't shown 2025-01-31 18:42:27 +00:00
johnnyq 73d0cb7497 Removed reports_contacts.php as it was incomplete for the hotfix 3 2025-01-31 13:23:49 -05:00
Hugo Sampaio e3ae8df4d8 new changes 2025-01-31 11:50:06 -03:00
johnnyq ef06c4141f Started work on Global Contacts, Added PrettyContent to guest footer and updated the guest document view to use it so images are responsive and tables show up correctly 2025-01-30 18:24:05 -05:00
Johnny 0dca0ce4bf
Merge pull request #1161 from itflow-org/develop
Release 25.01.2
2025-01-30 15:01:20 -05:00
johnnyq b5be8605d6 Final Version Fix in Changelog 2025-01-30 14:58:18 -05:00
johnnyq 2268a41b00 Update App version to 25.01.2 2025-01-30 14:55:06 -05:00
johnnyq 8efd22f47b Fix App Versioning 2025-01-30 14:54:09 -05:00
Hugo Sampaio f4eaba4384 fix whitespace qualitygate 2025-01-29 16:53:52 -03:00
Hugo Sampaio 9ee159c458 fix card move, after qualitygate changes 2025-01-29 16:52:12 -03:00
Hugo Sampaio 180635f318 update 2025-01-29 16:30:04 -03:00
Hugo Sampaio e498cc6036 update tickets_kanban.php 2025-01-29 16:27:53 -03:00
Hugo Sampaio b6e0990a78 update tickets.php 2025-01-29 16:21:08 -03:00
Hugo Sampaio 04fac54987 update tickets_list.php 2025-01-29 16:17:29 -03:00
Hugo Sampaio b31d1eba6a update ticket_kanban.php 2025-01-29 16:12:32 -03:00
Hugo Sampaio dec91d116a update tickets_compact.php 2025-01-29 16:10:40 -03:00
Hugo Sampaio 1fb243df11 update compact list 2025-01-29 16:03:24 -03:00
Hugo Sampaio c02b267d44 update tickets.php 2025-01-29 15:55:58 -03:00
Hugo Sampaio ed5aa9a0c2 sync last changes
sync last changes on days  27 and 28
2025-01-29 12:00:52 -03:00
Hugo Sampaio 3e0e72dedc database update for kanban 2025-01-29 11:39:54 -03:00
Hugo Sampaio 3aa26226e5 kanban post actions 2025-01-29 11:08:42 -03:00
Hugo Sampaio bb787cdc70 tickets initial idea of views
Kanban view
Compact view
2025-01-29 11:01:53 -03:00
4131 changed files with 405990 additions and 105735 deletions

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
custom: ["https://donate.itflow.org"]
custom: ["https://services.itflow.org"]

29
.gitignore vendored
View File

@ -4,8 +4,16 @@ config.php
uploads/favicon.ico
uploads/clients/*
!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/index.php
uploads/recurring_tickets/*
!uploads/recurring_tickets/index.php
uploads/settings/*
!uploads/settings/index.php
uploads/users/*
@ -14,6 +22,8 @@ uploads/tmp/*
!uploads/tmp/index.php
uploads/tickets/*
!uploads/tickets/index.php
uploads/ticket_templates/*
!uploads/ticket_templates/index.php
.idea/*
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/*
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/.gitkeep
@ -26,3 +36,22 @@ xcustom/*
!xcustom/readme.php
post/xcustom
!post/xcustom/readme.php
admin/custom/*
!admin/custom/readme.php
agent/custom/*
!agent/custom/readme.php
client/custom/*
!client/custom/readme.php
guest/custom/*
!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

View File

@ -2,7 +2,655 @@
This file documents all notable changes made to ITFlow.
## [25.01.01]
## [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]
### Breaking Changes
- Renamed `/user/` directory to `/agent/`.
- Deprecation Notice: `/scripts/cron_mail_queue.php` and `/scripts/cron_ticket_email_parser.php` are being phased out. Please transition to `/cron/mail_queue.php` and `/cron/ticket_email_parser.php`. These older scripts will be removed in the November release—update accordingly. New Installs via the script will have this already configured.
- Custom is working now. Custom code should be placed in /admin/custom/ , /agent/custom/ , /client/custom/ /guest/custom/
We will provide example code with directory structure for each custom directory a week after this release.
### Fixes
- Resolved issue with "Restore from Setup" not functioning correctly.
- Corrected asset name display in logs and flash messages when editing an asset in a ticket.
- Fixed Payment Provider Threshold not being applied.
- Fixed issue where Threshold setting was not saving properly.
- Various minor fixes for Payment Provider issues.
- Removed leads from the client selection list in the "New Ticket" modal.
- Fixed issues with the MFA modal.
- Resolved MFA enforcement bugs.
- Fixed KeepAlive functionality to maintain user sessions longer.
- Fixed multiple broken links caused by the `/user/` to `/agent/` path migration.
- Fixed Custom code directories.
### Added / Changed
- Removed "ACH" as a payment method; added "Bank Transfer" instead.
- Replaced relative paths with absolute paths for web assets.
- Tickets can now be resolved via the API.
- Added a filter for Archived Users and an option to restore them.
- Introduced a modal when archiving users, allowing reassignment of open and recurring tickets to another agent.
- Improved logic for determining the index/root page.
- Added "Assigned Agent" column for recurring tickets.
- Introduced "Additional Assets" option when editing assets in tickets; modal now uses the updated AJAX method.
- Added Gibraltar to the list of supported countries.
- Added Custom Link Option for the Admin Nav.
- Added Custom Link Option for the Reports Nav.
### Other notes
- Major releases will happen on the first week of every Month.
## [25.09.2]
### Fixes
- Fix Payment Method Select box in Revenue.
- Remove Extra Feeback Wording When Invoice Sends.
- Updated all CSV exports to use escape parameters.
- Fix Missing First row on Asset interface export.
- Fix Edit User not working due to incorrect modal footer path.
- Fix Add Certificate breaking due spelling on function.
- Update all CSV Exports to include company name or client name depending on when its being exported from.
- Introduced new function sanitize_filename and implmented it in all exports.
- Spruced up UI/UX Saved Paymented section in Client Portal.
- Fix add Payment Link in client portal recurring invoice section.
- Better Logic handling for default page redirect.
### Features
- Introduced new Beta mail parser cron using webklex imap library instead of php-imap as this is deprecated --Not Enabled on existing installs, only new installs.
- Introduced Beta support for OAUTH2 Authentication for Microsoft 365 and Google Workspaces for both incoming ticket parsing and outgoing email but must use new mail parser and mail queue for this to work, and requires changing the cron jobs: scripts/cron_mail_queue.php to cron/mail_queue.php and scripts/cron_ticket_email_parser.php to cron/ticket_email_parser.php.
---
## [25.09.1]
### Fixes
- **Web Installer**: Resolved issue with broken installer caused by incorrect database schema file name.
- Hide the "Add Credit" button as the feature is not fully implemented yet.
- Corrected long invoice/quote notes that were overlapping with the footer in PDF exports.
- Fixed AI settings not appearing in the Admin Menu when the Billing module was disabled.
- Enabled wrapping of client tags when they are too long.
- Fixed an issue where AI was not functioning correctly.
- Removed extra spacing between the contact name and icon in the Ticket Details contact card.
### Features
- Redesigned **AI Ticket Summary**, now divided into 3 sections: Main Issue, Actions Taken, and Resolution/Next Steps.
- Updated the **AI Ticket Summary** prompt to include ticket status, reply author, source, category, and priority.
---
## [25.09]
***BACK UP*** before updating.
---
### Breaking Changes and Notes
- We strongly recommend updating from the command line, however if performed via the webui and after performed it will return a 404. thats normal as the directory structure has changed, just close your browser then log back in then go back to update to perform the many database updates.
- This is a major release with significant changes. While the community has done a great job identifying bugs, some may still remain — continued testing is encouraged.
- All AI settings will be **reset** and must be reconfigured using the new AI provider backend.
- The `xcustom` directory has been renamed to `custom`. All custom libraries and post-processing scripts should now be placed here.
---
### Added / Changed
- Numerous UI improvements and refinements across the application.
- Enhanced visual clarity by thickening the left border on ticket comments to help identify comment types.
- Ticket details UI redesigned to use less space at the top of the screen.
- Introduced tracking for the **first response date/time** on tickets.
- New reporting feature: **Average time to first response** on tickets.
- Stripe integration rebuilt using the new **payment provider backend**.
- Clients can now save and manage **multiple payment methods**.
- Support for selecting saved cards for **recurring invoices** in both the client and agent portals.
- Initial database structure and logic added for **credit management** (feature not yet enabled).
- Major **backend directory restructuring**.
- Introduced **stock/inventory management**, including a stock ledger backend.
- Stock quantities now update automatically when invoice items are added or removed.
- Invoice autocomplete now includes: **name, description, price, tax, stock levels**, and links `product_id` to `item_id`.
- Added a **category filter** to invoices.
- Linked stock to related expenses.
- New product fields: **location, code, and type**.
- Products now separated into two types: **Service** and **Product**.
- **Dark mode** introduced.
- Projects: Now support linking **closed tickets**.
- Clients: Added bulk actions for tags, referral source, industry, hourly rate, email, archive, and restore.
- Invoices: Bulk action added to **assign categories**.
- Assets: New `client_uri` field, visible in both the agent and client portals.
- Client Portal: Clients can now **select an asset** during ticket creation.
- Client Portal: Company logo now **displays in the header**.
- Client Portal: Dashboard cards are now **clickable** for more detail.
- Assets: Option added to include **MAC Address** in additional columns.
- Asset Interface: Bulk actions added — set DHCP, network type, and delete.
- API:
- Added `/location` endpoint.
- Ticket content now supports **HTML formatting**.
- New option to filter and display **500 records per page** in the footer.
- Payment methods are now treated as a **separate entity** instead of being grouped under categories.
- Updated libraries:
- **TinyMCE**
- **Chart.js** (major upgrade)
- **DataTables**
- **Bootstrap**
- **FullCalendar**
- **php-stripe**
---
### Fixed
- Several security vulnerabilities patched (with thanks to www.helx.io).
- Ticket status is no longer updated when scheduling.
- Client Portal: Tech contacts can no longer edit their own details.
- Fixed overlapping logo issue in Invoice/Quote PDF exports.
- Refactored `check_login.php` into multiple files for modular login functionality.
- Removed redundant logging comments for redirects.
- Renamed `get_settings.php` to `load_global_settings.php`.
- Simplified syntax for `ajax-modal` and updated usage throughout the app.
- Fixed issue where primary contact text wasnt displaying.
- Corrected client **Net Terms** display.
- Fixed logic for recurring expense **next run date**.
- Resolved broken **IMAP test button**.
- Archived clients can no longer log into the portal.
- Searching closed tickets no longer reverts to open tickets.
- Fixed project search filter not showing completed projects.
- Fixed issue where company logo was not being removed correctly.
- Resolved API bugs:
- Default rate and net terms.
- Contact location.
- Document endpoint.
---
### Developer Updates
- Replaced legacy code with newer functions like `redirect()`, `getFieldById()`, and `flash_alert()`.
- Significantly improved performance of queries used for filter selection boxes.
## [25.06.1]
### Fixed
- Fixed a regression in setup causing it to crash and never complete, due to missing default for currency.
## [25.06]
### Breaking CHANGES
- Old Document Verions will be deleted due to the major backend rewrite how document versions work.
### Added / Changed
- Improved function for retrieving remote IP address for logging purposes.
- Ticket categories are now sorted alphabetically.
- Visiting a deleted invoice or recurring invoice now redirects to the listing page; delete option added to invoice details page.
- Added "Mark as Sent" and "Make Payment" actions directly on the invoice listing page.
- Introduced Ticket Category UI for recurring tickets.
- In Project Details, bulk actions and sorting are now available for tickets.
- Updated ticket details UI to use full card stacks with edit icons for stackable items (e.g., asset, watchers, contact).
- Added a new setting to toggle AutoStart Timer in ticket details (disabled by default).
- Applied gray accent theme in the client section to visually distinguish from the global view.
- Introduced Ticket Due Date functionality (currently supports add/edit only; more updates coming next release).
- Added settings option to display Company Tax ID on invoices.
- Client overview now displays badge counts for all entities.
- Overhauled UI for Invoice, Quote, and Recurring Invoice details; switched PDF generation to TCPDF PHP from PDFMake JS.
- Document versioning has been moved to a separate backend table to resolve permanent link issues -- SEE Breaking CHANGES.
- Migrated Document Templates, Vendor Templates, and Software/License Templates to dedicated tables.
- Added functionality to mark all tasks in a ticket as complete or incomplete.
- Asset CSV import now supports a purchase date field.
- Recurring Payments have been restructured to auto-charge on the invoice due date instead of at generation time.
- Added "Base Template" label for vendor templates when available.
- Backup and restore processes now use a temporary directory; files are cleaned up automatically if operations fail.
- Added confirmation prompt when accepting or declining a quote.
- Other minor code UI/UX cleanups and refactoring throughout the app.
### Fixed
- Resolved issue with enabling MFA.
- Fixed UI regression where ticket listing columns would misalign.
- Non-billable invoices are no longer included in calculations.
- Addressed multiple minor reported security vulnerabilities.
- Tickets with open tasks are no longer resolved in bulk; a warning is shown along with a count of affected tickets.
## [25.05.1]
### Added / Changed
- Added Domain Expiring Card to Client Portal Dashboard for Primary and Technical Users.
- Added Balance and Monthly Recurring Amount to Client Portal Dahboard for Primary and Technical Users.
- Added Archive Searching to network and certificates also added unarchive capabilities to them as well.
### Fixed
- Add Payment not showing in Invoice.
- Updated Client Overview Entities to not show archived client's Entities even though the entity may not be archived.
## [25.05]
### Added / Changed
- Expanded file upload allow-list to include .bat and .stk file types.
- Added full backup/restore functionality. Backup downloads a zip that includes the SQL dump and uploads folder, setup now has option to restore from zip backup.
- Migrated Asset and Contact Links to modals to resolve variable overlap issue.
- Added Pagination to Notification Modal.
- Removed 500 Records Per Page option.
- Removed unused old DB checks in the top nav.
- Clients can now use the portal to setup Stripe automatic payments themselves for recurring invoices
- Automatic payments are now disabled for all recurring invoices if the saved payment method is removed
- Added Card Details and Payment added to Client Stripe.
- UI / UX updates to guest pay Make use of cards.
- Don't show Checkbox columns when ticket is closed, compact ticket list now matches round pills for status and priority.
- Ticket UI/UX update allow the ticket toolbar to be a little more mobile-friendly
- UI / UX Updates to Expenses - Combine Category and Description into 1 column.
- Country information is now displayed in Invoices, Quotes, Recurring Invoices, Clients, Locations, and the client top header.
- Added country-based search filters in Locations and Clients sections.
- Changed the settings name from Integrations to Identity Providers to make room for future iDPs (e.g. Google).
- Bump FullCalendar from 6.1.15 to 6.1.17.
- Bump DataTables from 2.2.2 to 2.3.1.
- Bump TCPDF from 6.8.2 to 6.9.4.
- Bump tinyMCE from 7.7.1 to 7.9.0.
- Bump phpMailer from 6.9.2 to 6.10.0.
- Bump stripe-php from 16.4.0 to 17.2.1.
### Fixed
- "None" option for SMTP encryption now functions correctly.
- Debug table row counts now reflect actual counts instead of relying on SHOW TABLE STATUS.
- Archived Categories now display properly.
- Stripe saved payment methods are now limited to credit/debit cards only.
## [25.03.6]
### Fixed
- Set default to date to 2035-12-31 as 9999-12-31 and 2999-12-31 broke certain browsers.
- Update Client PDF Export, add header added company logo.
- Present Larger clearer Warning about updates on update page.
- Allow to search by project reference.
## [25.03.5]
### Fixed
- Fixed the user listing issue when copying a trip.
- Corrected the display of recurring invoice amounts on the dashboard.
- Fixed the linking of entities with assets and contacts.
- Resolved the issue with displaying the correct mobile country code in the contact listing.
- Set the default date to `9999-12-31` to ensure future items (like invoices) are displayed by default.
- Fixed the display issue where file folders were not showing properly during document creation.
- Migrated from Dragula to SortableJS for a more modern, mobile-friendly solution.
- Added Handlebars icons for drag-and-drop items.
- Changed behavior to open Contact and Asset Details pages directly instead of using a modal.
## [25.03.4]
### Fixed
- Ability to remove additional assets from the ticket details screen.
- Fix the ability to remove assets from edit ticket not working when only 1 asset exists.
- Fix Database Backup corruption.
- Client Portal - show ticket number instead of ticket id in ticket listing.
- Add Purchase Reference to copy asset.
- Add Link to asset details from the global search.
- Fix Bulk assign ticket only showing contacts instead of ITFlow users.
## [25.03.3]
### Fixed
- Fix adding ITFlow user.
- Do not alert on inactive recurring invoices.
- Fix ticket user assignment including bulk assignment.
- Fix adding a location phone extension.
- Do not default to +1 Country code, instead default to null.
- Do not format numbers unless a country code is entered.
- Fix editing network location.
- Fix ticket redaction on client replies.
- Remove more from user activity as it requires admin privledges.
- Fix MFA Enforcement page.
## [25.03.2]
### Fixed
- Revert DB.sql change
## [25.03.1]
### Fixed
- Phone number missing in various sections.
- Match Database.
- Client Export Only display licenses users and assets from the selected client only.
## [25.03]
### Fixed
- Resolved missing attachments in ticket replies processed via the email parser.
- Fixed issue where the top half of portrait image uploads appeared cut off at the bottom.
- Ensured all tables and fields use `CHARACTER SET utf8mb4` and `COLLATE utf8mb4_general_ci` for updates and new installations.
- Converted `service_domains` table to use InnoDB instead of MyISAM.
- Fixed the initials function to properly handle UTF-8 characters, preventing contact-related issues.
- Interfaces can now start with `0`.
- Adjusted AI prompt handling to focus solely on content, avoiding unnecessary additions.
### Added / Changed
- Introduced bulk delete functionality for assets.
- Added the ability to redact ticket replies after a ticket is closed.
- Added support for redacting specific text while a ticket is open.
- Switched file upload hashing from SHA256 to MD5 to significantly improve performance.
- Enabled assigning multiple assets to a single ticket.
- Updated all many-to-many tables to support cascading deletes using foreign key associations, improving efficiency, performance, and data integrity.
- Enabled caching for AJAX modals to reduce repeated reloads and enhance browser performance.
- Upgraded DataTables from 2.2.1 to 2.2.2.
- Upgraded TinyMCE from 7.6.1 to 7.7.1, providing a significant performance boost.
- Added “Copy Credentials to Clipboard” button in AJAX asset and contact views.
- Renamed and reorganized several tables.
- Improved theme color organization by grouping primary colors and their related shades.
- Displayed a user icon next to contacts who have user accounts.
- New image uploads are now converted to optimized `.webp` format by default; original files are no longer saved. Existing images remain unchanged.
- Added international phone number support throughout the system.
- Introduced user signatures in preferences, which are now appended to all ticket replies.
- Optimized search filters to only display defined tags.
- Added “Projects” to the client-side navigation.
- Enabled “Create New Ticket” from within project details.
- Reintroduced batch payment functionality in client invoices.
- Included client abbreviations in both client and global search options.
- Added assigned software license details (User/Asset) to the client PDF export.
- Replaced client-side `pdfMake` with the PHP-based `TCPDF` library for generating client export runbooks.
- Introduced the ability to download documents as PDFs.
- Added a “Reference” field to tickets and invoices generated from recurring templates (not yet in active use).
### Breaking Changes
> **Important:** To update to this version, you **must** run the following commands from the command line from the scripts directory:
>
> ```bash
> php update_cli.php
> php update_cli.php --db_update
> ```
>
> Repeat `--db_update` until no further updates are found.
>
> **Back up your system before upgrading.**
> This version includes numerous backend changes critical for future development.
## [25.02.4]
### Fixed
- Resolved issue preventing the addition or editing of licenses when no vendor was selected.
- Fixed several undeclared variables in AJAX contact details.
- Corrected the contact ticket count display.
- Addressed an issue where clicking "More Details" in AJAX contact/asset details failed to include the `client_id` in the URL.
- Fixed an issue with recurring invoices in the client URL: clicking "Inactive" or "Active" would unexpectedly navigate away from the client section.
- Added new php function getFieldById() to return a record using just an id and sanitized as well.
## [25.02.3]
### Fixed
- Fixed notifications being reversed as dismissed notifications.
## [25.02.2]
### Fixed
- Corrected some edit modals not showing notes correctly.
- Bugfix: When exporting to CSV, the first asset wasn't being shown.
- Fix broken create / edit credentials.
- Fixed missing Notificatons link.
- Fixed a few dead links.
- Fixed Overdue count also counting Non-Billable Invoices.
- Fix Edit Client Notes.
### Added / Changed
- Implemented SSL certificate history tracking.
- Added Inactive / Active Filter to Recurring Invoices.
- Merged Dismissed notifications and notification in one.
- Added Link Button to addd / edit Document WYSIWYG.
- Added Physical location to the asset export / import.
## [25.02.1]
### Fixed
- Resolved broken links in the client overview, project and client listings, and rack details.
- Corrected asset transfer functionality to clients.
- Fixed the ticket scheduling redirect.
- Corrected the ticket link in the Scheduled Ticket Agent Notification email.
- Addressed issues with credentials and ticket actions in the Contact Detail Modal.
- Fixed text wrapping in notifications.
- Adjusted notifications so that they are sorted with the newest first.
- Fixed drag-and-drop functionality for tickets in the Kanban view on mobile devices.
- Resolved a weird issue with TinyMCE that prevented using links referencing your ITFlow instance url.
- Corrected image orientation issues during upload and the preview optimization process.
### Added / Changed
- Introduced entity link indicator icons and counts in the contacts and credentials section.
- Implemented a fade animation for the new AJAX modal.
- Removed the Client Overview Expire Day Select and replaced it with simplified 1, 7, or 45-day options.
- Added the ability to link and unlink entities within asset details.
- Introduced quick tag/category creation across the app.
- Added a Vendor Quick Details Modal.
- Enabled vendor linking and added a License Purchase Reference in the Software Licenses section.
- Added download original, optimized and thumbnail option for images.
- Added Paid status to the top corner of Invoice PDFs.
## [25.02]
### Fixed
- Migrated several reports to the new permissions/roles system.
- Resolved issue with empty task box showing for closed/resolved tickets.
- Corrected ticket priority sorting.
- Cloned asset interfaces when transferring assets between clients.
### Added / Changed
- Restored max number of records per page option back to 500 since we dont have repeating modals.
- Bulk Categorize Tickets feature.
- Renamed "Interface port" to "Interface Description." "Interface Name" should now refer to port name and/or number.
- Changed "Transfer Asset to Client" from a single action to a bulk action.
- Updated Filter Footer UI to show "Showing x to x of x records" instead of just the total records.
- Added Client Overview section to view client assets, contacts, licenses, credentials, etc.
- Introduced Quick Peek for asset details, contact information, and document viewing throughout the ITFlow App, all made possible by AJAX.
- Enabled Simple Drag-and-Drop Ordering for Invoices, Recurring Invoices, Quotes, Ticket Tasks, and Ticket Template Tasks.
- Added new Ticket View options: Kanban and Simple View.
- Migrated all repeating modals to the new AJAX modal function for faster loading times and quicker development.
- Allowed clients to upload PDF documents to accepted quotes.
- Client Portal now shows ticket category.
- Custom links can now be added to the Client Portal navbar.
- Lots of little tweaks to UI, performance, bugs, etc.
### Breaking Changes
- Cron scripts have officially been moved to the /scripts folder and are no longer in the root directory; they must be updated to function properly.
## [25.01.3]
### Fixed
- Fixed ticket assignment modal showing client contacts.
## [25.01.2]
### Fixed
- Fixed app version.
## [25.01.1]
### Added / Changed
- Redesigned the Multi-Factor Authentication (MFA) Setup and Enforcement Flow UI/UX for a more intuitive user experience.
@ -79,4 +727,4 @@ This file documents all notable changes made to ITFlow.
## [24.12]
### Added / Changed
- Introduced versioned releases for the first time!
- Introduced versioned releases for the first time!

View File

@ -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:
- CompuMatter
- F1 for HELP
- JetBrains (PhpStorm)
## License
ITFlow is distributed "as is" under the GPL License, WITHOUT WARRANTY OF ANY KIND. See [`LICENSE`](https://github.com/itflow-org/itflow/blob/master/LICENSE) for details.

View File

@ -12,10 +12,8 @@
We operate a rolling release model. Any bug fixes will be released into latest version of ITFlow, so you must stay up-to-date.
| Version | Supported |
| ------- | ------------------ |
| Beta | :x: |
| 24.12 | :white_check_mark: |
| 25.1 | :white_check_mark: (When released) |
|---------| ------------------ |
| 25.12 | :white_check_mark: |
## Reporting a Vulnerability via GitHub Security Advisories

117
admin/ai_model.php Normal file
View File

@ -0,0 +1,117 @@
<?php
// Default Column Sortby Filter
$sort = "ai_model_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM ai_models LEFT JOIN ai_providers ON ai_model_ai_provider_id = ai_provider_id ORDER BY $sort $order");
$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-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Models</h3>
<div class="card-tools">
<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 class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_model_name&order=<?php echo $disp; ?>">
Model <?php if ($sort == 'ai_model_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_name&order=<?php echo $disp; ?>">
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_model_use_case&order=<?php echo $disp; ?>">
Use Case<?php if ($sort == 'ai_model_use_case') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark">Prompt</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$provider_id = intval($row['ai_provider_id']);
$provider_name = nullable_htmlentities($row['ai_provider_name']);
$model_id = intval($row['ai_model_id']);
$model_name = nullable_htmlentities($row['ai_model_name']);
$use_case = nullable_htmlentities($row['ai_model_use_case']);
$prompt = nl2br(nullable_htmlentities($row['ai_model_prompt']));
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
<?php echo $model_name; ?>
</a>
</td>
<td><?php echo $provider_name; ?></td>
<td><?php echo $use_case; ?></td>
<td><?php echo $prompt; ?></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-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_model=<?php echo $model_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

109
admin/ai_provider.php Normal file
View File

@ -0,0 +1,109 @@
<?php
// Default Column Sortby Filter
$sort = "ai_provider_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM ai_providers ORDER BY $sort $order");
$num_rows = mysqli_num_rows($sql);
?>
<div class="card card-dark">
<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>
<div class="card-tools">
<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 class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_name&order=<?php echo $disp; ?>">
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_api_url&order=<?php echo $disp; ?>">
URL <?php if ($sort == 'ai_provider_api_url') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_api_key&order=<?php echo $disp; ?>">
Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">
<a class="text-dark">Models</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$provider_id = intval($row['ai_provider_id']);
$provider_name = nullable_htmlentities($row['ai_provider_name']);
$url = nullable_htmlentities($row['ai_provider_api_url']);
$key = nullable_htmlentities($row['ai_provider_api_key']);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('ai_model_id') AS ai_model_count FROM ai_models WHERE ai_model_ai_provider_id = $provider_id"));
$ai_model_count = intval($row['ai_model_count']);
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
<?php echo $provider_name; ?>
</a>
</td>
<td><?php echo $url; ?></td>
<td><?php echo $key; ?></td>
<td class="text-center">
<a class="badge badge-dark badge-pill p-2" href="ai_model.php"><?= $ai_model_count ?></a>
<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-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_provider=<?php echo $provider_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

167
admin/api_keys.php Normal file
View File

@ -0,0 +1,167 @@
<?php
// Default Column Sortby Filter
$sort = "api_key_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM api_keys
LEFT JOIN clients on api_keys.api_key_client_id = clients.client_id
WHERE (api_key_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="fas fa-fw fa-key mr-2"></i>API Keys</h3>
<div class="card-tools">
<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 class="card-body">
<form autocomplete="off">
<div class="row">
<div class="col-md-4">
<div class="input-group mb-3 mb-md-0">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search keys">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-md-8">
<div class="btn-group float-right">
<div class="dropdown ml-2" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-fw fa-layer-group mr-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu">
<button class="dropdown-item text-danger text-bold"
type="submit" form="bulkActions" name="bulk_delete_api_keys">
<i class="fas fa-fw fa-trash mr-2"></i>Revoke
</button>
</div>
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<td class="pr-0">
<div class="form-check">
<input class="form-check-input" type="checkbox" onclick="checkAll(this)">
</div>
</td>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_name&order=<?php echo $disp; ?>">
Name <?php if ($sort == 'api_key_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_client_id&order=<?php echo $disp; ?>">
Client <?php if ($sort == 'api_key_client_id') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_secret&order=<?php echo $disp; ?>">
Secret <?php if ($sort == 'api_key_secret') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_created_at&order=<?php echo $disp; ?>">
Created <?php if ($sort == 'api_key_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_expire&order=<?php echo $disp; ?>">
Expires <?php if ($sort == 'api_key_expire') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$api_key_id = intval($row['api_key_id']);
$api_key_name = nullable_htmlentities($row['api_key_name']);
$api_key_secret = nullable_htmlentities("************" . substr($row['api_key_secret'], -4));
$api_key_created_at = nullable_htmlentities($row['api_key_created_at']);
$api_key_expire = nullable_htmlentities($row['api_key_expire']);
if ($api_key_expire < date("Y-m-d H:i:s")) {
$api_key_expire = $api_key_expire . " (Expired)";
}
if ($row['api_key_client_id'] == 0) {
$api_key_client = "<i>All Clients</i>";
} else {
$api_key_client = nullable_htmlentities($row['client_name']);
}
?>
<tr>
<td class="pr-0">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?php echo $api_key_id ?>">
</div>
</td>
<td class="text-bold"><?php echo $api_key_name; ?></td>
<td><?php echo $api_key_client; ?></td>
<td><?php echo $api_key_secret; ?></td>
<td><?php echo $api_key_created_at; ?></td>
<td><?php echo $api_key_expire; ?></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 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>Revoke
</a>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</form>
</div>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>
<script src="../js/bulk_actions.js"></script>
<?php
require_once "../includes/footer.php";

173
admin/app_log.php Normal file
View File

@ -0,0 +1,173 @@
<?php
// Default Column Sortby Filter
$sort = "app_log_id";
$order = "DESC";
require_once "includes/inc_all_admin.php";
// Log Type Filter
if (isset($_GET['type']) & !empty($_GET['type'])) {
$log_type_query = "AND (app_log_type = '" . sanitizeInput($_GET['type']) . "')";
$type_filter = nullable_htmlentities($_GET['type']);
} else {
// Default - any
$log_type_query = '';
$type_filter = '';
}
// Log Category Filter
if (isset($_GET['category']) & !empty($_GET['catergory'])) {
$log_category_query = "AND (app_log_category = '" . sanitizeInput($_GET['category']) . "')";
$category_filter = nullable_htmlentities($_GET['category']);
} else {
// Default - any
$log_category_query = '';
$category_filter = '';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM app_logs
WHERE (app_log_type LIKE '%$q%' OR app_log_category LIKE '%$q%' OR app_log_details LIKE '%$q%')
AND DATE(app_log_created_at) BETWEEN '$dtf' AND '$dtt'
$log_type_query
$log_category_query
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-3">
<h3 class="card-title"><i class="fas fa-fw fa-history mr-2"></i>App Logs</h3>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search app logs">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#advancedFilter"><i class="fas fa-filter"></i></button>
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="form-group">
<select class="form-control select2" name="type" onchange="this.form.submit()">
<option value="">- All Types -</option>
<?php
$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)) {
$log_type = nullable_htmlentities($row['app_log_type']);
?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="form-group">
<select class="form-control select2" name="category" onchange="this.form.submit()">
<option value="">- All Categories -</option>
<?php
$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)) {
$log_category = nullable_htmlentities($row['app_log_category']);
?>
<option <?php if ($category_filter == $log_category) { echo "selected"; } ?>><?php echo $log_category; ?></option>
<?php
}
?>
</select>
</div>
</div>
</div>
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Date range</label>
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
<input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-sm table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_created_at&order=<?php echo $disp; ?>">
Timestamp <?php if ($sort == 'app_log_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_type&order=<?php echo $disp; ?>">
Type <?php if ($sort == 'app_log_type') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_category&order=<?php echo $disp; ?>">
Category <?php if ($sort == 'app_log_category') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_details&order=<?php echo $disp; ?>">
Details <?php if ($sort == 'app_log_details') { echo $order_icon; } ?>
</a>
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$log_id = intval($row['app_log_id']);
$log_type = nullable_htmlentities($row['app_log_type']);
$log_category = nullable_htmlentities($row['app_log_category']);
$log_details = nullable_htmlentities($row['app_log_details']);
$log_created_at = nullable_htmlentities($row['app_log_created_at']);
?>
<tr>
<td><?php echo $log_created_at; ?></td>
<td><?php echo $log_type; ?></td>
<td><?php echo $log_category; ?></td>
<td><?php echo $log_details; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

283
admin/audit_log.php Normal file
View File

@ -0,0 +1,283 @@
<?php
// Default Column Sortby Filter
$sort = "log_id";
$order = "DESC";
require_once "includes/inc_all_admin.php";
// User Filter
if (isset($_GET['user']) & !empty($_GET['user'])) {
$user_query = 'AND (log_user_id = ' . intval($_GET['user']) . ')';
$user_filter = intval($_GET['user']);
} else {
// Default - any
$user_query = '';
$user_filter = '';
}
// Client Filter
if (isset($_GET['client']) & !empty($_GET['client'])) {
$client_query = 'AND (log_client_id = ' . intval($_GET['client']) . ')';
$client_filter = intval($_GET['client']);
} else {
// Default - any
$client_query = '';
$client_filter = '';
}
// Log Type Filter
if (isset($_GET['type']) & !empty($_GET['type'])) {
$log_type_query = "AND (log_type = '" . sanitizeInput($_GET['type']) . "')";
$type_filter = nullable_htmlentities($_GET['type']);
} else {
// Default - any
$log_type_query = '';
$type_filter = '';
}
// Log Action Filter
if (isset($_GET['action']) & !empty($_GET['action'])) {
$log_action_query = "AND (log_action = '" . sanitizeInput($_GET['action']) . "')";
$action_filter = nullable_htmlentities($_GET['action']);
} else {
// Default - any
$log_action_query = '';
$action_filter = '';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM logs
LEFT JOIN users ON log_user_id = user_id
LEFT JOIN clients ON log_client_id = client_id
WHERE (log_type LIKE '%$q%' OR log_action LIKE '%$q%' OR log_description LIKE '%$q%' OR log_ip LIKE '%$q%' OR log_user_agent LIKE '%$q%' OR user_name LIKE '%$q%' OR client_name LIKE '%$q%')
AND DATE(log_created_at) BETWEEN '$dtf' AND '$dtt'
$user_query
$client_query
$log_type_query
$log_action_query
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-3">
<h3 class="card-title"><i class="fas fa-fw fa-history mr-2"></i>Audit Logs</h3>
</div>
<div class="card-body">
<form class="mb-4" autocomplete="off">
<div class="row">
<div class="col-sm-4">
<div class="input-group mb-3 mb-md-0">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search audit logs">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#advancedFilter"><i class="fas fa-filter"></i></button>
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="client" onchange="this.form.submit()">
<option value="">- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "SELECT * FROM clients ORDER BY client_name ASC");
while ($row = mysqli_fetch_array($sql_clients_filter)) {
$client_id = intval($row['client_id']);
$client_name = nullable_htmlentities($row['client_name']);
?>
<option <?php if ($client_filter == $client_id) { echo "selected"; } ?> value="<?php echo $client_id; ?>"><?php echo $client_name; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="user" onchange="this.form.submit()">
<option value="">- All Users -</option>
<?php
$sql_users_filter = mysqli_query($mysqli, "SELECT * FROM users ORDER BY user_name ASC");
while ($row = mysqli_fetch_array($sql_users_filter)) {
$user_id = intval($row['user_id']);
$user_name = nullable_htmlentities($row['user_name']);
?>
<option <?php if ($user_filter == $user_id) { echo "selected"; } ?> value="<?php echo $user_id; ?>"><?php echo $user_name; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="type" onchange="this.form.submit()">
<option value="">- All Types -</option>
<?php
$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)) {
$log_type = nullable_htmlentities($row['log_type']);
?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="action" onchange="this.form.submit()">
<option value="">- All Actions -</option>
<?php
$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)) {
$log_action = nullable_htmlentities($row['log_action']);
?>
<option <?php if ($action_filter == $log_action) { echo "selected"; } ?>><?php echo $log_action; ?></option>
<?php
}
?>
</select>
</div>
</div>
</div>
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Date range</label>
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
<input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-sm 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=log_created_at&order=<?php echo $disp; ?>">
Timestamp <?php if ($sort == 'log_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=user_name&order=<?php echo $disp; ?>">
User <?php if ($sort == 'user_name') { echo $order_icon; } ?>
</a>
</th>
<?php if (empty($client)) { ?>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=client_name&order=<?php echo $disp; ?>">
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_type&order=<?php echo $disp; ?>">
Type <?php if ($sort == 'log_type') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_action&order=<?php echo $disp; ?>">
Action <?php if ($sort == 'log_action') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_description&order=<?php echo $disp; ?>">
Description <?php if ($sort == 'log_description') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_ip&order=<?php echo $disp; ?>">
IP Address <?php if ($sort == 'log_ip') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_user_agent&order=<?php echo $disp; ?>">
User Agent <?php if ($sort == 'log_user_agent') { echo $order_icon; } ?>
</a>
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$log_id = intval($row['log_id']);
$log_type = nullable_htmlentities($row['log_type']);
$log_action = nullable_htmlentities($row['log_action']);
$log_description = nullable_htmlentities($row['log_description']);
$log_ip = nullable_htmlentities($row['log_ip']);
$log_user_agent = nullable_htmlentities($row['log_user_agent']);
$log_user_os = getOS($log_user_agent);
$log_user_browser = getWebBrowser($log_user_agent);
$log_created_at = nullable_htmlentities($row['log_created_at']);
$user_id = intval($row['user_id']);
$user_name = nullable_htmlentities($row['user_name']);
if (empty($user_name)) {
$user_name_display = "-";
} else {
$user_name_display = $user_name;
}
$client_name = nullable_htmlentities($row['client_name']);
$client_id = intval($row['client_id']);
if (empty($client_name)) {
$client_name_display = "-";
} else {
$client_name_display = "<a href='../agent/client_overview.php?client_id=$client_id'>$client_name</a>";
}
$log_entity_id = intval($row['log_entity_id']);
?>
<tr>
<td><?php echo $log_created_at; ?></td>
<td><?php echo $user_name_display; ?></td>
<?php if(empty($client)) { ?>
<td><?php echo $client_name_display; ?></td>
<?php } ?>
<td><?php echo $log_type; ?></td>
<td><?php echo $log_action; ?></td>
<td><?php echo $log_description; ?></td>
<td><?php echo $log_ip; ?></td>
<td><?php echo "$log_user_os<div class='text-secondary'>$log_user_browser</div>"; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

38
admin/backup.php Normal file
View File

@ -0,0 +1,38 @@
<?php
require_once "includes/inc_all_admin.php";
?>
<div class="card card-dark mb-3">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-database mr-2"></i>Download Database</h3>
</div>
<div class="card-body" style="text-align: center;">
<div class="alert alert-secondary">If you are unable to back up the entire VM, you'll need to back up the files & database individually. There is no built-in restore. See the <a href="https://docs.itflow.org/backups" target="_blank">docs here</a>.</div>
<a class="btn btn-primary btn-lg p-3" href="post.php?download_backup&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-4x fa-download"></i><br><br>Download Backup</a>
</div>
</div>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-key mr-2"></i>Backup Master Encryption Key</h3>
</div>
<div class="card-body">
<div class="card-body">
<form action="post.php" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="d-flex justify-content-center">
<div class="input-group col-sm-4">
<input type="password" class="form-control" placeholder="Enter your account password" name="password" autocomplete="new-password" required>
<div class="input-group-append">
<button class="btn btn-primary" type="submit" name="backup_master_key"><i class="fas fa-key"></i></button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

179
admin/category.php Normal file
View File

@ -0,0 +1,179 @@
<?php
// Default Column Sortby Filter
$sort = "category_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
if (isset($_GET['category'])) {
$category = sanitizeInput($_GET['category']);
} else {
$category = "Expense";
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM categories
WHERE category_name LIKE '%$q%'
AND category_type = '$category'
AND category_$archive_query
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-list-ul mr-2"></i>
<?php echo nullable_htmlentities($category); ?> Categories
</h3>
<?php
if (!isset($_GET['archived'])) {
?>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/category/category_add.php?category=<?= nullable_htmlentities($category) ?>"><i
class="fas fa-plus mr-2"></i>New <?php echo nullable_htmlentities($category); ?> Category</button>
</div>
<?php
}
?>
</div>
<div class="card-body">
<form autocomplete="off">
<input type="hidden" name="category" value="<?php echo nullable_htmlentities($category); ?>">
<div class="row">
<div class="col-sm-4 mb-2">
<div class="input-group">
<input type="search" class="form-control" name="q"
value="<?php if (isset($q)) {
echo stripslashes(nullable_htmlentities($q));
} ?>"
placeholder="Search <?php echo nullable_htmlentities($category); ?> Categories ">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-sm-8">
<div class="btn-group float-right">
<a href="?category=Expense"
class="btn <?php if ($category == 'Expense') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Expense</a>
<a href="?category=Income"
class="btn <?php if ($category == 'Income') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Income</a>
<a href="?category=Referral"
class="btn <?php if ($category == 'Referral') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Referral</a>
<a href="?category=Ticket"
class="btn <?php if ($category == 'Ticket') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Ticket</a>
<a href="?<?php echo $url_query_strings_sort ?>&archived=1"
class="btn <?php if (isset($_GET['archived'])) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>"><i
class="fas fa-fw fa-archive mr-2"></i>Archived</a>
</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"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=category_name&order=<?php echo $disp; ?>">
Name <?php if ($sort == 'category_name') { echo $order_icon; } ?>
</a>
</th>
<th>Color</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$category_id = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']);
$category_color = nullable_htmlentities($row['category_color']);
?>
<tr>
<td>
<a class="text-dark ajax-modal" href="#"
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
<?php echo $category_name; ?>
</a>
</td>
<td><i class="fa fa-3x fa-circle" style="color:<?php echo $category_color; ?>;"></i></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">
<?php
if ($archived) {
?>
<a class="dropdown-item text-success confirm-link"
href="post.php?unarchive_category=<?php echo $category_id; ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Unarchive
</a>
<a class="dropdown-item text-danger confirm-link"
href="post.php?delete_category=<?php echo $category_id; ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
<?php
} else {
?>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<a class="dropdown-item text-danger confirm-link"
href="post.php?archive_category=<?php echo $category_id; ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Archive
</a>
<?php
}
?>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.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_array($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"; ?>

8
admin/custom/readme.php Normal file
View File

@ -0,0 +1,8 @@
<?php
/*
- Custom Pages -
If you wish to add custom pages to ITFlow, add them to this directory"
Link to Documentation for File Directory Structure and examples
*/

148
admin/custom_link.php Normal file
View File

@ -0,0 +1,148 @@
<?php
// Default Column Sortby Filter
$sort = "custom_link_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM custom_links
WHERE custom_link_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="fas fa-fw fa-external-link-alt mr-2"></i>Custom Links</h3>
<div class="card-tools">
<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 class="card-body">
<div class="row">
<div class="col-sm-4 mb-2">
<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 Links">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
</div>
<div class="col-sm-8">
</div>
</div>
<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>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_name&order=<?php echo $disp; ?>">
Name <?php if ($sort == 'custom_link_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_order&order=<?php echo $disp; ?>">
Order <?php if ($sort == 'custom_link_order') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_uri&order=<?php echo $disp; ?>">
URI / <span class="text-secondary">New Tab</span> <?php if ($sort == 'custom_link_uri') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_location&order=<?php echo $disp; ?>">
Location <?php if ($sort == 'custom_link_location') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$custom_link_id = intval($row['custom_link_id']);
$custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = nullable_htmlentities($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']);
$custom_link_new_tab = intval($row['custom_link_new_tab']);
if ($custom_link_new_tab == 1 ) {
$custom_link_new_tab_display = "<i class='fas fa-fw fa-checkmark'></i>";
} else {
$custom_link_new_tab_display = "";
}
$custom_link_order = intval($row['custom_link_order']);
if ($custom_link_order == 0 ) {
$custom_link_order_display = "-";
} else {
$custom_link_order_display = $custom_link_order;
}
$custom_link_location = intval($row['custom_link_location']);
if ($custom_link_location == 1) {
$custom_link_location_display = "Main Side Nav";
} elseif ($custom_link_location == 2) {
$custom_link_location_display = "Top Nav";
} elseif ($custom_link_location == 3) {
$custom_link_location_display = "Client Portal Nav";
} elseif ($custom_link_location == 4) {
$custom_link_location_display = "Admin Nav";
} elseif ($custom_link_location == 5) {
$custom_link_location_display = "Reports Nav";
}
?>
<tr>
<td>
<a class="ajax-modal" href="#"
data-modal-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_id ?>">
<i class="fa fa-fw fa-<?php echo $custom_link_icon; ?> mr-2"></i><?php echo $custom_link_name;?>
</a>
</td>
<td><?php echo $custom_link_order_display; ?></td>
<td><?php echo "$custom_link_uri $custom_link_new_tab_display"; ?></td>
<td><?php echo $custom_link_location_display; ?></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-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_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 confirm-link" href="post.php?delete_custom_link=<?php echo $custom_link_id; ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

4145
admin/database_updates.php Normal file

File diff suppressed because it is too large Load Diff

768
admin/debug.php Normal file
View File

@ -0,0 +1,768 @@
<?php
require_once "includes/inc_all_admin.php";
require_once "../includes/database_version.php";
require_once "../config.php";
$checks = [];
// Execute the git command to get the latest commit hash
$commitHash = shell_exec('git log -1 --format=%H');
// Get branch info
$gitBranch = shell_exec('git rev-parse --abbrev-ref HEAD');
// Section: System Information
$systemInfo = [];
// Operating System and Version
$os = php_uname();
$systemInfo[] = [
'name' => 'Operating System',
'value' => $os,
];
// Web Server and Version
$webServer = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
$systemInfo[] = [
'name' => 'Web Server',
'value' => $webServer,
];
// Kernel and Version
$kernelVersion = php_uname('r');
$systemInfo[] = [
'name' => 'Kernel Version',
'value' => $kernelVersion,
];
// Database and Version
$dbVersion = $mysqli->server_info;
$systemInfo[] = [
'name' => 'Database Version',
'value' => $dbVersion,
];
// Section: PHP Extensions
$phpExtensions = [];
$extensions = [
'php-mysqli' => 'mysqli',
'php-intl' => 'intl',
'php-curl' => 'curl',
'php-mbstring' => 'mbstring',
'php-gd' => 'gd',
'php-zip' => 'zip',
'php-xml' => 'xml',
];
foreach ($extensions as $name => $ext) {
$loaded = extension_loaded($ext);
$phpExtensions[] = [
'name' => "$name installed",
'passed' => $loaded,
'value' => $loaded ? 'Installed' : 'Not Installed',
];
}
// Section: PHP Configuration
$phpConfig = [];
// Check if shell_exec is enabled
$disabled_functions = explode(',', ini_get('disable_functions'));
$disabled_functions = array_map('trim', $disabled_functions);
$shell_exec_enabled = !in_array('shell_exec', $disabled_functions);
$phpConfig[] = [
'name' => 'shell_exec is enabled',
'passed' => $shell_exec_enabled,
'value' => $shell_exec_enabled ? 'Enabled' : 'Disabled',
];
// Check upload_max_filesize and post_max_size >= 500M
function return_bytes($val) {
$val = trim($val);
$unit = strtolower(substr($val, -1));
$num = (float)$val;
switch ($unit) {
case 'g':
$num *= 1024;
case 'm':
$num *= 1024;
case 'k':
$num *= 1024;
}
return $num;
}
$required_bytes = 500 * 1024 * 1024; // 500M in bytes
$upload_max_filesize = ini_get('upload_max_filesize');
$post_max_size = ini_get('post_max_size');
$upload_passed = return_bytes($upload_max_filesize) >= $required_bytes;
$post_passed = return_bytes($post_max_size) >= $required_bytes;
$phpConfig[] = [
'name' => 'upload_max_filesize >= 500M',
'passed' => $upload_passed,
'value' => $upload_max_filesize,
];
$phpConfig[] = [
'name' => 'post_max_size >= 500M',
'passed' => $post_passed,
'value' => $post_max_size,
];
// PHP Memory Limit >= 128M
$memoryLimit = ini_get('memory_limit');
$memoryLimitBytes = return_bytes($memoryLimit);
$memoryLimitPassed = $memoryLimitBytes >= (128 * 1024 * 1024);
$phpConfig[] = [
'name' => 'PHP Memory Limit >= 128M',
'passed' => $memoryLimitPassed,
'value' => $memoryLimit,
];
// Max Execution Time >= 300 seconds
$maxExecutionTime = ini_get('max_execution_time');
$maxExecutionTimePassed = $maxExecutionTime >= 300;
$phpConfig[] = [
'name' => 'Max Execution Time >= 300 seconds',
'passed' => $maxExecutionTimePassed,
'value' => $maxExecutionTime . ' seconds',
];
// Check PHP version >= 8.2.0
$php_version = PHP_VERSION;
$php_passed = version_compare($php_version, '8.2.0', '>=');
$phpConfig[] = [
'name' => 'PHP version >= 8.2.0',
'passed' => $php_passed,
'value' => $php_version,
];
// Section: Shell Commands
$shellCommands = [];
if ($shell_exec_enabled) {
$commands = ['whois', 'dig', 'git'];
foreach ($commands as $command) {
$which = trim(shell_exec("which $command 2>/dev/null"));
$exists = !empty($which);
$shellCommands[] = [
'name' => "Command '$command' available",
'passed' => $exists,
'value' => $exists ? $which : 'Not Found',
];
}
} else {
// If shell_exec is disabled, mark commands as unavailable
foreach (['whois', 'dig', 'git'] as $command) {
$shellCommands[] = [
'name' => "Command '$command' available",
'passed' => false,
'value' => 'shell_exec Disabled',
];
}
}
// Section: SSL Checks
$sslChecks = [];
// Check if accessing via HTTPS
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443;
$sslChecks[] = [
'name' => 'Accessing via HTTPS',
'passed' => $https,
'value' => $https ? 'Yes' : 'No',
];
// SSL Certificate Validity Check
if ($https) {
$streamContext = stream_context_create(["ssl" => ["capture_peer_cert" => true]]);
$socket = @stream_socket_client("ssl://{$_SERVER['HTTP_HOST']}:443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $streamContext);
if ($socket) {
$params = stream_context_get_params($socket);
$cert = $params['options']['ssl']['peer_certificate'];
$certInfo = openssl_x509_parse($cert);
$validFrom = $certInfo['validFrom_time_t'];
$validTo = $certInfo['validTo_time_t'];
$currentTime = time();
$certValid = ($currentTime >= $validFrom && $currentTime <= $validTo);
$sslChecks[] = [
'name' => 'SSL Certificate is valid',
'passed' => $certValid,
'value' => $certValid ? 'Valid' : 'Invalid or Expired',
];
} else {
$sslChecks[] = [
'name' => 'SSL Certificate is valid',
'passed' => false,
'value' => 'Unable to retrieve certificate',
];
}
} else {
$sslChecks[] = [
'name' => 'SSL Certificate is valid',
'passed' => false,
'value' => 'Not using HTTPS',
];
}
// Section: Domain Checks
$domainChecks = [];
// Check if the site has a valid FQDN
$fqdn = $_SERVER['HTTP_HOST'];
$isValidFqdn = (bool) filter_var('http://' . $fqdn, FILTER_VALIDATE_URL) && preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $fqdn);
$domainChecks[] = [
'name' => 'Site has a valid FQDN',
'passed' => $isValidFqdn,
'value' => $fqdn,
];
// Section: File Permissions
$filePermissions = [];
// Check if web user has write access to webroot directory
$webroot = $_SERVER['DOCUMENT_ROOT'];
$writable = is_writable($webroot);
$filePermissions[] = [
'name' => 'Web user has write access to webroot directory',
'passed' => $writable,
'value' => $webroot,
];
// Section: Uploads Directory Stats
$uploadsStats = [];
// Define the uploads directory path
$uploadsDir = __DIR__ . '/../uploads'; // Adjust the path if needed
if (is_dir($uploadsDir)) {
// Function to recursively count files and calculate total size
function getDirStats($dir) {
$files = 0;
$size = 0;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
foreach ($iterator as $file) {
if ($file->isFile()) {
$files++;
$size += $file->getSize();
}
}
return ['files' => $files, 'size' => $size];
}
$stats = getDirStats($uploadsDir);
$sizeInMB = round($stats['size'] / (1024 * 1024), 2);
$uploadsStats[] = [
'name' => 'Number of files in uploads directory',
'value' => $stats['files'],
];
$uploadsStats[] = [
'name' => 'Total size of uploads directory (MB)',
'value' => $sizeInMB . ' MB',
];
} else {
$uploadsStats[] = [
'name' => 'Uploads directory exists',
'value' => 'Directory not found',
];
}
// Section: Database Stats
$databaseStats = [];
// Get list of tables
$tablesResult = $mysqli->query("SHOW TABLE STATUS");
if ($tablesResult) {
$totalTables = 0;
$totalFields = 0;
$totalRows = 0;
$totalSize = 0;
$tableDetails = [];
while ($table = $tablesResult->fetch_assoc()) {
$tableName = $table['Name'];
// Accurate row count
$countResult = $mysqli->query("SELECT COUNT(*) AS cnt FROM `$tableName`");
$countRow = $countResult->fetch_assoc();
$tableRows = $countRow['cnt'];
$countResult->free();
$dataLength = $table['Data_length'];
$indexLength = $table['Index_length'];
$tableSize = ($dataLength + $indexLength) / (1024 * 1024); // Size in MB
// Get number of fields
$fieldsResult = $mysqli->query("SHOW COLUMNS FROM `$tableName`");
$numFields = $fieldsResult->num_rows;
$fieldsResult->free();
$totalTables++;
$totalFields += $numFields;
$totalRows += $tableRows;
$totalSize += $tableSize;
$tableDetails[] = [
'name' => $tableName,
'fields' => $numFields,
'rows' => $tableRows,
'size' => round($tableSize, 2),
];
}
$tablesResult->free();
$databaseStats[] = [
'name' => 'Total number of tables',
'value' => $totalTables,
];
$databaseStats[] = [
'name' => 'Total number of fields',
'value' => $totalFields,
];
$databaseStats[] = [
'name' => 'Total number of rows',
'value' => $totalRows,
];
$databaseStats[] = [
'name' => 'Total database size (MB)',
'value' => round($totalSize, 2) . ' MB',
];
}
// Section: Database Structure Comparison
$dbComparison = [];
// Path to the db.sql file
$dbSqlFile = __DIR__ . '/../db.sql';
if (file_exists($dbSqlFile)) {
// Read the db.sql file
$sqlContent = file_get_contents($dbSqlFile);
// Remove comments and empty lines
$lines = explode("\n", $sqlContent);
$sqlStatements = [];
$statement = '';
foreach ($lines as $line) {
// Remove single-line comments
$line = preg_replace('/--.*$/', '', $line);
$line = preg_replace('/\/\*.*?\*\//', '', $line);
// Skip empty lines
if (trim($line) == '') {
continue;
}
// Append line to the current statement
$statement .= $line . "\n";
// Check if the statement ends with a semicolon
if (preg_match('/;\s*$/', $line)) {
$sqlStatements[] = $statement;
$statement = '';
}
}
// Parse the CREATE TABLE statements
$sqlTables = [];
foreach ($sqlStatements as $sql) {
if (preg_match('/CREATE TABLE\s+`?([^` ]+)`?\s*\((.*)\)(.*?);/msi', $sql, $match)) {
$tableName = $match[1];
$columnsDefinition = $match[2];
// Extract column names and data types
$columns = [];
$columnLines = explode("\n", $columnsDefinition);
foreach ($columnLines as $line) {
$line = trim($line);
// Skip empty lines and lines that do not define columns
if ($line == '' || strpos($line, 'PRIMARY KEY') !== false || strpos($line, 'UNIQUE KEY') !== false || strpos($line, 'KEY') === 0 || strpos($line, 'CONSTRAINT') === 0 || strpos($line, ')') === 0) {
continue;
}
// Remove trailing comma if present
$line = rtrim($line, ',');
// Match column definition
if (preg_match('/^`([^`]+)`\s+(.+)/', $line, $colMatch)) {
$colName = $colMatch[1];
$colDefinition = $colMatch[2];
// Extract the data type from the column definition
$tokens = preg_split('/\s+/', $colDefinition);
$colType = $tokens[0];
// Handle data types with parentheses (e.g., varchar(255), decimal(15,2))
if (preg_match('/^([a-zA-Z]+)\(([^)]+)\)/', $colType, $typeMatch)) {
$colType = $typeMatch[1] . '(' . $typeMatch[2] . ')';
}
$columns[$colName] = $colType;
}
}
$sqlTables[$tableName] = $columns;
}
}
// Get current database table structures
$dbTables = [];
$tablesResult = $mysqli->query("SHOW TABLES");
while ($row = $tablesResult->fetch_row()) {
$tableName = $row[0];
$columnsResult = $mysqli->query("SHOW COLUMNS FROM `$tableName`");
$columns = [];
while ($col = $columnsResult->fetch_assoc()) {
$columns[$col['Field']] = $col['Type'];
}
$columnsResult->free();
$dbTables[$tableName] = $columns;
}
$tablesResult->free();
// Compare the structures
foreach ($sqlTables as $tableName => $sqlColumns) {
if (!isset($dbTables[$tableName])) {
$dbComparison[] = [
'name' => "Table `$tableName` missing in database",
'status' => 'Missing Table',
];
continue;
}
// Compare columns
$dbColumns = $dbTables[$tableName];
foreach ($sqlColumns as $colName => $colType) {
if (!isset($dbColumns[$colName])) {
$dbComparison[] = [
'name' => "Column `$colName` missing in table `$tableName`",
'status' => 'Missing Column',
];
} else {
// Normalize data types for comparison
$sqlColType = strtolower($colType);
$dbColType = strtolower($dbColumns[$colName]);
// Remove attributes and constraints
$sqlColType = preg_replace('/\s+.*$/', '', $sqlColType);
$dbColType = preg_replace('/\s+.*$/', '', $dbColType);
// Remove additional attributes like unsigned, zerofill, etc.
$sqlColType = preg_replace('/\s+unsigned|\s+zerofill|\s+binary/', '', $sqlColType);
$dbColType = preg_replace('/\s+unsigned|\s+zerofill|\s+binary/', '', $dbColType);
if ($sqlColType != $dbColType) {
$dbComparison[] = [
'name' => "Data type mismatch for `$colName` in table `$tableName`",
'status' => "Expected: $colType, Found: {$dbColumns[$colName]}",
];
}
}
}
// Check for extra columns in the database that are not in the SQL file
foreach ($dbColumns as $colName => $colType) {
if (!isset($sqlColumns[$colName])) {
$dbComparison[] = [
'name' => "Extra column `$colName` in table `$tableName` not present in db.sql",
'status' => 'Extra Column',
];
}
}
}
// Check for tables in the database not present in the db.sql file
foreach ($dbTables as $tableName => $dbColumns) {
if (!isset($sqlTables[$tableName])) {
$dbComparison[] = [
'name' => "Extra table `$tableName` in database not present in db.sql",
'status' => 'Extra Table',
];
}
}
} else {
$dbComparison[] = [
'name' => 'db.sql file not found',
'status' => 'File Missing',
];
}
$mysqli->close();
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-bug mr-2"></i>Debug</h3>
</div>
<div class="card-body">
<h2>Debugging</h2>
<ul>
<li>If you are experiencing a problem with ITFlow, this page should help you identify any configuration issues.</li>
<li>Note: You might also need to gather <a href="https://docs.itflow.org/gathering_logs#error_logs">error logs</a></li>
</ul>
<hr>
<div class="table-responsive">
<table class="table table-bordered mb-3">
<tr>
<th>ITFlow release version</th>
<th><?php echo APP_VERSION; ?></th>
</tr>
<tr>
<td>Current DB Version</td>
<td><?php echo CURRENT_DATABASE_VERSION; ?></td>
</tr>
<tr>
<td>Current Code Commit</td>
<td><?php echo $commitHash; ?></td>
</tr>
<tr>
<td>Current Branch</td>
<td><?php echo $gitBranch; ?></td>
</tr>
</table>
</div>
<!-- System Information Table -->
<h3>System Information</h3>
<table class="table table-sm table-bordered">
<tbody>
<?php foreach ($systemInfo as $info): ?>
<tr>
<td><?= htmlspecialchars($info['name']); ?></td>
<td><?= htmlspecialchars($info['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- PHP Extensions and Configuration Table -->
<h3 class="mt-3">PHP Extensions and Configuration</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<!-- PHP Extensions Section -->
<thead>
<tr class="table-secondary">
<th colspan="3">PHP Extensions</th>
</tr>
</thead>
<tbody>
<?php foreach ($phpExtensions as $check): ?>
<tr>
<td><?= htmlspecialchars($check['name']); ?></td>
<td class="text-center">
<?php if ($check['passed']): ?>
<i class="fas fa-check" style="color:green"></i>
<?php else: ?>
<i class="fas fa-times" style="color:red"></i>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($check['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<!-- PHP Configuration Section -->
<thead>
<tr class="table-secondary">
<th colspan="3">PHP Configuration</th>
</tr>
</thead>
<tbody>
<?php foreach ($phpConfig as $check): ?>
<tr>
<td><?= htmlspecialchars($check['name']); ?></td>
<td class="text-center">
<?php if ($check['passed']): ?>
<i class="fas fa-check" style="color:green"></i>
<?php else: ?>
<i class="fas fa-times" style="color:red"></i>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($check['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<thead>
<tr class="table-secondary">
<th colspan="3">Shell Commands</th>
</tr>
</thead>
<tbody>
<?php foreach ($shellCommands as $check): ?>
<tr>
<td><?= htmlspecialchars($check['name']); ?></td>
<td class="text-center">
<?php if ($check['passed']): ?>
<i class="fas fa-check" style="color:green"></i>
<?php else: ?>
<i class="fas fa-times" style="color:red"></i>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($check['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<thead>
<tr class="table-secondary">
<th colspan="3">SSL Checks</th>
</tr>
</thead>
<tbody>
<?php foreach ($sslChecks as $check): ?>
<tr>
<td><?= htmlspecialchars($check['name']); ?></td>
<td class="text-center">
<?php if ($check['passed']): ?>
<i class="fas fa-check" style="color:green"></i>
<?php else: ?>
<i class="fas fa-times" style="color:red"></i>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($check['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<thead>
<tr class="table-secondary">
<th colspan="3">Domain Checks</th>
</tr>
</thead>
<tbody>
<?php foreach ($domainChecks as $check): ?>
<tr>
<td><?= htmlspecialchars($check['name']); ?></td>
<td class="text-center">
<?php if ($check['passed']): ?>
<i class="fas fa-check" style="color:green"></i>
<?php else: ?>
<i class="fas fa-times" style="color:red"></i>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($check['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<!-- File Permissions Table -->
<thead>
<tr class="table-secondary">
<th colspan="3">File Permissions</th>
</tr>
</thead>
<tbody>
<?php foreach ($filePermissions as $check): ?>
<tr>
<td><?= htmlspecialchars($check['name']); ?></td>
<td class="text-center">
<?php if ($check['passed']): ?>
<i class="fas fa-check" style="color:green"></i>
<?php else: ?>
<i class="fas fa-times" style="color:red"></i>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($check['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Database Structure Comparison Table -->
<h3 class="mt-3">Database Structure Comparison</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<tbody>
<?php if (!empty($dbComparison)): ?>
<?php foreach ($dbComparison as $issue): ?>
<tr>
<td><?= htmlspecialchars($issue['name']); ?></td>
<td colspan="2"><?= htmlspecialchars($issue['status']); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="3">No discrepancies found between the database and db.sql file.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Uploads Directory Stats Table -->
<h3 class="mt-3">Uploads Directory Stats</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<tbody>
<?php foreach ($uploadsStats as $stat): ?>
<tr>
<td><?= htmlspecialchars($stat['name']); ?></td>
<td colspan="2"><?= htmlspecialchars($stat['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Database Stats Table -->
<h3 class="mt-3">Database Stats</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<tbody>
<?php foreach ($databaseStats as $stat): ?>
<tr>
<td><?= htmlspecialchars($stat['name']); ?></td>
<td colspan="2"><?= htmlspecialchars($stat['value']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Table Stats Table -->
<h3 class="mt-3">Table Stats</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Table Name</th>
<th>Fields / Rows</th>
<th>Size (MB)</th>
</tr>
</thead>
<tbody>
<?php foreach ($tableDetails as $table): ?>
<tr>
<td><?= htmlspecialchars($table['name']); ?></td>
<td><?= htmlspecialchars("Fields: {$table['fields']}, Rows: {$table['rows']}"); ?></td>
<td><?= htmlspecialchars($table['size'] . ' MB'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

124
admin/document_template.php Normal file
View File

@ -0,0 +1,124 @@
<?php
// Default Column Sort by Filter
$sort = "document_template_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM document_templates
LEFT JOIN users ON document_template_created_by = user_id
WHERE user_name LIKE '%$q%' OR document_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 mr-2"></i>Document Templates</h3>
<div class="card-tools">
<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
</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>
<a class="text-secondary" href="?<?php echo $url_query_strings_sort; ?>&sort=document_template_name&order=<?php echo $disp; ?>">
Template Name <?php if ($sort == 'document_template_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?php echo $url_query_strings_sort; ?>&sort=document_template_created_at&order=<?php echo $disp; ?>">
Created <?php if ($sort == 'document_template_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?php echo $url_query_strings_sort; ?>&sort=document_template_updated_at&order=<?php echo $disp; ?>">
Updated <?php if ($sort == 'document_template_updated_at') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">
Action
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$document_template_id = intval($row['document_template_id']);
$document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']);
$document_template_content = nullable_htmlentities($row['document_template_content']);
$document_template_created_by_name = nullable_htmlentities($row['user_name']);
$document_template_created_at = nullable_htmlentities($row['document_template_created_at']);
$document_template_updated_at = nullable_htmlentities($row['document_template_updated_at']);
?>
<tr>
<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>
<div class="mt-1 text-secondary"><?php echo $document_template_description; ?></div>
</td>
<td>
<?php echo $document_template_created_at; ?>
<div class="text-secondary"><?php echo $document_template_created_by_name; ?></div>
</td>
<td><?php echo $document_template_updated_at; ?></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/document_template/document_template_edit.php?id=<?= $document_template_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_document_template=<?php echo $document_template_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

@ -0,0 +1,70 @@
<?php
require_once "includes/inc_all_admin.php";
//Initialize the HTML Purifier to prevent XSS
require "../plugins/htmlpurifier/HTMLPurifier.standalone.php";
$purifier_config = HTMLPurifier_Config::createDefault();
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
$purifier = new HTMLPurifier($purifier_config);
if (isset($_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 LIMIT 1");
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_array($sql_document);
$document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']);
$document_template_content = $purifier->purify($row['document_template_content']);
$document_template_created_at = nullable_htmlentities($row['document_template_created_at']);
$document_template_updated_at = nullable_htmlentities($row['document_template_updated_at']);
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="users.php">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="document_template.php">Document Templates</a>
</li>
<li class="breadcrumb-item active"><i class="fas fa-file mr-2"></i><?php echo $document_template_name; ?></li>
</ol>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file mr-2"></i><?php echo $document_template_name; ?></h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal"
data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<i class="fas fa-edit mr-2"></i>Edit
</button>
</div>
</div>
<div class="card-body prettyContent">
<?php echo $document_template_content; ?>
</div>
</div>
<script src="../js/pretty_content.js"></script>
<?php
require_once "../includes/footer.php";

View File

@ -0,0 +1,58 @@
<?php
require_once "includes/inc_all_admin.php";
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-fingerprint mr-2"></i>Identity Providers</h3>
</div>
<div class="card-body">
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<h4>Client Portal SSO via Microsoft Entra</h4>
<div class="form-group">
<label>Identity Provider <small class='text-secondary'>(Currently only works with Microsoft Entra ID/AAD)</small></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
</div>
<select class="form-control select2" readonly>
<option <?php if (empty($config_azure_client_id)) { echo "selected"; } ?>>Disabled</option>
<option <?php if ($config_azure_client_id) { echo "selected"; } ?>>Microsoft Entra</option>
<option>Google (WIP)</option>
<option>Custom SSO (WIP)</option>
</select>
</div>
</div>
<div class="form-group">
<label>MS Entra OAuth App (Client) ID</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
</div>
<input type="text" class="form-control" name="azure_client_id" placeholder="e721e3b6-01d6-50e8-7f22-c84d951a52e7" value="<?php echo nullable_htmlentities($config_azure_client_id); ?>">
</div>
</div>
<div class="form-group">
<label>MS Entra OAuth Secret</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="password" class="form-control" name="azure_client_secret" placeholder="Auto-generated from App Registration" value="<?php echo nullable_htmlentities($config_azure_client_secret); ?>" autocomplete="new-password">
</div>
</div>
<hr>
<button type="submit" name="edit_identity_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>
</form>
</div>
</div>
<?php require_once "../includes/footer.php";

View File

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

325
admin/includes/side_nav.php Normal file
View File

@ -0,0 +1,325 @@
<!-- Main Sidebar Container -->
<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 ?>">
<p class="h6">
<i class="nav-icon fas fa-arrow-left ml-3 mr-2"></i>
<span class="brand-text">
Back | <strong>Administration</strong>
</span>
</p>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav>
<ul class="nav nav-pills nav-sidebar flex-column mt-2" data-widget="treeview" data-accordion="false">
<li class="nav-header">ACCESS</li>
<li class="nav-item">
<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>
<p>Users</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Roles</p>
</a>
</li>
<!-- 2025-12-05 JQ - Hide Permission Modules currently just shows modules
<li class="nav-item">
<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>
<p>API Keys</p>
</a>
</li>
<li class="nav-header">TAGS & CATEGORIES</li>
<li class="nav-item">
<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>
<p>Tags</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Categories</p>
</a>
</li>
<?php if ($config_module_enable_accounting) { ?>
<li class="nav-item">
<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>
<p>Taxes</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Payment Methods</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Payment Providers</p>
</a>
</li>
<?php } ?>
<li class="nav-item">
<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>
<p>AI Providers</p>
</a>
</li>
<?php if ($config_module_enable_ticketing) { ?>
<li class="nav-item">
<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>
<p>Ticket Statuses</p>
</a>
</li>
<?php } ?>
<li class="nav-item">
<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>
<p>Custom Links</p>
</a>
</li>
<?php if ($config_module_enable_itdoc) { ?>
<li class="nav-header">TEMPLATES</li>
<!-- 2025-11-16 JQ - Hide Contracts not yet ready
<li class="nav-item">
<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>
<p>Project Templates</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Ticket Templates</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Vendor Templates</p>
</a>
</li>
<li class="nav-item">
<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>
<p>License Templates</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Document Templates</p>
</a>
</li>
<?php } ?>
<li class="nav-header">MAINTENANCE</li>
<li class="nav-item">
<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>
<p>Mail Queue</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Audit Logs</p>
</a>
</li>
<li class="nav-item">
<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>
<p>App Logs</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Backup</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Debug</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Update</p>
</a>
</li>
<!-- SETTINGS Section -->
<li class="nav-item has-treeview mt-2 <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['settings_company.php', 'settings_localization.php', 'settings_theme.php', 'settings_security.php', 'settings_mail.php', 'settings_notification.php', 'settings_default.php', 'settings_invoice.php', 'settings_quote.php', 'settings_online_payment.php', 'settings_online_payment_clients.php', 'settings_project.php', 'settings_ticket.php', 'settings_ai.php', 'identity_provider.php', 'settings_telemetry.php', 'settings_module.php']) ? 'menu-open' : ''); ?>">
<a href="#" class="nav-link">
<p>
SETTINGS
<i class="right fas fa-angle-left"></i>
</p>
</a>
<ul class="nav nav-treeview">
<li class="nav-item">
<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>
<p>Company Details</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Localization</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Theme</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Security</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Mail</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Notifications</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Defaults</p>
</a>
</li>
<?php if ($config_module_enable_accounting) { ?>
<li class="nav-item">
<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>
<p>Invoice</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Quote</p>
</a>
</li>
<?php } ?>
<?php if ($config_module_enable_ticketing) { ?>
<li class="nav-item">
<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>
<p>Project</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Ticket</p>
</a>
</li>
<?php } ?>
<!-- Currently the only integration is the client portal SSO -->
<?php if ($config_client_portal_enable) { ?>
<li class="nav-item">
<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>
<p>Identity Provider</p>
</a>
</li>
<?php } ?>
<li class="nav-item">
<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>
<p>Telemetry</p>
</a>
</li>
<li class="nav-item">
<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>
<p>Modules</p>
</a>
</li>
</ul>
</li>
<?php
$sql_custom_links = mysqli_query($mysqli, "SELECT * FROM custom_links
WHERE custom_link_location = 4 AND custom_link_archived_at IS NULL
ORDER BY custom_link_order ASC, custom_link_name ASC"
);
while ($row = mysqli_fetch_array($sql_custom_links)) {
$custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = sanitize_url($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']);
$custom_link_new_tab = intval($row['custom_link_new_tab']);
if ($custom_link_new_tab == 1) {
$target = "target='_blank' rel='noopener noreferrer'";
} else {
$target = "";
}
?>
<li class="nav-item">
<a href="<?php echo $custom_link_uri; ?>" <?php echo $target; ?> class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == basename($custom_link_uri)) { echo "active"; } ?>">
<i class="fas fa-<?php echo $custom_link_icon; ?> nav-icon"></i>
<p><?php echo $custom_link_name; ?></p>
<i class="fas fa-angle-right nav-icon float-right"></i>
</a>
</li>
<?php } ?>
</ul>
</nav>
<!-- /.sidebar-menu -->
<div class="mb-3"></div>
</div>
<!-- /.sidebar -->
</aside>

4
admin/index.php Normal file
View File

@ -0,0 +1,4 @@
<?php
header('Location: users.php');

193
admin/mail_queue.php Normal file
View File

@ -0,0 +1,193 @@
<?php
// Default Column Sortby Filter
$sort = "email_id";
$order = "DESC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM email_queue
WHERE (email_id LIKE '%$q%' OR email_from LIKE '%$q%' OR email_from_name LIKE '%$q%' OR email_recipient LIKE '%$q%' OR email_recipient_name LIKE '%$q%' OR email_subject LIKE '%$q%')
AND DATE(email_queued_at) BETWEEN '$dtf' AND '$dtt'
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-3">
<h3 class="card-title"><i class="fas fa-fw fa-mail-bulk mr-2"></i>Email Queue</h3>
</div>
<div class="card-body">
<form class="mb-4" autocomplete="off">
<div class="row">
<div class="col-sm-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 mail queue">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#advancedFilter"><i class="fas fa-filter"></i></button>
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-sm-8">
<div class="dropdown float-right" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-fw fa-layer-group mr-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu">
<button class="dropdown-item"
type="submit" form="bulkActions" name="bulk_cancel_emails">
<i class="fas fa-fw fa-ban mr-2"></i>Cancel
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger text-bold"
type="submit" form="bulkActions" name="bulk_delete_emails">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</button>
</div>
</div>
</div>
</div>
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Date range</label>
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
<input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
</div>
</div>
</div>
</div>
</form>
<hr>
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<table class="table table-sm table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<td class="bg-light pr-0">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
</div>
</td>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=email_queued_at&order=<?php echo $disp; ?>">
Queued <?php if ($sort == 'email_queued_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=email_from&order=<?php echo $disp; ?>">
From <?php if ($sort == 'email_from') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=email_recipient&order=<?php echo $disp; ?>">
To <?php if ($sort == 'email_recipient') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=email_subject&order=<?php echo $disp; ?>">
Subject <?php if ($sort == 'email_subject') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=email_status&order=<?php echo $disp; ?>">
Status <?php if ($sort == 'email_status') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=email_attempts&order=<?php echo $disp; ?>">
Attempts <?php if ($sort == 'email_attempts') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$email_id = intval($row['email_id']);
$email_from = nullable_htmlentities($row['email_from']);
$email_from_name = nullable_htmlentities($row['email_from_name']);
$email_recipient = nullable_htmlentities($row['email_recipient']);
$email_recipient_name = nullable_htmlentities($row['email_recipient_name']);
$email_subject = nullable_htmlentities($row['email_subject']);
$email_attempts = intval($row['email_attempts']);
$email_queued_at = nullable_htmlentities($row['email_queued_at']);
$email_failed_at = nullable_htmlentities($row['email_failed_at']);
$email_sent_at = nullable_htmlentities($row['email_sent_at']);
$email_status = intval($row['email_status']);
if ($email_status == 0) {
$email_status_display = "<div class='text-primary'>Queued</div>";
} elseif($email_status == 1) {
$email_status_display = "<div class='text-warning'>Sending</div>";
} elseif($email_status == 2) {
$email_status_display = "<div class='text-danger'>Failed</div><small class='text-secondary'>$email_failed_at</small>";
} else {
$email_status_display = "<div class='text-success'>Sent</div><small class='text-secondary'>$email_sent_at</small>";
}
?>
<tr>
<td class="pr-0 bg-light">
<?php if ($email_status !== 3) { ?>
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="email_ids[]" value="<?php echo $email_id ?>">
</div>
<?php } ?>
</td>
<td><?php echo $email_queued_at; ?></td>
<td><?php echo "$email_from<br><small class='text-secondary'>$email_from_name</small>"?></td>
<td><?php echo "$email_recipient<br><small class='text-secondary'>$email_recipient_name</small>"?></td>
<td><?php echo $email_subject; ?></td>
<td><?php echo $email_status_display; ?></td>
<td><?php echo $email_attempts; ?></td>
<td class="text-center">
<a class="btn btn-sm btn-secondary ajax-modal" href="#"
data-modal-size="lg"
data-modal-url="modals/mail_queue/mail_queue_message_view.php?id=<?= $email_id ?>">
<i class="fas fa-fw fa-eye"></i>
</a>
<!-- Show force resend if all retries have failed -->
<?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>
<?php } ?>
<!-- Allow cancelling a message if it hasn't yet been picked up (e.g. stuck/bugged) -->
<?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>
<?php } ?>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
</form>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>
<script src="../js/bulk_actions.js"></script>
<?php
require_once "../includes/footer.php";

View File

@ -0,0 +1,77 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<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="form-group">
<label>Provider <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-robot"></i></span>
</div>
<select class="form-control select2" name="provider" required>
<option value="">- Select an AI Provider -</option>
<?php
$sql_ai_providers = mysqli_query($mysqli, "SELECT * FROM ai_providers");
while ($row = mysqli_fetch_array($sql_ai_providers)) {
$ai_provider_id = intval($row['ai_provider_id']);
$ai_provider_name = nullable_htmlentities($row['ai_provider_name']);
?>
<option value="<?php echo $ai_provider_id; ?>"><?php echo $ai_provider_name; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>Model 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-robot"></i></span>
</div>
<input type="text" class="form-control" name="model" placeholder="ex gpt-4">
</div>
</div>
<div class="form-group">
<label>Use Case <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-list"></i></span>
</div>
<select class="form-control select2" name="use_case">
<option>General</option>
<option>Tickets</option>
<option>Documentation</option>
</select>
</div>
</div>
<div class="form-group">
<textarea class="form-control" rows="8" name="prompt" placeholder="Enter a model prompt:"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_ai_model" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</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,90 @@
<?php
require_once '../../../includes/modal_header.php';
$model_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM ai_models WHERE ai_model_id = $model_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$ai_model_ai_provider_id = intval($row['ai_model_ai_provider_id']);
$model_id = intval($row['ai_model_id']);
$model_name = nullable_htmlentities($row['ai_model_name']);
$use_case = nullable_htmlentities($row['ai_model_use_case']);
$prompt = nullable_htmlentities($row['ai_model_prompt']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-robot mr-2"></i>Editing: <strong><?php echo $model_name; ?></strong></h5>
<button type="button" class="close text-light" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="model_id" value="<?php echo $model_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>Provider <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-robot"></i></span>
</div>
<select class="form-control select2" name="provider" required>
<option value="">- Select an AI Provider -</option>
<?php
$sql_ai_providers = mysqli_query($mysqli, "SELECT * FROM ai_providers");
while ($row = mysqli_fetch_array($sql_ai_providers)) {
$ai_provider_id = intval($row['ai_provider_id']);
$ai_provider_name = nullable_htmlentities($row['ai_provider_name']);
?>
<option <?php if ($ai_provider_id = $ai_model_ai_provider_id) { echo "selected"; } ?> value="<?php echo $ai_provider_id; ?>"><?php echo $ai_provider_name; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>Model 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-robot"></i></span>
</div>
<input type="text" class="form-control" name="model" value="<?php echo $model_name; ?>" placeholder="ex gpt-4">
</div>
</div>
<div class="form-group">
<label>Use Case <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-list"></i></span>
</div>
<select class="form-control select2" name="use_case">
<option <?php if ($use_case == 'General') { echo "selected"; } ?>>General</option>
<option <?php if ($use_case == 'Tickets') { echo "selected"; } ?>>Tickets</option>
<option <?php if ($use_case == 'Documentation') { echo "selected"; } ?>>Documentation</option>
</select>
</div>
</div>
<div class="form-group">
<textarea class="form-control" rows="8" name="prompt" placeholder="Enter a model prompt:"><?php echo $prompt; ?></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_ai_model" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,58 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<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="form-group">
<label>Provider 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-robot"></i></span>
</div>
<input type="text" class="form-control" name="provider" placeholder="ex OpenAI">
</div>
</div>
<div class="form-group">
<label>URL <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-globe"></i></span>
</div>
<input type="url" class="form-control" name="url" placeholder="ex https://ai.company.ext/api">
</div>
</div>
<div class="form-group">
<label>API Key</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="api_key" placeholder="Enter API key here">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_ai_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</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,69 @@
<?php
require_once '../../../includes/modal_header.php';
$provider_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM ai_providers WHERE ai_provider_id = $provider_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$provider_name = nullable_htmlentities($row['ai_provider_name']);
$url = nullable_htmlentities($row['ai_provider_api_url']);
$key = nullable_htmlentities($row['ai_provider_api_key']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-robot mr-2"></i>Editing: <strong><?php echo $provider_name; ?></strong></h5>
<button type="button" class="close text-light" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="provider_id" value="<?php echo $provider_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>Provider 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-robot"></i></span>
</div>
<input type="text" class="form-control" name="provider" value="<?php echo $provider_name; ?>" placeholder="ex OpenAI">
</div>
</div>
<div class="form-group">
<label>URL <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-globe"></i></span>
</div>
<input type="url" class="form-control" name="url" value="<?php echo $url; ?>" placeholder="ex https://ai.company.ext/api">
</div>
</div>
<div class="form-group">
<label>API Key</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="api_key" value="<?php echo $key; ?>" placeholder="Enter API key here">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_ai_provider" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,124 @@
<?php
require_once '../../../includes/modal_header.php';
$key = randomString(156);
$decryptPW = randomString(160);
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<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-api-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-api-keys">Keys</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-api-details">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="key" value="<?php echo $key ?>">
<input type="hidden" name="password" value="<?php echo $decryptPW ?>">
<div class="form-group">
<label>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-sticky-note"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Key Name" maxlength="255" required autofocus>
</div>
</div>
<div class="form-group">
<label>Expiration Date <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-calendar"></i></span>
</div>
<input type="date" class="form-control" name="expire" min="<?php echo date('Y-m-d')?>" max="2999-12-31" required>
</div>
</div>
<div class="form-group">
<label>Client Access <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-user"></i></span>
</div>
<select class="form-control select2" name="client" required>
<option value="0"> ALL CLIENTS </option>
<?php
$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)) {
$client_id = intval($row['client_id']);
$client_name = nullable_htmlentities($row['client_name']); ?>
<option value="<?php echo $client_id; ?>"><?php echo "$client_name (Client ID: $client_id)"; ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-api-keys">
<div class="form-group">
<label>API Key <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-key"></i></span>
</div>
<input type="text" class="form-control" value="<?php echo $key ?>" required disabled>
<div class="input-group-append">
<button class="btn btn-default clipboardjs" type="button" data-clipboard-text="<?php echo $key; ?>"><i class="fa fa-fw fa-copy"></i></button>
</div>
</div>
</div>
<div class="form-group">
<label>Login credential decryption password <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-unlock-alt"></i></span>
</div>
<input type="text" class="form-control" value="<?php echo $decryptPW ?>" required disabled>
<div class="input-group-append">
<button class="btn btn-default clipboardjs" type="button" data-clipboard-text="<?php echo $decryptPW; ?>"><i class="fa fa-fw fa-copy"></i></button>
</div>
</div>
</div>
<br>
<div class="form-group">
<label>I have made a copy of the key(s)<strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<input type="checkbox" name="ack" value="1" required>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_api_key" class="btn btn-primary text-bold"><i class="fa 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>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,70 @@
<?php
require_once '../../../includes/modal_header.php';
$category = nullable_htmlentities($_GET['category'] ?? '');
$category_types_array = ['Expense', 'Income', 'Referral', 'Ticket'];
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-list-ul mr-2"></i>New <strong><?= nullable_htmlentities($category) ?></strong> Category</h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<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">
<label>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-list-ul"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Category name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Color <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-paint-brush"></i></span>
</div>
<input type="color" class="form-control col-3" name="color" required>
</div>
</div>
</div>
<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 Category</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,56 @@
<?php
require_once '../../../includes/modal_header.php';
$category_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_id = $category_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$category_name = nullable_htmlentities($row['category_name']);
$category_color = nullable_htmlentities($row['category_color']);
$category_type = nullable_htmlentities($row['category_type']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-list-ul mr-2"></i>Editing category: <strong><?php echo $category_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="category_id" value="<?php echo $category_id; ?>">
<input type="hidden" name="type" value="<?php echo $category_type; ?>">
<div class="modal-body">
<div class="form-group">
<label>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-list-ul"></i></span>
</div>
<input type="text" class="form-control" name="name" maxlength="200" value="<?php echo $category_name; ?>" required>
</div>
</div>
<div class="form-group">
<label>Color <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-paint-brush"></i></span>
</div>
<input type="color" class="form-control col-3" name="color" value="<?php echo $category_color; ?>" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_category" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

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_array($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

@ -0,0 +1,41 @@
<div class="modal" id="createCustomFieldModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-th-list mr-2"></i>Create <?php echo nullable_htmlentities($table); ?> field</h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="table" value="<?php echo nullable_htmlentities($table); ?>">
<div class="modal-body">
<div class="form-group">
<label>Label <strong class="text-danger">*</strong></label>
<input type="text" class="form-control" name="label" placeholder="Enter a custom field label" maxlength="255" required autofocus>
</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="">- Select a field type -</option>
<option>Text</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="create_custom_field" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,40 @@
<div class="modal" id="editCustomFieldModal<?php echo $custom_field_id; ?>" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-th-list mr-2"></i>Editing custom field: <strong><?php echo $custom_field_label; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="custom_field_id" value="<?php echo $custom_field_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>Label <strong class="text-danger">*</strong></label>
<input type="text" class="form-control" name="label" maxlength="255" value="<?php echo $custom_field_label; ?>" required>
</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="">- Select a field type -</option>
<option <?php if ($custom_field_type == 'text') { echo "selected"; } ?> value="text">Text</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_custom_field" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,88 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
<div class="form-group">
<label>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-list-ul"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Link name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Order</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-sort-numeric-down"></i></span>
</div>
<input type="number" class="form-control" name="order" placeholder="Leave blank for no order">
</div>
</div>
<div class="form-group">
<label>URI <strong class="text-danger">*</strong></label> / <span class="text-secondary">Open New Tab</span>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-external-link-alt"></i></span>
</div>
<input type="text" class="form-control" name="uri" placeholder="Enter Link" maxlength="500" required>
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="new_tab" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Icon</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-image"></i></span>
</div>
<input type="text" class="form-control" name="icon" placeholder="Icon ex handshake" maxlength="200">
</div>
</div>
<div class="form-group">
<label>Location <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-home"></i></span>
</div>
<select class="form-control select2" name="location" required>
<option value="1">Main Side Nav</option>
<option value="2">Top Nav (Icon Required)</option>
<option value="3">Client Portal Nav</option>
<option value="4">Admin Nav</option>
<option value="5">Reports Nav</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_custom_link" class="btn btn-primary text-bold"><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>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,101 @@
<?php
require_once '../../../includes/modal_header.php';
$custom_link_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM custom_links WHERE custom_link_id = $custom_link_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = nullable_htmlentities($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']);
$custom_link_new_tab = intval($row['custom_link_new_tab']);
$custom_link_order = intval($row['custom_link_order']);
$custom_link_location = intval($row['custom_link_location']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-external-link-alt mr-2"></i>Editing link: <strong><?php echo $custom_link_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="custom_link_id" value="<?php echo $custom_link_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>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-list-ul"></i></span>
</div>
<input type="text" class="form-control" name="name" value="<?php echo $custom_link_name; ?>" maxlength="200" required>
</div>
</div>
<div class="form-group">
<label>Order</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-sort-numeric-down"></i></span>
</div>
<input type="number" class="form-control" name="order" placeholder="Leave blank for no order" value="<?php echo $custom_link_order; ?>">
</div>
</div>
<div class="form-group">
<label>URI <strong class="text-danger">*</strong></label> / <span class="text-secondary">Open New Tab</span>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-external-link-alt"></i></span>
</div>
<input type="text" class="form-control" name="uri" placeholder="Enter Link" maxlength="500" value="<?php echo $custom_link_uri; ?>" required>
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="new_tab" value="1" <?php if ($custom_link_new_tab == 1) { echo "checked"; } ?>>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Icon</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-image"></i></span>
</div>
<input type="text" class="form-control" name="icon" placeholder="Icon ex handshake" maxlength="200" value="<?php echo $custom_link_icon; ?>">
</div>
</div>
<div class="form-group">
<label>Location <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-home"></i></span>
</div>
<select class="form-control select2" name="location" required>
<option value="1" <?php if ($custom_link_location === 1) { echo "selected"; } ?> >Main Side Nav</option>
<option value="2" <?php if ($custom_link_location === 2) { echo "selected"; } ?> >Top Nav (Icon Required)</option>
<option value="3" <?php if ($custom_link_location === 3) { echo "selected"; } ?> >Client Portal Nav</option>
<option value="4" <?php if ($custom_link_location === 4) { echo "selected"; } ?> >Admin Nav</option>
<option value="5" <?php if ($custom_link_location === 5) { echo "selected"; } ?> >Reports Nav</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_custom_link" 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="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,87 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
<div class="form-group">
<input type="text" class="form-control" name="name" placeholder="Template name" maxlength="200">
</div>
<div class="form-group">
<label>Enter a prompt for the type of IT documentation you want to generate:</label>
<div class="input-group mb-3">
<input type="text" class="form-control" id="aiPrompt" placeholder="e.g. 'A network troubleshooting guide for junior technicians'">
<div class="input-group-append">
<button class="btn btn-info" type="button" id="generateAIContent">
<i class="fa fa-fw fa-magic mr-1"></i>Generate with AI
</button>
</div>
</div>
</div>
<!-- TinyMCE Content -->
<div class="form-group">
<textarea class="form-control tinymce" name="content"></textarea>
</div>
<div class="form-group">
<input type="text" class="form-control" name="description" placeholder="Enter a short summary">
</div>
</div>
<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="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div>
</form>
<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: '/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

@ -0,0 +1,47 @@
<?php
require_once '../../../includes/modal_header.php';
$document_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']);
$document_template_content = nullable_htmlentities($row['document_template_content']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-file-alt mr-2"></i>Editing template: <strong><?php echo $document_template_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="document_template_id" value="<?php echo $document_template_id; ?>">
<div class="modal-body">
<div class="form-group">
<input type="text" class="form-control" name="name" maxlength="200" value="<?php echo $document_template_name; ?>" placeholder="Name" required>
</div>
<div class="form-group">
<textarea class="form-control tinymce" name="content"><?php echo $document_template_content; ?></textarea>
</div>
<div class="form-group">
<input type="text" class="form-control" name="description" value="<?php echo $document_template_description; ?>" placeholder="Short summary">
</div>
</div>
<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="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,79 @@
<?php
require_once '../../../includes/modal_header.php';
if (!isset($session_is_admin) || !$session_is_admin) {
exit(WORDING_ROLECHECK_FAILED . "<br>Tell your admin: Your role does not have admin access.");
}
$email_id = intval($_GET['id']);
//Initialize the HTML Purifier to prevent XSS
require "../../../plugins/htmlpurifier/HTMLPurifier.standalone.php";
$purifier_config = HTMLPurifier_Config::createDefault();
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
$purifier = new HTMLPurifier($purifier_config);
$sql = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_id = $email_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$email_from = nullable_htmlentities($row['email_from']);
$email_from_name = nullable_htmlentities($row['email_from_name']);
$email_recipient = nullable_htmlentities($row['email_recipient']);
$email_recipient_name = nullable_htmlentities($row['email_recipient_name']);
$email_subject = nullable_htmlentities($row['email_subject']);
$email_content = $purifier->purify($row['email_content']);
$email_attempts = intval($row['email_attempts']);
$email_queued_at = nullable_htmlentities($row['email_queued_at']);
$email_failed_at = nullable_htmlentities($row['email_failed_at']);
$email_sent_at = nullable_htmlentities($row['email_sent_at']);
$email_status = intval($row['email_status']);
if ($email_status == 0) {
$email_status_display = "<div class='text-primary'>Queued</div>";
} elseif($email_status == 1) {
$email_status_display = "<div class='text-warning'>Sending</div>";
} elseif($email_status == 2) {
$email_status_display = "<div class='text-danger'>Failed</div><small class='text-secondary'>$email_failed_at</small>";
} else {
$email_status_display = "<div class='text-success'>Sent</div><small class='text-secondary'>$email_sent_at</small>";
}
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class='fas fa-fw fa-envelope-open mr-2'></i><strong><?php echo $email_subject; ?></strong></h5>
<button type="button" class="close text-light" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-1">
<span class="text-secondary">From:</span>
</div>
<div class="col-md-10">
<?php echo "<strong>$email_from_name</strong> ($email_from)"; ?>
</div>
</div>
<hr class="my-2">
<div class="row">
<div class="col-md-1">
<span class="text-secondary">To:</span>
</div>
<div class="col-md-10">
<?php echo "<strong>$email_recipient_name</strong> ($email_recipient)"; ?>
</div>
</div>
<hr class="my-2">
<div class="prettyContent">
<?php echo $email_content; ?>
</div>
</div>
<script src="../../js/pretty_content.js"></script>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,42 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<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="form-group">
<label>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-credit-card"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Payment method name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<textarea class="form-control" rows="3" name="description" placeholder="Enter a description..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_payment_method" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</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,51 @@
<?php
require_once '../../../includes/modal_header.php';
$payment_method_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM payment_methods WHERE payment_method_id = $payment_method_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$payment_method_id = intval($row['payment_method_id']);
$payment_method_name = nullable_htmlentities($row['payment_method_name']);
$payment_method_description = nullable_htmlentities($row['payment_method_description']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<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 $payment_method_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<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="form-group">
<label>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-credit-card"></i></span>
</div>
<input type="text" class="form-control" name="name" value="<?php echo $payment_method_name; ?>" placeholder="Payment method name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<textarea class="form-control" rows="3" name="description" placeholder="Enter a description..."><?php echo $payment_method_description; ?></textarea>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_payment_method" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,203 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<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="alert alert-info text-center">
<h6>Before Adding a Payment Provider!</h6>
We recommend you add an <strong>Account</strong> and <strong>Vendor</strong> based off the Provider name before continuing eg <strong>Stripe</strong>
</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">
<label>Provider <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-credit-card"></i></span>
</div>
<select class="form-control select2" name="provider">
<option>Stripe</option>
</select>
</div>
</div>
<div class="form-group">
<label>Publishable key <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-eye"></i></span>
</div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)">
</div>
</div>
<div class="form-group">
<label>Secret key <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-key"></i></span>
</div>
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)">
</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_array($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">
<label>Threshold</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00">
</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>
</div>
</div>
<div class="tab-pane fade" id="pills-expense">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" name="enable_expense" checked value="1" id="enableExpenseSwitch">
<label class="custom-control-label" for="enableExpenseSwitch">Enable Expense</label>
</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_array($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_array($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">
<label>Percentage Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" placeholder="Enter Percentage">
</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>
</div>
<div class="form-group">
<label>Flat Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" placeholder="0.030">
</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>
</div>
</div>
</div>
</div>
<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="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,197 @@
<?php
require_once '../../../includes/modal_header.php';
$provider_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers WHERE payment_provider_id = $provider_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$provider_name = nullable_htmlentities($row['payment_provider_name']);
$public_key = nullable_htmlentities($row['payment_provider_public_key']);
$private_key = nullable_htmlentities($row['payment_provider_private_key']);
$account_id = intval($row['payment_provider_account']);
$threshold = floatval($row['payment_provider_threshold']);
$vendor_id = intval($row['payment_provider_expense_vendor']);
$category_id = intval($row['payment_provider_expense_category']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="provider_id" value="<?= $provider_id ?>">
<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">
<label>Publishable key <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-eye"></i></span>
</div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?= $public_key ?>">
</div>
</div>
<div class="form-group">
<label>Secret key <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-key"></i></span>
</div>
<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_array($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 class="form-group">
<label>Threshold</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<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>
<small class="form-text text-muted">Will not show as an option at Checkout if above this number</small>
</div>
</div>
<div class="tab-pane fade" id="pills-expense">
<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_array($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_array($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>
<div class="form-group">
<label>Percentage Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
</div>
<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>
<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 class="form-group">
<label>Flat Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<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>
<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 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="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,45 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
<div class="form-group">
<label>Project 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-project-diagram"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Project Template Name" maxlength="255" required autofocus>
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Description">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_project_template" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Create</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,56 @@
<?php
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_array($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">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>Project 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-project-diagram"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Project Template Name" maxlength="255" value="<?php echo $project_template_name; ?>" required autofocus>
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Description" value="<?php echo $project_template_description; ?>">
</div>
</div>
</div>
<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>Save</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,71 @@
<?php
require_once '../../../includes/modal_header.php';
$project_template_id = intval($_GET['project_template_id']);
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="project_template_id" value="<?php echo $project_template_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>Ticket Template <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-life-ring"></i></span>
</div>
<select class="form-control select2" name="ticket_template_id" required>
<option value="">- Select a Ticket Template -</option>
<?php
$sql_ticket_templates_select = mysqli_query($mysqli, "SELECT ticket_template_id, ticket_template_name FROM ticket_templates
WHERE ticket_template_id NOT IN (
SELECT ticket_template_id FROM project_template_ticket_templates
WHERE project_template_id = $project_template_id
)
AND ticket_template_archived_at IS NULL
ORDER BY ticket_template_name ASC"
);
while ($row = mysqli_fetch_array($sql_ticket_templates_select)) {
$ticket_template_id_select = intval($row['ticket_template_id']);
$ticket_template_name_select = nullable_htmlentities($row['ticket_template_name']);
?>
<option value="<?php echo $ticket_template_id_select; ?>"><?php echo $ticket_template_name_select; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group">
<label>Order</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-sort-numeric-down"></i></span>
</div>
<input type="text" class="form-control" name="order" value="1">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_ticket_template_to_project_template" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Add</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,62 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body">
<div class="tab-content">
<div class="form-group">
<label>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-user-shield"></i></span>
</div>
<input type="text" class="form-control" name="role_name" placeholder="Role Name" maxlength="200" required>
</div>
</div>
<div class="form-group">
<label>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-chevron-right"></i></span>
</div>
<input type="text" class="form-control" name="role_description" placeholder="Role Description" maxlength="200" required>
</div>
</div>
<div class="form-group">
<label>Admin Access <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-tools"></i></span>
</div>
<select class="form-control select2" name="role_is_admin" required>
<option value="0">No - edit after creation to set permissions</option>
<option value="1">Yes - this role should have full admin access</option>
</select>
</div>
</div>
</div>
</div>
<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="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,153 @@
<?php
require_once '../../../includes/modal_header.php';
$role_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_id = $role_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$role_name = nullable_htmlentities($row['role_name']);
$role_description = nullable_htmlentities($row['role_description']);
$role_admin = intval($row['role_is_admin']);
// Count number of users that have each role
$sql_role_user_count = mysqli_query($mysqli, "SELECT COUNT(user_id) FROM users WHERE user_role_id = $role_id AND user_archived_at IS NULL");
$role_user_count = mysqli_fetch_row($sql_role_user_count)[0];
$sql_users = mysqli_query($mysqli, "SELECT * FROM users WHERE user_role_id = $role_id AND user_archived_at IS NULL");
// Initialize an empty array to hold user names
$user_names = [];
// Fetch each row and store the user_name in the array
while($row = mysqli_fetch_assoc($sql_users)) {
$user_names[] = nullable_htmlentities($row['user_name']);
}
// Convert the array of user names to a comma-separated string
$user_names_string = implode(",", $user_names) ;
if (empty($user_names_string)) {
$user_names_string = "-";
}
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-user-shield mr-2"></i>Editing role:
<strong><?php echo $role_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<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="role_id" value="<?php echo $role_id; ?>">
<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<?php echo $role_id; ?>">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-role-access<?php echo $role_id; ?>">Access</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-role-details<?php echo $role_id; ?>">
<div class="form-group">
<label>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-user-shield"></i></span>
</div>
<input type="text" class="form-control" name="role_name" placeholder="Role Name" maxlength="200" value="<?php echo $role_name; ?>" required>
</div>
</div>
<div class="form-group">
<label>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-chevron-right"></i></span>
</div>
<input type="text" class="form-control" name="role_description" placeholder="Role Description" maxlength="200" value="<?php echo $role_description; ?>" required>
</div>
</div>
<div class="form-group">
<label>Admin Access <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-tools"></i></span>
</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>
<option value="0" <?php if (!$role_admin) { echo 'selected'; } ?>>No - use permissions on the next tab</option>
</select>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-role-access<?php echo $role_id; ?>">
<?php if ($role_admin) { ?>
<div class="alert alert-warning"><strong>Module permissions do not apply to Admins.</strong></div>
<?php } ?>
<?php
// Enumerate modules
$sql_modules = mysqli_query($mysqli, "SELECT * FROM modules");
while ($row_modules = mysqli_fetch_array($sql_modules)) {
$module_id = intval($row_modules['module_id']);
$module_name = nullable_htmlentities($row_modules['module_name']);
$module_name_display = ucfirst(str_replace("module_","",$module_name));
$module_description = nullable_htmlentities($row_modules['module_description']);
// 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 = 0;
if ($module_permission_row) {
$module_permission = $module_permission_row['user_role_permission_level'];
}
?>
<div class="form-group">
<label> <?php echo $module_name_display ?> <strong class="text-danger">*</strong></label>
<div class="input-group">
<select class="form-control select2" name="<?php echo "$module_id##$module_name" ?>" required>
<option value="0" <?php if ($module_permission == 0) { echo 'selected'; } ?> >None</option>
<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>
</select>
</div>
<small class="form-text text-muted"><?php echo $module_description ?></small>
</div>
<?php } // End while ?>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_role" 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="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,88 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
<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-cube"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Software name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Version</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
</div>
<input type="text" class="form-control" name="version" placeholder="Software version" maxlength="200">
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Short description">
</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-tag"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Type -</option>
<?php foreach($software_types_array as $software_type) { ?>
<option><?php echo $software_type; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>License Type</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
</div>
<select class="form-control select2" name="license_type">
<option value="">- Select a License Type -</option>
<?php foreach($license_types_array as $license_type) { ?>
<option><?php echo $license_type; ?></option>
<?php } ?>
</select>
</div>
</div>
<textarea class="form-control" rows="8" placeholder="Enter some notes" name="notes"></textarea>
</div>
<div class="modal-footer">
<button type="submit" name="add_software_template" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</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,99 @@
<?php
require_once '../../../includes/modal_header.php';
$software_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM software_templates WHERE software_template_id = $software_template_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$software_name = nullable_htmlentities($row['software_template_name']);
$software_version = nullable_htmlentities($row['software_template_version']);
$software_description = nullable_htmlentities($row['software_template_description']);
$software_type = nullable_htmlentities($row['software_template_type']);
$software_license_type = nullable_htmlentities($row['software_template_license_type']);
$software_notes = nullable_htmlentities($row['software_template_notes']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-cube mr-2"></i>Editing template: <strong><?php echo $software_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="software_template_id" value="<?php echo $software_template_id; ?>">
<div class="modal-body">
<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-cube"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Software name" maxlength="200" value="<?php echo $software_name; ?>" required>
</div>
</div>
<div class="form-group">
<label>Version</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
</div>
<input type="text" class="form-control" name="version" placeholder="Software version" maxlength="200" value="<?php echo $software_version; ?>">
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Short description" value="<?php echo $software_description; ?>">
</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-tag"></i></span>
</div>
<select class="form-control select2" name="type" required>
<?php foreach($software_types_array as $software_type_select) { ?>
<option <?php if($software_type == $software_type_select) { echo "selected"; } ?>><?php echo $software_type_select; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>License Type</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
</div>
<select class="form-control select2" name="license_type">
<option value="">- Select a License Type -</option>
<?php foreach($license_types_array as $license_type_select) { ?>
<option <?php if($license_type_select == $software_license_type){ echo "selected"; } ?>><?php echo $license_type_select; ?></option>
<?php } ?>
</select>
</div>
</div>
<textarea class="form-control" rows="8" placeholder="Enter some notes" name="notes"><?php echo $software_notes; ?></textarea>
</div>
<div class="modal-footer">
<button type="submit" name="edit_software_template" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,98 @@
<?php
require_once '../../../includes/modal_header.php';
$type_display = '';
if (isset($_GET['type'])) {
$type = intval($_GET['type']);
if ($type === 1) {
$type_display = "Client";
} elseif($type === 2) {
$type_display = "Location";
} elseif ($type === 3) {
$type_display = "Contact";
} elseif ($type === 4) {
$type_display = "Credential";
} elseif ($type === 5) {
$type_display = "Asset";
}
}
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-tag mr-2"></i>New <strong><?= $type_display ?></strong> Tag</h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="type" value="<?php echo $type; ?>">
<div class="modal-body">
<div class="form-group">
<label>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-tag"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Tag name" maxlength="200" required autofocus>
</div>
</div>
<?php if (isset($_GET['type'])) { ?>
<input type="hidden" name="type" value="<?= $type ?>">
<?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-th"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Type -</option>
<option value="1">Client Tag</option>
<option value="2">Location Tag</option>
<option value="3">Contact Tag</option>
<option value="4">Credential Tag</option>
<option value="5">Asset Tag</option>
</select>
</div>
</div>
<?php } ?>
<div class="form-group">
<label>Color <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-paint-brush"></i></span>
</div>
<input type="color" class="form-control col-3" name="color" required>
</div>
</div>
<div class="form-group">
<label>Icon</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-image"></i></span>
</div>
<input type="text" class="form-control" name="icon" placeholder="Icon ex handshake">
</div>
</div>
</div>
<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 Tag</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,79 @@
<?php
require_once '../../../includes/modal_header.php';
$tag_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM tags WHERE tag_id = $tag_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$tag_name = nullable_htmlentities($row['tag_name']);
$tag_type = intval($row['tag_type']);
$tag_color = nullable_htmlentities($row['tag_color']);
$tag_icon = nullable_htmlentities($row['tag_icon']);
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();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="tag_id" value="<?php echo $tag_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>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-tag"></i></span>
</div>
<input type="text" class="form-control" name="name" maxlength="200" value="<?php echo $tag_name; ?>" required>
</div>
</div>
<div class="form-group">
<label>Color <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-paint-brush"></i></span>
</div>
<input type="color" class="form-control col-3" name="color" value="<?php echo $tag_color; ?>" required>
</div>
</div>
<div class="form-group">
<label>Icon</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-image"></i></span>
</div>
<input type="text" class="form-control" name="icon" placeholder="Icon ex handshake" value="<?php echo $tag_icon; ?>">
</div>
</div>
</div>
<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 changes</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,35 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span aria-hidden="true">&times;</span>
</button>
</div>
<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="form-group">
<label>Name <strong class="text-danger">*</strong></label>
<input type="text" class="form-control" name="name" placeholder="Tax name" maxlength="200" required autofocus>
</div>
<div class="form-group">
<label>Percent <strong class="text-danger">*</strong></label>
<input type="number" min="0" step="any" class="form-control col-md-4" name="percent">
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_tax" class="btn btn-primary text-bold"><i class="fa 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>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,46 @@
<?php
require_once '../../../includes/modal_header.php';
$tax_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM taxes WHERE tax_id = $tax_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$tax_name = nullable_htmlentities($row['tax_name']);
$tax_percent = floatval($row['tax_percent']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-balance-scale mr-2"></i>Editing tax: <strong><?php echo $tax_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tax_id" value="<?php echo $tax_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>Name <strong class="text-danger">*</strong></label>
<input type="text" class="form-control" name="name" maxlength="200" value="<?php echo $tax_name; ?>" required>
</div>
<div class="form-group">
<label>Percent <strong class="text-danger">*</strong></label>
<input type="number" min="0" step="any" class="form-control col-md-4" name="percent" value="<?php echo $tax_percent; ?>">
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_tax" 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="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,43 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
<div class="form-group">
<label>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-info-circle"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Ticket Status name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Color <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-paint-brush"></i></span>
</div>
<input type="color" class="form-control col-3" name="color" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_ticket_status" class="btn btn-primary text-bold"><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>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,79 @@
<?php
require_once '../../../includes/modal_header.php';
$ticket_status_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM ticket_statuses WHERE ticket_status_id = $ticket_status_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$ticket_status_name = nullable_htmlentities($row['ticket_status_name']);
$ticket_status_color = nullable_htmlentities($row['ticket_status_color']);
$ticket_status_order = intval($row['ticket_status_order']);
$ticket_status_active = intval($row['ticket_status_active']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-info-circle mr-2"></i>Editing Ticket Status: <strong><?php echo $ticket_status_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="ticket_status_id" value="<?php echo $ticket_status_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>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-tag"></i></span>
</div>
<input type="text" class="form-control" name="name" maxlength="200" value="<?php echo $ticket_status_name; ?>" required <?php if ($ticket_status_id <= 5) { echo "readonly"; } ?>>
</div>
</div>
<div class="form-group">
<label>Color <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-paint-brush"></i></span>
</div>
<input type="color" class="form-control col-3" name="color" value="<?php echo $ticket_status_color; ?>" required>
</div>
</div>
<div class="form-group">
<label>Order</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-sort-numeric-down"></i></span>
</div>
<input type="number" class="form-control" name="order" placeholder="Leave blank for no order" value="<?php echo $ticket_status_order; ?>">
</div>
</div>
<div class="form-group">
<label>Status <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-info-circle"></i></span>
</div>
<select class="form-control select2" name="status" required>
<option <?php if ($ticket_status_active == 1) { echo "selected"; } ?> value="1">Active</option>
<option <?php if ($ticket_status_active == 0) { echo "selected"; } ?> value="0" <?php if ($ticket_status_id <= 5) { echo "disabled"; } ?>>Inactive</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_ticket_status" 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="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,79 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
<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-life-ring"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Template name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Subject</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="subject" placeholder="Subject" maxlength="500">
</div>
</div>
<div class="form-group">
<textarea class="form-control tinymceTicket" name="details"></textarea>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Short description">
</div>
</div>
<div class="form-group">
<label>Add it to a Project Template?</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-project-diagram"></i></span>
</div>
<select class="form-control select2" name="project_template">
<option value="0">- No -</option>
<?php
$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)) {
$project_template_id_select = intval($row['project_template_id']);
$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>
<?php } ?>
</select>
</div>
</div>
</div>
<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 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,57 @@
<div class="modal" id="editTicketTemplateModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-life-ring mr-2"></i>Editing Ticket Template: <?php echo $ticket_template_name; ?></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="ticket_template_id" value="<?php echo $ticket_template_id; ?>">
<div class="modal-body">
<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-life-ring"></i></span>
</div>
<input type="text" class="form-control" name="name" maxlength="200" value="<?php echo $ticket_template_name; ?>" placeholder="Template name" required autofocus>
</div>
</div>
<div class="form-group">
<label>Subject</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="subject" maxlength="500" value="<?php echo $ticket_template_subject; ?>" placeholder="Subject">
</div>
</div>
<div class="form-group">
<textarea class="form-control tinymceTicket" name="details"><?php echo $ticket_template_details; ?></textarea>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" value="<?php echo $ticket_template_description; ?>" placeholder="Short description">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_ticket_template" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,62 @@
<?php
require_once '../../../includes/modal_header.php';
$task_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_id = $task_template_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$task_template_name = nullable_htmlentities($row['task_template_name']);
$task_template_order = intval($row['task_template_order']);
$task_template_completion_estimate = intval($row['task_template_completion_estimate']);
//$task_template_description = nullable_htmlentities($row['task_template_description']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-tasks mr-2"></i>Editing task</h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="task_template_id" value="<?php echo $task_template_id; ?>">
<div class="modal-body">
<div class="form-group">
<label>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-tag"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Name the task" maxlength="255" value="<?php echo $task_template_name; ?>" required autofocus>
</div>
</div>
<div class="form-group">
<label>Estimated Completion Time <span class="text-secondary">(Minutes)</span></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="completion_estimate" placeholder="Estimated time to complete task in mins" value="<?php echo $task_template_completion_estimate; ?>">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_ticket_template_task" class="btn btn-primary text-bold"><i class="fa 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>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,176 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<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-user-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-user-access">Restrict Access</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-user-details">
<div class="form-group">
<label>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-user"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Full Name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Email <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-envelope"></i></span>
</div>
<input type="email" class="form-control" name="email" placeholder="Email Address" maxlength="200" required>
</div>
</div>
<div class="form-group">
<label>Password <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-lock"></i></span>
</div>
<input type="password" class="form-control" data-toggle="password" name="password" id="password" placeholder="Enter a Password" autocomplete="new-password" minlength="8" required>
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</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 class="form-group">
<label>Role <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-user-shield"></i></span>
</div>
<select class="form-control select2" name="role" required>
<option value="">- Role -</option>
<?php
$sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_user_roles)) {
$role_id = intval($row['role_id']);
$role_name = nullable_htmlentities($row['role_name']);
?>
<option value="<?php echo $role_id; ?>"><?php echo $role_name; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>Avatar</label>
<input type="file" class="form-control-file" accept="image/*" name="file">
</div>
<div class="form-group" <?php if(empty($config_smtp_host)) { echo "hidden"; } ?>>
<div class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" id="sendEmailCheckBox" name="send_email" value="" checked>
<label for="sendEmailCheckBox" class="custom-control-label">
Send user e-mail with login details?
</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" id="forceMFACheckBox" name="force_mfa" value=1>
<label for="forceMFACheckBox" class="custom-control-label">
Force MFA
</label>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-user-access">
<div class="alert alert-info">
Check boxes to authorize user client access. No boxes grant full client access. Admin users are unaffected.
</div>
<ul class="list-group">
<li class="list-group-item bg-dark">
<div class="form-check">
<input type="checkbox" class="form-check-input" onclick="this.closest('.tab-pane').querySelectorAll('.client-checkbox').forEach(checkbox => checkbox.checked = this.checked);">
<label class="form-check-label ml-3"><strong>Restrict Access to Clients</strong></label>
</div>
</li>
<?php
$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)) {
$client_id = intval($row['client_id']);
$client_name = nullable_htmlentities($row['client_name']);
?>
<li class="list-group-item">
<div class="form-check">
<input type="checkbox" class="form-check-input client-checkbox" name="clients[]" value="<?php echo $client_id; ?>">
<label class="form-check-label ml-3"><?php echo $client_name; ?></label>
</div>
</li>
<?php } ?>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_user" class="btn btn-primary text-bold"><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>
</form>
<script>
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

@ -0,0 +1,35 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-body">
<div class="mb-4" style="text-align: center;">
<i class="far fas fa-10x fa-skull-crossbones text-danger mb-3 mt-3"></i>
<h2>Incident Response: Agent Password Reset</h2>
<br>
<div class="alert alert-danger" role="alert">
<b>This is a potentially destructive function.<br>It is intended to be used as part of a potential security incident.</b>
</div>
<h6 class="mb-4 text-secondary"><b>All ITFlow agent passwords will be reset and shown to you </b><i>(except yours - change yours first!)</i>.<br/><br/>You should communicate temporary passwords to agents out of band (e.g. via a phone call) and require they are changed ASAP.</h6>
<form action="post.php" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="row col-7 offset-4">
<div class="input-group">
<div class="input-group-prepend">
<input type="password" class="form-control" placeholder="Enter your account password to continue" name="admin_password" required>
</div>
</div>
</div>
<br>
<button class="btn btn-danger" type="submit" name="ir_reset_user_password"><i class="fas fa-fw fa-key mr-2"></i>Reset passwords</button>
</form>
</div>
<button type="button" class="btn btn-outline-secondary btn-lg px-5 mr-4" data-dismiss="modal">Cancel</button>
</div>
<?php
require_once "../../../includes/modal_footer.php";

View File

@ -0,0 +1,83 @@
<?php
require_once '../../../includes/modal_header.php';
$user_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM users WHERE users.user_id = $user_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$user_name = nullable_htmlentities($row['user_name']);
$user_email = nullable_htmlentities($row['user_email']);
$user_avatar = nullable_htmlentities($row['user_avatar']);
$user_initials = nullable_htmlentities(initials($user_name));
$sql_related_tickets = mysqli_query($mysqli, "SELECT * FROM tickets
WHERE ticket_assigned_to = $user_id AND ticket_resolved_at IS NULL AND ticket_closed_at IS NULL");
$ticket_count = mysqli_num_rows($sql_related_tickets);
// Related Recurring Tickets Query
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT * FROM recurring_tickets WHERE recurring_ticket_assigned_to = $user_id");
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-user-slash mr-2"></i>Archiving user:
<strong><?php echo $user_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="user_id" value="<?php echo $user_id; ?>">
<div class="modal-body">
<center class="mb-3">
<?php if (!empty($user_avatar)) { ?>
<img class="img-fluid" src="<?php echo "../uploads/users/$user_id/$user_avatar"; ?>">
<?php } else { ?>
<span class="fa-stack fa-4x">
<i class="fa fa-circle fa-stack-2x text-secondary"></i>
<span class="fa fa-stack-1x text-white"><?php echo $user_initials; ?></span>
</span>
<?php } ?>
</center>
<div class="form-group">
<label>Reassign <?= $ticket_count ?> Open Tickets and <?= $recurring_ticket_count ?> Recurring Tickets To:</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
</div>
<select class="form-control select2" name="ticket_assign" required>
<option value="0">No one</option>
<?php
$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)) {
$user_id_select = intval($row['user_id']);
$user_name_select = nullable_htmlentities($row['user_name']);
?>
<option value="<?= $user_id_select ?>"><?= $user_name_select ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="archive_user" class="btn btn-danger text-bold"><i class="fas fa-archive mr-2"></i>Archive</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once "../../../includes/modal_footer.php";

View File

@ -0,0 +1,225 @@
<?php
require_once '../../../includes/modal_header.php';
$user_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM users
LEFT JOIN user_settings ON users.user_id = user_settings.user_id
WHERE users.user_id = $user_id LIMIT 1"
);
$row = mysqli_fetch_array($sql);
$user_name = nullable_htmlentities($row['user_name']);
$user_email = nullable_htmlentities($row['user_email']);
$user_avatar = nullable_htmlentities($row['user_avatar']);
$user_token = nullable_htmlentities($row['user_token']);
$user_config_force_mfa = intval($row['user_config_force_mfa']);
$user_role_id = intval($row['user_role_id']);
$user_initials = nullable_htmlentities(initials($user_name));
// Get User Client Access Permissions
$user_client_access_sql = mysqli_query($mysqli,"SELECT client_id FROM user_client_permissions WHERE user_id = $user_id");
$client_access_array = [];
while ($row = mysqli_fetch_assoc($user_client_access_sql)) {
$client_access_array[] = intval($row['client_id']);
}
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-user-edit mr-2"></i>Editing user:
<strong><?php echo $user_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<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="user_id" value="<?php echo $user_id; ?>">
<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-user-details<?php echo $user_id; ?>">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-user-access<?php echo $user_id; ?>">Restrict Access</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-user-details<?php echo $user_id; ?>">
<center class="mb-3">
<?php if (!empty($user_avatar)) { ?>
<img class="img-fluid" src="<?php echo "../uploads/users/$user_id/$user_avatar"; ?>">
<?php } else { ?>
<span class="fa-stack fa-4x">
<i class="fa fa-circle fa-stack-2x text-secondary"></i>
<span class="fa fa-stack-1x text-white"><?php echo $user_initials; ?></span>
</span>
<?php } ?>
</center>
<div class="form-group">
<label>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-user"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Full Name" maxlength="200"
value="<?php echo $user_name; ?>" required>
</div>
</div>
<div class="form-group">
<label>Email <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-envelope"></i></span>
</div>
<input type="email" class="form-control" name="email" placeholder="Email Address" maxlength="200"
value="<?php echo $user_email; ?>" required>
</div>
</div>
<div class="form-group">
<label>New Password</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
</div>
<input type="password" class="form-control" data-toggle="password" name="new_password" id="password"
placeholder="Leave Blank For No Password Change" autocomplete="new-password">
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</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 class="form-group">
<label>Role <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-user-shield"></i></span>
</div>
<select class="form-control select2" name="role" required>
<?php
$sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_user_roles)) {
$role_id = intval($row['role_id']);
$role_name = nullable_htmlentities($row['role_name']);
?>
<option <?php if ($role_id == $user_role_id) {echo "selected";} ?> value="<?php echo $role_id; ?>"><?php echo $role_name; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label>Avatar</label>
<input type="file" class="form-control-file" accept="image/*" name="file">
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" id="forceMFACheckBox<?php echo $user_id; ?>" name="force_mfa" value="1" <?php if($user_config_force_mfa == 1){ echo "checked"; } ?>>
<label for="forceMFACheckBox<?php echo $user_id; ?>" class="custom-control-label">
Force MFA
</label>
</div>
</div>
<?php if (!empty($user_token)) { ?>
<div class="form-group">
<label>2FA</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-id-card"></i></span>
</div>
<select class="form-control" name="2fa">
<option value="">Keep enabled</option>
<option value="disable">Disable</option>
</select>
</div>
</div>
<?php } ?>
</div>
<div class="tab-pane fade" id="pills-user-access<?php echo $user_id; ?>">
<div class="alert alert-info">
Check boxes to authorize user client access. No boxes grant full client access. Admin users are unaffected.
</div>
<ul class="list-group">
<li class="list-group-item bg-dark">
<div class="form-check">
<input type="checkbox" class="form-check-input" onclick="this.closest('.tab-pane').querySelectorAll('.client-checkbox').forEach(checkbox => checkbox.checked = this.checked);">
<label class="form-check-label ml-3"><strong>Restrict Access to Clients</strong></label>
</div>
</li>
<?php
$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)) {
$client_id_select = intval($row['client_id']);
$client_name_select = nullable_htmlentities($row['client_name']);
?>
<li class="list-group-item">
<div class="form-check">
<input type="checkbox" class="form-check-input client-checkbox" name="clients[]" value="<?php echo $client_id_select; ?>" <?php if (in_array($client_id_select, $client_access_array)) { echo "checked"; } ?>>
<label class="form-check-label ml-2"><?php echo $client_name_select; ?></label>
</div>
</li>
<?php } ?>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_user" 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="fas fa-times mr-2"></i>Cancel</button>
</div>
</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
require_once "../../../includes/modal_footer.php";

View File

@ -0,0 +1,26 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<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>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="submit" name="export_users_csv" class="btn btn-primary text-bold"><i class="fas fa-fw fa-download mr-2"></i>Download CSV</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@ -0,0 +1,49 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body">
<div class="form-group">
<label>Email <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-envelope"></i></span>
</div>
<input type="email" class="form-control" name="email" placeholder="Email Address" maxlength="200" required>
</div>
</div>
<div class="form-group">
<label>Role <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-user-shield"></i></span>
</div>
<select class="form-control select2" name="role" required>
<option value="">- Role -</option>
<!-- //TODO: Pull from roles -->
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="invite_user" class="btn btn-primary text-bold"><i class="fas fa-paper-plane mr-2"></i>Send Invite</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,87 @@
<?php
require_once '../../../includes/modal_header.php';
$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");
$row = mysqli_fetch_array($sql);
$user_name = str_replace(" (archived)", "", $row['user_name']); //Removed (archived) from user_name
$user_name = nullable_htmlentities($user_name);
$user_email = nullable_htmlentities($row['user_email']);
$user_avatar = nullable_htmlentities($row['user_avatar']);
$user_initials = initials($user_name);
$user_role_id = intval($row['user_role_id']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-redo-alt mr-2"></i>Restoring user:
<strong><?php echo $user_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="user_id" value="<?php echo $user_id; ?>">
<div class="modal-body">
<center class="mb-3">
<?php if (!empty($user_avatar)) { ?>
<img class="img-fluid" src="<?php echo "../uploads/users/$user_id/$user_avatar"; ?>">
<?php } else { ?>
<span class="fa-stack fa-4x">
<i class="fa fa-circle fa-stack-2x text-secondary"></i>
<span class="fa fa-stack-1x text-white"><?php echo $user_initials; ?></span>
</span>
<?php } ?>
</center>
<div class="form-group">
<label>Set a New Password</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
</div>
<input type="password" class="form-control" data-toggle="password" name="new_password"
placeholder="Enter a new password" autocomplete="new-password" required>
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
</div>
</div>
<div class="form-group">
<label>Role <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-user-shield"></i></span>
</div>
<select class="form-control select2" name="role" required>
<?php
$sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL");
while ($row = mysqli_fetch_array($sql_user_roles)) {
$role_id = intval($row['role_id']);
$role_name = nullable_htmlentities($row['role_name']);
?>
<option <?php if ($role_id == $user_role_id) {echo "selected";} ?> value="<?php echo $role_id; ?>"><?php echo $role_name; ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="restore_user" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Restore</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once "../../../includes/modal_footer.php";

View File

@ -0,0 +1,171 @@
<?php
require_once '../../../includes/modal_header.php';
ob_start();
?>
<div class="modal-header bg-dark">
<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">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<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-support">Support</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-notes">Notes</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group">
<label>Vendor 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-building"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Vendor Name" maxlength="200" required autofocus>
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Description" maxlength="200">
</div>
</div>
<div class="form-group">
<label>Account Number</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
</div>
<input type="text" class="form-control" name="account_number" placeholder="Account number" maxlength="200">
</div>
</div>
<div class="form-group">
<label>Account Manager</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
</div>
<input type="text" class="form-control" name="contact_name" placeholder="Account manager's name" maxlength="200">
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-support">
<label>Support Phone / <span class="text-secondary">Extension</span></label>
<div class="form-row">
<div class="col-9">
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
</div>
<input type="tel" class="form-control col-2" name="phone_country_code" placeholder="+" maxlength="4">
<input type="tel" class="form-control" name="phone" placeholder="Phone Number" maxlength="200">
</div>
</div>
</div>
<div class="col-3">
<div class="form-group">
<input type="text" class="form-control" name="extension" placeholder="ext." maxlength="200">
</div>
</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="hours" placeholder="Support Hours" maxlength="200">
</div>
</div>
<div class="form-group">
<label>Support Email</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
</div>
<input type="email" class="form-control" name="email" placeholder="Support Email" maxlength="200">
</div>
</div>
<div class="form-group">
<label>Support Website URL</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
</div>
<input type="text" class="form-control" name="website" placeholder="Do not include http(s)://" maxlength="200">
</div>
</div>
<div class="form-group">
<label>Pin/Code</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="code" placeholder="Access Code or Pin" maxlength="200">
</div>
</div>
<div class="form-group">
<label>SLA</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
</div>
<input type="text" class="form-control" name="sla" placeholder="SLA Response Time" maxlength="200">
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-notes">
<div class="form-group">
<textarea class="form-control" rows="8" placeholder="Enter some notes" name="notes"></textarea>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_vendor_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,254 @@
<?php
require_once '../../../includes/modal_header.php';
$vendor_template_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM vendor_templates WHERE vendor_template_id = $vendor_template_id LIMIT 1");
$row = mysqli_fetch_array($sql);
$vendor_name = nullable_htmlentities($row['vendor_template_name']);
$vendor_description = nullable_htmlentities($row['vendor_template_description']);
$vendor_account_number = nullable_htmlentities($row['vendor_template_account_number']);
$vendor_contact_name = nullable_htmlentities($row['vendor_template_contact_name']);
$vendor_phone_country_code = intval($row['vendor_template_phone_country_code']);
$vendor_phone = formatPhoneNumber($row['vendor_template_phone'], $vendor_phone_country_code);
$vendor_extension = nullable_htmlentities($row['vendor_template_extension']);
$vendor_email = nullable_htmlentities($row['vendor_template_email']);
$vendor_website = nullable_htmlentities($row['vendor_template_website']);
$vendor_hours = nullable_htmlentities($row['vendor_template_hours']);
$vendor_sla = nullable_htmlentities($row['vendor_template_sla']);
$vendor_code = nullable_htmlentities($row['vendor_template_code']);
$vendor_notes = nullable_htmlentities($row['vendor_template_notes']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-building mr-2"></i>Editing vendor template: <strong><?php echo $vendor_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="vendor_template_id" value="<?php echo $vendor_template_id; ?>">
<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<?php echo $vendor_template_id; ?>">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-support<?php echo $vendor_template_id; ?>">Support</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-notes<?php echo $vendor_template_id; ?>">Notes</a>
</li>
</ul>
<hr>
<div class="alert alert-info">Check the fields you would like to update globally</div>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-details<?php echo $vendor_template_id; ?>">
<div class="form-group">
<label>Vendor 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-building"></i></span>
</div>
<input type="text" class="form-control" name="name" placeholder="Vendor Name" maxlength="200" value="<?php echo "$vendor_name"; ?>" required>
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_name" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Description" maxlength="200" value="<?php echo $vendor_description; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_description" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Account Number</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
</div>
<input type="text" class="form-control" name="account_number" placeholder="Account number" maxlength="200" value="<?php echo $vendor_account_number; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_account_number" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Account Manager</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
</div>
<input type="text" class="form-control" name="contact_name" maxlength="200" value="<?php echo $vendor_contact_name; ?>" placeholder="Vendor contact name">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_contact_name" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="updateVendorsCheckbox<?php echo $vendor_template_id; ?>" name="update_base_vendors" value="1" >
<label class="custom-control-label" for="updateVendorsCheckbox<?php echo $vendor_template_id; ?>">Update All Base Vendors</label>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-support<?php echo $vendor_template_id; ?>">
<label>Support Phone</label>
<div class="form-row">
<div class="col-8">
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
</div>
<input type="tel" class="form-control col-2" name="phone_country_code" placeholder="+" maxlength="4" value="<?php echo $vendor_phone_country_code; ?>">
<input type="tel" class="form-control" name="phone" value="<?php echo $vendor_phone; ?>">
</div>
</div>
</div>
<div class="col-4">
<div class="input-group">
<input type="text" class="form-control" name="extension" placeholder="Prompts" maxlength="200" value="<?php echo $vendor_extension; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_phone" value="1">
</div>
</div>
</div>
</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="hours" placeholder="Support Hours" maxlength="200" value="<?php echo $vendor_hours; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_hours" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Support Email</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
</div>
<input type="email" class="form-control" name="email" placeholder="Support Email" maxlength="200" value="<?php echo $vendor_email; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_email" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Support Website URL</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
</div>
<input type="text" class="form-control" name="website" placeholder="Do not include http(s)://" maxlength="200" value="<?php echo $vendor_website; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_website" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>SLA</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
</div>
<input type="text" class="form-control" name="sla" placeholder="SLA Response Time" maxlength="200" value="<?php echo $vendor_sla; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_sla" value="1">
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Pin/Code</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="code" placeholder="Access Code or Pin" maxlength="200" value="<?php echo $vendor_code; ?>">
<div class="input-group-append">
<div class="input-group-text">
<input type="checkbox" name="global_update_vendor_code" value="1">
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-notes<?php echo $vendor_template_id; ?>">
<div class="form-group">
<textarea class="form-control" rows="8" placeholder="Enter some notes" name="notes"><?php echo $vendor_notes; ?></textarea>
</div>
<div class="form-group">
<label>Update Notes Globally?</label>
<input type="checkbox" name="global_update_vendor_notes" value="1">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary text-bold" name="edit_vendor_template"><i class="fa fa-check mr-2"></i>Update 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';

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_array($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";

101
admin/payment_method.php Normal file
View File

@ -0,0 +1,101 @@
<?php
// Default Column Sortby Filter
$sort = "payment_method_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM payment_methods ORDER BY $sort $order");
$num_rows = mysqli_num_rows($sql);
?>
<div class="card card-dark">
<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>
<div class="card-tools">
<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 class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=payment_method_name&order=<?php echo $disp; ?>">
Method <?php if ($sort == 'payment_method_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=payment_method_description&order=<?php echo $disp; ?>">
Description <?php if ($sort == 'payment_method_description') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=payment_method_created_at&order=<?php echo $disp; ?>">
Created at <?php if ($sort == 'payment_method_created_at') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$payment_method_id = intval($row['payment_method_id']);
$payment_method_name = nullable_htmlentities($row['payment_method_name']);
$payment_method_description = nullable_htmlentities($row['payment_method_description']);
$payment_method_created_at = nullable_htmlentities($row['payment_method_created_at']);
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/payment_method/payment_method_edit.php?id=<?= $payment_method_id ?>">
<?php echo $payment_method_name; ?>
</a>
</td>
<td><?php echo $payment_method_description; ?></td>
<td><?php echo $payment_method_created_at; ?></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-url="modals/payment_method/payment_method_edit.php?id=<?= $payment_method_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_payment_method=<?php echo $payment_method_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

142
admin/payment_provider.php Normal file
View File

@ -0,0 +1,142 @@
<?php
// Default Column Sortby Filter
$sort = "payment_provider_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers
LEFT JOIN accounts ON payment_provider_account = account_id
LEFT JOIN vendors ON payment_provider_expense_vendor = vendor_id
LEFT JOIN categories ON payment_provider_expense_category = category_id
ORDER BY $sort $order"
);
$num_rows = mysqli_num_rows($sql);
?>
<div class="card card-dark">
<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>
<div class="card-tools">
<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 class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=payment_provider_name&order=<?php echo $disp; ?>">
Provider <?php if ($sort == 'payment_provider_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=account_name&order=<?php echo $disp; ?>">
Expense / Income Account <?php if ($sort == 'account_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=payment_provider_threshold&order=<?php echo $disp; ?>">
Threshold <?php if ($sort == 'payment_provider_threshold') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=vendor_name&order=<?php echo $disp; ?>">
Expense Vendor <?php if ($sort == 'vendor_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=category_name&order=<?php echo $disp; ?>">
Expense Category <?php if ($sort == 'category_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark">Expensed Fee</a>
</th>
<th class="text-center">
<a class="text-dark">Saved Payment Methods</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$provider_id = intval($row['payment_provider_id']);
$provider_name = nullable_htmlentities($row['payment_provider_name']);
$provider_description = nullable_htmlentities($row['payment_provider_description']);
$account_name = nullable_htmlentities($row['account_name']);
$threshold = floatval($row['payment_provider_threshold']);
$vendor_name = nullable_htmlentities($row['vendor_name'] ?? "Expense Disabled");
$category = nullable_htmlentities($row['category_name']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('saved_payment_id') AS saved_payment_count FROM client_saved_payment_methods WHERE saved_payment_provider_id = $provider_id"));
$saved_payment_count = intval($row['saved_payment_count']);
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/payment_provider/payment_provider_edit.php?id=<?= $provider_id ?>">
<?php echo $provider_name; ?>
</a>
<span class="text-secondary"><?php echo $provider_description; ?></span>
</td>
<td><?php echo $account_name; ?></td>
<td><?php echo numfmt_format_currency($currency_format, $threshold, $session_company_currency); ?></td>
<td><?php echo $vendor_name; ?></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 class="text-center">
<a class="badge badge-dark badge-pill p-2" href="saved_payment_method.php"><?= $saved_payment_count ?></a>
</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-url="modals/payment_provider/payment_provider_edit.php?id=<?= $provider_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<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-trash mr-2"></i><strong>Delete Provider and</strong>
<ul class="text-xs">
<li>Related Recurring Payments</li>
<li>Related Saved cards</li>
<li>Client Provider Relations</li>
</ul>
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

39
admin/post.php Normal file
View File

@ -0,0 +1,39 @@
<?php
/*
* ITFlow - Admin GET/POST request handler
*/
require_once "../config.php";
require_once "../functions.php";
require_once "../includes/check_login.php";
// Define a variable that we can use to only allow running post files via inclusion (prevents people/bots poking them)
define('FROM_POST_HANDLER', true);
// Determine which files we should load
// Parse URL & get the path
$path = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_PATH);
// Get the base name (the page name)
$module = explode(".", basename($path))[0];
// Strip off any _details bits
$module = str_ireplace('_details', '', $module);
// Dynamically load admin-related module POST logic
if (isset($session_is_admin) && $session_is_admin) {
// As (almost) every admin setting is only changed from 1 page, we can dynamically load the relevant logic inside this single admin check IF statement
// To add a new admin POST request handler, add a file named after the admin page
// e.g. changes made on the page http://itflow/admin_ticket_statues.php will load the page admin/post/admin_ticket_statues.php to handle the changes
include_once "post/$module.php";
}
// Logout is the same for user and admin
require_once "../post/logout.php";
// TODO: Find a home for these
require_once "../post/misc.php";

65
admin/post/ai_model.php Normal file
View File

@ -0,0 +1,65 @@
<?php
/*
* ITFlow - GET/POST request handler for AI Models ('ai_model')
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_ai_model'])) {
validateCSRFToken($_POST['csrf_token']);
$provider_id = intval($_POST['provider']);
$model = sanitizeInput($_POST['model']);
$prompt = sanitizeInput($_POST['prompt']);
$use_case = sanitizeInput($_POST['use_case']);
mysqli_query($mysqli,"INSERT INTO ai_models SET ai_model_name = '$model', ai_model_prompt = '$prompt', ai_model_use_case = '$use_case', ai_model_ai_provider_id = $provider_id");
$ai_model_id = mysqli_insert_id($mysqli);
logAction("AI Model", "Create", "$session_name created AI Model $model");
flash_alert("AI Model <strong>$model</strong> created");
redirect();
}
if (isset($_POST['edit_ai_model'])) {
validateCSRFToken($_POST['csrf_token']);
$model_id = intval($_POST['model_id']);
$model = sanitizeInput($_POST['model']);
$prompt = sanitizeInput($_POST['prompt']);
$use_case = sanitizeInput($_POST['use_case']);
mysqli_query($mysqli,"UPDATE ai_models SET ai_model_name = '$model', ai_model_prompt = '$prompt', ai_model_use_case = '$use_case' WHERE ai_model_id = $model_id");
logAction("AI Model", "Edit", "$session_name edited AI Model $model");
flash_alert("AI Model <strong>$model</strong> edited");
redirect();
}
if (isset($_GET['delete_ai_model'])) {
validateCSRFToken($_GET['csrf_token']);
$model_id = intval($_GET['delete_ai_model']);
$model_name = sanitizeInput(getFieldById('ai_models', $model_id, 'ai_model_name'));
mysqli_query($mysqli,"DELETE FROM ai_models WHERE ai_model_id = $model_id");
logAction("AI Model", "Delete", "$session_name deleted AI Model $model_name");
flash_alert("AI Model <strong>$model_name</strong> deleted", 'error');
redirect();
}

View File

@ -0,0 +1,65 @@
<?php
/*
* ITFlow - GET/POST request handler for AI Providers ('ai_provider')
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_ai_provider'])) {
validateCSRFToken($_POST['csrf_token']);
$provider = sanitizeInput($_POST['provider']);
$url = sanitizeInput($_POST['url']);
$model = sanitizeInput($_POST['model']);
$api_key = sanitizeInput($_POST['api_key']);
mysqli_query($mysqli,"INSERT INTO ai_providers SET ai_provider_name = '$provider', ai_provider_api_url = '$url', ai_provider_api_key = '$api_key'");
$ai_provider_id = mysqli_insert_id($mysqli);
logAction("AI Provider", "Create", "$session_name created AI Provider $provider");
flash_alert("AI Model <strong>$provider</strong> created");
redirect();
}
if (isset($_POST['edit_ai_provider'])) {
validateCSRFToken($_POST['csrf_token']);
$provider_id = intval($_POST['provider_id']);
$provider = sanitizeInput($_POST['provider']);
$url = sanitizeInput($_POST['url']);
$api_key = sanitizeInput($_POST['api_key']);
mysqli_query($mysqli,"UPDATE ai_providers SET ai_provider_name = '$provider', ai_provider_api_url = '$url', ai_provider_api_key = '$api_key' WHERE ai_provider_id = $provider_id");
logAction("AI Provider", "Edit", "$session_name edited AI Provider $provider");
flash_alert("AI Model <strong>$provider</strong> edited");
redirect();
}
if (isset($_GET['delete_ai_provider'])) {
validateCSRFToken($_GET['csrf_token']);
$provider_id = intval($_GET['delete_ai_provider']);
$provider_name = sanitizeInput(getFieldById('ai_providers', $provider_id, 'ai_provider_name'));
mysqli_query($mysqli,"DELETE FROM ai_providers WHERE ai_provider_id = $provider_id");
logAction("AI Provider", "Delete", "$session_name deleted AI Provider $provider_name", 'error');
flash_alert("AI Provider <strong>$provider_name</strong> deleted", 'error');
redirect();
}

87
admin/post/api_keys.php Normal file
View File

@ -0,0 +1,87 @@
<?php
/*
* ITFlow - GET/POST request handler for API settings
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_api_key'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']);
$expire = sanitizeInput($_POST['expire']);
$client_id = intval($_POST['client']);
$secret = sanitizeInput($_POST['key']); // API Key
// Credential decryption password
$password = password_hash(trim($_POST['password']), PASSWORD_DEFAULT);
$apikey_specific_encryption_ciphertext = encryptUserSpecificKey(trim($_POST['password']));
mysqli_query($mysqli,"INSERT INTO api_keys SET api_key_name = '$name', api_key_secret = '$secret', api_key_decrypt_hash = '$apikey_specific_encryption_ciphertext', api_key_expire = '$expire', api_key_client_id = $client_id");
$api_key_id = mysqli_insert_id($mysqli);
logAction("API Key", "Create", "$session_name created API key $name set to expire on $expire", $client_id, $api_key_id);
flash_alert("API Key <strong>$name</strong> created");
redirect();
}
if (isset($_GET['delete_api_key'])) {
validateCSRFToken($_GET['csrf_token']);
$api_key_id = intval($_GET['delete_api_key']);
// 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"));
$api_key_name = sanitizeInput($row['api_key_name']);
$client_id = intval($row['api_key_client_id']);
mysqli_query($mysqli,"DELETE FROM api_keys WHERE api_key_id = $api_key_id");
logAction("API Key", "Delete", "$session_name deleted API key $name", $client_id);
flash_alert("API Key <strong>$name</strong> deleted", 'error');
redirect();
}
if (isset($_POST['bulk_delete_api_keys'])) {
validateCSRFToken($_POST['csrf_token']);
if (isset($_POST['api_key_ids'])) {
$count = count($_POST['api_key_ids']);
// Cycle through array and delete each record
foreach ($_POST['api_key_ids'] as $api_key_id) {
$api_key_id = intval($api_key_id);
// 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"));
$api_key_name = sanitizeInput($row['api_key_name']);
$client_id = intval($row['api_key_client_id']);
mysqli_query($mysqli, "DELETE FROM api_keys WHERE api_key_id = $api_key_id");
logAction("API Key", "Delete", "$session_name deleted API key $name", $client_id);
}
logAction("API Key", "Bulk Delete", "$session_name deleted $count API key(s)");
flash_alert("Deleted <strong>$count</strong> API keys(s)", 'error');
}
redirect();
}

332
admin/post/backup.php Normal file
View File

@ -0,0 +1,332 @@
<?php
/*
* 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");
require_once "../includes/app_version.php";
// --- Optional performance levers for big backups ---
@set_time_limit(0);
if (function_exists('ini_set')) {
@ini_set('memory_limit', '1024M');
}
/**
* Write a line to a file handle with newline.
*/
function fwrite_ln($fh, string $s): void {
fwrite($fh, $s);
fwrite($fh, PHP_EOL);
}
/**
* 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");
}
// Preamble
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, "");
// Gather tables and views
$tables = [];
$views = [];
$res = $mysqli->query("SHOW FULL TABLES");
if (!$res) {
fclose($fh);
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();
// --- TABLES: structure and data ---
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();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
error_log("Failed to open zip file: $zipFilePath");
http_response_code(500);
exit("Internal Server Error: Cannot open zip archive.");
}
$folderReal = realpath($folderPath);
if (!$folderReal || !is_dir($folderReal)) {
// Create an empty archive if uploads folder doesn't exist yet
$zip->close();
return;
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($folderReal, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
/** @var SplFileInfo $file */
if ($file->isDir()) continue;
if ($file->isLink()) continue; // skip symlinks
$filePath = $file->getRealPath();
if ($filePath === false) continue;
// 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();
}
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_");
$uploadsZip = tempnam(sys_get_temp_dir(), $baseName . "_uploads_");
$versionFile = tempnam(sys_get_temp_dir(), $baseName . "_version_");
$finalZip = tempnam(sys_get_temp_dir(), $baseName . "_backup_");
foreach ([$sqlFile, $uploadsZip, $versionFile, $finalZip] as $f) {
$registerTempFileForCleanup($f);
@chmod($f, 0600);
}
// === Generate SQL Dump (streaming) ===
dump_database_streaming($mysqli, $sqlFile);
// === Zip the uploads folder (strict) ===
zipFolderStrict("../uploads", $uploadsZip);
// === Gather metadata & checksums ===
$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';
$dbSha = hash_file('sha256', $sqlFile) ?: 'N/A';
$upSha = hash_file('sha256', $uploadsZip) ?: 'N/A';
$versionContent = "ITFlow Backup Metadata\n";
$versionContent .= "-----------------------------\n";
$versionContent .= "Generated: " . date('Y-m-d H:i:s') . "\n";
$versionContent .= "Backup File: " . $downloadName . "\n";
$versionContent .= "Generated By: " . ($session_name ?? 'Unknown User') . "\n";
$versionContent .= "Host: " . gethostname() . "\n";
$versionContent .= "Git Branch: $gitBranch\n";
$versionContent .= "Git Commit: $commitHash\n";
$versionContent .= "ITFlow Version: " . (defined('APP_VERSION') ? APP_VERSION : 'Unknown') . "\n";
$versionContent .= "Database Version: " . (defined('CURRENT_DATABASE_VERSION') ? CURRENT_DATABASE_VERSION : 'Unknown') . "\n";
$versionContent .= "Checksums (SHA256):\n";
$versionContent .= " db.sql: $dbSha\n";
$versionContent .= " uploads.zip: $upSha\n";
file_put_contents($versionFile, $versionContent);
@chmod($versionFile, 0600);
// === Build final ZIP ===
$final = new ZipArchive();
if ($final->open($finalZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
error_log("Failed to create final zip: $finalZip");
http_response_code(500);
exit("Internal Server Error: Unable to create backup archive.");
}
$final->addFile($sqlFile, "db.sql");
$final->addFile($uploadsZip, "uploads.zip");
$final->addFile($versionFile, "version.txt");
$final->close();
@chmod($finalZip, 0600);
// === Serve final ZIP with a stable filename ===
header('Content-Type: application/zip');
header('X-Content-Type-Options: nosniff');
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
header('Content-Length: ' . filesize($finalZip));
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Transfer-Encoding: binary');
// Push file
flush();
$fp = fopen($finalZip, 'rb');
fpassthru($fp);
fclose($fp);
// Log + UX
logAction("System", "Backup Download", ($session_name ?? 'Unknown User') . " downloaded full backup.");
flash_alert("Full backup downloaded.");
exit;
}
if (isset($_POST['backup_master_key'])) {
validateCSRFToken($_POST['csrf_token']);
$password = $_POST['password'];
$sql = mysqli_query($mysqli, "SELECT * FROM users WHERE user_id = $session_user_id");
$row = mysqli_fetch_array($sql);
if (password_verify($password, $row['user_password'])) {
$site_encryption_master_key = decryptUserSpecificKey($row['user_specific_encryption_ciphertext'], $password);
logAction("Master Key", "Download", "$session_name retrieved the master encryption key");
appNotify("Master Key", "$session_name retrieved the master encryption key");
echo "==============================";
echo "<br>Master encryption key:<br>";
echo "<b>$site_encryption_master_key</b>";
echo "<br>==============================";
} else {
logAction("Master Key", "Download", "$session_name attempted to retrieve the master encryption key but failed");
flash_alert("Incorrect password.", 'error');
redirect();
}
}

99
admin/post/category.php Normal file
View File

@ -0,0 +1,99 @@
<?php
/*
* ITFlow - GET/POST request handler for categories ('category')
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_category'])) {
require_once 'category_model.php';
mysqli_query($mysqli,"INSERT INTO categories SET category_name = '$name', category_type = '$type', category_color = '$color'");
$category_id = mysqli_insert_id($mysqli);
logAction("Category", "Create", "$session_name created category $type $name", 0, $category_id);
flash_alert("Category $type <strong>$name</strong> created");
redirect();
}
if (isset($_POST['edit_category'])) {
require_once 'category_model.php';
$category_id = intval($_POST['category_id']);
mysqli_query($mysqli,"UPDATE categories SET category_name = '$name', category_type = '$type', category_color = '$color' WHERE category_id = $category_id");
logAction("Category", "Edit", "$session_name edited category $type $name", 0, $category_id);
flash_alert("Category $type <strong>$name</strong> edited");
redirect();
}
if (isset($_GET['archive_category'])) {
$category_id = intval($_GET['archive_category']);
// Get Category Name and Type for logging
$sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id");
$row = mysqli_fetch_array($sql);
$category_name = sanitizeInput($row['category_name']);
$category_type = sanitizeInput($row['category_type']);
mysqli_query($mysqli,"UPDATE categories SET category_archived_at = NOW() WHERE category_id = $category_id");
logAction("Category", "Archive", "$session_name archived category $category_type $category_name", 0, $category_id);
flash_alert("Category $category_type <strong>$category_name</strong> archived", 'error');
redirect();
}
if (isset($_GET['unarchive_category'])) {
$category_id = intval($_GET['unarchive_category']);
// Get Category Name and Type for logging
$sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id");
$row = mysqli_fetch_array($sql);
$category_name = sanitizeInput($row['category_name']);
$category_type = sanitizeInput($row['category_type']);
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);
flash_alert("Category $category_type <strong>$category_name</strong> unarchived");
redirect();
}
if (isset($_GET['delete_category'])) {
$category_id = intval($_GET['delete_category']);
// Get Category Name and Type for logging
$sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id");
$row = mysqli_fetch_array($sql);
$category_name = sanitizeInput($row['category_name']);
$category_type = sanitizeInput($row['category_type']);
mysqli_query($mysqli,"DELETE FROM categories WHERE category_id = $category_id");
logAction("Category", "Delete", "$session_name deleted category $category_type $category_name");
flash_alert("Category $category_type <strong>$category_name</strong> deleted", 'error');
redirect();
}

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

@ -0,0 +1,57 @@
<?php
/*
* ITFlow - GET/POST request handler for custom fields
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if(isset($_POST['create_custom_field'])){
require_once 'custom_field_model.php';
$table = sanitizeInput($_POST['table']);
mysqli_query($mysqli,"INSERT INTO custom_fields SET custom_field_table = '$table', custom_field_label = '$label', custom_field_type = '$type'");
$custom_field_id = mysqli_insert_id($mysqli);
logAction("Custom Field", "Create", "$session_name created custom field $label", 0, $custom_field_id);
flash_alert("Custom field <strong>$label</strong> created");
redirect();
}
if(isset($_POST['edit_custom_field'])){
require_once 'custom_field_model.php';
$custom_field_id = intval($_POST['custom_field_id']);
mysqli_query($mysqli,"UPDATE custom_fields SET custom_field_label = '$label', custom_field_type = '$type' WHERE custom_field_id = $custom_field_id");
logAction("Custom Field", "Edit", "$session_name edited custom field $label", 0, $custom_field_id);
flash_alert("Custom field <strong>$label</strong> edited");
redirect();
}
if(isset($_GET['delete_custom_field'])){
$custom_field_id = intval($_GET['delete_custom_field']);
$label = sanitizeInput(getFieldById('custom_fields', $custom_field_id, 'custom_field_label'));
mysqli_query($mysqli,"DELETE FROM custom_fields WHERE custom_field_id = $custom_field_id");
logAction("Custom Field", "Delete", "$session_name deleted custom field $label");
flash_alert("Custom field <strong>$label</strong> deleted", 'error');
redirect();
}

View File

@ -0,0 +1,68 @@
<?php
/*
* ITFlow - GET/POST request handler for showing custom links on navbars
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_custom_link'])) {
$name = sanitizeInput($_POST['name']);
$uri = sanitizeInput($_POST['uri']);
$new_tab = intval($_POST['new_tab'] ?? 0);
$icon = preg_replace("/[^0-9a-zA-Z-]/", "", sanitizeInput($_POST['icon']));
$order = intval($_POST['order'] ?? 0);
$location = intval($_POST['location']);
mysqli_query($mysqli,"INSERT INTO custom_links SET custom_link_name = '$name', custom_link_uri = '$uri', custom_link_new_tab = $new_tab, custom_link_icon = '$icon', custom_link_order = $order, custom_link_location = $location");
$custom_link_id = mysqli_insert_id($mysqli);
logAction("Custom Link", "Create", "$session_name created custom link $name -> $uri", 0, $custom_link_id);
flash_alert("Custom link <strong>$name</strong> created");
redirect();
}
if (isset($_POST['edit_custom_link'])) {
$custom_link_id = intval($_POST['custom_link_id']);
$name = sanitizeInput($_POST['name']);
$uri = sanitizeInput($_POST['uri']);
$new_tab = intval($_POST['new_tab'] ?? 0);
$icon = preg_replace("/[^0-9a-zA-Z-]/", "", sanitizeInput($_POST['icon']));
$order = intval($_POST['order'] ?? 0);
$location = intval($_POST['location']);
mysqli_query($mysqli,"UPDATE custom_links SET custom_link_name = '$name', custom_link_uri = '$uri', custom_link_new_tab = $new_tab, custom_link_icon = '$icon', custom_link_order = $order, custom_link_location = $location WHERE custom_link_id = $custom_link_id");
logAction("Custom Link", "Edit", "$session_name edited custom link $name -> $uri", 0, $custom_link_id);
flash_alert("Custom Link <strong>$name</strong> edited");
redirect();
}
if (isset($_GET['delete_custom_link'])) {
$custom_link_id = intval($_GET['delete_custom_link']);
// 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");
$row = mysqli_fetch_array($sql);
$custom_link_name = sanitizeInput($row['custom_link_name']);
$custom_link_uri = sanitizeInput($row['custom_link_uri']);
mysqli_query($mysqli,"DELETE FROM custom_links WHERE custom_link_id = $custom_link_id");
logAction("Custom Link", "Delete", "$session_name deleted custom link $custom_link_name -> $custom_link_uri");
flash_alert("Custom Link <strong>$name</strong> deleted", 'error');
redirect();
}

View File

@ -0,0 +1,87 @@
<?php
// Doc Templates
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_document_template'])) {
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
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);
$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);
flash_alert("Document template <strong>$name</strong> created");
redirect();
}
if (isset($_POST['edit_document_template'])) {
$document_template_id = intval($_POST['document_template_id']);
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
$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
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);
flash_alert("Document Template <strong>$name</strong> edited");
redirect();
}
if (isset($_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'));
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");
flash_alert("Document Template <strong>$document_template_name</strong> deleted", 'error');
redirect();
}

View File

@ -0,0 +1,20 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_identity_provider'])) {
validateCSRFToken($_POST['csrf_token']);
$azure_client_id = sanitizeInput($_POST['azure_client_id']);
$azure_client_secret = sanitizeInput($_POST['azure_client_secret']);
mysqli_query($mysqli,"UPDATE settings SET config_azure_client_id = '$azure_client_id', config_azure_client_secret = '$azure_client_secret' WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited identity provider settings");
flash_alert("Identity Provider Settings updated");
redirect();
}

87
admin/post/mail_queue.php Normal file
View File

@ -0,0 +1,87 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_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");
logAction("Email", "Send", "$session_name attempted to force send email id: $email_id in the mail queue", 0, $email_id);
flash_alert("Email Force Sent, give it a minute to resend");
redirect();
}
if (isset($_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");
logAction("Email", "Send", "$session_name canceled send email id: $email_id in the mail queue", 0, $email_id);
flash_alert("Email cancelled and marked as failed.", 'error');
redirect();
}
if (isset($_POST['bulk_cancel_emails'])) {
validateCSRFToken($_POST['csrf_token']);
if (isset($_POST['email_ids'])) {
$count = count($_POST['email_ids']);
// Cycle through array and mark each email as failed
foreach ($_POST['email_ids'] as $email_id) {
$email_id = intval($email_id);
mysqli_query($mysqli,"UPDATE email_queue SET email_status = 2, email_attempts = 99, email_failed_at = NOW() WHERE email_id = $email_id");
logAction("Email", "Cancel", "$session_name cancelled email id: $email_id in the mail queue", 0, $email_id);
}
logAction("Email", "Bulk Cancel", "$session_name cancelled $count email(s) in the mail queue");
flash_alert("Cancelled <strong>$count</strong> email(s)", 'error');
}
redirect();
}
if (isset($_POST['bulk_delete_emails'])) {
validateCSRFToken($_POST['csrf_token']);
if (isset($_POST['email_ids'])) {
$count = count($_POST['email_ids']);
// Cycle through array and delete each email
foreach ($_POST['email_ids'] as $email_id) {
$email_id = intval($email_id);
mysqli_query($mysqli,"DELETE FROM email_queue WHERE email_id = $email_id");
logAction("Email", "Delete", "$session_name deleted email id: $email_id from the mail queue");
}
logAction("Email", "Bulk Delete", "$session_name deleted $count email(s) from the mail queue");
flash_alert("Deleted <strong>$count</strong> email(s)", 'error');
}
redirect();
}

View File

@ -0,0 +1,74 @@
<?php
/*
* ITFlow - GET/POST request handler for AI Providers ('ai_providers')
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_payment_method'])) {
validateCSRFToken($_POST['csrf_token']);
$name = cleanInput($_POST['name']);
$description = cleanInput($_POST['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");
flash_alert("Payment Method <strong>$name</strong> created");
redirect();
}
if (isset($_POST['edit_payment_method'])) {
validateCSRFToken($_POST['csrf_token']);
$payment_method_id = intval($_POST['payment_method_id']);
$name = cleanInput($_POST['name']);
$description = cleanInput($_POST['description']);
$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");
flash_alert("Payment Method <strong>$name</strong> edited");
redirect();
}
if (isset($_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'));
mysqli_query($mysqli,"DELETE FROM payment_methods WHERE payment_method_id = $payment_method_id");
logAction("Payment Method", "Delete", "$session_name deleted Payment Method $payment_method_name");
flash_alert("Payment Method <strong>$payment_method_name</strong> deleted", 'error');
redirect();
}

View File

@ -0,0 +1,89 @@
<?php
/*
* ITFlow - GET/POST request handler for AI Providers ('ai_providers')
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_payment_provider'])) {
validateCSRFToken($_POST['csrf_token']);
$provider = sanitizeInput($_POST['provider']);
$public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']);
$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;
$flat_fee = floatval($_POST['flat_fee']) ?? 0;
// Check to ensure provider isn't added twice
$sql = mysqli_query($mysqli, "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1");
if (mysqli_num_rows($sql) > 0) {
flash_alert("Payment Provider <strong>$provider</strong> already exists", 'error');
redirect();
}
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");
$provider_id = mysqli_insert_id($mysqli);
logAction("Payment Provider", "Create", "$session_name created AI Provider $provider");
flash_alert("Payment provider <strong>$provider</strong> created");
redirect();
}
if (isset($_POST['edit_payment_provider'])) {
validateCSRFToken($_POST['csrf_token']);
$provider_id = intval($_POST['provider_id']);
$description = sanitizeInput($_POST['description']);
$public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']);
$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;
$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_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");
flash_alert("Payment Provider <strong>$provider</strong> edited");
redirect();
}
if (isset($_GET['delete_payment_provider'])) {
validateCSRFToken($_GET['csrf_token']);
$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'));
// Delete provider
mysqli_query($mysqli,"DELETE FROM payment_providers WHERE payment_provider_id = $provider_id");
logAction("Payment Provider", "Delete", "$session_name deleted Payment Provider $provider_name");
flash_alert("Payment Provider <strong>$provider_name</strong> deleted", 'error');
redirect();
}

View File

@ -0,0 +1,99 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_project_template'])) {
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
mysqli_query($mysqli, "INSERT INTO project_templates SET project_template_name = '$name', project_template_description = '$description'");
$project_template_id = mysqli_insert_id($mysqli);
logAction("Project Template", "Create", "$session_name created project template $name", 0, $project_template_id);
flash_alert("Project Template <strong>$name</strong> created");
redirect();
}
if (isset($_POST['edit_project_template'])) {
$project_template_id = intval($_POST['project_template_id']);
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
mysqli_query($mysqli, "UPDATE project_templates SET project_template_name = '$name', project_template_description = '$description' WHERE project_template_id = $project_template_id");
logAction("Project Template", "Edit", "$session_name edited project template $name", 0, $project_template_id);
flash_alert("Project Template <strong>$name</strong> edited");
redirect();
}
if (isset($_POST['edit_ticket_template_order'])) {
$ticket_template_id = intval($_POST['ticket_template_id']);
$project_template_id = intval($_POST['project_template_id']);
$order = intval($_POST['order']);
mysqli_query($mysqli, "UPDATE project_template_ticket_templates SET ticket_template_order = $order WHERE ticket_template_id = $ticket_template_id AND project_template_id = $project_template_id");
redirect();
}
if (isset($_POST['add_ticket_template_to_project_template'])) {
$project_template_id = intval($_POST['project_template_id']);
$ticket_template_id = intval($_POST['ticket_template_id']);
$order = intval($_POST['order']);
mysqli_query($mysqli, "INSERT INTO project_template_ticket_templates SET project_template_id = $project_template_id, ticket_template_id = $ticket_template_id, ticket_template_order = $order");
logAction("Project Template", "Edit", "$session_name added ticket template to project_template", 0, $project_template_id);
flash_alert("Ticket template added");
redirect();
}
if (isset($_POST['remove_ticket_template_from_project_template'])) {
validateTechRole();
$ticket_template_id = intval($_POST['ticket_template_id']);
$project_template_id = intval($_POST['project_template_id']);
mysqli_query($mysqli, "DELETE FROM project_template_ticket_templates WHERE project_template_id = $project_template_id AND ticket_template_id = $ticket_template_id");
logAction("Project Template", "Edit", "$session_name removed ticket template from project template", 0, $project_template_id);
flash_alert("Ticket template removed", 'error');
redirect();
}
if (isset($_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'));
mysqli_query($mysqli, "DELETE FROM project_templates WHERE project_template_id = $project_template_id");
// Remove Associated Ticket Templates
mysqli_query($mysqli, "DELETE FROM project_template_ticket_templates WHERE project_template_id = $project_template_id");
logAction("Project Template", "Delete", "$session_name deleted project template $project_template_name and its associated ticket templates and tasks");
flash_alert("Project Template <strong>$project_template_name</strong> and its associated ticket templates and tasks deleted", 'error');
redirect();
}

87
admin/post/roles.php Normal file
View File

@ -0,0 +1,87 @@
<?php
/*
* ITFlow - GET/POST request handler for roles
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_role'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['role_name']);
$description = sanitizeInput($_POST['role_description']);
$admin = intval($_POST['role_is_admin']);
mysqli_query($mysqli, "INSERT INTO user_roles SET role_name = '$name', role_description = '$description', role_is_admin = $admin");
$role_id = mysqli_insert_id($mysqli);
logAction("User Role", "Create", "$session_name created user role $name", 0, $role_id);
flash_alert("User Role <strong$name</strong> created");
redirect();
}
if (isset($_POST['edit_role'])) {
validateCSRFToken($_POST['csrf_token']);
$role_id = intval($_POST['role_id']);
$name = sanitizeInput($_POST['role_name']);
$description = sanitizeInput($_POST['role_description']);
$admin = intval($_POST['role_is_admin']);
mysqli_query($mysqli, "UPDATE user_roles SET role_name = '$name', role_description = '$description', role_is_admin = $admin WHERE role_id = $role_id");
// Update role access levels
mysqli_query($mysqli, "DELETE FROM user_role_permissions WHERE user_role_id = $role_id");
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", "Edit", "$session_name edited user role $name", 0, $role_id);
flash_alert("User Role <strong>$name</strong> edited");
redirect();
}
if (isset($_GET['archive_role'])) {
validateCSRFToken($_GET['csrf_token']);
$role_id = intval($_GET['archive_role']);
// Check role isn't in use
$sql_role_user_count = mysqli_query($mysqli, "SELECT COUNT(user_id) FROM users WHERE user_role_id = $role_id AND user_archived_at IS NULL");
$role_user_count = mysqli_fetch_row($sql_role_user_count)[0];
if ($role_user_count != 0) {
flash_alert("Role must not in use to archive it", 'error');
redirect();
}
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'));
logAction("User Role", "Archive", "$session_name archived user role $role_name", 0, $role_id);
flash_alert("User Role <strong>$role_name</strong> archived", 'error');
redirect();
}

View File

@ -0,0 +1,70 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_GET['delete_saved_payment'])) {
validateCSRFToken($_GET['csrf_token']);
$saved_payment_id = intval($_GET['delete_saved_payment']);
$sql = mysqli_query($mysqli, "
SELECT
client_saved_payment_methods.saved_payment_id,
client_saved_payment_methods.saved_payment_client_id,
client_saved_payment_methods.saved_payment_provider_id,
client_saved_payment_methods.saved_payment_provider_method,
client_saved_payment_methods.saved_payment_description,
client_payment_provider.payment_provider_client,
payment_providers.payment_provider_name,
payment_providers.payment_provider_private_key
FROM client_saved_payment_methods
LEFT JOIN client_payment_provider
ON client_payment_provider.client_id = client_saved_payment_methods.saved_payment_client_id
AND client_payment_provider.payment_provider_id = client_saved_payment_methods.saved_payment_provider_id
LEFT JOIN payment_providers
ON payment_providers.payment_provider_id = client_saved_payment_methods.saved_payment_provider_id
WHERE client_saved_payment_methods.saved_payment_id = $saved_payment_id"
);
$row = mysqli_fetch_array($sql);
$client_id = intval($row['saved_payment_client_id']);
$provider_id = intval($row['saved_payment_provider_id']);
$payment_provider_name = nullable_htmlentities($row['payment_provider_name']);
$saved_payment_description = nullable_htmlentities($row['saved_payment_description']);
$provider_client = nullable_htmlentities($row['payment_provider_client']);
$payment_method = $row['saved_payment_provider_method'];
$private_key = $row['payment_provider_private_key'];
// Separate logic for each Payment Provider
if ($payment_provider_name == 'Stripe') {
try {
// Initialize stripe
require_once '../plugins/stripe-php/init.php';
$stripe = new \Stripe\StripeClient($private_key);
// Detach PM
$stripe->paymentMethods->detach($payment_method, []);
} catch (Exception $e) {
$error = $e->getMessage();
error_log("Stripe payment error - encountered exception when removing payment method info for $payment_method: $error");
logApp("Stripe", "error", "Exception removing payment method for $payment_method: $error");
}
}
// 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");
// SQL Cascade delete will Remove All Associated Auto Payment Methods on recurring invoices in the recurring payments table.
logAction("Payment Provider", "Update", "$session_name deleted saved payment method $saved_payment_description (PM: $payment_method)", $client_id);
flash_alert("Payment method <strong>$saved_payment_description</strong> removed", 'error');
redirect();
}

View File

@ -0,0 +1,27 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_ai_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$provider = sanitizeInput($_POST['provider']);
if($provider){
$ai_enable = 1;
} else {
$ai_enable = 0;
}
$model = sanitizeInput($_POST['model']);
$url = sanitizeInput($_POST['url']);
$api_key = sanitizeInput($_POST['api_key']);
mysqli_query($mysqli,"UPDATE settings SET config_ai_enable = $ai_enable, config_ai_provider = '$provider', config_ai_model = '$model', config_ai_url = '$url', config_ai_api_key = '$api_key' WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited AI settings");
flash_alert("AI Settings updated");
redirect();
}

View File

@ -0,0 +1,71 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_company'])) {
validateCSRFToken($_POST['csrf_token']);
$name = sanitizeInput($_POST['name']);
$address = sanitizeInput($_POST['address']);
$city = sanitizeInput($_POST['city']);
$state = sanitizeInput($_POST['state']);
$zip = sanitizeInput($_POST['zip']);
$country = sanitizeInput($_POST['country']);
$phone_country_code = preg_replace("/[^0-9]/", '',$_POST['phone_country_code']);
$phone = preg_replace("/[^0-9]/", '',$_POST['phone']);
$email = sanitizeInput($_POST['email']);
$website = sanitizeInput($_POST['website']);
$tax_id = sanitizeInput($_POST['tax_id']);
$sql = mysqli_query($mysqli,"SELECT company_logo FROM companies WHERE company_id = 1");
$row = mysqli_fetch_array($sql);
$existing_file_name = sanitizeInput($row['company_logo']);
// Company logo
if (isset($_FILES['file']['tmp_name'])) {
if ($new_file_name = checkFileUpload($_FILES['file'], array('jpg', 'jpeg', 'png'))) {
$file_tmp_path = $_FILES['file']['tmp_name'];
// directory in which the uploaded file will be moved
$upload_file_dir = "../uploads/settings/";
$dest_path = $upload_file_dir . $new_file_name;
move_uploaded_file($file_tmp_path, $dest_path);
// Delete old file
unlink("../uploads/settings/$existing_file_name");
// Set Logo
mysqli_query($mysqli,"UPDATE companies SET company_logo = '$new_file_name' WHERE company_id = 1");
}
}
mysqli_query($mysqli,"UPDATE companies SET company_name = '$name', company_address = '$address', company_city = '$city', company_state = '$state', company_zip = '$zip', company_country = '$country', company_phone_country_code = '$phone_country_code', company_phone = '$phone', company_email = '$email', company_website = '$website', company_tax_id = '$tax_id' WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited company details");
flash_alert("Company <strong>$name</strong> edited");
redirect();
}
if (isset($_GET['remove_company_logo'])) {
$sql = mysqli_query($mysqli,"SELECT company_logo FROM companies");
$row = mysqli_fetch_array($sql);
$company_logo = $row['company_logo']; // FileSystem Operation Logo is already sanitized
unlink("../uploads/settings/$company_logo");
mysqli_query($mysqli,"UPDATE companies SET company_logo = NULL WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name deleted company logo");
flash_alert("Removed company logo", 'error');
redirect();
}

View File

@ -0,0 +1,28 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_default_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$start_page = sanitizeInput($_POST['start_page']);
$expense_account = intval($_POST['expense_account']);
$payment_account = intval($_POST['payment_account']);
$payment_method = sanitizeInput($_POST['payment_method']);
$expense_payment_method = sanitizeInput($_POST['expense_payment_method']);
$transfer_from_account = intval($_POST['transfer_from_account']);
$transfer_to_account = intval($_POST['transfer_to_account']);
$calendar = intval($_POST['calendar']);
$net_terms = intval($_POST['net_terms']);
$hourly_rate = floatval($_POST['hourly_rate']);
mysqli_query($mysqli,"UPDATE settings SET config_start_page = '$start_page', config_default_expense_account = $expense_account, config_default_payment_account = $payment_account, config_default_payment_method = '$payment_method', config_default_expense_payment_method = '$expense_payment_method', config_default_transfer_from_account = $transfer_from_account, config_default_transfer_to_account = $transfer_to_account, config_default_calendar = $calendar, config_default_net_terms = $net_terms, config_default_hourly_rate = $hourly_rate WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited default settings");
flash_alert("Default settings edited");
redirect();
}

View File

@ -0,0 +1,30 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_invoice_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_invoice_prefix = sanitizeInput($_POST['config_invoice_prefix']);
$config_invoice_next_number = intval($_POST['config_invoice_next_number']);
$config_invoice_footer = sanitizeInput($_POST['config_invoice_footer']);
$config_invoice_show_tax_id = intval($_POST['config_invoice_show_tax_id'] ?? 0);
$config_invoice_late_fee_enable = intval($_POST['config_invoice_late_fee_enable'] ?? 0);
$config_invoice_late_fee_percent = floatval($_POST['config_invoice_late_fee_percent']);
$config_recurring_invoice_prefix = sanitizeInput($_POST['config_recurring_invoice_prefix']);
$config_recurring_invoice_next_number = intval($_POST['config_recurring_invoice_next_number']);
$config_invoice_paid_notification_email = '';
if (filter_var($_POST['config_invoice_paid_notification_email'], FILTER_VALIDATE_EMAIL)) {
$config_invoice_paid_notification_email = sanitizeInput($_POST['config_invoice_paid_notification_email']);
}
mysqli_query($mysqli,"UPDATE settings SET config_invoice_prefix = '$config_invoice_prefix', config_invoice_next_number = $config_invoice_next_number, config_invoice_footer = '$config_invoice_footer', config_invoice_show_tax_id = $config_invoice_show_tax_id, config_invoice_late_fee_enable = $config_invoice_late_fee_enable, config_invoice_late_fee_percent = $config_invoice_late_fee_percent, config_invoice_paid_notification_email = '$config_invoice_paid_notification_email', config_recurring_invoice_prefix = '$config_recurring_invoice_prefix', config_recurring_invoice_next_number = $config_recurring_invoice_next_number WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited invoice settings");
flash_alert("Invoice Settings edited");
redirect();
}

View File

@ -0,0 +1,23 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_localization'])) {
validateCSRFToken($_POST['csrf_token']);
$locale = sanitizeInput($_POST['locale']);
$currency_code = sanitizeInput($_POST['currency_code']);
$timezone = sanitizeInput($_POST['timezone']);
mysqli_query($mysqli,"UPDATE companies SET company_locale = '$locale', company_currency = '$currency_code' WHERE company_id = 1");
mysqli_query($mysqli,"UPDATE settings SET config_timezone = '$timezone' WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited localization settings");
flash_alert("Company localization updated");
redirect();
}

View File

@ -0,0 +1,287 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_mail_smtp_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_smtp_provider = sanitizeInput($_POST['config_smtp_provider']);
$config_smtp_host = sanitizeInput($_POST['config_smtp_host']);
$config_smtp_port = intval($_POST['config_smtp_port'] ?? 0);
$config_smtp_encryption = sanitizeInput($_POST['config_smtp_encryption']);
$config_smtp_username = sanitizeInput($_POST['config_smtp_username']);
$config_smtp_password = sanitizeInput($_POST['config_smtp_password']);
// Shared OAuth fields
$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_smtp_provider = '$config_smtp_provider',
config_smtp_host = '$config_smtp_host',
config_smtp_port = $config_smtp_port,
config_smtp_encryption = '$config_smtp_encryption',
config_smtp_username = '$config_smtp_username',
config_smtp_password = '$config_smtp_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'
WHERE company_id = 1
");
logAction("Settings", "Edit", "$session_name edited SMTP settings");
flash_alert("SMTP Mail Settings updated");
redirect();
}
if (isset($_POST['edit_mail_imap_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_imap_provider = sanitizeInput($_POST['config_imap_provider']);
$config_imap_host = sanitizeInput($_POST['config_imap_host']);
$config_imap_port = intval($_POST['config_imap_port'] ?? 0);
$config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']);
$config_imap_username = sanitizeInput($_POST['config_imap_username']);
$config_imap_password = sanitizeInput($_POST['config_imap_password']);
// Shared OAuth fields
$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_host = '$config_imap_host',
config_imap_port = $config_imap_port,
config_imap_encryption = '$config_imap_encryption',
config_imap_username = '$config_imap_username',
config_imap_password = '$config_imap_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'
WHERE company_id = 1
");
logAction("Settings", "Edit", "$session_name edited IMAP settings");
flash_alert("IMAP Mail Settings updated");
redirect();
}
if (isset($_POST['edit_mail_from_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_mail_from_email = sanitizeInput(filter_var($_POST['config_mail_from_email'], FILTER_VALIDATE_EMAIL));
$config_mail_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_mail_from_name']));
$config_invoice_from_email = sanitizeInput(filter_var($_POST['config_invoice_from_email'], FILTER_VALIDATE_EMAIL));
$config_invoice_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_invoice_from_name']));
$config_quote_from_email = sanitizeInput(filter_var($_POST['config_quote_from_email'], FILTER_VALIDATE_EMAIL));
$config_quote_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_quote_from_name']));
$config_ticket_from_email = sanitizeInput(filter_var($_POST['config_ticket_from_email'], FILTER_VALIDATE_EMAIL));
$config_ticket_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_ticket_from_name']));
mysqli_query($mysqli,"UPDATE settings SET config_mail_from_email = '$config_mail_from_email', config_mail_from_name = '$config_mail_from_name', config_invoice_from_email = '$config_invoice_from_email', config_invoice_from_name = '$config_invoice_from_name', config_quote_from_email = '$config_quote_from_email', config_quote_from_name = '$config_quote_from_name', config_ticket_from_email = '$config_ticket_from_email', config_ticket_from_name = '$config_ticket_from_name' WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited mail from settings");
flash_alert("Mail From Settings updated");
redirect();
}
if (isset($_POST['test_email_smtp'])) {
validateCSRFToken($_POST['csrf_token']);
$test_email = intval($_POST['test_email']);
if($test_email == 1) {
$email_from = sanitizeInput($config_mail_from_email);
$email_from_name = sanitizeInput($config_mail_from_name);
} elseif ($test_email == 2) {
$email_from = sanitizeInput($config_invoice_from_email);
$email_from_name = sanitizeInput($config_invoice_from_name);
} elseif ($test_email == 3) {
$email_from = sanitizeInput($config_quote_from_email);
$email_from_name = sanitizeInput($config_quote_from_name);
} else {
$email_from = sanitizeInput($config_ticket_from_email);
$email_from_name = sanitizeInput($config_ticket_from_name);
}
$email_to = sanitizeInput($_POST['email_to']);
$subject = "Test email from ITFlow";
$body = "This is a test email from ITFlow. If you are reading this, it worked!";
$data = [
[
'from' => $email_from,
'from_name' => $email_from_name,
'recipient' => $email_to,
'recipient_name' => 'Chap',
'subject' => $subject,
'body' => $body
]
];
$mail = addToMailQueue($data);
if ($mail === true) {
flash_alert("Test email queued! <a class='text-bold text-light' href='mail_queue.php'>Check Admin > Mail queue</a>");
} else {
flash_alert("Failed to add test mail to queue", 'error');
}
redirect();
}
if (isset($_POST['test_email_imap'])) {
validateCSRFToken($_POST['csrf_token']);
$host = $config_imap_host;
$port = (int) $config_imap_port;
$encryption = strtolower(trim($config_imap_encryption)); // e.g. "ssl", "tls", "none"
$username = $config_imap_username;
$password = $config_imap_password;
// 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)
$contextOptions = [];
if (in_array($encryption, ['ssl', 'tls'], true)) {
$contextOptions['ssl'] = [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
];
}
$context = stream_context_create($contextOptions);
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) {
// Request STARTTLS
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));
}
// Enable crypto on the stream
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.");
}
}
// --- Do LOGIN command ---
$tag = 'A0002';
// Simple quoting; this may fail with some special chars in username/password.
$loginCmd = sprintf(
"%s LOGIN \"%s\" \"%s\"\r\n",
$tag,
addcslashes($username, "\\\""),
addcslashes($password, "\\\"")
);
fwrite($fp, $loginCmd);
$success = false;
$errorLine = '';
while (!feof($fp)) {
$line = fgets($fp, 2048);
if ($line === false) {
break;
}
// Look for tagged response for our LOGIN
if (strpos($line, $tag . ' ') === 0) {
if (stripos($line, $tag . ' OK') === 0) {
$success = true;
} else {
$errorLine = trim($line);
}
break;
}
}
// Always logout / close
fwrite($fp, "A0003 LOGOUT\r\n");
fclose($fp);
if ($success) {
flash_alert("Connected successfully");
} else {
if (!$errorLine) {
$errorLine = 'Unknown IMAP authentication error';
}
throw new Exception($errorLine);
}
} catch (Exception $e) {
flash_alert("<strong>IMAP connection failed:</strong> " . htmlspecialchars($e->getMessage()), 'error');
}
redirect();
}

View File

@ -0,0 +1,28 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_module_settings'])) {
$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_accounting = intval($_POST['config_module_enable_accounting'] ?? 0);
$config_client_portal_enable = intval($_POST['config_client_portal_enable'] ?? 0);
$config_whitelabel_key = sanitizeInput($_POST['config_whitelabel_key']);
mysqli_query($mysqli,"UPDATE settings SET config_module_enable_itdoc = $config_module_enable_itdoc, config_module_enable_ticketing = $config_module_enable_ticketing, config_module_enable_accounting = $config_module_enable_accounting, config_client_portal_enable = $config_client_portal_enable WHERE company_id = 1");
// Validate white label key
if (!empty($config_whitelabel_key && validateWhitelabelKey($config_whitelabel_key))) {
mysqli_query($mysqli, "UPDATE settings SET config_whitelabel_enabled = 1, config_whitelabel_key = '$config_whitelabel_key' WHERE company_id = 1");
} else {
mysqli_query($mysqli, "UPDATE settings SET config_whitelabel_enabled = 0, config_whitelabel_key = '' WHERE company_id = 1");
}
logAction("Settings", "Edit", "$session_name edited module settings");
flash_alert("Module Settings updated");
redirect();
}

View File

@ -0,0 +1,23 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_notification_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_enable_cron = intval($_POST['config_enable_cron'] ?? 0);
$config_enable_alert_domain_expire = intval($_POST['config_enable_alert_domain_expire'] ?? 0);
$config_send_invoice_reminders = intval($_POST['config_send_invoice_reminders'] ?? 0);
$config_recurring_auto_send_invoice = intval($_POST['config_recurring_auto_send_invoice'] ?? 0);
$config_ticket_client_general_notifications = intval($_POST['config_ticket_client_general_notifications'] ?? 0);
mysqli_query($mysqli,"UPDATE settings SET config_send_invoice_reminders = $config_send_invoice_reminders, config_recurring_auto_send_invoice = $config_recurring_auto_send_invoice, config_enable_cron = $config_enable_cron, config_enable_alert_domain_expire = $config_enable_alert_domain_expire, config_ticket_client_general_notifications = $config_ticket_client_general_notifications WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited notification settings");
flash_alert("Notification Settings updated");
redirect();
}

View File

@ -0,0 +1,30 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['edit_online_payment_settings'])) {
validateCSRFToken($_POST['csrf_token']);
$config_stripe_enable = intval($_POST['config_stripe_enable'] ?? 0);
$config_stripe_publishable = sanitizeInput($_POST['config_stripe_publishable']);
$config_stripe_secret = sanitizeInput($_POST['config_stripe_secret']);
$config_stripe_account = intval($_POST['config_stripe_account']);
$config_stripe_expense_vendor = intval($_POST['config_stripe_expense_vendor']);
$config_stripe_expense_category = intval($_POST['config_stripe_expense_category']);
$config_stripe_percentage_fee = floatval($_POST['config_stripe_percentage_fee']) / 100;
$config_stripe_flat_fee = floatval($_POST['config_stripe_flat_fee']);
mysqli_query($mysqli,"UPDATE settings SET config_stripe_enable = $config_stripe_enable, config_stripe_publishable = '$config_stripe_publishable', config_stripe_secret = '$config_stripe_secret', config_stripe_account = $config_stripe_account, config_stripe_expense_vendor = $config_stripe_expense_vendor, config_stripe_expense_category = $config_stripe_expense_category, config_stripe_percentage_fee = $config_stripe_percentage_fee, config_stripe_flat_fee = $config_stripe_flat_fee WHERE company_id = 1");
logAction("Settings", "Edit", "$session_name edited online payment settings");
if ($config_stripe_enable && $config_stripe_account == 0) {
flash_alert("Stripe payment account must be specified!", 'error');
} else {
flash_alert("Online Payment Settings updated");
}
redirect();
}

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