first commit
Some checks failed
CI / build-test (push) Has been cancelled

This commit is contained in:
2025-05-31 18:56:37 +02:00
commit 8c4798a5fd
1240 changed files with 190468 additions and 0 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
[{package.json,*.yml}]
indent_size = 2

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

58
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,58 @@
# Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race,
religion, or sexual identity and orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language.
* Being respectful of differing viewpoints and experiences.
* Gracefully accepting constructive criticism.
* Focusing on what is best for the community.
* Showing empathy towards other community members.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances.
* Trolling, insulting/derogatory comments, and personal or political attacks.
* Public or private harassment.
* Publishing others' private information, such as a physical or electronic address, without explicit permission.
* Other conduct which could reasonably be considered inappropriate in a professional setting.
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
project or its community. Examples of representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed representative at an online or offline
event. Representation of a project may be further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project owner at
[alextselegidis@gmail.com](alextselegidis@gmail.com). All complaints will be reviewed and investigated and will result
in a response that is deemed necessary and appropriate to the circumstances. The project owner is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be
posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
repercussions as determined by other members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4,
available at http://contributor-covenant.org/version/1/4.

17
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,17 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other
method with the owner of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a build.
2. Ensure that your changes comply with the project's coding guidelines and that it's sufficiently documented.
3. Update the README.md with details of changes to the interface, this includes new environment variables, exposed
ports, useful file locations and container parameters.
4. Target the develop branch for your Pull Requests as this is were new changes are introduced.
4. After being successfully reviewed pull requests will be merged to develop branch and will finally be included in an
upcoming release.

4
.github/SECURITY.md vendored Normal file
View File

@ -0,0 +1,4 @@
# Security Vulnerabilities
If you discover a security vulnerability within Easy!Appointments, please send an email to info@easyappointments.org.
All security vulnerabilities will be promptly addressed.

23
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: CI
on: [ push ]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Git clone
uses: actions/checkout@v2
- name: Install dependencies
uses: php-actions/composer@v6
with:
php_version: 8.2
php_extensions: gd
version: 2
- name: PHPUnit Tests
uses: php-actions/phpunit@v3
env:
TEST_NAME: Test
with:
configuration: phpunit.xml

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
/*.zip
/.idea/
/.tmp-package/
/build/
/docs/apigen/html
/docs/jsdoc/html
/docs/plato/html
/docker/mysql/
/docker/baikal/
/docker/openldap/
/node_modules/
/npm-debug.log
/assets/js/**/*.min.js
/assets/css/**/*.css
/assets/vendor
/config.php
/storage/backups/*
!/storage/backups/.htaccess
!/storage/backups/index.html
/storage/cache/*
!/storage/cache/.htaccess
!/storage/cache/index.html
/storage/logs/*
!/storage/logs/.htaccess
!/storage/logs/index.html
/storage/sessions/*
!/storage/sessions/.htaccess
!/storage/sessions/index.html
/storage/uploads/*
!/storage/uploads/index.html
/vendor/
/metafile
.DS_Store

11
.prettierignore Normal file
View File

@ -0,0 +1,11 @@
.idea
.github
.run
application/language/*
build
docker
docs
node_modules
storage
system
vendor

12
.prettierrc.json Normal file
View File

@ -0,0 +1,12 @@
{
"printWidth": 120,
"tabWidth": 4,
"singleQuote": true,
"bracketSpacing": false,
"bracketSameLine": false,
"quoteProps": "preserve",
"trailingComma": "all",
"trailingCommaPHP": true,
"phpVersion": "8.0",
"plugins": ["@prettier/plugin-php"]
}

17
.run/Build.run.xml Normal file
View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="if [ -z &quot;$(docker compose ls | grep $(basename &quot;$PWD&quot;))&quot; ]; then docker stop $(docker ps -a -q) &amp;&amp; docker network prune -f &amp;&amp; docker compose up -d --force-recreate &amp;&amp; docker exec -it $(basename &quot;$PWD&quot;)-php-fpm-1 npm run build; else docker exec -it $(basename &quot;$PWD&quot;)-php-fpm-1 npm run build; fi " />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

17
.run/SSH.run.xml Normal file
View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SSH" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="if [ -z &quot;$(docker compose ls | grep $(basename &quot;$PWD&quot;))&quot; ]; then docker stop $(docker ps -a -q) &amp;&amp; docker network prune -f &amp;&amp; docker compose up -d --force-recreate &amp;&amp; docker exec -it $(basename &quot;$PWD&quot;)-php-fpm-1 bash; else docker exec -it $(basename &quot;$PWD&quot;)-php-fpm-1 bash; fi" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

17
.run/Start.run.xml Normal file
View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="if [ -z &quot;$(docker compose ls | grep $(basename &quot;$PWD&quot;))&quot; ]; then docker stop $(docker ps -a -q) &amp;&amp; docker network prune -f &amp;&amp; docker compose up -d --force-recreate &amp;&amp; docker exec -it $(basename &quot;$PWD&quot;)-php-fpm-1 npm start; else echo &quot;Docker compose is already running\!&quot;; fi " />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

17
.run/Stop.run.xml Normal file
View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Stop" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="docker stop $(docker ps -a -q)" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

392
CHANGELOG.md Normal file
View File

@ -0,0 +1,392 @@
# Easy!Appointments - Changelog
This file contains the code changes that were introduced into each release (starting from v1.1.0) so that is easy for
developers to maintain and readjust their custom modifications on the main project codebase.
## [1.5.1] - 2025-01-20
### Added
- Add support for PHP 8.4 (#1640)
- Add new secret token header field to webhooks and default to X-EA-Token (#1607)
- Add company colors to notifications (#1569)
- Add Albanian Language Translations (#1646)
### Fixed
- Fix the date parsing issue on Safari web browsers during the booking process (#1584)
- Fix working plan configuration am/pm hour parsing so that it works in all languages (#1606)
- Improve the CalDAV syncing mechanism so that it connects to more systems without problems (#1622)
- Fix various 1.5.0 API issues (#1562)
- Correct email issues by replacing the internal email library with phpmailer (#1587)
- Fix ICS file mimetype (#1630)
- Incorrect Timezone Handling in CalDAV Synchronization Causes Time Shifts (#1626)
- No color change in the appointment modal window (in the backend calendar) (#1641)
- The plain user password might appear in the log messages in case of an error (#1590)
- Fix loop when going a month back in an edge case (#1577)
- Dedicated provider links do not pre-select the relevant provider on the booking page (#1651)
- Correct the service / provider booking header display (#1650)
## [1.5.0] - 2024-07-07
### Added
- Display month with the earliest available day (#1075)
- Allow admins to define the business closed dates (blocked-periods) (#432)
- Allow working plan exceptions to be defined as non-working days (#1383)
- Create an official docker image of the project and host it on Docker Hub(#1116)
- Automatically select the next available date in the booking page or display a message if this month is unavailable (#1204)
- Add Open Graph information to the public booking page so that it renders nicely once shared on social media apps (#1382)
- Preselect the date with a query parameter (#1376)
- Add the location and notes fields to the appointment email notifications (if a value was provided) (#1341)
- Add date, from and till query parameters to the filter the appointments index results by date (#1134)
- Allow the users to define their own status and assign them to appointments (#244)
- Add new setting for limiting new public bookings in the future (#1203)
- Automatically enable the secure cookie config if the current installation uses HTTPS (#1126)
- Add language and timezone properties to the customer API resource (#1157)
- Add support for the definition of custom webhooks via the settings page (#581)
- Allow the user to select their own preferred language (#1263)
- Support multiple Bootswatch themes for the app (#1205)
- Providers and secretaries must only be able to see and manage their own customers (#1199)
- Use the default service duration if the user just clicks on a calendar slot for creating a new appointment (#1237)
- Google Calendar synchronisation failure when symbols/emoji appear in events to be imported (#1182)
- Add the customer timezone field in the appointment modal of the calendar page (#1094)
- Add a new setting that toggles the login link of the booking page (#1148)
- Add custom Matomo analytics integration (#974)
- Prefill the form field though url parameters (#1021)
- Color code events by provider or service (#422)
- Service duration values shorter than 5 minutes should be acceptable via the services page (#1110)
- Add a new "is_private" flag to services and providers so that they do not appear in the booking page (#378)
- Skip the first booking step when only one service and one provider are available (#349)
- Enable the change of the brand logo and colors from the backend (#789)
- Add the ability to temporarily block new appointments / set away message (#940)
- Add optional (configurable with setting) phone number validation (#820)
- Add an option to deactivate the remove-all-data function for customers (#808)
- Skip the first booking step if both provider and service are preselected (#1117)
- Make delete appointment via API to send emails just like the calendar page does (#1101)
- Create new layout structure for the markup, so that common HTML markup is being reused (#1152)
- Have an option to hide customer data fields during booking (#1081)
- Add a SECURITY.md file to the repository (#1122)
- Add support for custom fields on customers (#1133)
- Add from email/name and reply-to settings in the email.php configuration file (#1465)
- Create a new setting that will define the default timezone of the application (#1390)
- Integrate CalDAV Protocol for appointment syncing (#209)
- Add LDAP / Active Directory integration (#128)
### Changed
- Do not allow a customer to book the same hours multiple times (#1420)
- All the user roles with access to the backend calendar page can filter by services (#956)
- Update Bootstrap to version 5 (#1150)
- Update FullCalendar to version 5 (#1151)
- The availability generation algorithm needs performance improvements when many appointments are stored in the system (#1171)
- Support for relative paths when loading resources or working with the session (#1158)
- Support line breaks when displaying the service description (#1149)
- Remove the CodeIgniter fork from the composer.json file and re-import the system directory (#1109)
### Fixed
- Sync all the providers without errors, when the user clicks on the "sync" button and "all" is selected in the calendar page (#1365)
- Non-working time not showing correctly in the week view of calendar (#1381)
- Make sure the booking cancellation is a post request and has a reason value provided (#1178)
### Removed
- Remove the engine directory and files form the app (they're deprecated) (#971)
- Remove the PHPMailer dependency from the app and use the built-in CodeIgniter mailer (#970)
## [1.4.3] - 2022-03-08
### Added
- #1208: Security configuration enhancements in the application.
- #1209: Add support for PHP 8.1.
### Changed
- #1207: Replace CodeIgniter with the system directory for smaller package size and more control over the framework.
- #1210: Move the change language operation into a new public controller.
- #1212: Remove the Google Calendar read-only scope use as it is not needed.
- #1213: Switch to go-to-latest database migration configuration for simplicity.
- #1216 Replace Google Client JS with the Google Calendar Template link in the book success page enhancement.
### Fixed
- #1211: The table calendar view breaks for secretaries and providers due to appointment and unavailability removals bug.
- #1214: Provider and secretary users can only add unavailabilities for their authorized users bug.
## [1.4.2] - 2021-07-27
### Added
- #1004: Add support for line breaks when displaying the service description in the frontend.
- #1040: Support all-day events while syncing with Google Calendar.
### Fixed
- #961: Timezone/UX issue: Wrong day is selected when timezone differs by -1 day.
- #966: Secretaries are getting notification emails for providers that are not assigned to them.
- #980: Missing Pacific (and potentially other) timezones.
- #982: The Any-Provider option might lead to double bookings, if all the providers have the same number of appointments for the selected date.
- #986: Managed to replicate appointment hash collisions.
- #989: Fix Critical mistake resulting in wrong date
- #990: The API availabilities controller throws an error when generating availability for services with multiple attendants.
- #991: Available hours generated with the "Any Provider" option in the booking page, may use the information of a provider that is not assigned to the selected service.
- #993: Add support for PHP8 (vendor packages need to be updated).
- #1000: Small fix for the display of the delete button in table view.
- #1011: Working plan exception - details pane shows incorrect details.
- #1023: Backend calendar table events missing or duplicated.
- #1026: The timepicker sliders do not work when using an iOS device.
- #1029: Enhance SMTP functions of PHPMailer.
- #1043: Unavailable events do not block time from services with multiple attendants.
- #1046: Make sure that saving the modifications of a single break does not cancel any pending break edits.
- #1068: Set minimum service duration field value to honor the value of EVENT_MINIMUM_DURATION.
- #1073: Update PHPMailer dependencies.
- #1074: In case of deletion of one appointment, system sends email to admins anyway even if they have email notifications disabled.
- #1092: Javascript RangeError on appointment change causing disabled calendar dates.
## [1.4.1] - 2020-12-14
### Added
- #952: Add timezone support in the REST API, when managing users.
- #955: Display confirmation modal when disabling a connected Google Calendar Sync.
### Fixed
- #945: Google Calendar sync throws an error with all day Google Calendar Events.
- #946: Typo in JavaScript code leads to a broken calendar view, when loading unavailability events with note contents.
- #948: Multiple attendant services may lead to double booking.
- #950: Cannot create provider without services via the API, some values (other endpoints) are optional too.
- #953: Current time indicator in fullcalendar is showing time in local timezone and not in the user selected timezone.
- #954: The password must be provided via the API when creating new users.
## [1.4.0] - 2020-12-09
### Added
- #203: Appointment location / 12-hour format / sync notes and location in Google Calendar.
- #221: Fixed/Improved sort breaks increasingly by hour within day.
- #247: Add new system-wide setting for removing the "Any Provider" option of the booking page.
- #251: Automatically populate the appointment end datetime in API.
- #301: Automatically reload the backend calendar events.
- #313: How to set the timezone from the user booking the appointment.
- #365: Only allow appointments for a few weeks in advance.
- #431: Add support for working plan exceptions.
- #471: Add new system-wide setting that enable users to choose the first day of the week.
- #496: Add pagination on every backend page in order to make filter requests faster.
- #501: Integrate script for assets minification.
- #502: Config::DEBUG value toggles the use of normal or minified asset files.
- #546: Add appointment edit link in the backend customers page.
- #550: Multi-Lang Front-End selection popup not working on mobile.
- #551: Front-End booking calendar not syncing with business logic working plan.
- #572: Ensure the database structure is compatible to at least MySQL 5.5.
- #576: Appointment cancelled exception not showing properly.
- #610: Token based authentication for the Rest API.
- #648: Add a warning when customers delete their personal information.
- #655: Creating an appointment requires user to enter their phone number enhancement.
- #659: Automatically detect browser language enhancement.
- #663: Language selector not working under legacy iOS (v.10.3.1).
- #680: Generate new password in the generate_random_string function may create duplicate passwords, plus it is not secure enough.
- #739: Enhance the table view mode by replacing the tables with fullcalendar instances.
- #770: Store customer's language and use it with notifications or when the customer manages and existing appointment.
- #889: Notify admins and secretaries on appointment changes.
### Changed
- #386: Service price should be optional.
- #428: Enable book advance timeout values in days.
- #568: Sort providers alphabetically in the booking page.
- #745: Add appointment notes preview in the event popover.
### Fixed
- #171: Google calendar sync - wrong timezone for appointments.
- #195: Fix Google calendar sync activation error (JavaScript).
- #298: Provider availability issue when selecting the "Any Provider" option.
- #396: Start and end time do not update correctly during calendar time selection on iPad (and other Safari based devices).
- #447: Captcha error using docker (500 error).
- #506: Working plan created in version v1.2.1 wrongly displayed in backend with version v1.3.1.
- #507: Need to manually clean the cache when migrating from v1.2.1 to v1.3.1.
- #541: Can't remove (empty) customer notes field.
- #549: Querying appointments API endpoint with the q parmeter produces PHP warnings.
- #557: App not connecting to MySQL with fresh docker run.
- #562: Unavailability periods with length of more than 1 day are not handled correctly.
- #563: Description field overflows with long text.
- #600: Unable to select Language on mobile phones.
- #611: Double replacement when using translation to other languages.
- #664: Easy!Appointments v1.3.2 allows sensitive information disclosure (username and password hash).
- #687: Errors when the provider modifies an appointment.
- #705: The alert notification of the installation is not being displayed on error.
- #757: Corrected display of datetimepickers when editing events.
- #801: Invalid time duration during appointment registration could lead to DOS of the service.
- #813: Hyperlinks are not being displayed correctly inside legal contents (they are escaped).
- #839: Provider is missing on appointment modal opened after a click on the link sent with the provider email confirmation.
- #840: Start/end datetime are not correctly initialized on Safari when the appointment modal is opened after a click in the backend calendar.
- #883: Appointment date is wrongly changed to today in some case.
- #903: Notification not working when creating/updating/deleting an appointment from the REST API.
## [1.3.2] - 2018-07-29
### Fixed
- #480: Make the app GDPR - new EU privacy regulations compliant.
- #485: Make REST API search check with "q" parameter case insensitive.
- #489: REST API response headers must use the Content-Type application/json value.
- #500: Performance optimization in backend calendar page, after the user clicks the insert appointment button.
- #510: Providers should not be able to create appointments for other providers in the backend calendar page.
- #512: Only show appointments of the currently logged in provider.
## [1.3.1] - 2018-06-03
### Added
- #410: Time format from American style to European
- #441: Added time format selection
- #452: Provide more information when errors occur during the installation.
### Changed
- #494: French translation corrections/improvements.
### Fixed
- #433: Selected date when editing an appointment
- #436: All days unavailable in agendaDay view
- #438: Error on update process from 1.2.1 to 1.3.0
- #440: Correct label text for customer phone number in back-end calendar event popup.
- #453: Unavailable periods not taken into account when more than one customer
- #455: French Spelling
- #459: Aggregate Appointment API crashes when a break was added
- #461: Invalid working plan parsing for foreign languages on day view of the default calendar view.
- #475: Booking page date selection is broken with any_provider option selected.
- #483: In backend, calendar for providers become unselectable if switched to calendar for service.
- #491: Replace hardcoded string with translation in appointment details email template.
- #495: Database migration fixes (from 1.2.1 to 1.3.x).
- #497: Backend settings are not being displayed on page load when the user is not an admin.
## [1.3.0] - 2018-02-28
### Added
- #65: Insert new appointment by clicking directly on the calendar.
- #122: Add customer email and phone number in the event popover of the backend/calendar page.
- #152: Add support for American Time Format (AM/PM).
- #176: Add Docker container for Easy!Appointments development.
- #362: Add Arabic language translation.
- #395: Add aggregates GET parameter in the appointments REST API resource.
- #397: Allow PHP v7.1 compatibility.
- #398: Send ICS files with customer/provider email confirmations.
- #399: Integrate cache busting for assets into the app.
- #402: Create simple "update" page.
### Changed
- #276: Update FullCalendar dependency.
- #394: Corrections in the Bootstrap classes in view files.
- #401: Replace loading spinner graphic with a newer one.
- #403: All calendars will start with Sunday as the first day.
### Fixed
- #155: Appointment management modal is not updated after appointment duration resize.
- #236: Duplicate availabilities with short service duration and unavailabilities ignorance.
- #315: Calendar doesn't update when Attendants number changes.
- #334: Use of session_start() function may cause issues as the default options are not being used.
- #336: Deleting provider doesn't work in some languages.
- #337: Full day appointment with multiple attendants are not being taken into concern during availabilities generation.
- #342: Email notifications must honor the date format value.
- #370: AJAX Error: SyntaxError: Unexpected token < in JSON at position 0
### Removed
- #400: Remove jscrollpane dependency.
### Deprecated
- The availabilities generation and AJAX endpoints will change with a future release.
## [1.2.1] - 2017-05-21
### Changed
- #241: Set default sessions save_path directory because many servers do not have this option set.
### Fixed
- #306: Back-end login does not work with PHP 7.1.
## [1.2.0] - 2016-11-09
### Added
- #24: Provide dedicated URL for separate provider/service bookings.
- #45: Add secure flag to CI_SESSION when HTTPS is enabled.
- #54: Default auto-complete for the appointment end time according to service duration.
- #109: Accept multiple attendants in a single service session.
- #180: Add login link to booking footer.
- #182: Add new appointment dashboard view: Table Scheduler
- #183: Display the appointments modal when the user clicks in an empty calendar slot.
- #185: Enable fixed availabilities setting for services.
- #174: Implementation of a REST API.
- #175: The backend must be responsive as well.
- #178: Load the application/config/email.php settings to PHPMailer instance.
### Changed
- #63: Update vendor files (CodeIgniter, FullCalendar, Bootstrap, jQuery ...)
### Fixed
- #173: JS Scrollbars do not work for backend/users entries.
- #137: Invalid responsive behavior in frontend booking wizard (step #2 - appointment date & time).
- #136: Loading spinner is not shown in during the installation.
- #127: Links in header contain index.php
- #22: Google Calendar Sync - Time Zone Issue
## [1.1.1] - 2016-02-14
### Fixed
- #116: Book advance timeout not taken into account for proposed appointments.
- #118: Google Calendar and notification mail problem bug.
- #120: Invalid appointment date set after editing an existing appointment.
## [1.1.0] 2016-01-24
### Added
- #14: Add Google Analytics tracking for the booking page.
- #15: Add captcha to booking page.
- #16: Responsive Frontend
- #25: Add a disable customer mail notifications setting
- #27: Support american time format within the app.
- #31: Double booking when two users try to book the same appointment hour and at the same time.
### Changed
- #4: Raising more useful exceptions and enable error logging by default.
- #13: Upgrade to Bootstrap 3.x.x.
- #38: Renamed `configuration.php` file to `config.php` and changed the `SystemConfiguration` class to `Config`. This class will contain constants with the project configuration and will be statically used.
- #39: Add latest translations to source code so that user can select them immediately.
- #42: Place all external assets to "ext" directory.
### Removed
- #40: Removed `.htaccess` file and updated all the URLs with the `index.php` file so that mod_rewrite problems are eliminated.
- #41: Removed `cancel.php` file. Frontend must use the `message.php` file for displaying simple messages to user.
### Fixed
- #6: Business Logic created is not getting assigned to service provider.
- #10: Unable to use address tags in email address.
- #18: Duration is not changing when adding a new appointment.
- #21: Fix Easy!Appointments installation problems with AJAX requests.
- #66: Trouble with breaks for providers.
## [1.0.0] - 2014-01-19
First Easy!Appointments release ever!

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
# ======================
# Etapa 1: Compilacion
# ======================
FROM php:8.1-cli AS builder
# Instalar herramientas
RUN apt-get update && apt-get install -y \
git curl unzip zip nodejs npm libzip-dev libpng-dev libjpeg-dev \
libfreetype6-dev libonig-dev libxml2-dev
RUN docker-php-ext-install pdo pdo_mysql mbstring zip gd mysqli
# Instalar Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Copiar el codigo fuente
WORKDIR /app
COPY . .
# Instalar dependencias PHP y JS + compilar
RUN composer install --no-dev --optimize-autoloader
RUN npm install && npm run build
# ======================
# Etapa 2: Imagen final
# ======================
FROM php:8.1-apache
RUN apt-get update && apt-get install -y libzip-dev libpng-dev libjpeg-dev \
libfreetype6-dev libonig-dev libxml2-dev && \
docker-php-ext-install pdo pdo_mysql mbstring zip gd mysqli && \
a2enmod rewrite
WORKDIR /var/www/html
# Copiar solo los archivos ya listos desde la etapa anterior
COPY --from=builder /app /var/www/html
# Permisos adecuados
RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

111
README.md Normal file
View File

@ -0,0 +1,111 @@
<h1 align="center">
<br>
<a href="https://easyappointments.org">
<img src="https://raw.githubusercontent.com/alextselegidis/easyappointments/develop/logo.png" alt="Easy!Appointments" width="150">
</a>
<br>
Easy!Appointments
<br>
</h1>
<br>
<h4 align="center">
A powerful Open Source Appointment Scheduler that can be installed on your server.
</h4>
<p align="center">
<img alt="GitHub" src="https://img.shields.io/github/license/alextselegidis/easyappointments?style=for-the-badge">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/alextselegidis/easyappointments?style=for-the-badge">
<img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/alextselegidis/easyappointments/total?style=for-the-badge">
<a href="https://discord.com/invite/UeeSkaw">
<img alt="Chat On Discord" src="https://img.shields.io/badge/chat-on%20discord-7289da?style=for-the-badge&logo=discord&logoColor=white">
</a>
</p>
<p align="center">
<a href="#about">About</a>
<a href="#features">Features</a>
<a href="#setup">Setup</a>
<a href="#installation">Installation</a>
<a href="#license">License</a>
</p>
![screenshot](screenshot.png)
## About
**Easy!Appointments** is a highly customizable web application that allows customers to book appointments with you
via a sophisticated web interface. Moreover, it provides the ability to sync your data with Google Calendar so you can
use them with other services. It is an open source project that you can download and install **even for commercial use**.
Easy!Appointments will run smoothly with your existing website as it can be installed in a single folder of the
server and of course share an existing database.
## Features
The application is designed to be flexible enough so that it can handle any enterprise work flow.
* Customers and appointments management.
* Services and providers organization.
* Working plan and booking rules.
* Google Calendar synchronization.
* Email notifications system.
* Self hosted installation.
* Translated user interface.
* User community support.
## Setup
To clone and run this application, you'll need [Git](https://git-scm.com), [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) and [Composer](https://getcomposer.org) installed on your computer. From your command line:
```bash
# Clone this repository
$ git clone https://github.com/alextselegidis/easyappointments.git
# Go into the repository
$ cd easyappointments
# Install dependencies
$ npm install && composer install
# Start the file watcher
$ npm start
```
Note: If you're using Linux Bash for Windows, [see this guide](https://www.howtogeek.com/261575/how-to-run-graphical-linux-desktop-applications-from-windows-10s-bash-shell/) or use `node` from the command prompt.
You can build the files by running `npm run build`. This command will bundle everything to a `build` directory.
## Installation
You will need to perform the following steps to install the application on your server:
* Make sure that your server has Apache/Nginx, PHP (8.2+) and MySQL installed.
* Create a new database (or use an existing one).
* Copy the "easyappointments" source folder on your server.
* Make sure that the "storage" directory is writable.
* Rename the "config-sample.php" file to "config.php" and update its contents based on your environment.
* Open the browser on the Easy!Appointments URL and follow the installation guide.
That's it! You can now use Easy!Appointments at your will.
You will find the latest release at [easyappointments.org](https://easyappointments.org).
If you have problems installing or configuring the application visit the
[official support group](https://groups.google.com/forum/#!forum/easy-appointments).
You can also report problems on the [issues page](https://github.com/alextselegidis/easyappointments/issues)
and help the development progress.
## License
Code Licensed Under [GPL v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) | Content Under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/)
---
Website [alextselegidis.com](https://alextselegidis.com) &nbsp;&middot;&nbsp;
GitHub [alextselegidis](https://github.com/alextselegidis) &nbsp;&middot;&nbsp;
Twitter [@alextselegidis](https://twitter.com/AlexTselegidis)
###### More Projects On Github
###### ⇾ [Plainpad &middot; Self Hosted Note Taking App](https://github.com/alextselegidis/plainpad)
###### ⇾ [Questionful &middot; Web Questionnaires Made Easy](https://github.com/alextselegidis/questionful)
###### ⇾ [Integravy &middot; Service Orchestration At Your Fingertips](https://github.com/alextselegidis/integravy)

1
application/.htaccess Normal file
View File

@ -0,0 +1 @@
Deny from all

View File

@ -0,0 +1,18 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
|--------------------------------------------------------------------------
| App Configuration
|--------------------------------------------------------------------------
|
| Declare some of the global config values of Easy!Appointments.
|
*/
$config['version'] = '1.5.1'; // This must be changed manually.
$config['url'] = Config::BASE_URL;
$config['debug'] = Config::DEBUG_MODE;
$config['cache_busting_token'] = 'TSJ77';

