├── LICENSE ├── composer.json ├── config └── ambulatory.php ├── package.json ├── public ├── app.css ├── app.js └── mix-manifest.json ├── readme.md ├── resources ├── js │ ├── app.js │ ├── base.js │ ├── bus.js │ ├── components │ │ ├── Alert.vue │ │ ├── AvatarUpload.vue │ │ ├── Booking.vue │ │ ├── FormEntry.vue │ │ ├── FormErrors.vue │ │ ├── IndexView.vue │ │ ├── Modal.vue │ │ ├── SelectGender.vue │ │ ├── SelectHealthFacility.vue │ │ ├── SelectMaritalStatus.vue │ │ ├── SettingTabs.vue │ │ └── SidebarMenu.vue │ ├── routes.js │ └── views │ │ ├── accounts │ │ └── edit.vue │ │ ├── doctors │ │ ├── edit.vue │ │ ├── index.vue │ │ └── preview.vue │ │ ├── errors │ │ └── 404.vue │ │ ├── facilities │ │ ├── edit.vue │ │ └── index.vue │ │ ├── inbox │ │ ├── index.vue │ │ └── preview.vue │ │ ├── invitations │ │ ├── edit.vue │ │ └── index.vue │ │ ├── medical │ │ ├── edit.vue │ │ └── index.vue │ │ ├── password │ │ └── new.vue │ │ ├── schedules │ │ ├── edit.vue │ │ └── index.vue │ │ ├── specializations │ │ ├── edit.vue │ │ └── index.vue │ │ └── staff │ │ └── index.vue ├── sass │ ├── app.scss │ ├── base.scss │ ├── card.scss │ ├── modal.scss │ ├── multiselect.scss │ └── sidebar.scss └── views │ ├── auth │ ├── login.blade.php │ ├── register.blade.php │ ├── request-password-reset.blade.php │ └── reset-password.blade.php │ ├── emails │ ├── credential.blade.php │ ├── forgot-password.blade.php │ └── invitation.blade.php │ └── layouts │ ├── auth.blade.php │ └── dashboard.blade.php ├── routes ├── auth.php └── dashboard.php ├── src ├── Ambulatory.php ├── AmbulatoryModel.php ├── AmbulatoryServiceProvider.php ├── Availability.php ├── Booking.php ├── Console │ ├── InstallCommand.php │ └── MigrateCommand.php ├── Doctor.php ├── HasSlug.php ├── HasUuid.php ├── HealthFacility.php ├── Http │ ├── Controllers │ │ ├── AmbulatoryController.php │ │ ├── Auth │ │ │ ├── AcceptInvitationController.php │ │ │ ├── ForgotPasswordController.php │ │ │ ├── LoginController.php │ │ │ └── RegisterController.php │ │ ├── AvailabilityController.php │ │ ├── BookAppointmentController.php │ │ ├── Controller.php │ │ ├── DoctorController.php │ │ ├── HealthFacilityController.php │ │ ├── InboxController.php │ │ ├── InvitationController.php │ │ ├── MedicalFormController.php │ │ ├── ScheduleAvailabilityController.php │ │ ├── ScheduleController.php │ │ ├── Settings │ │ │ ├── AccountController.php │ │ │ ├── DoctorProfileController.php │ │ │ ├── NewPasswordController.php │ │ │ └── UploadUserAvatarController.php │ │ ├── SpecializationController.php │ │ └── StaffController.php │ ├── Middleware │ │ ├── Admin.php │ │ ├── Authenticate.php │ │ ├── Doctor.php │ │ ├── RedirectIfAuthenticated.php │ │ └── VerifiedDoctor.php │ ├── Requests │ │ ├── AccountRequest.php │ │ ├── AvailabilityRequest.php │ │ ├── BookAppointmentRequest.php │ │ ├── DoctorProfileRequest.php │ │ ├── HealthFacilityRequest.php │ │ ├── InvitationRequest.php │ │ ├── MedicalFormRequest.php │ │ ├── NewPasswordRequest.php │ │ ├── ScheduleAvailabilityRequest.php │ │ ├── ScheduleRequest.php │ │ └── SpecializationRequest.php │ └── Resources │ │ ├── AvailabilityResource.php │ │ ├── BookingResource.php │ │ ├── DoctorResource.php │ │ ├── HealthFacilityResource.php │ │ ├── InvitationResource.php │ │ ├── MedicalFormResource.php │ │ ├── ScheduleResource.php │ │ ├── SpecializationResource.php │ │ └── UserResource.php ├── Invitation.php ├── Mail │ ├── CredentialEmail.php │ ├── InvitationEmail.php │ └── ResetPasswordEmail.php ├── MedicalForm.php ├── Migrations │ └── 2019_02_13_000000_create_tables.php ├── Policies │ ├── AvailabilityPolicy.php │ ├── DoctorPolicy.php │ ├── DoctorSchedulePolicy.php │ └── MedicalFormPolicy.php ├── Rules │ ├── BookingAvailabilityRule.php │ └── CurrentPassRule.php ├── Schedule.php ├── Specialization.php └── User.php ├── webpack.mix.js └── yarn.lock /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) David H. Sianturi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ambulatory/ambulatory", 3 | "description": "An open source ambulatory care platform", 4 | "keywords": ["laravel", "outpatient care", "ambulatory care", "scheduling appointment"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "David H. Sianturi", 9 | "email": "davidhsianturi@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.1.3", 14 | "laravel/framework": "~5.7.0|~5.8.0", 15 | "rlanvin/php-rrule": "^2.0" 16 | }, 17 | "require-dev": { 18 | "orchestra/testbench": "^3.7" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Ambulatory\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Ambulatory\\Tests\\": "tests/" 28 | } 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "1.0-dev" 33 | }, 34 | "laravel": { 35 | "providers": [ 36 | "Ambulatory\\AmbulatoryServiceProvider" 37 | ] 38 | } 39 | }, 40 | "config": { 41 | "sort-packages": true 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /config/ambulatory.php: -------------------------------------------------------------------------------- 1 | env('AMBULATORY_DB_CONNECTION', 'ambulatory'), 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Ambulatory Uploads Disk 16 | |-------------------------------------------------------------------------- 17 | | 18 | */ 19 | 20 | 'storage_disk' => env('AMBULATORY_STORAGE_DISK', 'local'), 21 | 22 | 'storage_path' => env('AMBULATORY_STORAGE_PATH', 'public/ambulatory'), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Ambulatory Path 27 | |-------------------------------------------------------------------------- 28 | | 29 | */ 30 | 'path' => env('AMBULATORY_PATH', 'ambulatory'), 31 | ]; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "axios": "^0.19", 14 | "bootstrap": "^4.3.1", 15 | "cross-env": "^5.2", 16 | "jquery": "^3.3.1", 17 | "laravel-mix": "^4.0.7", 18 | "lodash": "^4.17.15", 19 | "moment": "^2.24.0", 20 | "popper.js": "^1.14.7", 21 | "resolve-url-loader": "^2.3.1", 22 | "sass": "^1.15.2", 23 | "sass-loader": "^7.1.0", 24 | "vue": "^2.6.10", 25 | "vue-router": "^3.0.7", 26 | "vue-template-compiler": "^2.6.6" 27 | }, 28 | "dependencies": { 29 | "vue-multiselect": "^2.1.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=d432af3336390d493bb8", 3 | "/app.css": "/app.css?id=7b3dcba4823855b1ffd2" 4 | } 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Ambulatory is An experiment project for an outpatient basis, including diagnosis, observation, consultation, treatment, intervention, and rehabilitation services [Learn more about ambulatory care >>>](https://www.rasmussen.edu/degrees/nursing/blog/what-is-ambulatory-care/) 4 | 5 | ## Installation 6 | 7 | Ambulatory runs on any Laravel application, it uses a separate database connection and authentication system so that you don't have to modify any of your project code. 8 | 9 | Install Ambulatory via Composer: 10 | 11 | ``` 12 | composer require ambulatory/ambulatory 13 | ``` 14 | 15 | Once Composer is done, run the following command. 16 | 17 | ``` 18 | php artisan ambulatory:install 19 | ``` 20 | 21 | Create a symbolic link to ensure file uploads are publicly accessible from the web: 22 | 23 | ``` 24 | php artisan storage:link 25 | ``` 26 | 27 | Check `config/ambulatory.php` and configure the database connection ambulatory is going to be using, after that go run: 28 | 29 | ``` 30 | php artisan ambulatory:migrate 31 | ``` 32 | 33 | Head to `yourproject.test/ambulatory` and use the provided email and password to log in. 34 | 35 | ## Road map 36 | 37 | Ambulatory is still under heavy development, I decided to ship it in this early stage so you can help me make it better. [See the road map >>>](https://github.com/ambulatorycare/ambulatory/projects/1) 38 | 39 | ## Security Vulnerabilities 40 | 41 | If you discover a security vulnerability within Ambulatory, please send an e-mail to [davidhsianturi@gmail.com](mailto:davidhsianturi@gmail.com). All security vulnerabilities will be promptly addressed. 42 | 43 | ## License 44 | 45 | Ambulatory is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 46 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Base from './base'; 3 | import {Bus} from './bus.js'; 4 | import Routes from './routes'; 5 | import VueRouter from 'vue-router'; 6 | 7 | require('bootstrap'); 8 | 9 | Vue.use(VueRouter); 10 | 11 | window.Popper = require('popper.js').default; 12 | 13 | const router = new VueRouter({ 14 | routes: Routes, 15 | mode: 'history', 16 | base: '/' + Ambulatory.path, 17 | }); 18 | 19 | Vue.component('alert', require('./components/Alert.vue').default); 20 | Vue.component('modal', require('./components/Modal.vue').default); 21 | Vue.component('form-entry', require('./components/FormEntry.vue').default); 22 | Vue.component('index-view', require('./components/IndexView.vue').default); 23 | Vue.component('form-errors', require('./components/FormErrors.vue').default); 24 | Vue.component('sidebar-menu', require('./components/SidebarMenu.vue').default); 25 | Vue.component('booking', require('./components/Booking.vue').default); 26 | 27 | Vue.mixin(Base); 28 | 29 | new Vue({ 30 | el: '#ambulatory', 31 | 32 | router, 33 | 34 | data() { 35 | return { 36 | alert: { 37 | mode: null, 38 | type: null, 39 | autoClose: 0, 40 | message: '', 41 | confirmationProceed: null, 42 | confirmationCancel: null, 43 | }, 44 | 45 | booking: { 46 | schedule: null, 47 | } 48 | } 49 | }, 50 | 51 | mounted() { 52 | Bus.$on('httpError', message => this.alertError(message)); 53 | Bus.$on('httpForbidden', message => this.alertError(message)); 54 | } 55 | }); -------------------------------------------------------------------------------- /resources/js/base.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import axios from 'axios'; 3 | import moment from 'moment'; 4 | import {Bus} from './bus.js'; 5 | 6 | export default { 7 | computed: { 8 | Ambulatory() { 9 | return Ambulatory; 10 | } 11 | }, 12 | 13 | methods: { 14 | /** 15 | * Show the local date for today. 16 | */ 17 | localToday() { 18 | return moment().local(); 19 | }, 20 | 21 | /** 22 | * Show the local date time. 23 | */ 24 | localDateTime(dateTime) { 25 | return moment(dateTime) 26 | .utc() 27 | .local(); 28 | }, 29 | 30 | /** 31 | * Show the time ago format for the given time. 32 | */ 33 | timeAgo(time) { 34 | return moment(time) 35 | .utc() 36 | .local() 37 | .fromNow(); 38 | }, 39 | 40 | /** 41 | * Show the duration format for the given date. 42 | */ 43 | dateDuration(date) { 44 | return moment(date) 45 | .utc() 46 | .local() 47 | .fromNow(true); 48 | }, 49 | 50 | /** 51 | * Show a success message. 52 | */ 53 | alertSuccess(message, autoClose) { 54 | this.$root.alert.mode = 'flash'; 55 | this.$root.alert.type = 'success'; 56 | this.$root.alert.autoClose = autoClose; 57 | this.$root.alert.message = message; 58 | }, 59 | 60 | /** 61 | * Show an error message. 62 | */ 63 | alertError(message) { 64 | this.$root.alert.mode = 'dialog'; 65 | this.$root.alert.type = 'error'; 66 | this.$root.alert.autoClose = false; 67 | this.$root.alert.message = message; 68 | }, 69 | 70 | /** 71 | * Show confirmation message. 72 | */ 73 | alertConfirm(message, success, failure) { 74 | this.$root.alert.mode = 'dialog'; 75 | this.$root.alert.type = 'confirmation'; 76 | this.$root.alert.autoClose = false; 77 | this.$root.alert.message = message; 78 | this.$root.alert.confirmationProceed = success; 79 | this.$root.alert.confirmationCancel = failure; 80 | }, 81 | 82 | /** 83 | * Show a booking dialog. 84 | */ 85 | bookAppointment(schedule) { 86 | this.$root.booking.schedule = schedule; 87 | }, 88 | 89 | /** 90 | * Truncate the given string. 91 | */ 92 | truncate(string, length = 70) { 93 | return _.truncate(string, { 94 | 'length': length, 95 | 'separator': /,? +/ 96 | }); 97 | }, 98 | 99 | /** 100 | * Create an instance of axios. 101 | */ 102 | http() { 103 | let instance = axios.create(); 104 | 105 | instance.defaults.baseURL = '/' + Ambulatory.path; 106 | 107 | instance.interceptors.response.use( 108 | response => response, 109 | error => { 110 | switch (error.response.status) { 111 | case 500: 112 | Bus.$emit('httpError', error.response.data.message); 113 | break; 114 | case 403: 115 | Bus.$emit('httpForbidden', error.response.data.message); 116 | break; 117 | case 401: 118 | window.location.href = '/' + Ambulatory.path + '/logout'; 119 | break; 120 | } 121 | 122 | return Promise.reject(error); 123 | } 124 | ); 125 | 126 | return instance; 127 | }, 128 | } 129 | }; -------------------------------------------------------------------------------- /resources/js/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export const Bus = new Vue(); -------------------------------------------------------------------------------- /resources/js/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 92 | -------------------------------------------------------------------------------- /resources/js/components/AvatarUpload.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 57 | 58 | -------------------------------------------------------------------------------- /resources/js/components/Booking.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 149 | -------------------------------------------------------------------------------- /resources/js/components/FormEntry.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 119 | -------------------------------------------------------------------------------- /resources/js/components/FormErrors.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /resources/js/components/IndexView.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | -------------------------------------------------------------------------------- /resources/js/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /resources/js/components/SelectGender.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | -------------------------------------------------------------------------------- /resources/js/components/SelectHealthFacility.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 52 | -------------------------------------------------------------------------------- /resources/js/components/SelectMaritalStatus.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | -------------------------------------------------------------------------------- /resources/js/components/SettingTabs.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 65 | -------------------------------------------------------------------------------- /resources/js/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | {path: '/', redirect: '/inbox'}, 3 | 4 | // Inbox. 5 | { 6 | path: '/inbox', 7 | name: 'inbox', 8 | component: require('./views/inbox/index').default 9 | }, 10 | { 11 | path: '/inbox/:id', 12 | name: 'inbox-preview', 13 | component: require('./views/inbox/preview').default 14 | }, 15 | 16 | // Schedules. 17 | { 18 | path: '/schedules', 19 | name: 'schedules', 20 | component: require('./views/schedules/index').default 21 | }, 22 | { 23 | path: '/schedules/new', 24 | name: 'schedules-new', 25 | component: require('./views/schedules/edit').default 26 | }, 27 | { 28 | path: '/schedules/:id', 29 | name: 'schedules-edit', 30 | component: require('./views/schedules/edit').default 31 | }, 32 | 33 | // Medical form. 34 | { 35 | path: '/medical-forms', 36 | name: 'medical-forms', 37 | component: require('./views/medical/index').default 38 | }, 39 | { 40 | path: '/medical-forms/new', 41 | name: 'medical-forms-new', 42 | component: require('./views/medical/edit').default 43 | }, 44 | { 45 | path: '/medical-forms/:id', 46 | name: 'medical-forms-edit', 47 | component: require('./views/medical/edit').default 48 | }, 49 | 50 | // Staff. 51 | { 52 | path: '/staff', 53 | name: 'staff', 54 | component: require('./views/staff/index').default 55 | }, 56 | 57 | // Invitations. 58 | { 59 | path: '/invitations', 60 | name: 'invitations', 61 | component: require('./views/invitations/index').default 62 | }, 63 | { 64 | path: '/invitations/new', 65 | name: 'invitations-new', 66 | component: require('./views/invitations/edit').default 67 | }, 68 | { 69 | path: '/invitations/:id', 70 | name: 'invitations-edit', 71 | component: require('./views/invitations/edit').default 72 | }, 73 | 74 | // Specializations. 75 | { 76 | path: '/specializations', 77 | name: 'specializations', 78 | component: require('./views/specializations/index').default 79 | }, 80 | { 81 | path: '/specializations/new', 82 | name: 'specializations-new', 83 | component: require('./views/specializations/edit').default 84 | }, 85 | { 86 | path: '/specializations/:id', 87 | name: 'specializations-edit', 88 | component: require('./views/specializations/edit').default 89 | }, 90 | 91 | // Health facilities. 92 | { 93 | path: '/health-facilities', 94 | name: 'health-facilities', 95 | component: require('./views/facilities/index').default 96 | }, 97 | { 98 | path: '/health-facilities/new', 99 | name: 'health-facilities-new', 100 | component: require('./views/facilities/edit').default 101 | }, 102 | { 103 | path: '/health-facilities/:id', 104 | name: 'health-facilities-edit', 105 | component: require('./views/facilities/edit').default 106 | }, 107 | 108 | // Settings. 109 | { 110 | path: '/settings/account', 111 | name: 'settings-account', 112 | component: require('./views/accounts/edit').default 113 | }, 114 | { 115 | path: '/settings/password/new', 116 | name: 'new-password', 117 | component: require('./views/password/new').default 118 | }, 119 | { 120 | path: '/settings/doctor-profile', 121 | name: 'settings-doctor-profile', 122 | component: require('./views/doctors/edit').default 123 | }, 124 | 125 | { 126 | path: '/doctors', 127 | name: 'doctors', 128 | component: require('./views/doctors/index').default 129 | }, 130 | { 131 | path: '/doctors/:id', 132 | name: 'doctors-preview', 133 | component: require('./views/doctors/preview').default 134 | }, 135 | 136 | // Catch All. 137 | { 138 | path: '*', 139 | name: 'catch-all', 140 | component: require('./views/errors/404').default, 141 | }, 142 | ]; -------------------------------------------------------------------------------- /resources/js/views/accounts/edit.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 98 | -------------------------------------------------------------------------------- /resources/js/views/doctors/edit.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 128 | -------------------------------------------------------------------------------- /resources/js/views/doctors/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /resources/js/views/doctors/preview.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 74 | -------------------------------------------------------------------------------- /resources/js/views/facilities/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/js/views/facilities/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/js/views/inbox/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/js/views/inbox/preview.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /resources/js/views/invitations/edit.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /resources/js/views/invitations/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/js/views/medical/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /resources/js/views/password/new.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /resources/js/views/schedules/edit.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /resources/js/views/schedules/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/js/views/specializations/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/js/views/specializations/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/js/views/staff/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | $body-bg: #FFF; 2 | $font-size-base: 1rem; 3 | $font-weight-semibold: 600; 4 | $badge-font-size: 0.7rem; 5 | $badge-font-weight: normal; 6 | 7 | $primary: #40A6C8; 8 | $primary-light: #40A6C8A6; 9 | $secondary: #DAE1E7; 10 | $success: #51D88A; 11 | $info: #4090D1; 12 | $warning: #FFF382; 13 | $danger: #EF5753; 14 | $dark: #14171A; 15 | $gray: #687785; 16 | $light: #E7ECF0; 17 | 18 | $sidebar-nav-color: $dark; 19 | $sidebar-icon-color: $dark; 20 | 21 | $table-headers-color: #F1F5F8; 22 | $table-border-color: #F8FAFC; 23 | $table-hover-bg: #F1F7FA; 24 | 25 | $card-cap-bg: #FFF; 26 | $card-bg-light: #F1F7FA; 27 | $card-shadow-color: #CDD8DF; 28 | $card-border-radius: 0rem; 29 | 30 | $border-color: $light; 31 | 32 | $control-action-icon-color: #CCD2DF; 33 | 34 | $pill-link-active: $primary; 35 | 36 | $paginator-button-color: #9EA7AC; 37 | 38 | @import "base"; -------------------------------------------------------------------------------- /resources/sass/base.scss: -------------------------------------------------------------------------------- 1 | @import "multiselect"; 2 | @import "~bootstrap/scss/bootstrap"; 3 | 4 | // custom components. 5 | @import "card"; 6 | @import "modal"; 7 | @import "sidebar"; 8 | 9 | [v-cloak] { 10 | display: none; 11 | } 12 | 13 | svg.icon { 14 | width: 1rem; 15 | height: 1rem; 16 | } 17 | 18 | // fill color 19 | .fill-text-color { 20 | fill: $body-color; 21 | } 22 | 23 | .fill-danger { 24 | fill: $danger; 25 | } 26 | 27 | .fill-warning { 28 | fill: $warning; 29 | } 30 | 31 | .fill-info { 32 | fill: $info; 33 | } 34 | 35 | .fill-success { 36 | fill: $success; 37 | } 38 | 39 | .fill-primary { 40 | fill: $primary; 41 | } 42 | 43 | // form auth 44 | .auth { 45 | display: -ms-flexbox; 46 | display: flex; 47 | -ms-flex-align: center; 48 | align-items: center; 49 | padding-top: 40px; 50 | padding-bottom: 40px; 51 | height: 100%; 52 | } 53 | 54 | .form-auth { 55 | width: 100%; 56 | max-width: 420px; 57 | padding: 15px; 58 | margin: auto; 59 | } 60 | 61 | .form-card { 62 | border-radius: 14px !important; 63 | border: 1px solid $border-color; 64 | padding: 40px 32px 12px; 65 | background-color: #fff; 66 | } 67 | 68 | // list group 69 | .list-group { 70 | a { 71 | svg { 72 | width: 1.2rem; 73 | height: 1.2rem; 74 | margin-right: 9px; 75 | fill: $dark; 76 | } 77 | } 78 | } 79 | 80 | .list-enter-active { 81 | transition: background 2s linear; 82 | } 83 | 84 | .list-enter, 85 | .list-leave-to { 86 | background: $secondary; 87 | } 88 | 89 | // button behavior 90 | button:hover { 91 | .fill-primary { 92 | fill: #FFF; 93 | } 94 | } 95 | 96 | .btn-outline-primary.active { 97 | .fill-primary { 98 | fill: $body-bg; 99 | } 100 | } 101 | 102 | .btn-outline-primary:not(:disabled):not(.disabled).active:focus { 103 | box-shadow: none !important; 104 | } 105 | 106 | // custom utilities 107 | .paginator { 108 | .btn { 109 | text-decoration: none; 110 | color: $paginator-button-color; 111 | 112 | &:hover { 113 | color: $primary; 114 | } 115 | } 116 | } 117 | 118 | .font-weight-bold { 119 | font-weight: $font-weight-semibold !important; 120 | } 121 | 122 | .rounded-full { 123 | border-radius: 9999px; 124 | } 125 | 126 | .rounded-xl { 127 | border-radius: 14px; 128 | } 129 | 130 | .shadow-xs { 131 | box-shadow: rgba(0, 0, 0, 0.08) 0px 8px 28px; 132 | } 133 | 134 | .form-info { 135 | padding: 2rem 2rem; 136 | 137 | svg { 138 | width: 1.7rem; 139 | height: 1.7rem; 140 | } 141 | } -------------------------------------------------------------------------------- /resources/sass/card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | min-height: 100vh; 3 | border-color: $border-color; 4 | 5 | .bottom-radius { 6 | border-bottom-left-radius: $card-border-radius; 7 | border-bottom-right-radius: $card-border-radius; 8 | } 9 | 10 | .card-header { 11 | border-color: $border-color; 12 | background-color: $card-cap-bg; 13 | 14 | .btn { 15 | padding: 0.2rem 0.5rem; 16 | } 17 | 18 | h1 { 19 | margin: 0; 20 | color: $dark; 21 | font-size: 19px; 22 | font-weight: 800; 23 | } 24 | } 25 | 26 | .card-body { 27 | padding: 2rem 2rem; 28 | } 29 | 30 | .card-footer { 31 | padding: 1rem 2rem; 32 | border-color: $border-color; 33 | } 34 | 35 | .nav-pills { 36 | border-bottom: solid 1px $border-color; 37 | } 38 | 39 | .nav-pills .nav-link.active { 40 | background: none; 41 | color: $pill-link-active; 42 | border-bottom: solid 2px $primary; 43 | } 44 | 45 | .nav-pills .nav-link { 46 | border-radius: 0; 47 | font-size: 0.9rem; 48 | font-weight: bold; 49 | color: $gray; 50 | padding: 0.75rem 1.25rem; 51 | } 52 | 53 | .table { 54 | th, 55 | td { 56 | padding: 0.75rem 1.25rem; 57 | } 58 | 59 | &.table-sm { 60 | th, 61 | td { 62 | padding: 0.9rem 1.2rem; 63 | } 64 | } 65 | 66 | th { 67 | border-bottom: 0; 68 | font-weight: 400; 69 | padding: .5rem 1.25rem; 70 | background-color: $table-headers-color; 71 | } 72 | 73 | &:not(.table-borderless) { 74 | td { 75 | border-top: 1px solid $table-border-color; 76 | } 77 | } 78 | 79 | th.table-fit, 80 | td.table-fit { 81 | width: 1%; 82 | white-space: nowrap; 83 | } 84 | } 85 | } 86 | 87 | .card-bg-light { 88 | background: $card-bg-light; 89 | } -------------------------------------------------------------------------------- /resources/sass/modal.scss: -------------------------------------------------------------------------------- 1 | #modal-full { 2 | position: fixed; 3 | z-index: 9998; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background: #FFF; 9 | transition: opacity 0.3s ease; 10 | } 11 | 12 | #modal-full .modal-container { 13 | background: #FFF; 14 | width: 100%; 15 | overflow: scroll; 16 | transition: all 0.3s ease; 17 | border-top: 0.6rem solid $primary; 18 | } 19 | 20 | #modal-dialog { 21 | position: absolute; 22 | z-index: 9998; 23 | width: 100%; 24 | height: 100%; 25 | background: rgba(0, 0, 0, .5); 26 | transition: opacity 0.3s ease; 27 | } 28 | 29 | #modal-dialog .modal-container { 30 | max-width: 350px; 31 | max-height: 100%; 32 | margin: 30px auto; 33 | background: #FFF; 34 | border-radius: 14px; 35 | padding-left: 20px; 36 | padding-right: 20px; 37 | padding-top: 30px; 38 | padding-bottom: 30px; 39 | transition: all 0.3s ease; 40 | } 41 | 42 | #modal-flash { 43 | position: fixed; 44 | z-index: 9998; 45 | bottom: 20px; 46 | right: 10px; 47 | } 48 | 49 | .modal-enter { 50 | opacity: 0; 51 | } 52 | 53 | .modal-leave-active { 54 | opacity: 0; 55 | } 56 | 57 | .modal-enter .modal-container, 58 | .modal-leave-active .modal-container { 59 | -webkit-transform: scale(1.1); 60 | transform: scale(1.1); 61 | } -------------------------------------------------------------------------------- /resources/sass/multiselect.scss: -------------------------------------------------------------------------------- 1 | @import "~vue-multiselect/dist/vue-multiselect.min.css"; 2 | 3 | .multiselect__tags { 4 | border: 0; 5 | background: $light; 6 | } 7 | 8 | .multiselect__tag { 9 | background: $primary-light; 10 | } 11 | 12 | .multiselect__input, 13 | .multiselect__single { 14 | background: $light; 15 | } 16 | 17 | .multiselect__option--highlight { 18 | background: $primary-light; 19 | } 20 | 21 | .multiselect__tag-icon:after { 22 | color: darken($primary-light, 15%); 23 | } 24 | 25 | .multiselect__tag-icon:focus, 26 | .multiselect__tag-icon:hover { 27 | background: $primary-light; 28 | } 29 | 30 | .multiselect__spinner:after, 31 | .multiselect__spinner:before { 32 | border-color: $primary; 33 | } 34 | 35 | .multiselect__content-wrapper { 36 | border: 0px; 37 | background: $light; 38 | } 39 | 40 | .multiselect--above .multiselect__content-wrapper { 41 | border-top: 0px; 42 | } -------------------------------------------------------------------------------- /resources/sass/sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar .nav-item { 2 | padding-top: 7px; 3 | padding-bottom: 7px; 4 | 5 | a { 6 | color: $sidebar-nav-color; 7 | padding: 10px; 8 | font-size: 19px; 9 | font-weight: bold; 10 | line-height: 1.31; 11 | overflow-wrap: break-word; 12 | 13 | svg { 14 | height: 1.75rem; 15 | width: 24px; 16 | color: $sidebar-icon-color; 17 | fill: none; 18 | stroke: currentColor; 19 | stroke-width: 1.7; 20 | margin-right: 20px; 21 | vertical-align: text-bottom; 22 | } 23 | 24 | &.active { 25 | color: $primary; 26 | 27 | svg { 28 | fill: none; 29 | stroke: $primary; 30 | } 31 | } 32 | 33 | } 34 | 35 | :hover { 36 | color: $primary; 37 | 38 | svg { 39 | fill: none; 40 | stroke: $primary; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /resources/views/auth/login.blade.php: -------------------------------------------------------------------------------- 1 | @extends('ambulatory::layouts.auth') 2 | 3 | @section('title', 'Sign In') 4 | 5 | @section('form-content') 6 |
7 |

Sign in

8 | 9 | Forgot? 10 |
11 | 12 |
13 | @csrf 14 | 15 |
16 | 17 | 18 | 24 | 25 | @if ($errors->has('email')) 26 | 27 | {{ $errors->first('email') }} 28 | 29 | @endif 30 |
31 | 32 |
33 | 34 | 35 | 40 | 41 | @if ($errors->has('password')) 42 | 43 | {{ $errors->first('password') }} 44 | 45 | @endif 46 |
47 | 48 |
49 |
50 |
51 | 52 | 53 | 56 |
57 |
58 |
59 | 60 |
61 | 62 |
63 | 64 |

65 | Don't have an account? Sign Up 66 |

67 |
68 | @endsection -------------------------------------------------------------------------------- /resources/views/auth/register.blade.php: -------------------------------------------------------------------------------- 1 | @extends('ambulatory::layouts.auth') 2 | 3 | @section('title', 'Sign Up') 4 | 5 | @section('form-content') 6 |
7 |

Sign up

8 | 9 | Sign in? 10 |
11 | 12 |
13 | @csrf 14 | 15 |
16 | 17 | 18 | 25 | 26 | @if ($errors->has('name')) 27 | 28 | {{ $errors->first('name') }} 29 | 30 | @endif 31 |
32 | 33 |
34 | 35 | 36 | 41 | 42 | @if ($errors->has('email')) 43 | 44 | {{ $errors->first('email') }} 45 | 46 | @endif 47 |
48 | 49 |
50 | 51 | 52 | 58 | 59 | @if ($errors->has('password')) 60 | 61 | {{ $errors->first('password') }} 62 | 63 | @endif 64 |
65 | 66 |
67 | 68 | 69 | 75 |
76 | 77 |
78 | 79 |
80 | 81 |

82 | Forgot your password? 83 | Reset! 84 |

85 |
86 | @endsection -------------------------------------------------------------------------------- /resources/views/auth/request-password-reset.blade.php: -------------------------------------------------------------------------------- 1 | @extends('ambulatory::layouts.auth') 2 | 3 | @section('title', 'Reset password') 4 | 5 | @section('form-content') 6 |
7 |

Reset password

8 | 9 | Sign in? 10 |
11 | 12 |
13 | @csrf 14 | 15 | @if (session()->has('invalidResetToken')) 16 |
17 | Invalid reset token. 18 |
19 | @endif 20 | 21 | @if (session()->has('passwordResetLinkSent')) 22 |
23 | We have e-mailed your password reset link! 24 |
25 | @endif 26 | 27 |
28 | 29 | 30 | 36 | 37 | @if ($errors->has('email')) 38 | 39 | {{ $errors->first('email') }} 40 | 41 | @endif 42 |
43 | 44 |
45 | 46 |
47 | 48 |

49 | Don't have an account? Sign Up 50 |

51 |
52 | @endsection -------------------------------------------------------------------------------- /resources/views/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 | @extends('ambulatory::layouts.auth') 2 | 3 | @section('title', 'Reset password') 4 | 5 | @section('form-content') 6 |
7 |

New password

8 | 9 | Sign in? 10 |
11 | 12 |
13 | Copy your new password, use it for your 14 | next login, 15 | and then reset it. 16 | 17 | {{$password}} 18 |
19 | @endsection -------------------------------------------------------------------------------- /resources/views/emails/credential.blade.php: -------------------------------------------------------------------------------- 1 | Hello, 2 |
Use your email to log in to {{config('app.name')}}. use this password and then reset it. 3 |
Password: {{$password}} -------------------------------------------------------------------------------- /resources/views/emails/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 | Hello 2 |
Please follow this link to reset your password: 3 |
{{$link}} 4 |
This password reset link will expire in 60 minutes. -------------------------------------------------------------------------------- /resources/views/emails/invitation.blade.php: -------------------------------------------------------------------------------- 1 | Hello, 2 |
You have been invited to {{config('app.name')}} with access as a {{$role}}. 3 |
Please follow this link to accept the invitation: 4 |
{{$link}} -------------------------------------------------------------------------------- /resources/views/layouts/auth.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{-- Meta Information --}} 5 | 6 | 7 | 8 | 9 | 10 | {{ config('app.name') }} - @yield('title') 11 | 12 | 13 | 14 | 15 | 16 |
17 | {{-- header --}} 18 |
19 |