View File

@ -0,0 +1,133 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
| -------------------------------------------------------------------
| AUTO-LOADER
| -------------------------------------------------------------------
| This file specifies which systems should be loaded by default.
|
| In order to keep the framework as light-weight as possible only the
| absolute minimal resources are loaded by default. For example,
| the database is not connected to automatically since no assumption
| is made regarding whether you intend to use it. This file lets
| you globally define which systems you would like loaded with every
| request.
|
| -------------------------------------------------------------------
| Instructions
| -------------------------------------------------------------------
|
| These are the things you can load automatically:
|
| 1. Packages
| 2. Libraries
| 3. Helper files
| 4. Custom config files
| 5. Language files
| 6. Models
|
*/
/*
| -------------------------------------------------------------------
| Auto-load Packages
| -------------------------------------------------------------------
| Prototype:
|
| $autoload['packages'] = array(APPPATH.'third_party', '/usr/local/shared');
|
*/
$autoload['packages'] = [];
/*
| -------------------------------------------------------------------
| Auto-load Libraries
| -------------------------------------------------------------------
| These are the classes located in the system/libraries folder
| or in your application/libraries folder.
|
| Prototype:
|
| $autoload['libraries'] = array('database', 'session', 'xmlrpc');
*/
$autoload['libraries'] = ['database', 'session'];
/*
| -------------------------------------------------------------------
| Auto-load Helper Files
| -------------------------------------------------------------------
| Prototype:
|
| $autoload['helper'] = array('url', 'file');
*/
$autoload['helper'] = [
'array',
'asset',
'config',
'date',
'debug',
'env',
'file',
'html',
'http',
'installation',
'language',
'password',
'path',
'permission',
'rate_limit',
'routes',
'session',
'setting',
'string',
'url',
'validation'
];
/*
| -------------------------------------------------------------------
| Auto-load Config files
| -------------------------------------------------------------------
| Prototype:
|
| $autoload['config'] = array('config1', 'config2');
|
| NOTE: This item is intended for use ONLY if you have created custom
| config files. Otherwise, leave it blank.
|
*/
$autoload['config'] = ['app', 'google', 'email'];
/*
| -------------------------------------------------------------------
| Auto-load Language files
| -------------------------------------------------------------------
| Prototype:
|
| $autoload['language'] = array('lang1', 'lang2');
|
| NOTE: Do not include the "_lang" part of your file. For example
| "codeigniter_lang.php" would be referenced as array('codeigniter');
|
*/
$autoload['language'] = [];
/*
| -------------------------------------------------------------------
| Auto-load Models
| -------------------------------------------------------------------
| Prototype:
|
| $autoload['model'] = array('model1', 'model2');
|
*/
$autoload['model'] = [];
/* End of file autoload.php */
/* Location: ./application/config/autoload.php */

View File

@ -0,0 +1,476 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
|--------------------------------------------------------------------------
| Base Site URL
|--------------------------------------------------------------------------
|
| URL to your CodeIgniter root. Typically this will be your base URL,
| WITH a trailing slash:
|
| http://example.com/
|
| If this is not set then CodeIgniter will guess the protocol, domain and
| path to your installation.
|
*/
$protocol =
(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ||
(isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
? 'https://'
: 'http://';
$domain = $_SERVER['HTTP_HOST'] ?? 'localhost';
$request_uri = dirname($_SERVER['SCRIPT_NAME'] ?? 'index.php');
if ($request_uri === '.') {
$request_uri = '';
}
$config['base_url'] = !is_cli() ? trim($protocol . $domain . $request_uri, '/') : Config::BASE_URL;
/*
|--------------------------------------------------------------------------
| Index File
|--------------------------------------------------------------------------
|
| Typically this will be your index.php file, unless you've renamed it to
| something else. If you are using mod_rewrite to remove the page set this
| variable so that it is blank.
|
*/
$config['index_page'] = 'index.php';
/*
|--------------------------------------------------------------------------
| URI PROTOCOL
|--------------------------------------------------------------------------
|
| This item determines which server global should be used to retrieve the
| URI string. The default setting of 'AUTO' works for most servers.
| If your links do not seem to work, try one of the other delicious flavors:
|
| 'AUTO' Default - auto-detects
| 'PATH_INFO' Uses the PATH_INFO
| 'QUERY_STRING' Uses the QUERY_STRING
| 'REQUEST_URI' Uses the REQUEST_URI
| 'ORIG_PATH_INFO' Uses the ORIG_PATH_INFO
|
*/
$config['uri_protocol'] = 'AUTO';
/*
|--------------------------------------------------------------------------
| URL suffix
|--------------------------------------------------------------------------
|
| This option allows you to add a suffix to all URLs generated by CodeIgniter.
| For more information please see the user guide:
|
| http://codeigniter.com/user_guide/general/urls.html
*/
$config['url_suffix'] = '';
/*
|--------------------------------------------------------------------------
| Default Language
|--------------------------------------------------------------------------
|
| This determines which set of language files should be used. Make sure
| there is an available translation if you intend to use something other
| than english.
|
*/
$languages = [
'sq' => 'albanian',
'ar' => 'arabic',
'bs' => 'bosnian',
'bu' => 'bulgarian',
'ca' => 'catalan',
'cs' => 'czech',
'da' => 'danish',
'de' => 'german',
'el' => 'greek',
'en' => 'english',
'es' => 'spanish',
'et' => 'estonian',
'fa' => 'persian',
'fi' => 'finnish',
'fr' => 'french',
'he' => 'hebrew',
'hi' => 'hindi',
'hr' => 'croatian',
'hu' => 'hungarian',
'it' => 'italian',
'ja' => 'japanese',
'lb' => 'luxembourgish',
'lt' => 'lithuanian',
'lv' => 'latvian',
'mr' => 'marathi',
'nl' => 'dutch',
'no' => 'norwegian',
'pl' => 'polish',
'pt' => 'portuguese',
'ro' => 'romanian',
'rs' => 'serbian',
'ru' => 'russian',
'sk' => 'slovak',
'sl' => 'slovenian',
'sv' => 'swedish',
'th' => 'thai',
'tr' => 'turkish',
'zh' => 'chinese',
];
$config['language_codes'] = $languages;
$language_code = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2) : 'en';
$config['language'] =
$_GET['language'] ??
(isset($_SERVER['HTTP_ACCEPT_LANGUAGE'], $languages[$language_code])
? $languages[$language_code]
: Config::LANGUAGE);
$config['language_code'] = array_search($config['language'], $languages) ?: 'en';
/*
|--------------------------------------------------------------------------
| Available Languages
|--------------------------------------------------------------------------
|
| Each item of this array must be a directory with the translation files in
| the /application/language directory. The users will be able to select one
| of these languages.
|
*/
$config['available_languages'] = [
'albanian',
'arabic',
'bosnian',
'bulgarian',
'catalan',
'chinese',
'croatian',
'czech',
'danish',
'dutch',
'english',
'estonian',
'finnish',
'french',
'german',
'greek',
'hebrew',
'hindi',
'hungarian',
'italian',
'japanese',
'latvian',
'lithuanian',
'luxembourgish',
'marathi',
'norwegian',
'persian',
'polish',
'portuguese',
'portuguese-br',
'romanian',
'russian',
'serbian',
'slovak',
'slovenian',
'spanish',
'swedish',
'thai',
'traditional-chinese',
'turkish',
];
/*
|--------------------------------------------------------------------------
| Default Character Set
|--------------------------------------------------------------------------
|
| This determines which character set is used by default in various methods
| that require a character set to be provided.
|
*/
$config['charset'] = 'UTF-8';
/*
|--------------------------------------------------------------------------
| Enable/Disable System Hooks
|--------------------------------------------------------------------------
|
| If you would like to use the 'hooks' feature you must enable it by
| setting this variable to TRUE (boolean). See the user guide for details.
|
*/
$config['enable_hooks'] = true;
/*
|--------------------------------------------------------------------------
| Class Extension Prefix
|--------------------------------------------------------------------------
|
| This item allows you to set the filename/classname prefix when extending
| native libraries. For more information please see the user guide:
|
| http://codeigniter.com/user_guide/general/core_classes.html
| http://codeigniter.com/user_guide/general/creating_libraries.html
|
*/
$config['subclass_prefix'] = 'EA_';
/*
|--------------------------------------------------------------------------
| Allowed URL Characters
|--------------------------------------------------------------------------
|
| This lets you specify with a regular expression which characters are permitted
| within your URLs. When someone tries to submit a URL with disallowed
| characters they will get a warning message.
|
| As a security measure you are STRONGLY encouraged to restrict URLs to
| as few characters as possible. By default, only these are allowed: a-z 0-9~%.:_-
|
| Leave blank to allow all characters -- but only if you are insane.
|
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
*/
$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-';
/*
|--------------------------------------------------------------------------
| Enable Query Strings
|--------------------------------------------------------------------------
|
| By default CodeIgniter uses search-engine friendly segment based URLs:
| example.com/who/what/where/
|
| By default CodeIgniter enables access to the $_GET array. If for some
| reason you would like to disable it, set 'allow_get_array' to FALSE.
|
| You can optionally enable standard query string based URLs:
| example.com?who=me&what=something&where=here
|
| Options are: TRUE or FALSE (boolean)
|
| The other items let you set the query string 'words' that will
| invoke your controllers and its functions:
| example.com/index.php?c=controller&m=function
|
| Please note that some of the helpers won't work as expected when
| this feature is enabled, since CodeIgniter is designed primarily to
| use segment based URLs.
|
*/
$config['allow_get_array'] = true;
$config['enable_query_strings'] = false;
$config['controller_trigger'] = 'c';
$config['function_trigger'] = 'm';
$config['directory_trigger'] = 'd'; // experimental not currently in use
/*
|--------------------------------------------------------------------------
| Error Logging Threshold
|--------------------------------------------------------------------------
|
| If you have enabled error logging, you can set an error threshold to
| determine what gets logged. Threshold options are:
| You can enable error logging by setting a threshold over zero. The
| threshold determines what gets logged. Threshold options are:
|
| 0 = Disables logging, Error logging TURNED OFF
| 1 = Error Messages (including PHP errors)
| 2 = Debug Messages
| 3 = Informational Messages
| 4 = All Messages
|
| For a live site you'll usually only enable Errors (1) to be logged otherwise
| your log files will fill up very fast.
|
*/
$config['log_threshold'] = 1;
/*
|--------------------------------------------------------------------------
| Error Logging Directory Path
|--------------------------------------------------------------------------
|
| Leave this BLANK unless you would like to set something other than the default
| application/logs/ folder. Use a full server path with trailing slash.
|
*/
$config['log_path'] = __DIR__ . '/../../storage/logs/';
/*
|--------------------------------------------------------------------------
| Date Format for Logs
|--------------------------------------------------------------------------
|
| Each item that is logged has an associated date. You can use PHP date
| codes to set your own date formatting
|
*/
$config['log_date_format'] = 'Y-m-d H:i:s';
/*
|--------------------------------------------------------------------------
| Cache Directory Path
|--------------------------------------------------------------------------
|
| Leave this BLANK unless you would like to set something other than the default
| system/cache/ folder. Use a full server path with trailing slash.
|
*/
$config['cache_path'] = __DIR__ . '/../../storage/cache/';
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| If you use the Encryption class or the Session class you
| MUST set an encryption key. See the user guide for info.
|
*/
$config['encryption_key'] = base64_encode(APPPATH);
/*
|--------------------------------------------------------------------------
| Session Variables
|--------------------------------------------------------------------------
|
| 'sess_cookie_name' = the name you want for the cookie
| 'sess_expiration' = the number of SECONDS you want the session to last.
| by default sessions last 7200 seconds (two hours). Set to zero for no expiration.
| 'sess_expire_on_close' = Whether to cause the session to expire automatically
| when the browser window is closed
| 'sess_encrypt_cookie' = Whether to encrypt the cookie
| 'sess_use_database' = Whether to save the session data to a database
| 'sess_table_name' = The name of the session database table
| 'sess_match_ip' = Whether to match the user's IP address when reading the session data
| 'sess_match_useragent' = Whether to match the User Agent when reading the session data
| 'sess_time_to_update' = how many seconds between CI refreshing Session Information
|
*/
$config['sess_driver'] = 'files';
$config['sess_cookie_name'] = 'ea_session';
$config['sess_expiration'] = 7200;
$config['sess_save_path'] = __DIR__ . '/../../storage/sessions';
$config['sess_match_ip'] = false;
$config['sess_time_to_update'] = 300;
$config['sess_regenerate_destroy'] = true;
/*
|--------------------------------------------------------------------------
| Cookie Related Variables
|--------------------------------------------------------------------------
|
| 'cookie_prefix' = Set a prefix if you need to avoid collisions
| 'cookie_domain' = Set to .your-domain.com for site-wide cookies
| 'cookie_path' = Typically will be a forward slash
| 'cookie_secure' = Cookies will only be set if a secure HTTPS connection exists.
|
*/
$config['cookie_prefix'] = '';
$config['cookie_domain'] = '';
$config['cookie_path'] = '/';
$config['cookie_secure'] = strpos($config['base_url'], 'https') !== false;
/*
|--------------------------------------------------------------------------
| Cross Site Request Forgery
|--------------------------------------------------------------------------
| Enables a CSRF cookie token to be set. When set to TRUE, token will be
| checked on a submitted form. If you are accepting user data, it is strongly
| recommended CSRF protection be enabled.
|
| 'csrf_token_name' = The token name
| 'csrf_cookie_name' = The cookie name
| 'csrf_expire' = The number in seconds the token should expire.
*/
$config['csrf_protection'] = true;
$config['csrf_token_name'] = 'csrf_token';
$config['csrf_cookie_name'] = 'csrf_cookie';
$config['csrf_expire'] = 7200;
$config['csrf_exclude_uris'] = ['api/v1/.*', 'booking/.*', 'booking_cancellation/.*', 'booking_confirmation/.*'];
/*
|--------------------------------------------------------------------------
| Output Compression
|--------------------------------------------------------------------------
|
| Enables Gzip output compression for faster page loads. When enabled,
| the output class will test whether your server supports Gzip.
| Even if it does, however, not all browsers support compression
| so enable only if you are reasonably sure your visitors can handle it.
|
| VERY IMPORTANT: If you are getting a blank page when compression is enabled it
| means you are prematurely outputting something to your browser. It could
| even be a line of whitespace at the end of one of your scripts. For
| compression to work, nothing can be sent before the output buffer is called
| by the output class. Do not 'echo' any values with compression enabled.
|
*/
$config['compress_output'] = false;
/*
|--------------------------------------------------------------------------
| Master Time Reference
|--------------------------------------------------------------------------
|
| Options are 'local' or 'gmt'. This pref tells the system whether to use
| your server's local time as the master 'now' reference, or convert it to
| GMT. See the 'date helper' page of the user guide for information
| regarding date handling.
|
*/
$config['time_reference'] = 'local';
/*
|--------------------------------------------------------------------------
| Rewrite PHP Short Tags
|--------------------------------------------------------------------------
|
| If your PHP installation does not have short tag support enabled CI
| can rewrite the tags on-the-fly, enabling you to utilize that syntax
| in your view files. Options are TRUE or FALSE (boolean)
|
*/
$config['rewrite_short_tags'] = false;
/*
|--------------------------------------------------------------------------
| Reverse Proxy IPs
|--------------------------------------------------------------------------
|
| If your server is behind a reverse proxy, you must whitelist the proxy IP
| addresses from which CodeIgniter should trust the HTTP_X_FORWARDED_FOR
| header in order to properly identify the visitor's IP address.
| Comma-delimited, e.g. '10.0.1.200,10.0.1.201'
|
*/
$config['proxy_ips'] = '';
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| Toggle the rate limiting feature in your application. Using rate limiting
| will control the number of requests a client can send to the app.
|
*/
$config['rate_limiting'] = true;
/* End of file config.php */
/* Location: ./application/config/config.php */

View File

@ -0,0 +1,157 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
|--------------------------------------------------------------------------
| File and Directory Modes
|--------------------------------------------------------------------------
|
| These prefs are used when checking and setting modes when working
| with the file system. The defaults are fine on servers with proper
| security, but you may wish (or even need) to change the values in
| certain environments (Apache running a separate process for each
| user, PHP under CGI with Apache suEXEC, etc.). Octal values should
| always be used to set the mode correctly.
|
*/
const FILE_READ_MODE = 0644;
const FILE_WRITE_MODE = 0666;
const DIR_READ_MODE = 0755;
const DIR_WRITE_MODE = 0777;
/*
|--------------------------------------------------------------------------
| File Stream Modes
|--------------------------------------------------------------------------
|
| These modes are used when working with fopen()/popen()
|
*/
const FOPEN_READ = 'rb';
const FOPEN_READ_WRITE = 'r+b';
const FOPEN_WRITE_CREATE_DESTRUCTIVE = 'wb'; // truncates existing file data, use with care
const FOPEN_READ_WRITE_CREATE_DESTRUCTIVE = 'w+b'; // truncates existing file data, use with care
const FOPEN_WRITE_CREATE = 'ab';
const FOPEN_READ_WRITE_CREATE = 'a+b';
const FOPEN_WRITE_CREATE_STRICT = 'xb';
const FOPEN_READ_WRITE_CREATE_STRICT = 'x+b';
/*
|--------------------------------------------------------------------------
| Application Data
|--------------------------------------------------------------------------
|
| These constants are used globally from the application when handling data.
|
*/
const DB_SLUG_CUSTOMER = 'customer';
const DB_SLUG_PROVIDER = 'provider';
const DB_SLUG_ADMIN = 'admin';
const DB_SLUG_SECRETARY = 'secretary';
const FILTER_TYPE_ALL = 'all';
const FILTER_TYPE_PROVIDER = 'provider';
const FILTER_TYPE_SERVICE = 'service';
const AJAX_SUCCESS = 'SUCCESS';
const AJAX_FAILURE = 'FAILURE';
const SETTINGS_SYSTEM = 'SETTINGS_SYSTEM';
const SETTINGS_USER = 'SETTINGS_USER';
const PRIV_VIEW = 1;
const PRIV_ADD = 2;
const PRIV_EDIT = 4;
const PRIV_DELETE = 8;
const PRIV_APPOINTMENTS = 'appointments';
const PRIV_CUSTOMERS = 'customers';
const PRIV_SERVICES = 'services';
const PRIV_USERS = 'users';
const PRIV_SYSTEM_SETTINGS = 'system_settings';
const PRIV_USER_SETTINGS = 'user_settings';
const PRIV_WEBHOOKS = 'webhooks';
const PRIV_BLOCKED_PERIODS = 'blocked_periods';
const DATE_FORMAT_DMY = 'DMY';
const DATE_FORMAT_MDY = 'MDY';
const DATE_FORMAT_YMD = 'YMD';
const TIME_FORMAT_REGULAR = 'regular';
const TIME_FORMAT_MILITARY = 'military';
const MIN_PASSWORD_LENGTH = 7;
const MAX_PASSWORD_LENGTH = 100;
const ANY_PROVIDER = 'any-provider';
const CALENDAR_VIEW_DEFAULT = 'default';
const CALENDAR_VIEW_TABLE = 'table';
const AVAILABILITIES_TYPE_FLEXIBLE = 'flexible';
const AVAILABILITIES_TYPE_FIXED = 'fixed';
const EVENT_MINIMUM_DURATION = 5; // Minutes
const DEFAULT_COMPANY_COLOR = '#ffffff';
const LDAP_DEFAULT_FILTER = '(&(objectClass=*)(|(cn={{KEYWORD}})(sn={{KEYWORD}})(mail={{KEYWORD}})(givenName={{KEYWORD}})(uid={{KEYWORD}})))';
const LDAP_WHITELISTED_ATTRIBUTES = [
'givenname',
'cn',
'dn',
'sn',
'mail',
'telephonenumber',
'description',
'member',
'objectclass',
'objectcategory',
'instancetype',
'whencreated',
'name',
'samaccountname',
'samaccounttype',
'objectcategory',
'memberof',
'distinguishedname',
];
const LDAP_DEFAULT_FIELD_MAPPING = [
'first_name' => 'givenname',
'last_name' => 'sn',
'email' => 'mail',
'phone_number' => 'telephonenumber',
'username' => 'cn',
];
/*
|--------------------------------------------------------------------------
| Webhook Actions
|--------------------------------------------------------------------------
|
| External application endpoints can subscribe to these webhook actions.
|
*/
const WEBHOOK_APPOINTMENT_SAVE = 'appointment_save';
const WEBHOOK_APPOINTMENT_DELETE = 'appointment_delete';
const WEBHOOK_UNAVAILABILITY_SAVE = 'unavailability_save';
const WEBHOOK_UNAVAILABILITY_DELETE = 'unavailability_delete';
const WEBHOOK_CUSTOMER_SAVE = 'customer_save';
const WEBHOOK_CUSTOMER_DELETE = 'customer_delete';
const WEBHOOK_SERVICE_SAVE = 'service_save';
const WEBHOOK_SERVICE_DELETE = 'service_delete';
const WEBHOOK_SERVICE_CATEGORY_SAVE = 'service_category_save';
const WEBHOOK_SERVICE_CATEGORY_DELETE = 'service_category_delete';
const WEBHOOK_PROVIDER_SAVE = 'provider_save';
const WEBHOOK_PROVIDER_DELETE = 'provider_delete';
const WEBHOOK_SECRETARY_SAVE = 'secretary_save';
const WEBHOOK_SECRETARY_DELETE = 'secretary_delete';
const WEBHOOK_ADMIN_SAVE = 'admin_save';
const WEBHOOK_ADMIN_DELETE = 'admin_delete';
const WEBHOOK_BLOCKED_PERIOD_SAVE = 'blocked_period_save';
const WEBHOOK_BLOCKED_PERIOD_DELETE = 'blocked_period_delete';
/* End of file constants.php */
/* Location: ./application/config/constants.php */

View File

@ -0,0 +1,69 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
| -------------------------------------------------------------------
| DATABASE CONNECTIVITY SETTINGS
| -------------------------------------------------------------------
| This file will contain the settings needed to access your database.
|
| For complete instructions please consult the 'Database Connection'
| page of the User Guide.
|
| -------------------------------------------------------------------
| EXPLANATION OF VARIABLES
| -------------------------------------------------------------------
|
| ['hostname'] The hostname of your database server.
| ['username'] The username used to connect to the database
| ['password'] The password used to connect to the database
| ['database'] The name of the database you want to connect to
| ['dbdriver'] The database type. ie: mysql. Currently supported:
mysql, mysqli, postgre, odbc, mssql, sqlite, oci8
| ['dbprefix'] You can add an optional prefix, which will be added
| to the table name when using the Active Record class
| ['pconnect'] TRUE/FALSE - Whether to use a persistent connection
| ['db_debug'] TRUE/FALSE - Whether database errors should be displayed.
| ['cache_on'] TRUE/FALSE - Enables/disables query caching
| ['cachedir'] The path to the folder where cache files should be stored
| ['char_set'] The character set used in communicating with the database
| ['dbcollat'] The character collation used in communicating with the database
| NOTE: For MySQL and MySQLi databases, this setting is only used
| as a backup if your server is running PHP < 5.2.3 or MySQL < 5.0.7
| (and in table creation queries made with DB Forge).
| There is an incompatibility in PHP with mysql_real_escape_string() which
| can make your site vulnerable to SQL injection if you are using a
| multi-byte character set and are running versions lower than these.
| Sites using Latin-1 or UTF-8 database character set and collation are unaffected.
| ['swap_pre'] A default table prefix that should be swapped with the dbprefix
| ['autoinit'] Whether to automatically initialize the database.
| ['stricton'] TRUE/FALSE - forces 'Strict Mode' connections
| - good for ensuring strict SQL while developing
|
| The $active_group variable lets you choose which connection group to
| make active. By default there is only one group (the 'default' group).
|
| The $active_record variables lets you determine whether or not to load
| the active record class
*/
$active_group = 'default';
$query_builder = true;
$db['default']['hostname'] = Config::DB_HOST;
$db['default']['username'] = Config::DB_USERNAME;
$db['default']['password'] = Config::DB_PASSWORD;
$db['default']['database'] = Config::DB_NAME;
$db['default']['dbdriver'] = 'mysqli';
$db['default']['dbprefix'] = 'ea_';
$db['default']['pconnect'] = true;
$db['default']['db_debug'] = true;
$db['default']['cache_on'] = false;
$db['default']['cachedir'] = '';
$db['default']['char_set'] = 'utf8mb4';
$db['default']['dbcollat'] = 'utf8mb4_unicode_ci';
$db['default']['swap_pre'] = '';
$db['default']['autoinit'] = true;
$db['default']['stricton'] = false;
/* End of file database.php */
/* Location: ./application/config/database.php */

View File

@ -0,0 +1,21 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
// Add custom values by settings them to the $config array.
// Example: $config['smtp_host'] = 'smtp.gmail.com';
// @link https://codeigniter.com/user_guide/libraries/email.html
$config['useragent'] = 'Easy!Appointments';
$config['protocol'] = 'mail'; // or 'smtp'
$config['mailtype'] = 'html'; // or 'text'
// $config['smtp_debug'] = '0'; // or '1'
// $config['smtp_auth'] = TRUE; //or FALSE for anonymous relay.
// $config['smtp_host'] = '';
// $config['smtp_user'] = '';
// $config['smtp_pass'] = '';
// $config['smtp_crypto'] = 'ssl'; // or 'tls'
// $config['smtp_port'] = 25;
// $config['from_name'] = '';
// $config['from_address'] = '';
// $config['reply_to'] = '';
$config['crlf'] = "\r\n";
$config['newline'] = "\r\n";

View File

@ -0,0 +1,17 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
|--------------------------------------------------------------------------
| Google Calendar - Internal Configuration
|--------------------------------------------------------------------------
|
| Declare some of the global config values of the Google Calendar
| synchronization feature.
|
*/
$config['google_sync_feature'] = Config::GOOGLE_SYNC_FEATURE;
$config['google_client_id'] = Config::GOOGLE_CLIENT_ID;
$config['google_client_secret'] = Config::GOOGLE_CLIENT_SECRET;

View File

@ -0,0 +1,15 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
| -------------------------------------------------------------------------
| Hooks
| -------------------------------------------------------------------------
| This file lets you define "hooks" to extend CI without hacking the core
| files. Please see the user guide for info:
|
| http://codeigniter.com/user_guide/general/hooks.html
|
*/
/* End of file hooks.php */
/* Location: ./application/config/hooks.php */

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>

View File

@ -0,0 +1,39 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
|--------------------------------------------------------------------------
| Enable/Disable Migrations
|--------------------------------------------------------------------------
|
| Migrations are disabled by default but should be enabled
| whenever you intend to do a schema migration.
|
*/
$config['migration_enabled'] = true;
/*
|--------------------------------------------------------------------------
| Migrations version
|--------------------------------------------------------------------------
|
| This is used to set migration version that the file system should be on.
| If you run $this->migration->latest() this is the version that schema will
| be upgraded / downgraded to.
|
*/
$config['migration_version'] = 0;
/*
|--------------------------------------------------------------------------
| Migrations Path
|--------------------------------------------------------------------------
|
| Path to your "migrations" folder.
| Typically, it will be within your application path.
| Also, writing permission is required within the "migrations" path.
|
*/
$config['migration_path'] = APPPATH . 'migrations/';
/* End of file migration.php */
/* Location: ./application/config/migration.php */

View File

@ -0,0 +1,294 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/*
| -------------------------------------------------------------------
| MIME TYPES
| -------------------------------------------------------------------
| This file contains an array of mime types. It is used by the
| Upload class to help identify allowed file types.
|
*/
return [
'hqx' => [
'application/mac-binhex40',
'application/mac-binhex',
'application/x-binhex40',
'application/x-mac-binhex40',
],
'cpt' => 'application/mac-compactpro',
'csv' => [
'text/x-comma-separated-values',
'text/comma-separated-values',
'application/octet-stream',
'application/vnd.ms-excel',
'application/x-csv',
'text/x-csv',
'text/csv',
'application/csv',
'application/excel',
'application/vnd.msexcel',
'text/plain',
],
'bin' => [
'application/macbinary',
'application/mac-binary',
'application/octet-stream',
'application/x-binary',
'application/x-macbinary',
],
'dms' => 'application/octet-stream',
'lha' => 'application/octet-stream',
'lzh' => 'application/octet-stream',
'exe' => ['application/octet-stream', 'application/x-msdownload'],
'class' => 'application/octet-stream',
'psd' => ['application/x-photoshop', 'image/vnd.adobe.photoshop'],
'so' => 'application/octet-stream',
'sea' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'oda' => 'application/oda',
'pdf' => ['application/pdf', 'application/force-download', 'application/x-download', 'binary/octet-stream'],
'ai' => ['application/pdf', 'application/postscript'],
'eps' => 'application/postscript',
'ps' => 'application/postscript',
'smi' => 'application/smil',
'smil' => 'application/smil',
'mif' => 'application/vnd.mif',
'xls' => [
'application/vnd.ms-excel',
'application/msexcel',
'application/x-msexcel',
'application/x-ms-excel',
'application/x-excel',
'application/x-dos_ms_excel',
'application/xls',
'application/x-xls',
'application/excel',
'application/download',
'application/vnd.ms-office',
'application/msword',
],
'ppt' => [
'application/powerpoint',
'application/vnd.ms-powerpoint',
'application/vnd.ms-office',
'application/msword',
],
'pptx' => [
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/x-zip',
'application/zip',
],
'wbxml' => 'application/wbxml',
'wmlc' => 'application/wmlc',
'dcr' => 'application/x-director',
'dir' => 'application/x-director',
'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar',
'gz' => 'application/x-gzip',
'gzip' => 'application/x-gzip',
'php' => [
'application/x-httpd-php',
'application/php',
'application/x-php',
'text/php',
'text/x-php',
'application/x-httpd-php-source',
],
'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source',
'js' => ['application/x-javascript', 'text/plain'],
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar',
'tgz' => ['application/x-tar', 'application/x-gzip-compressed'],
'z' => 'application/x-compress',
'xhtml' => 'application/xhtml+xml',
'xht' => 'application/xhtml+xml',
'zip' => [
'application/x-zip',
'application/zip',
'application/x-zip-compressed',
'application/s-compressed',
'multipart/x-zip',
],
'rar' => ['application/x-rar', 'application/rar', 'application/x-rar-compressed'],
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mpga' => 'audio/mpeg',
'mp2' => 'audio/mpeg',
'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'],
'aif' => ['audio/x-aiff', 'audio/aiff'],
'aiff' => ['audio/x-aiff', 'audio/aiff'],
'aifc' => 'audio/x-aiff',
'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio',
'rv' => 'video/vnd.rn-realvideo',
'wav' => ['audio/x-wav', 'audio/wave', 'audio/wav'],
'bmp' => [
'image/bmp',
'image/x-bmp',
'image/x-bitmap',
'image/x-xbitmap',
'image/x-win-bitmap',
'image/x-windows-bmp',
'image/ms-bmp',
'image/x-ms-bmp',
'application/bmp',
'application/x-bmp',
'application/x-win-bitmap',
],
'gif' => 'image/gif',
'jpeg' => ['image/jpeg', 'image/pjpeg'],
'jpg' => ['image/jpeg', 'image/pjpeg'],
'jpe' => ['image/jpeg', 'image/pjpeg'],
'jp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'j2k' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpf' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpg2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpx' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpm' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mj2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mjp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'png' => ['image/png', 'image/x-png'],
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'css' => ['text/css', 'text/plain'],
'html' => ['text/html', 'text/plain'],
'htm' => ['text/html', 'text/plain'],
'shtml' => ['text/html', 'text/plain'],
'txt' => 'text/plain',
'text' => 'text/plain',
'log' => ['text/plain', 'text/x-log'],
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
'xml' => ['application/xml', 'text/xml', 'text/plain'],
'xsl' => ['application/xml', 'text/xsl', 'text/xml'],
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'avi' => ['video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'],
'movie' => 'video/x-sgi-movie',
'doc' => ['application/msword', 'application/vnd.ms-office'],
'docx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
'application/msword',
'application/x-zip',
],
'dot' => ['application/msword', 'application/vnd.ms-office'],
'dotx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
'application/msword',
],
'xlsx' => [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/zip',
'application/vnd.ms-excel',
'application/msword',
'application/x-zip',
],
'word' => ['application/msword', 'application/octet-stream'],
'xl' => 'application/excel',
'eml' => 'message/rfc822',
'json' => ['application/json', 'text/json'],
'pem' => ['application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'],
'p10' => ['application/x-pkcs10', 'application/pkcs10'],
'p12' => 'application/x-pkcs12',
'p7a' => 'application/x-pkcs7-signature',
'p7c' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7m' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature',
'crt' => ['application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'],
'crl' => ['application/pkix-crl', 'application/pkcs-crl'],
'der' => 'application/x-x509-ca-cert',
'kdb' => 'application/octet-stream',
'pgp' => 'application/pgp',
'gpg' => 'application/gpg-keys',
'sst' => 'application/octet-stream',
'csr' => 'application/octet-stream',
'rsa' => 'application/x-pkcs7',
'cer' => ['application/pkix-cert', 'application/x-x509-ca-cert'],
'3g2' => 'video/3gpp2',
'3gp' => ['video/3gp', 'video/3gpp'],
'mp4' => 'video/mp4',
'm4a' => 'audio/x-m4a',
'f4v' => ['video/mp4', 'video/x-f4v'],
'flv' => 'video/x-flv',
'webm' => 'video/webm',
'aac' => 'audio/x-acc',
'm4u' => 'application/vnd.mpegurl',
'm3u' => 'text/plain',
'xspf' => 'application/xspf+xml',
'vlc' => 'application/videolan',
'wmv' => ['video/x-ms-wmv', 'video/x-ms-asf'],
'au' => 'audio/x-au',
'ac3' => 'audio/ac3',
'flac' => 'audio/x-flac',
'ogg' => ['audio/ogg', 'video/ogg', 'application/ogg'],
'kmz' => ['application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'],
'kml' => ['application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'],
'ics' => 'text/calendar',
'ical' => 'text/calendar',
'zsh' => 'text/x-scriptzsh',
'7z' => [
'application/x-7z-compressed',
'application/x-compressed',
'application/x-zip-compressed',
'application/zip',
'multipart/x-zip',
],
'7zip' => [
'application/x-7z-compressed',
'application/x-compressed',
'application/x-zip-compressed',
'application/zip',
'multipart/x-zip',
],
'cdr' => [
'application/cdr',
'application/coreldraw',
'application/x-cdr',
'application/x-coreldraw',
'image/cdr',
'image/x-cdr',
'zz-application/zz-winassoc-cdr',
],
'wma' => ['audio/x-ms-wma', 'video/x-ms-asf'],
'jar' => [
'application/java-archive',
'application/x-java-application',
'application/x-jar',
'application/x-compressed',
],
'svg' => ['image/svg+xml', 'application/xml', 'text/xml'],
'vcf' => 'text/x-vcard',
'srt' => ['text/srt', 'text/plain'],
'vtt' => ['text/vtt', 'text/plain'],
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
'odc' => 'application/vnd.oasis.opendocument.chart',
'otc' => 'application/vnd.oasis.opendocument.chart-template',
'odf' => 'application/vnd.oasis.opendocument.formula',
'otf' => 'application/vnd.oasis.opendocument.formula-template',
'odg' => 'application/vnd.oasis.opendocument.graphics',
'otg' => 'application/vnd.oasis.opendocument.graphics-template',
'odi' => 'application/vnd.oasis.opendocument.image',
'oti' => 'application/vnd.oasis.opendocument.image-template',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'otp' => 'application/vnd.oasis.opendocument.presentation-template',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template',
'odt' => 'application/vnd.oasis.opendocument.text',
'odm' => 'application/vnd.oasis.opendocument.text-master',
'ott' => 'application/vnd.oasis.opendocument.text-template',
'oth' => 'application/vnd.oasis.opendocument.text-web',
];

View File

@ -0,0 +1,16 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
| -------------------------------------------------------------------------
| Profiler Sections
| -------------------------------------------------------------------------
| This file lets you determine whether or not various sections of Profiler
| data are displayed when the Profiler is enabled.
| Please see the user guide for info:
|
| http://codeigniter.com/user_guide/general/profiling.html
|
*/
/* End of file profiler.php */
/* Location: ./application/config/profiler.php */

View File

@ -0,0 +1,163 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
| -------------------------------------------------------------------------
| URI ROUTING
| -------------------------------------------------------------------------
| This file lets you re-map URI requests to specific controller functions.
|
| Typically there is a one-to-one relationship between a URL string
| and its corresponding controller class/method. The segments in a
| URL normally follow this pattern:
|
| example.com/class/method/id/
|
| In some instances, however, you may want to remap this relationship
| so that a different class/function is called than the one
| corresponding to the URL.
|
| Please see the user guide for complete details:
|
| https://codeigniter.com/userguide3/general/routing.html
|
| -------------------------------------------------------------------------
| RESERVED ROUTES
| -------------------------------------------------------------------------
|
| There are three reserved routes:
|
| $route['default_controller'] = 'welcome';
|
| This route indicates which controller class should be loaded if the
| URI contains no data. In the above example, the "welcome" class
| would be loaded.
|
| $route['404_override'] = 'errors/page_missing';
|
| This route will tell the Router which controller/method to use if those
| provided in the URL cannot be matched to a valid route.
|
| $route['translate_uri_dashes'] = FALSE;
|
| This is not exactly a route, but allows you to automatically route
| controller and method names that contain dashes. '-' isn't a valid
| class or method name character, so it requires translation.
| When you set this option to TRUE, it will replace ALL dashes with
| underscores in the controller and method URI segments.
|
| Examples: my-controller/index -> my_controller/index
| my-controller/my-method -> my_controller/my_method
*/
require_once __DIR__ . '/../helpers/routes_helper.php';
$route['default_controller'] = 'booking';
$route['404_override'] = '';
$route['translate_uri_dashes'] = false;
/*
| -------------------------------------------------------------------------
| FRAME OPTIONS HEADERS
| -------------------------------------------------------------------------
| Set the appropriate headers so that iframe control and permissions are
| properly configured.
|
| Enable this if you want to disable use of Easy!Appointments within an
| iframe.
|
| Options:
|
| - DENY
| - SAMEORIGIN
|
*/
// header('X-Frame-Options: SAMEORIGIN');
/*
| -------------------------------------------------------------------------
| CORS HEADERS
| -------------------------------------------------------------------------
| Set the appropriate headers so that CORS requirements are met and any
| incoming preflight options request succeeds.
|
*/
header('Access-Control-Allow-Origin: ' . ($_SERVER['HTTP_ORIGIN'] ?? '*')); // NOTICE: Change this header to restrict CORS access.
header('Access-Control-Allow-Credentials: "true"');
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
// May also be using PUT, PATCH, HEAD etc
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD');
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header('Access-Control-Allow-Headers: ' . $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
}
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
/*
| -------------------------------------------------------------------------
| REST API ROUTING
| -------------------------------------------------------------------------
| Define the API resource routes using the routing helper function. By
| default, each resource will have by default the following actions:
|
| - index [GET]
|
| - show/:id [GET]
|
| - store [POST]
|
| - update [PUT]
|
| - destroy [DELETE]
|
| Some resources like the availabilities and the settings do not follow this
| pattern and are explicitly defined.
|
*/
route_api_resource($route, 'appointments', 'api/v1/');
route_api_resource($route, 'admins', 'api/v1/');
route_api_resource($route, 'service_categories', 'api/v1/');
route_api_resource($route, 'customers', 'api/v1/');
route_api_resource($route, 'providers', 'api/v1/');
route_api_resource($route, 'secretaries', 'api/v1/');
route_api_resource($route, 'services', 'api/v1/');
route_api_resource($route, 'unavailabilities', 'api/v1/');
route_api_resource($route, 'webhooks', 'api/v1/');
$route['api/v1/settings']['get'] = 'api/v1/settings_api_v1/index';
$route['api/v1/settings/(:any)']['get'] = 'api/v1/settings_api_v1/show/$1';
$route['api/v1/settings/(:any)']['put'] = 'api/v1/settings_api_v1/update/$1';
$route['api/v1/availabilities']['get'] = 'api/v1/availabilities_api_v1/get';
/*
| -------------------------------------------------------------------------
| CUSTOM ROUTING
| -------------------------------------------------------------------------
| You can add custom routes to the following section to define URL patterns
| that are later mapped to the available controllers in the filesystem.
|
*/
/* End of file routes.php */
/* Location: ./application/config/routes.php */

View File

@ -0,0 +1,19 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/*
| -------------------------------------------------------------------------
| TESTING ROUTES
| -------------------------------------------------------------------------
| The following routes are defined in order for CI to be able to process
| test execution requests via the CLI.
|
| The Test controller class is used as a placeholder for this purpose.
|
*/
$route['default_controller'] = 'test/index';
$route['404_override'] = 'test/index'; // when in doubt, use the hammer
$route['translate_uri_dashes'] = FALSE;
/* End of file routes.php */
/* Location: ./application/config/testing/routes.php */

View File