20 | {{ config('app.name') }} 21 |

22 | 23 | @if(session()->has('loggedOut')) 24 |

25 | You've been logged out. 26 |

27 | @endif 28 |
29 | 30 | {{-- content --}} 31 |
32 | @yield('form-content') 33 |
34 |
35 | 36 | -------------------------------------------------------------------------------- /resources/views/layouts/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{-- Meta Information --}} 5 | 6 | 7 | 8 | 9 | 10 | {{ config('app.name') }} 11 | 12 | 13 | 14 | 15 | 16 |
17 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 30 | 31 |
32 | 33 |
34 |
35 |
36 |
37 | 38 | {{-- Global Ambulatory Object --}} 39 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | name('login'); 5 | Route::post('/login', 'LoginController@login')->name('login.attempt'); 6 | 7 | // Register new account. 8 | Route::get('/register', 'RegisterController@showRegistrationForm')->name('register'); 9 | Route::post('/register', 'RegisterController@register')->name('register.attempt'); 10 | 11 | // Forgot password. 12 | Route::get('/password/forgot', 'ForgotPasswordController@showResetRequestForm')->name('password.forgot'); 13 | Route::post('/password/forgot', 'ForgotPasswordController@sendResetLinkEmail')->name('password.email'); 14 | Route::get('/password/reset/{token}', 'ForgotPasswordController@showNewPassword')->name('password.reset'); 15 | 16 | // Accept invitation. 17 | Route::get('/invitation/{token}', 'AcceptInvitationController@show')->name('accept.invitation'); 18 | -------------------------------------------------------------------------------- /routes/dashboard.php: -------------------------------------------------------------------------------- 1 | name('inbox'); 5 | Route::get('/api/inbox/{id}', 'InboxController@show')->name('inbox.show'); 6 | 7 | // Schedules. 8 | Route::get('/api/schedules', 'ScheduleController@index')->name('schedules'); 9 | Route::post('/api/schedules', 'ScheduleController@store')->name('schedules.store'); 10 | Route::get('/api/schedules/{schedule}', 'ScheduleController@show')->name('schedules.show'); 11 | Route::patch('/api/schedules/{schedule}', 'ScheduleController@update')->name('schedules.update'); 12 | // schedule availability. 13 | Route::post('/api/schedules/{schedule}/availability', 'ScheduleAvailabilityController@store')->name('schedules.availability'); 14 | 15 | // book an appointment. 16 | Route::get('/api/booking/{schedule}/availability-slots', 'BookAppointmentController@index')->name('book.appointment'); 17 | Route::post('/api/booking/{schedule}/availability-slots', 'BookAppointmentController@store')->name('book.appointment'); 18 | 19 | // Doctors. 20 | Route::get('/api/doctors', 'DoctorController@index')->name('doctors'); 21 | Route::get('/api/doctors/{doctor}', 'DoctorController@show')->name('doctors.show'); 22 | 23 | // Medical Forms. 24 | Route::get('/api/medical-forms', 'MedicalFormController@index')->name('medical-forms'); 25 | Route::post('/api/medical-forms', 'MedicalFormController@store')->name('medical-forms.store'); 26 | Route::get('/api/medical-forms/{medicalForm}', 'MedicalFormController@show')->name('medical-forms.show'); 27 | Route::patch('/api/medical-forms/{medicalForm}', 'MedicalFormController@update')->name('medical-forms.update'); 28 | 29 | // Invitations. 30 | Route::get('/api/invitations', 'InvitationController@index')->name('invitations'); 31 | Route::post('/api/invitations', 'InvitationController@store')->name('invitations.store'); 32 | Route::get('/api/invitations/{invitation}', 'InvitationController@show')->name('invitations.show'); 33 | Route::patch('/api/invitations/{invitation}', 'InvitationController@update')->name('invitations.update'); 34 | Route::delete('/api/invitations/{invitation}', 'InvitationController@destroy')->name('invitations.destroy'); 35 | 36 | // Health Facilities. 37 | Route::get('/api/health-facilities', 'HealthFacilityController@index')->name('health-facilities'); 38 | Route::post('/api/health-facilities', 'HealthFacilityController@store')->name('health-facilities.store'); 39 | Route::get('/api/health-facilities/{healthFacility}', 'HealthFacilityController@show')->name('health-facilities.show'); 40 | Route::patch('/api/health-facilities/{healthFacility}', 'HealthFacilityController@update')->name('health-facilities.update'); 41 | 42 | // Specializations. 43 | Route::get('/api/specializations', 'SpecializationController@index')->name('specializations'); 44 | Route::post('/api/specializations', 'SpecializationController@store')->name('specializations.store'); 45 | Route::get('/api/specializations/{specialization}', 'SpecializationController@show')->name('specializations.show'); 46 | Route::patch('/api/specializations/{specialization}', 'SpecializationController@update')->name('specializations.update'); 47 | Route::delete('/api/specializations/{specialization}', 'SpecializationController@destroy')->name('specializations.destroy'); 48 | 49 | // Staff. 50 | Route::get('/api/staff', 'StaffController@index')->name('staff'); 51 | // Availabilities. 52 | Route::patch('/api/availabilities/{availability}', 'AvailabilityController@update')->name('availabilities.update'); 53 | 54 | // Settings. 55 | Route::namespace('Settings')->group(function () { 56 | // Account. 57 | Route::get('/api/account', 'AccountController@show')->name('account'); 58 | Route::patch('/api/account', 'AccountController@update')->name('account'); 59 | // Doctor profile. 60 | Route::get('/api/doctor-profile/{doctor}', 'DoctorProfileController@show')->name('doctor-profile.show'); 61 | Route::post('/api/doctor-profile', 'DoctorProfileController@store')->name('doctor-profile.store'); 62 | Route::patch('/api/doctor-profile/{doctor}', 'DoctorProfileController@update')->name('doctor-profile.update'); 63 | // User Avatar. 64 | Route::post('/api/uploads-user-avatar', 'UploadUserAvatarController@create')->name('upload-user-avatar'); 65 | // New password. 66 | Route::post('/api/new-password', 'NewPasswordController@update')->name('new-password'); 67 | }); 68 | 69 | // Logout Route. 70 | Route::get('/logout', 'Auth\LoginController@logout')->name('logout'); 71 | // Catch-all Route. 72 | Route::get('/{view?}', 'AmbulatoryController')->where('view', '(.*)')->name('ambulatory'); 73 | -------------------------------------------------------------------------------- /src/Ambulatory.php: -------------------------------------------------------------------------------- 1 | DoctorPolicy::class, 21 | Schedule::class => DoctorSchedulePolicy::class, 22 | MedicalForm::class => MedicalFormPolicy::class, 23 | Availability::class => AvailabilityPolicy::class, 24 | ]; 25 | 26 | /** 27 | * Register ambulatory policies. 28 | * 29 | * @return void 30 | */ 31 | public function registerPolicies() 32 | { 33 | foreach ($this->policies as $key => $value) { 34 | Gate::policy($key, $value); 35 | } 36 | } 37 | 38 | /** 39 | * Get the default JavaScript variables for Ambulatory. 40 | * 41 | * @return array 42 | */ 43 | public static function scriptVariables() 44 | { 45 | return [ 46 | 'path' => config('ambulatory.path'), 47 | 'user' => auth('ambulatory')->check() 48 | ? auth('ambulatory')->user()->scriptVariables() 49 | : null, 50 | ]; 51 | } 52 | 53 | /** 54 | * Disable wrapping of the outer-most resource by default. 55 | * 56 | * @return void 57 | */ 58 | public function wrapResource() 59 | { 60 | Resource::withoutWrapping(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AmbulatoryModel.php: -------------------------------------------------------------------------------- 1 | registerRoutes(); 20 | $this->registerMigrations(); 21 | $this->registerAuthGuard(); 22 | $this->registerPublishing(); 23 | $this->registerResourceViews(); 24 | 25 | $ambulatory->registerPolicies(); 26 | $ambulatory->wrapResource(); 27 | } 28 | 29 | /** 30 | * Register the package routes. 31 | * 32 | * @return void 33 | */ 34 | private function registerRoutes() 35 | { 36 | $path = config('ambulatory.path'); 37 | 38 | Route::namespace('Ambulatory\Http\Controllers\Auth') 39 | ->middleware(['web', RedirectIfAuthenticated::class]) 40 | ->as('ambulatory.') 41 | ->prefix($path) 42 | ->group(function () { 43 | $this->loadRoutesFrom(__DIR__.'/../routes/auth.php'); 44 | }); 45 | 46 | Route::namespace('Ambulatory\Http\Controllers') 47 | ->middleware(['web', Authenticate::class]) 48 | ->as('ambulatory.') 49 | ->prefix($path) 50 | ->group(function () { 51 | $this->loadRoutesFrom(__DIR__.'/../routes/dashboard.php'); 52 | }); 53 | } 54 | 55 | /** 56 | * Register the package's migrations. 57 | * 58 | * @return void 59 | */ 60 | private function registerMigrations() 61 | { 62 | $this->loadMigrationsFrom(__DIR__.'/Migrations'); 63 | } 64 | 65 | /** 66 | * Register the package's authentication guard. 67 | * 68 | * @return void 69 | */ 70 | private function registerAuthGuard() 71 | { 72 | $this->app['config']->set('auth.providers.ambulatory_users', [ 73 | 'driver' => 'eloquent', 74 | 'model' => User::class, 75 | ]); 76 | 77 | $this->app['config']->set('auth.guards.ambulatory', [ 78 | 'driver' => 'session', 79 | 'provider' => 'ambulatory_users', 80 | ]); 81 | } 82 | 83 | /** 84 | * Register the package's publishable resources. 85 | * 86 | * @return void 87 | */ 88 | private function registerPublishing() 89 | { 90 | if ($this->app->runningInConsole()) { 91 | $this->publishes([ 92 | __DIR__.'/../public' => public_path('vendor/ambulatory'), 93 | ], 'ambulatory-assets'); 94 | 95 | $this->publishes([ 96 | __DIR__.'/../config/ambulatory.php' => config_path('ambulatory.php'), 97 | ], 'ambulatory-config'); 98 | } 99 | } 100 | 101 | /** 102 | * Register the package resource views. 103 | * 104 | * @return void 105 | */ 106 | public function registerResourceViews() 107 | { 108 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'ambulatory'); 109 | } 110 | 111 | /** 112 | * Register any package services. 113 | * 114 | * @return void 115 | */ 116 | public function register() 117 | { 118 | $this->mergeConfigFrom(__DIR__.'/../config/ambulatory.php', 'ambulatory'); 119 | 120 | $this->commands([ 121 | Console\InstallCommand::class, 122 | Console\MigrateCommand::class, 123 | ]); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Availability.php: -------------------------------------------------------------------------------- 1 | 'array', 60 | ]; 61 | 62 | /** 63 | * The schedule of availability. 64 | * 65 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 66 | */ 67 | public function schedule() 68 | { 69 | return $this->belongsTo(Schedule::class, 'schedule_id'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Booking.php: -------------------------------------------------------------------------------- 1 | 'boolean', 56 | ]; 57 | 58 | /** 59 | * The booking schedule. 60 | * 61 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 62 | */ 63 | public function schedule() 64 | { 65 | return $this->belongsTo(Schedule::class, 'schedule_id'); 66 | } 67 | 68 | /** 69 | * The booking medical form. 70 | * 71 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 72 | */ 73 | public function medicalForm() 74 | { 75 | return $this->belongsTo(MedicalForm::class, 'medical_form_id'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | comment('Publishing Ambulatory Assets...'); 31 | $this->callSilent('vendor:publish', ['--tag' => 'ambulatory-assets']); 32 | 33 | $this->comment('Publishing Ambulatory Configuration...'); 34 | $this->callSilent('vendor:publish', ['--tag' => 'ambulatory-config']); 35 | 36 | $this->info('Ambulatory was installed successfully.'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | hasTable('ambulatory_users') || 36 | ! User::count(); 37 | 38 | $this->call('migrate', [ 39 | '--database' => config('ambulatory.database_connection'), 40 | '--path' => 'vendor/ambulatory/ambulatory/src/Migrations', 41 | ]); 42 | 43 | if ($shouldCreateNewUser) { 44 | User::create([ 45 | 'id' => (string) Str::uuid(), 46 | 'name' => 'John Doe', 47 | 'email' => 'admin@mail.com', 48 | 'type' => User::ADMIN, 49 | 'password' => Hash::make($password = Str::random()), 50 | ]); 51 | 52 | $this->line(''); 53 | $this->line('Database migrations installed successfully.'); 54 | $this->line('You may log in using admin@mail.com and password: '.$password.''); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Doctor.php: -------------------------------------------------------------------------------- 1 | 'boolean', 65 | ]; 66 | 67 | /** 68 | * The attributes excluded from the model's JSON form. 69 | * 70 | * @var array 71 | */ 72 | protected $hidden = [ 73 | 'working_hours_rule', 74 | ]; 75 | 76 | /** 77 | * Get the fields for generating the slug. 78 | * 79 | * @var array 80 | */ 81 | protected static $slugFieldsFrom = ['full_name']; 82 | 83 | /** 84 | * The specializations the doctor belongs to. 85 | * 86 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 87 | */ 88 | public function specializations() 89 | { 90 | return $this->belongsToMany(Specialization::class, 'ambulatory_doctors_specializations', 'doctor_id', 'specialization_id'); 91 | } 92 | 93 | /** 94 | * Doctor account. 95 | * 96 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 97 | */ 98 | public function user() 99 | { 100 | return $this->belongsTo(User::class, 'user_id'); 101 | } 102 | 103 | /** 104 | * Doctor schedules. 105 | * 106 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 107 | */ 108 | public function schedules() 109 | { 110 | return $this->hasMany(Schedule::class, 'doctor_id'); 111 | } 112 | 113 | /** 114 | * Appointments for the doctor. 115 | * 116 | * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough 117 | */ 118 | public function appointments() 119 | { 120 | return $this->hasManyThrough(Booking::class, Schedule::class); 121 | } 122 | 123 | /** 124 | * The working hours of a doctor. 125 | * 126 | * @return array 127 | */ 128 | public function workingHours() 129 | { 130 | $rfc = new RRule($this->working_hours_rule); 131 | 132 | $rule = $rfc->getRule(); 133 | 134 | return collect(explode(',', $rule['BYDAY']))->map(function ($day) use ($rule) { 135 | return [ 136 | 'type' => 'wday', 137 | 'intervals' => [ 138 | 'from' => date_format($rule['DTSTART'], 'H:i'), 139 | 'to' => date_format($rule['UNTIL'], 'H:i'), 140 | ], 141 | 'wday' => $day, 142 | ]; 143 | })->toArray(); 144 | } 145 | 146 | /** 147 | * The working hour slot of a doctor. 148 | * 149 | * @param string $incomingDate 150 | * @return array 151 | */ 152 | public function workingHourSlots($incomingDate) 153 | { 154 | $date = Carbon::parse($incomingDate); 155 | 156 | $availability = Arr::where($this->workingHours(), function ($value) use ($date) { 157 | return $value['wday'] === Str::upper(Str::limit($date->format('l'), 2, '')); 158 | }); 159 | 160 | return Arr::collapse($availability); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/HasSlug.php: -------------------------------------------------------------------------------- 1 | addSlug(); 16 | }); 17 | } 18 | 19 | /** 20 | * Add slug to the model. 21 | */ 22 | protected function addSlug() 23 | { 24 | $slugSource = $this->generateSlugFrom(); 25 | 26 | $slug = $this->generateUniqueSlug($slugSource); 27 | 28 | $this->slug = $slug; 29 | } 30 | 31 | /** 32 | * Generate slug from slug fields. 33 | * 34 | * @return string 35 | */ 36 | protected function generateSlugFrom() 37 | { 38 | $slugFields = static::$slugFieldsFrom; 39 | 40 | $slugSource = collect($slugFields) 41 | ->map(function (string $fieldName) { 42 | return data_get($this, $fieldName, ''); 43 | }) 44 | ->implode('-'); 45 | 46 | return substr($slugSource, 0, 100); 47 | } 48 | 49 | /** 50 | * Generate unique slug. 51 | * 52 | * @param string $value 53 | * @return string 54 | */ 55 | protected function generateUniqueSlug(string $value) 56 | { 57 | $slug = $originalSlug = Str::slug($value); 58 | $i = 0; 59 | 60 | while ($this->slugExists($slug, $this->exists ? $this->id : null)) { 61 | $slug = $originalSlug.'-'.$i++; 62 | } 63 | 64 | return $slug; 65 | } 66 | 67 | /** 68 | * Find slug. 69 | * 70 | * @param string $slug 71 | * @param string $ignoreId 72 | * @return bool 73 | */ 74 | protected function slugExists(string $slug, string $ignoreId = null) 75 | { 76 | $query = $this->where('slug', $slug); 77 | 78 | if ($ignoreId) { 79 | $query->where('id', '!=', $ignoreId); 80 | } 81 | 82 | return $query->exists(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/HasUuid.php: -------------------------------------------------------------------------------- 1 | getKey()) { 16 | $model->{$model->getKeyName()} = (string) Str::uuid(); 17 | } 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/HealthFacility.php: -------------------------------------------------------------------------------- 1 | Ambulatory::scriptVariables(), 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/AcceptInvitationController.php: -------------------------------------------------------------------------------- 1 | firstOrFail(), function ($invitation) { 18 | $invitation->accepted(); 19 | 20 | $invitation->delete(); 21 | }); 22 | 23 | return redirect()->route('ambulatory.login')->with('invitationAccepted', true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/ForgotPasswordController.php: -------------------------------------------------------------------------------- 1 | all(), [ 33 | 'email' => 'required|email', 34 | ])->validate(); 35 | 36 | if ($user = User::whereEmail(request('email'))->first()) { 37 | cache(['password.reset.'.$user->id => $token = Str::random()], 38 | now()->addMinutes(60) 39 | ); 40 | 41 | Mail::to($user->email)->send(new ResetPasswordEmail( 42 | encrypt($user->id.'|'.$token) 43 | )); 44 | } 45 | 46 | return redirect()->route('ambulatory.password.forgot')->with('passwordResetLinkSent', true); 47 | } 48 | 49 | /** 50 | * Show the new password to the user. 51 | * 52 | * @param string $token 53 | * @return \Illuminate\Http\Response 54 | */ 55 | public function showNewPassword($token) 56 | { 57 | try { 58 | $token = decrypt($token); 59 | 60 | [$userId, $token] = explode('|', $token); 61 | 62 | $user = User::findOrFail($userId); 63 | } catch (Throwable $e) { 64 | return redirect()->route('ambulatory.password.forgot')->with('invalidResetToken', true); 65 | } 66 | 67 | if (cache('password.reset.'.$userId) != $token) { 68 | return redirect()->route('ambulatory.password.forgot')->with('invalidResetToken', true); 69 | } 70 | 71 | cache()->forget('password.reset.'.$userId); 72 | 73 | $user->password = Hash::make($password = Str::random()); 74 | 75 | $user->save(); 76 | 77 | return view('ambulatory::auth.reset-password', [ 78 | 'password' => $password, 79 | ]); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/LoginController.php: -------------------------------------------------------------------------------- 1 | guard()->logout(); 43 | 44 | $request->session()->invalidate(); 45 | 46 | return redirect()->route('ambulatory.login')->with('loggedOut', true); 47 | } 48 | 49 | /** 50 | * Get the guard to be used during authentication. 51 | * 52 | * @return \Illuminate\Contracts\Auth\StatefulGuard 53 | */ 54 | protected function guard() 55 | { 56 | return Auth::guard('ambulatory'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/RegisterController.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'max:255'], 37 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:ambulatory_users'], 38 | 'password' => ['required', 'string', 'min:6', 'confirmed'], 39 | ]); 40 | } 41 | 42 | /** 43 | * Create a new user instance after a valid registration. 44 | * 45 | * @param array $data 46 | * @return \Ambulatory\Ambulatory\User 47 | */ 48 | protected function create(array $data) 49 | { 50 | return User::create([ 51 | 'id' => Str::uuid(), 52 | 'name' => $data['name'], 53 | 'email' => $data['email'], 54 | 'password' => Hash::make($data['password']), 55 | ]); 56 | } 57 | 58 | /** 59 | * Where to redirect users after register. 60 | * 61 | * @return string 62 | */ 63 | public function redirectPath() 64 | { 65 | return '/'.config('ambulatory.path'); 66 | } 67 | 68 | /** 69 | * Get the guard to be used during registration. 70 | * 71 | * @return \Illuminate\Contracts\Auth\StatefulGuard 72 | */ 73 | protected function guard() 74 | { 75 | return Auth::guard('ambulatory'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Http/Controllers/AvailabilityController.php: -------------------------------------------------------------------------------- 1 | middleware(VerifiedDoctor::class); 18 | } 19 | 20 | /** 21 | * Update the specified availability in storage. 22 | * 23 | * @param Availability $availability 24 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 25 | */ 26 | public function update(AvailabilityRequest $request, Availability $availability) 27 | { 28 | $this->authorize('manage', $availability); 29 | 30 | $availability->update($request->validated()); 31 | 32 | return new AvailabilityResource($availability); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Controllers/BookAppointmentController.php: -------------------------------------------------------------------------------- 1 | json([ 22 | 'data' => $schedule->availabilitySlots($date), 23 | ]); 24 | } 25 | 26 | /** 27 | * Store a newly created booking in storage. 28 | * 29 | * @param BookAppointmentRequest $request 30 | * @param Schedule $schedule 31 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 32 | */ 33 | public function store(BookAppointmentRequest $request, Schedule $schedule) 34 | { 35 | $booking = $schedule->bookings()->create($request->validated()); 36 | 37 | return new BookingResource($booking); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | latest()->paginate(25); 18 | 19 | return DoctorResource::collection($doctors); 20 | } 21 | 22 | /** 23 | * Display the specified doctor. 24 | * 25 | * @param Doctor $doctor 26 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 27 | */ 28 | public function show(Doctor $doctor) 29 | { 30 | return new DoctorResource($doctor->load('user', 'specializations', 'schedules.healthFacility')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Controllers/HealthFacilityController.php: -------------------------------------------------------------------------------- 1 | middleware(Admin::class)->except('index'); 18 | } 19 | 20 | /** 21 | * Display a listing of the health facilities. 22 | * 23 | * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse 24 | */ 25 | public function index() 26 | { 27 | $healthFacilities = HealthFacility::latest()->paginate(25); 28 | 29 | return HealthFacilityResource::collection($healthFacilities); 30 | } 31 | 32 | /** 33 | * Store a newly created health facility in storage. 34 | * 35 | * @param HealthFacilityRequest $request 36 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 37 | */ 38 | public function store(HealthFacilityRequest $request) 39 | { 40 | $healthFacility = HealthFacility::create($request->validated()); 41 | 42 | return new HealthFacilityResource($healthFacility); 43 | } 44 | 45 | /** 46 | * Display the specified medical form. 47 | * 48 | * @param HealthFacility $healthFacility 49 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 50 | */ 51 | public function show(HealthFacility $healthFacility) 52 | { 53 | return new HealthFacilityResource($healthFacility); 54 | } 55 | 56 | /** 57 | * Update the specified health facility in storage. 58 | * 59 | * @param HealthFacilityRequest $request 60 | * @param HealthFacility $healthFacility 61 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 62 | */ 63 | public function update(HealthFacilityRequest $request, HealthFacility $healthFacility) 64 | { 65 | $healthFacility->update($request->validated()); 66 | 67 | return new HealthFacilityResource($healthFacility); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Http/Controllers/InboxController.php: -------------------------------------------------------------------------------- 1 | user() 17 | ->inbox() 18 | ->with('schedule.doctor', 'schedule.healthFacility') 19 | ->paginate(25); 20 | 21 | return BookingResource::collection($bookings); 22 | } 23 | 24 | /** 25 | * Show the appointment to the user inbox. 26 | * 27 | * @param string $id 28 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 29 | */ 30 | public function show($id) 31 | { 32 | $booking = auth('ambulatory')->user() 33 | ->inbox() 34 | ->with('schedule.doctor', 'schedule.healthFacility', 'medicalForm') 35 | ->findOrFail($id); 36 | 37 | return new BookingResource($booking); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Http/Controllers/InvitationController.php: -------------------------------------------------------------------------------- 1 | middleware(Admin::class); 18 | } 19 | 20 | /** 21 | * Display a listing of the invitations. 22 | * 23 | * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse 24 | */ 25 | public function index() 26 | { 27 | $invitations = Invitation::latest()->paginate(25); 28 | 29 | return InvitationResource::collection($invitations); 30 | } 31 | 32 | /** 33 | * Store a newly created invitation in storage. 34 | * 35 | * @param InvitationRequest $request 36 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 37 | */ 38 | public function store(InvitationRequest $request) 39 | { 40 | $invitation = Invitation::create($request->validatedFields()); 41 | 42 | return new InvitationResource($invitation); 43 | } 44 | 45 | /** 46 | * Display the specified invitation. 47 | * 48 | * @param Invitation $invitation 49 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 50 | */ 51 | public function show(Invitation $invitation) 52 | { 53 | return new InvitationResource($invitation); 54 | } 55 | 56 | /** 57 | * Update the specified invitation in storage. 58 | * 59 | * @param InvitationRequest $request 60 | * @param Invitation $invitation 61 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 62 | */ 63 | public function update(InvitationRequest $request, Invitation $invitation) 64 | { 65 | $invitation->update($request->validatedFields()); 66 | 67 | return new InvitationResource($invitation); 68 | } 69 | 70 | /** 71 | * Remove the specified invitation from storage. 72 | * 73 | * @param Invitation $invitation 74 | * @return \Illuminate\Http\Response 75 | */ 76 | public function destroy(Invitation $invitation) 77 | { 78 | $invitation->delete(); 79 | 80 | return response()->json(null, 204); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Http/Controllers/MedicalFormController.php: -------------------------------------------------------------------------------- 1 | id())->paginate(25); 19 | 20 | return MedicalFormResource::collection($medicalForms); 21 | } 22 | 23 | /** 24 | * Store a newly created medical form in storage. 25 | * 26 | * @param MedicalFormRequest $request 27 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 28 | */ 29 | public function store(MedicalFormRequest $request) 30 | { 31 | $medicalForm = MedicalForm::create($request->validated() + ['user_id' => auth('ambulatory')->id()]); 32 | 33 | return new MedicalFormResource($medicalForm); 34 | } 35 | 36 | /** 37 | * Display the specified medical form. 38 | * 39 | * @param MedicalForm $medicalForm 40 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 41 | */ 42 | public function show(MedicalForm $medicalForm) 43 | { 44 | $this->authorize('manage', $medicalForm); 45 | 46 | return new MedicalFormResource($medicalForm->load('user')); 47 | } 48 | 49 | /** 50 | * Update the specified medical form in storage. 51 | * 52 | * @param MedicalFormRequest $request 53 | * @param MedicalForm $medicalForm 54 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 55 | */ 56 | public function update(MedicalFormRequest $request, MedicalForm $medicalForm) 57 | { 58 | $this->authorize('manage', $medicalForm); 59 | 60 | $medicalForm->update($request->validated()); 61 | 62 | return new MedicalFormResource($medicalForm); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Http/Controllers/ScheduleAvailabilityController.php: -------------------------------------------------------------------------------- 1 | middleware(VerifiedDoctor::class)->except('index'); 18 | } 19 | 20 | /** 21 | * Display a listing of the availability of schedule. 22 | * 23 | * @param Schedule $schedule 24 | * @return \Illuminate\Http\JsonResponse 25 | */ 26 | public function index(Schedule $schedule) 27 | { 28 | // @todo Custom schedule availability. 29 | } 30 | 31 | /** 32 | * Store a newly created schedule availability in storage. 33 | * 34 | * @param ScheduleAvailabilityRequest $request 35 | * @param Schedule $schedule 36 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 37 | */ 38 | public function store(ScheduleAvailabilityRequest $request, Schedule $schedule) 39 | { 40 | $this->authorize('manage', $schedule); 41 | 42 | $availability = $schedule->addCustomAvailability($request->validated()); 43 | 44 | return new AvailabilityResource($availability); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Http/Controllers/ScheduleController.php: -------------------------------------------------------------------------------- 1 | middleware(VerifiedDoctor::class); 18 | } 19 | 20 | /** 21 | * Display a listing of the schedules. 22 | * 23 | * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse 24 | */ 25 | public function index() 26 | { 27 | $schedules = Schedule::with('healthFacility') 28 | ->where('doctor_id', auth('ambulatory')->user()->doctorProfile->id) 29 | ->latest() 30 | ->paginate(25); 31 | 32 | return ScheduleResource::collection($schedules); 33 | } 34 | 35 | /** 36 | * Store a newly created schedule in storage. 37 | * 38 | * @param ScheduleRequest $request 39 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 40 | */ 41 | public function store(ScheduleRequest $request) 42 | { 43 | $schedule = Schedule::create($request->validatedFields()); 44 | 45 | return new ScheduleResource($schedule); 46 | } 47 | 48 | /** 49 | * Display the specified schedule. 50 | * 51 | * @param Schedule $schedule 52 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 53 | */ 54 | public function show(Schedule $schedule) 55 | { 56 | $this->authorize('manage', $schedule); 57 | 58 | return new ScheduleResource($schedule->load('healthFacility')); 59 | } 60 | 61 | /** 62 | * Update the specified schedule in storage. 63 | * 64 | * @param ScheduleRequest $request 65 | * @param Schedule $schedule 66 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 67 | */ 68 | public function update(ScheduleRequest $request, Schedule $schedule) 69 | { 70 | $this->authorize('manage', $schedule); 71 | 72 | $schedule->update($request->validatedFields()); 73 | 74 | return new ScheduleResource($schedule); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Http/Controllers/Settings/AccountController.php: -------------------------------------------------------------------------------- 1 | user()); 18 | } 19 | 20 | /** 21 | * Update the user account. 22 | * 23 | * @param AccountRequest $request 24 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 25 | */ 26 | public function update(AccountRequest $request) 27 | { 28 | auth('ambulatory')->user()->update($request->validated()); 29 | 30 | return new UserResource(auth('ambulatory')->user()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Controllers/Settings/DoctorProfileController.php: -------------------------------------------------------------------------------- 1 | middleware(AmbulatoryDoctor::class); 20 | } 21 | 22 | /** 23 | * Get doctors' profile. 24 | * 25 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 26 | */ 27 | public function show(Doctor $doctor) 28 | { 29 | $this->authorize('update', $doctor); 30 | 31 | return new DoctorResource($doctor->load('specializations')); 32 | } 33 | 34 | /** 35 | * Store doctors' profile. 36 | * 37 | * @param DoctorProfileRequest $request 38 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 39 | */ 40 | public function store(DoctorProfileRequest $request) 41 | { 42 | $doctor = Doctor::create($request->validatedFields() + ['user_id' => auth('ambulatory')->id()]); 43 | 44 | $doctor->specializations()->sync( 45 | $this->specializations(request('specializations')) 46 | ); 47 | 48 | return new DoctorResource($doctor->load('specializations')); 49 | } 50 | 51 | /** 52 | * Update doctors' profile. 53 | * 54 | * @param DoctorProfileRequest $request 55 | * @param Doctor $doctor 56 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 57 | */ 58 | public function update(DoctorProfileRequest $request, Doctor $doctor) 59 | { 60 | $this->authorize('update', $doctor); 61 | 62 | $doctor->update($request->validatedFields()); 63 | 64 | $doctor->specializations()->sync( 65 | $this->specializations(request('specializations')) 66 | ); 67 | 68 | return new DoctorResource($doctor->load('specializations')); 69 | } 70 | 71 | protected function specializations($specializations) 72 | { 73 | $allSpecializations = Specialization::all(); 74 | 75 | return collect($specializations)->map(function ($specialization) use ($allSpecializations) { 76 | $speciality = $allSpecializations->where('id', $specialization['id'])->first(); 77 | 78 | return (string) $speciality->id; 79 | })->toArray(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Http/Controllers/Settings/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | user()->update([ 20 | 'password' => Hash::make($request->new_password), 21 | ]); 22 | 23 | return response()->json(null, 204); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Controllers/Settings/UploadUserAvatarController.php: -------------------------------------------------------------------------------- 1 | image->store('/public/ambulatory/images', config('ambulatory.storage_disk')) 16 | ); 17 | 18 | return response()->json([ 19 | 'url' => '/'.$path, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/SpecializationController.php: -------------------------------------------------------------------------------- 1 | middleware(Admin::class)->except('index'); 18 | } 19 | 20 | /** 21 | * Display a listing of the specializations. 22 | * 23 | * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse 24 | */ 25 | public function index() 26 | { 27 | $specializations = Specialization::latest()->paginate(25); 28 | 29 | return SpecializationResource::collection($specializations); 30 | } 31 | 32 | /** 33 | * Store a newly created specialization in storage. 34 | * 35 | * @param SpecializationRequest $request 36 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 37 | */ 38 | public function store(SpecializationRequest $request) 39 | { 40 | $specialization = Specialization::create($request->validated()); 41 | 42 | return new SpecializationResource($specialization); 43 | } 44 | 45 | /** 46 | * Display the specified specialization. 47 | * 48 | * @param Specialization $specialization 49 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 50 | */ 51 | public function show(Specialization $specialization) 52 | { 53 | return new SpecializationResource($specialization); 54 | } 55 | 56 | /** 57 | * Update the specified specialization in storage. 58 | * 59 | * @param SpecializationRequest $request 60 | * @param Specialization $specialization 61 | * @return \Illuminate\Http\Resources\Json\JsonResource|\Illuminate\Http\JsonResponse 62 | */ 63 | public function update(SpecializationRequest $request, Specialization $specialization) 64 | { 65 | $specialization->update($request->validated()); 66 | 67 | return new SpecializationResource($specialization); 68 | } 69 | 70 | /** 71 | * Remove the specified specialization from storage. 72 | * 73 | * @param Specialization $specialization 74 | * @return \Illuminate\Http\JsonResponse 75 | */ 76 | public function destroy(Specialization $specialization) 77 | { 78 | $specialization->delete(); 79 | 80 | return response()->json(null, 204); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Http/Controllers/StaffController.php: -------------------------------------------------------------------------------- 1 | middleware(Admin::class); 17 | } 18 | 19 | /** 20 | * Display a listing of the staff. 21 | * 22 | * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse 23 | */ 24 | public function index() 25 | { 26 | $staff = User::whereNotIn('type', [User::PATIENT])->latest()->paginate(25); 27 | 28 | return UserResource::collection($staff); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Middleware/Admin.php: -------------------------------------------------------------------------------- 1 | user()->isAdmin()) { 23 | return $next($request); 24 | } 25 | 26 | throw new HttpException(403, 'Sorry, you are forbidden from accessing this resources.'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 28 | } 29 | 30 | /** 31 | * Handle an incoming request. 32 | * 33 | * @param \Illuminate\Http\Request $request 34 | * @param \Closure $next 35 | * @return mixed 36 | * 37 | * @throws \Illuminate\Auth\AuthenticationException 38 | */ 39 | public function handle(Request $request, Closure $next) 40 | { 41 | if ($this->auth->guard('ambulatory')->check()) { 42 | $this->auth->shouldUse('ambulatory'); 43 | } else { 44 | throw new AuthenticationException( 45 | 'Unauthenticated.', ['ambulatory'], route('ambulatory.login') 46 | ); 47 | } 48 | 49 | return $next($request); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Http/Middleware/Doctor.php: -------------------------------------------------------------------------------- 1 | user()->isDoctor()) { 22 | return $next($request); 23 | } 24 | 25 | return abort(403, 'Sorry, you are forbidden from accessing this resources.'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 27 | } 28 | 29 | /** 30 | * Handle an incoming request. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @param \Closure $next 34 | * @return mixed 35 | * 36 | * @throws \Illuminate\Auth\AuthenticationException 37 | */ 38 | public function handle(Request $request, Closure $next) 39 | { 40 | if ($this->auth->guard('ambulatory')->check()) { 41 | return redirect('/'.config('ambulatory.path')); 42 | } 43 | 44 | return $next($request); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Http/Middleware/VerifiedDoctor.php: -------------------------------------------------------------------------------- 1 | user()->isVerifiedDoctor()) { 22 | return $next($request); 23 | } 24 | 25 | return abort(403, 'Please verify your doctor profile first'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/Requests/AccountRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string', 29 | 'name' => 'required|string|min:3', 30 | 'email' => [ 31 | 'required', 32 | 'email', 33 | Rule::unique(config('ambulatory.database_connection').'.ambulatory_users', 'email') 34 | ->ignore(auth('ambulatory')->id()), 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Http/Requests/AvailabilityRequest.php: -------------------------------------------------------------------------------- 1 | 'nullable|array', 28 | 'date' => 'required|date', 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http/Requests/BookAppointmentRequest.php: -------------------------------------------------------------------------------- 1 | route('schedule'); 29 | 30 | return [ 31 | 'preferred_date_time' => [ 32 | 'bail', 33 | 'required', 34 | 'date', 35 | 'after_or_equal:'.$schedule->start_date->toDateTimeString(), 36 | 'before_or_equal:'.$schedule->end_date->toDateTimeString(), 37 | Rule::unique(config('ambulatory.database_connection').'.ambulatory_bookings', 'preferred_date_time') 38 | ->where('schedule_id', $schedule->id), 39 | new BookingAvailabilityRule($schedule), 40 | ], 41 | 'medical_form_id' => [ 42 | 'bail', 43 | 'required', 44 | 'string', 45 | Rule::exists(config('ambulatory.database_connection').'.ambulatory_medical_forms', 'id') 46 | ->where('user_id', auth('ambulatory')->id()), 47 | ], 48 | 'description' => 'nullable|string', 49 | ]; 50 | } 51 | 52 | /** 53 | * Get custom attributes for validator errors. 54 | * 55 | * @return array 56 | */ 57 | public function attributes() 58 | { 59 | return [ 60 | 'medical_form_id' => 'medical form', 61 | ]; 62 | } 63 | 64 | /** 65 | * Get the error messages for the defined validation rules. 66 | * 67 | * @return array 68 | */ 69 | public function messages() 70 | { 71 | return [ 72 | 'preferred_date_time.unique' => 'The preferred date time has already been booked.', 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Http/Requests/DoctorProfileRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|max:255', 30 | 'qualification' => 'required|string|max:255', 31 | 'practicing_from' => 'required|date|max:255', 32 | 'professional_statement' => 'nullable|string|min:2|max:255', 33 | 'specializations' => 'required|array', 34 | 'specializations.*.id' => Rule::exists(config('ambulatory.database_connection').'.ambulatory_specializations', 'id'), 35 | ]; 36 | } 37 | 38 | /** 39 | * Set the validated fields request that apply to the model. 40 | * 41 | * @return array 42 | */ 43 | public function validatedFields() 44 | { 45 | return [ 46 | 'full_name' => $this->full_name, 47 | 'qualification' => $this->qualification, 48 | 'practicing_from' => $this->practicing_from, 49 | 'professional_statement' => $this->professional_statement, 50 | 'working_hours_rule' => $this->setDefaultWorkingHours(), 51 | ]; 52 | } 53 | 54 | /** 55 | * Set the default working hours rule. 56 | * 57 | * @return string 58 | */ 59 | protected function setDefaultWorkingHours() 60 | { 61 | $rule = new RRule([ 62 | 'freq' => 'daily', 63 | 'byday' => 'MO,TU,WE,TH,FR', 64 | 'dtstart' => today()->createFromTime(9, 0, 0), 65 | 'until' => today()->createFromTime(17, 00, 00), 66 | ]); 67 | 68 | return $rule->rfcString(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Http/Requests/HealthFacilityRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|min:2|max:255', 28 | 'address' => 'required|string|min:2|max:255', 29 | 'city' => 'required|string|min:2|max:255', 30 | 'state' => 'nullable|string|min:2|max:255', 31 | 'country' => 'required|string|max:255', 32 | 'zip_code' => 'required|string|min:2|max:255', 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Requests/InvitationRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|email|max:255|unique:ambulatory_invitations|unique:ambulatory_users', 29 | 'role' => 'required|string', 30 | ]; 31 | } 32 | 33 | /** 34 | * Set the validated fields request that apply to the model. 35 | * 36 | * @return array 37 | */ 38 | public function validatedFields() 39 | { 40 | return [ 41 | 'email' => $this->email, 42 | 'role' => $this->role, 43 | 'token' => Str::limit(md5($this->email.Str::random()), 25, ''), 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Http/Requests/MedicalFormRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|min:2|max:225', 28 | 'full_name' => 'required|string|min:2|max:225', 29 | 'dob' => 'required|date', 30 | 'gender' => 'required|string|min:2|max:225', 31 | 'address' => 'required|string|min:2|max:225', 32 | 'city' => 'required|string|min:2|max:225', 33 | 'state' => 'nullable|string|min:2|max:225', 34 | 'zip_code' => 'required|string|min:2|max:225', 35 | 'home_phone' => 'nullable|string|min:2|max:225', 36 | 'cell_phone' => 'required|string|min:2|max:225', 37 | 'marital_status' => 'required|string|min:2|max:225', 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Http/Requests/NewPasswordRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'min:8', new CurrentPassRule], 29 | 'new_password' => 'required|string|min:8', 30 | 'confirm_new_password' => 'required|string|min:8|same:new_password', 31 | ]; 32 | } 33 | 34 | /** 35 | * Get the error messages for the defined validation rules. 36 | * 37 | * @return array 38 | */ 39 | public function messages() 40 | { 41 | return [ 42 | 'confirm_new_password.same' => 'Password confirmation should match to the new password', 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/Requests/ScheduleAvailabilityRequest.php: -------------------------------------------------------------------------------- 1 | 'required|array', 28 | 'intervals.*.from' => 'required|string', 29 | 'intervals.*.to' => 'required|string', 30 | 'date' => 'required|date', 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Requests/ScheduleRequest.php: -------------------------------------------------------------------------------- 1 | 'required|array', 30 | 'health_facility.*id' => [ 31 | 'bail', 32 | 'required', 33 | Rule::exists(config('ambulatory.database_connection').'.ambulatory_health_facilities', 'id'), 34 | Rule::unique(config('ambulatory.database_connection').'.ambulatory_schedules', 'health_facility_id') 35 | ->ignore($this->id) 36 | ->where('doctor_id', auth('ambulatory')->user()->doctorProfile->id), 37 | ], 38 | 'start_date' => [ 39 | 'bail', 40 | 'required', 41 | 'date', 42 | 'after_or_equal:'.today()->toDateString(), 43 | ], 44 | 'end_date' => 'bail|required|date|after:start_date', 45 | 'service_time' => 'nullable|integer', 46 | ]; 47 | } 48 | 49 | /** 50 | * Set the validated fields request that apply to the model. 51 | * 52 | * @return array 53 | */ 54 | public function validatedFields() 55 | { 56 | $serviceTime = $this->service_time === null 57 | ? Schedule::ESTIMATED_SERVICE_TIME 58 | : $this->service_time; 59 | 60 | return [ 61 | 'doctor_id' => auth('ambulatory')->user()->doctorProfile->id, 62 | 'health_facility_id' => $this->health_facility['id'], 63 | 'start_date' => $this->start_date, 64 | 'end_date' => $this->end_date, 65 | 'estimated_service_time_in_minutes' => $serviceTime, 66 | ]; 67 | } 68 | 69 | /** 70 | * Get custom attributes for validator errors. 71 | * 72 | * @return array 73 | */ 74 | public function attributes() 75 | { 76 | return [ 77 | 'health_facility.id' => 'Health facility', 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Http/Requests/SpecializationRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|min:2|max:255', 28 | 'description' => 'nullable|string|min:2|max:255', 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http/Resources/AvailabilityResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'type' => $this->type, 20 | 'intervals' => $this->intervals, 21 | 'date' => $this->date, 22 | 'created_at' => $this->created_at, 23 | 'updated_at' => $this->updated_at, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Resources/BookingResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'preferred_date_time' => $this->preferred_date_time, 20 | 'actual_end_date_time' => $this->actual_end_date_time, 21 | 'description' => $this->description, 22 | 'is_active' => (bool) $this->is_active, 23 | 'medical_form' => new MedicalFormResource($this->whenLoaded('medicalForm')), 24 | 'doctor_schedule' => new ScheduleResource($this->whenLoaded('schedule')), 25 | 'created_at' => $this->created_at, 26 | 'updated_at' => $this->updated_at, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Http/Resources/DoctorResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'full_name' => $this->full_name, 20 | 'slug' => $this->slug, 21 | 'qualification' => $this->qualification, 22 | 'practicing_from' => $this->practicing_from, 23 | 'professional_statement' => $this->professional_statement, 24 | 'is_active' => (bool) $this->is_active, 25 | 'account' => new UserResource($this->whenLoaded('user')), 26 | 'schedules' => ScheduleResource::collection($this->whenLoaded('schedules')), 27 | 'specializations' => SpecializationResource::collection($this->whenLoaded('specializations')), 28 | 'created_at' => $this->created_at, 29 | 'updated_at' => $this->updated_at, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Resources/HealthFacilityResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'slug' => $this->slug, 21 | 'address' => $this->address, 22 | 'city' => $this->city, 23 | 'state' => $this->state, 24 | 'country' => $this->country, 25 | 'zip_code' => $this->zip_code, 26 | 'created_at' => $this->created_at, 27 | 'updated_at' => $this->updated_at, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Resources/InvitationResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'email' => $this->email, 20 | 'role' => $this->role, 21 | 'created_at' => $this->created_at, 22 | 'updated_at' => $this->updated_at, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Resources/MedicalFormResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'slug' => $this->slug, 20 | 'form_name' => $this->form_name, 21 | 'full_name' => $this->full_name, 22 | 'dob' => $this->dob, 23 | 'gender' => $this->gender, 24 | 'address' => $this->address, 25 | 'city' => $this->city, 26 | 'state' => $this->state, 27 | 'zip_code' => $this->zip_code, 28 | 'home_phone' => $this->home_phone, 29 | 'cell_phone' => $this->cell_phone, 30 | 'marital_status' => $this->marital_status, 31 | 'verified_at' => $this->verified_at, 32 | 'account' => new UserResource($this->whenLoaded('user')), 33 | 'created_at' => $this->created_at, 34 | 'updated_at' => $this->updated_at, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Resources/ScheduleResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'start_date' => $this->start_date, 20 | 'end_date' => $this->end_date, 21 | 'estimated_service_time_in_minutes' => $this->estimated_service_time_in_minutes, 22 | 'doctor' => new DoctorResource($this->whenLoaded('doctor')), 23 | 'health_facility' => new HealthFacilityResource($this->whenLoaded('healthFacility')), 24 | 'created_at' => $this->created_at, 25 | 'updated_at' => $this->updated_at, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/Resources/SpecializationResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'slug' => $this->slug, 21 | 'description' => $this->description, 22 | 'created_at' => $this->created_at, 23 | 'updated_at' => $this->updated_at, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->when($this->id === auth('ambulatory')->id(), $this->email), 21 | 'avatar' => $this->avatar, 22 | 'role' => $this->role, 23 | 'created_at' => $this->created_at, 24 | 'updated_at' => $this->updated_at, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Invitation.php: -------------------------------------------------------------------------------- 1 | email)->send(new InvitationEmail($invitation)); 66 | }); 67 | } 68 | 69 | /** 70 | * Accept the invitation. 71 | * 72 | * @return mixed 73 | */ 74 | public function accepted() 75 | { 76 | $credential = Str::random(); 77 | 78 | tap($this->createNewUser($credential), function ($user) use ($credential) { 79 | Mail::to($user->email)->send(new CredentialEmail($credential)); 80 | }); 81 | } 82 | 83 | /** 84 | * Create a new user. 85 | * 86 | * @param string $credential 87 | * @return User 88 | */ 89 | protected function createNewUser(string $credential) 90 | { 91 | return User::create([ 92 | 'id' => Str::uuid(), 93 | 'name' => $this->email, 94 | 'email' => $this->email, 95 | 'password' => Hash::make($credential), 96 | 'type' => $this->findUserType(), 97 | ]); 98 | } 99 | 100 | /** 101 | * Get the user type. 102 | */ 103 | protected function findUserType() 104 | { 105 | if ($this->role === 'admin') { 106 | return User::ADMIN; 107 | } 108 | 109 | return User::DOCTOR; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Mail/CredentialEmail.php: -------------------------------------------------------------------------------- 1 | password = $password; 23 | } 24 | 25 | /** 26 | * Build the message. 27 | * 28 | * @return $this 29 | */ 30 | public function build() 31 | { 32 | return $this->subject(config('app.name').' [credential]') 33 | ->view('ambulatory::emails.credential', [ 34 | 'password' => $this->password, 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mail/InvitationEmail.php: -------------------------------------------------------------------------------- 1 | invitation = $invitation; 24 | } 25 | 26 | /** 27 | * Build the message. 28 | * 29 | * @return $this 30 | */ 31 | public function build() 32 | { 33 | return $this->subject(config('app.name').': Invitation') 34 | ->view('ambulatory::emails.invitation', [ 35 | 'link' => route('ambulatory.accept.invitation', ['token' => $this->invitation->token]), 36 | 'role' => $this->invitation->role, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mail/ResetPasswordEmail.php: -------------------------------------------------------------------------------- 1 | token = $token; 25 | } 26 | 27 | /** 28 | * Build the message. 29 | * 30 | * @return $this 31 | */ 32 | public function build() 33 | { 34 | return $this->subject(config('app.name').': Reset Password Notification') 35 | ->view('ambulatory::emails.forgot-password', [ 36 | 'link' => route('ambulatory.password.reset', ['token' => $this->token]), 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MedicalForm.php: -------------------------------------------------------------------------------- 1 | hasMany(Booking::class, 'medical_form_id'); 59 | } 60 | 61 | /** 62 | * The medical forms belongs to a user. 63 | * 64 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 65 | */ 66 | public function user() 67 | { 68 | return $this->belongsTo(User::class, 'user_id'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Migrations/2019_02_13_000000_create_tables.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->string('password'); 21 | $table->string('avatar')->nullable(); 22 | $table->smallInteger('type', false, true)->default(1); 23 | $table->rememberToken(); 24 | $table->timestamps(); 25 | }); 26 | 27 | Schema::create('ambulatory_doctors', function (Blueprint $table) { 28 | $table->uuid('id')->primary(); 29 | $table->uuid('user_id')->unique(); 30 | $table->string('slug')->unique(); 31 | $table->string('full_name'); 32 | $table->string('qualification'); 33 | $table->date('practicing_from'); 34 | $table->text('professional_statement')->nullable(); 35 | $table->boolean('is_active')->default(true); 36 | $table->text('working_hours_rule'); 37 | $table->timestamps(); 38 | }); 39 | 40 | Schema::create('ambulatory_specializations', function (Blueprint $table) { 41 | $table->uuid('id')->primary(); 42 | $table->string('slug')->unique(); 43 | $table->string('name'); 44 | $table->string('description')->nullable(); 45 | $table->timestamps(); 46 | }); 47 | 48 | Schema::create('ambulatory_doctors_specializations', function (Blueprint $table) { 49 | $table->uuid('doctor_id'); 50 | $table->uuid('specialization_id'); 51 | // $table->timestamps(); 52 | 53 | $table->unique(['doctor_id', 'specialization_id'], 'doctor_id_specialization_id_unique'); 54 | }); 55 | 56 | Schema::create('ambulatory_medical_forms', function (Blueprint $table) { 57 | $table->uuid('id')->primary(); 58 | $table->uuid('user_id')->index(); 59 | $table->string('slug')->unique(); 60 | $table->string('form_name')->index(); 61 | $table->string('full_name'); 62 | $table->date('dob'); 63 | $table->string('gender'); 64 | $table->text('address'); 65 | $table->string('city'); 66 | $table->string('state')->nullable(); 67 | $table->string('zip_code'); 68 | $table->string('home_phone')->nullable(); 69 | $table->string('cell_phone'); 70 | $table->string('marital_status'); 71 | $table->timestamp('verified_at')->nullable(); 72 | $table->timestamps(); 73 | }); 74 | 75 | Schema::create('ambulatory_health_facilities', function (Blueprint $table) { 76 | $table->uuid('id')->primary(); 77 | $table->string('slug')->unique(); 78 | $table->string('name'); 79 | $table->string('address'); 80 | $table->string('city'); 81 | $table->string('state')->nullable(); 82 | $table->string('country'); 83 | $table->string('zip_code'); 84 | $table->timestamps(); 85 | }); 86 | 87 | Schema::create('ambulatory_invitations', function (Blueprint $table) { 88 | $table->uuid('id')->primary(); 89 | $table->string('email')->unique(); 90 | $table->string('role'); 91 | $table->string('token')->unique(); 92 | $table->timestamps(); 93 | }); 94 | 95 | Schema::create('ambulatory_schedules', function (Blueprint $table) { 96 | $table->uuid('id')->primary(); 97 | $table->uuid('doctor_id'); 98 | $table->uuid('health_facility_id'); 99 | $table->date('start_date'); 100 | $table->date('end_date'); 101 | $table->integer('estimated_service_time_in_minutes')->nullable(); 102 | $table->timestamps(); 103 | 104 | $table->unique(['doctor_id', 'health_facility_id']); 105 | }); 106 | 107 | // to store custom availabilities of schedule. 108 | Schema::create('ambulatory_availabilities', function (Blueprint $table) { 109 | $table->uuid('id')->primary(); 110 | $table->uuid('schedule_id')->index(); 111 | $table->string('type')->default('date'); 112 | $table->text('intervals'); 113 | $table->date('date'); 114 | $table->timestamps(); 115 | }); 116 | 117 | Schema::create('ambulatory_bookings', function (Blueprint $table) { 118 | $table->uuid('id')->primary(); 119 | $table->uuid('schedule_id'); 120 | $table->uuid('medical_form_id')->index(); 121 | $table->dateTime('preferred_date_time'); 122 | $table->dateTime('actual_end_date_time')->nullable(); 123 | $table->string('description')->nullable(); 124 | $table->boolean('is_active')->default(true); 125 | $table->timestamps(); 126 | 127 | $table->unique(['schedule_id', 'preferred_date_time'], 'book_schedule_date_unique'); 128 | }); 129 | } 130 | 131 | /** 132 | * Reverse the migrations. 133 | * 134 | * @return void 135 | */ 136 | public function down() 137 | { 138 | Schema::dropIfExists('ambulatory_users'); 139 | Schema::dropIfExists('ambulatory_doctors'); 140 | Schema::dropIfExists('ambulatory_specializations'); 141 | Schema::dropIfExists('ambulatory_doctors_specializations'); 142 | Schema::dropIfExists('ambulatory_medical_forms'); 143 | Schema::dropIfExists('ambulatory_health_facilities'); 144 | Schema::dropIfExists('ambulatory_invitations'); 145 | Schema::dropIfExists('ambulatory_schedules'); 146 | Schema::dropIfExists('ambulatory_availabilities'); 147 | Schema::dropIfExists('ambulatory_bookings'); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Policies/AvailabilityPolicy.php: -------------------------------------------------------------------------------- 1 | doctorProfile->id === $availability->schedule->doctor_id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Policies/DoctorPolicy.php: -------------------------------------------------------------------------------- 1 | id === $doctor->user_id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Policies/DoctorSchedulePolicy.php: -------------------------------------------------------------------------------- 1 | doctorProfile->id === $schedule->doctor_id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Policies/MedicalFormPolicy.php: -------------------------------------------------------------------------------- 1 | id === $medicalForm->user_id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Rules/BookingAvailabilityRule.php: -------------------------------------------------------------------------------- 1 | schedule = $schedule; 25 | } 26 | 27 | /** 28 | * Determine if the validation rule passes. 29 | * 30 | * @param string $attribute 31 | * @param mixed $value 32 | * @return bool 33 | */ 34 | public function passes($attribute, $value) 35 | { 36 | return $this->schedule->checkAvailabilitySlot($value); 37 | } 38 | 39 | /** 40 | * Get the validation error message. 41 | * 42 | * @return string 43 | */ 44 | public function message() 45 | { 46 | return 'The preferred date time is not available.'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Rules/CurrentPassRule.php: -------------------------------------------------------------------------------- 1 | user()->getAuthPassword()); 20 | } 21 | 22 | /** 23 | * Get the validation error message. 24 | * 25 | * @return string 26 | */ 27 | public function message() 28 | { 29 | return 'Your current password is incorrect.'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Specialization.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Doctor::class, 'ambulatory_doctors_specializations', 'specialization_id', 'doctor_id'); 59 | } 60 | 61 | /** 62 | * boot the model. 63 | * 64 | * @return void 65 | */ 66 | protected static function boot() 67 | { 68 | parent::boot(); 69 | 70 | static::deleting(function ($item) { 71 | $item->doctors()->detach(); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | const webpack = require('webpack'); 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Mix Asset Management 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Mix provides a clean, fluent API for defining some Webpack build steps 10 | | for your Laravel application. By default, we are compiling the Sass 11 | | file for your application, as well as bundling up your JS files. 12 | | 13 | */ 14 | 15 | mix 16 | .options({ 17 | uglify: { 18 | uglifyOptions: { 19 | compress: { 20 | drop_console: true, 21 | } 22 | } 23 | } 24 | }) 25 | .webpackConfig({ 26 | plugins: [ 27 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 28 | ], 29 | }); 30 | 31 | mix 32 | .setPublicPath('public') 33 | .js('resources/js/app.js', 'public') 34 | .sass('resources/sass/app.scss', 'public') 35 | .version() 36 | .copy('public', '../ambulatorytest/public/vendor/ambulatory'); --------------------------------------------------------------------------------