@ -0,0 +1,79 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* About controller.
*
* Handles about settings related operations.
*
* @package Controllers
*/
class About extends EA_Controller
{
/**
* About constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('customers_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
$this->load->model('settings_model');
$this->load->library('accounts');
$this->load->library('google_sync');
$this->load->library('notifications');
$this->load->library('synchronization');
$this->load->library('timezones');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('about')]);
$user_id = session('user_id');
if (cannot('view', PRIV_USER_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
]);
html_vars([
'page_title' => lang('settings'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
]);
$this->load->view('pages/about');
}
}

View File

@ -0,0 +1,166 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Account controller.
*
* Handles current account related operations.
*
* @package Controllers
*/
class Account extends EA_Controller
{
public array $allowed_user_fields = [
'id',
'first_name',
'last_name',
'email',
'mobile_number',
'phone_number',
'address',
'city',
'state',
'zip_code',
'notes',
'timezone',
'language',
'settings',
];
public array $optional_user_fields = [
//
];
public array $allowed_user_setting_fields = ['username', 'password', 'notifications', 'calendar_view'];
public array $optional_user_setting_fields = [
//
];
/**
* Account constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('customers_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
$this->load->model('settings_model');
$this->load->library('accounts');
$this->load->library('google_sync');
$this->load->library('notifications');
$this->load->library('synchronization');
$this->load->library('timezones');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('account')]);
$user_id = session('user_id');
if (cannot('view', PRIV_USER_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$account = $this->users_model->find($user_id);
script_vars([
'account' => $account,
]);
html_vars([
'page_title' => lang('settings'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'grouped_timezones' => $this->timezones->to_grouped_array(),
]);
$this->load->view('pages/account');
}
/**
* Save general settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_USER_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$account = request('account');
$account['id'] = session('user_id');
$this->users_model->only($account, $this->allowed_user_fields);
$this->users_model->optional($account, $this->optional_user_fields);
$this->users_model->only($account['settings'], $this->allowed_user_setting_fields);
$this->users_model->optional($account['settings'], $this->optional_user_setting_fields);
if (empty($account['password'])) {
unset($account['password']);
}
$this->users_model->save($account);
session([
'user_email' => $account['email'],
'username' => $account['settings']['username'],
'timezone' => $account['timezone'],
'language' => $account['language'],
]);
response();
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Make sure the username is valid and unique in the database.
*/
public function validate_username(): void
{
try {
$username = request('username');
$user_id = request('user_id');
$is_valid = $this->users_model->validate_username($username, $user_id);
json_response([
'is_valid' => $is_valid,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,247 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Admins controller.
*
* Handles the admins related operations.
*
* @package Controllers
*/
class Admins extends EA_Controller
{
public array $allowed_admin_fields = [
'id',
'first_name',
'last_name',
'email',
'mobile_number',
'phone_number',
'address',
'city',
'state',
'zip_code',
'notes',
'timezone',
'language',
'ldap_dn',
'settings',
];
public array $optional_admin_fields = [
//
];
public array $allowed_admin_setting_fields = ['username', 'password', 'notifications', 'calendar_view'];
public array $optional_admin_setting_fields = [
//
];
/**
* Admins constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('admins_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Render the backend admins page.
*
* On this page admin users will be able to manage admins, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('admins')]);
$user_id = session('user_id');
if (cannot('view', PRIV_USERS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'timezones' => $this->timezones->to_array(),
'min_password_length' => MIN_PASSWORD_LENGTH,
'default_language' => setting('default_language'),
'default_timezone' => setting('default_timezone'),
]);
html_vars([
'page_title' => lang('admins'),
'active_menu' => PRIV_USERS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'grouped_timezones' => $this->timezones->to_grouped_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
]);
$this->load->view('pages/admins');
}
/**
* Filter admins by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$admins = $this->admins_model->search($keyword, $limit, $offset, $order_by);
json_response($admins);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new admin.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$admin = request('admin');
$this->admins_model->only($admin, $this->allowed_admin_fields);
$this->admins_model->optional($admin, $this->optional_admin_fields);
$this->admins_model->only($admin['settings'], $this->allowed_admin_setting_fields);
$this->admins_model->optional($admin['settings'], $this->optional_admin_setting_fields);
$admin_id = $this->admins_model->save($admin);
$admin = $this->admins_model->find($admin_id);
$this->webhooks_client->trigger(WEBHOOK_ADMIN_SAVE, $admin);
json_response([
'success' => true,
'id' => $admin_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find an admin.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$admin_id = request('admin_id');
$admin = $this->admins_model->find($admin_id);
json_response($admin);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update an admin.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$admin = request('admin');
$this->admins_model->only($admin, $this->allowed_admin_fields);
$this->admins_model->only($admin['settings'], $this->allowed_admin_setting_fields);
$admin_id = $this->admins_model->save($admin);
$admin = $this->admins_model->find($admin_id);
$this->webhooks_client->trigger(WEBHOOK_ADMIN_SAVE, $admin);
json_response([
'success' => true,
'id' => $admin_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove an admin.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$admin_id = request('admin_id');
$admin = $this->admins_model->find($admin_id);
$this->admins_model->delete($admin_id);
$this->webhooks_client->trigger(WEBHOOK_ADMIN_DELETE, $admin);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,102 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* API settings controller.
*
* Handles API settings related operations.
*
* @package Controllers
*/
class Api_settings extends EA_Controller
{
/**
* Api_settings constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('settings_model');
$this->load->library('accounts');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('api_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'api_settings' => $this->settings_model->get('name like "api_%"'),
]);
html_vars([
'page_title' => lang('api'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
]);
$this->load->view('pages/api_settings');
}
/**
* Save general settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('api_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,200 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Appointments controller.
*
* Handles the appointments related operations.
*
* Notice: This file used to have the booking page related code which since v1.5 has now moved to the Booking.php
* controller for improved consistency.
*
* @package Controllers
*/
class Appointments extends EA_Controller
{
public array $allowed_appointment_fields = [
'id',
'start_datetime',
'end_datetime',
'location',
'notes',
'color',
'status',
'is_unavailability',
'id_users_provider',
'id_users_customer',
'id_services',
];
public array $optional_appointment_fields = [
//
];
/**
* Appointments constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Support backwards compatibility for appointment links that still point to this URL.
*
* @param string $appointment_hash
*
* @deprecated Since 1.5
*/
public function index(string $appointment_hash = ''): void
{
redirect('booking/' . $appointment_hash);
}
/**
* Filter appointments by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$appointments = $this->appointments_model->search($keyword, $limit, $offset, $order_by);
json_response($appointments);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new appointment.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$appointment = json_decode(request('appointment'), true);
$this->appointments_model->only($appointment, $this->allowed_appointment_fields);
$this->appointments_model->optional($appointment, $this->optional_appointment_fields);
$appointment_id = $this->appointments_model->save($appointment);
$appointment = $this->appointments_model->find($appointment);
$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_SAVE, $appointment);
json_response([
'success' => true,
'id' => $appointment_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find an appointment.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$appointment_id = request('appointment_id');
$appointment = $this->appointments_model->find($appointment_id);
json_response($appointment);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a appointment.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$appointment = json_decode(request('appointment'), true);
$this->appointments_model->only($appointment, $this->allowed_appointment_fields);
$this->appointments_model->optional($appointment, $this->optional_appointment_fields);
$appointment_id = $this->appointments_model->save($appointment);
json_response([
'success' => true,
'id' => $appointment_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a appointment.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$appointment_id = request('appointment_id');
$appointment = $this->appointments_model->find($appointment_id);
$this->appointments_model->delete($appointment_id);
$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_DELETE, $appointment);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,103 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/*
|------------------------------------------------------------------------------
| Deprecation Notice
|------------------------------------------------------------------------------
|
| This file is still in the project for backwards compatibility reasons and for
| providing additional information on how to migrate your code to the latest
| codebase state.
|
| Visit the Easy!Appointments Developers website for more information:
|
| https://developers.easyappointments.org
|
| Since v1.5, the methods of this controller were ported to standalone controller
| classes, that can both handle the page rendering and all asynchronous HTTP
| requests.
|
*/
/**
* Backend controller.
*
* Handles the backend related operations.
*
* @package Controllers
*
* @deprecated Since 1.5
*/
class Backend extends EA_Controller
{
/**
* Display the calendar page.
*
* @param string $appointment_hash Appointment edit dialog will appear when the page loads (default '').
*/
public function index(string $appointment_hash = ''): void
{
if (empty($appointment_hash)) {
redirect('calendar');
} else {
redirect('calendar/reschedule/' . $appointment_hash);
}
}
/**
* Display the customers page.
*/
public function customers(): void
{
redirect('customers');
}
/**
* Display the services page.
*/
public function services(): void
{
redirect('services');
}
/**
* Display the users page.
*
* Notice: Since the "users" page is split into multiple pages (providers, secretaries, admins), this method will
* redirect to "providers" page by default
*/
public function users(): void
{
redirect('providers');
}
/**
* Display settings page.
*
* Notice: Since the "settings" page is split into multiple pages (general, business, booking etc.), this method will
* redirect to "general" page by default.
*/
public function settings(): void
{
redirect('general_settings');
}
/**
* Display the update page.
*/
public function update(): void
{
redirect('update');
}
}

View File

@ -0,0 +1,307 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/*
|------------------------------------------------------------------------------
| Deprecation Notice
|------------------------------------------------------------------------------
|
| This file is still in the project for backwards compatibility reasons and for
| providing additional information on how to migrate your code to the latest
| codebase state.
|
| Visit the Easy!Appointments Developers website for more information:
|
| https://developers.easyappointments.org
|
| Since v1.5, the methods of this controller were ported to standalone controller
| classes, that can both handle the page rendering and all asynchronous HTTP
| requests.
|
*/
/**
* Backend API controller.
*
* Handles the backend API related operations.
*
* @package Controllers
*
* @deprecated Since 1.5
*/
class Backend_api extends EA_Controller
{
/**
* Get Calendar Events
*/
public function ajax_get_calendar_events(): void
{
redirect('calendar/get_calendar_appointments_for_table_view');
}
/**
* Get the registered appointments for the given date period and record.
*/
public function ajax_get_calendar_appointments(): void
{
redirect('calendar/get_calendar_appointments');
}
/**
* Save appointment changes that are made from the backend calendar page.
*/
public function ajax_save_appointment(): void
{
redirect('calendar/save_appointment');
}
/**
* Delete appointment from the database.
*/
public function ajax_delete_appointment(): void
{
redirect('calendar/delete_appointment');
}
/**
* Disable a providers sync setting.
*/
public function ajax_disable_provider_sync(): void
{
redirect('google/disable_provider_sync');
}
/**
* Filter the customer records with the given key string.
*/
public function ajax_filter_customers(): void
{
redirect('customers/search');
}
/**
* Insert or update an unavailability.
*/
public function ajax_save_unavailability(): void
{
redirect('calendar/save_unavailability');
}
/**
* Delete an unavailability time period from database.
*/
public function ajax_delete_unavailability(): void
{
redirect('calendar/delete_unavailability');
}
/**
* Insert of update working plan exceptions to database.
*/
public function ajax_save_working_plan_exception(): void
{
redirect('calendar/save_working_plan_exception');
}
/**
* Delete a working plan exceptions time period to database.
*/
public function ajax_delete_working_plan_exception(): void
{
redirect('calendar/delete_working_plan_exception');
}
/**
* Save (insert or update) a customer record.
*/
public function ajax_save_customer(): void
{
redirect('customers/create'); // or "customers/update"
}
/**
* Delete customer from database.
*/
public function ajax_delete_customer(): void
{
redirect('customers/destroy');
}
/**
* Save (insert or update) service record.
*/
public function ajax_save_service(): void
{
redirect('services/create'); // or "services/update"
}
/**
* Delete service record from database.
*/
public function ajax_delete_service(): void
{
redirect('services/destroy');
}
/**
* Filter service records by given key string.
*/
public function ajax_filter_services(): void
{
redirect('services/search');
}
/**
* Save (insert or update) category record.
*/
public function ajax_save_service_category(): void
{
redirect('categories/create'); // or "categories/update"
}
/**
* Delete category record from database.
*/
public function ajax_delete_service_category(): void
{
redirect('categories/destroy');
}
/**
* Filter services categories with key string.
*/
public function ajax_filter_service_categories(): void
{
redirect('categories/search');
}
/**
* Filter admin records with string key.
*/
public function ajax_filter_admins(): void
{
redirect('admins/search');
}
/**
* Save (insert or update) admin record into database.
*/
public function ajax_save_admin(): void
{
redirect('admins/create'); // or "admins/update"
}
/**
* Delete an admin record from the database.
*/
public function ajax_delete_admin(): void
{
redirect('admins/destroy');
}
/**
* Filter provider records with string key.
*/
public function ajax_filter_providers(): void
{
redirect('providers/search');
}
/**
* Save (insert or update) a provider record into database.
*/
public function ajax_save_provider(): void
{
redirect('providers/create'); // or "providers/update"
}
/**
* Delete a provider record from the database.
*/
public function ajax_delete_provider(): void
{
redirect('providers/destroy');
}
/**
* Filter secretary records with string key.
*/
public function ajax_filter_secretaries(): void
{
redirect('secretaries/search');
}
/**
* Save (insert or update) a secretary record into database.
*/
public function ajax_save_secretary(): void
{
redirect('secretaries/create'); // or "secretaries/update"
}
/**
* Delete a secretary record from the database.
*/
public function ajax_delete_secretary(): void
{
redirect('secretaries/destroy');
}
/**
* Save a setting or multiple settings in the database.
*/
public function ajax_save_settings(): void
{
redirect('general_settings/save'); // or "business_settings/save", "booking_settings/save", "legal_settings/save"
}
/**
* This method checks whether the username already exists in the database.
*/
public function ajax_validate_username(): void
{
redirect('account/validate_username');
}
/**
* Change system language for current user.
*/
public function ajax_change_language(): void
{
redirect('account/change_language');
}
/**
* This method will return a list of the available Google Calendars.
*/
public function ajax_get_google_calendars(): void
{
redirect('google/get_google_calendars');
}
/**
* Select a specific google calendar for a provider.
*/
public function ajax_select_google_calendar(): void
{
redirect('google/select_google_calendar');
}
/**
* Apply global working plan to all providers.
*/
public function ajax_apply_global_working_plan(): void
{
redirect('business_settings/apply_global_working_plan');
}
}

View File

@ -0,0 +1,220 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Blocked_periods controller.
*
* Handles the blocked-periods related operations.
*
* @package Controllers
*/
class Blocked_periods extends EA_Controller
{
public array $allowed_blocked_period_fields = ['id', 'name', 'start_datetime', 'end_datetime', 'notes'];
public array $optional_blocked_period_fields = [
//
];
/**
* Blocked_periods constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('blocked_periods_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Render the backend blocked-periods page.
*
* On this page admin users will be able to manage blocked-periods, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('blocked_periods')]);
$user_id = session('user_id');
if (cannot('view', PRIV_BLOCKED_PERIODS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
'first_weekday' => setting('first_weekday'),
]);
html_vars([
'page_title' => lang('blocked_periods'),
'active_menu' => PRIV_BLOCKED_PERIODS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'timezones' => $this->timezones->to_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
]);
$this->load->view('pages/blocked_periods');
}
/**
* Filter blocked-periods by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_BLOCKED_PERIODS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$blocked_periods = $this->blocked_periods_model->search($keyword, $limit, $offset, $order_by);
json_response($blocked_periods);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new service-category.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_BLOCKED_PERIODS)) {
abort(403, 'Forbidden');
}
$blocked_period = request('blocked_period');
$this->blocked_periods_model->only($blocked_period, $this->allowed_blocked_period_fields);
$this->blocked_periods_model->optional($blocked_period, $this->optional_blocked_period_fields);
$blocked_period_id = $this->blocked_periods_model->save($blocked_period);
$blocked_period = $this->blocked_periods_model->find($blocked_period_id);
$this->webhooks_client->trigger(WEBHOOK_BLOCKED_PERIOD_SAVE, $blocked_period);
json_response([
'success' => true,
'id' => $blocked_period_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find a service-category.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_BLOCKED_PERIODS)) {
abort(403, 'Forbidden');
}
$blocked_period_id = request('blocked_period_id');
$blocked_period = $this->blocked_periods_model->find($blocked_period_id);
json_response($blocked_period);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a service-category.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_BLOCKED_PERIODS)) {
abort(403, 'Forbidden');
}
$blocked_period = request('blocked_period');
$this->blocked_periods_model->only($blocked_period, $this->allowed_blocked_period_fields);
$this->blocked_periods_model->optional($blocked_period, $this->optional_blocked_period_fields);
$blocked_period_id = $this->blocked_periods_model->save($blocked_period);
$blocked_period = $this->blocked_periods_model->find($blocked_period_id);
$this->webhooks_client->trigger(WEBHOOK_BLOCKED_PERIOD_SAVE, $blocked_period);
json_response([
'success' => true,
'id' => $blocked_period_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a service-category.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_BLOCKED_PERIODS)) {
abort(403, 'Forbidden');
}
$blocked_period_id = request('blocked_period_id');
$blocked_period = $this->blocked_periods_model->find($blocked_period_id);
$this->blocked_periods_model->delete($blocked_period_id);
$this->webhooks_client->trigger(WEBHOOK_BLOCKED_PERIOD_DELETE, $blocked_period);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,772 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Booking controller.
*
* Handles the booking related operations.
*
* Notice: This file used to have the booking page related code which since v1.5 has now moved to the Booking.php
* controller for improved consistency.
*
* @package Controllers
*/
class Booking extends EA_Controller
{
public array $allowed_customer_fields = [
'id',
'first_name',
'last_name',
'email',
'phone_number',
'address',
'city',
'state',
'zip_code',
'timezone',
'language',
'custom_field_1',
'custom_field_2',
'custom_field_3',
'custom_field_4',
'custom_field_5',
];
public mixed $allowed_provider_fields = ['id', 'first_name', 'last_name', 'services', 'timezone'];
public array $allowed_appointment_fields = [
'id',
'start_datetime',
'end_datetime',
'location',
'notes',
'color',
'status',
'is_unavailability',
'id_users_provider',
'id_users_customer',
'id_services',
];
/**
* Booking constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('admins_model');
$this->load->model('secretaries_model');
$this->load->model('service_categories_model');
$this->load->model('services_model');
$this->load->model('customers_model');
$this->load->model('settings_model');
$this->load->model('consents_model');
$this->load->library('timezones');
$this->load->library('synchronization');
$this->load->library('notifications');
$this->load->library('availability');
$this->load->library('webhooks_client');
}
/**
* Render the booking page and display the selected appointment.
*
* This method will call the "index" callback to handle the page rendering.
*
* @param string $appointment_hash
*/
public function reschedule(string $appointment_hash): void
{
html_vars(['appointment_hash' => $appointment_hash]);
$this->index();
}
/**
* Render the booking page.
*
* This method creates the appointment book wizard.
*/
public function index(): void
{
if (!is_app_installed()) {
redirect('installation');
return;
}
$company_name = setting('company_name');
$company_logo = setting('company_logo');
$company_color = setting('company_color');
$disable_booking = setting('disable_booking');
$google_analytics_code = setting('google_analytics_code');
$matomo_analytics_url = setting('matomo_analytics_url');
$matomo_analytics_site_id = setting('matomo_analytics_site_id');
if ($disable_booking) {
$disable_booking_message = setting('disable_booking_message');
html_vars([
'show_message' => true,
'page_title' => lang('page_title') . ' ' . $company_name,
'message_title' => lang('booking_is_disabled'),
'message_text' => $disable_booking_message,
'message_icon' => base_url('assets/img/error.png'),
'google_analytics_code' => $google_analytics_code,
'matomo_analytics_url' => $matomo_analytics_url,
'matomo_analytics_site_id' => $matomo_analytics_site_id,
]);
$this->load->view('pages/booking_message');
return;
}
$available_services = $this->services_model->get_available_services(true);
$available_providers = $this->providers_model->get_available_providers(true);
foreach ($available_providers as &$available_provider) {
// Only expose the required provider data.
$this->providers_model->only($available_provider, $this->allowed_provider_fields);
}
$date_format = setting('date_format');
$time_format = setting('time_format');
$first_weekday = setting('first_weekday');
$display_first_name = setting('display_first_name');
$require_first_name = setting('require_first_name');
$display_last_name = setting('display_last_name');
$require_last_name = setting('require_last_name');
$display_email = setting('display_email');
$require_email = setting('require_email');
$display_phone_number = setting('display_phone_number');
$require_phone_number = setting('require_phone_number');
$display_address = setting('display_address');
$require_address = setting('require_address');
$display_city = setting('display_city');
$require_city = setting('require_city');
$display_zip_code = setting('display_zip_code');
$require_zip_code = setting('require_zip_code');
$display_notes = setting('display_notes');
$require_notes = setting('require_notes');
$display_cookie_notice = setting('display_cookie_notice');
$cookie_notice_content = setting('cookie_notice_content');
$display_terms_and_conditions = setting('display_terms_and_conditions');
$terms_and_conditions_content = setting('terms_and_conditions_content');
$display_privacy_policy = setting('display_privacy_policy');
$privacy_policy_content = setting('privacy_policy_content');
$display_any_provider = setting('display_any_provider');
$display_login_button = setting('display_login_button');
$display_delete_personal_information = setting('display_delete_personal_information');
$book_advance_timeout = setting('book_advance_timeout');
$theme = request('theme', setting('theme', 'default'));
if (empty($theme) || !file_exists(__DIR__ . '/../../assets/css/themes/' . $theme . '.min.css')) {
$theme = 'default';
}
$timezones = $this->timezones->to_array();
$grouped_timezones = $this->timezones->to_grouped_array();
$appointment_hash = html_vars('appointment_hash');
if (!empty($appointment_hash)) {
// Load the appointments data and enable the manage mode of the booking page.
$manage_mode = true;
$results = $this->appointments_model->get(['hash' => $appointment_hash]);
if (empty($results)) {
html_vars([
'show_message' => true,
'page_title' => lang('page_title') . ' ' . $company_name,
'message_title' => lang('appointment_not_found'),
'message_text' => lang('appointment_does_not_exist_in_db'),
'message_icon' => base_url('assets/img/error.png'),
'google_analytics_code' => $google_analytics_code,
'matomo_analytics_url' => $matomo_analytics_url,
'matomo_analytics_site_id' => $matomo_analytics_site_id,
]);
$this->load->view('pages/booking_message');
return;
}
// Make sure the appointment can still be rescheduled.
$start_datetime = strtotime($results[0]['start_datetime']);
$limit = strtotime('+' . $book_advance_timeout . ' minutes', strtotime('now'));
if ($start_datetime < $limit) {
$hours = floor($book_advance_timeout / 60);
$minutes = $book_advance_timeout % 60;
html_vars([
'show_message' => true,
'page_title' => lang('page_title') . ' ' . $company_name,
'message_title' => lang('appointment_locked'),
'message_text' => strtr(lang('appointment_locked_message'), [
'{$limit}' => sprintf('%02d:%02d', $hours, $minutes),
]),
'message_icon' => base_url('assets/img/error.png'),
'google_analytics_code' => $google_analytics_code,
'matomo_analytics_url' => $matomo_analytics_url,
'matomo_analytics_site_id' => $matomo_analytics_site_id,
]);
$this->load->view('pages/booking_message');
return;
}
$appointment = $results[0];
$provider = $this->providers_model->find($appointment['id_users_provider']);
$customer = $this->customers_model->find($appointment['id_users_customer']);
$customer_token = md5(uniqid(mt_rand(), true));
// Cache the token for 10 minutes.
$this->cache->save('customer-token-' . $customer_token, $customer['id'], 600);
} else {
$manage_mode = false;
$customer_token = false;
$appointment = null;
$provider = null;
$customer = null;
}
script_vars([
'manage_mode' => $manage_mode,
'available_services' => $available_services,
'available_providers' => $available_providers,
'date_format' => $date_format,
'time_format' => $time_format,
'first_weekday' => $first_weekday,
'display_cookie_notice' => $display_cookie_notice,
'display_any_provider' => setting('display_any_provider'),
'future_booking_limit' => setting('future_booking_limit'),
'appointment_data' => $appointment,
'provider_data' => $provider,
'customer_data' => $customer,
'default_language' => setting('default_language'),
'default_timezone' => setting('default_timezone'),
]);
html_vars([
'available_services' => $available_services,
'available_providers' => $available_providers,
'theme' => $theme,
'company_name' => $company_name,
'company_logo' => $company_logo,
'company_color' => $company_color === '#ffffff' ? '' : $company_color,
'date_format' => $date_format,
'time_format' => $time_format,
'first_weekday' => $first_weekday,
'display_first_name' => $display_first_name,
'require_first_name' => $require_first_name,
'display_last_name' => $display_last_name,
'require_last_name' => $require_last_name,
'display_email' => $display_email,
'require_email' => $require_email,
'display_phone_number' => $display_phone_number,
'require_phone_number' => $require_phone_number,
'display_address' => $display_address,
'require_address' => $require_address,
'display_city' => $display_city,
'require_city' => $require_city,
'display_zip_code' => $display_zip_code,
'require_zip_code' => $require_zip_code,
'display_notes' => $display_notes,
'require_notes' => $require_notes,
'display_cookie_notice' => $display_cookie_notice,
'cookie_notice_content' => $cookie_notice_content,
'display_terms_and_conditions' => $display_terms_and_conditions,
'terms_and_conditions_content' => $terms_and_conditions_content,
'display_privacy_policy' => $display_privacy_policy,
'privacy_policy_content' => $privacy_policy_content,
'display_any_provider' => $display_any_provider,
'display_login_button' => $display_login_button,
'display_delete_personal_information' => $display_delete_personal_information,
'google_analytics_code' => $google_analytics_code,
'matomo_analytics_url' => $matomo_analytics_url,
'matomo_analytics_site_id' => $matomo_analytics_site_id,
'timezones' => $timezones,
'grouped_timezones' => $grouped_timezones,
'manage_mode' => $manage_mode,
'customer_token' => $customer_token,
'appointment_data' => $appointment,
'provider_data' => $provider,
'customer_data' => $customer,
]);
$this->load->view('pages/booking');
}
/**
* Register the appointment to the database.
*/
public function register(): void
{
try {
$disable_booking = setting('disable_booking');
if ($disable_booking) {
abort(403);
}
$post_data = request('post_data');
$captcha = request('captcha');
$appointment = $post_data['appointment'];
$customer = $post_data['customer'];
$manage_mode = filter_var($post_data['manage_mode'], FILTER_VALIDATE_BOOLEAN);
if (!array_key_exists('address', $customer)) {
$customer['address'] = '';
}
if (!array_key_exists('city', $customer)) {
$customer['city'] = '';
}
if (!array_key_exists('zip_code', $customer)) {
$customer['zip_code'] = '';
}
if (!array_key_exists('notes', $customer)) {
$customer['notes'] = '';
}
if (!array_key_exists('phone_number', $customer)) {
$customer['phone_number'] = '';
}
// Check appointment availability before registering it to the database.
$appointment['id_users_provider'] = $this->check_datetime_availability();
if (!$appointment['id_users_provider']) {
throw new RuntimeException(lang('requested_hour_is_unavailable'));
}
$provider = $this->providers_model->find($appointment['id_users_provider']);
$service = $this->services_model->find($appointment['id_services']);
$require_captcha = (bool) setting('require_captcha');
$captcha_phrase = session('captcha_phrase');
// Validate the CAPTCHA string.
if ($require_captcha && strtoupper($captcha_phrase) !== strtoupper($captcha)) {
json_response([
'captcha_verification' => false,
]);
return;
}
if ($this->customers_model->exists($customer)) {
$customer['id'] = $this->customers_model->find_record_id($customer);
$existing_appointments = $this->appointments_model->get([
'id !=' => $manage_mode ? $appointment['id'] : null,
'id_users_customer' => $customer['id'],
'start_datetime <=' => $appointment['start_datetime'],
'end_datetime >=' => $appointment['end_datetime'],
]);
if (count($existing_appointments)) {
throw new RuntimeException(lang('customer_is_already_booked'));
}
}
if (empty($appointment['location']) && !empty($service['location'])) {
$appointment['location'] = $service['location'];
}
if (empty($appointment['color']) && !empty($service['color'])) {
$appointment['color'] = $service['color'];
}
$customer_ip = $this->input->ip_address();
// Create the consents (if needed).
$consent = [
'first_name' => $customer['first_name'] ?? '-',
'last_name' => $customer['last_name'] ?? '-',
'email' => $customer['email'] ?? '-',
'ip' => $customer_ip,
];
if (setting('display_terms_and_conditions')) {
$consent['type'] = 'terms-and-conditions';
$this->consents_model->save($consent);
}
if (setting('display_privacy_policy')) {
$consent['type'] = 'privacy-policy';
$this->consents_model->save($consent);
}
// Save customer language (the language which is used to render the booking page).
$customer['language'] = session('language') ?? config('language');
$this->customers_model->only($customer, $this->allowed_customer_fields);
$customer_id = $this->customers_model->save($customer);
$customer = $this->customers_model->find($customer_id);
$appointment['id_users_customer'] = $customer_id;
$appointment['is_unavailability'] = false;
$appointment['color'] = $service['color'];
$appointment_status_options_json = setting('appointment_status_options', '[]');
$appointment_status_options = json_decode($appointment_status_options_json, true) ?? [];
$appointment['status'] = $appointment_status_options[0] ?? null;
$this->appointments_model->only($appointment, $this->allowed_appointment_fields);
$appointment_id = $this->appointments_model->save($appointment);
$appointment = $this->appointments_model->find($appointment_id);
$company_color = setting('company_color');
$settings = [
'company_name' => setting('company_name'),
'company_link' => setting('company_link'),
'company_email' => setting('company_email'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
];
$this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings);
$this->notifications->notify_appointment_saved(
$appointment,
$service,
$provider,
$customer,
$settings,
$manage_mode,
);
$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_SAVE, $appointment);
$response = [
'appointment_id' => $appointment['id'],
'appointment_hash' => $appointment['hash'],
];
json_response($response);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Check whether the provider is still available in the selected appointment date.
*
* It is possible that two or more customers select the same appointment date and time concurrently. The app won't
* allow this to happen, so one of the two will eventually get the selected date and the other one will have
* to choose for another one.
*
* Use this method just before the customer confirms the appointment registration. If the selected date was reserved
* in the meanwhile, the customer must be prompted to select another time.
*
* @return int|null Returns the ID of the provider that is available for the appointment.
*
* @throws Exception
*/
protected function check_datetime_availability(): ?int
{
$post_data = request('post_data');
$appointment = $post_data['appointment'];
$appointment_start = new DateTime($appointment['start_datetime']);
$date = $appointment_start->format('Y-m-d');
$hour = $appointment_start->format('H:i');
if ($appointment['id_users_provider'] === ANY_PROVIDER) {
$appointment['id_users_provider'] = $this->search_any_provider($appointment['id_services'], $date, $hour);
return $appointment['id_users_provider'];
}
$service = $this->services_model->find($appointment['id_services']);
$exclude_appointment_id = $appointment['id'] ?? null;
$provider = $this->providers_model->find($appointment['id_users_provider']);
$available_hours = $this->availability->get_available_hours(
$date,
$service,
$provider,
$exclude_appointment_id,
);
$is_still_available = false;
$appointment_hour = date('H:i', strtotime($appointment['start_datetime']));
foreach ($available_hours as $available_hour) {
if ($appointment_hour === $available_hour) {
$is_still_available = true;
break;
}
}
return $is_still_available ? $appointment['id_users_provider'] : null;
}
/**
* Search for any provider that can handle the requested service.
*
* This method will return the database ID of the provider with the most available periods.
*
* @param int $service_id Service ID
* @param string $date Selected date (Y-m-d).
* @param string|null $hour Selected hour (H:i).
*
* @return int|null Returns the ID of the provider that can provide the service at the selected date.
*
* @throws Exception
*/
protected function search_any_provider(int $service_id, string $date, ?string $hour = null): ?int
{
$available_providers = $this->providers_model->get_available_providers(true);
$service = $this->services_model->find($service_id);
$provider_id = null;
$max_hours_count = 0;
foreach ($available_providers as $provider) {
foreach ($provider['services'] as $provider_service_id) {
if ($provider_service_id == $service_id) {
// Check if the provider is available for the requested date.
$available_hours = $this->availability->get_available_hours($date, $service, $provider);
if (
count($available_hours) > $max_hours_count &&
(empty($hour) || in_array($hour, $available_hours))
) {
$provider_id = $provider['id'];
$max_hours_count = count($available_hours);
}
}
}
}
return $provider_id;
}
/**
* Get the available appointment hours for the selected date.
*
* This method answers to an AJAX request. It calculates the available hours for the given service, provider and
* date.
*/
public function get_available_hours(): void
{
try {
$disable_booking = setting('disable_booking');
if ($disable_booking) {
abort(403);
}
$provider_id = request('provider_id');
$service_id = request('service_id');
$selected_date = request('selected_date');
// Do not continue if there was no provider selected (more likely there is no provider in the system).
if (empty($provider_id)) {
json_response();
return;
}
// If manage mode is TRUE then the following we should not consider the selected appointment when
// calculating the available time periods of the provider.
$exclude_appointment_id = request('manage_mode') ? request('appointment_id') : null;
// If the user has selected the "any-provider" option then we will need to search for an available provider
// that will provide the requested service.
$service = $this->services_model->find($service_id);
if ($provider_id === ANY_PROVIDER) {
$providers = $this->providers_model->get();
$available_hours = [];
foreach ($providers as $provider) {
if (!in_array($service_id, $provider['services'])) {
continue;
}
$provider_available_hours = $this->availability->get_available_hours(
$selected_date,
$service,
$provider,
$exclude_appointment_id,
);
$available_hours = array_merge($available_hours, $provider_available_hours);
}
$available_hours = array_unique(array_values($available_hours));
sort($available_hours);
$response = $available_hours;
} else {
$provider = $this->providers_model->find($provider_id);
$response = $this->availability->get_available_hours(
$selected_date,
$service,
$provider,
$exclude_appointment_id,
);
}
json_response($response);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get Unavailable Dates
*
* Get an array with the available dates of a specific provider, service and month of the year. Provide the
* "provider_id", "service_id" and "selected_date" as GET parameters to the request. The "selected_date" parameter
* must have the "Y-m-d" format.
*
* Outputs a JSON string with the unavailability dates. that are unavailability.
*/
public function get_unavailable_dates(): void
{
try {
$disable_booking = setting('disable_booking');
if ($disable_booking) {
abort(403);
}
$provider_id = request('provider_id');
$service_id = request('service_id');
$appointment_id = request('appointment_id');
$manage_mode = filter_var(request('manage_mode'), FILTER_VALIDATE_BOOLEAN);
$selected_date_string = request('selected_date');
$selected_date = new DateTime($selected_date_string);
$number_of_days_in_month = (int) $selected_date->format('t');
$unavailable_dates = [];
$provider_ids =
$provider_id === ANY_PROVIDER ? $this->search_providers_by_service($service_id) : [$provider_id];
$exclude_appointment_id = $manage_mode ? $appointment_id : null;
// Get the service record.
$service = $this->services_model->find($service_id);
for ($i = 1; $i <= $number_of_days_in_month; $i++) {
$current_date = new DateTime($selected_date->format('Y-m') . '-' . $i);
if ($current_date < new DateTime(date('Y-m-d 00:00:00'))) {
// Past dates become immediately unavailability.
$unavailable_dates[] = $current_date->format('Y-m-d');
continue;
}
// Finding at least one slot of availability.
foreach ($provider_ids as $current_provider_id) {
$provider = $this->providers_model->find($current_provider_id);
$available_hours = $this->availability->get_available_hours(
$current_date->format('Y-m-d'),
$service,
$provider,
$exclude_appointment_id,
);
if (!empty($available_hours)) {
break;
}
}
// No availability amongst all the provider.
if (empty($available_hours)) {
$unavailable_dates[] = $current_date->format('Y-m-d');
}
}
if (count($unavailable_dates) === $number_of_days_in_month) {
json_response([
'is_month_unavailable' => true,
]);
return;
}
json_response($unavailable_dates);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Search for any provider that can handle the requested service.
*
* This method will return the database ID of the providers affected to the requested service.
*
* @param int $service_id The requested service ID.
*
* @return array Returns the ID of the provider that can provide the requested service.
*/
protected function search_providers_by_service(int $service_id): array
{
$available_providers = $this->providers_model->get_available_providers(true);
$provider_list = [];
foreach ($available_providers as $provider) {
foreach ($provider['services'] as $provider_service_id) {
if ($provider_service_id === $service_id) {
// Check if the provider is affected to the selected service.
$provider_list[] = $provider['id'];
}
}
}
return $provider_list;
}
}

View File

@ -0,0 +1,130 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Booking cancellation controller.
*
* Handles the booking cancellation related operations.
*
* @package Controllers
*/
class Booking_cancellation extends EA_Controller
{
/**
* Booking_cancellation constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('services_model');
$this->load->model('customers_model');
$this->load->library('synchronization');
$this->load->library('notifications');
$this->load->library('webhooks_client');
}
/**
* Cancel an existing appointment.
*
* This method removes an appointment from the company's schedule. In order for the appointment to be deleted, the
* hash string must be provided. The customer can only cancel the appointment if the edit time period is not over
* yet.
*
* @param string $appointment_hash This appointment hash identifier.
*/
public function of(string $appointment_hash): void
{
try {
$disable_booking = setting('disable_booking');
if ($disable_booking) {
abort(403);
}
$cancellation_reason = request('cancellation_reason');
if ($this->input->method() !== 'post' || empty($cancellation_reason)) {
abort(403, 'Forbidden');
}
$occurrences = $this->appointments_model->get(['hash' => $appointment_hash]);
if (empty($occurrences)) {
html_vars([
'page_title' => lang('appointment_not_found'),
'company_color' => setting('company_color'),
'message_title' => lang('appointment_not_found'),
'message_text' => lang('appointment_does_not_exist_in_db'),
'message_icon' => base_url('assets/img/error.png'),
'google_analytics_code' => setting('google_analytics_code'),
'matomo_analytics_url' => setting('matomo_analytics_url'),
'matomo_analytics_site_id' => setting('matomo_analytics_site_id'),
]);
$this->load->view('pages/booking_message');
return;
}
$appointment = $occurrences[0];
$provider = $this->providers_model->find($appointment['id_users_provider']);
$customer = $this->customers_model->find($appointment['id_users_customer']);
$service = $this->services_model->find($appointment['id_services']);
$company_color = setting('company_color');
$settings = [
'company_name' => setting('company_name'),
'company_email' => setting('company_email'),
'company_link' => setting('company_link'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
];
$this->appointments_model->delete($appointment['id']);
$this->synchronization->sync_appointment_deleted($appointment, $provider);
$this->notifications->notify_appointment_deleted(
$appointment,
$service,
$provider,
$customer,
$settings,
$cancellation_reason,
);
$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_DELETE, $appointment);
} catch (Throwable $e) {
log_message('error', 'Booking Cancellation Exception: ' . $e->getMessage());
}
html_vars([
'page_title' => lang('appointment_cancelled_title'),
'company_color' => setting('company_color'),
'google_analytics_code' => setting('google_analytics_code'),
'matomo_analytics_url' => setting('matomo_analytics_url'),
'matomo_analytics_site_id' => setting('matomo_analytics_site_id'),
]);
$this->load->view('pages/booking_cancellation');
}
}

View File

@ -0,0 +1,70 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Booking confirmation controller.
*
* Handles the booking confirmation related operations.
*
* @package Controllers
*/
class Booking_confirmation extends EA_Controller
{
/**
* Booking_confirmation constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('services_model');
$this->load->model('customers_model');
$this->load->library('google_sync');
}
/**
* Display the appointment registration success page.
*
* @throws Exception
*/
public function of(): void
{
$appointment_hash = $this->uri->segment(3);
$occurrences = $this->appointments_model->get(['hash' => $appointment_hash]);
if (empty($occurrences)) {
redirect('appointments'); // The appointment does not exist.
return;
}
$appointment = $occurrences[0];
$add_to_google_url = $this->google_sync->get_add_to_google_url($appointment['id']);
html_vars([
'page_title' => lang('success'),
'company_color' => setting('company_color'),
'google_analytics_code' => setting('google_analytics_code'),
'matomo_analytics_url' => setting('matomo_analytics_url'),
'matomo_analytics_site_id' => setting('matomo_analytics_site_id'),
'add_to_google_url' => $add_to_google_url,
]);
$this->load->view('pages/booking_confirmation');
}
}

View File

@ -0,0 +1,121 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Booking settings controller.
*
* Handles booking settings related operations.
*
* @package Controllers
*/
class Booking_settings extends EA_Controller
{
public array $allowed_setting_fields = ['id', 'name', 'value'];
public array $optional_setting_fields = [
//
];
/**
* Booking_settings constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('customers_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
$this->load->model('settings_model');
$this->load->library('accounts');
$this->load->library('google_sync');
$this->load->library('notifications');
$this->load->library('synchronization');
$this->load->library('timezones');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('booking_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'booking_settings' => $this->settings_model->get_batch(),
]);
html_vars([
'page_title' => lang('settings'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
]);
$this->load->view('pages/booking_settings');
}
/**
* Save booking settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('booking_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->only($setting, $this->allowed_setting_fields);
$this->settings_model->optional($setting, $this->optional_setting_fields);
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,147 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Business logic controller.
*
* Handles general settings related operations.
*
* @package Controllers
*/
class Business_settings extends EA_Controller
{
public array $allowed_setting_fields = ['id', 'name', 'value'];
public array $optional_setting_fields = [
//
];
/**
* Business_logic constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('customers_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
$this->load->model('settings_model');
$this->load->library('accounts');
$this->load->library('google_sync');
$this->load->library('notifications');
$this->load->library('synchronization');
$this->load->library('timezones');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('business_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'business_settings' => $this->settings_model->get(),
'first_weekday' => setting('first_weekday'),
'time_format' => setting('time_format'),
]);
html_vars([
'page_title' => lang('settings'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
]);
$this->load->view('pages/business_settings');
}
/**
* Save general settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('business_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->only($setting, $this->allowed_setting_fields);
$this->settings_model->optional($setting, $this->optional_setting_fields);
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Apply global working plan to all providers.
*/
public function apply_global_working_plan(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$working_plan = request('working_plan');
$providers = $this->providers_model->get();
foreach ($providers as $provider) {
$this->providers_model->set_setting($provider['id'], 'working_plan', $working_plan);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,315 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
use GuzzleHttp\Exception\GuzzleException;
use Jsvrcek\ICS\Exception\CalendarEventException;
/**
* Caldav controller.
*
* Handles the Caldav Calendar synchronization related operations.
*
* @package Controllers
*/
class Caldav extends EA_Controller
{
/**
* Caldav constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('caldav_sync');
$this->load->model('appointments_model');
$this->load->model('unavailabilities_model');
$this->load->model('providers_model');
}
/**
* Connect to the target CalDAV server
*
* @return void
*/
public function connect_to_server(): void
{
try {
$provider_id = request('provider_id');
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$caldav_url = request('caldav_url');
$caldav_username = request('caldav_username');
$caldav_password = request('caldav_password');
$this->caldav_sync->test_connection($caldav_url, $caldav_username, $caldav_password);
$provider = $this->providers_model->find($provider_id);
$provider['settings']['caldav_sync'] = true;
$provider['settings']['caldav_url'] = $caldav_url;
$provider['settings']['caldav_username'] = $caldav_username;
$provider['settings']['caldav_password'] = $caldav_password;
$this->providers_model->save($provider);
json_response([
'success' => true,
]);
} catch (GuzzleException | InvalidArgumentException $e) {
json_response([
'success' => false,
'message' => $e->getMessage(),
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Sync the provider events with the remote CalDAV calendar.
*
* @param string $provider_id Provider ID (String because this is used with HTTP and CLI)
*
* @return void
*
* @throws CalendarEventException
* @throws Exception
* @throws Throwable
*/
public static function sync(string $provider_id): void
{
/** @var EA_Controller $CI */
$CI = get_instance();
$CI->load->library('caldav_sync');
// Load the libraries as this method is called statically from the CLI command
$CI->load->model('appointments_model');
$CI->load->model('unavailabilities_model');
$CI->load->model('providers_model');
$CI->load->model('services_model');
$CI->load->model('customers_model');
$CI->load->model('settings_model');
$user_id = session('user_id');
if (!$user_id && !is_cli()) {
return;
}
if (empty($provider_id)) {
throw new InvalidArgumentException('No provider ID provided.');
}
$provider = $CI->providers_model->find($provider_id);
// Check whether the selected provider has the CalDAV Sync enabled.
if (!$provider['settings']['caldav_sync']) {
return; // The selected provider does not have the CalDAV Sync enabled.
}
// Fetch provider's appointments that belong to the sync time period.
$sync_past_days = $provider['settings']['sync_past_days'];
$sync_future_days = $provider['settings']['sync_future_days'];
$start_date_time = date('Y-m-d H:i:s', strtotime('-' . $sync_past_days . ' days'));
$end_date_time = date('Y-m-d H:i:s', strtotime('+' . $sync_future_days . ' days'));
$where = [
'start_datetime >=' => $start_date_time,
'end_datetime <=' => $end_date_time,
'id_users_provider' => $provider['id'],
];
$appointments = $CI->appointments_model->get($where);
$unavailabilities = $CI->unavailabilities_model->get($where);
$local_events = [...$appointments, ...$unavailabilities];
// Sync each appointment with CalDAV Calendar by following the project's sync protocol (see documentation).
foreach ($local_events as $local_event) {
if (str_contains((string) $local_event['id_caldav_calendar'], 'RECURRENCE')) {
continue;
}
if (!$local_event['is_unavailability']) {
$service = $CI->services_model->find($local_event['id_services']);
$customer = $CI->customers_model->find($local_event['id_users_customer']);
$events_model = $CI->appointments_model;
} else {
$service = null;
$customer = null;
$events_model = $CI->unavailabilities_model;
}
if (!$local_event['id_caldav_calendar']) {
if (!$local_event['is_unavailability']) {
$caldav_event_id = $CI->caldav_sync->save_appointment($local_event, $service, $provider, $customer);
} else {
$caldav_event_id = $CI->caldav_sync->save_unavailability($local_event, $provider);
}
$local_event['id_caldav_calendar'] = $caldav_event_id;
$events_model->save($local_event); // Save the CalDAV Calendar ID.
continue;
}
// Appointment is synced with CalDAV Calendar.
try {
$caldav_event = $CI->caldav_sync->get_event($provider, $local_event['id_caldav_calendar']);
if (!$caldav_event || $caldav_event['status'] === 'CANCELLED') {
throw new Exception('Event is cancelled, remove the record from Easy!Appointments.');
}
// If CalDAV Calendar event is different from Easy!Appointments appointment then update Easy!Appointments record.
$local_event_start = strtotime($local_event['start_datetime']);
$local_event_end = strtotime($local_event['end_datetime']);
$caldav_event_start = new DateTime($caldav_event['start_datetime']);
$caldav_event_end = new DateTime($caldav_event['end_datetime']);
$is_different =
$local_event_start !== $caldav_event_start->getTimestamp() ||
$local_event_end !== $caldav_event_end->getTimestamp() ||
$local_event['notes'] !== $caldav_event['description'];
if ($is_different) {
$local_event['start_datetime'] = $caldav_event_start->format('Y-m-d H:i:s');
$local_event['end_datetime'] = $caldav_event_end->format('Y-m-d H:i:s');
$local_event['notes'] = $caldav_event['description'];
$events_model->save($local_event);
}
} catch (Throwable) {
// Appointment not found on CalDAV Calendar, delete from Easy!Appointments.
$events_model->delete($local_event['id']);
$local_event['id_caldav_calendar'] = null;
}
}
// Add CalDAV Calendar events that do not exist in Easy!Appointments.
try {
$caldav_events = $CI->caldav_sync->get_sync_events($provider, $start_date_time, $end_date_time);
} catch (Throwable $e) {
if ($e->getCode() === 404) {
log_message('error', 'CalDAV - Remote Calendar not found for provider ID: ' . $provider_id);
return; // The remote calendar was not found.
} else {
throw $e;
}
}
$CI->appointments_model->delete_caldav_recurring_events($start_date_time, $end_date_time);
foreach ($caldav_events as $caldav_event) {
if ($caldav_event['status'] === 'CANCELLED') {
continue;
}
if ($caldav_event['start_datetime'] === $caldav_event['end_datetime']) {
continue; // Cannot sync events with the same start and end date time value
}
$appointment_results = $CI->appointments_model->get(['id_caldav_calendar' => $caldav_event['id']]);
if (!empty($appointment_results)) {
continue;
}
$unavailability_results = $CI->unavailabilities_model->get([
'id_caldav_calendar' => $caldav_event['id'],
]);
if (!empty($unavailability_results)) {
continue;
}
// Record doesn't exist in the Easy!Appointments, so add the event now.
$local_event = [
'start_datetime' => $caldav_event['start_datetime'],
'end_datetime' => $caldav_event['end_datetime'],
'location' => $caldav_event['location'],
'notes' => $caldav_event['summary'] . ' ' . $caldav_event['description'],
'id_users_provider' => $provider_id,
'id_caldav_calendar' => $caldav_event['id'],
];
$CI->unavailabilities_model->save($local_event);
}
json_response([
'success' => true,
]);
}
/**
* Disable a providers sync setting.
*
* This method resets the CalDAV related settings from the user_settings DB table.
*
* @return void
*/
public function disable_provider_sync(): void
{
try {
$provider_id = request('provider_id');
if (!$provider_id) {
throw new Exception('Provider id not specified.');
}
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$provider = $this->providers_model->find($provider_id);
$provider['settings']['caldav_sync'] = false;
$provider['settings']['caldav_url'] = null;
$provider['settings']['caldav_username'] = null;
$provider['settings']['caldav_password'] = null;
$this->providers_model->save($provider);
$this->appointments_model->clear_caldav_sync_ids($provider_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,807 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Calendar controller.
*
* Handles calendar related operations.
*
* @package Controllers
*/
class Calendar extends EA_Controller
{
public array $allowed_customer_fields = [
'id',
'first_name',
'last_name',
'email',
'phone_number',
'address',
'city',
'state',
'zip_code',
'timezone',
'language',
'notes',
'custom_field_1',
'custom_field_2',
'custom_field_3',
'custom_field_4',
'custom_field_5',
];
public array $optional_customer_fields = [
//
];
public array $allowed_appointment_fields = [
'id',
'start_datetime',
'end_datetime',
'location',
'notes',
'color',
'status',
'is_unavailability',
'id_users_provider',
'id_users_customer',
'id_services',
];
public array $optional_appointment_fields = [
//
];
/**
* Calendar constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('unavailabilities_model');
$this->load->model('blocked_periods_model');
$this->load->model('customers_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('google_sync');
$this->load->library('notifications');
$this->load->library('synchronization');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Render the calendar page and display the selected appointment.
*
* This method will call the "index" callback to handle the page rendering.
*
* @param string $appointment_hash Appointment hash.
*/
public function reschedule(string $appointment_hash): void
{
$this->index($appointment_hash);
}
/**
* Display the main backend page.
*
* This method displays the main backend page. All login permission can view this page which displays a calendar
* with the events of the selected provider or service. If a user has more privileges he will see more menus at the
* top of the page.
*
* @param string $appointment_hash Appointment hash.
*/
public function index(string $appointment_hash = ''): void
{
session([
'dest_url' => site_url('calendar/index' . (!empty($appointment_hash) ? '/' . $appointment_hash : '')),
]);
$user_id = session('user_id');
if (cannot('view', PRIV_APPOINTMENTS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
$user = $this->users_model->find($user_id);
$secretary_providers = [];
if ($role_slug === DB_SLUG_SECRETARY) {
$secretary = $this->secretaries_model->find(session('user_id'));
$secretary_providers = $secretary['providers'];
}
$edit_appointment = null;
if (!empty($appointment_hash)) {
$occurrences = $this->appointments_model->get(['hash' => $appointment_hash]);
if ($appointment_hash !== '' && !empty($occurrences)) {
$edit_appointment = $occurrences[0];
$this->appointments_model->load($edit_appointment, ['customer']);
}
}
$privileges = $this->roles_model->get_permissions_by_slug($role_slug);
$available_providers = $this->providers_model->get_available_providers();
if ($role_slug === DB_SLUG_PROVIDER) {
$available_providers = array_values(
array_filter($available_providers, function ($available_provider) use ($user_id) {
return (int) $available_provider['id'] === (int) $user_id;
}),
);
}
if ($role_slug === DB_SLUG_SECRETARY) {
$available_providers = array_values(
array_filter($available_providers, function ($available_provider) use ($secretary_providers) {
return in_array($available_provider['id'], $secretary_providers);
}),
);
}
$available_services = $this->services_model->get_available_services();
$calendar_view = request('view', $user['settings']['calendar_view']);
$appointment_status_options = setting('appointment_status_options');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
'first_weekday' => setting('first_weekday'),
'company_working_plan' => setting('company_working_plan'),
'timezones' => $this->timezones->to_array(),
'privileges' => $privileges,
'calendar_view' => $calendar_view,
'available_providers' => $available_providers,
'available_services' => $available_services,
'secretary_providers' => $secretary_providers,
'edit_appointment' => $edit_appointment,
'google_sync_feature' => config('google_sync_feature'),
'customers' => $this->customers_model->get(null, 50, null, 'update_datetime DESC'),
'default_language' => setting('default_language'),
'default_timezone' => setting('default_timezone'),
]);
html_vars([
'page_title' => lang('calendar'),
'active_menu' => PRIV_APPOINTMENTS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'timezone' => session('timezone'),
'timezones' => $this->timezones->to_array(),
'grouped_timezones' => $this->timezones->to_grouped_array(),
'privileges' => $privileges,
'calendar_view' => $calendar_view,
'available_providers' => $available_providers,
'available_services' => $available_services,
'secretary_providers' => $secretary_providers,
'appointment_status_options' => json_decode($appointment_status_options, true) ?? [],
'require_first_name' => setting('require_first_name'),
'require_last_name' => setting('require_last_name'),
'require_email' => setting('require_email'),
'require_phone_number' => setting('require_phone_number'),
'require_address' => setting('require_address'),
'require_city' => setting('require_city'),
'require_zip_code' => setting('require_zip_code'),
'require_notes' => setting('require_notes'),
]);
$this->load->view('pages/calendar');
}
/**
* Save appointment changes that are made from the backend calendar page.
*/
public function save_appointment(): void
{
try {
$customer_data = request('customer_data');
$appointment_data = request('appointment_data');
$this->check_event_permissions((int) $appointment_data['id_users_provider']);
// Save customer changes to the database.
if ($customer_data) {
$customer = $customer_data;
$required_permissions = !empty($customer['id'])
? can('add', PRIV_CUSTOMERS)
: can('edit', PRIV_CUSTOMERS);
if (!$required_permissions) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$this->customers_model->only($customer, $this->allowed_customer_fields);
$this->customers_model->optional($customer, $this->optional_customer_fields);
$customer['id'] = $this->customers_model->save($customer);
}
// Save appointment changes to the database.
$manage_mode = !empty($appointment_data['id']);
if ($appointment_data) {
$appointment = $appointment_data;
$required_permissions = !empty($appointment['id'])
? can('add', PRIV_APPOINTMENTS)
: can('edit', PRIV_APPOINTMENTS);
if (!$required_permissions) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
// If the appointment does not contain the customer record id, then it means that is going to be inserted.
if (!isset($appointment['id_users_customer'])) {
$appointment['id_users_customer'] = $customer['id'] ?? $customer_data['id'];
}
if ($manage_mode && !empty($appointment['id'])) {
$this->synchronization->remove_appointment_on_provider_change($appointment['id']);
}
$this->appointments_model->only($appointment, $this->allowed_appointment_fields);
$this->appointments_model->optional($appointment, $this->optional_appointment_fields);
$appointment['id'] = $this->appointments_model->save($appointment);
}
if (empty($appointment['id'])) {
throw new RuntimeException('The appointment ID is not available.');
}
$appointment = $this->appointments_model->find($appointment['id']);
$provider = $this->providers_model->find($appointment['id_users_provider']);
$customer = $this->customers_model->find($appointment['id_users_customer']);
$service = $this->services_model->find($appointment['id_services']);
$company_color = setting('company_color');
$settings = [
'company_name' => setting('company_name'),
'company_link' => setting('company_link'),
'company_email' => setting('company_email'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
];
$this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings);
$this->notifications->notify_appointment_saved(
$appointment,
$service,
$provider,
$customer,
$settings,
$manage_mode,
);
$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_SAVE, $appointment);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
private function check_event_permissions(int $provider_id): void
{
$user_id = (int) session('user_id');
$role_slug = session('role_slug');
if (
$role_slug === DB_SLUG_SECRETARY &&
!$this->secretaries_model->is_provider_supported($user_id, $provider_id)
) {
abort(403);
}
if ($role_slug === DB_SLUG_PROVIDER && $user_id !== $provider_id) {
abort(403);
}
}
/**
* Delete appointment from the database.
*
* This method deletes an existing appointment from the database. Once this action is finished it cannot be undone.
* Notification emails are send to both provider and customer and the delete action is executed to the Google
* Calendar account of the provider, if the "google_sync" setting is enabled.
*/
public function delete_appointment(): void
{
try {
if (cannot('delete', 'appointments')) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$appointment_id = request('appointment_id');
$cancellation_reason = (string) request('cancellation_reason');
if (empty($appointment_id)) {
throw new InvalidArgumentException('No appointment id provided.');
}
// Store appointment data for later use in this method.
$appointment = $this->appointments_model->find($appointment_id);
$this->check_event_permissions((int) $appointment['id_users_provider']);
$provider = $this->providers_model->find($appointment['id_users_provider']);
$customer = $this->customers_model->find($appointment['id_users_customer']);
$service = $this->services_model->find($appointment['id_services']);
$company_color = setting('company_color');
$settings = [
'company_name' => setting('company_name'),
'company_email' => setting('company_email'),
'company_link' => setting('company_link'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
];
// Delete appointment record from the database.
$this->appointments_model->delete($appointment_id);
$this->notifications->notify_appointment_deleted(
$appointment,
$service,
$provider,
$customer,
$settings,
$cancellation_reason,
);
$this->synchronization->sync_appointment_deleted($appointment, $provider);
$this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_DELETE, $appointment);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Insert of update unavailability to database.
*/
public function save_unavailability(): void
{
try {
// Check privileges
$unavailability = request('unavailability');
$required_permissions = !isset($unavailability['id'])
? can('add', PRIV_APPOINTMENTS)
: can('edit', PRIV_APPOINTMENTS);
if (!$required_permissions) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$provider_id = (int) $unavailability['id_users_provider'];
$this->check_event_permissions($provider_id);
$provider = $this->providers_model->find($provider_id);
$unavailability_id = $this->unavailabilities_model->save($unavailability);
$unavailability = $this->unavailabilities_model->find($unavailability_id);
$this->synchronization->sync_unavailability_saved($unavailability, $provider);
$this->webhooks_client->trigger(WEBHOOK_UNAVAILABILITY_SAVE, $unavailability);
json_response([
'success' => true,
'warnings' => $warnings ?? [],
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete an unavailability from database.
*/
public function delete_unavailability(): void
{
try {
if (cannot('delete', PRIV_APPOINTMENTS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$unavailability_id = request('unavailability_id');
$unavailability = $this->unavailabilities_model->find($unavailability_id);
$this->check_event_permissions((int) $unavailability['id_users_provider']);
$provider = $this->providers_model->find($unavailability['id_users_provider']);
$this->unavailabilities_model->delete($unavailability_id);
$this->synchronization->sync_unavailability_deleted($unavailability, $provider);
$this->webhooks_client->trigger(WEBHOOK_UNAVAILABILITY_DELETE, $unavailability);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Insert of update working plan exceptions to database.
*/
public function save_working_plan_exception(): void
{
try {
if (cannot('edit', PRIV_USERS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$date = request('date');
$original_date = request('original_date');
$working_plan_exception = request('working_plan_exception');
if (!$working_plan_exception) {
$working_plan_exception = null;
}
$provider_id = request('provider_id');
$this->providers_model->save_working_plan_exception($provider_id, $date, $working_plan_exception);
if ($original_date && $date !== $original_date) {
$this->providers_model->delete_working_plan_exception($provider_id, $original_date);
}
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete a working plan exceptions time period to database.
*/
public function delete_working_plan_exception(): void
{
try {
$required_permissions = can('edit', PRIV_CUSTOMERS);
if (!$required_permissions) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$date = request('date');
$provider_id = request('provider_id');
$this->providers_model->delete_working_plan_exception($provider_id, $date);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get Calendar Events
*
* This method will return all the calendar events within a specified period.
*/
public function get_calendar_appointments_for_table_view(): void
{
try {
$required_permissions = can('view', PRIV_APPOINTMENTS);
if (!$required_permissions) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$start_date = request('start_date') . ' 00:00:00';
$end_date = request('end_date') . ' 23:59:59';
$response = [
'appointments' => $this->appointments_model->get([
'start_datetime >=' => $start_date,
'end_datetime <=' => $end_date,
]),
'unavailabilities' => $this->unavailabilities_model->get([
'start_datetime >=' => $start_date,
'end_datetime <=' => $end_date,
]),
];
foreach ($response['appointments'] as &$appointment) {
$appointment['provider'] = $this->providers_model->find($appointment['id_users_provider']);
$appointment['service'] = $this->services_model->find($appointment['id_services']);
$appointment['customer'] = $this->customers_model->find($appointment['id_users_customer']);
}
unset($appointment);
$user_id = session('user_id');
$role_slug = session('role_slug');
// If the current user is a provider he must only see his own appointments.
if ($role_slug === DB_SLUG_PROVIDER) {
foreach ($response['appointments'] as $index => $appointment) {
if ((int) $appointment['id_users_provider'] !== (int) $user_id) {
unset($response['appointments'][$index]);
}
}
$response['appointments'] = array_values($response['appointments']);
foreach ($response['unavailabilities'] as $index => $unavailability) {
if ((int) $unavailability['id_users_provider'] !== (int) $user_id) {
unset($response['unavailabilities'][$index]);
}
}
$response['unavailabilities'] = array_values($response['unavailabilities']);
}
// If the current user is a secretary he must only see the appointments of his providers.
if ($role_slug === DB_SLUG_SECRETARY) {
$providers = $this->secretaries_model->find($user_id)['providers'];
foreach ($response['appointments'] as $index => $appointment) {
if (!in_array((int) $appointment['id_users_provider'], $providers)) {
unset($response['appointments'][$index]);
}
}
$response['appointments'] = array_values($response['appointments']);
foreach ($response['unavailabilities'] as $index => $unavailability) {
if (!in_array((int) $unavailability['id_users_provider'], $providers)) {
unset($response['unavailabilities'][$index]);
}
}
$response['unavailabilities'] = array_values($response['unavailabilities']);
}
foreach ($response['unavailabilities'] as &$unavailability) {
$unavailability['provider'] = $this->providers_model->find($unavailability['id_users_provider']);
}
unset($unavailability);
// Add blocked periods to the response.
$start_date = request('start_date');
$end_date = request('end_date');
$response['blocked_periods'] = $this->blocked_periods_model->get_for_period($start_date, $end_date);
json_response($response);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get the registered appointments for the given date period and record.
*
* This method returns the database appointments and unavailability periods for the user selected date period and
* record type (provider or service).
*/
public function get_calendar_appointments(): void
{
try {
if (cannot('view', PRIV_APPOINTMENTS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$record_id = request('record_id');
$is_all = request('record_id') === FILTER_TYPE_ALL;
$filter_type = request('filter_type');
if (!$filter_type && !$is_all) {
json_response([
'appointments' => [],
'unavailabilities' => [],
]);
return;
}
$record_id = $this->db->escape($record_id);
if ($filter_type == FILTER_TYPE_PROVIDER) {
$where_id = 'id_users_provider';
} elseif ($filter_type === FILTER_TYPE_SERVICE) {
$where_id = 'id_services';
} else {
$where_id = $record_id;
}
// Get appointments
$start_date = $this->db->escape(request('start_date'));
$end_date = $this->db->escape(date('Y-m-d', strtotime(request('end_date') . ' +1 day')));
$where_clause =
$where_id .
' = ' .
$record_id .
'
AND ((start_datetime > ' .
$start_date .
' AND start_datetime < ' .
$end_date .
')
or (end_datetime > ' .
$start_date .
' AND end_datetime < ' .
$end_date .
')
or (start_datetime <= ' .
$start_date .
' AND end_datetime >= ' .
$end_date .
'))
AND is_unavailability = 0
';
$response['appointments'] = $this->appointments_model->get($where_clause);
foreach ($response['appointments'] as &$appointment) {
$appointment['provider'] = $this->providers_model->find($appointment['id_users_provider']);
$appointment['service'] = $this->services_model->find($appointment['id_services']);
$appointment['customer'] = $this->customers_model->find($appointment['id_users_customer']);
}
unset($appointment);
// Get unavailability periods (only for provider).
$response['unavailabilities'] = [];
if ($filter_type == FILTER_TYPE_PROVIDER || $is_all) {
$where_clause =
$where_id .
' = ' .
$record_id .
'
AND ((start_datetime > ' .
$start_date .
' AND start_datetime < ' .
$end_date .
')
or (end_datetime > ' .
$start_date .
' AND end_datetime < ' .
$end_date .
')
or (start_datetime <= ' .
$start_date .
' AND end_datetime >= ' .
$end_date .
'))
AND is_unavailability = 1
';
$response['unavailabilities'] = $this->unavailabilities_model->get($where_clause);
}
$user_id = session('user_id');
$role_slug = session('role_slug');
// If the current user is a provider he must only see his own appointments.
if ($role_slug === DB_SLUG_PROVIDER) {
foreach ($response['appointments'] as $index => $appointment) {
if ((int) $appointment['id_users_provider'] !== (int) $user_id) {
unset($response['appointments'][$index]);
}
}
$response['appointments'] = array_values($response['appointments']);
foreach ($response['unavailabilities'] as $index => $unavailability) {
if ((int) $unavailability['id_users_provider'] !== (int) $user_id) {
unset($response['unavailabilities'][$index]);
}
}
unset($unavailability);
$response['unavailabilities'] = array_values($response['unavailabilities']);
}
// If the current user is a secretary he must only see the appointments of his providers.
if ($role_slug === DB_SLUG_SECRETARY) {
$providers = $this->secretaries_model->find($user_id)['providers'];
foreach ($response['appointments'] as $index => $appointment) {
if (!in_array((int) $appointment['id_users_provider'], $providers)) {
unset($response['appointments'][$index]);
}
}
$response['appointments'] = array_values($response['appointments']);
foreach ($response['unavailabilities'] as $index => $unavailability) {
if (!in_array((int) $unavailability['id_users_provider'], $providers)) {
unset($response['unavailabilities'][$index]);
}
}
$response['unavailabilities'] = array_values($response['unavailabilities']);
}
foreach ($response['unavailabilities'] as &$unavailability) {
$unavailability['provider'] = $this->providers_model->find($unavailability['id_users_provider']);
}
unset($unavailability);
// Add blocked periods to the response.
$start_date = request('start_date');
$end_date = request('end_date');
$response['blocked_periods'] = $this->blocked_periods_model->get_for_period($start_date, $end_date);
json_response($response);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,47 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Captcha controller.
*
* Handles the captcha operations.
*
* @package Controllers
*/
class Captcha extends EA_Controller
{
/**
* Class Constructor
*/
public function __construct()
{
parent::__construct();
$this->load->library('captcha_builder');
}
/**
* Make a request to this method to get a captcha image.
*/
public function index(): void
{
$this->captcha_builder->setDistortion(true);
$this->captcha_builder->setMaxBehindLines(1);
$this->captcha_builder->setMaxFrontLines(1);
$this->captcha_builder->setBackgroundColor(255, 255, 255);
$this->captcha_builder->build();
session(['captcha_phrase' => $this->captcha_builder->getPhrase()]);
header('Content-type: image/jpeg');
$this->captcha_builder->output();
}
}

View File

@ -0,0 +1,73 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.3.2
* ---------------------------------------------------------------------------- */
/**
* Consents controller.
*
* Handles user consent related operations.
*
* @package Controllers
*/
class Consents extends EA_Controller
{
/**
* Consents constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('consents_model');
}
/**
* Save (insert or update) the consent
*/
public function save(): void
{
try {
$consent = request('consent');
$consent['ip'] = $this->input->ip_address();
$occurrences = $this->consents_model->get(['ip' => $consent['ip']], 1, 0, 'create_datetime DESC');
if (!empty($occurrences)) {
$last_consent = $occurrences[0];
$last_consent_create_datetime_instance = new DateTime($last_consent['create_datetime']);
$threshold_datetime_instance = new DateTime('-24 hours');
if ($last_consent_create_datetime_instance > $threshold_datetime_instance) {
// Do not create a new consent.
json_response([
'success' => true,
]);
return;
}
}
$consent['id'] = $this->consents_model->save($consent);
json_response([
'success' => true,
'id' => $consent['id'],
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,193 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.3.2
* ---------------------------------------------------------------------------- */
use Jsvrcek\ICS\Exception\CalendarEventException;
require_once __DIR__ . '/Google.php';
require_once __DIR__ . '/Caldav.php';
/**
* Console controller.
*
* Handles all the Console related operations.
*/
class Console extends EA_Controller
{
/**
* Console constructor.
*/
public function __construct()
{
if (!is_cli()) {
exit('No direct script access allowed');
}
parent::__construct();
$this->load->dbutil();
$this->load->library('instance');
$this->load->model('admins_model');
$this->load->model('customers_model');
$this->load->model('providers_model');
$this->load->model('services_model');
$this->load->model('settings_model');
}
/**
* Perform a console installation.
*
* Use this method to install Easy!Appointments directly from the terminal.
*
* Usage:
*
* php index.php console install
*
* @throws Exception
*/
public function install(): void
{
$this->instance->migrate('fresh');
$password = $this->instance->seed();
response(
PHP_EOL . '⇾ Installation completed, login with "administrator" / "' . $password . '".' . PHP_EOL . PHP_EOL,
);
}
/**
* Migrate the database to the latest state.
*
* Use this method to upgrade an Easy!Appointments instance to the latest database state.
*
* Notice:
*
* Do not use this method to install the app as it will not seed the database with the initial entries (admin,
* provider, service, settings etc.).
*
* Usage:
*
* php index.php console migrate
*
* php index.php console migrate fresh
*
* @param string $type
*/
public function migrate(string $type = ''): void
{
$this->instance->migrate($type);
}
/**
* Seed the database with test data.
*
* Use this method to add test data to your database
*
* Usage:
*
* php index.php console seed
* @throws Exception
*/
public function seed(): void
{
$this->instance->seed();
}
/**
* Create a database backup file.
*
* Use this method to back up your Easy!Appointments data.
*
* Usage:
*
* php index.php console backup
*
* php index.php console backup /path/to/backup/folder
*
* @throws Exception
*/
public function backup(): void
{
$this->instance->backup($GLOBALS['argv'][3] ?? null);
}
/**
* Trigger the synchronization of all provider calendars with Google Calendar.
*
* Use this method in a cronjob to automatically sync events between Easy!Appointments and Google Calendar.
*
* Notice:
*
* Google syncing must first be enabled for each individual provider from inside the backend calendar page.
*
* Usage:
*
* php index.php console sync
*
* @throws CalendarEventException
* @throws Exception
* @throws Throwable
*/
public function sync(): void
{
$providers = $this->providers_model->get();
foreach ($providers as $provider) {
if (filter_var($provider['settings']['google_sync'], FILTER_VALIDATE_BOOLEAN)) {
Google::sync((string) $provider['id']);
}
if (filter_var($provider['settings']['caldav_sync'], FILTER_VALIDATE_BOOLEAN)) {
Caldav::sync((string) $provider['id']);
}
}
}
/**
* Show help information about the console capabilities.
*
* Use this method to see the available commands.
*
* Usage:
*
* php index.php console help
*/
public function help(): void
{
$help = [
'',
'Easy!Appointments ' . config('version'),
'',
'Usage:',
'',
'⇾ php index.php console [command] [arguments]',
'',
'Commands:',
'',
'⇾ php index.php console migrate',
'⇾ php index.php console migrate fresh',
'⇾ php index.php console migrate up',
'⇾ php index.php console migrate down',
'⇾ php index.php console seed',
'⇾ php index.php console install',
'⇾ php index.php console backup',
'⇾ php index.php console sync',
'',
'',
];
response(implode(PHP_EOL, $help));
}
}

View File

@ -0,0 +1,312 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Customers controller.
*
* Handles the customers related operations.
*
* @package Controllers
*/
class Customers extends EA_Controller
{
public array $allowed_customer_fields = [
'id',
'first_name',
'last_name',
'email',
'phone_number',
'address',
'city',
'state',
'zip_code',
'notes',
'timezone',
'language',
'custom_field_1',
'custom_field_2',
'custom_field_3',
'custom_field_4',
'custom_field_5',
'ldap_dn',
];
public array $optional_customer_fields = [
//
];
/**
* Customers constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('customers_model');
$this->load->model('secretaries_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('permissions');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Render the backend customers page.
*
* On this page admin users will be able to manage customers, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('customers')]);
$user_id = session('user_id');
if (cannot('view', PRIV_CUSTOMERS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
$date_format = setting('date_format');
$time_format = setting('time_format');
$require_first_name = setting('require_first_name');
$require_last_name = setting('require_last_name');
$require_email = setting('require_email');
$require_phone_number = setting('require_phone_number');
$require_address = setting('require_address');
$require_city = setting('require_city');
$require_zip_code = setting('require_zip_code');
$secretary_providers = [];
if ($role_slug === DB_SLUG_SECRETARY) {
$secretary = $this->secretaries_model->find($user_id);
$secretary_providers = $secretary['providers'];
}
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'date_format' => $date_format,
'time_format' => $time_format,
'timezones' => $this->timezones->to_array(),
'secretary_providers' => $secretary_providers,
'default_language' => setting('default_language'),
'default_timezone' => setting('default_timezone'),
]);
html_vars([
'page_title' => lang('customers'),
'active_menu' => PRIV_CUSTOMERS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'timezones' => $this->timezones->to_array(),
'grouped_timezones' => $this->timezones->to_grouped_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
'require_first_name' => $require_first_name,
'require_last_name' => $require_last_name,
'require_email' => $require_email,
'require_phone_number' => $require_phone_number,
'require_address' => $require_address,
'require_city' => $require_city,
'require_zip_code' => $require_zip_code,
'available_languages' => config('available_languages'),
]);
$this->load->view('pages/customers');
}
/**
* Find a customer.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
$user_id = session('user_id');
$customer_id = request('customer_id');
if (!$this->permissions->has_customer_access($user_id, $customer_id)) {
abort(403, 'Forbidden');
}
$customer = $this->customers_model->find($customer_id);
json_response($customer);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Filter customers by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$customers = $this->customers_model->search($keyword, $limit, $offset, $order_by);
$user_id = session('user_id');
foreach ($customers as $index => &$customer) {
if (!$this->permissions->has_customer_access($user_id, $customer['id'])) {
unset($customers[$index]);
continue;
}
$appointments = $this->appointments_model->get(['id_users_customer' => $customer['id']]);
foreach ($appointments as &$appointment) {
$this->appointments_model->load($appointment, ['service', 'provider']);
}
$customer['appointments'] = $appointments;
}
json_response(array_values($customers));
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new customer.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
if (session('role_slug') !== DB_SLUG_ADMIN && setting('limit_customer_visibility')) {
abort(403);
}
$customer = request('customer');
$this->customers_model->only($customer, $this->allowed_customer_fields);
$this->customers_model->optional($customer, $this->optional_customer_fields);
$customer_id = $this->customers_model->save($customer);
$customer = $this->customers_model->find($customer_id);
$this->webhooks_client->trigger(WEBHOOK_CUSTOMER_SAVE, $customer);
json_response([
'success' => true,
'id' => $customer_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a customer.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
$user_id = session('user_id');
$customer = request('customer');
if (!$this->permissions->has_customer_access($user_id, $customer['id'])) {
abort(403, 'Forbidden');
}
$this->customers_model->only($customer, $this->allowed_customer_fields);
$this->customers_model->optional($customer, $this->optional_customer_fields);
$customer_id = $this->customers_model->save($customer);
$customer = $this->customers_model->find($customer_id);
$this->webhooks_client->trigger(WEBHOOK_CUSTOMER_SAVE, $customer);
json_response([
'success' => true,
'id' => $customer_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a customer.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
$user_id = session('user_id');
$customer_id = request('customer_id');
if (!$this->permissions->has_customer_access($user_id, $customer_id)) {
abort(403, 'Forbidden');
}
$customer = $this->customers_model->find($customer_id);
$this->customers_model->delete($customer_id);
$this->webhooks_client->trigger(WEBHOOK_CUSTOMER_DELETE, $customer);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,112 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* General settings controller.
*
* Handles general settings related operations.
*
* @package Controllers
*/
class General_settings extends EA_Controller
{
/**
* Calendar constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('settings_model');
$this->load->library('accounts');
$this->load->library('timezones');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('general_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
$available_theme_files = glob(__DIR__ . '/../../assets/css/themes/*.min.css');
$available_themes = array_map(function ($available_theme_file) {
return str_replace('.min.css', '', basename($available_theme_file));
}, $available_theme_files);
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'timezones' => $this->timezones->to_array(),
'general_settings' => $this->settings_model->get(),
]);
html_vars([
'page_title' => lang('settings'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'grouped_timezones' => $this->timezones->to_grouped_array(),
'available_themes' => $available_themes,
]);
$this->load->view('pages/general_settings');
}
/**
* Save general settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('general_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,439 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Google controller.
*
* Handles the Google Calendar synchronization related operations.
*
* @package Controllers
*/
class Google extends EA_Controller
{
/**
* Google constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('google_sync');
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
}
/**
* Complete synchronization of appointments between Google Calendar and Easy!Appointments.
*
* This method will completely sync the appointments of a provider with his Google Calendar account. The sync period
* needs to be relatively small, because a lot of API calls might be necessary and this will lead to consuming the
* Google limit for the Calendar API usage.
*/
public static function sync(?string $provider_id = null): void
{
try {
/** @var EA_Controller $CI */
$CI = get_instance();
$CI->load->library('google_sync');
// Load the libraries as this method is called statically from the CLI command
$CI->load->model('appointments_model');
$CI->load->model('unavailabilities_model');
$CI->load->model('providers_model');
$CI->load->model('services_model');
$CI->load->model('customers_model');
$CI->load->model('settings_model');
$user_id = session('user_id');
if (!$user_id && !is_cli()) {
return;
}
if (!$provider_id) {
throw new InvalidArgumentException('No provider ID provided.');
}
$provider = $CI->providers_model->find($provider_id);
// Check whether the selected provider has the Google Sync enabled.
$google_sync = $CI->providers_model->get_setting($provider['id'], 'google_sync');
if (!$google_sync) {
return; // The selected provider does not have the Google Sync enabled.
}
$google_token = json_decode($provider['settings']['google_token'], true);
$CI->google_sync->refresh_token($google_token['refresh_token']);
// Fetch provider's appointments that belong to the sync time period.
$sync_past_days = $provider['settings']['sync_past_days'];
$sync_future_days = $provider['settings']['sync_future_days'];
$start = strtotime('-' . $sync_past_days . ' days', strtotime(date('Y-m-d')));
$end = strtotime('+' . $sync_future_days . ' days', strtotime(date('Y-m-d')));
$where = [
'start_datetime >=' => date('Y-m-d H:i:s', $start),
'end_datetime <=' => date('Y-m-d H:i:s', $end),
'id_users_provider' => $provider['id'],
];
$appointments = $CI->appointments_model->get($where);
$unavailabilities = $CI->unavailabilities_model->get($where);
$local_events = [...$appointments, ...$unavailabilities];
$company_color = setting('company_color');
$settings = [
'company_name' => setting('company_name'),
'company_link' => setting('company_link'),
'company_email' => setting('company_email'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
];
$provider_timezone = new DateTimeZone($provider['timezone']);
// Sync each appointment with Google Calendar by following the project's sync protocol (see documentation).
foreach ($local_events as $local_event) {
if (!$local_event['is_unavailability']) {
$service = $CI->services_model->find($local_event['id_services']);
$customer = $CI->customers_model->find($local_event['id_users_customer']);
$events_model = $CI->appointments_model;
} else {
$service = null;
$customer = null;
$events_model = $CI->unavailabilities_model;
}
// If current appointment not synced yet, add to Google Calendar.
if (!$local_event['id_google_calendar']) {
if (!$local_event['is_unavailability']) {
$google_event = $CI->google_sync->add_appointment(
$local_event,
$provider,
$service,
$customer,
$settings,
);
} else {
$google_event = $CI->google_sync->add_unavailability($provider, $local_event);
}
$local_event['id_google_calendar'] = $google_event->getId();
$events_model->save($local_event); // Save the Google Calendar ID.
continue;
}
// Appointment is synced with Google Calendar.
try {
$google_event = $CI->google_sync->get_event($provider, $local_event['id_google_calendar']);
if ($google_event->getStatus() == 'cancelled') {
throw new Exception('Event is cancelled, remove the record from Easy!Appointments.');
}
// If Google Calendar event is different from Easy!Appointments appointment then update Easy!Appointments record.
$local_event_start = strtotime($local_event['start_datetime']);
$local_event_end = strtotime($local_event['end_datetime']);
$google_event_start = new DateTime(
$google_event->getStart()->getDateTime() ?? $google_event->getEnd()->getDate(),
);
$google_event_start->setTimezone($provider_timezone);
$google_event_end = new DateTime(
$google_event->getEnd()->getDateTime() ?? $google_event->getEnd()->getDate(),
);
$google_event_end->setTimezone($provider_timezone);
$google_event_notes = $local_event['is_unavailability']
? $google_event->getSummary() . ' ' . $google_event->getDescription()
: $google_event->getDescription();
$is_different =
$local_event_start !== $google_event_start->getTimestamp() ||
$local_event_end !== $google_event_end->getTimestamp() ||
$local_event['notes'] !== $google_event_notes;
if ($is_different) {
$local_event['start_datetime'] = $google_event_start->format('Y-m-d H:i:s');
$local_event['end_datetime'] = $google_event_end->format('Y-m-d H:i:s');
$local_event['notes'] = $google_event_notes;
$events_model->save($local_event);
}
} catch (Throwable) {
// Appointment not found on Google Calendar, delete from Easy!Appointments.
$events_model->delete($local_event['id']);
$local_event['id_google_calendar'] = null;
}
}
// Add Google Calendar events that do not exist in Easy!Appointments.
$google_calendar = $provider['settings']['google_calendar'];
try {
$google_events = $CI->google_sync->get_sync_events($google_calendar, $start, $end);
} catch (Throwable $e) {
if ($e->getCode() === 404) {
log_message('error', 'Google - Remote Calendar not found for provider ID: ' . $provider_id);
return; // The remote calendar was not found.
} else {
throw $e;
}
}
foreach ($google_events->getItems() as $google_event) {
if ($google_event->getStatus() === 'cancelled') {
continue;
}
if ($google_event->getStart() === null || $google_event->getEnd() === null) {
continue;
}
if ($google_event->getStart()->getDateTime() === $google_event->getEnd()->getDateTime()) {
continue;
}
$google_event_start = new DateTime($google_event->getStart()->getDateTime());
$google_event_start->setTimezone($provider_timezone);
$google_event_end = new DateTime($google_event->getEnd()->getDateTime());
$google_event_end->setTimezone($provider_timezone);
$appointment_results = $CI->appointments_model->get(['id_google_calendar' => $google_event->getId()]);
if (!empty($appointment_results)) {
continue;
}
$unavailability_results = $CI->unavailabilities_model->get([
'id_google_calendar' => $google_event->getId(),
]);
if (!empty($unavailability_results)) {
continue;
}
// Record doesn't exist in the Easy!Appointments, so add the event now.
$local_event = [
'start_datetime' => $google_event_start->format('Y-m-d H:i:s'),
'end_datetime' => $google_event_end->format('Y-m-d H:i:s'),
'is_unavailability' => true,
'location' => $google_event->getLocation(),
'notes' => $google_event->getSummary() . ' ' . $google_event->getDescription(),
'id_users_provider' => $provider_id,
'id_google_calendar' => $google_event->getId(),
'id_users_customer' => null,
'id_services' => null,
];
$CI->unavailabilities_model->save($local_event);
}
json_response([
'success' => true,
]);
} catch (Throwable $e) {
log_message(
'error',
'Google - Sync completed with an error (provider ID "' . $provider_id . '"): ' . $e->getMessage(),
);
json_exception($e);
}
}
/**
* Authorize Google Calendar API usage for a specific provider.
*
* Since it is required to follow the web application flow, in order to retrieve a refresh token from the Google API
* service, this method is going to authorize the given provider.
*
* @param string $provider_id The provider id, for whom the sync authorization is made.
*/
public function oauth(string $provider_id): void
{
if (!$this->session->userdata('user_id')) {
show_error('Forbidden', 403);
}
// Store the provider id for use on the callback function.
session(['oauth_provider_id' => $provider_id]);
// Redirect browser to google user content page.
header('Location: ' . $this->google_sync->get_auth_url());
}
/**
* Callback method for the Google Calendar API authorization process.
*
* Once the user grants consent with his Google Calendar data usage, the Google OAuth service will redirect him back
* in this page. Here we are going to store the refresh token, because this is what will be used to generate access
* tokens in the future.
*
* IMPORTANT: Because it is necessary to authorize the application using the web server flow (see official
* documentation of OAuth), every Easy!Appointments installation should use its own calendar api key. So in every
* api console account, the "http://path-to-Easy!Appointments/google/oauth_callback" should be included in an
* allowed redirect URL.
*
* @throws Exception
*/
public function oauth_callback(): void
{
if (!session('user_id')) {
abort(403, 'Forbidden');
}
$code = request('code');
if (empty($code)) {
response('Code authorization failed.');
return;
}
$token = $this->google_sync->authenticate($code);
if (empty($token)) {
response('Token authorization failed.');
return;
}
// Store the token into the database for future reference.
$oauth_provider_id = session('oauth_provider_id');
if ($oauth_provider_id) {
$this->providers_model->set_setting($oauth_provider_id, 'google_sync', true);
$this->providers_model->set_setting($oauth_provider_id, 'google_token', json_encode($token));
$this->providers_model->set_setting($oauth_provider_id, 'google_calendar', 'primary');
} else {
response('Sync provider id not specified.');
}
}
/**
* This method will return a list of the available Google Calendars.
*
* The user will need to select a specific calendar from this list to sync his appointments with. Google access must
* be already granted for the specific provider.
*/
public function get_google_calendars(): void
{
try {
$provider_id = (int) request('provider_id');
if (empty($provider_id)) {
throw new Exception('Provider id is required in order to fetch the google calendars.');
}
// Check if selected provider has sync enabled.
$google_sync = $this->providers_model->get_setting($provider_id, 'google_sync');
if (!$google_sync) {
json_response([
'success' => false,
]);
return;
}
$google_token = json_decode($this->providers_model->get_setting($provider_id, 'google_token'), true);
$this->google_sync->refresh_token($google_token['refresh_token']);
$calendars = $this->google_sync->get_google_calendars();
json_response($calendars);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Select a specific google calendar for a provider.
*
* All the appointments will be synced with this particular calendar.
*/
public function select_google_calendar(): void
{
try {
$provider_id = request('provider_id');
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$calendar_id = request('calendar_id');
$this->providers_model->set_setting($provider_id, 'google_calendar', $calendar_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Disable a providers sync setting.
*
* This method deletes the "google_sync" and "google_token" settings from the database.
*
* After that the provider's appointments will be no longer synced with Google Calendar.
*/
public function disable_provider_sync(): void
{
try {
$provider_id = request('provider_id');
if (!$provider_id) {
throw new Exception('Provider id not specified.');
}
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$this->providers_model->set_setting($provider_id, 'google_sync', false);
$this->providers_model->set_setting($provider_id, 'google_token');
$this->appointments_model->clear_google_sync_ids($provider_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,102 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Google Analytics settings controller.
*
* Handles Google Analytics settings related operations.
*
* @package Controllers
*/
class Google_analytics_settings extends EA_Controller
{
/**
* Google_analytics_settings constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('settings_model');
$this->load->library('accounts');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('google_analytics_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'google_analytics_settings' => $this->settings_model->get('name like "google_analytics_%"'),
]);
html_vars([
'page_title' => lang('google_analytics'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
]);
$this->load->view('pages/google_analytics_settings');
}
/**
* Save general settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('google_analytics_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,140 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.1.0
* ---------------------------------------------------------------------------- */
/**
* Installation controller.
*
* Handles the installation related operations.
*
* @package Controllers
*/
class Installation extends EA_Controller
{
/**
* Installation constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('admins_model');
$this->load->model('settings_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('customers_model');
$this->load->library('instance');
}
/**
* Display the installation page.
*/
public function index(): void
{
if (is_app_installed()) {
redirect();
return;
}
$this->load->view('pages/installation', [
'base_url' => config('base_url'),
]);
}
/**
* Installs Easy!Appointments on the server.
*/
public function perform(): void
{
try {
if (is_app_installed()) {
return;
}
$admin = request('admin');
$company = request('company');
$this->instance->migrate();
// Insert admin
$admin['timezone'] = date_default_timezone_get();
$admin['settings']['username'] = $admin['username'];
$admin['settings']['password'] = $admin['password'];
$admin['settings']['notifications'] = true;
$admin['settings']['calendar_view'] = CALENDAR_VIEW_DEFAULT;
unset($admin['username'], $admin['password']);
$admin['id'] = $this->admins_model->save($admin);
session([
'user_id' => $admin['id'],
'user_email' => $admin['email'],
'role_slug' => DB_SLUG_ADMIN,
'language' => $admin['language'],
'timezone' => $admin['timezone'],
'username' => $admin['settings']['username'],
]);
// Save company settings
setting([
'company_name' => $company['company_name'],
'company_email' => $company['company_email'],
'company_link' => $company['company_link'],
]);
// Service
$service_id = $this->services_model->save([
'name' => 'Service',
'duration' => '30',
'price' => '0',
'currency' => '',
'availabilities_type' => 'flexible',
'attendants_number' => '1',
]);
// Provider
$this->providers_model->save([
'first_name' => 'Jane',
'last_name' => 'Doe',
'email' => 'jane@example.org',
'phone_number' => '+1 (000) 000-0000',
'services' => [$service_id],
'language' => $admin['language'],
'timezone' => $admin['timezone'],
'settings' => [
'username' => 'janedoe',
'password' => random_string(),
'working_plan' => setting('company_working_plan'),
'notifications' => true,
'google_sync' => false,
'sync_past_days' => 30,
'sync_future_days' => 90,
'calendar_view' => CALENDAR_VIEW_DEFAULT,
],
]);
// Customer
$this->customers_model->save([
'first_name' => 'James',
'last_name' => 'Doe',
'email' => 'james@example.org',
'phone_number' => '+1 (000) 000-0000',
]);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,74 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Integrations controller.
*
* Displays the integrations page.
*
* @package Controllers
*/
class Integrations extends EA_Controller
{
/**
* Integrations constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('customers_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
$this->load->model('settings_model');
$this->load->library('accounts');
$this->load->library('google_sync');
$this->load->library('notifications');
$this->load->library('synchronization');
$this->load->library('timezones');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('about')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
html_vars([
'page_title' => lang('integrations'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
]);
$this->load->view('pages/integrations');
}
}

View File

@ -0,0 +1,132 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* LDAP settings controller.
*
* Handles LDAP settings related operations.
*
* @package Controllers
*/
class Ldap_settings extends EA_Controller
{
/**
* Ldap_settings constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('settings_model');
$this->load->library('accounts');
$this->load->library('ldap_client');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('ldap_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'ldap_settings' => $this->settings_model->get('name like "ldap_%"'),
'ldap_default_filter' => LDAP_DEFAULT_FILTER,
'ldap_default_field_mapping' => LDAP_DEFAULT_FIELD_MAPPING,
]);
html_vars([
'page_title' => lang('ldap'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'roles' => $this->roles_model->get(),
]);
$this->load->view('pages/ldap_settings');
}
/**
* Save general settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('ldap_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Search the LDAP directory.
*
* @return void
*/
public function search(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
if (!extension_loaded('ldap')) {
throw new RuntimeException('The LDAP extension is not loaded.');
}
$keyword = request('keyword');
$entries = $this->ldap_client->search($keyword);
json_response($entries);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,102 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Client form controller.
*
* Handles legal contents settings related operations.
*
* @package Controllers
*/
class Legal_settings extends EA_Controller
{
/**
* Legal_contents constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('settings_model');
$this->load->library('accounts');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('legal_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'legal_settings' => $this->settings_model->get(),
]);
html_vars([
'page_title' => lang('settings'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
]);
$this->load->view('pages/legal_settings');
}
/**
* Save legal settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('legal_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,55 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.3
* ---------------------------------------------------------------------------- */
/**
* Localization Controller
*
* Contains all the localization related methods.
*
* @package Controllers
*/
class Localization extends EA_Controller
{
/**
* Change system language for current user.
*
* The language setting is stored in session data and retrieved every time the user visits any of the system pages.
*
* Notice: This method used to be in the Backend_api.php.
*/
public function change_language(): void
{
try {
// Check if language exists in the available languages.
$language = request('language');
if (!in_array($language, config('available_languages'))) {
throw new RuntimeException(
'Translations for the given language does not exist (' . request('language') . ').',
);
}
$language = request('language');
session(['language' => $language]);
config(['language' => $language]);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,98 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Login controller.
*
* Handles the login page functionality.
*
* @package Controllers
*/
class Login extends EA_Controller
{
/**
* Login constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('accounts');
$this->load->library('ldap_client');
$this->load->library('email_messages');
script_vars([
'dest_url' => session('dest_url', site_url('calendar')),
]);
}
/**
* Render the login page.
*/
public function index(): void
{
if (session('user_id')) {
redirect('calendar');
return;
}
html_vars([
'page_title' => lang('login'),
'base_url' => config('base_url'),
'dest_url' => session('dest_url', site_url('calendar')),
'company_name' => setting('company_name'),
]);
$this->load->view('pages/login');
}
/**
* Validate the provided credentials and start a new session if the validation was successful.
*/
public function validate(): void
{
try {
$username = request('username');
if (empty($username)) {
throw new InvalidArgumentException('No username value provided.');
}
$password = request('password');
if (empty($password)) {
throw new InvalidArgumentException('No password value provided.');
}
$user_data = $this->accounts->check_login($username, $password);
if (empty($user_data)) {
$user_data = $this->ldap_client->check_login($username, $password);
}
if (empty($user_data)) {
throw new InvalidArgumentException(lang('invalid_credentials_provided'));
}
$this->session->sess_regenerate();
session($user_data); // Save data in the session.
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,39 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Logout controller.
*
* Handles the logout page functionality.
*
* @package Controllers
*/
class Logout extends EA_Controller
{
/**
* Render the logout page.
*/
public function index(): void
{
$this->session->sess_destroy();
$company_name = setting('company_name');
html_vars([
'page_title' => lang('log_out'),
'company_name' => $company_name,
]);
$this->load->view('pages/logout');
}
}

View File

@ -0,0 +1,102 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Matomo Analytics settings controller.
*
* Handles Matomo Analytics settings related operations.
*
* @package Controllers
*/
class Matomo_analytics_settings extends EA_Controller
{
/**
* Matomo_analytics_settings constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('settings_model');
$this->load->library('accounts');
}
/**
* Render the settings page.
*/
public function index(): void
{
session(['dest_url' => site_url('matomo_analytics_settings')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'matomo_analytics_settings' => $this->settings_model->get('name like "matomo_analytics_%"'),
]);
html_vars([
'page_title' => lang('matomo_analytics'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
]);
$this->load->view('pages/matomo_analytics_settings');
}
/**
* Save general settings.
*/
public function save(): void
{
try {
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$settings = request('matomo_analytics_settings', []);
foreach ($settings as $setting) {
$existing_setting = $this->settings_model
->query()
->where('name', $setting['name'])
->get()
->row_array();
if (!empty($existing_setting)) {
$setting['id'] = $existing_setting['id'];
}
$this->settings_model->save($setting);
}
response();
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,70 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.3.2
* ---------------------------------------------------------------------------- */
/**
* Privacy controller.
*
* Handles the privacy related operations.
*
* @package Controllers
*/
class Privacy extends EA_Controller
{
/**
* Privacy constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->driver('cache', ['adapter' => 'file']);
$this->load->model('customers_model');
}
/**
* Remove all customer data (including appointments) from the system.
*/
public function delete_personal_information(): void
{
try {
$display_delete_personal_information = setting('display_delete_personal_information');
if (!$display_delete_personal_information) {
abort(403, 'Forbidden');
}
$customer_token = request('customer_token');
if (empty($customer_token)) {
throw new InvalidArgumentException('Invalid customer token value provided.');
}
$customer_id = $this->cache->get('customer-token-' . $customer_token);
if (empty($customer_id)) {
throw new InvalidArgumentException(
'Customer ID does not exist, please reload the page ' . 'and try again.',
);
}
$this->customers_model->delete($customer_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,279 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Providers controller.
*
* Handles the providers related operations.
*
* @package Controllers
*/
class Providers extends EA_Controller
{
public array $allowed_provider_fields = [
'id',
'first_name',
'last_name',
'email',
'alt_number',
'phone_number',
'address',
'city',
'state',
'zip_code',
'notes',
'timezone',
'language',
'is_private',
'ldap_dn',
'id_roles',
'settings',
'services',
];
public array $optional_provider_fields = [
'services' => [],
];
public array $allowed_provider_setting_fields = [
'username',
'password',
'working_plan',
'working_plan_exceptions',
'notifications',
'calendar_view',
];
public array $optional_provider_setting_fields = [
'working_plan' => null,
'working_plan_exceptions' => '{}',
];
public array $allowed_service_fields = ['id', 'name'];
/**
* Providers constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('providers_model');
$this->load->model('services_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
$this->optional_provider_setting_fields['working_plan'] = setting('company_working_plan');
}
/**
* Render the backend providers page.
*
* On this page admin users will be able to manage providers, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('providers')]);
$user_id = session('user_id');
if (cannot('view', PRIV_USERS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
$services = $this->services_model->get();
foreach ($services as &$service) {
$this->services_model->only($service, $this->allowed_service_fields);
}
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'company_working_plan' => setting('company_working_plan'),
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
'first_weekday' => setting('first_weekday'),
'min_password_length' => MIN_PASSWORD_LENGTH,
'timezones' => $this->timezones->to_array(),
'services' => $services,
'default_language' => setting('default_language'),
'default_timezone' => setting('default_timezone'),
]);
html_vars([
'page_title' => lang('providers'),
'active_menu' => PRIV_USERS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'grouped_timezones' => $this->timezones->to_grouped_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
'services' => $this->services_model->get(),
]);
$this->load->view('pages/providers');
}
/**
* Filter providers by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$providers = $this->providers_model->search($keyword, $limit, $offset, $order_by);
json_response($providers);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new provider.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$provider = request('provider');
$this->providers_model->only($provider, $this->allowed_provider_fields);
$this->providers_model->only($provider['settings'], $this->allowed_provider_setting_fields);
$this->providers_model->optional($provider, $this->optional_provider_fields);
$this->providers_model->optional($provider['settings'], $this->optional_provider_setting_fields);
$provider_id = $this->providers_model->save($provider);
$provider = $this->providers_model->find($provider_id);
$this->webhooks_client->trigger(WEBHOOK_PROVIDER_SAVE, $provider);
json_response([
'success' => true,
'id' => $provider_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find a provider.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$provider_id = request('provider_id');
$provider = $this->providers_model->find($provider_id);
json_response($provider);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a provider.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$provider = request('provider');
$this->providers_model->only($provider, $this->allowed_provider_fields);
$this->providers_model->only($provider['settings'], $this->allowed_provider_setting_fields);
$this->providers_model->optional($provider, $this->optional_provider_fields);
$this->providers_model->optional($provider['settings'], $this->optional_provider_setting_fields);
$provider_id = $this->providers_model->save($provider);
$provider = $this->providers_model->find($provider_id);
$this->webhooks_client->trigger(WEBHOOK_PROVIDER_SAVE, $provider);
json_response([
'success' => true,
'id' => $provider_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a provider.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$provider_id = request('provider_id');
$provider = $this->providers_model->find($provider_id);
$this->providers_model->delete($provider_id);
$this->webhooks_client->trigger(WEBHOOK_PROVIDER_DELETE, $provider);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,90 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Recovery controller.
*
* Handles the recovery page functionality.
*
* @package Controllers
*/
class Recovery extends EA_Controller
{
/**
* User constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('accounts');
$this->load->library('email_messages');
}
/**
* Display the password recovery page.
*/
public function index(): void
{
$company_name = setting('company_name');
html_vars([
'page_title' => lang('forgot_your_password'),
'dest_url' => session('dest_url', site_url('backend')),
'company_name' => $company_name,
]);
$this->load->view('pages/recovery');
}
/**
* Recover the user password and notify the user via email.
*/
public function perform(): void
{
try {
$username = request('username');
if (empty($username)) {
throw new InvalidArgumentException('No username value provided.');
}
$email = request('email');
if (empty($email)) {
throw new InvalidArgumentException('No email value provided.');
}
$new_password = $this->accounts->regenerate_password($username, $email);
$company_color = setting('company_color');
if ($new_password) {
$settings = [
'company_name' => setting('company_name'),
'company_link' => setting('company_link'),
'company_email' => setting('company_email'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
];
$this->email_messages->send_password($new_password, $email, $settings);
}
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,254 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Secretaries controller.
*
* Handles the secretaries related operations.
*
* @package Controllers
*/
class Secretaries extends EA_Controller
{
public array $allowed_provider_fields = ['id', 'first_name', 'last_name'];
public array $allowed_secretary_fields = [
'id',
'first_name',
'last_name',
'email',
'alt_number',
'phone_number',
'address',
'city',
'state',
'zip_code',
'notes',
'timezone',
'language',
'is_private',
'ldap_dn',
'id_roles',
'settings',
'providers',
];
public array $allowed_secretary_setting_fields = ['username', 'password', 'notifications', 'calendar_view'];
public array $optional_secretary_fields = [
'providers' => [],
];
/**
* Secretaries constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('secretaries_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Render the backend secretaries page.
*
* On this page secretary users will be able to manage secretaries, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('secretaries')]);
$user_id = session('user_id');
if (cannot('view', PRIV_USERS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
$providers = $this->providers_model->get();
foreach ($providers as &$provider) {
$this->providers_model->only($provider, $this->allowed_provider_fields);
}
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'timezones' => $this->timezones->to_array(),
'min_password_length' => MIN_PASSWORD_LENGTH,
'providers' => $providers,
'default_language' => setting('default_language'),
'default_timezone' => setting('default_timezone'),
]);
html_vars([
'page_title' => lang('secretaries'),
'active_menu' => PRIV_USERS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'grouped_timezones' => $this->timezones->to_grouped_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
'providers' => $this->providers_model->get(),
]);
$this->load->view('pages/secretaries');
}
/**
* Filter secretaries by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$secretaries = $this->secretaries_model->search($keyword, $limit, $offset, $order_by);
json_response($secretaries);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new secretary.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$secretary = request('secretary');
$this->secretaries_model->only($secretary, $this->allowed_secretary_fields);
$this->secretaries_model->only($secretary['settings'], $this->allowed_secretary_setting_fields);
$this->secretaries_model->optional($secretary, $this->optional_secretary_fields);
$secretary_id = $this->secretaries_model->save($secretary);
$secretary = $this->secretaries_model->find($secretary_id);
$this->webhooks_client->trigger(WEBHOOK_SECRETARY_SAVE, $secretary);
json_response([
'success' => true,
'id' => $secretary_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find a secretary.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$secretary_id = request('secretary_id');
$secretary = $this->secretaries_model->find($secretary_id);
json_response($secretary);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a secretary.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$secretary = request('secretary');
$this->secretaries_model->only($secretary, $this->allowed_secretary_fields);
$this->secretaries_model->only($secretary['settings'], $this->allowed_secretary_setting_fields);
$this->secretaries_model->optional($secretary, $this->optional_secretary_fields);
$secretary_id = $this->secretaries_model->save($secretary);
$secretary = $this->secretaries_model->find($secretary_id);
$this->webhooks_client->trigger(WEBHOOK_SECRETARY_SAVE, $secretary);
json_response([
'success' => true,
'id' => $secretary_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a secretary.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_USERS)) {
abort(403, 'Forbidden');
}
$secretary_id = request('secretary_id');
$secretary = $this->secretaries_model->find($secretary_id);
$this->secretaries_model->delete($secretary_id);
$this->webhooks_client->trigger(WEBHOOK_SECRETARY_DELETE, $secretary);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,215 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Service-categories controller.
*
* Handles the service-categories related operations.
*
* @package Controllers
*/
class Service_categories extends EA_Controller
{
public array $allowed_service_category_fields = ['id', 'name', 'description'];
public array $optional_service_category_fields = [];
/**
* Service-categories constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('service_categories_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Render the backend service-categories page.
*
* On this page admin users will be able to manage service-categories, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('service_categories')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SERVICES)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
]);
html_vars([
'page_title' => lang('service_categories'),
'active_menu' => PRIV_SERVICES,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'timezones' => $this->timezones->to_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
]);
$this->load->view('pages/service_categories');
}
/**
* Filter service-categories by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$service_categories = $this->service_categories_model->search($keyword, $limit, $offset, $order_by);
json_response($service_categories);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new service-category.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service_category = request('service_category');
$this->service_categories_model->only($service_category, $this->allowed_service_category_fields);
$this->service_categories_model->optional($service_category, $this->optional_service_category_fields);
$service_category_id = $this->service_categories_model->save($service_category);
$service_category = $this->service_categories_model->find($service_category_id);
$this->webhooks_client->trigger(WEBHOOK_SERVICE_CATEGORY_SAVE, $service_category);
json_response([
'success' => true,
'id' => $service_category_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find a service-category.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service_category_id = request('service_category_id');
$service_category = $this->service_categories_model->find($service_category_id);
json_response($service_category);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a service-category.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service_category = request('service_category');
$this->service_categories_model->only($service_category, $this->allowed_service_category_fields);
$this->service_categories_model->optional($service_category, $this->optional_service_category_fields);
$service_category_id = $this->service_categories_model->save($service_category);
$service_category = $this->service_categories_model->find($service_category_id);
$this->webhooks_client->trigger(WEBHOOK_SERVICE_CATEGORY_SAVE, $service_category);
json_response([
'success' => true,
'id' => $service_category_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a service-category.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service_category_id = request('service_category_id');
$service_category = $this->service_categories_model->find($service_category_id);
$this->service_categories_model->delete($service_category_id);
$this->webhooks_client->trigger(WEBHOOK_SERVICE_CATEGORY_DELETE, $service_category);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,230 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Services controller.
*
* Handles the services related operations.
*
* @package Controllers
*/
class Services extends EA_Controller
{
public array $allowed_service_fields = [
'id',
'name',
'duration',
'price',
'currency',
'description',
'color',
'location',
'availabilities_type',
'attendants_number',
'is_private',
'id_service_categories',
];
public array $optional_service_fields = [
'id_service_categories' => null,
];
/**
* Services constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('services_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Render the backend services page.
*
* On this page admin users will be able to manage services, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('services')]);
$user_id = session('user_id');
if (cannot('view', PRIV_SERVICES)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
'event_minimum_duration' => EVENT_MINIMUM_DURATION,
]);
html_vars([
'page_title' => lang('services'),
'active_menu' => PRIV_SERVICES,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'timezones' => $this->timezones->to_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
]);
$this->load->view('pages/services');
}
/**
* Filter services by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$services = $this->services_model->search($keyword, $limit, $offset, $order_by);
json_response($services);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new service.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service = request('service');
$this->services_model->only($service, $this->allowed_service_fields);
$this->services_model->optional($service, $this->optional_service_fields);
$service_id = $this->services_model->save($service);
$service = $this->services_model->find($service_id);
$this->webhooks_client->trigger(WEBHOOK_SERVICE_SAVE, $service);
json_response([
'success' => true,
'id' => $service_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find a service.
*/
public function find(): void
{
try {
if (cannot('delete', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service_id = request('service_id');
$service = $this->services_model->find($service_id);
json_response($service);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a service.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service = request('service');
$this->services_model->only($service, $this->allowed_service_fields);
$this->services_model->optional($service, $this->optional_service_fields);
$service_id = $this->services_model->save($service);
$service = $this->services_model->find($service_id);
$this->webhooks_client->trigger(WEBHOOK_SERVICE_SAVE, $service);
json_response([
'success' => true,
'id' => $service_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a service.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_SERVICES)) {
abort(403, 'Forbidden');
}
$service_id = request('service_id');
$service = $this->services_model->find($service_id);
$this->services_model->delete($service_id);
$this->webhooks_client->trigger(WEBHOOK_SERVICE_DELETE, $service);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,39 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/*
* This file can only be used in a testing environment and only from the terminal.
*/
if (ENVIRONMENT !== 'testing' || !is_cli()) {
show_404();
}
/**
* Test controller.
*
* This controller does not have or need any logic, it is just used so that CI can be loaded properly during the test
* execution.
*/
class Test extends EA_Controller
{
/**
* Placeholder callback.
*
* @return void
*/
public function index(): void
{
//
}
}

View File

@ -0,0 +1,193 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Unavailabilities controller.
*
* Handles the unavailabilities related operations.
*
* @package Controllers
*/
class Unavailabilities extends EA_Controller
{
public array $allowed_unavailability_fields = [
'id',
'start_datetime',
'end_datetime',
'location',
'notes',
'is_unavailability',
'id_users_provider',
];
public array $optional_unavailability_fields = [
//
];
/**
* Unavailabilities constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('unavailabilities_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
$this->load->library('webhooks_client');
}
/**
* Filter unavailabilities by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$unavailabilities = $this->unavailabilities_model->search($keyword, $limit, $offset, $order_by);
json_response($unavailabilities);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new unavailability.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$unavailability = request('unavailability');
$this->unavailabilities_model->only($unavailability, $this->allowed_unavailability_fields);
$this->unavailabilities_model->optional($unavailability, $this->optional_unavailability_fields);
$unavailability_id = $this->unavailabilities_model->save($unavailability);
$unavailability = $this->unavailabilities_model->find($unavailability_id);
$provider = $this->providers_model->find($unavailability['id_users_provider']);
$this->synchronization->sync_unavailability_saved($unavailability, $provider);
$this->webhooks_client->trigger(WEBHOOK_UNAVAILABILITY_SAVE, $unavailability);
json_response([
'success' => true,
'id' => $unavailability_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find an unavailability.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$unavailability_id = request('unavailability_id');
$unavailability = $this->unavailabilities_model->find($unavailability_id);
json_response($unavailability);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a unavailability.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$unavailability = request('unavailability');
$this->unavailabilities_model->only($unavailability, $this->allowed_unavailability_fields);
$this->unavailabilities_model->optional($unavailability, $this->optional_unavailability_fields);
$unavailability_id = $this->unavailabilities_model->save($unavailability);
$unavailability = $this->unavailabilities_model->find($unavailability_id);
$provider = $this->providers_model->find($unavailability['id_users_provider']);
$this->synchronization->sync_unavailability_saved($unavailability, $provider);
$this->webhooks_client->trigger(WEBHOOK_UNAVAILABILITY_SAVE, $unavailability);
json_response([
'success' => true,
'id' => $unavailability_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a unavailability.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$unavailability_id = request('unavailability_id');
$unavailability = $this->unavailabilities_model->find($unavailability_id);
$this->unavailabilities_model->delete($unavailability_id);
$this->webhooks_client->trigger(WEBHOOK_UNAVAILABILITY_DELETE, $unavailability);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,74 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.1.0
* ---------------------------------------------------------------------------- */
/**
* Update controller.
*
* Handles the update related operations.
*
* @package Controllers
*/
class Update extends EA_Controller
{
/**
* Update constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('admins_model');
$this->load->model('settings_model');
$this->load->model('services_model');
$this->load->model('providers_model');
$this->load->model('customers_model');
$this->load->library('instance');
}
/**
* This method will update the instance to the latest available version in the server.
*
* IMPORTANT: The code files must exist in the server, this method will not fetch any new files but will update
* the database schema.
*
* This method can be used either by loading the page in the browser or by an ajax request. But it will answer with
* JSON encoded data.
*/
public function index(): void
{
try {
$user_id = session('user_id');
if (cannot('edit', PRIV_SYSTEM_SETTINGS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$this->instance->migrate();
$view = ['success' => true];
} catch (Throwable $e) {
$view = ['success' => false, 'exception' => $e->getMessage()];
}
html_vars($view);
$this->load->view('pages/update');
}
}

View File

@ -0,0 +1,71 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* User controller.
*
* Handles the user related operations.
*
* @package Controllers
*/
class User extends EA_Controller
{
/**
* User constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('accounts');
$this->load->library('email_messages');
}
/**
* Redirect to the login page.
*/
public function index(): void
{
redirect('login');
}
/**
* Display the login page.
*
* @deprecated Since 1.5 Use the Login controller instead.
*/
public function login(): void
{
redirect('login');
}
/**
* Display the logout page.
*
* @deprecated Since 1.5 Use the Logout controller instead.
*/
public function logout(): void
{
redirect('logout');
}
/**
* Display the password recovery page.
*
* @deprecated Since 1.5 Use the Logout controller instead.
*/
public function forgot_password(): void
{
redirect('recovery');
}
}

View File

@ -0,0 +1,233 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Webhooks controller.
*
* Handles the webhooks related operations.
*
* @package Controllers
*/
class Webhooks extends EA_Controller
{
public array $allowed_webhook_fields = [
'id',
'name',
'url',
'actions',
'secret_header',
'secret_token',
'is_ssl_verified',
'notes',
];
public array $optional_webhook_fields = [
//
];
/**
* Webhooks constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('webhooks_model');
$this->load->model('roles_model');
$this->load->library('accounts');
$this->load->library('timezones');
}
/**
* Render the backend webhooks page.
*
* On this page admin users will be able to manage webhooks, which are eventually selected by customers during the
* booking process.
*/
public function index(): void
{
session(['dest_url' => site_url('webhooks')]);
$user_id = session('user_id');
if (cannot('view', PRIV_WEBHOOKS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
redirect('login');
return;
}
$role_slug = session('role_slug');
script_vars([
'user_id' => $user_id,
'role_slug' => $role_slug,
]);
html_vars([
'page_title' => lang('webhooks'),
'active_menu' => PRIV_SYSTEM_SETTINGS,
'user_display_name' => $this->accounts->get_user_display_name($user_id),
'timezones' => $this->timezones->to_array(),
'privileges' => $this->roles_model->get_permissions_by_slug($role_slug),
'available_actions' => [
WEBHOOK_APPOINTMENT_SAVE,
WEBHOOK_APPOINTMENT_DELETE,
WEBHOOK_UNAVAILABILITY_SAVE,
WEBHOOK_UNAVAILABILITY_DELETE,
WEBHOOK_BLOCKED_PERIOD_SAVE,
WEBHOOK_BLOCKED_PERIOD_DELETE,
WEBHOOK_CUSTOMER_SAVE,
WEBHOOK_CUSTOMER_DELETE,
WEBHOOK_SERVICE_SAVE,
WEBHOOK_SERVICE_DELETE,
WEBHOOK_SERVICE_CATEGORY_SAVE,
WEBHOOK_SERVICE_CATEGORY_DELETE,
WEBHOOK_PROVIDER_SAVE,
WEBHOOK_PROVIDER_DELETE,
WEBHOOK_SECRETARY_SAVE,
WEBHOOK_SECRETARY_DELETE,
WEBHOOK_ADMIN_SAVE,
WEBHOOK_ADMIN_DELETE,
],
]);
$this->load->view('pages/webhooks');
}
/**
* Filter webhooks by the provided keyword.
*/
public function search(): void
{
try {
if (cannot('view', PRIV_WEBHOOKS)) {
abort(403, 'Forbidden');
}
$keyword = request('keyword', '');
$order_by = request('order_by', 'update_datetime DESC');
$limit = request('limit', 1000);
$offset = (int) request('offset', '0');
$webhooks = $this->webhooks_model->search($keyword, $limit, $offset, $order_by);
json_response($webhooks);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new webhook.
*/
public function store(): void
{
try {
if (cannot('add', PRIV_WEBHOOKS)) {
abort(403, 'Forbidden');
}
$webhook = request('webhook');
$this->webhooks_model->only($webhook, $this->allowed_webhook_fields);
$this->webhooks_model->optional($webhook, $this->optional_webhook_fields);
$webhook_id = $this->webhooks_model->save($webhook);
json_response([
'success' => true,
'id' => $webhook_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a webhook.
*/
public function update(): void
{
try {
if (cannot('edit', PRIV_WEBHOOKS)) {
abort(403, 'Forbidden');
}
$webhook = request('webhook');
$this->webhooks_model->only($webhook, $this->allowed_webhook_fields);
$this->webhooks_model->optional($webhook, $this->optional_webhook_fields);
$webhook_id = $this->webhooks_model->save($webhook);
json_response([
'success' => true,
'id' => $webhook_id,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Remove a webhook.
*/
public function destroy(): void
{
try {
if (cannot('delete', PRIV_WEBHOOKS)) {
abort(403, 'Forbidden');
}
$webhook_id = request('webhook_id');
$this->webhooks_model->delete($webhook_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Find a webhook.
*/
public function find(): void
{
try {
if (cannot('view', PRIV_WEBHOOKS)) {
abort(403, 'Forbidden');
}
$webhook_id = request('webhook_id');
$webhook = $this->webhooks_model->find($webhook_id);
json_response($webhook);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,10 @@
<html lang="en">
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>

View File

@ -0,0 +1,202 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Admins API v1 controller.
*
* @package Controllers
*/
class Admins_api_v1 extends EA_Controller
{
/**
* Admins_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('admins_model');
$this->load->library('api');
$this->api->auth();
$this->api->model('admins_model');
}
/**
* Get an admin collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$admins = empty($keyword)
? $this->admins_model->get(null, $limit, $offset, $order_by)
: $this->admins_model->search($keyword, $limit, $offset, $order_by);
foreach ($admins as &$admin) {
$this->admins_model->api_encode($admin);
if (!empty($fields)) {
$this->admins_model->only($admin, $fields);
}
if (!empty($with)) {
$this->admins_model->load($admin, $with);
}
}
json_response($admins);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single admin.
*
* @param int|null $id Admin ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->admins_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$admin = $this->admins_model->find($id);
$this->admins_model->api_encode($admin);
if (!empty($fields)) {
$this->admins_model->only($admin, $fields);
}
if (!empty($with)) {
$this->admins_model->load($admin, $with);
}
json_response($admin);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new admin.
*/
public function store(): void
{
try {
$admin = request();
$this->admins_model->api_decode($admin);
if (array_key_exists('id', $admin)) {
unset($admin['id']);
}
if (!array_key_exists('settings', $admin)) {
throw new InvalidArgumentException('No settings property provided.');
}
$admin_id = $this->admins_model->save($admin);
$created_admin = $this->admins_model->find($admin_id);
$this->admins_model->api_encode($created_admin);
json_response($created_admin, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update an admin.
*
* @param int $id Admin ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->admins_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_admin = $occurrences[0];
$admin = request();
$this->admins_model->api_decode($admin, $original_admin);
$admin_id = $this->admins_model->save($admin);
$updated_admin = $this->admins_model->find($admin_id);
$this->admins_model->api_encode($updated_admin);
json_response($updated_admin);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete an admin.
*
* @param int $id Admin ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->admins_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->admins_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,383 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Appointments API v1 controller.
*
* @package Controllers
*/
class Appointments_api_v1 extends EA_Controller
{
/**
* Appointments_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->model('appointments_model');
$this->load->model('customers_model');
$this->load->model('providers_model');
$this->load->model('services_model');
$this->load->model('settings_model');
$this->load->library('api');
$this->load->library('synchronization');
$this->load->library('notifications');
$this->api->auth();
$this->api->model('appointments_model');
}
/**
* Get an appointment collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$where = null;
// Date query param.
$date = request('date');
if (!empty($date)) {
$where['DATE(start_datetime)'] = new DateTime($date)->format('Y-m-d');
}
// From query param.
$from = request('from');
if (!empty($from)) {
$where['DATE(start_datetime) >='] = new DateTime($from)->format('Y-m-d');
}
// Till query param.
$till = request('till');
if (!empty($till)) {
$where['DATE(end_datetime) <='] = new DateTime($till)->format('Y-m-d');
}
// Service ID query param.
$service_id = request('serviceId');
if (!empty($service_id)) {
$where['id_services'] = $service_id;
}
// Provider ID query param.
$provider_id = request('providerId');
if (!empty($provider_id)) {
$where['id_users_provider'] = $provider_id;
}
// Customer ID query param.
$customer_id = request('customerId');
if (!empty($customer_id)) {
$where['id_users_customer'] = $customer_id;
}
$appointments = empty($keyword)
? $this->appointments_model->get($where, $limit, $offset, $order_by)
: $this->appointments_model->search($keyword, $limit, $offset, $order_by);
foreach ($appointments as &$appointment) {
$this->appointments_model->api_encode($appointment);
$this->aggregates($appointment);
if (!empty($fields)) {
$this->appointments_model->only($appointment, $fields);
}
if (!empty($with)) {
$this->appointments_model->load($appointment, $with);
}
}
json_response($appointments);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Load the relations of the current appointment if the "aggregates" query parameter is present.
*
* This is a compatibility addition to the appointment resource which was the only one to support it.
*
* Use the "attach" query parameter instead as this one will be removed.
*
* @param array $appointment Appointment data.
*
* @deprecated Since 1.5
*/
private function aggregates(array &$appointment): void
{
$aggregates = request('aggregates') !== null;
if ($aggregates) {
$appointment['service'] = $this->services_model->find(
$appointment['id_services'] ?? ($appointment['serviceId'] ?? null),
);
$appointment['provider'] = $this->providers_model->find(
$appointment['id_users_provider'] ?? ($appointment['providerId'] ?? null),
);
$appointment['customer'] = $this->customers_model->find(
$appointment['id_users_customer'] ?? ($appointment['customerId'] ?? null),
);
$this->services_model->api_encode($appointment['service']);
$this->providers_model->api_encode($appointment['provider']);
$this->customers_model->api_encode($appointment['customer']);
}
}
/**
* Get a single appointment.
*
* @param int|null $id Appointment ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->appointments_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$appointment = $this->appointments_model->find($id);
$this->appointments_model->api_encode($appointment);
if (!empty($fields)) {
$this->appointments_model->only($appointment, $fields);
}
if (!empty($with)) {
$this->appointments_model->load($appointment, $with);
}
json_response($appointment);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new appointment.
*/
public function store(): void
{
try {
$appointment = request();
$this->appointments_model->api_decode($appointment);
if (array_key_exists('id', $appointment)) {
unset($appointment['id']);
}
if (!array_key_exists('end_datetime', $appointment)) {
$appointment['end_datetime'] = $this->calculate_end_datetime($appointment);
}
$appointment_id = $this->appointments_model->save($appointment);
$created_appointment = $this->appointments_model->find($appointment_id);
$this->notify_and_sync_appointment($created_appointment);
$this->appointments_model->api_encode($created_appointment);
json_response($created_appointment, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Calculate the end date time of an appointment based on the selected service.
*
* @param array $appointment Appointment data.
*
* @return string Returns the end date time value.
*
* @throws Exception
*/
private function calculate_end_datetime(array $appointment): string
{
$duration = $this->services_model->value($appointment['id_services'], 'duration');
$end = new DateTime($appointment['start_datetime']);
$end->add(new DateInterval('PT' . $duration . 'M'));
return $end->format('Y-m-d H:i:s');
}
/**
* Send the required notifications and trigger syncing after saving an appointment.
*
* @param array $appointment Appointment data.
* @param string $action Performed action ("store" or "update").
*/
private function notify_and_sync_appointment(array $appointment, string $action = 'store'): void
{
$manage_mode = $action === 'update';
$service = $this->services_model->find($appointment['id_services']);
$provider = $this->providers_model->find($appointment['id_users_provider']);
$customer = $this->customers_model->find($appointment['id_users_customer']);
$company_color = setting('company_color');
$settings = [
'company_name' => setting('company_name'),
'company_email' => setting('company_email'),
'company_link' => setting('company_link'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
];
$this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings);
$this->notifications->notify_appointment_saved(
$appointment,
$service,
$provider,
$customer,
$settings,
$manage_mode,
);
}
/**
* Update an appointment.
*
* @param int $id Appointment ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->appointments_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_appointment = $occurrences[0];
$appointment = request();
$this->appointments_model->api_decode($appointment, $original_appointment);
$appointment_id = $this->appointments_model->save($appointment);
$updated_appointment = $this->appointments_model->find($appointment_id);
$this->notify_and_sync_appointment($updated_appointment, 'update');
$this->appointments_model->api_encode($updated_appointment);
json_response($updated_appointment);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete an appointment.
*
* @param int $id Appointment ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->appointments_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$deleted_appointment = $occurrences[0];
$service = $this->services_model->find($deleted_appointment['id_services']);
$provider = $this->providers_model->find($deleted_appointment['id_users_provider']);
$customer = $this->customers_model->find($deleted_appointment['id_users_customer']);
$company_color = setting('company_color');
$settings = [
'company_name' => setting('company_name'),
'company_email' => setting('company_email'),
'company_link' => setting('company_link'),
'company_color' => !empty($company_color) && $company_color != DEFAULT_COMPANY_COLOR ? $company_color : null,
'date_format' => setting('date_format'),
'time_format' => setting('time_format'),
];
$this->appointments_model->delete($id);
$this->synchronization->sync_appointment_deleted($deleted_appointment, $provider);
$this->notifications->notify_appointment_deleted(
$deleted_appointment,
$service,
$provider,
$customer,
$settings,
);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,81 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Availabilities API v1 controller.
*
* @package Controllers
*/
class Availabilities_api_v1 extends EA_Controller
{
/**
* Availabilities_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('services_model');
$this->load->model('settings_model');
$this->load->library('availability');
}
/**
* Generate the available hours based on the selected date, service and provider.
*
* This resource requires the following query parameters:
*
* - serviceId
* - providerI
* - date
*
* Based on those values it will generate the available hours, just like how the booking page works.
*
* You can then safely create a new appointment starting on one of the selected hours.
*
* Notice: The returned hours are in the provider's timezone.
*
* If no date parameter is provided then the current date will be used.
*/
public function get(): void
{
try {
$provider_id = request('providerId');
$service_id = request('serviceId');
$date = request('date');
if (!$date) {
$date = date('Y-m-d');
}
$provider = $this->providers_model->find($provider_id);
$service = $this->services_model->find($service_id);
$available_hours = $this->availability->get_available_hours($date, $service, $provider);
json_response($available_hours);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,190 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Customers API v1 controller.
*
* @package Controllers
*/
class Customers_api_v1 extends EA_Controller
{
/**
* Customers_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('customers_model');
}
/**
* Get a customer collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$customers = empty($keyword)
? $this->customers_model->get(null, $limit, $offset, $order_by)
: $this->customers_model->search($keyword, $limit, $offset, $order_by);
foreach ($customers as &$customer) {
$this->customers_model->api_encode($customer);
if (!empty($fields)) {
$this->customers_model->only($customer, $fields);
}
if (!empty($with)) {
$this->customers_model->load($customer, $with);
}
}
json_response($customers);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single customer.
*
* @param int|null $id Customer ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->customers_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$customer = $this->customers_model->find($id);
$this->customers_model->api_encode($customer);
if (!empty($fields)) {
$this->customers_model->only($customer, $fields);
}
json_response($customer);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new customer.
*/
public function store(): void
{
try {
$customer = request();
$this->customers_model->api_decode($customer);
if (array_key_exists('id', $customer)) {
unset($customer['id']);
}
$customer_id = $this->customers_model->save($customer);
$created_customer = $this->customers_model->find($customer_id);
$this->customers_model->api_encode($created_customer);
json_response($created_customer, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a customer.
*
* @param int $id Customer ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->customers_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_customer = $occurrences[0];
$customer = request();
$this->customers_model->api_decode($customer, $original_customer);
$customer_id = $this->customers_model->save($customer);
$updated_customer = $this->customers_model->find($customer_id);
$this->customers_model->api_encode($updated_customer);
json_response($updated_customer);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete a customer.
*
* @param int $id Customer ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->customers_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->customers_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,212 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Providers API v1 controller.
*
* @package Controllers
*/
class Providers_api_v1 extends EA_Controller
{
/**
* Providers_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('providers_model');
}
/**
* Get a provider collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$providers = empty($keyword)
? $this->providers_model->get(null, $limit, $offset, $order_by)
: $this->providers_model->search($keyword, $limit, $offset, $order_by);
foreach ($providers as &$provider) {
$this->providers_model->api_encode($provider);
if (!empty($fields)) {
$this->providers_model->only($provider, $fields);
}
if (!empty($with)) {
$this->providers_model->load($provider, $with);
}
}
json_response($providers);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single provider.
*
* @param int|null $id Provider ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->providers_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$provider = $this->providers_model->find($id);
$this->providers_model->api_encode($provider);
if (!empty($fields)) {
$this->providers_model->only($provider, $fields);
}
if (!empty($with)) {
$this->providers_model->load($provider, $with);
}
json_response($provider);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new provider.
*/
public function store(): void
{
try {
$provider = request();
$this->providers_model->api_decode($provider);
if (array_key_exists('id', $provider)) {
unset($provider['id']);
}
if (!array_key_exists('services', $provider)) {
throw new InvalidArgumentException('No services property provided.');
}
if (!array_key_exists('settings', $provider)) {
throw new InvalidArgumentException('No settings property provided.');
}
if (!array_key_exists('working_plan', $provider['settings'])) {
$provider['settings']['working_plan'] = setting('company_working_plan');
}
if (!array_key_exists('working_plan_exceptions', $provider['settings'])) {
$provider['settings']['working_plan_exceptions'] = '{}';
}
$provider_id = $this->providers_model->save($provider);
$created_provider = $this->providers_model->find($provider_id);
$this->providers_model->api_encode($created_provider);
json_response($created_provider, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a provider.
*
* @param int $id Provider ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->providers_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_provider = $occurrences[0];
$provider = request();
$this->providers_model->api_decode($provider, $original_provider);
$provider_id = $this->providers_model->save($provider);
$updated_provider = $this->providers_model->find($provider_id);
$this->providers_model->api_encode($updated_provider);
json_response($updated_provider);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete a provider.
*
* @param int $id Provider ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->providers_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->providers_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,198 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Secretaries API v1 controller.
*
* @package Controllers
*/
class Secretaries_api_v1 extends EA_Controller
{
/**
* Secretaries_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('secretaries_model');
}
/**
* Get a secretary collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$secretaries = empty($keyword)
? $this->secretaries_model->get(null, $limit, $offset, $order_by)
: $this->secretaries_model->search($keyword, $limit, $offset, $order_by);
foreach ($secretaries as &$secretary) {
$this->secretaries_model->api_encode($secretary);
if (!empty($fields)) {
$this->secretaries_model->only($secretary, $fields);
}
if (!empty($with)) {
$this->secretaries_model->load($secretary, $with);
}
}
json_response($secretaries);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single secretary.
*
* @param int|null $id Secretary ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->secretaries_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$secretary = $this->secretaries_model->find($id);
$this->secretaries_model->api_encode($secretary);
if (!empty($fields)) {
$this->secretaries_model->only($secretary, $fields);
}
json_response($secretary);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new secretary.
*/
public function store(): void
{
try {
$secretary = request();
$this->secretaries_model->api_decode($secretary);
if (array_key_exists('id', $secretary)) {
unset($secretary['id']);
}
if (!array_key_exists('providers', $secretary)) {
throw new InvalidArgumentException('No providers property provided.');
}
if (!array_key_exists('settings', $secretary)) {
throw new InvalidArgumentException('No settings property provided.');
}
$secretary_id = $this->secretaries_model->save($secretary);
$created_secretary = $this->secretaries_model->find($secretary_id);
$this->secretaries_model->api_encode($created_secretary);
json_response($created_secretary, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a secretary.
*
* @param int $id Secretary ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->secretaries_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_secretary = $occurrences[0];
$secretary = request();
$this->secretaries_model->api_decode($secretary, $original_secretary);
$secretary_id = $this->secretaries_model->save($secretary);
$updated_secretary = $this->secretaries_model->find($secretary_id);
$this->secretaries_model->api_encode($updated_secretary);
json_response($updated_secretary);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete a secretary.
*
* @param int $id Secretary ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->secretaries_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->secretaries_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,196 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Service-categories API v1 controller.
*
* @package Controllers
*/
class Service_categories_api_v1 extends EA_Controller
{
/**
* Service_categories_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('service_categories_model');
}
/**
* Get a service-category collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$service_categories = empty($keyword)
? $this->service_categories_model->get(null, $limit, $offset, $order_by)
: $this->service_categories_model->search($keyword, $limit, $offset, $order_by);
foreach ($service_categories as &$service_category) {
$this->service_categories_model->api_encode($service_category);
if (!empty($fields)) {
$this->service_categories_model->only($service_category, $fields);
}
if (!empty($with)) {
$this->service_categories_model->load($service_category, $with);
}
}
json_response($service_categories);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single service-category.
*
* @param int|null $id Service-category ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->service_categories_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$service_category = $this->service_categories_model->find($id);
$this->service_categories_model->api_encode($service_category);
if (!empty($fields)) {
$this->service_categories_model->only($service_category, $fields);
}
if (!empty($with)) {
$this->service_categories_model->load($service_category, $with);
}
json_response($service_category);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new service-category.
*/
public function store(): void
{
try {
$service_category = request();
$this->service_categories_model->api_decode($service_category);
if (array_key_exists('id', $service_category)) {
unset($service_category['id']);
}
$service_category_id = $this->service_categories_model->save($service_category);
$created_service_category = $this->service_categories_model->find($service_category_id);
$this->service_categories_model->api_encode($created_service_category);
json_response($created_service_category, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a service-category.
*
* @param int $id Service-category ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->service_categories_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_category = $occurrences[0];
$service_category = request();
$this->service_categories_model->api_decode($service_category, $original_category);
$service_category_id = $this->service_categories_model->save($service_category);
$updated_service_category = $this->service_categories_model->find($service_category_id);
$this->service_categories_model->api_encode($updated_service_category);
json_response($updated_service_category);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete a service-category.
*
* @param int $id Service-category ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->service_categories_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->service_categories_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,196 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Services API v1 controller.
*
* @package Controllers
*/
class Services_api_v1 extends EA_Controller
{
/**
* Services_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('services_model');
}
/**
* Get a service collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$services = empty($keyword)
? $this->services_model->get(null, $limit, $offset, $order_by)
: $this->services_model->search($keyword, $limit, $offset, $order_by);
foreach ($services as &$service) {
$this->services_model->api_encode($service);
if (!empty($fields)) {
$this->services_model->only($service, $fields);
}
if (!empty($with)) {
$this->services_model->load($service, $with);
}
}
json_response($services);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single service.
*
* @param int|null $id Service ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->services_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$service = $this->services_model->find($id);
$this->services_model->api_encode($service);
if (!empty($fields)) {
$this->services_model->only($service, $fields);
}
if (!empty($with)) {
$this->services_model->load($service, $with);
}
json_response($service);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new service.
*/
public function store(): void
{
try {
$service = request();
$this->services_model->api_decode($service);
if (array_key_exists('id', $service)) {
unset($service['id']);
}
$service_id = $this->services_model->save($service);
$created_service = $this->services_model->find($service_id);
$this->services_model->api_encode($created_service);
json_response($created_service, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a service.
*
* @param int $id Service ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->services_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_service = $occurrences[0];
$service = request();
$this->services_model->api_decode($service, $original_service);
$service_id = $this->services_model->save($service);
$updated_service = $this->services_model->find($service_id);
$this->services_model->api_encode($updated_service);
json_response($updated_service);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete a service.
*
* @param int $id Service ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->services_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->services_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,108 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Settings API v1 controller.
*
* @package Controllers
*/
class Settings_api_v1 extends EA_Controller
{
/**
* Settings_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('settings_model');
}
/**
* Get a setting collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$settings = empty($keyword)
? $this->settings_model->get(null, $limit, $offset, $order_by)
: $this->settings_model->search($keyword, $limit, $offset, $order_by);
foreach ($settings as &$setting) {
$this->settings_model->api_encode($setting);
if (!empty($fields)) {
$this->settings_model->only($setting, $fields);
}
}
json_response($settings);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a setting value by name.
*
* @param string $name Setting name.
*/
public function show(string $name): void
{
try {
$value = setting($name);
json_response([
'name' => $name,
'value' => $value,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Set a setting value by name.
*
* @param string $name Setting name.
*/
public function update(string $name): void
{
try {
$value = request('value');
setting([$name => $value]);
json_response([
'name' => $name,
'value' => $value,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,196 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Unavailabilities API v1 controller.
*
* @package Controllers
*/
class Unavailabilities_api_v1 extends EA_Controller
{
/**
* Unavailabilities_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('unavailabilities_model');
}
/**
* Get an unavailability collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$unavailabilities = empty($keyword)
? $this->unavailabilities_model->get(null, $limit, $offset, $order_by)
: $this->unavailabilities_model->search($keyword, $limit, $offset, $order_by);
foreach ($unavailabilities as &$unavailability) {
$this->unavailabilities_model->api_encode($unavailability);
if (!empty($fields)) {
$this->unavailabilities_model->only($unavailability, $fields);
}
if (!empty($with)) {
$this->unavailabilities_model->load($unavailability, $with);
}
}
json_response($unavailabilities);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single unavailability.
*
* @param int|null $id Unavailability ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->unavailabilities_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$unavailability = $this->unavailabilities_model->find($id);
$this->unavailabilities_model->api_encode($unavailability);
if (!empty($fields)) {
$this->unavailabilities_model->only($unavailability, $fields);
}
if (!empty($with)) {
$this->unavailabilities_model->load($unavailability, $with);
}
json_response($unavailability);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new unavailability.
*/
public function store(): void
{
try {
$unavailability = request();
$this->unavailabilities_model->api_decode($unavailability);
if (array_key_exists('id', $unavailability)) {
unset($unavailability['id']);
}
$unavailability_id = $this->unavailabilities_model->save($unavailability);
$created_unavailability = $this->unavailabilities_model->find($unavailability_id);
$this->unavailabilities_model->api_encode($created_unavailability);
json_response($created_unavailability, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update an unavailability.
*
* @param int $id Unavailability ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->unavailabilities_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_unavailability = $occurrences[0];
$unavailability = request();
$this->unavailabilities_model->api_decode($unavailability, $original_unavailability);
$unavailability_id = $this->unavailabilities_model->save($unavailability);
$updated_unavailability = $this->unavailabilities_model->find($unavailability_id);
$this->unavailabilities_model->api_encode($updated_unavailability);
json_response($updated_unavailability);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete an unavailability.
*
* @param int $id Unavailability ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->unavailabilities_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->unavailabilities_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,196 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Webhooks API v1 controller.
*
* @package Controllers
*/
class Webhooks_api_v1 extends EA_Controller
{
/**
* Webhooks_api_v1 constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('api');
$this->api->auth();
$this->api->model('webhooks_model');
}
/**
* Get a webhook collection.
*/
public function index(): void
{
try {
$keyword = $this->api->request_keyword();
$limit = $this->api->request_limit();
$offset = $this->api->request_offset();
$order_by = $this->api->request_order_by();
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$webhooks = empty($keyword)
? $this->webhooks_model->get(null, $limit, $offset, $order_by)
: $this->webhooks_model->search($keyword, $limit, $offset, $order_by);
foreach ($webhooks as &$webhook) {
$this->webhooks_model->api_encode($webhook);
if (!empty($fields)) {
$this->webhooks_model->only($webhook, $fields);
}
if (!empty($with)) {
$this->webhooks_model->load($webhook, $with);
}
}
json_response($webhooks);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Get a single webhook.
*
* @param int|null $id Webhook ID.
*/
public function show(?int $id = null): void
{
try {
$occurrences = $this->webhooks_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$fields = $this->api->request_fields();
$with = $this->api->request_with();
$webhook = $this->webhooks_model->find($id);
$this->webhooks_model->api_encode($webhook);
if (!empty($fields)) {
$this->webhooks_model->only($webhook, $fields);
}
if (!empty($with)) {
$this->webhooks_model->load($webhook, $with);
}
json_response($webhook);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Store a new webhook.
*/
public function store(): void
{
try {
$webhook = request();
$this->webhooks_model->api_decode($webhook);
if (array_key_exists('id', $webhook)) {
unset($webhook['id']);
}
$webhook_id = $this->webhooks_model->save($webhook);
$created_webhook = $this->webhooks_model->find($webhook_id);
$this->webhooks_model->api_encode($created_webhook);
json_response($created_webhook, 201);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Update a webhook.
*
* @param int $id Webhook ID.
*/
public function update(int $id): void
{
try {
$occurrences = $this->webhooks_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$original_webhook = $occurrences[0];
$webhook = request();
$this->webhooks_model->api_decode($webhook, $original_webhook);
$webhook_id = $this->webhooks_model->save($webhook);
$updated_webhook = $this->webhooks_model->find($webhook_id);
$this->webhooks_model->api_encode($updated_webhook);
json_response($updated_webhook);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Delete a webhook.
*
* @param int $id Webhook ID.
*/
public function destroy(int $id): void
{
try {
$occurrences = $this->webhooks_model->get(['id' => $id]);
if (empty($occurrences)) {
response('', 404);
return;
}
$this->webhooks_model->delete($id);
response('', 204);
} catch (Throwable $e) {
json_exception($e);
}
}
}

View File

@ -0,0 +1,10 @@
<html lang="en">
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>

View File

@ -0,0 +1,10 @@
<html lang="en">
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments benchmark.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Benchmark extends CI_Benchmark
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments cache.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Cache extends CI_Cache
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments calendar.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Calendar extends CI_Calendar
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments config.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Config extends CI_Config
{
//
}

View File

@ -0,0 +1,157 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments controller.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*
* @property Admins_model $admins_model
* @property Appointments_model $appointments_model
* @property Service_categories_model $service_categories_model
* @property Consents_model $consents_model
* @property Customers_model $customers_model
* @property Providers_model $providers_model
* @property Roles_model $roles_model
* @property Secretaries_model $secretaries_model
* @property Services_model $services_model
* @property Settings_model $settings_model
* @property Unavailabilities_model $unavailabilities_model
* @property Users_model $users_model
* @property Webhooks_model $webhooks_model
* @property Blocked_periods_model $blocked_periods_model
*
* @property Accounts $accounts
* @property Api $api
* @property Availability $availability
* @property Email_messages $email_messages
* @property Captcha_builder $captcha_builder
* @property Google_Sync $google_sync
* @property Caldav_Sync $caldav_sync
* @property Ics_file $ics_file
* @property Instance $instance
* @property Ldap_client $ldap_client
* @property Notifications $notifications
* @property Permissions $permissions
* @property Synchronization $synchronization
* @property Timezones $timezones
* @property Webhooks_client $webhooks_client
*/
class EA_Controller extends CI_Controller
{
/**
* EA_Controller constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('accounts');
$this->ensure_user_exists();
$this->configure_language();
$this->load_common_html_vars();
$this->load_common_script_vars();
rate_limit($this->input->ip_address());
}
private function ensure_user_exists()
{
$user_id = session('user_id');
if (!$user_id || !$this->db->table_exists('users')) {
return;
}
if (!$this->accounts->does_account_exist($user_id)) {
session_destroy();
abort(403, 'Forbidden');
}
}
/**
* Configure the language.
*/
private function configure_language()
{
$session_language = session('language');
if ($session_language) {
$language_codes = config('language_codes');
config([
'language' => $session_language,
'language_code' => array_search($session_language, $language_codes) ?: 'en',
]);
}
$this->lang->load('translations');
}
/**
* Load common script vars for all requests.
*/
private function load_common_html_vars()
{
html_vars([
'base_url' => config('base_url'),
'index_page' => config('index_page'),
'available_languages' => config('available_languages'),
'language' => $this->lang->language,
'csrf_token' => $this->security->get_csrf_hash(),
]);
}
/**
* Load common script vars for all requests.
*/
private function load_common_script_vars()
{
script_vars([
'base_url' => config('base_url'),
'index_page' => config('index_page'),
'available_languages' => config('available_languages'),
'csrf_token' => $this->security->get_csrf_hash(),
'language' => config('language'),
'language_code' => config('language_code'),
]);
}
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments DB forge.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_DB_forge extends CI_DB_forge
{
//
}

View File

@ -0,0 +1,47 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments DB query builder.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*
* @method int insert_id()
*/
class EA_DB_query_builder extends CI_DB_query_builder
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments DB utility.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_DB_utility extends CI_DB_utility
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments email.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Email extends CI_Email
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments encrypt.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Encrypt extends CI_Encrypt
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments encryption.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Encryption extends CI_Encryption
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments exceptions.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Exceptions extends CI_Exceptions
{
//
}

View File

@ -0,0 +1,45 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.0
* ---------------------------------------------------------------------------- */
/**
* Easy!Appointments hooks.
*
* @property EA_Benchmark $benchmark
* @property EA_Cache $cache
* @property EA_Calendar $calendar
* @property EA_Config $config
* @property EA_DB_forge $dbforge
* @property EA_DB_query_builder $db
* @property EA_DB_utility $dbutil
* @property EA_Email $email
* @property EA_Encrypt $encrypt
* @property EA_Encryption $encryption
* @property EA_Exceptions $exceptions
* @property EA_Hooks $hooks
* @property EA_Input $input
* @property EA_Lang $lang
* @property EA_Loader $load
* @property EA_Log $log
* @property EA_Migration $migration
* @property EA_Output $output
* @property EA_Profiler $profiler
* @property EA_Router $router
* @property EA_Security $security
* @property EA_Session $session
* @property EA_Upload $upload
* @property EA_URI $uri
*/
class EA_Hooks extends CI_Hooks
{
//
}

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