commit 94cd8a1dc9fbca708a9b1fe538a8ef59510ca0d1 Author: Inshal Date: Fri Oct 25 01:05:27 2024 +0500 initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e9a221d Binary files /dev/null and b/.DS_Store differ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json new file mode 100644 index 0000000..3a62cc0 --- /dev/null +++ b/.eslintrc-auto-import.json @@ -0,0 +1,323 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": true, + "WritableComputedRef": true, + "acceptHMRUpdate": true, + "asyncComputed": true, + "autoResetRef": true, + "computed": true, + "computedAsync": true, + "computedEager": true, + "computedInject": true, + "computedWithControl": true, + "controlledComputed": true, + "controlledRef": true, + "createApp": true, + "createEventHook": true, + "createGenericProjection": true, + "createGlobalState": true, + "createInjectionState": true, + "createPinia": true, + "createProjection": true, + "createReactiveFn": true, + "createReusableTemplate": true, + "createSharedComposable": true, + "createTemplatePromise": true, + "createUnrefFn": true, + "customRef": true, + "debouncedRef": true, + "debouncedWatch": true, + "defineAsyncComponent": true, + "defineComponent": true, + "defineStore": true, + "eagerComputed": true, + "effectScope": true, + "extendRef": true, + "getActivePinia": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "ignorableWatch": true, + "inject": true, + "injectLocal": true, + "isDefined": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "logicAnd": true, + "logicNot": true, + "logicOr": true, + "makeDestructurable": true, + "mapActions": true, + "mapGetters": true, + "mapState": true, + "mapStores": true, + "mapWritableState": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeRouteLeave": true, + "onBeforeRouteUpdate": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onClickOutside": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onKeyStroke": true, + "onLongPress": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onStartTyping": true, + "onUnmounted": true, + "onUpdated": true, + "pausableWatch": true, + "provide": true, + "provideLocal": true, + "reactify": true, + "reactifyObject": true, + "reactive": true, + "reactiveComputed": true, + "reactiveOmit": true, + "reactivePick": true, + "readonly": true, + "ref": true, + "refAutoReset": true, + "refDebounced": true, + "refDefault": true, + "refThrottled": true, + "refWithControl": true, + "resolveComponent": true, + "resolveRef": true, + "resolveUnref": true, + "setActivePinia": true, + "setMapStoreSuffix": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "storeToRefs": true, + "syncRef": true, + "syncRefs": true, + "templateRef": true, + "throttledRef": true, + "throttledWatch": true, + "toRaw": true, + "toReactive": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "tryOnBeforeMount": true, + "tryOnBeforeUnmount": true, + "tryOnMounted": true, + "tryOnScopeDispose": true, + "tryOnUnmounted": true, + "unref": true, + "unrefElement": true, + "until": true, + "useAbs": true, + "useActiveElement": true, + "useAnimate": true, + "useArrayDifference": true, + "useArrayEvery": true, + "useArrayFilter": true, + "useArrayFind": true, + "useArrayFindIndex": true, + "useArrayFindLast": true, + "useArrayIncludes": true, + "useArrayJoin": true, + "useArrayMap": true, + "useArrayReduce": true, + "useArraySome": true, + "useArrayUnique": true, + "useAsyncQueue": true, + "useAsyncState": true, + "useAttrs": true, + "useAverage": true, + "useBase64": true, + "useBattery": true, + "useBluetooth": true, + "useBreakpoints": true, + "useBroadcastChannel": true, + "useBrowserLocation": true, + "useCached": true, + "useCeil": true, + "useClamp": true, + "useClipboard": true, + "useClipboardItems": true, + "useCloned": true, + "useColorMode": true, + "useConfirmDialog": true, + "useCounter": true, + "useCssModule": true, + "useCssVar": true, + "useCssVars": true, + "useCurrentElement": true, + "useCycleList": true, + "useDark": true, + "useDateFormat": true, + "useDebounce": true, + "useDebounceFn": true, + "useDebouncedRefHistory": true, + "useDeviceMotion": true, + "useDeviceOrientation": true, + "useDevicePixelRatio": true, + "useDevicesList": true, + "useDisplayMedia": true, + "useDocumentVisibility": true, + "useDraggable": true, + "useDropZone": true, + "useElementBounding": true, + "useElementByPoint": true, + "useElementHover": true, + "useElementSize": true, + "useElementVisibility": true, + "useEventBus": true, + "useEventListener": true, + "useEventSource": true, + "useEyeDropper": true, + "useFavicon": true, + "useFetch": true, + "useFileDialog": true, + "useFileSystemAccess": true, + "useFloor": true, + "useFocus": true, + "useFocusWithin": true, + "useFps": true, + "useFullscreen": true, + "useGamepad": true, + "useGeolocation": true, + "useIdle": true, + "useImage": true, + "useInfiniteScroll": true, + "useIntersectionObserver": true, + "useInterval": true, + "useIntervalFn": true, + "useKeyModifier": true, + "useLastChanged": true, + "useLink": true, + "useLocalStorage": true, + "useMagicKeys": true, + "useManualRefHistory": true, + "useMath": true, + "useMax": true, + "useMediaControls": true, + "useMediaQuery": true, + "useMemoize": true, + "useMemory": true, + "useMin": true, + "useMounted": true, + "useMouse": true, + "useMouseInElement": true, + "useMousePressed": true, + "useMutationObserver": true, + "useNavigatorLanguage": true, + "useNetwork": true, + "useNow": true, + "useObjectUrl": true, + "useOffsetPagination": true, + "useOnline": true, + "usePageLeave": true, + "useParallax": true, + "useParentElement": true, + "usePerformanceObserver": true, + "usePermission": true, + "usePointer": true, + "usePointerLock": true, + "usePointerSwipe": true, + "usePrecision": true, + "usePreferredColorScheme": true, + "usePreferredContrast": true, + "usePreferredDark": true, + "usePreferredLanguages": true, + "usePreferredReducedMotion": true, + "usePrevious": true, + "useProjection": true, + "useRafFn": true, + "useRefHistory": true, + "useResizeObserver": true, + "useRound": true, + "useRoute": true, + "useRouter": true, + "useScreenOrientation": true, + "useScreenSafeArea": true, + "useScriptTag": true, + "useScroll": true, + "useScrollLock": true, + "useSessionStorage": true, + "useShare": true, + "useSlots": true, + "useSorted": true, + "useSpeechRecognition": true, + "useSpeechSynthesis": true, + "useStepper": true, + "useStorage": true, + "useStorageAsync": true, + "useStyleTag": true, + "useSum": true, + "useSupported": true, + "useSwipe": true, + "useTemplateRefsList": true, + "useTextDirection": true, + "useTextSelection": true, + "useTextareaAutosize": true, + "useThrottle": true, + "useThrottleFn": true, + "useThrottledRefHistory": true, + "useTimeAgo": true, + "useTimeout": true, + "useTimeoutFn": true, + "useTimeoutPoll": true, + "useTimestamp": true, + "useTitle": true, + "useToNumber": true, + "useToString": true, + "useToggle": true, + "useTransition": true, + "useTrunc": true, + "useUrlSearchParams": true, + "useUserMedia": true, + "useVModel": true, + "useVModels": true, + "useVibrate": true, + "useVirtualList": true, + "useWakeLock": true, + "useWebNotification": true, + "useWebSocket": true, + "useWebWorker": true, + "useWebWorkerFn": true, + "useWindowFocus": true, + "useWindowScroll": true, + "useWindowSize": true, + "watch": true, + "watchArray": true, + "watchAtMost": true, + "watchDebounced": true, + "watchDeep": true, + "watchEffect": true, + "watchIgnorable": true, + "watchImmediate": true, + "watchOnce": true, + "watchPausable": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "watchThrottled": true, + "watchTriggerable": true, + "watchWithFilter": true, + "whenever": true + } +} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..925f72e --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,230 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: [ + '.eslintrc-auto-import.json', + 'plugin:vue/vue3-recommended', + 'plugin:import/recommended', + 'plugin:promise/recommended', + 'plugin:sonarjs/recommended', + + // 'plugin:unicorn/recommended', + ], + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 13, + sourceType: 'module', + }, + plugins: [ + 'vue', + 'regex', + ], + ignorePatterns: ['resources/js/@iconify/*.js', 'node_modules', 'dist'], + rules: { + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + + // indentation (Already present in TypeScript) + 'comma-spacing': ['error', { before: false, after: true }], + 'key-spacing': ['error', { afterColon: true }], + + 'vue/first-attribute-linebreak': ['error', { + singleline: 'beside', + multiline: 'below', + }], + + + // indentation (Already present in TypeScript) + 'indent': ['error', 2], + + // Enforce trailing comma (Already present in TypeScript) + 'comma-dangle': ['error', 'always-multiline'], + + // Enforce consistent spacing inside braces of object (Already present in TypeScript) + 'object-curly-spacing': ['error', 'always'], + + // Disable max-len + 'max-len': 'off', + + // we don't want it + 'semi': ['error', 'never'], + + // add parens ony when required in arrow function + 'arrow-parens': ['error', 'as-needed'], + + // add new line above comment + 'newline-before-return': 'error', + + // add new line above comment + 'lines-around-comment': [ + 'error', + { + beforeBlockComment: true, + beforeLineComment: true, + allowBlockStart: true, + allowClassStart: true, + allowObjectStart: true, + allowArrayStart: true, + }, + ], + + // Ignore _ as unused variable + + 'array-element-newline': ['error', 'consistent'], + 'array-bracket-newline': ['error', 'consistent'], + + 'vue/multi-word-component-names': 'off', + + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: 'expression', next: 'const' }, + { blankLine: 'always', prev: 'const', next: 'expression' }, + { blankLine: 'always', prev: 'multiline-const', next: '*' }, + { blankLine: 'always', prev: '*', next: 'multiline-const' }, + ], + + // Plugin: eslint-plugin-import + 'import/prefer-default-export': 'off', + 'import/newline-after-import': ['error', { count: 1 }], + 'no-restricted-imports': ['error', 'vuetify/components'], + + // For omitting extension for ts files + 'import/extensions': [ + 'error', + 'ignorePackages', + { + js: 'never', + jsx: 'never', + ts: 'never', + tsx: 'never', + }, + ], + + // ignore virtual files + 'import/no-unresolved': [2, { + ignore: [ + '~pages$', + 'virtual:generated-layouts', + + // Ignore vite's ?raw imports + '.*\?raw', + ], + }], + + // Thanks: https://stackoverflow.com/a/63961972/10796681 + 'no-shadow': 'off', + + + // Plugin: eslint-plugin-promise + 'promise/always-return': 'off', + 'promise/catch-or-return': 'off', + + // ESLint plugin vue + 'vue/block-tag-newline': 'error', + 'vue/component-api-style': 'error', + 'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }], + 'vue/custom-event-name-casing': ['error', 'camelCase', { + ignores: [ + '/^(click):[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?/', + ], + }], + 'vue/define-macros-order': 'error', + 'vue/html-comment-content-newline': 'error', + 'vue/html-comment-content-spacing': 'error', + 'vue/html-comment-indent': 'error', + 'vue/match-component-file-name': 'error', + 'vue/no-child-content': 'error', + 'vue/require-default-prop': 'off', + + // NOTE this rule only supported in SFC, Users of the unplugin-vue-define-options should disable that rule: https://github.com/vuejs/eslint-plugin-vue/issues/1886 + // 'vue/no-duplicate-attr-inheritance': 'error', + 'vue/no-empty-component-block': 'error', + 'vue/no-multiple-objects-in-class': 'error', + 'vue/no-reserved-component-names': 'error', + 'vue/no-template-target-blank': 'error', + 'vue/no-useless-mustaches': 'error', + 'vue/no-useless-v-bind': 'error', + 'vue/padding-line-between-blocks': 'error', + 'vue/prefer-separate-static-class': 'error', + 'vue/prefer-true-attribute-shorthand': 'error', + 'vue/v-on-function-call': 'error', + 'vue/no-restricted-class': ['error', '/^(p|m)(l|r)-/'], + 'vue/valid-v-slot': ['error', { + allowModifiers: true, + }], + + // -- Extension Rules + 'vue/no-irregular-whitespace': 'error', + 'vue/template-curly-spacing': 'error', + + // -- Sonarlint + 'sonarjs/no-duplicate-string': 'off', + 'sonarjs/no-nested-template-literals': 'off', + + // -- Unicorn + // 'unicorn/filename-case': 'off', + // 'unicorn/prevent-abbreviations': ['error', { + // replacements: { + // props: false, + // }, + // }], + + // https://github.com/gmullerb/eslint-plugin-regex + 'regex/invalid': [ + 'error', + [ + { + regex: '@/assets/images', + replacement: '@images', + message: 'Use \'@images\' path alias for image imports', + }, + { + regex: '@/styles', + replacement: '@styles', + message: 'Use \'@styles\' path alias for importing styles from \'resources/js/styles\'', + }, + + // { + // id: 'Disallow icon of icon library', + // regex: 'tabler-\\w', + // message: 'Only \'mdi\' icons are allowed', + // }, + + { + regex: '@core/\\w', + message: 'You can\'t use @core when you are in @layouts module', + files: { + inspect: '@layouts/.*', + }, + }, + { + regex: 'useLayouts\\(', + message: '`useLayouts` composable is only allowed in @layouts & @core directory. Please use `useThemeConfig` composable instead.', + files: { + inspect: '^(?!.*(@core|@layouts)).*', + }, + }, + { + regex: 'import axios from \'axios\'', + replacement: 'import axios from \'@axios\'', + message: 'Use axios instances created in \'resources/js/plugin/axios.js\' instead of unconfigured axios', + files: { + ignore: '^.*plugins/axios.js.*', + }, + }, + ], + + // Ignore files + '\.eslintrc\.js', + ], + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.js', '.jsx', '.jsx', '.mjs', '.png', '.jpg'], + }, alias: { 'extensions': ['.ts', '.js', '.tsx', '.jsx', '.mjs'], 'map': [["@", "./resources/js"], ["@core", "./resources/js/@core"], ["@layouts", "./resources/js/@layouts"], ["@images", "./resources/images/"], ["@styles", "./resources/styles/"], ["@configured-variables", "./resources/styles/variables/_template.scss"], ["@axios", "./resources/js/plugins/axios"], ["apexcharts", "node_modules/apexcharts-clevision"]] }, + }, + }, +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56c1b11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +node_modules +/public/build +/public/hot +/public/storage +vendor +/storage/*.key +.env +.env.backup +.env.production +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.fleet +/.idea +/.vscode +/.phpunit.cache +vendor/* +.env copy +jaasauth.key +jaasauth.key.pub diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..2d81017 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,34 @@ +{ + "extends": [ + "stylelint-config-standard-scss", + "stylelint-config-idiomatic-order" + ], + "plugins": [ + "stylelint-use-logical-spec" + ], + "overrides": [ + { + "files": [ + "**/*.scss" + ], + "customSyntax": "postcss-scss" + }, + { + "files": [ + "**/*.vue" + ], + "customSyntax": "postcss-html" + } + ], + "rules": { + "max-line-length": [ + 120, + { + "ignore": "comments" + } + ], + "liberty/use-logical-spec": true, + "selector-class-pattern": null, + "color-function-notation": null + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1824fc1 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com/)** +- **[Tighten Co.](https://tighten.co)** +- **[WebReinvent](https://webreinvent.com/)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** +- **[Cyber-Duck](https://cyber-duck.co.uk)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Jump24](https://jump24.co.uk)** +- **[Redberry](https://redberry.international/laravel/)** +- **[Active Logic](https://activelogic.com)** +- **[byte5](https://byte5.de)** +- **[OP.GG](https://op.gg)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/_jaasauth.key b/_jaasauth.key new file mode 100644 index 0000000..41bb841 --- /dev/null +++ b/_jaasauth.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAy097HDz6C7OD1P8dUW0ehu01L1FMz+TtdzruDVGNuzmUFkBK +tX2BFJJtXB5AiR7fhHzH0rsHCI2iq1X6LpdNuoItzKvdG2yxGhQGOkONi2m1KxTQ +Qh77+84mEyaS+PFm6e7Ppt/l/iGiySlo7k9nYmmLLoBEada6LOQvaeV6+MuO/pCk +4zQEiIfr2LhpFZPVTxlzKl37phvZjj1+jFsaNtVnAZVOT15HQvcQWoSqXJ9B2wvw +254JAAoJ705NY7wNVzzQT8NiTlt2OCyIbS1m6LAhXydQyZXlpDZCtOniScQF8VA6 +zKgY7WwtXRVNB1gbpLzaIaWOjOp6iufOAjduuG9Ccsh1icNqVxEHmqGdmJQGayBc +qdWebRkT6rZK5Y0APU46hcdc8eRy05+eB8V13hr9nK9eY5PDoW4nJEM/ep6V68vO ++mam9/L3EPBCb1WmFhdQrS5WTr4UC7kc12BCh67UHeZC065/m/mz2gDbkd3ch6Hr +PmmKCWf64QHdG6LWhRFNEqSyLFHCiFTNu7mutPNJPBkBtBtCim8pAE1zGCDO6V/9 +yoc03VbVA/6y0qHDPcYiYrqUkoLKjqMdikFT4gH5xF8ClCJdPbLVBGeF4eBEyQkF +z5g6fR3gPt/fXsLfAsLwjbq6mSjvxL3qj2Cq4i/yB4JJNHbAdwPI7uhBpw8CAwEA +AQKCAgBi1jzPOYWFcrvALFWgZB/XEDIu0qM43d3jfK6FowP35MHtH0wydtTtn1Gy +2rIc9vlKey7Zbzq4GcLe8GkzhTKwXODPAy32Sxy9xMZSRMzm3XjJfNDRlCaD/2/b +F4GTrCePyh0AzxAMP2XXzV3JnKhx20ViUdDwcwcHE9dI79qcYLkfYEoIeh1KEmnB +bcUITFoCniiyjAXudtOIprSCnzdbONtelasAQqD6GQnGN4BqjrGBP4jT+zv83OR/ +hd1xgtYpdtL+M8nytdv+6wdXrB6/OADBNWe82DZPYmGDecAchEMvUQWEwKN9qH1V +JynPTDIEIBsbCFUIhGUWaiJoSXJ1MQ2jtaQ8dh7ap8l+3dd6epbc3HG5N5Dv6OZV +ZuxhFgYD5jGqNuSWXbUJvpJJAdTFv9QhmBvxMdGEj52JLazQ50TTufZAYdG1Bfbo +NotNO8RQBRyCwkM4ZU8KdM2tKpV1IJbVtwy4dgtAU6/sgJfsfWzc9aAZhJ82OQ82 +YSJgtDD8/8/E/iKammKU15xYCOmwgbS1hdDAJ1O5qAPLRaHlEpEJG4f/7lz0Fo3e +qzizQeFudtDcCbWfv9O0Pl1gWNpjTPoFLKBUZarFYcc6OVSKbOoBQXsElGZ+1BX5 +/UNaYpzwxHK+afx0aulXZa8vVLhYGdhFgspv7/C4QMlal7ovoQKCAQEA7DtlggDw +Z/2isCbNkXdZjUIgsIeCUrZXlxRHepqU/gQyk5S7CxGY+kdH+f8umEC/dEb7dxbq +jMlnKLPaq9T6/a8aUHbjzwSGLrghId0E7QNq+rVyO9FKj9SyoIJWQ7eBWHFMNVgJ +076fcizyyP1d0wbB5PdLtOsxilUjdugy91Tip3MTAJlQiSwcIiTN0UJRoXnQYyjS +k067Jj5CJw1EaDkth/F0MYDpJryedbNjry0V6SGJ1ioGzjbAHZP99g0p4Lp0P0I/ +3zaGDqYeg4xwC27JamUIwfWMwPWIVWmvJYw6MCCAVdx40UAae4jezQZhElG1QN3F +v/g98lcOJ8T/gwKCAQEA3FLVUv2hNm+v/G//j7JYNUNxhoDN1CQHzRfaaJMpvtlg +/imY6cAzUuvQp+80CYRhwYkjrTn+0IJroVLRrmFqB4NBNlq8sce8n+1xWngG4HF2 +fHIG6+o0rTUY2SgqEpctHV5t8ZCCanT0cEFRNr1YzOOjU+ByErCwx9g0pzPC2hjA +AjEgFnTnP/LE0ve9r9eQxXFBQ7KV74rlFAi0HFQQTWlA9z9x9OuiQ/FFi+VAQjm3 +bvWTOQ+sJ97CEQvz7ftx1+yImyCEjblh8zZKU1HE3v0pFc2g4jNtsMwhclftNlmq +o7DBqjQ+bmU9sjXPRZNd53dhHK7wIAmQMHKXC3b4hQKCAQEAzFIjXepKBOfPiuRE +8Qh1oEQN2iGaisfDwpx7poOFUp92z0bY8J3r3q1Ah+468t0tuviaD0r224Znf0Dw +Zab03+5PqEDskOWs2UPRBGUSGZ3XLbk6cJp5DyY8ya9xxWg2q7Ry4cCf31EEv9A4 +vbbHK/qwQcXS6UxzsN4qqnHzgxEcaRCS4vW4Pqy0OKP0kIX37N5bayin2VlDbbRz +qytCe5LY2rfwc9C32BVUSSE7mww340hq253F/R5F1E9oXSTNj941JXG7pOiX0pvD +0KvrTTgpZai1hm1HrK2xmY6rOqKqwW2bEqh6pyH2xdqAOnzGAP5C8zPeEkg37B93 +0tYE2QKCAQEAn+9pd+MxoeiVofRTWiamrZOV14Os5rB5EUKdg4hAp4/5PsdHf3fM +Sgdw4ldcOQRmSi2ZPmh5NzdVljgeii2g4G9BaYmYrJ1HqfidboTuyQLUdiX4LE1J +i+qdbRYg5HnlgApKWS8D3O+lec+QeuIcki81IvAtHrAwxAGIx09lxRhuWaj7mBGo +xN0gT90TT5B2QT2jmNcMzGTRQifHR0EmzGr5hAIEYTykABom2BTE/s4TAHM4OhXM +bOzhh6pbmogK7imASMvkLVVDa2AfuDgFZ7Hynyc0AMBAgjTMmsqFIg0ZN9ZZ34op +P84yaSlymxkM87fPQRkkqr0GdITabLIiWQKCAQAENsMr1kFIiS6dOo2xMNd26pfS +dA/icV/stOqdqxjbULPQF5DIu+HHPbEkIZRcJXIxjRV6uuNLg2PG2UNZeqa2C6RE +2vgwHXy0ost+YXaVtJv6jK4c56m6CBwiBIPO9mQQkZSKClX16KqTSqXLZj+h4RTH +zM/Lwa/Arac2UT2CBI+h/wBEPJVflXbfMDfyUdRpA6XwXMuitjXiPSlhL7v0JrNA +KksjJnKzyAEYHbxLEJbIiE/sQfjpsVPZt5h/OK1modHKIxdx4yQERQQN7Ql4AEn1 ++jReaD4P161rTRCrChpTf12zGoV7AqSoKpOlpMSCiPhCyYkE40yokihao+zw +-----END RSA PRIVATE KEY----- diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..c10fa31 Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/Classes/Calendly.php b/app/Classes/Calendly.php new file mode 100644 index 0000000..4a7a3da --- /dev/null +++ b/app/Classes/Calendly.php @@ -0,0 +1,357 @@ +clientId); + $url .= '&response_type=code'; + $url .= '&redirect_uri=' . urlencode(route('redirectURI')); + //$url .= '&redirect_uri=' . urlencode('https://app.example.com/calendly/redirect-code/'); + + return $url; + } + public function authorize($code) + { + $tokenUrl = 'https://auth.calendly.com/oauth/token'; + + $client = new Client(); + + try { + $response = $client->post($tokenUrl, [ + 'form_params' => [ + 'grant_type' => 'authorization_code', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'redirect_uri' => route("redirectURI"), + //'redirect_uri' => 'https://app.example.com/calendly/redirect-code/', + 'code' => $code, + ], + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + ]); + + $data = json_decode($response->getBody()->getContents(), true); + Log::info('Info from function authorize(): ', $data); + $setting = Setting::find(1); + $setting->calendly_access_token = $data['access_token']; + $setting->calendly_refresh_token = $data['refresh_token']; + $setting->save(); + Cache::forget('calendly_access_token'); + Cache::put('calendly_access_token', $data['access_token'], now()->addSeconds($data['expires_in'])); + + return [ + 'access_token' => $data['access_token'], + 'refresh_token' => $data['refresh_token'], + 'token_type' => $data['token_type'], + 'expires_in' => $data['expires_in'] + ]; + } catch (\Exception $e) { + throw $e; + } + } + public function accessToken() + { + + if (Cache::has('calendly_access_token')) { + return Cache::get('calendly_access_token'); + } + + $setting = Setting::find(1); + + $tokenUrl = 'https://auth.calendly.com/oauth/token'; + + $client = new Client(); + + $response = $client->post($tokenUrl, [ + 'form_params' => [ + 'grant_type' => 'refresh_token', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'refresh_token' => $setting->calendly_refresh_token, + ], + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + ]); + + // Decode the response + $data = json_decode($response->getBody()->getContents(), true); + Log::info('Info from function accessToken(): ', $data); + // Store the new access token and refresh token in cache + Cache::put('calendly_access_token', $data['access_token'], now()->addSeconds($data['expires_in'])); + + return $data['access_token']; + } + public function getUserUri() + { + // 1. Call the /users/me API to get user information + $client = new Client(); + try { + $response = $client->request('GET', 'https://api.calendly.com/users/me', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->accessToken(), + 'Content-Type' => 'application/json', + ] + ]); + + $data = json_decode($response->getBody(), true); + return $userUri = $data['resource']['uri']; + } catch (\Exception $e) { + return false; + } + } + public function eventTypes() + { + // 1. Call the /users/me API to get user information + $client = new Client(); + $responseEvent = $client->request('GET', 'https://api.calendly.com/event_types?user=' . $this->getUserUri(), [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->accessToken(), + 'Content-Type' => 'application/json', + ] + ]); + $dataEvent = json_decode($responseEvent->getBody(), true); + return $dataEvent['collection']; + } + public function setEventUri($even_type_url) + { + $setting = Setting::find(1); + $setting->event_type = $even_type_url; + $setting->save(); + } + public function resetEventUri() + { + $setting = Setting::find(1); + $setting->event_type = null; + $setting->calendly_access_token = null; + $setting->calendly_refresh_token = null; + + $setting->save(); + } + function getAvailableDates($even_type_url, $month, $tz = "UTC") + { + + try { + $availableTimes = []; + + $date = Carbon::createFromDate(Carbon::now()->year, $month, 1, $tz); + $date = $date->startOfMonth()->tz("UTC"); + $endMonthDate = Carbon::createFromDate(Carbon::now()->year, $month, 1, $tz); + $endMonthDate = $endMonthDate->endOfMonth()->tz("UTC"); + + + while ($date < $endMonthDate) { + $start_time = $date->startOfDay()->format('Y-m-d\T24:00:00.000000\Z'); + $end_time = $date->addDays(7)->endOfDay()->format('Y-m-d\T24:00:00.000000\Z'); + + + $client = new Client(); + try { + + // Prepare API endpoint with the required parameters + $eventTypeUrl = 'https://api.calendly.com/event_type_available_times'; + $queryParams = [ + 'event_type' => $even_type_url, + 'start_time' => $start_time, + 'end_time' => $end_time + ]; + $str = "event_type=" . urlencode($queryParams['event_type']) . "&" . "start_time=" . urlencode($queryParams['start_time']) . "&" . "end_time=" . urlencode($queryParams['end_time']); + $eventTypeUrl = $eventTypeUrl . "?" . ($str); + // Send the request to Calendly + $response = $client->request('GET', $eventTypeUrl, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->accessToken(), + 'Content-Type' => 'application/json', + ], + // 'query' => $queryParams + ]); + + + $data = json_decode($response->getBody(), true); + //$availableTimes += $data['collection']; + foreach ($data['collection'] as $slot) { + $slotDate = Carbon::parse($slot['start_time'])->tz($tz)->format('Y-m-d'); + if (!isset($availableTimes[$slotDate])) { + $availableTimes[$slotDate] = []; + } + $slotDateTime = Carbon::parse($slot['start_time'])->tz($tz); + $dateKey = $slotDateTime->format('Y-m-d'); + $slot['formatted_datetime'] = $slotDateTime->format('Y-m-d g:i:00'); + $availableTimes[$slotDate][] = $slot; + } + } catch (Exception $e) { + Log::error('Error from function getAvailableDates(): ' . $e->getMessage(), [ + 'eventTypeUrl' => $eventTypeUrl, + 'queryParams' => $queryParams, + 'full_rul' => $eventTypeUrl, + + 'Authorization' => 'Bearer ' . $this->accessToken(), + ]); + continue; + } + $date->addSecond(); + } + + return $availableTimes; + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to fetch available times: ' . $e->getMessage()], 500); + } + } + + + + // write function to do php get and post request and add json support + function makeRequest($url, $method = 'GET', $data = [], $headers = [], $json = false, $proxy = "iproyal15202:pb86ljih495_country-us@geo.iproyal.com:12321") + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_ENCODING, '',); + if ($proxy) { + curl_setopt($ch, CURLOPT_PROXY, 'http://geo.iproyal.com:12321'); // Set the proxy address and port + curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'iproyal15202:Pb86ljiH495_country-us'); // Set the proxy authentication credentials + } + $headers = ['User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36', ...$headers]; + if ($method === 'POST') { + + if ($json) { + $headers = ['Content-Type: application/json', ...$headers]; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } else { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + } + } + if (!empty($headers)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + $response = curl_exec($ch); + curl_close($ch); + + return $response; + } + + + function getCrfToken($url) + { + $response = $this->makeRequest($url); + + // var_dump($response); + // + preg_match('//', $response, $matches); + return $matches[1]; + } + + function getEventDetails($url) + { + $response = $this->makeRequest($url); + return $response; + } + + //random string function + function generateRandomString($length = 10) + { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, strlen($characters) - 1)]; + } + return $randomString; + } + + + public function bookEvent($url, $name, $email, $timezone = "UTC") + { + $url_parts = explode("/", $url); + $bookingDate = $url_parts[5]; + $event_type = $url_parts[4]; + $owner_id = $url_parts[3]; + $eventDetailsUrl = "https://calendly.com/api/booking/profiles/$owner_id/event_types/$event_type"; + $eventDetails = json_decode($this->makeRequest($eventDetailsUrl)); + // var_dump($eventDetails); + $crfToken = $this->getCrfToken($url); + + $eventUuid = $eventDetails->uuid; + $link_uuid = $eventDetails->scheduling_link->uid; + $custom_fields_id = $eventDetails->custom_fields[0]->id; + $booking_request_id = urlencode($this->generateRandomString(36) . "|$bookingDate|$eventUuid|$name"); + //convert to php array + $bookingData = [ + "analytics" => [ + //php date with 10 sec gap + "invitee_landed_at" => date("Y-m-d\TH:i:s.uP", strtotime($bookingDate) - 200), + "browser" => "Chrome 129", + "device" => "undefined Mac OS X 10.15.7", + "fields_filled" => 1, + "fields_presented" => 1, + "booking_flow" => "v3", + "seconds_to_convert" => 200 + ], + "embed" => [], + "event" => [ + "start_time" => $bookingDate, + "location_configuration" => [ + "location" => "", + "phone_number" => "", + "additional_info" => "" + ], + "guests" => [] + ], + "event_fields" => [ + [ + "id" => $custom_fields_id, + "name" => "Please share anything that will help prepare for our meeting.", + "format" => "text", + "required" => false, + "position" => 0, + "answer_choices" => null, + "include_other" => false, + "value" => "" + ] + ], + "invitee" => [ + "timezone" => $timezone, + "time_notation" => "24h", + "full_name" => $name, + "email" => $email + ], + "payment_token" => [], + "tracking" => [ + "fingerprint" => $this->generateRandomString(32) + ], + "scheduling_link_uuid" => $link_uuid, + "locale" => "en", + "verification_code" => null, + "remember_device" => false + ]; + + + $response = $this->makeRequest("https://calendly.com/api/booking/invitees", 'POST', $bookingData, [ + 'x-csrf-token: ' . $crfToken, + 'x-page-rendered-at: ' . date('Y-m-d\TH:i:s'), + 'x-requested-with: XMLHttpRequest', + 'referer: ' . $url, + "booking-request-id: $booking_request_id" + ], true); + + return $response; + } +} diff --git a/app/Classes/Constant.php b/app/Classes/Constant.php new file mode 100644 index 0000000..089bbce --- /dev/null +++ b/app/Classes/Constant.php @@ -0,0 +1,49 @@ +toSql(); + foreach ($query->getBindings() as $iter => $binding) { + + $type = gettype($binding); + switch ($type) { + case "integer": + case "double": + $bindingStr = "$binding"; + break; + case "string": + $bindingStr = "'$binding'"; + break; + case "object": + $class = get_class($binding); + switch ($class) { + case "DateTime": + $bindingStr = "'" . $binding->format('Y-m-d H:i:s') . "'"; + break; + default: + throw new \Exception("Unexpected binding argument class ($class)"); + } + break; + default: + throw new \Exception("Unexpected binding argument type ($type)"); + } + + $currentPos = strpos($sqlStr, '?'); + if ($currentPos === false) { + throw new \Exception("Cannot find binding location in Sql String for bundung parameter $binding ($iter)"); + } + + $sqlStr = substr($sqlStr, 0, $currentPos) . $bindingStr . substr($sqlStr, $currentPos + 1); + } + + $search = ["select", "distinct", "from", "where", "and", "order by", "asc", "desc", "inner join", "join"]; + $replace = ["SELECT", "DISTINCT", "\n FROM", "\n WHERE", "\n AND", "\n ORDER BY", "ASC", "DESC", "\n INNER JOIN", "\n JOIN"]; + $sqlStr = str_replace($search, $replace, $sqlStr); + + return $sqlStr; + } +} diff --git a/app/Classes/JassJWT.php b/app/Classes/JassJWT.php new file mode 100644 index 0000000..f1086fd --- /dev/null +++ b/app/Classes/JassJWT.php @@ -0,0 +1,84 @@ + 'chat', + 'aud' => 'jitsi', + 'exp' => time() + $EXP_DELAY_SEC, + 'nbf' => time() - $NBF_DELAY_SEC, + 'room' => "*", + 'sub' => $APP_ID, + 'context' => [ + 'user' => [ + 'moderator' => $USER_IS_MODERATOR ? "true" : "false", + 'email' => $USER_EMAIL, + 'name' => $USER_NAME, + 'avatar' => $USER_AVATAR_URL, + 'id' => $USER_ID + ], + 'features' => [ + 'recording' => $RECORDING_IS_ENABLED ? "true" : "false", + 'livestreaming' => $LIVESTREAMING_IS_ENABLED ? "true" : "false", + 'transcription' => $TRANSCRIPTION_IS_ENABLED ? "true" : "false", + 'outbound-call' => $OUTBOUND_IS_ENABLED ? "true" : "false" + ] + ] + ]); + + + $jws = $jwsBuilder + ->create() + ->withPayload($payload) + ->addSignature($jwk, [ + 'alg' => 'RS256', + 'kid' => $API_KEY, + 'typ' => 'JWT' + ]) + ->build(); + + + $serializer = new CompactSerializer(); + $token = $serializer->serialize($jws, 0); + return $token; + } +} diff --git a/app/Console/.DS_Store b/app/Console/.DS_Store new file mode 100644 index 0000000..240b2d9 Binary files /dev/null and b/app/Console/.DS_Store differ diff --git a/app/Console/Commands/InsertDataForApp.php b/app/Console/Commands/InsertDataForApp.php new file mode 100644 index 0000000..67b35e7 --- /dev/null +++ b/app/Console/Commands/InsertDataForApp.php @@ -0,0 +1,216 @@ + 'Telemedpro ' . ($i + 1), + 'email' => 'telemedpro' . ($i + 1) . '@example.com', + 'phone' => '123-456-789' . $i, + ]); + } */ + + $faker = Faker::create(); + + for ($i = 500; $i < 800; $i++) { + $patient = Patient::create([ + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'email' => $faker->unique()->safeEmail, + 'phone' => $faker->phoneNumber, + 'password' => bcrypt("12345"), + 'address' => $faker->address, + 'city' => $faker->city, + 'state' => $faker->state, + 'zip_code' => $faker->postcode, + 'lat' => $faker->latitude, + 'long' => $faker->longitude, + 'dob' => $faker->date($format = 'Y-m-d', $max = 'now'), + 'recording_switch' => $faker->boolean, + 'country' => $faker->country, + 'phone_no' => $faker->phoneNumber, + 'shipping_address' => $faker->address, + 'shipping_city' => $faker->city, + 'shipping_state' => $faker->state, + 'shipping_country' => $faker->country, + 'shipping_zipcode' => $faker->postcode, + 'timezone' => 'UTC', + 'gender' => $faker->randomElement(['male', 'female']), + 'marital_status' => $faker->randomElement(['single', 'married']), + 'height' => $faker->numberBetween(150, 200), // Height in cm + 'weight' => $faker->numberBetween(50, 100) // Weight in kg + ]); + + PatientRegActivity::create([ + 'patient_id' => $patient->id, + 'activity' => 'patient_registered' + ]); + $plans = PlanV1::all(); + PatientPlan::create([ + 'patient_id' => $patient->id, + 'plan_id' => $plans->random()->id, + ]); + /* } */ + + $telemedpros = Telemedpro::all(); + #$patients = Patient::all(); + + + /* for ($i = 0; $i < 50; $i++) { */ + $telemedpro = $telemedpros->random(); + #$patient = $patients->random(); + #$appointmentTime = Carbon::now()->addDays(rand(0, 30))->addHours(rand(0, 23))->addMinutes(rand(0, 59)); + $appointmentTime = Carbon::now()->subMonth()->addDays(rand(0, 30))->addHours(rand(0, 23))->addMinutes(rand(0, 59)); + $duration = rand(15, 120); // Duration between 15 minutes to 2 hours + $startTime = $appointmentTime; + $endTime = $startTime->copy()->addMinutes($duration); + + $appointment = Appointment::create([ + 'telemed_pros_id' => $telemedpro->id, + 'patient_id' => $patient->id, + 'appointment_time' => $appointmentTime->toDateTimeString(), + 'in_call' => rand(0, 1), + 'meeting_id' => Str::uuid(), + 'agent_call_token' => Str::random(20), + 'patient_call_token' => Str::random(20), + 'video_token' => Str::random(20), + 'appointment_date' => $appointmentTime->toDateString(), + 'patient_email' => $patient->email, + 'patient_name' => $patient->name, + 'timezone' => 'UTC', + 'analytics' => json_encode(['metric1' => rand(0, 100), 'metric2' => rand(0, 100)]), + 'start_time' => $startTime->toDateTimeString(), + 'end_time' => $endTime->toDateTimeString(), + 'duration' => $duration + ]); + $statusOptions = ['delivered', 'pending']; + + $labkit = LabKit::inRandomOrder()->first(); + $cart = new Cart(); + $cart->lab_kit_id = $labkit->id; + $cart->first_name = $patient->first_name; + $cart->last_name = $patient->last_name; + $cart->email = $patient->email; + $cart->phone = $patient->phone; + $cart->status = $statusOptions[array_rand($statusOptions)]; + $cart->date_of_birth = $patient->dob; + $cart->patient_id = $patient->id; + $cart->shipping_address1 = $faker->streetAddress; + $cart->shipping_address2 = $faker->secondaryAddress; + $cart->shipping_city = $faker->city; + $cart->shipping_state = $faker->state; + $cart->shipping_zipcode = $faker->postcode; + $cart->shipping_country = $faker->country; + $cart->billing_address1 = $faker->streetAddress; + $cart->billing_address2 = $faker->secondaryAddress; + $cart->billing_city = $faker->city; + $cart->billing_state = $faker->state; + $cart->billing_zipcode = $faker->postcode; + $cart->billing_country = $faker->country; + $cart->shipping_amount = $faker->randomFloat(2, 5, 20); // Random shipping amount between $5 and $20 + $cart->total_amount = $faker->randomFloat(2, 50, 200); // Random total amount between $50 and $200 + $cart->save(); + + $prescription = Prescription::inRandomOrder()->first(); + + PatientPrescription::create([ + 'patient_id' => $patient->id, + 'appointment_id' => $appointment->id, + 'prescription_id' => $prescription->id, + 'direction_one' => $faker->sentence, + 'direction_two' => $faker->sentence, + 'dont_substitute' => rand(0, 1), + 'comments' => $faker->sentence, + 'status' => $statusOptions[array_rand($statusOptions)], + ]); + + $addNotePatient = PatientNote::create([ + 'note' => "Patient didn't send back test kit yet", + 'note_type' => "Notes", + 'patient_id' => $patient->id, + 'appointment_id' => $appointment->id, + 'telemed_pros_id' => $telemedpro->id + ]); + $category = ProfileCategory::where("category_link", 'weight_loss')->first(); + + + $jsonString = '{ + "weight_lb": "42", + "height_feet": "5", + "height_inches": "2", + "expecting": "not_applicable", + "evaluate_weight_loss": "yes", + "weight_management": "no", + "caloric_intake": "yes", + "physical_activity": "no", + "weightloss_goal": "yes", + "medical_evaluation": "less_then_a_year_ago", + "lab_tests_completed": "no", + "comorbidities": [ + "high_cholesterol", + "fatty_liver_disease" + ], + "chronic_pancreatitis": [ + "none_of_the_above" + ], + "smoke_alcohol": null, + "family_history_thyroid_cancer": [ + "none_of_above_them" + ], + "kindney_history": [ + "appointment_or_consultation_with", + "history_of_solitary_kidney_or_kidney_transplant" + ] + }'; + $questionBuilderData = []; + // Convert JSON string to PHP array + $data = json_decode($jsonString, true); + foreach ($data as $key => $value) { + if (is_array($value)) { + $value = serialize($value); + } + if (!empty($value)) { + $questionBuilderData[] = [ + 'key' => $key, + 'value' => $value, + 'profile_category_id' => $category->id, + 'customer_id' => $patient->id + ]; + } + } + $questionBuilder = QuestionBuilder::insert($questionBuilderData); + } + } +} diff --git a/app/Console/Commands/InsertDataForPatient.php b/app/Console/Commands/InsertDataForPatient.php new file mode 100644 index 0000000..bc4a92d --- /dev/null +++ b/app/Console/Commands/InsertDataForPatient.php @@ -0,0 +1,87 @@ + 'Telemedpro ' . ($i + 1), + 'email' => 'telemedpro' . ($i + 1) . '@example.com', + 'phone' => '123-456-789' . $i, + ]); + } */ + + $faker = Faker::create(); + + for ($i = 10000; $i < 20000; $i++) { + $patient = Patient::create([ + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'email' => $faker->unique()->safeEmail, + 'phone' => $faker->phoneNumber, + 'password' => bcrypt("12345"), + 'address' => $faker->address, + 'city' => $faker->city, + 'state' => $faker->state, + 'zip_code' => $faker->postcode, + 'lat' => $faker->latitude, + 'long' => $faker->longitude, + 'dob' => $faker->date($format = 'Y-m-d', $max = 'now'), + 'recording_switch' => $faker->boolean, + 'country' => $faker->country, + 'phone_no' => $faker->phoneNumber, + 'shipping_address' => $faker->address, + 'shipping_city' => $faker->city, + 'shipping_state' => $faker->state, + 'shipping_country' => $faker->country, + 'shipping_zipcode' => $faker->postcode, + 'timezone' => 'UTC', + 'gender' => $faker->randomElement(['male', 'female']), + 'marital_status' => $faker->randomElement(['single', 'married']), + 'height' => $faker->numberBetween(150, 200), // Height in cm + 'weight' => $faker->numberBetween(50, 100) // Weight in kg + ]); + + PatientRegActivity::create([ + 'patient_id' => $patient->id, + 'activity' => 'patient_registered' + ]); + $plans = PlanV1::all(); + PatientPlan::create([ + 'patient_id' => $patient->id, + 'plan_id' => $plans->random()->id, + ]); + } + } +} diff --git a/app/Console/Commands/InsertDataForPatientBulk.php b/app/Console/Commands/InsertDataForPatientBulk.php new file mode 100644 index 0000000..c7d4bdb --- /dev/null +++ b/app/Console/Commands/InsertDataForPatientBulk.php @@ -0,0 +1,100 @@ +unique()->safeEmail; + $emailParts = explode('@', $email); + $emailParts[0] .= rand(1000, 9999); // Append a random 4-digit number + $uniqueEmail = implode('@', $emailParts); + + $patientData[] = [ + 'id' => $patientId, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'email' => $uniqueEmail, + //'phone' => $faker->phoneNumber, + 'password' => bcrypt("12345"), + 'address' => $faker->address, + 'city' => $faker->city, + 'state' => $faker->state, + 'zip_code' => $faker->postcode, + 'lat' => $faker->latitude, + 'long' => $faker->longitude, + 'dob' => $faker->date($format = 'Y-m-d', $max = 'now'), + 'recording_switch' => $faker->boolean, + 'country' => $faker->country, + 'phone_no' => $faker->phoneNumber, + 'shipping_address' => $faker->address, + 'shipping_city' => $faker->city, + 'shipping_state' => $faker->state, + 'shipping_country' => $faker->country, + 'shipping_zipcode' => $faker->postcode, + 'timezone' => 'UTC', + 'gender' => $faker->randomElement(['male', 'female']), + 'marital_status' => $faker->randomElement(['single', 'married']), + 'height' => $faker->numberBetween(150, 200), + 'weight' => $faker->numberBetween(50, 100), + 'created_at' => now(), + 'updated_at' => now(), + ]; + + $patientRegActivityData[] = [ + 'patient_id' => $patientId, + 'activity' => 'patient_registered', + 'created_at' => now(), + 'updated_at' => now(), + ]; + + $patientPlanData[] = [ + 'patient_id' => $patientId, + 'plan_id' => $plans->random()->id, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + // Bulk insert data + DB::table('patients')->insert($patientData); + DB::table('patient_reg_activity')->insert($patientRegActivityData); + DB::table('patient_plan')->insert($patientPlanData); + + $this->info("Inserted batch of $batchSize records. Total progress: " . ($j + $batchSize) . " / $totalRecords"); + } + + $this->info('Data insertion completed successfully.'); + } +} diff --git a/app/Console/Commands/InsertPlansData.php b/app/Console/Commands/InsertPlansData.php new file mode 100644 index 0000000..f99f0fe --- /dev/null +++ b/app/Console/Commands/InsertPlansData.php @@ -0,0 +1,746 @@ +argument('type'); + + /* $plansData = [ + 'hgh' => [ + [ + 'title' => 'Basic Plan', + 'currency' => '$', + 'price' => '195.00', + 'list_one_title' => 'TRT prescription', + 'list_two_title' => 'A nazial mist or a pre-filled syringe for self-injection', + 'list_sub_title' => 'Either be cream or oral liquied', + 'image_url' => '', + ], + [ + 'title' => 'Pro Plan', + 'currency' => '$', + 'price' => '195.00', + 'list_one_title' => 'TRT prescription', + 'list_two_title' => 'A nazial mist or a pre-filled syringe for self-injection', + 'list_sub_title' => 'Either be cream or oral liquied', + 'image_url' => '', + ], + [ + 'title' => 'Elite Plan', + 'currency' => '$', + 'price' => '995.00', + 'list_one_title' => 'HGH & TRT prescription', + 'list_two_title' => 'Growth hormone zinc and magnesium', + 'list_sub_title' => 'Everything in Pro Plan', + 'image_url' => '', + ], + ], + 'peptide' => [ + [ + 'title' => 'Semaglutide', + 'currency' => '$', + 'price' => '399.00', + 'list_one_title' => 'RX Prescription', + 'list_two_title' => 'Semaglutide, generic Ozempic and generic wegovy, is a potent medical weight loss treatment, with users typically experiencing an average loss of 15% of their body weight.', + 'list_sub_title' => 'Weight Loss', + 'image_url' => 'semaglutide.png', + ], + [ + 'title' => 'Tirzepatide', + 'currency' => '$', + 'price' => '499.00', + 'list_one_title' => 'RX Prescription', + 'list_two_title' => 'Tirzepatide, generic Zepbound and generic Mounjaro, is a powerful medical weight loss treatment, often enabling individuals to lose 20% or more of their body weight, showcasing its enhanced efficacy compared to Semaglutide.', + 'list_sub_title' => 'Weight Loss', + 'image_url' => 'Tirzepatide.webp', + ], + [ + 'title' => 'Sermorelin Injections', + 'currency' => '$', + 'price' => '249.00', + 'list_one_title' => 'RX Prescription', + 'list_two_title' => 'Sermorelin injections stimulate the body\'s natural production of growth hormone, promoting muscle growth, fat loss, and overall rejuvenation.', + 'list_sub_title' => 'Lean Muscle Mass & Fat Loss', + 'image_url' => 'Sermorelin.webp', + ], + [ + 'title' => 'PT-141 Injections', + 'currency' => '$', + 'price' => '345.00', + 'list_one_title' => 'RX Prescription', + 'list_two_title' => 'Sexual health and Performance. Supplies are shipped with treatment', + 'list_sub_title' => 'Sexual Performance', + 'image_url' => 'PT-141.webp', + ], + [ + 'title' => 'PT-141 Troche', + 'currency' => '$', + 'price' => '418.00', + 'list_one_title' => 'RX Prescription', + 'list_two_title' => 'Sexual health and Performance. Supplies are shipped with treatment', + 'list_sub_title' => 'Sexual Performance', + 'image_url' => 'pt-141_troche-2.webp', + ], + [ + 'title' => 'VIP Nasal Spray', + 'currency' => '$', + 'price' => '210.00', + 'list_one_title' => 'RX Prescription', + 'list_two_title' => 'VIP nasal spray offers targeted delivery of therapeutic peptides to support digestive health, immune function, and circadian regulation.', + 'list_sub_title' => 'Systemic Wellness', + 'image_url' => 'vip_nasal-spray.webp', + ], + [ + 'title' => 'SS-31 Injections', + 'currency' => '$', + 'price' => '1230.00', + 'list_one_title' => 'RX Prescription', + 'list_two_title' => 'VIP nasal spray offers targeted delivery of therapeutic peptides to support digestive health, immune function, and circadian regulation.', + 'list_sub_title' => 'Longevity & Performance', + 'image_url' => 'ss-31_syringes.webp', + ], + ], + 'peptide_new' => [ + [ + 'title' => 'AOD9604 [ 5mg]', + 'currency' => '$', + 'price' => '70.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'AOD9604 is a peptide fragment of human growth hormone (HGH) used for its potential benefits in fat metabolism, weight loss, and joint health.', + 'list_sub_title' => 'Weight Loss', + 'image_url' => 'AOD9604-05mg.webp', + 'slug' => 'AOD9604-5mg', + ], + [ + 'title' => 'BPC – 157', + 'currency' => '$', + 'price' => '199.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => "BPC-157 is a penta-decapeptide composed of 15 amino acids. It is a partial sequence of the body protection compound (BPC) that was discovered in and isolated from human gastric juice. Animal studies have shown it to accelerate the healing of many different wounds, including muscle, tendon, and damaged ligaments. Additionally, BPC 157 has been shown to protect organs and aid in the prevention of gastric ulcers.", + 'list_sub_title' => 'Body Protection Compound', + 'image_url' => 'BPC-157-10mg.webp', + 'slug' => 'bpc–157', + ], + [ + 'title' => 'CJC-1295, Ipamorelin Blend [ 10mg]', + 'currency' => '$', + 'price' => '95.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'CJC-1295 causes the natural release of GH by naturally stimulating the growth hormone-releasing hormone receptor', + 'list_sub_title' => 'Growth hormone', + 'image_url' => 'CJC-1295-Ipamorelin-Blend-10mg.webp', + 'slug' => 'CJC-1295-10mg', + ], + [ + 'title' => 'PT-141 Injection', + 'currency' => '$', + 'price' => '149.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'PT-141, also known as Bremelanotide, is a melanocortin receptor agonist that boosts sexual desire and arousal by stimulating specific receptors in the brain. This peptide promotes increased libido and sexual function for both men and women, offering a unique solution for enhancing intimate experiences. Enjoy the convenience of our peptide subscription, delivering in-home treatments directly to your door', + 'list_sub_title' => 'Sexual Performance', + 'image_url' => 'PT-141-10-mg.webp', + 'slug' => 'pt-141-Injection-10mg', + ], + [ + 'title' => 'Semaglutide [3mg]', + 'currency' => '$', + 'price' => '199.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'Our Semaglutide weight loss program, led by board-certified physicians, can help you on a transformative journey toward a healthier lifestyle. The program includes monthly personalized Semaglutide prescriptions, regular virtual check-ins, and unlimited messaging with providers. Join the thousands of people who have already achieved their weight loss goals through this program.', + 'list_sub_title' => 'Weight Loss', + 'image_url' => 'Semaglutide-05mg.webp', + 'slug' => 'semaglutide-3mg', + ], + [ + 'title' => 'Semaglutide [5mg]', + 'currency' => '$', + 'price' => '220.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'Our Semaglutide weight loss program, led by board-certified physicians, can help you on a transformative journey toward a healthier lifestyle. The program includes monthly personalized Semaglutide prescriptions, regular virtual check-ins, and unlimited messaging with providers. Join the thousands of people who have already achieved their weight loss goals through this program.', + 'list_sub_title' => 'Weight Loss', + 'image_url' => 'Semaglutide-05mg.webp', + 'slug' => 'semaglutide-5mg', + ], + [ + 'title' => 'TB-500 (Thymosin Beta-4) [ 10mg ]', + 'currency' => '$', + 'price' => '170.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'TB-500, also known as Thymosin Beta-4, is a peptide consisting of 43 amino acids. In animal studies, Thymosin Beta-4 has demonstrated the ability to promote blood vessel growth, regulate wound healing, decrease inflammation, and reduce oxidative damage in the heart and central nervous system. This peptide plays a significant role in protecting tissues, and facilitating tissue repair, regeneration, and remodeling of injured or damaged tissues. Additionally, it is a subject of active research in the field of anti-aging.', + 'list_sub_title' => 'Injury Recovery', + 'image_url' => 'TB-500-Thymosin-Beta-4-10-mg.webp', + 'slug' => 'tb-500-10mg', + ], + [ + 'title' => 'Tesamorelin [5mg]', + 'currency' => '$', + 'price' => '75.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'Tesamorelin is a synthetic peptide consisting of 44 amino acids that mimic the growth hormone-releasing hormone (GHRH). It is specifically designed to stimulate the production and release of endogenous growth hormone (GH) from the pituitary gland. Originally developed for the treatment of lipodystrophy in HIV-positive individuals, Tesamorelin has shown potential benefits in reducing visceral adipose tissue and improving body composition. This peptide is utilized in various research and clinical settings for its ability to enhance GH levels, which may support lean muscle mass development, fat loss, and overall metabolic health., immune function, and circadian regulation.', + 'list_sub_title' => 'Weight loss', + 'image_url' => 'Tesamorelin-05mg.webp', + 'slug' => 'tesamorelin-5mg', + ], + [ + 'title' => 'Tesamorelin [10mg]', + 'currency' => '$', + 'price' => '135.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'Tesamorelin is a synthetic peptide consisting of 44 amino acids that mimic the growth hormone-releasing hormone (GHRH). It is specifically designed to stimulate the production and release of endogenous growth hormone (GH) from the pituitary gland. Originally developed for the treatment of lipodystrophy in HIV-positive individuals, Tesamorelin has shown potential benefits in reducing visceral adipose tissue and improving body composition. This peptide is utilized in various research and clinical settings for its ability to enhance GH levels, which may support lean muscle mass development, fat loss, and overall metabolic health., immune function, and circadian regulation.', + 'list_sub_title' => 'Weight loss', + 'image_url' => 'Tesamorelin-05mg.webp', + 'slug' => 'tesamorelin-10mg', + ], + [ + 'title' => 'Tirzepatide [ 5mg ]', + 'currency' => '$', + 'price' => '249.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'Tirzepatide, the same active ingredient in Mounjaro®, represents the latest and most potent GLP-1 class medication for sustained weight loss. Patients have experienced an impressive average weight reduction of 24.3% over 72 weeks, in conjunction with a calorie-restricted diet and heightened physical activity. This dual-action GLP-1/GIP is for individuals with obesity (BMI ≥30) or overweight (BMI ≥27) accompanied by a weight-related condition, alongside dietary and exercise modifications.', + 'list_sub_title' => 'Weight loss', + 'image_url' => 'Tirzepatide-0mg.webp', + 'slug' => 'tirzepatide-5mg', + ], + [ + 'title' => 'GHK-CU', + 'currency' => '$', + 'price' => '0.00', + 'list_one_title' => 'Peptides', + 'list_two_title' => 'GHK-Cu is a natural peptide found in human blood plasma, urine, and saliva. Research in animals indicates that GHK-Cu can enhance wound healing, boost immune function, and improve skin health by stimulating collagen production, activating fibroblasts, and promoting blood vessel growth. Evidence suggests that it acts as a feedback signal generated after tissue injury. Additionally, GHK-Cu suppresses free-radical damage, making it a potent antioxidant.', + 'list_sub_title' => 'Skin Regeneration', + 'image_url' => 'GHK-Cu-10mg.webp', + 'slug' => 'GHK-CU', + ], + ], + ]; + + if (isset($plansData[$type])) { + foreach ($plansData[$type] as $plan) { + PlanV1::create($plan); + } + $this->info('Plans data inserted successfully.'); + } else { + $this->error('Invalid type provided.'); + } */ + + + + $data = [ + [ + "Category" => "Weight Loss", + "Sub-Category" => "GLP-1 Weight Loss", + "Drug" => "Semaglutide Injection", + "Supply" => "1 Month or +", + "Retail Cost" => 299, + "Plus Shipping Cost" => 22.5, + "Description" => "GLP-1 Weight Loss Injection", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Weight Loss", + "Sub-Category" => "GLP-1 Weight Loss", + "Drug" => "Semaglutide Oral Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 299, + "Plus Shipping Cost" => 22.5, + "Description" => "GLP-1 Weight Loss Injection", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Weight Loss", + "Sub-Category" => "GLP-1 Weight Loss", + "Drug" => "Semaglutide Oral Suspension", + "Supply" => "1 Month or +", + "Retail Cost" => 299, + "Plus Shipping Cost" => 22.5, + "Description" => "GLP-1 Weight Loss Injection", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Weight Loss", + "Sub-Category" => "GLP-1 Weight Loss", + "Drug" => "Semaglutide Oral Troche", + "Supply" => "1 Month or +", + "Retail Cost" => 299, + "Plus Shipping Cost" => 22.5, + "Description" => "GLP-1 Weight Loss Injection", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Weight Loss", + "Sub-Category" => "GLP-1 Weight Loss", + "Drug" => "Tirzepatide Injection", + "Supply" => "1 Month or +", + "Retail Cost" => 599, + "Plus Shipping Cost" => 22.5, + "Description" => "GLP-1 Weight Loss Injection", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Health & Wellness", + "Sub-Category" => "Hair Loss/Regrowth", + "Drug" => "Hair Loss Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 99, + "Plus Shipping Cost" => 12.5, + "Description" => "Stopping Hair Loss/Hair Regrowth", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Health & Wellness", + "Sub-Category" => "Hair Loss/Regrowth", + "Drug" => "Hair Loss Topical Solution", + "Supply" => "1 Month or +", + "Retail Cost" => 99, + "Plus Shipping Cost" => 12.5, + "Description" => "Stopping Hair Loss/Hair Regrowth", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Health & Wellness", + "Sub-Category" => "Menopausal", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Health & Wellness", + "Sub-Category" => "Pain and Mental Wellness", + "Drug" => "Low Dose Naltrexone ", + "Supply" => "1 Month or +", + "Retail Cost" => 69, + "Plus Shipping Cost" => 12.5, + "Description" => "Treats Pain, Depression, Chronic Fatigue", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Sexual Health", + "Sub-Category" => "Sexual Health", + "Drug" => "OXYTOCIN 250 unit, 500 unit tab triturate", + "Supply" => "Per Dose, Qty 15", + "Retail Cost" => 89, + "Plus Shipping Cost" => 12.5, + "Description" => "Female Sexual Health Solutions", + "__EMPTY" => "One time + Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Sexual Health", + "Sub-Category" => "Sexual Health", + "Drug" => "SCREAM CREAM (Arginine 60 mg/Pentoxifylline 50 mg/Sildenafil 10 mg/Testosterone 1 mg/gm)", + "Supply" => "1 Month or +", + "Retail Cost" => 79, + "Plus Shipping Cost" => 12.5, + "Description" => "Female Sexual Health Solutions", + "__EMPTY" => "One time + Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Sexual Health", + "Sub-Category" => "Sexual Health", + "Drug" => "TADALAFIL 2.5 mg/PT141 1mg/Oxytocin 250 unit tab triturate", + "Supply" => "Per Dose, Qty 15", + "Retail Cost" => 119, + "Plus Shipping Cost" => 12.5, + "Description" => "Female Sexual Health Solutions", + "__EMPTY" => "One time + Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Female Sexual Health", + "Sub-Category" => "Sexual Health", + "Drug" => "TADALAFIL 5 mg/PT141 2 mg/Oxytocin 500 unit tab triturate", + "Supply" => "Per Dose, Qty 15", + "Retail Cost" => 119, + "Plus Shipping Cost" => 12.5, + "Description" => "Female Sexual Health Solutions", + "__EMPTY" => "One time + Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hair Loss/Regrowth", + "Drug" => "Hair Loss Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 99, + "Plus Shipping Cost" => 12.5, + "Description" => "Stopping Hair Loss/Hair Regrowth", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hair Loss/Regrowth", + "Drug" => "Hair Loss Topical Solution", + "Supply" => "1 Month or +", + "Retail Cost" => 99, + "Plus Shipping Cost" => 12.5, + "Description" => "Stopping Hair Loss/Hair Regrowth", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Clomiphine Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 69, + "Plus Shipping Cost" => 12.5, + "Description" => "Low Testosterone Levels, Low Energy", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Enclomaphine Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 119, + "Plus Shipping Cost" => 12.5, + "Description" => "Low Testosterone Levels, Low Energy", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "HGH (Zomacton)", + "Supply" => "1 Week", + "Retail Cost" => 549, + "Plus Shipping Cost" => 22.5, + "Description" => "Human Growth Hormone", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Nandralone Injection", + "Supply" => "1 Month or +", + "Retail Cost" => 299, + "Plus Shipping Cost" => 22.5, + "Description" => "Promotes Lean Muscle, Endurance, Healing, Increase Bone Density", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Oxandralone Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 159, + "Plus Shipping Cost" => 12.5, + "Description" => "Maintain Weight, Decrease Muscle Loss", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Sermorelin Injection", + "Supply" => "1 Month or +", + "Retail Cost" => 129, + "Plus Shipping Cost" => 22.5, + "Description" => "Growth Homrone Promotes Lean Muscle, Endurance, Healing, Sleep", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Stanozolol Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 109, + "Plus Shipping Cost" => 12.5, + "Description" => "Retention of Lean Muscle", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "T3/T4 Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 69, + "Plus Shipping Cost" => 12.5, + "Description" => "Thyroid Deficiency", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Testosterone Cream", + "Supply" => "1 Month or +", + "Retail Cost" => 139, + "Plus Shipping Cost" => 12.5, + "Description" => "Low Testosterone Levels, Low Energy", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Hormone Therapy", + "Drug" => "Testosterone Injection", + "Supply" => "10 Week Supply", + "Retail Cost" => 139, + "Plus Shipping Cost" => 12.5, + "Description" => "Low Testosterone Levels, Low Energy", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Sexual Health", + "Sub-Category" => "Erectile Dysfunction", + "Drug" => "OXYTOCIN 250 unit, 500 unit tab triturate", + "Supply" => "Per Dose, Qty 15", + "Retail Cost" => 89, + "Plus Shipping Cost" => 12.5, + "Description" => "Erectile Dysfuction Solutions", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Sexual Health", + "Sub-Category" => "Erectile Dysfunction", + "Drug" => "Sildenafil (Viagra)", + "Supply" => "1 Month or +", + "Retail Cost" => 69, + "Plus Shipping Cost" => 12.5, + "Description" => "Erectile Dysfuction Solutions", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Sexual Health", + "Sub-Category" => "Erectile Dysfunction", + "Drug" => "Tadalafil (Cialas)", + "Supply" => "1 Month or +", + "Retail Cost" => 69, + "Plus Shipping Cost" => 12.5, + "Description" => "Erectile Dysfuction Solutions", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Sexual Health", + "Sub-Category" => "Erectile Dysfunction", + "Drug" => "TADALAFIL 10 mg/PT-141 2 mg/Oxytocin 250 unit tab triturate", + "Supply" => "Per Dose, Qty 15", + "Retail Cost" => 119, + "Plus Shipping Cost" => 12.5, + "Description" => "Erectile Dysfuction Solutions", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Sexual Health", + "Sub-Category" => "Erectile Dysfunction", + "Drug" => "TADALAFIL 20 mg/PT-141 2 mg/Oxytocin 500 unit tab triturate", + "Supply" => "Per Dose, Qty 15", + "Retail Cost" => 119, + "Plus Shipping Cost" => 12.5, + "Description" => "Erectile Dysfuction Solutions", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Sexual Health", + "Sub-Category" => "Erectile Dysfunction", + "Drug" => "Trimix Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 129, + "Plus Shipping Cost" => 12.5, + "Description" => "Erectile Dysfuction Solutions", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "AOD 600 mcg cap", + "Supply" => "1 Month or +", + "Retail Cost" => 139, + "Plus Shipping Cost" => 12.5, + "Description" => "Weight Loss - Speed Metabolism, Burn Fat", + "__EMPTY" => "Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "BPC-157 500 mcg cap", + "Supply" => "1 Month or +", + "Retail Cost" => 129, + "Plus Shipping Cost" => 12.5, + "Description" => "Promotes GI Protection, Wound Healing, Muscle Recovery, Anabolic Effects", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "DIHEXA 10mg Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 159, + "Plus Shipping Cost" => 12.5, + "Description" => "Enhances Memory and Cognition", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "DIHEXA 20mg Capsule", + "Supply" => "1 Month or +", + "Retail Cost" => 299, + "Plus Shipping Cost" => 12.5, + "Description" => "Enhances Memory and Cognition", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "DSIP 500 mcg cap", + "Supply" => "1 Month or +", + "Retail Cost" => 119, + "Plus Shipping Cost" => 12.5, + "Description" => "Promotes Sleep, Regulates Circadian Rythms, Reduces Pain, Improves Mood", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "KPV 500 mcg cap", + "Supply" => "1 Month or +", + "Retail Cost" => 129, + "Plus Shipping Cost" => 12.5, + "Description" => "Lowers Inflammation, Improves Gut Health", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "KPV 500 mcg/BPC-157 mcg cap", + "Supply" => "1 Month or +", + "Retail Cost" => 139, + "Plus Shipping Cost" => 12.5, + "Description" => "Lowers Inflammation, Improves Gut Health", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "NAD+ 300 mg/mL Nasal Spray", + "Supply" => "1 Month or +", + "Retail Cost" => 99, + "Plus Shipping Cost" => 12.5, + "Description" => "Energy, Cell Repair, Overall Health", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Peptides", + "Sub-Category" => "Oral Peptides", + "Drug" => "NAD+ 400 mg patch", + "Supply" => "4 Patches", + "Retail Cost" => 129, + "Plus Shipping Cost" => 12.5, + "Description" => "Energy, Cell Repair, Overall Health", + "__EMPTY" => "One time + Subscription" + ], + [ + "Category" => "Female Health & Wellness", + "Sub-Category" => "Energy", + "Drug" => "B12 Injections", + "Supply" => "10mL", + "Retail Cost" => 129, + "Plus Shipping Cost" => 12.5, + "Description" => "Low Energy", + "__EMPTY" => "One time + Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Category" => "Male Health & Wellness", + "Sub-Category" => "Energy", + "Drug" => "B12 Injections", + "Supply" => "10mL", + "Retail Cost" => 129, + "Plus Shipping Cost" => 12.5, + "Description" => "Low Energy", + "__EMPTY" => "One time + Subscription", + "__EMPTY_1" => "Prescription required. " + ], + [ + "Drug" => "Rapamycin", + "__EMPTY" => "One time + Subscription", + "__EMPTY_1" => "Prescription required. " + ] + ]; + + + + + + foreach ($data as $item) { + if (!isset($item['Drug'])) + continue; + if (!isset($item['Description'])) + continue; + // Find the existing record based on title and list_two_title + $existingPlan = PlanV1::where('title', $item['Drug']) + ->where('list_two_title', $item['Description']) + ->first(); + $is_prescription_required = false; + if (isset($item['__EMPTY_1']) && $item['__EMPTY_1'] == "Prescription required. ") + $is_prescription_required = true; + if ($existingPlan) { + // Update the existing record + $existingPlan->update([ + 'currency' => '$', + 'price' => $item['Retail Cost'] ?? 0, + 'shipping_cost' => $item['Plus Shipping Cost'] ?? 0, + 'list_one_title' => "hgh", + 'is_prescription_required' => $is_prescription_required, + 'list_sub_title' => $item['Description'] ?? "", + 'list_sub_title' => $item['Description'] ?? "", + 'slug' => PlanV1::generateUniqueSlug($item['Drug']), + ]); + } else { + // Create a new record + PlanV1::create([ + 'title' => $item['Drug'], + 'currency' => '$', + 'price' => $item['Retail Cost'] ?? 0, + 'shipping_cost' => $item['shipping_cost'] ?? 0, + 'list_one_title' => "hgh", + 'is_prescription_required' => $is_prescription_required, + 'list_two_title' => $item['Description'] ?? "", + 'list_sub_title' => $item['Description'] ?? "", + 'slug' => PlanV1::generateUniqueSlug($item['Drug']), + ]); + } + } + + return 0; + } +} diff --git a/app/Console/Commands/ParseQuestionsSheet.php b/app/Console/Commands/ParseQuestionsSheet.php new file mode 100644 index 0000000..cdc4edb --- /dev/null +++ b/app/Console/Commands/ParseQuestionsSheet.php @@ -0,0 +1,77 @@ +error('File not found: ' . $filePath); + return Command::FAILURE; + } + // Load the Excel file + $spreadsheet = IOFactory::load($filePath); + $worksheet = $spreadsheet->getSheetByName('doctor intake questions'); + + if (!$worksheet) { + $this->error('Sheet1 not found in the Excel file.'); + return Command::FAILURE; + } + + $jsonArray = []; + + // Iterate through rows starting from the second row (assuming the first row contains headers) + foreach ($worksheet->getRowIterator(2) as $row) { + $cellIterator = $row->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(false); + + // Get the data from each cell + $label = $cellIterator->current()->getValue(); + $cellIterator->next(); + $key = $cellIterator->current()->getValue(); + $cellIterator->next(); + $type = $cellIterator->current()->getValue(); + + // Add data to the JSON array + $jsonArray[] = [ + 'label' => $label, + 'key' => $key, + 'type' => $type, + ]; + } + + // Convert the array to JSON + $json = json_encode($jsonArray, JSON_PRETTY_PRINT); + + // Save JSON to a file + $outputFilePath = resource_path('js/views/pages/questionere/questions_parse.json'); + file_put_contents($outputFilePath, $json); + + $this->info('JSON data saved to: ' . $outputFilePath); + } +} diff --git a/app/Console/Commands/ReadCsv.php b/app/Console/Commands/ReadCsv.php new file mode 100644 index 0000000..7f7ae43 --- /dev/null +++ b/app/Console/Commands/ReadCsv.php @@ -0,0 +1,145 @@ +error('File not found: ' . $filePath); + return Command::FAILURE; + } + + $csv = Reader::createFromPath($filePath, 'r'); + $csv->setHeaderOffset(0); + + $records = $csv->getRecords(); + + // Get the header row + $header = $csv->getHeader(); + + // Loop through each row + foreach ($csv->getRecords($header) as $record) { + + + $Category = $record['Category']; + if (count(explode(",", $Category)) > 1) { + foreach (explode(",", $Category) as $cat) { + $ProfileCategory = ProfileCategory::firstOrCreate([ + 'name' => trim($cat), + 'icon' => $this->convertTextToLowerAndReplaceDashes($cat) . ".png", + 'category_link' => $this->convertTextToLowerAndReplaceSpaces($cat) + ]); + $this->insertREcord($ProfileCategory, $record); + } + } elseif ($Category) { + $ProfileCategory = ProfileCategory::firstOrCreate([ + 'name' => trim($Category), + 'icon' => $this->convertTextToLowerAndReplaceDashes($Category) . ".png", + 'category_link' => $this->convertTextToLowerAndReplaceSpaces($Category) + ]); + $this->insertREcord($ProfileCategory, $record); + } + } + + return Command::SUCCESS; + } + function insertREcord($ProfileCategory, $record) + { + $type = $record['type']; + + $options = rtrim($record['options']); + $options = serialize(explode(",", $options)); + $Question = $record['Question']; + + $ProfileQuestion = ProfileQuestion::firstOrCreate([ + 'question' => $Question, + 'question_options' => $options, + 'question_type' => $type, + ]); + + try { + ProfileCategoryQuestion::firstOrCreate([ + 'category_id' => $ProfileCategory->id, + 'question_id' => $ProfileQuestion->id + ]); + } catch (Exception $e) { + } + + $BaseQuestionR = $record['Base Question (Reference)']; + $ProfileQuestion_id = null; + if ($BaseQuestionR !== "") { + $ProfileQuestion = ProfileQuestion::where("question", $BaseQuestionR)->first(); + if ($ProfileQuestion) + $ProfileQuestion_id = $ProfileQuestion->id; + } + + + $SubQuestionR = $record['Sub Question (Reference)']; + $parent_sub_question_id = null; + if ($SubQuestionR !== "") { + $ProfileSubQuestion = ProfileSubQuestion::where("question", $SubQuestionR)->first(); + if ($ProfileSubQuestion) + $parent_sub_question_id = $ProfileSubQuestion->id; + } + + $SubQuestion = $record['SubQuestion']; + + $ProfileSubQuestion2 = ProfileSubQuestion::updateOrCreate([ + 'category_id' => $ProfileCategory->id, + 'question_id' => $ProfileQuestion_id, + 'question' => $SubQuestion, + 'parent_sub_question_id' => $parent_sub_question_id, + 'sub_question_type' => $type, + 'sub_question_options' => $options, + ]); + } +} diff --git a/app/Console/Commands/SendReminderEmails.php b/app/Console/Commands/SendReminderEmails.php new file mode 100644 index 0000000..5b87f77 --- /dev/null +++ b/app/Console/Commands/SendReminderEmails.php @@ -0,0 +1,65 @@ +whereNotExists(function ($query) { + $query->select('id') + ->from('patient_reg_activity') + ->whereColumn('patient_reg_activity.patient_id', 'patient_reg_activity.patient_id') + ->where('activity', 'patient_appointment_booked'); + })->where("patient_reg_activity.email_sent", "!=", 1)->get(); + + foreach ($incompleteRegistrations as $patient) { + + $patient->notify(new RegistrationReminder($patient->patient_id)); + $patient->email_sent = 1; + $patient->save(); + } + + $incompleteAppointments = PatientRegActivity::where('activity', 'patient_appointment_booked') + ->whereNotExists(function ($query) { + $query->select('id') + ->from('patient_reg_activity') + ->whereColumn('patient_reg_activity.patient_id', 'patient_reg_activity.patient_id') + ->where('activity', 'patient_medical_question_entered'); + })->where("patient_reg_activity.email_sent", "!=", 1)->get(); + + foreach ($incompleteAppointments as $patient) { + echo "Into patient_medical_question_entered"; + $patient->notify(new AppointmentReminder($patient->patient_id)); + $patient->email_sent = 1; + $patient->save(); + } + + $this->info('Reminder emails sent successfully.'); + } +} diff --git a/app/Console/Commands/parseTypeSheet.php b/app/Console/Commands/parseTypeSheet.php new file mode 100644 index 0000000..fa87750 --- /dev/null +++ b/app/Console/Commands/parseTypeSheet.php @@ -0,0 +1,110 @@ +error('File not found: ' . $filePath); + return Command::FAILURE; + } + + // Load the Excel file + $spreadsheet = IOFactory::load($filePath); + $worksheet = $spreadsheet->getSheetByName('type'); + + if (!$worksheet) { + $this->error('Sheet2 not found in the Excel file.'); + return Command::FAILURE; + } + + $objects = []; + $counter = 1; // Initialize counter + + // Get the highest column number + $highestColumn = $worksheet->getHighestColumn(); + // Convert the column letter to a number + $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); + + // Iterate through columns + for ($col = 1; $col <= $highestColumnIndex; $col += 2) { + $type = $worksheet->getCellByColumnAndRow($col, 1)->getValue(); // Get the type + $name = $worksheet->getCellByColumnAndRow($col + 1, 1)->getValue(); // Get the name + $values = []; + + // Initialize row counter + $row = 2; + + // Iterate through the values in the column pair using a do-while loop to ensure it executes at least once + do { + $keyCell = $worksheet->getCellByColumnAndRow($col, $row); + $valueCell = $worksheet->getCellByColumnAndRow($col + 1, $row); + + // Check if key and value cells are not empty + if ($keyCell->getValue() !== null && $valueCell->getValue() !== null) { + $key = $keyCell->getValue(); + $value = $valueCell->getValue(); + + // Add key-value pair to the values array + $values[$key] = $value; + + // Increment row counter + $row++; + } else { + // If either key or value cell is empty, exit the loop + break; + } + } while (true); + + // Add object to the objects array only if type and name are not null and values are not empty + if ($type !== null && $name !== null && !empty($values)) { + $objects[$type] = [ + 'type' => $name, + 'values' => $values + ]; + } + } + + // Convert the array to JSON + $json = json_encode($objects, JSON_PRETTY_PRINT); + + // Save JSON to a file + $outputFilePath = resource_path('js/views/pages/questionere/type_parse.json'); + file_put_contents($outputFilePath, $json); + + $this->info('JSON data saved to: ' . $outputFilePath); + } + + + // Helper function to get the cell value safely + private function getCellValue($cell) + { + return $cell ? $cell->getValue() : ''; + } +} diff --git a/app/Console/Commands/send15MinAlertAppointment.php b/app/Console/Commands/send15MinAlertAppointment.php new file mode 100644 index 0000000..c115b1c --- /dev/null +++ b/app/Console/Commands/send15MinAlertAppointment.php @@ -0,0 +1,61 @@ +addMinutes(15)->format('H:i:s'))->where('appointment_time', '>=', Carbon::now()->format('H:i:s'))->get(); + + + + foreach ($appointments as $appointment) { + + // Concatenate date and time + $datetimeUtc = $appointment->appointment_date . ' ' . $appointment->appointment_time; + + // Convert to UTC timezone + $dateTimeUtc = Carbon::createFromFormat('Y-m-d H:i:s', $datetimeUtc, 'UTC'); + + // Convert to the appointment's timezone + $appointmentTimeZone = new CarbonTimeZone($appointment->timezone); + $dateTimeInAppointmentTimeZone = $dateTimeUtc->setTimezone($appointmentTimeZone); + + // Split the datetime into date and time components in the appointment timezone + $appointment->appointment_date = $appointmentDate = $dateTimeInAppointmentTimeZone->format('Y-m-d'); + $appointment->appointment_time = $appointmentTime = $dateTimeInAppointmentTimeZone->format('H:i:s'); + + + $patient = $appointment->patient; + Mail::send('emails.upcoming-meeting', ['patient' => $patient, 'appointment' => $appointment], function ($message) use ($patient) { + $message->to("muhammadawais95@gmail.com", $patient->first_name) + ->subject('Upcoming Meeting Reminder'); + }); + } + } +} diff --git a/app/Console/Commands/send1MinAlertAppointment.php b/app/Console/Commands/send1MinAlertAppointment.php new file mode 100644 index 0000000..5a711e6 --- /dev/null +++ b/app/Console/Commands/send1MinAlertAppointment.php @@ -0,0 +1,58 @@ +addMinutes(1)->format('H:i:s'))->where('appointment_time', '>=', Carbon::now()->format('H:i:s'))->get(); + + foreach ($appointments as $appointment) { + + // Concatenate date and time + $datetimeUtc = $appointment->appointment_date . ' ' . $appointment->appointment_time; + + // Convert to UTC timezone + $dateTimeUtc = Carbon::createFromFormat('Y-m-d H:i:s', $datetimeUtc, 'UTC'); + + // Convert to the appointment's timezone + $appointmentTimeZone = new CarbonTimeZone($appointment->timezone); + $dateTimeInAppointmentTimeZone = $dateTimeUtc->setTimezone($appointmentTimeZone); + + // Split the datetime into date and time components in the appointment timezone + $appointment->appointment_date = $appointmentDate = $dateTimeInAppointmentTimeZone->format('Y-m-d'); + $appointment->appointment_time = $appointmentTime = $dateTimeInAppointmentTimeZone->format('H:i:s'); + + $patient = $appointment->patient; + Mail::send('emails.start-meeting', ['patient' => $patient, 'appointment' => $appointment], function ($message) use ($patient) { + $message->to($patient->email, $patient->first_name) + ->subject('Join Our Online Appointment Now'); + }); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 0000000..440a6f0 --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,28 @@ +command('app:send15-min-alert-appointment')->everyMinute(); + $schedule->command('app:send1-min-alert-appointment')->everyFifteenMinutes(); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__ . '/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/app/Events/AppointmentBooked.php b/app/Events/AppointmentBooked.php new file mode 100644 index 0000000..c040d89 --- /dev/null +++ b/app/Events/AppointmentBooked.php @@ -0,0 +1,39 @@ +appointment = $appointment; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('channel-name'), + ]; + } +} diff --git a/app/Events/AppointmentCallEnded.php b/app/Events/AppointmentCallEnded.php new file mode 100644 index 0000000..e629199 --- /dev/null +++ b/app/Events/AppointmentCallEnded.php @@ -0,0 +1,52 @@ +patient_id = $patient_id; + $this->appointment_id = $appointment_id; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('patient-end-call-' . $this->patient_id), + ]; + } + + /** + * Get the data to broadcast. + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'patient_id' => $this->patient_id, + 'appointment_id' => $this->appointment_id + ]; + } +} diff --git a/app/Events/AppointmentCreated.php b/app/Events/AppointmentCreated.php new file mode 100644 index 0000000..e7d61f3 --- /dev/null +++ b/app/Events/AppointmentCreated.php @@ -0,0 +1,59 @@ +patient_id = $patient_id; + $this->appointment_id = $appointment_id; + $this->meeting_id = $meeting_id; + $this->call_type = $call_type; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('patient-' . $this->patient_id), + ]; + } + + /** + * Get the data to broadcast. + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'patient_id' => $this->patient_id, + 'appointment_id' => $this->appointment_id, + 'meeting_id' => $this->meeting_id, + 'call_type' => $this->call_type + ]; + } +} diff --git a/app/Events/DeviceCurrentStatus.php b/app/Events/DeviceCurrentStatus.php new file mode 100644 index 0000000..00d8d97 --- /dev/null +++ b/app/Events/DeviceCurrentStatus.php @@ -0,0 +1,39 @@ +micStatus = $micStatus; + $this->camStatus = $camStatus; + } + + public function broadcastOn() + { + return new PrivateChannel('agent-queue'); + } + + public function broadcastWith() + { + return [ + 'mic' => $this->micStatus, + 'cam' => $this->camStatus, + ]; + } +} diff --git a/app/Events/PatientRegistered.php b/app/Events/PatientRegistered.php new file mode 100644 index 0000000..83d9a85 --- /dev/null +++ b/app/Events/PatientRegistered.php @@ -0,0 +1,41 @@ +patient = $patient; + $this->validatedData = $validatedData; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('channel-name'), + ]; + } +} diff --git a/app/Events/PaymentProcessed.php b/app/Events/PaymentProcessed.php new file mode 100644 index 0000000..cda31b9 --- /dev/null +++ b/app/Events/PaymentProcessed.php @@ -0,0 +1,39 @@ +patient = $patient; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('channel-name'), + ]; + } +} diff --git a/app/Events/StartCall.php b/app/Events/StartCall.php new file mode 100644 index 0000000..d01d8ed --- /dev/null +++ b/app/Events/StartCall.php @@ -0,0 +1,39 @@ +message = $message; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return ['my-channel']; + } + public function broadcastAs() + { + return 'my-event'; + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..56af264 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,30 @@ + + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + */ + public function register(): void + { + $this->reportable(function (Throwable $e) { + // + }); + } +} diff --git a/app/Http/.DS_Store b/app/Http/.DS_Store new file mode 100644 index 0000000..b40161f Binary files /dev/null and b/app/Http/.DS_Store differ diff --git a/app/Http/Controllers/Admin/AgentController.php b/app/Http/Controllers/Admin/AgentController.php new file mode 100644 index 0000000..4407fae --- /dev/null +++ b/app/Http/Controllers/Admin/AgentController.php @@ -0,0 +1,70 @@ + $agents]); + } + + public function add() + { + return view('admin.agents.add'); + } + + public function save(Request $request) + { + $agent = Telemedpro::where('email',$request->input('email'))->first(); + if($agent) + { + $request->session()->flash('error', 'The email has already been taken.'); + return redirect()->back(); + } + Telemedpro::create([ + 'name' => $request->input('name'), + 'email' => $request->input('email'), + 'password' => bcrypt($request->input('password')), + ]); + $request->session()->flash('message', 'Agent created successfully'); + return redirect()->back(); + } + + public function edit($id) + { + $agent = Telemedpro::where('id',$id)->first(); + return view('admin.agents.edit', ['agent' => $agent]); + } + + public function update($id,Request $request) + { + $agent = Telemedpro::where('id',$id)->first(); + $request->validate([ + 'name' => 'required', + 'email' => 'required|email|unique:telemed_pros,email,' . $id, + // Other validation rules... + ]); + $agent->name = $request->input('name'); + $agent->email = $request->input('email'); + if($request->input('password')) + $agent->password = $request->input('password'); + $agent->save(); + + $request->session()->flash('message', 'Agent updated successfully'); + return redirect()->back(); + } + + public function delete($id,Request $request) + { + Telemedpro::where('id',$id)->delete(); + $request->session()->flash('message', 'Agent deleted successfully'); + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/Admin/Api/AdminController.php b/app/Http/Controllers/Admin/Api/AdminController.php new file mode 100644 index 0000000..727b798 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/AdminController.php @@ -0,0 +1,122 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function index(){ + try { + $this->authorizeForUser($this->user,'list', new Admin); + $adminData = Admin::all(); + foreach($adminData as $admin) + { + $admin->image_path = $this->url->to('/storage/profile_pictures/' . $admin->image_path); + } + + return DataTables::of($adminData)->make(true); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + + } + public function getRoles() + { + $roles = Permission::select('id','role_name as role')->get(); + return response()->json([ + 'data' => $roles + ], 201); + } + public function saveAdmin(Request $request) + { + $this->authorize('add', new Admin); + $data =[ + "name" => $request->get('name'), + "email" => $request->get('email'), + "password" => Hash::make($request->input('password')), + "last_name" => $request->get('last_name'), + "phone_no" => $request->get('phone_no'), + "role_id" => $request->get('role_id') + ]; + $admin = Admin::create($data); + $image = $request->get('profile_pic'); + $fileName = 'profile-' . time(); + + $logo = base64_decode($image); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + + $imageName = $fileName . '.' . $ext; + Storage::disk('local')->put("/public/profile_pictures/" . $imageName, $logo); + + $admin->image_path = $imageName; + $admin->save(); + return response()->json([ + 'success' => "Data Saved! " + ], 201); + } + public function editAdmin($id) + { + $adminData = Admin::find($id); + + if($adminData->image_path) + $adminData->image_path = $this->url->to('/storage/profile_pictures/' . $adminData->image_path); + else + $adminData->image_path=''; + + return response()->json([ + 'data' => $adminData + ], 201); + } + public function updateAdmin($id,Request $request) + { + $admin = Admin::find($id); + $admin->name = $request->get('name'); + // $admin->email = $request->get('email'); + if($request->input('password')) + $admin->password = Hash::make($request->input('password')); + $admin->last_name = $request->get('last_name'); + $admin->phone_no = $request->get('phone_no'); + $admin->role_id = $request->get('role_id'); + + if($request->get('profile_pic')) + { + $image = $request->get('profile_pic'); + $fileName = 'profile-' . time(); + + $logo = base64_decode($image); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + + $imageName = $fileName . '.' . $ext; + Storage::disk('local')->put("/public/profile_pictures/" . $imageName, $logo); + $admin->image_path = $imageName; + } + + $admin->save(); + return response()->json([ + 'data' => $admin + ], 201); + } + public function detailAdmin($id) + { + + $admin = Admin::find($id); + $admin->image_path = $this->url->to('/storage/profile_pictures/' . $admin->image_path); + return response()->json([ + 'data' => $admin + ], 201); + } +} diff --git a/app/Http/Controllers/Admin/Api/AppointmentController.php b/app/Http/Controllers/Admin/Api/AppointmentController.php new file mode 100644 index 0000000..22875ce --- /dev/null +++ b/app/Http/Controllers/Admin/Api/AppointmentController.php @@ -0,0 +1,643 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function getAppointmentList() + { + try { + $this->authorizeForUser($this->user, 'list', new Appointment); + $appointments = Appointment::select("patients.first_name", "patients.last_name", "telemed_pros.name as agent_name", "appointments.*") // Eager load the associated telemed pro + ->leftJoin("telemed_pros", "telemed_pros.id", "appointments.telemed_pros_id") + ->leftJoin("patients", "patients.id", "appointments.patient_id") + /* ->orderBy('appointment_time', 'desc') */ // Optional: sort by appointment time + ->get(); + + return response()->json($appointments, 200); + } catch (AuthorizationException $e) { + + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } + public function getMeetingHistory(Patient $patient, $filter = '12_months') + { + try { + $this->authorizeForUser($this->user, 'meeting_history', new Appointment); + $currentMonth = Carbon::now(); + + // Filter logic + switch ($filter) { + case 'current_month': + $startDate = $currentMonth->copy()->startOfMonth(); + break; + case '1_month': + $startDate = $currentMonth->copy()->subMonth()->startOfMonth(); + break; + case '2_months': + $startDate = $currentMonth->copy()->subMonths(2)->startOfMonth(); + break; + case '3_months': + $startDate = $currentMonth->copy()->subMonths(3)->startOfMonth(); + break; + case '6_months': + $startDate = $currentMonth->copy()->subMonths(6)->startOfMonth(); + break; + default: // Default to 12 months + $startDate = $currentMonth->copy()->subMonths(12)->startOfMonth(); + } + + $endDate = $currentMonth->endOfMonth(); + + // Fetch patient names and appointment counts directly from the database + $monthlyData = Appointment::select( + 'patient_id', + /* DB::raw('COUNT(*) as appointment_count'), */ + 'appointment_time', + 'appointment_date', + 'start_time', + 'end_time', + 'duration', + 'id' + ) + ->where("patient_id", $patient->id) + ->whereNotNull("end_time") + ->whereBetween('created_at', [$startDate, $endDate]) + + ->get(); + + $patients = []; + + foreach ($monthlyData as $dataPoint) { + $patientName = $dataPoint->patient->first_name . " " . $dataPoint->patient->last_name; // Assuming 'name' is the field representing patient names + /* $appointmentCount = $dataPoint->appointment_count; */ + $start_time = $dataPoint->start_time; + $end_time = $dataPoint->end_time; + $duration = $dataPoint->duration; + $appointment_time = $dataPoint->appointment_time; + $appointment_date = $dataPoint->appointment_date; + $id = $dataPoint->id; + + $patients[] = [ + 'patient_name' => $patientName, + 'appointment_time' => $appointment_time, + 'appointment_date' => $appointment_date, + /* 'appointment_count' => $appointmentCount, */ + 'start_time' => $start_time, + 'end_time' => $end_time, + 'duration' => $duration, + 'id' => $id, + ]; + } + + return response()->json([ + 'patients' => $patients, + ]); + } catch (AuthorizationException $e) { + + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } + public function getAppointmentByid($patient, $appointment, Request $request) + { + try { + $this->authorizeForUser($this->user, 'list', new Appointment); + // Assuming user can be either telemedPro or patient + $data = Appointment::select('appointments.*', 'telemed_pros.name as agent_name') + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', '=', 'telemed_pros.id') + ->where('appointments.patient_id', $patient) + ->where('appointments.id', $appointment) + ->first(); + // dd($data); + return response()->json(['data' => $data]); + } catch (AuthorizationException $e) { + + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } + public function bookAppointment(Request $request) + { + try { + $this->authorizeForUser($this->user, 'list', new Appointment); + $validatedData = $request->validate([ + /* 'telemed_pros_id' => 'required|exists:telemed_pros,id', */ + 'patient_id' => 'required|exists:patients,id', + 'appointment_time' => 'required|date_format:H:i:s', + 'appointment_date' => 'required|date_format:Y-m-d', + 'patient_name' => 'required', + 'patient_email' => 'required', + 'timezone' => 'required', + ]); + try { + $tz = new DateTimeZone($validatedData['timezone']); + $standardTz = $tz->getName(); + } catch (Exception $e) { + return response()->json([ + 'message' => $e->getMessage() + ], 400); + } + try { + $timezoneMap = [ + 'EST' => 'America/New_York', + 'EDT' => 'America/New_York', + 'CST' => 'America/Chicago', + 'CDT' => 'America/Chicago', + 'MST' => 'America/Denver', + 'MDT' => 'America/Denver', + 'PST' => 'America/Los_Angeles', + 'PDT' => 'America/Los_Angeles', + // Add more mappings as needed + ]; + $timezone = $validatedData['timezone']; + if (array_key_exists($timezone, $timezoneMap)) { + $timezone = $timezoneMap[$timezone]; + } + + $appointmentDateTime = new DateTime( + $validatedData['appointment_date'] . ' ' . $validatedData['appointment_time'], + new DateTimeZone($timezone) + ); + + $appointmentDateTime->setTimezone(new DateTimeZone('UTC')); + + $validatedData['appointment_time'] = $appointmentDateTime->format('H:i:s'); + $validatedData['appointment_date'] = $appointmentDateTime->format('Y-m-d'); + } catch (Exception $e) { + return response()->json([ + 'message' => $e->getMessage() + ], 400); + } + + $availableTelemedPros = Telemedpro::select("telemed_pros.id", "telemed_pros.name")/* ->where('is_busy', false) */ + ->leftJoin('appointments', function ($join) use ($validatedData) { + $join->on('telemed_pros.id', '=', 'appointments.telemed_pros_id') + ->where('appointments.appointment_time', '=', $validatedData['appointment_time']) + ->where('appointments.appointment_date', '=', $validatedData['appointment_date']); + }) + ->whereNull('appointments.id') + ->first(); + + if (!$availableTelemedPros) + return response()->json([ + 'message' => 'Appointment time not available' + ], 400); + + $existingAppointment = Appointment::where('telemed_pros_id', $availableTelemedPros->id) + ->where('appointment_time', $validatedData['appointment_time']) + ->where('appointment_date', $validatedData['appointment_date']) + ->first(); + + if ($existingAppointment) { + return response()->json([ + 'message' => 'Appointment time not available' + ], 400); + } + $validatedData['telemed_pros_id'] = $availableTelemedPros->id; + $validatedData['status'] = 'pending'; + + // Create the appointment + $appointment = Appointment::create($validatedData); + $appointment_booking_tokens = $this->bookAppointmentApi($appointment, $availableTelemedPros); + $appointment->agent_call_token = $appointment_booking_tokens['tokenAgent']; + $appointment->patient_call_token = $appointment_booking_tokens['tokenPatient']; + $appointment->save(); + + PatientRegActivity::create([ + 'patient_id' => $validatedData['patient_id'], + 'activity' => 'patient_appointment_booked' + ]); + $patient = $appointment->patient; + $datetimeUtc = $appointment->appointment_date . ' ' . $appointment->appointment_time; + $dateTimeUtc = Carbon::createFromFormat('Y-m-d H:i:s', $datetimeUtc, 'UTC'); + $appointmentTimeZone = new CarbonTimeZone($appointment->timezone); + $dateTimeInAppointmentTimeZone = $dateTimeUtc->setTimezone($appointmentTimeZone); + $appointment->appointment_date = $appointmentDate = $dateTimeInAppointmentTimeZone->format('Y-m-d'); + $appointment->appointment_time = $appointmentTime = $dateTimeInAppointmentTimeZone->format('H:i:s'); + $setting = Setting::find(1); + //event(new AppointmentBooked($appointment)); + $cart = Cart::find($request->input("cart_id")); + $cart->appointment_id = $appointment->id; + $cart->save(); + return response()->json([ + 'message' => 'Appointment booked successfully', + 'meeting_id' => $appointment->agent_call_token, + 'appointment' => $appointment, + 'appointment_time' => $validatedData['appointment_time'], + 'appointment_date' => $validatedData['appointment_date'] + ]); + } catch (AuthorizationException $e) { + + return response()->json(['error' => $e->getMessage()], 500); + } + } + public function editAppointment(Appointment $appointment, Request $request) + { + try { + $this->authorizeForUser($this->user, 'list', new Appointment); + $validatedData = $request->validate([ + 'patient_id' => 'sometimes|exists:patients,id', + 'appointment_time' => 'sometimes|date_format:H:i:s', + 'appointment_date' => 'sometimes|date_format:Y-m-d', + 'patient_name' => 'sometimes|string', + 'patient_email' => 'sometimes|email', + 'timezone' => 'sometimes|string', + ]); + + if (isset($validatedData['timezone'])) { + try { + $tz = new DateTimeZone($validatedData['timezone']); + $standardTz = $tz->getName(); + } catch (Exception $e) { + return response()->json([ + 'message' => $e->getMessage() + ], 400); + } + + $timezoneMap = [ + 'EST' => 'America/New_York', + 'EDT' => 'America/New_York', + 'CST' => 'America/Chicago', + 'CDT' => 'America/Chicago', + 'MST' => 'America/Denver', + 'MDT' => 'America/Denver', + 'PST' => 'America/Los_Angeles', + 'PDT' => 'America/Los_Angeles', + // Add more mappings as needed + ]; + + $timezone = $validatedData['timezone']; + if (array_key_exists($timezone, $timezoneMap)) { + $timezone = $timezoneMap[$timezone]; + } + + if (isset($validatedData['appointment_date']) && isset($validatedData['appointment_time'])) { + try { + $appointmentDateTime = new DateTime( + $validatedData['appointment_date'] . ' ' . $validatedData['appointment_time'], + new DateTimeZone($timezone) + ); + + $appointmentDateTime->setTimezone(new DateTimeZone('UTC')); + + $validatedData['appointment_time'] = $appointmentDateTime->format('H:i:s'); + $validatedData['appointment_date'] = $appointmentDateTime->format('Y-m-d'); + } catch (Exception $e) { + return response()->json([ + 'message' => $e->getMessage() + ], 400); + } + + // Check if the new time slot is available + $availableTelemedPros = Telemedpro::select("telemed_pros.id", "telemed_pros.name") + ->leftJoin('appointments', function ($join) use ($validatedData, $appointment) { + $join->on('telemed_pros.id', '=', 'appointments.telemed_pros_id') + ->where('appointments.appointment_time', '=', $validatedData['appointment_time']) + ->where('appointments.appointment_date', '=', $validatedData['appointment_date']) + ->where('appointments.id', '!=', $appointment->id); // Exclude the current appointment + }) + ->whereNull('appointments.id') + ->first(); + + if (!$availableTelemedPros) { + return response()->json([ + 'message' => 'New appointment time not available' + ], 400); + } + + // Update the telemed_pros_id if it's different + if ($availableTelemedPros->id !== $appointment->telemed_pros_id) { + $validatedData['telemed_pros_id'] = $availableTelemedPros->id; + + // Re-book the appointment with the new telemed pro + $appointment_booking_tokens = $this->bookAppointmentApi($appointment, $availableTelemedPros); + $validatedData['agent_call_token'] = $appointment_booking_tokens['tokenAgent']; + $validatedData['patient_call_token'] = $appointment_booking_tokens['tokenPatient']; + } + } + + // Update the appointment + $appointment->update($validatedData); + + // Update related cart if it exists + $cart = Cart::where('appointment_id', $appointment->id)->first(); + if ($cart) { + $cart->appointment_id = $appointment->id; + $cart->save(); + } + + // Convert appointment time to the specified timezone for the response + $datetimeUtc = $appointment->appointment_date . ' ' . $appointment->appointment_time; + $dateTimeUtc = Carbon::createFromFormat('Y-m-d H:i:s', $datetimeUtc, 'UTC'); + $appointmentTimeZone = new CarbonTimeZone($appointment->timezone); + $dateTimeInAppointmentTimeZone = $dateTimeUtc->setTimezone($appointmentTimeZone); + $appointmentDate = $dateTimeInAppointmentTimeZone->format('Y-m-d'); + $appointmentTime = $dateTimeInAppointmentTimeZone->format('H:i:s'); + + return response()->json([ + 'message' => 'Appointment updated successfully', + 'meeting_id' => $appointment->agent_call_token, + 'appointment' => $appointment, + 'appointment_time' => $appointmentTime, + 'appointment_date' => $appointmentDate + ]); + } + } catch (AuthorizationException $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } + public function bookAppointmentApi($appointment, $availableTelemedPros) + { + + try { + $this->authorizeForUser($this->user, 'edit', new Appointment); + $roomName = 'appointment-' . $appointment->id . "-" . uniqid(); + $opts = (new RoomCreateOptions()) + ->setName($roomName) + ->setEmptyTimeout(10) + ->setMaxParticipants(5); + $host = "https://plugnmeet.codelfi.com"; + $svc = new RoomServiceClient($host, config('app.LK_API_KEY'), config('app.LK_API_SECRET')); + $room = $svc->createRoom($opts); + + $participantPatientName = "patient-" . uniqid() . $appointment->patient->first_name . " " . $appointment->patient->last_name; + + $tokenOptionsPatient = (new AccessTokenOptions()) + ->setIdentity($participantPatientName); + $videoGrantPatient = (new VideoGrant()) + ->setRoomJoin() + ->setRoomName($roomName); + $tokenPatient = (new AccessToken(config('app.LK_API_KEY'), config('app.LK_API_SECRET'))) + ->init($tokenOptionsPatient) + ->setGrant($videoGrantPatient) + ->toJwt(); + + $participantAgentName = "agent-" . uniqid() . $availableTelemedPros->name; + $tokenOptionsAgent = (new AccessTokenOptions()) + ->setIdentity($participantAgentName); + $videoGrantAgent = (new VideoGrant()) + ->setRoomJoin() + ->setRoomName($roomName); + $tokenAgent = (new AccessToken(config('app.LK_API_KEY'), config('app.LK_API_SECRET'))) + ->init($tokenOptionsAgent) + ->setGrant($videoGrantAgent) + ->toJwt(); + return [ + 'tokenPatient' => $tokenPatient, + 'tokenAgent' => $tokenAgent, + ]; + } catch (AuthorizationException | Error | Exception | TwirpError $e) { + + return response()->json(['error' => $e->getMessage()], 500); + } + } + + public function availableSlots($date) + { + try { + $this->authorizeForUser($this->user, 'book_appointment', new Appointment); + // Ensure date is in a valid format + $date = Carbon::parse($date); + $originalDate = Carbon::parse($date); + + // Generate all possible 30-minute slots between 9 AM and 4 PM + $slots = collect(); + $startTime = Carbon::parse($date)->subHours(24)->setTime(9, 0, 0); + $endTime = Carbon::parse($date)->addHours(24)->setTime(16, 0, 0); + while ($startTime < $endTime) { + $slots->push($startTime->format('Y-m-d H:i:s')); + $startTime->addMinutes(15); + } + + /* $user = Patient::find($patient_id); */ + // Filter out booked slots + $bookedAppointments = Appointment::where('appointment_date', '>=', $date->format('Y-m-d')) + ->where('appointment_date', '<', $date->addDay()->format('Y-m-d')) + ->pluck('appointment_date'); + + $availableSlots = $slots->diff($bookedAppointments); + + $formattedSlots = $availableSlots->map(function ($slot) { + $start = Carbon::parse($slot); + $startTime = $start->format('Y-m-d H:i:s'); + return $startTime; + }); + + // Additional checking if slot is booked + $formattedSlots = $formattedSlots->filter(function ($slot) use ($originalDate) { + $time = Carbon::parse($slot); + return !Appointment::where('appointment_time', $time->format('H:i:s')) + ->where('appointment_date', $originalDate->format('Y-m-d')) + ->exists(); + }); + + return response()->json([ + 'available_slots' => $formattedSlots->toArray() + ]); + } catch (AuthorizationException $e) { + + return response()->json(['error' => $e->getMessage()], 500); + } + } + public function getItemByOrder($order_id) + { + try { + $this->authorizeForUser($this->user, 'list', new Appointment); + $labkits = LabkitOrderItem::leftJoin( + 'lab_kit', + 'labkit_order_items.lab_kit_id', + 'lab_kit.id' + ) + ->leftJoin( + 'items', + 'items.id', + 'labkit_order_items.item_id' + ) + ->leftJoin( + 'plans_v1', + 'plans_v1.id', + 'items.plans_id' + ) + ->leftJoin( + 'carts', + 'carts.id', + 'labkit_order_items.cart_id' + ) + ->where('carts.id', $order_id) + ->select( + 'labkit_order_items.id', + 'labkit_order_items.status', + 'labkit_order_items.result', + 'lab_kit.name as lab_kit_name', + 'plans_v1.id as product_id', + 'plans_v1.title as product_name' + ) + ->get(); + return response()->json([ + 'order_item' => $labkits + ]); + } catch (AuthorizationException $e) { + + return response()->json(['error' => $e->getMessage()], 500); + } + } + public function getAgentLastAppointment(Patient $patient, Request $request) + { + try { + $this->authorizeForUser($this->user, 'list', new Appointment); + $appointments = Appointment::select( + "patients.first_name", + "patients.last_name", + "telemed_pros.name as agent_name", + "appointments.*", + "carts.shipping_address1", + "carts.shipping_address2", + "carts.id as order_id", + "carts.shipping_city", + "carts.shipping_state", + "carts.shipping_zipcode", + "carts.shipping_country" + + ) // Eager load the associated telemed pro + ->leftJoin("telemed_pros", "telemed_pros.id", "appointments.telemed_pros_id") + ->leftJoin("patients", "patients.id", "appointments.patient_id") + ->leftJoin("carts", "carts.appointment_id", "appointments.id") + ->where("appointments.patient_id", $patient->id) + ->orderBy('appointments.created_at', 'desc') + ->first(); + + $upcoming_appointments = Appointment::select( + "patients.first_name", + "patients.last_name", + "telemed_pros.name as agent_name", + "appointments.*", + "carts.shipping_address1", + "carts.shipping_address2", + "carts.id as order_id", + "carts.shipping_city", + "carts.shipping_state", + "carts.shipping_zipcode", + "carts.shipping_country", + "appointments.id as order_appointment_id" + + ) // Eager load the associated telemed pro + ->leftJoin("telemed_pros", "telemed_pros.id", "appointments.telemed_pros_id") + ->leftJoin("patients", "patients.id", "appointments.patient_id") + ->leftJoin("carts", "carts.appointment_id", "appointments.id") + //->where('appointments.appointment_date', '<', $appointments->appointment_date) + ->where("appointments.patient_id", $patient->id) + ->where("appointments.status", 'pending') + ->whereNull("appointments.start_time") + + ->orderBy('appointments.created_at', 'desc') + ->get(); + + if (!$appointments) + return response()->json(['error' => 'No Record found.'], 500); + + $timezone = config('app.timezone'); + if ($appointments->timezone) { + + $tz = new DateTimeZone($appointments->timezone); + + $standardTz = $tz->getName(); + $appointmentDateTime = $appointmentCurrent = Carbon::parse($appointments->appointment_date . ' ' . $appointments->appointment_time)->shiftTimezone($standardTz); + //$appointmentDateTime = $appointmentDateTime->shiftTimezone($timezone); + + $appointmentCurrent = Carbon::now($timezone); + + $diff = $appointmentDateTime->diff($appointmentCurrent); + + if ($diff->invert == 0) { + // Appointment is in future, increment count + $diff = $diff->format('0 days 0 hours 0 minutes 0 seconds'); + } else + + $diff = $diff->format('%a days %h hours %i minutes %s seconds'); + } else { + $diff = ""; + } + $filePath = public_path("assets/profiles/{$patient->id}.png"); + + if ($patient->profile_picture) + $patient->profile_picture = $this->url->to("storage/profile_pictures", $patient->profile_picture); + else + $patient->profile_picture = null; + + if (File::exists($filePath)) { + $patient->url = "/assets/profiles/{$patient->id}.png"; + } else { + $patient->url = null; + } + foreach ($upcoming_appointments as $upcoming_appointment) { + + if ($upcoming_appointment->timezone) { + + $tz = new DateTimeZone($upcoming_appointment->timezone); + + $standardTz = $tz->getName(); + $appointmentDateTime = $appointmentCurrent = Carbon::parse($upcoming_appointment->appointment_date . ' ' . $upcoming_appointment->appointment_time)->shiftTimezone($standardTz); + //$appointmentDateTime = $appointmentDateTime->shiftTimezone($timezone); + + $appointmentCurrent = Carbon::now($timezone); + + $diff = $appointmentDateTime->diff($appointmentCurrent); + + if ($diff->invert == 0) { + // Appointment is in future, increment count + $diff = $diff->format('0 days 0 hours 0 minutes 0 seconds'); + } else + + $diff = $diff->format('%a days %h hours %i minutes %s seconds'); + } else { + $diff = ""; + } + + $upcoming_appointment->items_data = $this->getOrderItems($upcoming_appointment->order_id); + $upcoming_appointment->time_diff = $diff; + } + + return response()->json(['upcoming_appointments' => $upcoming_appointments, 'appointment' => $appointments, 'time_diff' => $diff, 'patient' => $patient, "items_data" => $this->getOrderItems($appointments->order_id)], 200); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } +} diff --git a/app/Http/Controllers/Admin/Api/CalendlyController.php b/app/Http/Controllers/Admin/Api/CalendlyController.php new file mode 100644 index 0000000..34343ef --- /dev/null +++ b/app/Http/Controllers/Admin/Api/CalendlyController.php @@ -0,0 +1,110 @@ +authUrl(); + return response()->json(['url' => $url]); + } + public function getRedirectCode(Request $request) + { + $calendly = new Calendly(); + $calendly->authorize($request->input("code")); + return redirect("https://webmd-provider.codelfi.com/build/admin/dashboard"); + } + public function getEvent(Request $request) + { + $calendly = new Calendly(); + $events = $calendly->eventTypes(); + $final_event = []; + foreach ($events as $event) { + $array = []; + $array['slug'] = $event['slug']; + $array['uri'] = $event['uri']; + $array['type'] = $event['type']; + array_push($final_event, $array); + //$final_event[] += $array; + } + return response()->json(['message' => 'Admin has been authenticated.', 'events' => $final_event], 200); + } + public function setEvent(Request $request) + { + $uri = $request->input('url'); + $calendly = new Calendly(); + $calendly->setEventUri($uri); + return response()->json(['message' => 'Event URI selected.'], 200); + } + public function resetEventUri() + { + $calendly = new Calendly(); + $calendly->resetEventUri(); + return response()->json(['message' => 'Event URI reset!.'], 200); + } + + public function getAvailableDates(Request $request) + { + $setting = Setting::find(1); + $month = $request->input("month"); + $timezone = $request->input("timezone"); + + $calendly = new Calendly(); + + $slots = $calendly->getAvailableDates($setting->event_type, $month, $timezone); + + return response()->json(['slots' => $slots], 200); + } + public function generateHexString($length = 32) + { + return bin2hex(random_bytes($length / 2)); + } + public function generateRandomString($length = 37) + { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-'; + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + return $randomString; + } + + public function bookSchedule(Request $request) + { + $referel = $url = $request->input("url"); + $patient_email = $request->input("patient_email"); + $patient_name = $request->input("patient_name"); + $timezone = $request->input("timezone"); + $calendly = new Calendly(); + + $attempts = 0; + $maxAttempts = 3; + + while ($attempts < $maxAttempts) { + $response = $calendly->bookEvent($url, $patient_name, $patient_email, $timezone); + $response = json_decode($response, true); + + if ($response && isset($response["event"]['start_time'])) { + return response()->json(['success' => 'Event has been booked. ' . $response["event"]['start_time']], 200); + } + + $attempts++; + } + + return response()->json(['error' => 'Failed to complete the request after ' . $maxAttempts . ' attempts', 'response' => $response], 400); + } +} diff --git a/app/Http/Controllers/Admin/Api/CalendlyControllerOld.php b/app/Http/Controllers/Admin/Api/CalendlyControllerOld.php new file mode 100644 index 0000000..9447360 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/CalendlyControllerOld.php @@ -0,0 +1,366 @@ +clientId); + $url .= '&response_type=code'; + $url .= '&redirect_uri=' . urlencode($this->redirectUri); + + return response()->json(['url' => $url]); + } + + // Handle the redirect with authorization code and exchange for access token + public function getRedirectCode(Request $request) + { + // Get the authorization code from the request + $authorizationCode = $request->input('code'); + + if (!$authorizationCode) { + return response()->json(['error' => 'Authorization code is missing'], 400); + } + //return $this->getCalendlyUserAndAvailability(); + // Call method to fetch access token and cache it + $this->getAccessTokenFromCode($authorizationCode); + return response()->json(['message' => 'Admin has been authenticated.'], 200); + } + // Handle the redirect with authorization code and exchange for access token + public function getAvailabeSlotDates(Request $request) + { + return $this->getCalendlyUserAndAvailability(); + // Call method to fetch access token and cache it + } + + // Fetch or refresh access token if needed + public function getAccessToken() + { + // Check if the access token exists in cache + if (Cache::has('calendly_access_token')) { + return Cache::get('calendly_access_token'); + } + + // If no token is available, return error or trigger refresh + return response()->json(['error' => 'No valid access token. Please authenticate again.'], 401); + } + + // Exchange authorization code for access token and store it in cache + private function getAccessTokenFromCode($authorizationCode) + { + $tokenUrl = 'https://auth.calendly.com/oauth/token'; + + // Use GuzzleHttp client to make the POST request + $client = new Client(); + + try { + $response = $client->post($tokenUrl, [ + 'form_params' => [ + 'grant_type' => 'authorization_code', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'redirect_uri' => $this->redirectUri, + 'code' => $authorizationCode, + ], + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + ]); + + // Decode the JSON response + $data = json_decode($response->getBody()->getContents(), true); + + // Store access token and refresh token in cache with an expiration + //Cache::put('calendly_access_token', $data['access_token'], now()->addSeconds($data['expires_in'])); + //Cache::put('calendly_refresh_token', $data['refresh_token'], now()->addDays(30)); // Refresh tokens don't expire until used + + return response()->json([ + 'access_token' => $data['access_token'], + 'refresh_token' => $data['refresh_token'], + 'token_type' => $data['token_type'], + 'expires_in' => $data['expires_in'] + ]); + } catch (\Exception $e) { + // Handle errors + return response()->json(['error' => 'Failed to fetch access token: ' . $e->getMessage()], 500); + } + } + + // Use refresh token to get a new access token when expired + private function refreshAccessToken() + { + if (!Cache::has('calendly_refresh_token')) { + return response()->json(['error' => 'Refresh token not available.'], 401); + } + + $refreshToken = Cache::get('calendly_refresh_token'); + + $tokenUrl = 'https://auth.calendly.com/oauth/token'; + + $client = new Client(); + + try { + $response = $client->post($tokenUrl, [ + 'form_params' => [ + 'grant_type' => 'refresh_token', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'refresh_token' => $refreshToken, + ], + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + ]); + + // Decode the response + $data = json_decode($response->getBody()->getContents(), true); + + // Store the new access token and refresh token in cache + Cache::put('calendly_access_token', $data['access_token'], now()->addSeconds($data['expires_in'])); + Cache::put('calendly_refresh_token', $data['refresh_token'], now()->addDays(30)); // New refresh token + + return $data['access_token']; + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to refresh access token: ' . $e->getMessage()], 500); + } + } + + function getCalendlyUserAndAvailability() + { + // Get the access token from cache + $accessToken = Cache::get('calendly_access_token'); + + // If the token is not in cache, fetch a new one + if (!$accessToken) { + $accessToken = $this->fetchCalendlyAccessToken(); + } + + if (!$accessToken) { + return response()->json(['error' => 'Token Expired!'], 500); + } + + // 1. Call the /users/me API to get user information + $client = new Client(); + try { + $response = $client->request('GET', 'https://api.calendly.com/users/me', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + ] + ]); + + $data = json_decode($response->getBody(), true); + $userUri = $data['resource']['uri']; + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to fetch user details: ' . $e->getMessage()], 500); + } + + // 2. Use the user URI to fetch availability schedules + try { + $availabilityResponse = $client->request('GET', 'https://api.calendly.com/user_availability_schedules', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + ], + 'query' => [ + 'user' => $userUri + ] + ]); + + $availabilityData = json_decode($availabilityResponse->getBody(), true); + // Get the rules from the availability data + $rules = $availabilityData['collection'][0]['rules']; + + // Map day of the week to the date in the current week + $weekDates = $this->getCurrentWeekDates(); // Get this week's dates + + // Add the corresponding date to each rule + $updatedRules = array_map(function ($rule) use ($weekDates) { + $wday = $rule['wday']; + // Check if we have a corresponding date for this weekday + if (isset($weekDates[$wday])) { + $rule['date'] = $weekDates[$wday]; // Add date to the rule + } else { + $rule['date'] = null; // No availability on that day + } + return $rule; + }, $rules); + + // Add the updated rules with dates back to the response + $availabilityData['collection'][0]['rules'] = $updatedRules; // Get the rules from the availability data + $rules = $availabilityData['collection'][0]['rules']; + + // Map day of the week to the date in the current week + $weekDates = $this->getCurrentWeekDates(); // Get this week's dates + + // Add the corresponding date to each rule + $updatedRules = array_map(function ($rule) use ($weekDates) { + $wday = $rule['wday']; + // Check if we have a corresponding date for this weekday + if (isset($weekDates[$wday])) { + $rule['date'] = $weekDates[$wday]; // Add date to the rule + } else { + $rule['date'] = null; // No availability on that day + } + return $rule; + }, $rules); + + $filteredDates = array_values(array_filter(array_map(function ($rule) { + return !empty($rule['intervals']) ? $rule['date'] : null; + }, $updatedRules))); + // Add the updated rules with dates back to the response + $availabilityData['collection'][0]['rules'] = $updatedRules; + return response()->json($availabilityData, 200); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to fetch availability: ' . $e->getMessage()], 500); + } + } + private function getCurrentWeekDates() + { + $weekDates = []; + + // Start from Sunday as 0, loop through the week + for ($day = 0; $day < 7; $day++) { + $carbonDate = Carbon::now()->startOfWeek()->addDays($day); // Start on Sunday + $weekday = strtolower($carbonDate->format('l')); // Get the day name (sunday, monday, etc.) + $weekDates[$weekday] = $carbonDate->toDateString(); // Store the date for that day + } + + return $weekDates; // Return array of week dates + } + // Helper function to fetch access token + function fetchCalendlyAccessToken() + { + $client = new Client(); + try { + $response = $client->post('https://auth.calendly.com/oauth/token', [ + 'form_params' => [ + 'grant_type' => 'authorization_code', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'redirect_uri' => $this->redirectUri, + 'code' => request()->input('code'), // Get authorization code from request + ], + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ] + ]); + + $data = json_decode($response->getBody(), true); + $accessToken = $data['access_token']; + + // Store access token in cache for 2 hours + Cache::put('calendly_access_token', $accessToken, 120 * 60); + + return $accessToken; + } catch (\Exception $e) { + return null; + } + } + public function getAvailableTimes(Request $request) + { + try { + // Validate start_time and end_time input + /* $validatedData = $request->validate([ + 'start_time' => 'required|date_format:Y-m-d\TH:i:s\Z', + 'end_time' => 'required|date_format:Y-m-d\TH:i:s\Z', + ]); */ + + // Get the access token from cache + $accessToken = Cache::get('calendly_access_token'); + + // If the token is not in cache, fetch a new one + if (!$accessToken) { + $accessToken = $this->fetchCalendlyAccessToken(); + } + + if (!$accessToken) { + return response()->json(['error' => 'Unable to retrieve access token'], 500); + } + + // 1. Call the /users/me API to get user information + $client = new Client(); + $response = $client->request('GET', 'https://api.calendly.com/users/me', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + ] + ]); + + $data = json_decode($response->getBody(), true); + $userUri = $data['resource']['uri']; + + // 1. Call the /users/me API to get user information + $client = new Client(); + $responseEvent = $client->request('GET', 'https://api.calendly.com/event_types?user=' . $userUri, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + ] + ]); + + $dataEvent = json_decode($responseEvent->getBody(), true); + $even_type_url = $dataEvent['collection'][0]['uri']; + $userUri = $data['resource']['uri']; + + + $client = new Client(); + + // Prepare API endpoint with the required parameters + $eventTypeUrl = 'https://api.calendly.com/event_type_available_times'; + $queryParams = [ + 'event_type' => $even_type_url, //'https://api.calendly.com/event_types/60992c14-2f0b-42c2-af7b-95062d065600', // Use your event_type URL + 'start_time' => $request->input('start_time'), + 'end_time' => $request->input('end_time') + ]; + $str = "event_type=" . urlencode($queryParams['event_type']) . "&" . "start_time=" . urlencode($queryParams['start_time']) . "&" . "end_time=" . urlencode($queryParams['end_time']); + $eventTypeUrl = $eventTypeUrl . "?" . ($str); + // Send the request to Calendly + $response = $client->request('GET', $eventTypeUrl, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + ], + // 'query' => $queryParams + ]); + + $data = json_decode($response->getBody(), true); + $processedTimes = array_map(function ($item) { + $dateTime = new DateTime($item['start_time']); + $readableTime = $dateTime->format('Y-m-d H:i'); // Format: YYYY-MM-DD h:mm AM/PM + + return [ + 'status' => $item['status'], + 'start_time' => $readableTime, + 'invitees_remaining' => $item['invitees_remaining'], + 'scheduling_url' => $item['scheduling_url'] + ]; + }, $data['collection']); + + // Extract scheduling URLs from available time slots + $availableTimes = array_map(function ($slot) { + return $slot['scheduling_url']; + }, $data['collection']); + + return response()->json([ + 'available_times' => $processedTimes + ], 200); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to fetch available times: ' . $e->getMessage()], 500); + } + } +} diff --git a/app/Http/Controllers/Admin/Api/DashboardController.php b/app/Http/Controllers/Admin/Api/DashboardController.php new file mode 100644 index 0000000..ac0528b --- /dev/null +++ b/app/Http/Controllers/Admin/Api/DashboardController.php @@ -0,0 +1,352 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function getStats() + { + + + $date = Carbon::now(); + $startOfWeek = $date->startOfWeek(Carbon::MONDAY)->format("Y-m-d"); + $endDate = Carbon::now()->format('Y-m-d'); + $newPatients = self::getNewPatients($startOfWeek, $endDate); + $newProviders = self::getNewProviders($newPatients, $endDate); + $analytics = self::getAnalytics(); + $upcomingMeetings = Appointment::where('appointment_date', '>=', $date->toDateString())->count(); + return response()->json([ + 'upcoming_meetings' => $upcomingMeetings, + 'new_customers' => $newPatients, + 'new_providers' => $newProviders, + 'analytics' => $analytics + ]); + } + protected function getNewPatients($newPatients, $endOfWeek) + { + $upcomingMeetings = Patient::where('created_at', '>=', $newPatients) + ->where('created_at', '<=', $endOfWeek) + ->count(); + return $upcomingMeetings; + } + protected function getNewProviders($newPatients, $endOfWeek) + { + $upcomingMeetings = Telemedpro::where('created_at', '>=', $newPatients) + ->where('created_at', '<=', $endOfWeek) + ->count(); + return $upcomingMeetings; + } + protected function getAnalytics($filter = '12_months') + { + $currentMonth = Carbon::now(); + + // Filter logic + switch ($filter) { + case 'current_month': + $startDate = $currentMonth->copy()->startOfMonth(); + break; + case '1_month': + $startDate = $currentMonth->copy()->subMonth()->startOfMonth(); + break; + case '2_months': + $startDate = $currentMonth->copy()->subMonths(2)->startOfMonth(); + break; + case '3_months': + $startDate = $currentMonth->copy()->subMonths(3)->startOfMonth(); + break; + case '6_months': + $startDate = $currentMonth->copy()->subMonths(6)->startOfMonth(); + break; + default: // Default to 12 months + $startDate = $currentMonth->copy()->subMonths(12)->startOfMonth(); + } + + $endDate = $currentMonth->endOfMonth(); + + + $appointments = Appointment::with('patient') + ->whereBetween('created_at', [$startDate, $endDate]) + ->get(); + + $totalSessions = $appointments->count(); + $totalCallTime = 10; // Assuming you have some logic to calculate this + if ($totalSessions != 0) { + $avgSessionTime = $totalCallTime / $totalSessions; + $avgSessionTime = round(($avgSessionTime / 60), 2); + } else + $avgSessionTime = ''; + + + $monthlyData = []; + + // Loop through each month in the last 12 months + for ($date = $startDate->copy(); $date->lte($endDate); $date->addMonth()) { + $monthStart = $date->startOfMonth()->format('Y-m-d'); + $monthEnd = $date->copy()->endOfMonth()->format('Y-m-d'); // Key change here! + + $monthAppointments = Appointment::with('patient') + ->whereBetween('created_at', [$monthStart, $monthEnd]) + ->get(); + + + // Calculate any metrics you need from $monthAppointments + $monthlyData[] = [ + 'month' => $date->format('M'), // Example: Jan 2024 + 'appointment_count' => $monthAppointments->count() + // Add other metrics as needed + ]; + } + $monthsList = []; + $monthlySessionCount = []; + + foreach ($monthlyData as $dataPoint) { + $monthsList[] = $dataPoint['month']; + $monthlySessionCount[] = $dataPoint['appointment_count']; + } + + + return [ + // 'total_sessions' => $totalSessions, + 'total_call_time' => $totalCallTime, + // 'avg_session_time' => $avgSessionTime, + 'data' => array_values($monthlySessionCount), + 'months_list' => $monthsList, + ]; + } + public function getAdminDetails(Request $request) + { + $user = Auth::guard('admin')->user(); + $permissionManager = new Permissions($user->role->permissions); + $permissions = $permissionManager->permissionsApi(); + if (isset($user->image_path)) + $user->image_path = $this->url->to('/storage/profile_pictures/' . $user->image_path); + else + $user->image_path = Null; + return response()->json([ + 'admin_details' => $user, + 'permissions'=>$permissions + ]); + } + public function updateAdminDetails(Request $request) + { + $userId = Auth::guard('admin')->user()->id; + $user = Admin::find($userId); + + if($request->get('image')) + { + $image = $request->get('image'); + $fileName = 'profile-' . time(); + + $logo = base64_decode($image); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + + $imageName = $fileName . '.' . $ext; + Storage::disk('local')->put("/public/profile_pictures/" . $imageName, $logo); + } + + $user->name = $request->get('first_name'); + $user->last_name = $request->get('last_name'); + $user->phone_no = $request->get('phone_no'); + if ($request->get('image')) + $user->image_path = $imageName; + $user->save(); + return response()->json([ + 'admin_details' => $user + ]); + } + public function uploadImage($image, $fileName, $path) + { + $logo = base64_decode($image); + $filename = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[0]); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + $imageName = $fileName . '.' . $ext; + $path = $path . $imageName; + file_put_contents($path, $logo); + return $imageName; + } + public function index(Request $request){ + + try{ + $this->authorizeForUser($this->user,'DashboardData', new Admin); + $start_date = $request->get('start_date'); + $end_date = $request->get('end_date'); + $totalPatients = $this->getTotals($start_date,$end_date); + $graphData = $this->graphData($start_date,$end_date); + $patientActivity = $this->patientActivit($start_date,$end_date); + $ordersData = $this->ordersData($start_date,$end_date); + $completedMeetings = $this->completedMeetings($start_date,$end_date); + $orders = $this->productsData($start_date,$end_date); + return response()->json([ + 'totals' =>$totalPatients, + 'graph_data'=>$graphData, + 'patient_reg_activity'=>$patientActivity, + 'orders_data'=>$ordersData, + 'completed_meetings'=>$completedMeetings, + 'products'=>$orders + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function getTotals($start_date,$end_date) + { + $totalPatients = Patient:: + where('created_at', '>=', $start_date." 00:00:00") + ->where('created_at', '<=', $end_date." 23:59:59") + ->count(); + $totalOrders = Cart::select( + DB::raw("count(carts.id) as total_sales"), + DB::raw("sum(carts.total_amount ) as sales_amount"), + DB::raw("count(items.id) as products_sold") + ) + ->leftJoin('items','items.cart_id','carts.id') + ->where('carts.created_at', '>=', $start_date." 00:00:00") + ->where('carts.created_at', '<=', $end_date." 23:59:59") + ->where('carts.status','completed') + ->first(); + + return [ + 'total_patints'=>$totalPatients, + 'total_orders'=>$totalOrders->total_sales, + 'total_amount'=>$totalOrders->sales_amount, + 'total_products_sold'=>$totalOrders->products_sold + ]; + } + public function graphData($start_date,$end_date) + { + $dates = []; + $sales =[]; + $totalMeetingsData =[]; + $startDate = Carbon::parse($start_date); + $endDate = Carbon::parse($end_date); + for ($date = $startDate; $date->lte($endDate); $date->addDay()) { + //get total sales data + $values = Cart::select( + DB::raw('DATE(created_at) as date'), + DB::raw("SUM(case when carts.status = 'completed' then carts.total_amount else 0 end) as amount")) + ->where('carts.created_at', '>=', $date->format("Y-m-d")." 00:00:00") + ->where('carts.created_at', '<=', $date->format("Y-m-d")." 23:59:59") + ->groupBy(DB::raw('DATE(created_at)')); + $graphsValues = $values->first(); + // get total meetings + $totalMeetings = Appointment:: + where('start_time', '>=', $date->format("Y-m-d")." 00:00:00") + ->where('start_time', '<=', $date->format("Y-m-d")." 23:59:59") + ->where('status', 'completed') + ->count(); + $dates[] = $date->format("M d/y"); + if($graphsValues) + $sales[] = round($graphsValues->amount,2); + else + $sales[] = 0.00; + + $totalMeetingsData[] = round($totalMeetings,2); + } + return [ + 'dates'=>$dates, + 'data'=> + [ + 'total_sales'=>$sales, + 'total_meetings'=>$totalMeetingsData + ] + ]; + } + public function patientActivit($start_date,$end_date){ + $patientActivity = PatientRegActivity:: + where('created_at', '>=', $start_date." 00:00:00") + ->where('created_at', '<=', $end_date." 23:59:59") + ->get(); + + $activity = $patientActivity->map(function ($query,$key){ + $patient = Patient::find($query->patient_id); + if($query->activity=='patient_registered') + { + $query->activity = $patient->first_name. " ". $patient->last_name. " Singed Up"; + } + if($query->activity=='patient_appointment_booked') + { + $query->activity = $patient->first_name. " ". $patient->last_name. " Booked an appointment "; + } + return $query; + + }); + + return $patientActivity; + } + public function ordersData($start_date,$end_date){ + return Cart::select('carts.id as order_id','carts.total_amount as amount', + DB::raw("CONCAT(first_name,' ',last_name) as patient_name"), + 'created_at as date') + ->where('created_at', '>=', $start_date." 00:00:00") + ->where('created_at', '<=', $end_date." 23:59:59") + ->get(); + } + public function completedMeetings($start_date,$end_date){ + return Appointment::select( + 'appointments.patient_id', + 'appointments.appointment_time', + 'appointments.appointment_date', + 'appointments.start_time', + 'appointments.end_time', + 'appointments.timezone', + 'telemed_pros.name as provider_name', + 'telemed_pros_id as provider_id', + 'carts.id as order_id', + 'appointments.patient_name' + ) + ->Join('telemed_pros', 'telemed_pros.id', 'appointments.telemed_pros_id') + ->Join("carts", "appointments.id","carts.appointment_id") + ->where('appointments.status', "completed") + ->where('appointments.end_time', '>=', $start_date." 00:00:00") + ->where('appointments.end_time', '<=', $end_date." 23:59:59") + ->get(); + } + public function productsData($start_date,$end_date) + { + return Item::select( + DB::raw("sum(case when items.status='delivered' then items.quantity else 0 end) as total_orders"), + DB::raw("sum(case when items.status='delivered' then (items.quantity*plans_v1.price) else 0 end) as total_amount"), + 'plans_v1.title as product_name', + 'items.plans_id as product_id' + ) + ->leftJoin('plans_v1','plans_v1.id','items.plans_id') + ->where('items.created_at', '>=', $start_date." 00:00:00") + ->where('items.created_at', '<=', $end_date." 23:59:59") + ->groupby('plans_v1.title', 'items.plans_id') + ->get(); + // dd(Constant::getFullSql($products)); + } +} diff --git a/app/Http/Controllers/Admin/Api/HomeController.php b/app/Http/Controllers/Admin/Api/HomeController.php new file mode 100644 index 0000000..dbb942d --- /dev/null +++ b/app/Http/Controllers/Admin/Api/HomeController.php @@ -0,0 +1,415 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function updateAdminProfile(Admin $admin, Request $request) + { + try { + $this->authorizeForUser($this->user, 'edit', new Admin); + $admin->update($request->all()); + return response()->json([ + 'message' => 'Admin updated successfully', + 'telemed' => $admin + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + + + + public function product(PlanV1 $product) + { + return response()->json([ + 'product' => $product + + ]); + } + + + + + + + public function labsList() + { + $labs = Lab::all(); + return response()->json([ + 'patients' => $labs + ]); + } + public function labs(Lab $lab) + { + return response()->json([ + 'patient' => $lab + ]); + } + public function labsDelete(Lab $lab) + { + $lab->delete(); + return response()->json([ + 'message' => "Deleted Successfully" + ]); + } + public function labsUpdate(Lab $lab, Request $request) + { + $lab->update($request->all()); + + return response()->json([ + 'message' => 'Lab updated successfully', + 'telemed' => $lab + ]); + } + + public function getQuestionBuilderStore(Patient $patient, Request $request) + { + + $questionBuilder = QuestionBuilder::select('key', 'value')->where("customer_id", $patient->id)->get(); + $jsonData = $questionBuilder->mapWithKeys(function ($item) { + return [$item->key => $item->value]; + }); + // Store data + return response()->json([ + 'message' => 'Data Sent', + 'data' => $jsonData + ], 200); + } + public function getProducts() + { + return response()->json([ + 'data' => PlanV1::select('plans_v1.*')->get() + ], 200); + } + public function storeOrderData(LabKit $labkit, Patient $patient, Request $request) + { + $user = $patient; + $cart = new Cart(); + $cart->lab_kit_id = $labkit->id; + $cart->first_name = $request->first_name; + $cart->last_name = $request->last_name; + /* $cart->appointment_id = $request->appointment_id; */ + $cart->email = $request->email; + $cart->phone = $request->phone; + $cart->status = "pending"; + $cart->prescription_status = "pending"; + + + + $cart->date_of_birth = $request->date_of_birth ?? null; + + $cart->patient_id = $user->id; + + $cart->shipping_address1 = $request->shipping_address1; + $cart->shipping_address2 = $request->shipping_address2; + $cart->shipping_city = $request->shipping_city; + $cart->shipping_state = $request->shipping_state; + $cart->shipping_zipcode = $request->shipping_zipcode; + $cart->shipping_country = $request->shipping_country; + + $cart->billing_address1 = $request->billing_address1; + $cart->billing_address2 = $request->billing_address2; + $cart->billing_city = $request->billing_city; + $cart->billing_state = $request->billing_state; + $cart->billing_zipcode = $request->billing_zipcode; + $cart->billing_country = $request->billing_country; + $cart->short_description = "Your order has been placed successfully"; + $cart->shipping_amount = $request->shipping_amount; + $cart->total_amount = $request->total_amount; + + $cart->save(); + + if ($request->has('items')) { + foreach ($request->items as $itemData) { + $item = new Item(); + $item->plans_id = $itemData['plans_id']; + $item->quantity = $itemData['quantity']; + + + $item->status = "pending"; + $item->labkit_delivery_status = "pending"; + $item->cart_id = $cart->id; + $item->save(); + + $itemHistory = new ItemHistory(); + $itemHistory->note = "Order was placed (Order ID: #" . $cart->id . ")"; + $itemHistory->short_description = "Your order has been placed successfully"; + $itemHistory->cart_id = $cart->id; + $itemHistory->status = "pending"; + $itemHistory->item_id = $item->id; + if (isset($itemData['subscription']) && $itemData['subscription'] == true && $itemData['onetime'] == false) { + $subscription = new Subscription(); + $subscription->subscription_start_date = Carbon::now(); + $subscription->subscription_renewal_date = Carbon::now()->addDays(30); + $subscription->subscription_status = "Active"; + $subscription->cart_id = $cart->id; + /* $subscription->status = "active"; */ + + $subscription->item_id = $item->id; + $subscription->patient_id = $user->id; + + $subscription->save(); + } + + $itemHistory->save(); + + $plan = PlanV1::find($itemData['plans_id']); + if ($plan->is_prescription_required == true) + $labkitOrderItem = LabkitOrderItem::create([ + 'cart_id' => $cart->id, + 'item_id' => $item->id, + 'lab_kit_id' => 1, + /* 'result' => $request['result'], */ + 'status' => "Ordered", + ]); + } + } + return response()->json(['status' => 'Success', 'cart' => $cart], 200); + } + public function editOrderData(Cart $cart, Request $request) + { + // Validate the request data + $validatedData = $request->validate([ + 'first_name' => 'sometimes|string|max:255', + 'last_name' => 'sometimes|string|max:255', + 'email' => 'sometimes|email|max:255', + 'phone' => 'sometimes|string|max:20', + 'date_of_birth' => 'sometimes|date|nullable', + 'shipping_address1' => 'sometimes|string|max:255', + 'shipping_address2' => 'sometimes|string|max:255|nullable', + 'shipping_city' => 'sometimes|string|max:255', + 'shipping_state' => 'sometimes|string|max:255', + 'shipping_zipcode' => 'sometimes|string|max:20', + 'shipping_country' => 'sometimes|string|max:255', + 'patient_id' => 'sometimes', + /* 'billing_address1' => 'sometimes|string|max:255', + 'billing_address2' => 'sometimes|string|max:255|nullable', + 'billing_city' => 'sometimes|string|max:255', + 'billing_state' => 'sometimes|string|max:255', + 'billing_zipcode' => 'sometimes|string|max:20', + 'billing_country' => 'sometimes|string|max:255', */ + 'shipping_amount' => 'sometimes|numeric', + 'total_amount' => 'sometimes|numeric', + 'items' => 'sometimes|array', + /* 'items.*.plans_id' => 'required_with:items|exists:plans,id', + 'items.*.quantity' => 'required_with:items|integer|min:1', + 'items.*.subscription' => 'sometimes|boolean', + 'items.*.onetime' => 'sometimes|boolean', */ + ]); + + // Update the cart with validated data + $cart->fill($validatedData); + $cart->save(); + + // Update or create items + if ($request->has('items')) { + foreach ($request->items as $itemData) { + $item = Item::updateOrCreate( + ['cart_id' => $cart->id, 'plans_id' => $itemData['plans_id']], + [ + 'quantity' => $itemData['quantity'], + 'status' => 'pending', + 'labkit_delivery_status' => 'pending', + ] + ); + + // Update or create subscription + if (isset($itemData['subscription']) && $itemData['subscription'] == true && $itemData['onetime'] == false) { + Subscription::updateOrCreate( + ['cart_id' => $cart->id, 'item_id' => $item->id], + [ + 'subscription_start_date' => $item->created_at, + 'subscription_renewal_date' => $item->created_at->addDays(30), + 'subscription_status' => 'Active', + 'patient_id' => $cart->patient_id, + ] + ); + } else { + // Remove subscription if it exists and is no longer needed + Subscription::where('cart_id', $cart->id) + ->where('item_id', $item->id) + ->delete(); + } + + // Update or create LabkitOrderItem + $plan = PlanV1::find($itemData['plans_id']); + if ($plan->is_prescription_required) { + LabkitOrderItem::updateOrCreate( + ['cart_id' => $cart->id, 'item_id' => $item->id], + [ + 'lab_kit_id' => 1, + 'status' => 'Ordered', + ] + ); + } + } + } + + // Remove items that are no longer in the request + /* if ($request->has('items')) { + $currentItemIds = collect($request->items)->pluck('plans_id')->toArray(); + ItemHistory::where('cart_id', $cart->id) + //->whereNotIn('plans_id', $currentItemIds) + ->delete(); + Item::where('cart_id', $cart->id) + ->whereNotIn('plans_id', $currentItemIds) + ->delete(); + } */ + + // Refresh the cart to get the updated data + $cart->refresh(); + + return response()->json(['status' => 'Success', 'cart' => $cart], 200); + } + + + public function getPrescription() + { + $prescriptions = Prescription::query(); + + return DataTables::of($prescriptions)->make(true); + } + public function processPayment(Patient $patient, Request $request) + { + //event(new PaymentProcessed($patient)); + return response()->json(['status' => 'Success'], 200); + } + + + public function questionBuilderStore(Patient $patient, $category, Request $request) + { + $data = $request->all(); + + $questionBuilderData = []; + $category = ProfileCategory::where("category_link", $category)->first(); + if (!$category) + return response()->json([ + 'message' => 'Invalid Category Link', + 'data' => '' + ], 200); + + foreach ($data as $key => $value) { + if (is_array($value)) { + $value = serialize($value); + } + if (!empty($value)) { + $questionBuilderData[] = [ + 'key' => $key, + 'value' => $value, + 'profile_category_id' => $category->id, + 'customer_id' => $patient->id + ]; + } + } + // dd($questionBuilderData); + $questionBuilder = QuestionBuilder::insert($questionBuilderData); + + $questionBuilder = QuestionBuilder::select('key', 'value')->get(); + + // Convert the data to a key-value JSON format + $jsonData = $questionBuilder->mapWithKeys(function ($item) { + return [$item->key => $item->value]; + }); + // Store data+ + return response()->json([ + 'message' => 'Data Inserted', + 'data' => $jsonData + ], 200); + } + public function getMedicalHistoryQuestion(Patient $patient, Request $request) + { + $answers = MedicalHistoryAnswer::where('patient_id', $patient->id)->get(); + + return response()->json([ + 'status' => 'Success', + 'answers' => $answers + ], 200); + } + public function postMedicalHistoryQuestion(Patient $patient, Request $request) + { + + foreach ($request->answers as $answer) { + $existing = MedicalHistoryAnswer::where("patient_id", $patient->id)->where('question_key', $answer['question_key'])->first(); + + if ($existing) { + $existing->answer = $answer['answer']; + $existing->patient_id = $patient->id; + $existing->type = $answer['type']; + $existing->save(); + } else { + $newAnswer = new MedicalHistoryAnswer(); + $newAnswer->question_key = $answer['question_key']; + $newAnswer->patient_id = $patient->id; + $newAnswer->answer = $answer['answer']; + $newAnswer->type = $answer['type']; + $newAnswer->save(); + } + } + + PatientRegActivity::create([ + 'patient_id' => $patient->id, + 'activity' => 'patient_medical_question_entered' + ]); + + return response()->json(['status' => 'Success'], 200); + } +} diff --git a/app/Http/Controllers/Admin/Api/LoginController.php b/app/Http/Controllers/Admin/Api/LoginController.php new file mode 100644 index 0000000..ef557f7 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/LoginController.php @@ -0,0 +1,66 @@ +only('email', 'password'); + + // Validate the request + $validator = Validator::make($credentials, [ + 'email' => 'required|email', + 'password' => 'required|string', + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + // Find the user by email + $admin = Admin::where('email', $credentials['email'])->first(); + + if (!$admin || !Auth::guard('admin')->validate($credentials)) { + return response()->json(['error' => 'Invalid email or password'], 401); + } + + // Gen$admin->role;erate the JWT token + $token = JWTAuth::fromUser($admin); + $permissionManager = new Permissions($admin->role->permissions); + $permissions = $permissionManager->permissionsApi(); + + // Construct user data + $userData = [ + 'id' => $admin->id, + 'fullName' => $admin->name, // Assuming 'name' field contains the full name + 'username' => $admin->username, // Assuming you have a 'username' field + 'avatar' => '/images/avatars/avatar-1.png', // Static for example; replace with dynamic if available + 'email' => $admin->email, + 'role' => strtolower($admin->role->role_name), // Assuming the role is 'admin', + + ]; + // Construct the response + return response()->json([ + 'userAbilityRules' => [ + [ + 'action' => 'manage', + 'subject' =>'all' + ] + ], + 'accessToken' => $token, + 'userData' => $userData, + 'permissions'=>$permissions + ]); + } + + +} diff --git a/app/Http/Controllers/Admin/Api/MedicineController.php b/app/Http/Controllers/Admin/Api/MedicineController.php new file mode 100644 index 0000000..b68bfc4 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/MedicineController.php @@ -0,0 +1,240 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function getMedList() + { + try{ + $this->authorizeForUser($this->user,'list', new PlanV1); + $medicines = PlanV1::query(); + return Datatables::of($medicines) + ->addColumn('image_url', function ($med) { + return URL::to("product/" . $med->image_url); + }) + ->toJson(); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function SaveMed(Request $request) + { + try{ + $this->authorizeForUser($this->user,'add', new PlanV1); + $slug = self::createSlug($request->get('slug')); + if ($request->get('image')) { + //upload website logo + $fileName = $slug; + $filePath = public_path() . '/product/'; + $fileName = $this->uploadImage($request->get('image'), $fileName, $filePath); + //////////////// + } + + PlanV1::create([ + 'title' => $request->get('title'), + 'currency' => $request->get('currency'), + 'price' => $request->get('price'), + 'list_one_title' => $request->get('list_one_title'), + 'list_two_title' => $request->get('list_two_title'), + 'list_sub_title' => $request->get('list_sub_title'), + 'image_url' => $fileName, + 'slug' => $slug, + 'domain' => $request->get('domain'), + 'product_file_path' => null + ]); + return response()->json([ + 'message' => "success" + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function uploadImage($image, $fileName, $path) + { + try{ + $this->authorizeForUser($this->user,'edit', new PlanV1); + $logo = base64_decode($image); + $filename = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[0]); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + $imageName = $fileName . '.' . $ext; + $path = $path . $imageName; + file_put_contents($path, $logo); + return $imageName; + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function EditMed($id, Request $request) + { + try{ + $this->authorizeForUser($this->user,'edit', new PlanV1); + $medicine = PlanV1::find($id); + $slug = self::createSlug($request->get('slug')); + $fileName = null; + if ($request->get('image')) { + //upload website logo + $fileName = $slug; + $filePath = public_path() . '/product/'; + $fileName = $this->uploadImage($request->get('image'), $fileName, $filePath); + //////////////// + } + $medicine->title = $request->get('title'); + $medicine->currency = $request->get('currency'); + $medicine->price = $request->get('price'); + $medicine->list_one_title = $request->get('list_one_title'); + $medicine->list_two_title = $request->get('list_two_title'); + $medicine->list_sub_title = $request->get('list_sub_title'); + $medicine->image_url = $fileName; + $medicine->slug = $slug; + $medicine->domain = $request->get('domain'); + $medicine->save(); + return response()->json([ + 'message' => "success" + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function DeleteMed($id, Request $request) + { + try{ + $this->authorizeForUser($this->user,'edit', new PlanV1); + $medicine = PlanV1::where("id", $id)->delete(); + return response()->json([ + 'message' => "success" + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + private function createSlug($string) + { + $string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens. + + return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars. + } + private function storeFile($file, $destinationPath) + { + //Display File Name + $file->getClientOriginalName(); + $file->getClientOriginalExtension(); + $file->getRealPath(); + $file->getSize(); + $file->getMimeType(); + //Move Uploaded File + + $file->move($destinationPath, $file->getClientOriginalName() . '.' . $file->getClientOriginalExtension()); + } + public function getFileList() + { + $files = PlanV1::select('product_file_path')->groupBy('product_file_path')->get(); + return response()->json([ + 'medicines' => $files + ]); + } + public function updateStatusPatientPrescription(PatientPrescription $PatientPrescription, Request $request) + { + $PatientPrescription->status = $request->input("status"); + $PatientPrescription->save(); + return response()->json([ + 'status' => 'updated to ' . $request->input("status") + ]); + } + public function updateStatusLabkit(Cart $cart, Request $request) + { + try{ + $this->authorizeForUser($this->user,'edit', new LabKit); + $cart->status = $request->input("status"); + $cart->save(); + return response()->json([ + 'status' => 'updated to ' . $request->input("status") + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function labkitList() + { + try{ + $this->authorizeForUser($this->user,'edit', new LabKit); + $labkit = LabKit::all(); + return response()->json([ + 'labkit' => $labkit + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function labsKitDelete(LabKit $labkit) + { + try{ + $this->authorizeForUser($this->user,'delete', new LabKit); + $labkit->delete(); + return response()->json([ + 'message' => "Deleted Successfully" + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function labskitUpdate(LabKit $labkit, Request $request) + { + try{ + $this->authorizeForUser($this->user,'edit', new LabKit); + $labkit->update($request->all()); + return response()->json([ + 'message' => 'Labkit updated successfully', + 'telemed' => $labkit + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function labskitCreate(LabKit $labkit, Request $request) + { + try{ + $this->authorizeForUser($this->user,'add', new LabKit); + $labkit->create($request->all()); + return response()->json([ + 'message' => 'Labkit created successfully', + 'telemed' => $labkit + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function getPatientLabKitOrders(Patient $patient, Request $request) + { + $cart = Cart::with("patient")->where("patient_id", $patient->id)->get(); + return response()->json(['cart' => $cart], 200); + } +} diff --git a/app/Http/Controllers/Admin/Api/OrderController.php b/app/Http/Controllers/Admin/Api/OrderController.php new file mode 100644 index 0000000..45bd289 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/OrderController.php @@ -0,0 +1,538 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function orderList(Request $request) + { + try { + $this->authorizeForUser($this->user, 'view', new Cart); + $fromDate = $request->get('from_date'); + $toDate = $request->get('to_date'); + $status = $request->get('status'); + $orderList = Cart::select( + "appointments.*", + 'appointments.id as appointment_id', + 'carts.*', + 'carts.id as order_id', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name") + ) + ->leftJoin('appointments', 'appointments.id', 'carts.appointment_id'); + + + if ($fromDate != "all") { + $from_date = Carbon::createFromFormat('m-d-Y', $fromDate)->format('Y-m-d'); + $orderList->where('carts.created_at', ">=", $from_date . " 00:00:00"); + } + if ($toDate != "all") { + $to_date = Carbon::createFromFormat('m-d-Y', $toDate)->format('Y-m-d'); + $orderList->where('carts.created_at', "<=", $to_date . " 23:59:59"); + } + if ($status != "all") { + $orderList->where('carts.status', $status); + } + // dd(Constant::getFullSql($orderList)); + return Datatables::of($orderList) + ->addColumn('order_total_amount', function ($order) { + $items = Item::where('cart_id', $order->id)->get(); + return $items->sum(function ($item) { + return $item->quantity * $item->price; + }); + }) + ->addColumn('order_total_shipping', function ($order) { + $items = Item::where('cart_id', $order->id)->get(); + return $items->sum('shipping_cost'); + }) + ->addColumn('appointment_status', function ($order) { + $appointment = Appointment::find($order->appointment_id); + return $appointment ? $appointment->status : 'null'; + }) + ->addColumn('total_items', function ($order) { + return Item::where('cart_id', $order->id)->sum('quantity'); + }) + ->addColumn('order_items', function ($order) { + $items = Item::with('plansV1') + ->where('cart_id', $order->id) + ->get() + ->map(function ($item) { + $planV1 = $item->plansV1; + if ($planV1) { + $planV1->qty = $item->quantity; + $planV1->status = $item->status; + } + return $planV1; + }); + return $items; + }) + ->make(true); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function orderListbyPatient(Patient $patient, Request $request) + { + + $fromDate = $request->get('from_date'); + $toDate = $request->get('to_date'); + $orderList = Cart::where('carts.patient_id', $patient->id); + if ($fromDate != "") { + $from_date = Carbon::createFromFormat('m-d-Y', $fromDate)->format('Y-m-d'); + $orderList->where('created_at', ">=", $from_date . " 00:00:00"); + } + if ($toDate != "") { + $to_date = Carbon::createFromFormat('m-d-Y', $toDate)->format('Y-m-d'); + $orderList->where('created_at', "<=", $to_date . " 23:59:59"); + } + + $orderListData = $orderList->get(); + $totalPrice = 0; + $totalShippingCost = 0; + foreach ($orderListData as $order) { + $totalPrice = 0; + $total_products = 0; + $quantity = []; + $totalShippingCost = 0; + $order->order_total_amount = $totalPrice; + $order->order_total_shipping = $totalShippingCost; + $items = Item::leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('cart_id', $order->id) + ->get(); + //$order->appointment_status = Appointment::where('id', $order->appointment_id)->first()->status; + + $orderItems = []; + foreach ($items as $item) { + array_push($orderItems, $item->plansV1); + $totalShippingCost += $item->shipping_cost; + $item->total_price = $item->quantity * $item->price; + $totalPrice += $item->total_price; + $order->order_total_amount = $totalPrice; + $order->order_total_shipping = $totalShippingCost; + $item->plansV1->qty = $item->quantity; + } + + $order->total_items = $total_products; + $order->order_items = $orderItems; + } + return response() + ->json([ + 'order_data' => $orderListData + ]); + } + public function orderDetails($id) + { + try { + $this->authorizeForUser($this->user, 'details', new Cart); + + $orderItems = $this->getOrderItems($id); + $orderDetails = Cart::find($id); + $items = Item::where('cart_id', $orderDetails->id)->get(); + $appointments = Appointment::select( + 'appointments.*', + 'telemed_pros.name as provider_name', + 'telemed_pros.email as provider_email', + 'telemed_pros.phone_number as provider_phone', + 'carts.total_amount', + 'carts.shipping_amount' + ) + ->leftJoin('telemed_pros', 'telemed_pros.id', 'appointments.telemed_pros_id') + ->leftJoin('carts', 'carts.appointment_id', 'appointments.id') + + ->where('appointments.id', $orderDetails->appointment_id) + ->first(); + if (Gate::forUser($this->user)->allows('prescriptions', new Cart)) { + $prescription = PatientPrescription::select( + 'patient_prescription.id as patient_prescription_id', + 'patient_prescription.id', + 'patient_prescription.created_by_id', + 'patient_prescription.created_by_type', + 'patient_prescription.direction_quantity', + 'patient_prescription.refill_quantity', + 'patient_prescription.dosage', + 'patient_prescription.status', + 'patient_prescription.direction_one', + 'patient_prescription.direction_two', + 'patient_prescription.dont_substitute', + 'patient_prescription.comments', + 'patient_prescription.brand', + 'patient_prescription.from', + 'patient_prescription.quantity', + 'patient_prescription.created_at as prescription_date', + 'prescriptions.name as prescription_name', + 'patient_prescription.prescription_id', + 'telemed_pros.name as provide_name', + 'telemed_pros.id as provider_id', + ) + ->where("appointment_id", $orderDetails->appointment_id) + ->leftJoin('appointments', 'appointments.id', 'patient_prescription.appointment_id') + ->leftJoin('prescriptions', 'prescriptions.id', 'patient_prescription.prescription_id') + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', 'telemed_pros.id') + ->get(); + } else { + $prescription = ['error' => "Access Denied!"]; + } + if (Gate::forUser($this->user)->allows('detail_notes', new Cart)) { + $patientNotes = PatientNote::where("appointment_id", $orderDetails->appointment_id)->get(); + } else { + $patientNotes = ['error' => "Access Denied!"]; + } + + if ($appointments) + $appointments->provider_id = $appointments->telemed_pros_id; + $patient = $orderDetails->patient; + $patient->profile_picture = $this->url->to("storage/profile_pictures/" . $patient->profile_picture); + + return response() + ->json([ + 'order_details' => $orderDetails, + 'order_items' => $orderItems, + 'patient_details' => $patient, + 'appointment_details' => $appointments, + 'items_activity' => $this->getShippingActivity($id), + 'appointment_notes' => $patientNotes, + 'prescription' => $prescription + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function getOrderItems($id) + { + $items = Item::select('plans_v1.*', 'items.*', 'items.id as item_id', 'plans_v1.id as plans_id') + ->leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('cart_id', $id) + ->get(); + + $totalPrice = 0; + $totalShippingCost = 0; + $total_products = 0; + + $itemsWithFlags = $items->map(function ($item) { + $subscription = Subscription::where('item_id', $item->item_id)->first(); + + $item->subscription = $subscription ? true : false; + $item->onetime = $subscription ? true : false; + + $item->total_price = $item->quantity * $item->price; + $item->image_url = $this->url->to("product/" . $item->image_url); + + return $item; + }); + + foreach ($itemsWithFlags as $item) { + $totalShippingCost += $item->shipping_cost; + $totalPrice += $item->total_price; + $total_products += $item->quantity; + } + + return [ + 'items' => $itemsWithFlags, + 'total_amount' => $totalPrice, + 'total_shipping_cost' => $totalShippingCost, + 'total_products' => $total_products, + 'total' => $totalPrice + $totalShippingCost + ]; + } + public function getShippingActivity($id) + { + $itemsHistory = ItemHistory::select('items_history.*', 'plans_v1.title as item_name') + ->where('items_history.cart_id', $id) + ->leftJoin('items', 'items.id', 'items_history.item_id') + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->get(); + return $itemsHistory; + } + public function getPaymentDetail($id) + { + + $orderDetails = Cart::find($id); + $payment = Payment::where('order_id', $orderDetails->id)->first(); + + return response() + ->json([ + 'payment' => $payment + ]); + } + public function labkitOrderItemGet(Request $request) + { + $labkitOrderItems = LabkitOrderItem::where('labkit_order_items.cart_id', $request->input('cart_id')) + ->leftJoin( + 'lab_kit', + 'labkit_order_items.lab_kit_id', + '=', + 'lab_kit.id' + ) + ->leftJoin( + 'items', + 'items.id', + 'labkit_order_items.item_id' + ) + ->leftJoin( + 'plans_v1', + 'plans_v1.id', + 'items.plans_id' + ) + ->select( + 'labkit_order_items.id', + 'labkit_order_items.status', + 'labkit_order_items.result', + 'lab_kit.name as lab_kit_name', + 'plans_v1.title as item_name' + ) + ->get(); + foreach ($labkitOrderItems as $labKit) { + + if ($labKit->result != "") + $labKit->result = $this->url->to('storage/lab_results/' . $labKit->result); + } + + return response()->json([ + 'data' => $labkitOrderItems, + ]); + } + public function orderCount(Request $request) + { + + $fromDate = $request->get('from_date'); + $toDate = $request->get('to_date'); + $total_order = Cart::select( + "appointments.*", + 'appointments.id as appointment_id', + 'carts.*', + 'carts.id as order_id', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name"), + )->leftJoin('appointments', 'appointments.id', 'carts.appointment_id'); + $total_order = $total_order->count(); + + $total_appointment_order = Cart::select( + "appointments.*", + 'appointments.id as appointment_id', + 'carts.*', + 'carts.id as order_id', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name"), + )->join('appointments', 'appointments.id', 'carts.appointment_id') + ->whereNotNull("appointments.id"); + $total_appointment_order = $total_appointment_order->count(); + + $total_appointment_order_without = Cart::select( + "appointments.*", + 'appointments.id as appointment_id', + 'carts.*', + 'carts.id as order_id', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name"), + )->leftJoin('appointments', 'appointments.id', 'carts.appointment_id') + ->whereNull("appointments.id"); + $total_appointment_order_without = $total_appointment_order_without->count(); + + $upcomingMeetings = Cart::select( + 'carts.id as order_id', + 'appointments.id', + 'appointments.patient_id', + 'appointments.appointment_time', + 'appointments.appointment_date', + DB::raw( + 'CONCAT(patients.first_name, " " , patients.last_name) as patient_name' + ) + ) + ->join('appointments', 'appointments.id', 'carts.appointment_id') + ->leftJoin('patients', 'patients.id', 'appointments.patient_id') + ->where('appointments.appointment_date', ">=", Carbon::now()->format("Y-m-d")) + ->where('appointments.start_time', null) + ->count(); + + $completedMeetings = Cart::select( + 'carts.id as order_id', + 'appointments.patient_id', + 'appointments.appointment_time', + 'appointments.appointment_date', + 'appointments.start_time', + 'appointments.end_time', + 'telemed_pros.name as provider_name', + 'appointments.telemed_pros_id as provider_id', + + DB::raw( + 'CONCAT(patients.first_name, " " , patients.last_name) as patient_name' + ) + ) + ->join('appointments', 'appointments.id', 'carts.appointment_id') + ->leftJoin('patients', 'patients.id', 'appointments.patient_id') + ->leftJoin('telemed_pros', 'telemed_pros.id', 'appointments.telemed_pros_id') + ->where('appointments.start_time', "!=", null) + ->where('appointments.end_time', "!=", null) + ->count(); + + + $prescribeOrderList = Cart::select( + "appointments.*", + 'appointments.id as appointment_id', + 'carts.*', + 'carts.id as order_id', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name"), + ) + ->leftJoin('appointments', 'appointments.id', 'carts.appointment_id'); + + + $prescribeOrderCount = $prescribeOrderList->where("prescription_status", 1)->count(); + return response() + ->json([ + 'total_appointment_order' => $total_appointment_order, + 'total_order' => $total_order, + 'total_appointment_order_without' => $total_appointment_order_without, + 'upcomingMeetings' => $upcomingMeetings, + 'completedMeetings' => $completedMeetings, + 'prescribeOrderCount' => $prescribeOrderCount, + ]); + } + public function updateItemStatus($id, Request $request) + { + try { + $this->authorizeForUser($this->user, 'edit', new Cart); + Item::where('id', $id) + ->update([ + 'status' => $request->get('status') + ]); + $itemsCount = Item::where('cart_id', $request->get('order_id')); + $statusNeeded = $itemsCount->where('status', '!=', 'pending') + ->where('status', '!=', 'canceled') + ->where('status', '!=', 'failed') + ->where('status', '!=', 'refunded') + ->where('status', '!=', 'processing') + ->count(); + + if ($itemsCount->count() == $statusNeeded) { + Cart::where('id', $request->get('order_id'))->update([ + 'status' => 'completed' + ]); + } + return response() + ->json([ + 'success' => "Updated !" + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function addNotePatient(Cart $cart, Request $request) + { + + //$user = Auth::user(); + $appointment = Appointment::find($cart->appointment_id); + $addNotePatient = PatientNote::create([ + 'note' => $request->input('note'), + 'note_type' => $request->input('note_type'), + 'patient_id' => $cart->patient_id, + 'appointment_id' => $cart->appointment_id, + 'telemed_pros_id' => $appointment->telemed_pros_id ?? null, + 'admin_id' => Auth::guard('admin')->user()->id + + ]); + $addNotePatient->file_url = ""; + if ($request->hasFile('file')) { + $file = $request->file('file'); + + $filename = $addNotePatient->id . '.' . $file->getClientOriginalExtension(); + + $file->move(public_path('assets/files'), $filename); + + $addNotePatient->file_url = "assets/files" . $addNotePatient->id . '.' . $file->getClientOriginalExtension(); + } + $patient = $addNotePatient->patient; + $setting = Setting::find(1); + /* Mail::send('emails.noteAdded', ['patient' => $patient, 'agent' => $user, 'setting' => $setting], function ($message) use ($patient, $user) { + $message->to($patient->email, $patient->first_name) + ->subject('You Have a New Note from ' . $user->name); + }); */ + return response()->json([ + 'message' => 'Note created', + 'data' => $addNotePatient + ], 200); + } + public function editNotePatient($id, Request $request) + { + $note = PatientNote::findOrFail($id); + $note->update([ + 'note' => $request->input('note'), + 'note_type' => $request->input('note_type'), + 'admin_id' => Auth::guard('admin')->user()->id + ]); + + if ($request->hasFile('file')) { + // Delete old file if it exists + if ($note->file_url) { + $oldFilePath = public_path($note->file_url); + if (file_exists($oldFilePath)) { + unlink($oldFilePath); + } + } + + $file = $request->file('file'); + $filename = $note->id . '.' . $file->getClientOriginalExtension(); + $file->move(public_path('assets/files'), $filename); + $note->file_url = "assets/files" . $note->id . '.' . $file->getClientOriginalExtension(); + $note->save(); + } + + return response()->json([ + 'message' => 'Note updated', + 'data' => $note + ], 200); + } + + public function deleteNotePatient($id) + { + $note = PatientNote::findOrFail($id); + + // Delete associated file if it exists + if ($note->file_url) { + $filePath = public_path($note->file_url); + if (file_exists($filePath)) { + unlink($filePath); + } + } + + $note->delete(); + + return response()->json([ + 'message' => 'Note deleted' + ], 200); + } + public function getNotePatient($id) + { + $note = PatientNote::with(['admin'])->findOrFail($id); + + return response()->json([ + 'note' => $note + ], 200); + } +} diff --git a/app/Http/Controllers/Admin/Api/PatientController.php b/app/Http/Controllers/Admin/Api/PatientController.php new file mode 100644 index 0000000..6d97ba1 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/PatientController.php @@ -0,0 +1,550 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function newPatient(Request $request) + { + try { + $this->authorizeForUser($this->user, 'add', new Patient); + $validatedData = $request->validate([ + 'first_name' => 'required|string|max:255', + 'last_name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:patients', + 'password' => 'required', + 'dob' => 'required|date_format:Y-m-d', + 'phone_no' => 'required' + ]); + $patient = Patient::create([ + 'first_name' => $request->input('first_name'), + 'last_name' => $request->input('last_name'), + 'phone_no' => $request->input('phone_no'), + 'email' => $request->input('email'), + 'password' => Hash::make($request->input('password')), + 'dob' => $request->input('dob'), + 'gender' => $request->input('gender') ?? "", + ]); + + $patient->address = $request->input('address'); + $patient->state = $request->input('state'); + $patient->city = $request->input('city'); + $patient->country = $request->input('country'); + + $patient->zip_code = $request->input('zip'); + + $patient->shipping_address = $request->input('address'); + $patient->shipping_state = $request->input('state'); + $patient->shipping_city = $request->input('city'); + $patient->shipping_zipcode = $request->input('zip'); + + + $image = $request->get('profile_pic'); + $fileName = 'profile-' . time(); + + $logo = base64_decode($image); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + + $imageName = $fileName . '.' . $ext; + Storage::disk('local')->put("public/profile_pictures/" . $imageName, $logo); + $patient->profile_picture = $imageName; + $patient->save(); + + + if ($patient->dob) { + $birthDate = new DateTime($patient->dob); + $today = new DateTime(date('Y-m-d')); + $age = $today->diff($birthDate)->y; + $patient->age = $age; + } else { + $patient->age = 0; + } + PatientRegActivity::create([ + 'patient_id' => $patient->id, + 'activity' => 'patient_registered' + ]); + $setting = Setting::find(1); + event(new PatientRegistered($patient, $validatedData)); + return response() + ->json([ + 'data' => $patient + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function patientShippingAddress($id, Request $request) + { + try { + $this->authorizeForUser($this->user, 'edit', new Patient); + $patient = Patient::find($id); + $patient->shipping_address = $request->input('address'); + $patient->shipping_state = $request->input('state'); + $patient->shipping_city = $request->input('city'); + $patient->shipping_zipcode = $request->input('zip'); + $patient->save(); + return response() + ->json([ + 'data' => $patient + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function patientList(Request $request) + { + try { + $this->authorizeForUser($this->user, 'list', new Patient); + $patients = Patient::query(); + + // Filter by state + if ($request->input('state') != "all") { + $patients->where('patients.state', $request->input('state')); + } + + // Filter by gender + if ($request->input('gender') != "all") { + $patients->where('patients.gender', $request->input('gender')); + } + + // Filter by plan (assuming you have a plan field or relation) + if ($request->input('plan') != "all") { + $planNames = $request->input('plan'); + + $patients->leftJoin('patient_plan', 'patients.id', '=', 'patient_plan.patient_id') + ->leftJoin('plans_v1', 'patient_plan.plan_id', '=', 'plans_v1.id') + ->where('plans_v1.slug', $planNames); + } + + // Join with the carts table to get order details + $patients->leftJoin('carts', 'patients.id', '=', 'carts.patient_id') + ->select('patients.*') + ->addSelect([ + 'last_order_date' => Cart::selectRaw('MAX(created_at)') + ->whereColumn('patient_id', 'patients.id'), + 'total_orders' => Cart::selectRaw('COUNT(*)') + ->whereColumn('patient_id', 'patients.id'), + 'total_subscriptions' => Cart::selectRaw('COUNT(DISTINCT start_subscription)') + ->whereColumn('patient_id', 'patients.id'), + ]); + + Log::info('PatientList Datatable:', [ + 'sql' => $patients->toSql(), + 'bindings' => $patients->getBindings() + ]); + + // Use DataTables to process the query + return DataTables::of($patients)->make(true); + } catch (\Exception | Error $e) { + // Log the error + Log::error('Error in patientList: ' . $e->getMessage()); + // Return an error response + return response()->json([ + 'error' => 'An error occurred while processing the request.', + 'message' => $e->getMessage() + ], 500); + } + } + public function patientFullDetail(Patient $patient) + { + try { + $this->authorizeForUser($this->user, 'edit', new Patient); + $patient->first_name = $patient->first_name . " " . $patient->last_name; + $plans = PatientPlan::join('plans_v1', 'patient_plan.plan_id', '=', 'plans_v1.id') + ->leftJoin('medication_categories', 'plans_v1.medication_category_id', '=', 'medication_categories.id') + ->where('patient_plan.patient_id', $patient->id) + ->select('plans_v1.*', 'medication_categories.category_name') + ->orderBy('plans_v1.created_at', 'desc') + ->first(); + $upcomingMeetings = Appointment::select( + 'appointments.patient_id', + 'appointments.timezone', + 'appointments.appointment_time', + 'appointments.appointment_date', + 'carts.id as order_id' + ) + ->leftJoin("carts", "carts.appointment_id", "appointments.id") + ->where("appointments.patient_id", $patient->id) + // dd(Constant::getFullSql($upcomingMeetings)); + ->get(); + $completedMeetings = Appointment::select( + 'appointments.patient_id', + 'appointments.appointment_time', + 'appointments.appointment_date', + 'appointments.start_time', + 'appointments.end_time', + 'appointments.timezone', + 'telemed_pros.name as provider_name', + 'telemed_pros_id as provider_id', + 'carts.id as order_id' + ) + ->leftJoin('telemed_pros', 'telemed_pros.id', 'appointments.telemed_pros_id') + ->leftJoin("carts", "carts.appointment_id", "appointments.id") + ->where("appointments.patient_id", $patient->id) + ->where('appointments.start_time', "!=", null) + ->where('appointments.end_time', "!=", null) + ->get(); + $patientNotes = PatientNote::select( + 'patient_notes.id', + 'patient_notes.note', + 'patient_notes.note_type', + 'telemed_pros.name as provider_name', + 'telemed_pros.id as provider_id', + 'patient_notes.created_at', + 'patient_notes.patient_id', + 'carts.id as order_id', + 'patient_notes.created_by_id', + 'patient_notes.created_by_type' + ) + ->leftJoin('telemed_pros', 'patient_notes.telemed_pros_id', 'telemed_pros.id') + ->leftJoin("carts", "carts.appointment_id", "patient_notes.appointment_id") + ->where("patient_notes.patient_id", $patient->id) + ->get(); + + foreach ($patientNotes as $notes) { + if ($notes->note_type != 'Notes') + $notes->note = $this->url->to("assets/files/" . $notes->patient_id . ".png"); + else + $notes->note = $notes->note; + } + $patientPrescription = PatientPrescription::select( + 'patient_prescription.*', + 'telemed_pros.name as provider_name', + 'prescriptions.*', + 'carts.id as order_id' + ) + ->leftJoin('appointments', 'patient_prescription.appointment_id', 'appointments.id') + ->leftJoin("carts", "carts.appointment_id", "appointments.id") + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', 'telemed_pros.id') + ->leftJoin('prescriptions', 'prescriptions.id', 'patient_prescription.prescription_id') + ->where('patient_prescription.patient_id', $patient->id)->get(); + $patient->profile_completion_Percentage = $patient->profile_completion_Percentage; + $labkits = LabkitOrderItem::leftJoin( + 'lab_kit', + 'labkit_order_items.lab_kit_id', + 'lab_kit.id' + ) + ->leftJoin( + 'items', + 'items.id', + 'labkit_order_items.item_id' + ) + ->leftJoin( + 'plans_v1', + 'plans_v1.id', + 'items.plans_id' + ) + ->leftJoin( + 'carts', + 'carts.id', + 'labkit_order_items.cart_id' + ) + ->where('carts.patient_id', $patient->id) + ->select( + 'labkit_order_items.id', + 'labkit_order_items.status', + 'labkit_order_items.result', + 'lab_kit.name as lab_kit_name', + 'plans_v1.id as product_id', + 'plans_v1.title as product_name' + ) + ->get(); + $orderList = Cart::select("appointments.*", 'appointments.id as appointment_id', 'carts.*', 'carts.id as order_id', 'telemed_pros.name as agent_name', 'telemed_pros.email as agent_email') + ->leftJoin('appointments', 'appointments.id', 'carts.appointment_id') + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', '=', 'telemed_pros.id') + ->where('appointments.patient_id', $patient->id); + + $orderListData = $orderList->get(); + return response()->json([ + 'patient' => $patient, + 'plans' => $plans, + 'upcomingMeetings' => $upcomingMeetings, + 'completed_meetings' => $completedMeetings, + 'patientNotes' => $patientNotes, + 'prescriptionData' => $patientPrescription, + 'labkit' => $labkits, + 'orderListData' => $orderListData, + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function patientDelete(Patient $patient) + { + try { + $this->authorizeForUser($this->user, 'delete', new Patient); + Patient::where("id", $patient->id)->delete(); + return response()->json([ + 'patient' => "Deleted Successfully" + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function patientUpdate(Patient $patient, Request $request) + { + try { + $this->authorizeForUser($this->user, 'edit', new Patient); + $patient->first_name = $request->input('first_name'); + $patient->last_name = $request->input('last_name'); + $patient->phone_no = $request->input('phone_no'); + $patient->shipping_address = $request->input('gender'); + $patient->shipping_address = $request->input('dob'); + if ($request->input('password')) { + $patient->password = Hash::make($request->input('password')); + } + $patient->shipping_address = $request->input('address'); + $patient->shipping_state = $request->input('state'); + $patient->shipping_city = $request->input('city'); + $patient->shipping_zipcode = $request->input('zip'); + $patient->shipping_country = $request->input('country'); + + $patient->save(); + return response()->json([ + 'message' => 'Patient updated successfully', + 'telemed' => $patient + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function getNotePatient(Patient $patient, Appointment $appointment, Request $request) + { + try { + $this->authorizeForUser($this->user, 'patient_notes', new Patient); + $patientNotes = PatientNote::where("patient_id", $patient->id) + ->where("appointment_id", $appointment->id) + ->with('appointment') + ->get(); + + $data = $patientNotes->map(function ($patientNote) { + $fileUrl = "/assets/files/{$patientNote->id}.png"; + $filePath = public_path($fileUrl); + + if (File::exists($filePath)) { + $fileUrl = "/assets/files/{$patientNote->id}.png"; + } else { + $fileUrl = null; + } + + return [ + 'id' => $patientNote->id, + 'note' => $patientNote->note, + 'note_type' => $patientNote->note_type, + 'created_at' => $patientNote->created_at, + 'patient_id' => $patientNote->patient_id, + 'appointment' => $patientNote->appointment, + 'telemedPro' => $patientNote->telemedPro, + 'file_url' => $fileUrl, + 'telemedPro' => $patientNote->appointment?->telemedPro + ]; + }); + + return response()->json([ + 'message' => 'Patient notes retrieved', + 'data' => $data + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function patient(Patient $patient) + { + return response()->json([ + 'data' => $patient + ], 200); + } + public function getPatientPrescription($patient_id, $appointment_id) + { + try { + $this->authorizeForUser($this->user, 'patinet_prescriptions', new Patient); + $patientPrescription = PatientPrescription::with('prescription') + ->where('patient_id', $patient_id) + ->where('appointment_id', $appointment_id) + ->get(); + + $prescriptionData = []; + foreach ($patientPrescription as $prescription) { + $prescriptionData[] = [ + 'patient' => $prescription->patient, + 'prescription' => $prescription->prescription, + 'created_at' => $prescription->created_at, + 'updated_at' => $prescription->updated_at, + 'direction_one' => $prescription->direction_one, + 'direction_two' => $prescription->direction_two, + 'dont_substitute' => $prescription->dont_substitute, + 'comments' => $prescription->comments, + 'appointment_id' => $prescription->appointment_id, + 'status' => $prescription->status, + 'appointment' => $prescription->appointment, + 'telemedPro' => $prescription->appointment->telemedPro, + 'licenseNumber' => LicenseNumberModel::where("provider_id", $patient_id)->orderBy('id', 'DESC')->first() + ]; + } + if (!$patientPrescription->isEmpty()) { + return response()->json($prescriptionData); + } else { + return response()->json(['message' => 'Prescription not found'], 404); + } + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function storePatientPrescription(Request $request) + { + try { + $this->authorizeForUser($this->user, 'patinet_prescriptions', new Patient); + $cart = Cart::find($request->input("order_id")); + $prescription = PatientPrescription::create($request->all()); + $prescription->appointment_id = $cart->appointment_id; + $prescription->status = "pending"; + $prescription->save(); + $patient = $prescription->patient; + $setting = Setting::find(1); + /* Mail::send('emails.prescriptionAdd', ['patient' => $patient, 'prescription' => $prescription, 'setting' => $setting], function ($message) use ($patient, $user) { + $message->to($patient->email, $patient->first_name) + ->subject('New Prescription Details from ' . $user->name); + }); */ + return response()->json($prescription, 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function updateStatusPrescription($patient_prescription_id, Request $request) + { + // + try { + $this->authorizeForUser($this->user, 'patinet_prescriptions_edit', new Patient); + $status = $request->input("status"); + $prescription = PatientPrescription::find($patient_prescription_id); + $prescription->status = $status; + $prescription->save(); + $patient = $prescription->patient; + $setting = Setting::find(1); + /* Mail::send('emails.prescriptionUpdated', ['patient' => $patient, 'setting' => $setting], function ($message) use ($patient) { + $message->to($patient->email, $patient->first_name) + ->subject('Prescription updated.'); + }); */ + return response()->json($prescription, 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function getStatusPrescription($patient_prescription_id) + { + $prescription = PatientPrescription::with(['prescription']) + ->findOrFail($patient_prescription_id); + return response()->json($prescription, 200); + } + public function updatePatientPrescription($id, Request $request) + { + try { + $prescription = PatientPrescription::findOrFail($id); + $prescription->update($request->all()); + + if ($request->has('status')) { + $prescription->status = $request->input('status'); + } + + $prescription->save(); + + $patient = $prescription->patient; + $setting = Setting::find(1); + + // You might want to add email notification here if needed + + return response()->json([ + 'message' => 'Prescription updated successfully', + 'data' => $prescription + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'An error occurred while updating the prescription', + 'error' => $e->getMessage() + ], 500); + } + } + + public function deletePatientPrescription($id) + { + try { + $prescription = PatientPrescription::findOrFail($id); + $prescription->delete(); + + return response()->json([ + 'message' => 'Prescription deleted successfully' + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'An error occurred while deleting the prescription', + 'error' => $e->getMessage() + ], 500); + } + } +} diff --git a/app/Http/Controllers/Admin/Api/PermissionsController.php b/app/Http/Controllers/Admin/Api/PermissionsController.php new file mode 100644 index 0000000..a161ef4 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/PermissionsController.php @@ -0,0 +1,126 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + + public function index() + { + try{ + $this->authorizeForUser($this->user,'list', new Permission); + $roleList = Permission::all(); + return Datatables::of($roleList) + ->toJson(); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + + } + public function storeRole(Request $request) + { + try{ + $this->authorizeForUser($this->user,'add', new Permission); + Permission::create( + [ + 'role_name' => $request->input('role_name'), + 'role_guard' => $request->input('role_guard') + ] + ); + return response()->json([ + 'success' => 'Data Saved!' + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function editRoles($id) + { + try{ + $this->authorizeForUser($this->user,'edit', new Permission); + return response()->json([ + 'data' => Permission::find($id) + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function updateRoles($id,Request $request) + { + try{ + $this->authorizeForUser($this->user,'edit', new Permission); + $permission = Permission::find($id); + $permission->role_name = $request->input('role_name'); + $permission->role_guard = $request->input('role_guard'); + $permission->save(); + return response()->json([ + 'data' => Permission::find($id) + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function deleteRoles($id){ + try{ + $this->authorizeForUser($this->user,'delete', new Permission); + Permission::find($id)->delete(); + return response()->json([ + 'success' => 'role Deleted Successfully !' + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function updatePermissions($id,Request $request) + { + $permission = Permission::find($id); + } + public function getPermissions($id) + { + try{ + $this->authorizeForUser($this->user,'list', new Permission); + $role = Permission::find($id); + $rolePermissions = $role->permissions; + $permissionManager = new Permissions($rolePermissions); + $permissions = $permissionManager->getPermissions(); + // $permissions = $permissionManager->permissionsApi(); + return response()->json([ + 'data' => $permissions + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function savePermissions($id,Request $request) + { + try{ + $this->authorizeForUser($this->user,'edit', new Permission); + // $permissions = 'DASHBOARD_,DASHBOARD_FILTERS,DASHBOARD_DATA,PRODUCT_,PRODUCT_VIEW,PRODUCT_ADD,PRODUCT_EDIT,PRODUCT_DELETE,PROVIDER_,PROVIDER_VIEW,PROVIDER_ADD,PROVIDER_EDIT,PROVIDER_DELETE,ADMIN_,ADMIN_VIEW,ADMIN_ADD,ADMIN_EDIT,ADMIN_DELETE,ADMIN_SITE_SETTINGS,ADMIN_SECURITY'; + $permissions = $request->input('permisssions'); + $permissionsArray = explode(',',$permissions); + + $permissionsData = Permission::find($id); + $permissionsData->permissions = $permissionsArray; + $permissionsData->save(); + return response()->json([ + 'success' => "permissions saved !" + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } +} diff --git a/app/Http/Controllers/Admin/Api/PrescriptionController.php b/app/Http/Controllers/Admin/Api/PrescriptionController.php new file mode 100644 index 0000000..0f27cb1 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/PrescriptionController.php @@ -0,0 +1,97 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function index(){} + public function create(Request $request){ + try{ + $this->authorizeForUser($this->user,'add', new Prescription); + $data = [ + 'name'=>$request->input('name'), + 'brand'=>$request->input('brand'), + 'from'=>$request->input('from'), + 'dosage'=>$request->input('dosage'), + 'quantity'=>$request->input('quantity'), + 'direction_quantity'=>$request->input('direction_quantity'), + 'refill_quantity'=>$request->input('refill_quantity') + ]; + Prescription::create($data); + return response() + ->json([ + 'success' => "Data Saved !" + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function update($id,Request $request){ + try{ + $this->authorizeForUser($this->user,'edit', new Prescription); + $prescription = $this->details($id); + $prescription->name =$request->input('name'); + $prescription->brand =$request->input('brand'); + $prescription->from = $request->input('from'); + $prescription->dosage = $request->input('dosage'); + $prescription->quantity = $request->input('quantity'); + $prescription->direction_quantity = $request->input('direction_quantity'); + $prescription->refill_quantity = $request->input('refill_quantity'); + $prescription->save(); + return response() + ->json([ + 'success' => "Data Updated !" + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function details($id){ + try{ + // $this->authorizeForUser($this->user,'view', new Prescription); + return Prescription::find($id); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function edit($id) + { + try{ + $this->authorizeForUser($this->user,'edit', new Prescription); + return response() + ->json([ + 'data' => $this->details($id) + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function delete($id) + { + try{ + $this->authorizeForUser($this->user,'delete', new Prescription); + Prescription::find($id)->delete(); + return response() + ->json([ + 'success' => "Entry Deleted !" + ], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } +} diff --git a/app/Http/Controllers/Admin/Api/ReportsController.php b/app/Http/Controllers/Admin/Api/ReportsController.php new file mode 100644 index 0000000..89f4fcd --- /dev/null +++ b/app/Http/Controllers/Admin/Api/ReportsController.php @@ -0,0 +1,584 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function providerReportFilters() + { + $providers = Telemedpro::select( + DB::raw("CONCAT(license_numbers.license_number,',',license_numbers.state) as provider_license_number"), + 'appointments.patient_name', + 'appointments.appointment_date', + 'appointments.appointment_time', + 'appointments.timezone', + 'start_time', + 'end_time', + 'duration', + DB::raw("CONCAT(patients.first_name,',',patients.last_name) as patient_name"), + 'patients.phone_no', + 'patients.email', + 'patients.address', + 'patients.city', + 'patients.state', + 'patients.zip_code', + 'patients.country', + 'patients.gender', + 'patients.dob', + 'patients.height', + 'patients.weight' + ) + ->LeftJoin('license_numbers', 'provider_id', 'telemed_pros.id') + ->LeftJoin('appointments', 'appointments.telemed_pros_id', 'telemed_pros.id') + ->leftJoin('patients', 'appointments.patient_id', 'patients.id') + ->whereNotNull('appointments.start_time') + ->whereNotNull('appointments.end_time') + ->get(); + + foreach ($providers as $provider) { + $start_datetime = new DateTime($provider->start_time); + $diff = $start_datetime->diff(new DateTime($provider->end_time)); + $duration = $diff->h . " hours " . $diff->i . " Min"; + // dd($providers->duration,$duration); + $provider->duration = $duration; + } + + return response()->json([ + 'provider_list' => $providers, + ]); + } + public function providerReportPost(Request $request) + { + return response()->json([ + 'provider_list' => '' + ]); + } + public function overviewReport(Request $request) + { + try { + $this->authorizeForUser($this->user, 'overview_analytics', new ProfileQuestion); + $start_date = $request->get('start_date'); + $end_date = $request->get('end_date'); + $totalOrdersStats = Cart::select( + DB::raw("sum(case when carts.status = 'completed' then 1 else 0 end) as total_sales"), + DB::raw("sum(case when carts.status = 'completed' then carts.total_amount else 0 end) as sales_amount"), + DB::raw("count(items.id) as products_sold") + ) + ->Join('items', 'items.cart_id', 'carts.id') + ->where('carts.created_at', '>=', $start_date . " 00:00:00") + ->where('carts.created_at', '<=', $end_date . " 23:59:59") + ->where('carts.status', '=', 'completed') + ->get(); + $orderCollection = Cart::select( + 'carts.id as order_id', + 'carts.status', + 'carts.email', + 'carts.total_amount', + 'carts.created_at as date', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name") + ) + ->where('carts.created_at', '>=', $start_date . " 00:00:00") + ->where('carts.created_at', '<=', $end_date . " 23:59:59") + ->where('carts.status', '=', 'completed') + ->get(); + $orderData = $orderCollection->map(function ($query, $key) { + $patientType = $query->where('email', $query->email)->count(); + $itemSold = Item::select(DB::raw("GROUP_CONCAT(title SEPARATOR ', ') as items")) + ->where('cart_id', $query->order_id) + ->leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id'); + $itemCount = $itemSold->count(); + $products = $itemSold->first(); + if ($patientType > 1) + $query->customer_type = 'returning'; + else + $query->customer_type = 'new'; + + $query->products = $products->items ?? null; + $query->item_sold = $itemCount ?? 0; + + $query->attribution = 'direct'; + + return $query; + }); + $dates = []; + $sales = []; + $startDate = Carbon::parse($start_date); + $endDate = Carbon::parse($end_date); + for ($date = $startDate; $date->lte($endDate); $date->addDay()) { + $values = Cart::select( + DB::raw('DATE(created_at) as date'), + DB::raw("SUM(case when carts.status = 'completed' then carts.total_amount else 0 end) as amount") + ) + ->where('carts.created_at', '>=', $date->format("Y-m-d") . " 00:00:00") + ->where('carts.created_at', '<=', $date->format("Y-m-d") . " 23:59:59") + ->groupBy(DB::raw('DATE(created_at)')); + $graphsValues = $values->first(); + + $dates[] = $date->format("M d/y"); + if ($graphsValues) + $sales[] = $graphsValues->amount; + else + $sales[] = 0; + } + $newUser = 0; + $returnUser = 0; + $newUsers = []; + $returningUsers = []; + //getting here unique rows for patient stats + $uniqueKeys = array_map(function ($item) { + return $item['email']; + }, $orderCollection->toArray()); + + $uniqueRecords = array_intersect_key($orderCollection->toArray(), array_unique($uniqueKeys)); + $uniqueRecords = array_values($uniqueRecords); + + foreach ($orderCollection as $userStats) { + $userStatus = Cart::where('email', $userStats->email)->count(); + if ($userStatus > 1) { + $returnUser++; + $returningUsers[] = $userStats; + } else { + $newUser++; + $newUsers[] = $userStats; + } + }; + + $percentageReturning = 0; + $percentageNewuser = 0; + if ($returnUser > 0 || $newUser > 0) { + $percentageReturning = ($returnUser / ($returnUser + $newUser)) * 100; + $percentageNewuser = ($newUser / ($returnUser + $newUser)) * 100; + } + + //check here users engagement + + $newUserEngagement = $this->calculateEngagement($newUsers); + $returningUserEngagement = $this->calculateEngagement($returningUsers); + return response()->json([ + 'totals' => $totalOrdersStats, + 'orders' => $orderData, + 'chart' => [ + 'chart_dates' => $dates, + 'chart_data' => $sales + ], + 'patient_stats' => + [ + 'returning_users' => [$returnUser, round($percentageReturning, 0) . "%"], + 'new_users' => [$newUser, round($percentageNewuser, 0) . "%"] + ] + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function ordersFilters() + { + $patient = Patient::select('id', DB::raw("CONCAT(first_name,' ',last_name) as patient_name"))->get(); + return response()->json([ + 'patients' => $patient + + ]); + } + public function initialPatients() + { + $patients = Patient::select('id', DB::raw("CONCAT(first_name,' ',last_name) as patient_name")) + ->limit(100) + ->orderBy('patient_name', 'asc') + ->get(); + + return response()->json([ + 'patients' => $patients + ]); + } + + public function searchPatients(Request $request) + { + $searchTerm = $request->input('term'); + + $patients = Patient::select('id', DB::raw("CONCAT(first_name,' ',last_name) as patient_name")) + ->where(DB::raw("CONCAT(first_name,' ',last_name)"), 'LIKE', "%{$searchTerm}%") + ->limit(500) + ->orderBy('patient_name', 'asc') + ->get(); + + return response()->json([ + 'patients' => $patients + ]); + } + + // Function to calculate engagement metrics + function calculateEngagement($users) + { + $totalUsers = count($users); + $completedOrders = 0; + $totalAmount = 0; + + foreach ($users as $user) { + if ($user['status'] === 'delivered') { + $completedOrders++; + } + $totalAmount += floatval($user['total_amount']); + } + + $orderCompletionRate = $totalUsers > 0 ? ($completedOrders / $totalUsers) * 100 : 0; + $averageOrderValue = $totalUsers > 0 ? $totalAmount / $totalUsers : 0; + + return [ + 'total_users' => $totalUsers, + 'completed_orders' => $completedOrders, + 'order_completion_rate' => $orderCompletionRate, + 'average_order_value' => $averageOrderValue + ]; + } + public function ordersReport(Request $request) + { + try { + $this->authorizeForUser($this->user, 'orders_analytics', new ProfileQuestion); + $start_date = $request->get('start_date'); + $end_date = $request->get('end_date'); + $status = $request->get('status'); + $patient = $request->get('patient'); + $query = Cart::select( + 'carts.id as order_id', + 'carts.status', + 'carts.email', + 'carts.total_amount', + 'carts.created_at as date', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name") + ) + ->where('carts.created_at', '>=', $start_date . " 00:00:00") + ->where('carts.created_at', '<=', $end_date . " 23:59:59"); + + // Apply filters + if ($status != 'all') { + $query->where('carts.status', $status); + } + + if ($patient != 'all') { + $query->where('carts.patient_id', $patient); + } + $dates = []; + $sales = []; + $startDate = Carbon::parse($start_date); + $endDate = Carbon::parse($end_date); + for ($date = $startDate; $date->lte($endDate); $date->addDay()) { + $values = Cart::select( + DB::raw('DATE(created_at) as date'), + DB::raw("SUM(carts.total_amount) as amount") + ) + ->where('carts.created_at', '>=', $date->format("Y-m-d") . " 00:00:00") + ->where('carts.created_at', '<=', $date->format("Y-m-d") . " 23:59:59") + ->groupBy(DB::raw('DATE(created_at)')); + if ($status != 'all') { + $values->where('carts.status', $status); + } + + if ($patient != 'all') { + $values->where('carts.patient_id', $patient); + } + $graphsValues = $values->first(); + + $dates[] = $date->format("M d/y"); + if ($graphsValues) + $sales[] = $graphsValues->amount; + else + $sales[] = 0; + } + // dd(Constant::getFullSql($query)); + $orderCollection = $query->get(); + $orderData = $orderCollection->map(function ($cart) { + $patientType = Cart::where('email', $cart->email)->count(); + + $itemSold = Item::select(DB::raw("GROUP_CONCAT(plans_v1.title SEPARATOR ', ') as items")) + ->where('cart_id', $cart->order_id) + ->leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id'); + $itemCount = $itemSold->count(); + $products = $itemSold->first(); + + $cart->customer_type = $patientType > 1 ? 'returning' : 'new'; + $cart->products = $products->items ?? null; + $cart->item_sold = $itemCount ?? null; + $cart->attribution = 'direct'; + + return $cart; + }); + $totalOrdersStats = Cart::select( + // DB::raw("sum(case when carts.status = 'delivered' then 1 else 0 end) as total_sales"), + DB::raw("count(carts.id) as total_sales"), + DB::raw("sum(carts.total_amount ) as sales_amount"), + DB::raw("count(items.id) as products_sold") + ) + ->Join('items', 'items.cart_id', 'carts.id') + ->where('carts.created_at', '>=', $start_date . " 00:00:00") + ->where('carts.created_at', '<=', $end_date . " 23:59:59"); + + if ($status != 'all') { + + $totalOrdersStats->where('carts.status', $status); + } + + if ($patient != 'all') { + $totalOrdersStats->where('carts.patient_id', $patient); + } + $totals = $totalOrdersStats->get(); + return response()->json([ + 'orders' => $orderData, + 'totals' => $totals, + 'chart' => [ + 'chart_dates' => $dates, + 'chart_data' => $sales + ] + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function productAnalytics(Request $request) + { + try { + $this->authorizeForUser($this->user, 'orders_analytics', new ProfileQuestion); + $start_date = $request->get('start_date'); + $end_date = $request->get('end_date'); + $singleProduct = $request->get('single_product'); + $patient = $request->get('patient'); + $query = Item::select( + DB::raw("sum(case when items.status='delivered' then items.quantity else 0 end) as total_item_sold"), + DB::raw("sum(case when items.status='delivered' then 1 else 0 end) as total_orders"), + DB::raw("sum(case when items.status='delivered' then (items.quantity*plans_v1.price) else 0 end) as total_amount"), + 'plans_v1.title as product_name', + 'items.plans_id as product_id' + ) + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->where('items.created_at', '>=', $start_date . " 00:00:00") + ->where('items.created_at', '<=', $end_date . " 23:59:59") + ->where('items.status', 'delivered') + ->groupby('plans_v1.title', 'items.plans_id'); + // Apply filters + if ($singleProduct != 'all') { + $query->where('items.plans_id', $singleProduct); + } + + $dates = []; + $sales = []; + $startDate = Carbon::parse($start_date); + $endDate = Carbon::parse($end_date); + for ($date = $startDate; $date->lte($endDate); $date->addDay()) { + $graphsValues = Item::select( + DB::raw("sum(case when items.status='delivered' then 1 else 0 end) as total_orders"), + DB::raw("sum(case when items.status='delivered' then (items.quantity*plans_v1.price) else 0 end) as total_amount"), + ) + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->where('items.created_at', '>=', $date->format("Y-m-d") . " 00:00:00") + ->where('items.created_at', '<=', $date->format("Y-m-d") . " 23:59:59") + ->where('items.status', 'delivered') + ->groupby('plans_v1.title', 'items.plans_id'); + + if ($singleProduct != 'all') { + $graphsValues->where('items.plans_id', $singleProduct); + } + + $graphVal = $graphsValues->first(); + + $dates[] = $date->format("M d/y"); + if ($graphVal) + $sales[] = $graphVal->total_amount; + else + $sales[] = 0; + } + $orderData = $query->get(); + $totalOrdersStats = Item::select( + DB::raw("count(items.id) as total_orders"), + DB::raw("sum(case when items.status='delivered' then (items.quantity*plans_v1.price) else 0 end) as sales_amount"), + DB::raw("sum(case when items.status='delivered' then items.quantity else 0 end) as products_sold") + ) + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->where('items.created_at', '>=', $start_date . " 00:00:00") + ->where('items.created_at', '<=', $end_date . " 23:59:59") + ->where('items.status', 'delivered'); + + if ($singleProduct != 'all') { + $totalOrdersStats->where('items.plans_id', $singleProduct); + } + $totals = $totalOrdersStats->get(); + return response()->json([ + 'orders' => $orderData, + 'totals' => $totals, + 'chart' => [ + 'chart_dates' => $dates, + 'chart_data' => $sales + ] + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function totalSales() + { + $start_date = request()->input('start_date'); + $end_date = request()->input('end_date'); + $startDate = Carbon::parse($start_date); + $endDate = Carbon::parse($end_date); + $sales = []; + for ($date = $startDate; $date->lte($endDate); $date->addDay()) { + $graphsValues = Item::select( + DB::raw("sum(case when items.status='delivered' then 1 else 0 end) as total_orders"), + DB::raw("sum(case when items.status='delivered' then (items.quantity*plans_v1.price) else 0 end) as total_amount"), + ) + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->where('items.created_at', '>=', $date->format("Y-m-d") . " 00:00:00") + ->where('items.created_at', '<=', $date->format("Y-m-d") . " 23:59:59") + ->where('items.status', 'delivered') + ->groupby('plans_v1.title', 'items.plans_id'); + + $graphVal = $graphsValues->first(); + + $dates[] = $date->format("M d/y"); + if ($graphVal) { + $sales[$date->format("Y-m-d")] = ["total_amount" => $graphVal->total_amount, "order_count" => $graphVal->total_orders]; + } else { + $sales[$date->format("Y-m-d")] = ["total_amount" => 0, "order_count" => 0]; + } + } + dd($dates, $sales); + } + public function ordersAnalytics(Request $request) + { + try { + $this->authorizeForUser($this->user, 'orders_analytics', new ProfileQuestion); + $start_date = $request->get('start_date'); + $end_date = $request->get('end_date'); + $singleProduct = $request->get('single_product'); + $query = Cart::select( + 'carts.id as order_id', + "carts.status as order_status", + "carts.created_at as order_date", + DB::raw("GROUP_CONCAT(plans_v1.title SEPARATOR ', ') as items"), + DB::raw("CONCAT(carts.first_name,' ',carts.last_name) as patient_name"), + "carts.total_amount", + DB::raw("sum(case when carts.status='completed' then 1 else 0 end) as item_sold") + ) + ->leftJoin('items', 'items.cart_id', 'carts.id') + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->where('carts.created_at', '>=', $start_date . " 00:00:00") + ->where('carts.created_at', '<=', $end_date . " 23:59:59") + ->where('carts.status', 'completed') + ->groupby('carts.id', + 'carts.status', + 'carts.created_at', + DB::raw("CONCAT(carts.first_name,' ',carts.last_name)"), + "carts.total_amount"); + + $dates = []; + $sales = []; + $singleMonth = []; + $current_month = null; + $graphDates = null; + $startDate = Carbon::parse($start_date); + $endDate = Carbon::parse($end_date); + for ($date = $startDate; $date->lte($endDate); $date->addDay()) + { + $graphsValues = Cart::select( + DB::raw('DATE(created_at) as date'), + DB::raw("SUM(carts.total_amount) as amount") + ) + ->where('carts.created_at', '>=', $date->format("Y-m-d") . " 00:00:00") + ->where('carts.created_at', '<=', $date->format("Y-m-d") . " 23:59:59") + ->where('carts.status', 'completed') + ->groupBy(DB::raw('DATE(created_at)')); + $graphVal = $graphsValues->first(); + + $month = $date->format('F Y'); + + if ($month != $current_month) + { + // Month has changed or it's the first iteration, echo the first day of the month + $dates[] = $month; + $current_month = $month; + } else { + + $dates[] = " "; + } + + $singleMonth[] = $date->format("M d/y"); + + if ($graphVal) + $sales[] = $graphVal->amount; + else + $sales[] = 0; + } + // count if user select more then one month + $dateIterate = $this->monthItrator($start_date, $end_date); + if ($dateIterate == 1) + $graphDates = $singleMonth; + else + $graphDates = $dates; + + $orderData = $query->get(); + $totalOrdersStats = Item::select( + DB::raw("count(items.id) as total_orders"), + DB::raw("sum(case when items.status='delivered' then (items.quantity*plans_v1.price) else 0 end) as sales_amount"), + DB::raw("sum(case when items.status='delivered' then items.quantity else 0 end) as products_sold") + ) + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->where('items.created_at', '>=', $start_date . " 00:00:00") + ->where('items.created_at', '<=', $end_date . " 23:59:59") + ->where('items.status', 'delivered'); + + $totals = $totalOrdersStats->get(); + return response()->json([ + 'orders' => $orderData, + 'totals' => $totals, + 'chart' => [ + 'chart_dates' => $graphDates, + 'chart_data' => $sales + ] + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function monthItrator($start_date, $end_date) + { + + $start = (clone Carbon::parse($start_date))->modify('first day of this month'); + $end = (clone Carbon::parse($end_date))->modify('first day of next month'); + + $interval = DateInterval::createFromDateString('1 month'); + $period = new DatePeriod($start, $interval, $end); + return iterator_count($period); + } +} diff --git a/app/Http/Controllers/Admin/Api/SiteSettingsController.php b/app/Http/Controllers/Admin/Api/SiteSettingsController.php new file mode 100644 index 0000000..f0a179b --- /dev/null +++ b/app/Http/Controllers/Admin/Api/SiteSettingsController.php @@ -0,0 +1,105 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function getSiteSettings(Request $request) + { + try{ + $this->authorizeForUser($this->user,'list', new Setting); + $settings = Setting::first(); + $favicon = $this->url->to("/" . $settings->favicon); + $logo = $this->url->to("/assets/logo/" . $settings->logo); + $settings['favicon'] = $favicon; + $settings['logo'] = $logo; + return response()->json([ + 'settings_data' => $settings + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function updateSettings($id, Request $request) + { + try{ + $this->authorizeForUser($this->user,'edit', new Setting); + $settings = Setting::find($id); + //upload website logo + $fileName = 'logo-' . time(); + $logoPath = public_path() . '/assets/logo/'; + $imageName = $this->uploadImage($request->get('logo'), $fileName, $logoPath); + //////////////// + //upload favicon + $fileName = 'favicon-' . time(); + $faviconPath = public_path('/'); + $faviconImageName = $this->uploadImage($request->get('favicon'), $fileName, $faviconPath); + ///////////////////////////// + $settings->plan_main_title = $request->get('plan_main_title'); + $settings->plan_description = $request->get('plan_description'); + $settings->plan_description_pargraph = $request->get('plan_description_pargraph'); + if ($request->get('logo')) + $settings->logo = $imageName; + $settings->footer_text = $request->get('footer_text'); + if ($request->get('favicon')) + $settings->favicon = $faviconImageName; + $settings->header_title = $request->get('header_title'); + $settings->domain_name = $request->get('domain_name'); + $settings->save(); + return response()->json([ + 'msg' => "Settings updated " + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function uploadImage($image, $fileName, $path) + { + try{ + $this->authorizeForUser($this->user,'edit', new Setting); + $logo = base64_decode($image); + $filename = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[0]); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + $imageName = $fileName . '.' . $ext; + $path = $path . $imageName; + file_put_contents($path, $logo); + return $imageName; + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function passwordReset(Request $request) + { + $userId = Auth::guard('admin')->user()->id; + $user = Admin::find($userId); + if (Hash::check($request->get('password'), $user->password)) { + $password = $request->get('new_password'); + $user->password = bcrypt($password); + $user->save(); + return response()->json([ + 'msg' => "Password updated" + ]); + } else { + return response()->json([ + 'msg' => "Password does not match", + 'status' => 'error' + ]); + } + } +} diff --git a/app/Http/Controllers/Admin/Api/SubscriptionController.php b/app/Http/Controllers/Admin/Api/SubscriptionController.php new file mode 100644 index 0000000..d20214c --- /dev/null +++ b/app/Http/Controllers/Admin/Api/SubscriptionController.php @@ -0,0 +1,225 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + + public function getSubscriptionList() + { + try { + $this->authorizeForUser($this->user, 'list', new Subscription); + + $subscriptions = Subscription::with(['cart', 'item.plansV1', 'patient']) + ->join('patients', 'subscription.patient_id', '=', 'patients.id') // Join with the patient table + ->join('items', 'subscription.item_id', '=', 'items.id') // Join with the plansV1 table + ->join('plans_v1', 'items.plans_id', '=', 'plans_v1.id') // Join with the plansV1 table + + ->select([ + 'subscription.*', + 'patients.first_name', + 'patients.last_name', + 'plans_v1.title as product_title', + 'plans_v1.price as price' + ]); // Select necessary columns + + return DataTables::of($subscriptions) + ->addColumn('product_title', function ($subscription) { + return $subscription->item?->plansV1?->title ?? 'N/A'; + }) + ->addColumn('price', function ($subscription) { + return $subscription->item?->plansV1?->price ?? 'N/A'; + }) + ->addColumn('currency', function ($subscription) { + $plan = $subscription->item?->plansV1; + return $plan ? $plan->currency : 'N/A'; + }) + ->addColumn('first_name', function ($subscription) { + return $subscription->first_name ?? 'N/A'; + }) + ->addColumn('last_name', function ($subscription) { + return $subscription->last_name ?? 'N/A'; + }) + ->filterColumn('first_name', function ($query, $keyword) { + $query->where('patients.first_name', 'like', "%{$keyword}%"); + }) + ->filterColumn('last_name', function ($query, $keyword) { + $query->where('patients.last_name', 'like', "%{$keyword}%"); + }) + ->filterColumn('product_title', function ($query, $keyword) { + $query->where('plans_v1.title', 'like', "%{$keyword}%"); + }) + ->filterColumn('product_price', function ($query, $keyword) { + $query->where('plans_v1.price', 'like', "%{$keyword}%"); + }) + ->make(true); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + + + + public function updateSubscription(Request $request, $subid) + { + try { + $this->authorizeForUser($this->user, 'edit', new Subscription); + // Find the subscription + $subscription = Subscription::find($subid); + if (!$subscription) { + return response()->json(['message' => 'Subscription not found'], 404); + } + // Define the fillable fields + $fillable = [ + 'subscription_start_date', + 'subscription_renewal_date', + 'subscription_status', + 'cart_id', + 'item_id', + 'patient_id', + 'status' + ]; + // Filter the request data to only include fillable fields that are present + $dataToUpdate = array_filter( + $request->only($fillable), + function ($value) { + return $value !== null; + } + ); + + // Validate the filtered data + $validator = Validator::make($dataToUpdate, [ + 'subscription_start_date' => 'required', + 'subscription_renewal_date' => 'required', + 'subscription_status' => 'string', + 'cart_id' => 'exists:carts,id', + 'item_id' => 'exists:items,id', + 'patient_id' => 'exists:patients,id', + 'status' => 'string', + ]); + + if ($validator->fails()) { + return response()->json(['errors' => $validator->errors()], 422); + } + // Update the subscription + $subscription->update($dataToUpdate); + + return response()->json(['message' => 'Subscription updated successfully', 'data' => $subscription], 200); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function CreateSubscription(Request $request) + { + try { + $this->authorizeForUser($this->user, 'add', new Subscription); + // Validate the incoming request data + $validator = Validator::make($request->all(), [ + 'subscription_start_date' => 'required', + 'subscription_renewal_date' => 'required', + 'subscription_status' => 'required', + 'cart_id' => 'required|exists:carts,id', + 'item_id' => 'required|exists:items,id', + 'patient_id' => 'required|exists:patients,id', + //'status' => 'required' + ]); + + if ($validator->fails()) { + return response()->json(['errors' => $validator->errors()], 422); + } + + // Create the subscription + $subscription = Subscription::create($request->all()); + + return response()->json([ + 'message' => 'Subscription created successfully', + 'data' => $subscription + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + + public function getSubscription(Subscription $subscription, Request $request) + { + try { + $this->authorizeForUser($this->user, 'list', new Subscription); + return response()->json([ + 'data' => $subscription + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function deleteSubscription(Subscription $subscription, Request $request) + { + try { + $this->authorizeForUser($this->user, 'delete', new Subscription); + $subscription->delete(); + return response()->json([ + 'status' => 'deleted', + 'message' => 'subscription deleted' + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } +} diff --git a/app/Http/Controllers/Admin/Api/TelemedProAgentController.php b/app/Http/Controllers/Admin/Api/TelemedProAgentController.php new file mode 100644 index 0000000..7280172 --- /dev/null +++ b/app/Http/Controllers/Admin/Api/TelemedProAgentController.php @@ -0,0 +1,425 @@ +url = $url; + $this->user = Auth::guard('admin')->user(); + } + public function register(Request $request) + { + try { + $this->authorizeForUser($this->user, 'add', new Telemedpro); + // Validate the request data + $validator = Validator::make($request->all(), [ + 'first_name' => ['required', 'string', 'max:255'], + 'last_name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:telemed_pros'], + 'password' => ['required', 'string', 'min:8'], + ]); + + if ($validator->fails()) { + return response()->json([ + 'errors' => $validator->errors() + ], 422); + } + + $first_name = $request->input('first_name'); + $last_name = $request->input('last_name'); + $email = $request->input('email'); + $digits = 4; + $code = rand(pow(10, $digits - 1), pow(10, $digits) - 1); + + // Prepare data for creating a new Telemedpro user + $userData = [ + 'name' => $first_name . " " . $last_name, + 'first_name' => $first_name, + 'last_name' => $last_name, + 'email' => $email, + 'password' => Hash::make($request->input('password')), + 'status' => 1, + 'email_verification' => $code, + 'home_address' => $request->input('home_address'), + 'city' => $request->input('city'), + 'state' => $request->input('state'), + 'zip_code' => $request->input('zip_code'), + 'medical_license_number' => json_encode($request->input('medical_license_number')), // Convert to JSON string + 'years_of_experience' => $request->input('years_of_experience'), + 'specialty' => $request->input('specialty'), + 'gender' => $request->input('gender'), + 'practice_state' => json_encode($request->input('practice_state')), // Convert to JSON string + 'phone_number' => $request->input('phone'), + 'availability_from' => $request->input('availabilityFrom'), + 'availability_to' => $request->input('availabilityTo'), + ]; + + // Create the new user + $user = Telemedpro::create($userData); + + // Create an auth token + $token = $user->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'user' => $user, + 'token' => $token, + 'message' => 'User registered successfully', + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function details($id) + { + try { + $this->authorizeForUser($this->user, 'view', new Telemedpro); + return response()->json([ + 'provider' => Telemedpro::find($id) + + ], 201); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + + + public function telemedProFullDetail(Telemedpro $telemed) + { + try { + $this->authorizeForUser($this->user, 'view', new Telemedpro); + $upcomingMeetings = Cart::select( + 'carts.id as order_id', + 'appointments.id', + 'appointments.patient_id', + 'appointments.appointment_time', + 'appointments.appointment_date', + DB::raw( + 'CONCAT(patients.first_name, " " , patients.last_name) as patient_name' + ) + ) + ->join('appointments', 'appointments.id', 'carts.appointment_id') + ->leftJoin('patients', 'patients.id', 'appointments.patient_id') + ->where("appointments.telemed_pros_id", $telemed->id) + ->where('appointments.appointment_date', ">=", Carbon::now()->format("Y-m-d")) + ->get(); + $completedMeetings = Cart::select( + 'carts.id as order_id', + 'appointments.patient_id', + 'appointments.appointment_time', + 'appointments.appointment_date', + 'appointments.start_time', + 'appointments.end_time', + 'telemed_pros.name as provider_name', + 'appointments.telemed_pros_id as provider_id', + + DB::raw( + 'CONCAT(patients.first_name, " " , patients.last_name) as patient_name' + ) + ) + ->join('appointments', 'appointments.id', 'carts.appointment_id') + ->leftJoin('patients', 'patients.id', 'appointments.patient_id') + ->leftJoin('telemed_pros', 'telemed_pros.id', 'appointments.telemed_pros_id') + //->leftJoin('carts', 'appointments.id', 'carts.appointment_id') + ->where("appointments.telemed_pros_id", $telemed->id) + ->where('appointments.start_time', "!=", null) + ->where('appointments.end_time', "!=", null) + ->get(); + $patientNotes = PatientNote::select( + 'patient_notes.note', + 'patient_notes.note_type', + 'telemed_pros.name as provider_name', + 'telemed_pros.id as provider_id', + 'patient_notes.created_at', + 'carts.id as order_id', + 'patient_notes.created_by_id', + 'patient_notes.created_by_type' + ) + ->leftJoin('telemed_pros', 'patient_notes.telemed_pros_id', 'telemed_pros.id') + ->leftJoin('appointments', 'patient_notes.appointment_id', 'appointments.id') + ->leftJoin('carts', 'appointments.id', 'carts.appointment_id') + ->where("appointments.telemed_pros_id", $telemed->id) + ->get(); + + foreach ($patientNotes as $notes) { + if ($notes->note_type != 'Notes') + $notes->note = $this->url->to("assets/files/" . $notes->patient_id . ".png"); + else + $notes->note = $notes->note; + } + $patientPrescription = PatientPrescription::select( + 'patient_prescription.*', + 'telemed_pros.name as provider_name', + 'prescriptions.*', + 'carts.id as order_id' + ) + ->leftJoin('appointments', 'patient_prescription.appointment_id', 'appointments.id') + ->leftJoin('carts', 'appointments.id', 'carts.appointment_id') + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', 'telemed_pros.id') + ->leftJoin('prescriptions', 'prescriptions.id', 'patient_prescription.prescription_id') + ->where('appointments.telemed_pros_id', $telemed->id)->get(); + return response()->json([ + 'telemed' => $telemed, + 'upcomingMeetings' => $upcomingMeetings, + 'completed_meetings' => $completedMeetings, + 'notes' => $patientNotes, + 'prescriptions' => $patientPrescription, + + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function telemedList(Request $request) + { + try { + $this->authorizeForUser($this->user, 'list', new Telemedpro); + // Get filter inputs from the request + $practiceState = $request->input('practice_state'); + $gender = $request->input('gender'); + $specialty = $request->input('specialty'); + $state = $request->input('state'); + $search = $request->input('search'); + $availabilityFrom = $request->input('availability_from'); + $availabilityTo = $request->input('availability_to'); + + // Build the query with optional filters and join + $query = Telemedpro::query() + ->leftJoin('appointments', 'telemed_pros.id', '=', 'appointments.telemed_pros_id') + ->leftJoin('carts', 'appointments.id', '=', 'carts.appointment_id') + ->select( + 'telemed_pros.id', + 'telemed_pros.name', + 'telemed_pros.first_name', + 'telemed_pros.last_name', + 'telemed_pros.email', + 'telemed_pros.is_busy', + 'telemed_pros.recording_switch', + 'telemed_pros.ai_switch', + 'telemed_pros.status', + 'telemed_pros.practice_state', + 'telemed_pros.phone_number', + 'telemed_pros.gender', + 'telemed_pros.specialty', + 'telemed_pros.home_address', + 'telemed_pros.medical_license_number', + 'telemed_pros.years_of_experience', + 'telemed_pros.email_verification', + 'telemed_pros.city', + 'telemed_pros.state', + 'telemed_pros.zip_code', + 'telemed_pros.availability_to', + 'telemed_pros.availability_from' + ) + ->selectRaw('COUNT(DISTINCT carts.id) as meeting_count') + ->groupBy( + 'telemed_pros.id', + 'telemed_pros.name', + 'telemed_pros.first_name', + 'telemed_pros.last_name', + 'telemed_pros.email', + 'telemed_pros.is_busy', + 'telemed_pros.recording_switch', + 'telemed_pros.ai_switch', + 'telemed_pros.status', + 'telemed_pros.practice_state', + 'telemed_pros.phone_number', + 'telemed_pros.gender', + 'telemed_pros.specialty', + 'telemed_pros.home_address', + 'telemed_pros.medical_license_number', + 'telemed_pros.years_of_experience', + 'telemed_pros.email_verification', + 'telemed_pros.city', + 'telemed_pros.state', + 'telemed_pros.zip_code', + 'telemed_pros.availability_to', + 'telemed_pros.availability_from' + ); + + if ($practiceState && $practiceState !== 'All') { + $query->where('telemed_pros.practice_state', $practiceState); + } + if ($gender && $gender !== 'All') { + $query->where('telemed_pros.gender', $gender); + } + if ($specialty && $specialty !== 'All') { + $query->where('telemed_pros.specialty', $specialty); + } + if ($state && $state !== 'All') { + $query->where('telemed_pros.state', $state); + } + if ($availabilityFrom && $availabilityFrom !== 'All') { + $query->where('telemed_pros.availability_from', '<=', $availabilityFrom); + } + if ($availabilityTo && $availabilityTo !== 'All') { + $query->where('telemed_pros.availability_to', '>=', $availabilityTo); + } + return DataTables::of($query) + ->addColumn('availability_from', function ($telemedpro) { + return $telemedpro->availability_from; + }) + ->addColumn('availability_to', function ($telemedpro) { + return $telemedpro->availability_to; + }) + ->addColumn('specialty', function ($telemedpro) { + return $telemedpro->specialty; + }) + ->addColumn('meeting_count', function ($telemedpro) { + return $telemedpro->meeting_count; + }) + ->make(true); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + + public function telemed(Telemedpro $telemed) + { + return response()->json([ + 'patient' => $telemed + ]); + } + public function telemedDelete(Telemedpro $telemed) + { + try { + $this->authorizeForUser($this->user, 'delete', new Telemedpro); + $telemed->delete(); + return response()->json([ + 'message' => "Deleted Successfully" + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function telemedUpdate(Telemedpro $telemed, Request $request) + { + try { + $this->authorizeForUser($this->user, 'delete', new Subscription); + $first_name = $request->input('first_name'); + $last_name = $request->input('last_name'); + $email = $request->input('email'); + + + $telemed->name = $first_name . " " . $last_name; + $telemed->first_name = $first_name; + $telemed->last_name = $last_name; + $telemed->email = $email; + $telemed->password = Hash::make($request->input('password')); + $telemed->status = 1; + $telemed->home_address = $request->input('home_address'); + $telemed->city = $request->input('city'); + $telemed->state = $request->input('state'); + $telemed->zip_code = $request->input('zip_code'); + $telemed->medical_license_number = json_encode($request->input('medical_license_number')); // Convert to JSON string + $telemed->years_of_experience = $request->input('years_of_experience'); + $telemed->specialty = $request->input('specialty'); + $telemed->gender = $request->input('gender'); + $telemed->practice_state = json_encode($request->input('practice_state')); // Convert to JSON string + $telemed->phone_number = $request->input('phone'); + $telemed->availability_from = $request->input('availabilityFrom'); + $telemed->availability_to = $request->input('availabilityTo'); + $telemed->save(); + return response()->json([ + 'message' => 'Telemedpro updated successfully', + 'telemed' => $telemed + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } + public function getMeetingHistoryTelemedpro(Telemedpro $telemedpro, $filter = '12_months') + { + try { + $this->authorizeForUser($this->user, 'meeting_history', new Telemedpro); + $currentMonth = Carbon::now(); + // Filter logic + switch ($filter) { + case 'current_month': + $startDate = $currentMonth->copy()->startOfMonth(); + break; + case '1_month': + $startDate = $currentMonth->copy()->subMonth()->startOfMonth(); + break; + case '2_months': + $startDate = $currentMonth->copy()->subMonths(2)->startOfMonth(); + break; + case '3_months': + $startDate = $currentMonth->copy()->subMonths(3)->startOfMonth(); + break; + case '6_months': + $startDate = $currentMonth->copy()->subMonths(6)->startOfMonth(); + break; + default: // Default to 12 months + $startDate = $currentMonth->copy()->subMonths(12)->startOfMonth(); + } + $endDate = $currentMonth->endOfMonth(); + // Fetch patient names and appointment counts directly from the database + $monthlyData = Appointment::select( + 'patient_id', + 'telemed_pros_id', + 'appointment_time', + 'appointment_date', + 'start_time', + 'end_time', + 'duration', + 'id' + ) + ->where("telemed_pros_id", $telemedpro->id) + ->whereNotNull("end_time") + ->whereBetween('created_at', [$startDate, $endDate]) + ->get(); + $patients = []; + foreach ($monthlyData as $dataPoint) { + $patientName = $dataPoint->patient->first_name . " " . $dataPoint->patient->last_name; // Assuming 'name' is the field representing patient names + /* $appointmentCount = $dataPoint->appointment_count; */ + $start_time = $dataPoint->start_time; + $end_time = $dataPoint->end_time; + $duration = $dataPoint->duration; + $appointment_time = $dataPoint->appointment_time; + $appointment_date = $dataPoint->appointment_date; + $patient_id = $dataPoint->patient_id; + $id = $dataPoint->id; + + $patients[] = [ + 'patient_name' => $patientName, + 'appointment_time' => $appointment_time, + 'appointment_date' => $appointment_date, + /* 'appointment_count' => $appointmentCount, */ + 'start_time' => $start_time, + 'end_time' => $end_time, + 'duration' => $duration, + 'id' => $id, + 'patient_id' => $patient_id, + ]; + } + return response()->json([ + 'patients' => $patients, + ]); + } catch (AuthorizationException $e) { + return $e->getMessage(); + } + } +} diff --git a/app/Http/Controllers/Admin/Auth/ConfirmPasswordController.php b/app/Http/Controllers/Admin/Auth/ConfirmPasswordController.php new file mode 100644 index 0000000..138c1f0 --- /dev/null +++ b/app/Http/Controllers/Admin/Auth/ConfirmPasswordController.php @@ -0,0 +1,40 @@ +middleware('auth'); + } +} diff --git a/app/Http/Controllers/Admin/Auth/ForgotPasswordController.php b/app/Http/Controllers/Admin/Auth/ForgotPasswordController.php new file mode 100644 index 0000000..465c39c --- /dev/null +++ b/app/Http/Controllers/Admin/Auth/ForgotPasswordController.php @@ -0,0 +1,22 @@ +middleware('guest')->except('logout'); + } + + public function showLoginForm() + { + return view('admin.auth.login'); + } + + protected function login(Request $request) + { + $credentials = $request->only('email', 'password'); + + $user = Admin::where($this->username(), $credentials['email'])->first(); + + if ($user && Hash::check($credentials['password'], $user->password)) { + Auth::guard('admin')->login($user, $request->has('remember')); + return redirect($this->redirectTo); + } + + return back()->withErrors(['email' => 'Invalid credentials']); + } + + public function redirectPath() { + return "/admin"; + } +} diff --git a/app/Http/Controllers/Admin/Auth/RegisterController.php b/app/Http/Controllers/Admin/Auth/RegisterController.php new file mode 100644 index 0000000..9a63781 --- /dev/null +++ b/app/Http/Controllers/Admin/Auth/RegisterController.php @@ -0,0 +1,80 @@ +middleware('guest'); + } + + public function showRegisterForm() + { + return view('admin.auth.register'); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @return \App\Models\Admin + */ + protected function register(Request $request) + { + Admin::create([ + 'name' => $request->input('name'), + 'email' => $request->input('email'), + 'password' => bcrypt($request->input('password')), + ]); + return back(); + } +} diff --git a/app/Http/Controllers/Admin/Auth/ResetPasswordController.php b/app/Http/Controllers/Admin/Auth/ResetPasswordController.php new file mode 100644 index 0000000..b1726a3 --- /dev/null +++ b/app/Http/Controllers/Admin/Auth/ResetPasswordController.php @@ -0,0 +1,30 @@ +middleware('auth'); + $this->middleware('signed')->only('verify'); + $this->middleware('throttle:6,1')->only('verify', 'resend'); + } +} diff --git a/app/Http/Controllers/Admin/DoctorController.php b/app/Http/Controllers/Admin/DoctorController.php new file mode 100644 index 0000000..1468227 --- /dev/null +++ b/app/Http/Controllers/Admin/DoctorController.php @@ -0,0 +1,70 @@ + $doctors]); + } + + public function add() + { + return view('admin.doctors.add'); + } + + public function save(Request $request) + { + $doctor = Doctor::where('email',$request->input('email'))->first(); + if($doctor) + { + $request->session()->flash('error', 'The email has already been taken.'); + return redirect()->back(); + } + Doctor::create([ + 'name' => $request->input('name'), + 'email' => $request->input('email'), + 'password' => bcrypt($request->input('password')), + ]); + $request->session()->flash('message', 'Doctor created successfully'); + return redirect()->back(); + } + + public function edit($id) + { + $doctor = Doctor::where('id',$id)->first(); + return view('admin.doctors.edit', ['doctor' => $doctor]); + } + + public function update($id,Request $request) + { + $doctor = Doctor::where('id',$id)->first(); + $request->validate([ + 'name' => 'required', + 'email' => 'required|email|unique:doctors,email,' . $id, + // Other validation rules... + ]); + $doctor->name = $request->input('name'); + $doctor->email = $request->input('email'); + if($request->input('password')) + $doctor->password = $request->input('password'); + $doctor->save(); + + $request->session()->flash('message', 'Doctor updated successfully'); + return redirect()->back(); + } + + public function delete($id,Request $request) + { + Doctor::where('id',$id)->delete(); + $request->session()->flash('message', 'Doctor deleted successfully'); + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php new file mode 100644 index 0000000..59ee42c --- /dev/null +++ b/app/Http/Controllers/Admin/HomeController.php @@ -0,0 +1,14 @@ + $labs]); + } + public function create() + { + return view('admin.labs.create'); + } + public function save(Request $request) + { + Lab::create([ + 'name' => $request->name, + 'address' => $request->address, + 'city' => $request->city, + 'state' => $request->state, + 'zip_code' => $request->zip + ]); + return redirect('admin/labs'); + } + public function edit($id) + { + $labEdit = Lab::find($id); + return view('admin.labs.edit', ['labEdit' => $labEdit]); + } + public function update($id, Request $request) + { + $labEdit = Lab::find($id); + $labEdit->name = $request->name; + $labEdit->address = $request->address; + $labEdit->city = $request->city; + $labEdit->state = $request->state; + $labEdit->zip_code = $request->zip; + $labEdit->save(); + return redirect('admin/labs'); + } + public function getOrderData(Request $request) + { + $perPage = $request->get('per_page', 20); // Items per page (default 10) + + // Get carts with patient data and order count + $carts = Cart::with('patient') + ->select('patient_id', DB::raw('COUNT(*) as cart_count')) + ->groupBy('patient_id') + ->paginate($perPage); + + // Manually create paginator instance (due to aggregation) + $paginator = new LengthAwarePaginator( + $carts->items(), + $carts->total(), + $perPage, + $carts->currentPage() + ); + + return response()->json([ + 'status' => 'Success', + 'orderData' => $paginator + ], 200); + } +} diff --git a/app/Http/Controllers/Admin/PatientController.php b/app/Http/Controllers/Admin/PatientController.php new file mode 100644 index 0000000..fb380ee --- /dev/null +++ b/app/Http/Controllers/Admin/PatientController.php @@ -0,0 +1,24 @@ + $patients]); + } + + public function delete($id,Request $request) + { + Patient::where('id',$id)->delete(); + $request->session()->flash('message', 'Patient deleted successfully'); + return redirect()->back(); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Agent/AppointmentController.php b/app/Http/Controllers/Agent/AppointmentController.php new file mode 100644 index 0000000..38b10d2 --- /dev/null +++ b/app/Http/Controllers/Agent/AppointmentController.php @@ -0,0 +1,913 @@ +id)->get(); + foreach ($appointments as $appointment) { + $patient = Patient::where('id', $appointment->patient_id)->first(); + array_push($data, [ + 'id' => $appointment->id, + 'patient_name' => $patient->first_name . " " . $patient->last_name, + 'patient_id' => $appointment->patient_id, + 'telemed_pros_id ' => $appointment->telemed_pros_id, + 'appointment_time' => $appointment->appointment_time, + 'meeting_id' => $appointment->meeting_id, + 'created_at' => $appointment->created_at, + 'updated_at' => $appointment->updated_at, + 'in_call' => $appointment->in_call + ]); + } + // dd($data); + return view('agent.appointments.index', ['appointments' => $data]); + } + + public function profile(Request $request) + { + $user = Auth::user(); + return response()->json(['profile' => $user], 200); + } + public function profileImageUpload(Patient $patient, Request $request) + { + $user = $patient; + $image = $request->get('image'); + $fileName = 'profile-' . time(); + + $logo = base64_decode($image); + $ext = (explode('/', finfo_buffer(finfo_open(), $logo, FILEINFO_MIME_TYPE))[1]); + + $imageName = $fileName . '.' . $ext; + Storage::disk('local')->put("public/profile_pictures/" . $imageName, $logo); + $user->profile_picture = $imageName; + $user->save(); + return response()->json(['profile' => $user], 200); + } + + public function delete($id, Request $request) + { + Appointment::where('id', $id)->delete(); + $request->session()->flash('message', 'Appointment deleted successfully'); + return redirect()->back(); + } + + public function patientDetails($id) + { + $appointment = Appointment::where('id', $id)->first(); + $patient = Patient::where('id', $appointment->patient_id)->first(); + return view('agent.appointments.patient-details', ['patient' => $patient, 'appointment_id' => $appointment->id]); + } + public function patientProfileDetails(Patient $patient) + { + return response()->json(['patient' => $patient], 200); + } + + + public function patientAddress($id) + { + $patient = Patient::where('id', $id)->first(); + return view('agent.appointments.patient-address', ['patient' => $patient]); + } + + public function savePatientAddress($id, Request $request) + { + $appointment = Appointment::where('id', $id)->first(); + $patient = Patient::where('id', $appointment->patient_id)->first(); + + $address = $request->input('address'); + $city = $request->input('city'); + $state = $request->input('state'); + $zip_code = $request->input('zip_code'); + + $patient->address = $address; + $patient->city = $city; + $patient->state = $state; + $patient->zip_code = $zip_code; + $patient->save(); + + $request->session()->flash('message', 'Address saved successfully'); + + + return view('agent.appointments.patient-details', ['patient' => $patient, 'appointment_id' => $appointment->id]); + } + + public function patientLabs($id) + { + $patient = Patient::where('id', $id)->first(); + $labs = Lab::where('city', 'like', '%' . $patient->city . '%') + ->orWhere('state', 'like', '%' . $patient->state . '%') + ->orWhere('zip_code', 'like', '%' . $patient->zip_code . '%') + ->get(); + return view('agent.appointments.patient-labs', ['patient' => $patient, 'labs' => $labs]); + } + + public function patientBookLab(Appointment $appointment, Request $request) + { + + $lab = new Lab(); + $lab->address = $request->input('lab_address'); + $lab->name = $request->input('lab_name'); + $lab->city = $request->input('lab_city'); + $lab->state = $request->input('lab_state'); + $lab->distance = $request->input('lab_distance'); + $lab->contact_no = $request->input('lab_contact_no'); + $lab->lang = $request->input('lab_lang'); + $lab->lat = $request->input('lab_lat'); + + $lab->slot_date = $request->input('slot_date'); + $lab->slot_time = $request->input('slot_time'); + $lab->booking_time = Carbon::now()->format('Y-m-d H:i:s'); + + $lab->appointment_id = $appointment->id; + $lab->save(); + + return ['message' => 'Lab booked successfully']; + } + public function patientBookLabGet(Appointment $appointment) + { + $lab = Lab::where("appointment_id", $appointment->id)->first(); + return response()->json([ + 'lab_name' => $lab->name, + 'lab_address' => $lab->address, + 'lab_city' => $lab->city, + 'lab_state' => $lab->state, + 'lab_distance' => $lab->distance, + 'lab_contact_no' => $lab->contact_no, + 'lab_lang' => $lab->lang, + 'lab_lat' => $lab->lat, + 'slot_date' => $lab->slot_date, + 'slot_time' => $lab->slot_time, + 'booking_time' => $lab->booking_time, + ]); + } + + public function doctorAppointment($id) + { + $appointment = Appointment::where('id', $id)->first(); + $patient = Patient::where('id', $appointment->patient_id)->first(); + $doctors = Doctor::all(); + return view('agent.appointments.doctor-appointment', ['patient' => $patient, 'doctors' => $doctors, 'appointment' => $appointment]); + } + public function pendingAppointmentDetail() + { + + $appointments = Appointment::whereNull('end_time') + ->where(function ($query) { + $startTimeM = Carbon::now()->subHours(12)->format('Y-m-d'); + $endTimeM = Carbon::now()->addHours(12)->format('Y-m-d'); + $startTime = Carbon::now()->subHours(12)->format('H:i:s'); + $endTime = Carbon::now()->addHours(12)->format('H:i:s'); + $query //->where('appointment_time', '>=', $startTime) + //->where('appointment_time', '<=', $endTime) + ->where('appointment_date', '>=', $startTimeM) + ->where('appointment_date', '<=', $endTimeM); + }) + ->with('patient:id,first_name,last_name,address,city,state,zip_code,country') + ->get([ + 'id', + 'patient_id', + 'appointment_time', + 'appointment_date', + 'start_time', + 'end_time', + 'duration', + 'timezone' + ]) + ->groupBy('patient_id', 'appointment_time', 'appointment_date', 'start_time') + ->map(function ($group) { + $patient = $group->first()->patient; + $appointments = $group->sortByDesc('id'); + $array = []; + foreach ($appointments as $appointment) { + $filePath = public_path("assets/profiles/{$patient->id}.png"); + + if (File::exists($filePath)) { + $patient->url = "/assets/profiles/{$patient->id}.png"; + } else { + $patient->url = null; + } + $cart = Cart::where("appointment_id", $appointment->id)->first(); + $array[] = [ + 'id' => $patient->id, + 'patient_timezone' => $patient->timezone, + 'appointment_timezone' => $appointment->timezone, + 'name' => $patient->first_name . ' ' . $patient->last_name, + 'address' => $patient->address, + 'city' => $patient->city, + 'state' => $patient->state, + 'zip_code' => $patient->zip_code, + 'url' => $patient->url, + 'country' => $patient->country, + 'time' => time(), + 'order_id' => $cart->id ?? "", + 'appointment' => [ + 'id' => $appointment->id, + 'appointment_time' => $appointment->appointment_time, + 'appointment_date' => $appointment->appointment_date, + 'start_time' => $appointment->start_time, + 'end_time' => $appointment->end_time, + 'duration' => $appointment->duration, + 'timezone' => $appointment->timezone + + ] + ]; + } + return $array; + })->flatten(1); + + return response()->json($appointments, 200); + } + + + + public function doctorAppointmentSave($id, Request $request) + { + $appointment_date = $request->input('appointment_date'); + $appointment_time = $request->input('appointment_time'); + $appointment_date = new DateTime($appointment_date); + $appointment_date = $appointment_date->format('Y-m-d'); + $doctor_id = $request->input('doctor_id'); + + $appointment = Appointment::where('id', $id)->first(); + + $doctorAppointment = DoctorAppointment::where('patient_id', $appointment->patient_id) + ->where('doctor_id', $doctor_id) + ->where('appointment_id', $id) + ->where('appointment_date', $appointment_date) + ->where('appointment_time', $appointment_time) + ->first(); + + if (empty($doctorAppointment)) { + DoctorAppointment::create([ + 'patient_id' => $appointment->patient_id, + 'doctor_id' => $doctor_id, + 'appointment_id' => $id, + 'appointment_date' => $appointment_date, + 'appointment_time' => $appointment_time + ]); + + + return response()->json(['message' => 'Doctor appointment booked'], 200); + } else { + return response()->json(['message' => 'Error in booking Appointment!'], 200); + } + return redirect()->back(); + } + + public function patientTasks($id) + { + $appointment = Appointment::where('id', $id)->first(); + $patient = Patient::where('id', $appointment->patient_id)->first(); + $patientTasks = PatientTask::where('patient_id', $appointment->patient_id)->get(); + return view('agent.appointments.patient-tasks', ['patient' => $patient, 'appointment' => $appointment, 'patientTasks' => $patientTasks]); + } + + public function patientTasksSave($id, Request $request) + { + $description = $request->input('description'); + + PatientTask::create([ + 'patient_id' => $id, + 'description' => $description, + ]); + + $request->session()->flash('message', 'Task saved successfully'); + return redirect()->back(); + } + + public function patientTaskDelete($id, Request $request) + { + PatientTask::where('id', $id)->delete(); + $request->session()->flash('message', 'Task deleted successfully'); + return redirect()->back(); + } + public function questions() + { + return 'ddd'; + } + public function questionsList() + { + $questionsData = []; + $groups = QuestionGroup::all(); + foreach ($groups as $group) { + $questions = Question::where('group_id', $group->id)->get()->toArray(); + $questionsData[$group->title] = $questions; + } + return response()->json( + $questionsData + ); + } + public function questionsAnswers($patient_id, $appointment_id) + { + $questionsData = []; + $answers = []; + $groups = QuestionGroup::all(); + foreach ($groups as $group) { + $questions = Question::where('group_id', $group->id)->get(); + foreach ($questions as $question) { + + $answer = PatientAnswer::where('question_id', $question->id)->where('patient_id', $patient_id)->where('appointment_id', $appointment_id)->first(); + if (isset($answer->answer)) + $question['answer'] = $answer->answer; + else + $question['answer'] = null; + } + $questionsData[$group->title] = $questions; + } + return response()->json( + $questionsData + ); + } + + public function storeAnswers(Patient $patient, Appointment $appointment, Request $request) + { + + $data = $request->input("data"); + + foreach ($data as $row) { + if (isset($row['answer'])) + PatientAnswer::create([ + 'patient_id' => $patient->id, + 'appointment_id' => $appointment->id, + 'question_id' => $row['question_id'], + 'answer' => $row['answer'] + ]); + } + + return response()->json([ + 'message' => 'Answers stored successfully' + ]); + } + public function getQuestions(Patient $patient, Appointment $appointment, Request $request) + { + $questions = PatientAnswer::select('questions.*', 'patient_answers.answer') + ->leftJoin('questions', 'questions.id', '=', 'patient_answers.question_id') + ->where('patient_answers.patient_id', $patient->id) + ->where('patient_answers.appointment_id', $appointment->id) + ->get(); + + return response()->json([ + 'questions' => $questions + ]); + } + public function switchButton(Telemedpro $agent, $switch) + { + if ($switch == 1) + $agent->recording_switch = $switch; + else + $agent->recording_switch = 0; + $agent->save(); + + return response()->json([ + 'message' => 'Recording Switched: ' . $switch + ]); + } + public function switchButtonGet(Telemedpro $agent) + { + return response()->json([ + 'recording_switch' => $agent->recording_switch, + ]); + } + public function switchAiButton(Telemedpro $agent, $switch) + { + if ($switch == 1) + $agent->ai_switch = $switch; + else + $agent->ai_switch = 0; + $agent->save(); + + return response()->json([ + 'message' => 'AI Switched: ' . $switch + ]); + } + public function switchAiButtonGet(Telemedpro $agent) + { + return response()->json([ + 'ai_switch' => $agent->ai_switch, + ]); + } + public function getProfile(Request $request) + { + $user = Auth::user(); + return response()->json([ + 'ai_switch' => $user, + ]); + } + + + public function questionsListV1() + { + $questionGroups = QuestionGroup::with('questions')->get(); + + $formattedGroups = []; + + foreach ($questionGroups as $group) { + $formattedGroups[$group->title] = []; + foreach ($group->questions as $question) { + $formattedGroups[$group->title][$question->question] = [$question->type => unserialize($question->options)]; + } + } + + return response()->json($formattedGroups); + } + public function DeviceCurrentStatus(Request $request) + { + /* $patient = Patient::where("id", $patient_id)->firstOrFail(); */ + $micStatus = $request->input('micStatus'); + $camStatus = $request->input('camStatus'); + event(new DeviceCurrentStatus($micStatus, $camStatus)); + + return true; + } + public function getAnalytics($filter = '12_months') + { + $currentMonth = Carbon::now(); + + // Filter logic + switch ($filter) { + case 'current_month': + $startDate = $currentMonth->copy()->startOfMonth(); + break; + case '1_month': + $startDate = $currentMonth->copy()->subMonth()->startOfMonth(); + break; + case '2_months': + $startDate = $currentMonth->copy()->subMonths(2)->startOfMonth(); + break; + case '3_months': + $startDate = $currentMonth->copy()->subMonths(3)->startOfMonth(); + break; + case '6_months': + $startDate = $currentMonth->copy()->subMonths(6)->startOfMonth(); + break; + default: // Default to 12 months + $startDate = $currentMonth->copy()->subMonths(12)->startOfMonth(); + } + + $endDate = $currentMonth->endOfMonth(); + + + $appointments = Appointment::with('patient') + ->where("telemed_pros_id", Auth::user()->id) + ->whereBetween('created_at', [$startDate, $endDate]) + ->get(); + + $totalSessions = $appointments->count(); + $totalCallTime = 10; // Assuming you have some logic to calculate this + if ($totalSessions != 0) { + $avgSessionTime = $totalCallTime / $totalSessions; + $avgSessionTime = round(($avgSessionTime / 60), 2); + } else + $avgSessionTime = ''; + + + $monthlyData = []; + + // Loop through each month in the last 12 months + for ($date = $startDate->copy(); $date->lte($endDate); $date->addMonth()) { + $monthStart = $date->startOfMonth()->format('Y-m-d'); + $monthEnd = $date->copy()->endOfMonth()->format('Y-m-d'); // Key change here! + + $monthAppointments = Appointment::with('patient') + ->where("telemed_pros_id", Auth::user()->id) + ->whereBetween('created_at', [$monthStart, $monthEnd]) + ->get(); + + + // Calculate any metrics you need from $monthAppointments + $monthlyData[] = [ + 'month' => $date->format('M'), // Example: Jan 2024 + 'appointment_count' => $monthAppointments->count() + // Add other metrics as needed + ]; + } + $monthsList = []; + $monthlySessionCount = []; + + foreach ($monthlyData as $dataPoint) { + $monthsList[] = $dataPoint['month']; + $monthlySessionCount[] = $dataPoint['appointment_count']; + } + + + return response()->json([ + 'total_sessions' => $totalSessions, + 'total_call_time' => $totalCallTime, + 'avg_session_time' => $avgSessionTime, + 'data' => array_values($monthlySessionCount), // Ensure consistent order + 'months_list' => $monthsList, + ]); + } + + public function getMeetingHistory($filter = '12_months') + { + $currentMonth = Carbon::now(); + + // Filter logic + switch ($filter) { + case 'current_month': + $startDate = $currentMonth->copy()->startOfMonth(); + break; + case '1_month': + $startDate = $currentMonth->copy()->subMonth()->startOfMonth(); + break; + case '2_months': + $startDate = $currentMonth->copy()->subMonths(2)->startOfMonth(); + break; + case '3_months': + $startDate = $currentMonth->copy()->subMonths(3)->startOfMonth(); + break; + case '6_months': + $startDate = $currentMonth->copy()->subMonths(6)->startOfMonth(); + break; + default: // Default to 12 months + $startDate = $currentMonth->copy()->subMonths(12)->startOfMonth(); + } + + $endDate = $currentMonth->endOfMonth(); + + // Fetch patient names and appointment counts directly from the database + $monthlyData = Appointment::select( + 'appointments.patient_id', + /* DB::raw('COUNT(*) as appointment_count'), */ + 'appointment_time', + 'appointment_date', + 'start_time', + 'end_time', + 'duration', + 'appointments.id as appointment_id', + 'carts.id as order_id' + ) + ->leftJoin('carts', 'carts.appointment_id', 'appointments.id') + ->where("appointments.telemed_pros_id", Auth::user()->id) + ->whereBetween('appointments.created_at', [$startDate, $endDate]) + + ->get(); + + $patients = []; + + foreach ($monthlyData as $dataPoint) { + $patient = $dataPoint->patient; + /* $appointmentCount = $dataPoint->appointment_count; */ + $start_time = $dataPoint->start_time; + $end_time = $dataPoint->end_time; + $duration = $dataPoint->duration; + $appointment_time = $dataPoint->appointment_time; + $appointment_date = $dataPoint->appointment_date; + $appointment_id = $dataPoint->appointment_id; + $order_id = $dataPoint->order_id; + + $patients[] = [ + 'patient' => $patient, + 'appointment_time' => $appointment_time, + 'appointment_date' => $appointment_date, + /* 'appointment_count' => $appointmentCount, */ + 'start_time' => $start_time, + 'end_time' => $end_time, + 'duration' => $duration, + 'appointment_id' => $appointment_id, + 'order_id' => $order_id + ]; + } + + return response()->json([ + 'patients' => $patients, + ]); + } + public function sessionHistory(Request $request) + { + $user = $request->user(); + + // Assuming user can be either telemedPro or patient + $history = Appointment::select( + 'appointments.*', + 'appointments.patient_id', + 'patients.first_name as patient_name', + 'carts.id as order_id' + ) + ->leftJoin('patients', 'appointments.patient_id', '=', 'patients.id') + ->leftJoin('carts', 'carts.appointment_id', '=', 'appointments.id') + ->where(function ($query) use ($user) { + $query->where('appointments.telemed_pros_id', $user->id); + }) + ->whereNotNull("appointments.end_time") + ->orderBy('appointments.appointment_date', 'desc') + ->get(); + + return response()->json(['history' => $history]); + + return response()->json(['history' => $history]); + } + public function getAppointmentByid($patient, $appointment, Request $request) + { + $user = $request->user(); + + // Assuming user can be either telemedPro or patient + $data = Appointment::select( + 'appointments.*', + 'telemed_pros.name as agent_name', + ) + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', '=', 'telemed_pros.id') + ->where('appointments.telemed_pros_id', $user->id) + ->where('appointments.patient_id', $patient) + ->where('appointments.id', $appointment) + ->first(); + $order = Cart::where('appointment_id', $data->id)->first(); + $orderItems = Item::leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->where('cart_id', $order->id)->get(); + $data->order = $order; + $data->telemedPro; + // $data->order_items = $orderItems; + $totalPrice = 0; + $total_products = 0; + $quantity = []; + $totalShippingCost = 0; + $data->order_notes = PatientNote::where('appointment_id', $appointment)->get(); + foreach ($orderItems as $item) { + $totalShippingCost += $item->shipping_cost; + $item->total_price = $item->quantity * $item->price; + $totalPrice += $item->total_price; + $order->order_total_amount = $totalPrice; + $order->order_total_shipping = $totalShippingCost; + $item->plansV1->qty = $item->quantity; + } + $data->order_items = $orderItems; + + $data->shipping_activity = ItemHistory::where('cart_id', $order->id)->get(); + return response()->json(['data' => $data]); + } + public function addNotePatient(Patient $patient, Appointment $appointment, Request $request) + { + $user = Auth::user(); + $addNotePatient = PatientNote::create([ + 'note' => $request->input('note'), + 'note_type' => $request->input('note_type'), + 'patient_id' => $patient->id, + 'appointment_id' => $appointment->id, + 'telemed_pros_id' => $user->id + ]); + $addNotePatient->file_url = ""; + if ($request->hasFile('file')) { + $file = $request->file('file'); + + $filename = $addNotePatient->id . '.' . $file->getClientOriginalExtension(); + + $file->move(public_path('assets/files'), $filename); + + $addNotePatient->file_url = "assets/files" . $addNotePatient->id . '.' . $file->getClientOriginalExtension(); + } + $patient = $addNotePatient->patient; + $setting = Setting::find(1); + Mail::send('emails.noteAdded', ['patient' => $patient, 'agent' => $user, 'setting' => $setting], function ($message) use ($patient, $user) { + $message->to($patient->email, $patient->first_name) + ->subject('You Have a New Note from ' . $user->name); + }); + return response()->json([ + 'message' => 'Note created', + 'data' => $addNotePatient + ], 200); + } + + public function getNotePatient(Patient $patient, Appointment $appointment, Request $request) + { + $patientNotes = PatientNote::where("patient_id", $patient->id) + ->where("appointment_id", $appointment->id) + ->with('appointment') + ->get(); + + $data = $patientNotes->map(function ($patientNote) { + $fileUrl = "/assets/files/{$patientNote->id}.png"; + $filePath = public_path($fileUrl); + + if (File::exists($filePath)) { + $fileUrl = "/assets/files/{$patientNote->id}.png"; + } else { + $fileUrl = null; + } + + return [ + 'id' => $patientNote->id, + 'note' => $patientNote->note, + 'note_type' => $patientNote->note_type, + 'created_at' => $patientNote->created_at, + 'patient_id' => $patientNote->patient_id, + 'appointment' => $patientNote->appointment, + 'telemedPro' => $patientNote->telemedPro, + 'file_url' => $fileUrl, + 'telemedPro' => $patientNote->appointment?->telemedPro + ]; + }); + + return response()->json([ + 'message' => 'Patient notes retrieved', + 'data' => $data + ], 200); + } + + public function getQuestionBuilderStore(Patient $patient, Request $request) + { + + $questionBuilder = QuestionBuilder::select('key', 'value')->where("customer_id", $patient->id)->get(); + $jsonData = $questionBuilder->mapWithKeys(function ($item) { + return [$item->key => $item->value]; + }); + // Store data + return response()->json([ + 'message' => 'Data Sent', + 'data' => $jsonData + ], 200); + } + public function getPrescription() + { + $prescriptions = Prescription::all(); + return response()->json($prescriptions); + } + public function storePrescription(Request $request) + { + $prescription = Prescription::create($request->all()); + return response()->json($prescription, 200); + } + public function storePatientPrescription(Request $request) + { + $user = Auth::user(); + $prescription = PatientPrescription::create($request->all()); + $prescription->status = "pending"; + $prescription->save(); + $patient = $prescription->patient; + $setting = Setting::find(1); + Mail::send('emails.prescriptionAdd', ['patient' => $patient, 'prescription' => $prescription, 'setting' => $setting], function ($message) use ($patient, $user) { + $message->to($patient->email, $patient->first_name) + ->subject('New Prescription Details from ' . $user->name); + }); + return response()->json($prescription, 200); + } + public function updateStatusPrescription($patient_prescription_id, Request $request) + { + $status = $request->input("status"); + $prescription = PatientPrescription::find($patient_prescription_id); + $prescription->status = $status; + $prescription->save(); + $patient = $prescription->patient; + $setting = Setting::find(1); + Mail::send('emails.prescriptionUpdated', ['patient' => $patient, 'setting' => $setting], function ($message) use ($patient) { + $message->to($patient->email, $patient->first_name) + ->subject('Prescription updated.'); + }); + return response()->json($prescription, 200); + } + + public function getStatusPrescription($patient_prescription_id) + { + $prescription = PatientPrescription::find($patient_prescription_id); + return response()->json($prescription, 200); + } + public function getPatientPrescription($patient_id, $appointment_id) + { + $patientPrescription = PatientPrescription::with('prescription') + ->where('patient_id', $patient_id) + ->where('appointment_id', $appointment_id) + ->get(); + + $prescriptionData = []; + foreach ($patientPrescription as $prescription) { + $prescriptionData[] = [ + 'patient' => $prescription->patient, + 'prescription' => $prescription->prescription, + 'created_at' => $prescription->created_at, + 'updated_at' => $prescription->updated_at, + 'direction_one' => $prescription->direction_one, + 'direction_two' => $prescription->direction_two, + 'dont_substitute' => $prescription->dont_substitute, + 'comments' => $prescription->comments, + 'appointment_id' => $prescription->appointment_id, + 'status' => $prescription->status, + 'appointment' => $prescription->appointment, + 'telemedPro' => $prescription->appointment->telemedPro, + 'licenseNumber' => LicenseNumberModel::where("provider_id", $patient_id)->orderBy('id', 'DESC')->first() + ]; + } + if (!$patientPrescription->isEmpty()) { + return response()->json($prescriptionData); + } else { + return response()->json(['message' => 'Prescription not found'], 404); + } + } + public function getOrderData(Cart $cart, Request $request) + { + $cart = Cart::with("patient")->get(); + return response()->json(['cart' => $cart], 200); + } + + public function getLabKit(Cart $cart, Request $request) + { + $kit = LabKit::all(); + return response()->json(['kit' => $kit], 200); + } + + public function orderLabKit(LabKit $labkit, Patient $patient, Request $request) + { + + + $user = $patient; + $cart = new Cart(); + $cart->lab_kit_id = $labkit->id; + $cart->first_name = $patient->first_name; + $cart->last_name = $patient->last_name; + $cart->email = $patient->email; + $cart->phone = $patient->phone_no; + $cart->status = "pending"; + + $cart->date_of_birth = $patient->dob ?? null; + + $cart->patient_id = $user->id; + + $cart->shipping_address1 = $patient->address; + $cart->shipping_city = $patient->city; + $cart->shipping_state = $patient->state; + $cart->shipping_zipcode = $patient->zip_code; + $cart->shipping_country = $patient->country; + + + $cart->shipping_amount = $labkit->amount; + $cart->total_amount = $labkit->amount; + + $cart->save(); + return response()->json(['status' => 'Success', 'cart' => $cart], 200); + } + + public function getorderedLabKit(LabKit $labkit, Patient $patient, Request $request) + { + $detail = Cart::select("carts.*", "lab_kit.name")->leftJoin("lab_kit", "carts.lab_kit_id", "=", "lab_kit.id") + ->where("carts.lab_kit_id", $labkit->id) + ->where("carts.patient_id", $patient->id) + ->get(); + + return response()->json(['order' => $detail], 200); + } + public function getorderedLabKitBasedOnPatient(Patient $patient, Request $request) + { + $detail = Cart::select("carts.*", "lab_kit.name")->leftJoin("lab_kit", "carts.lab_kit_id", "=", "lab_kit.id") + ->where("carts.patient_id", $patient->id) + ->get(); + + return response()->json(['order' => $detail], 200); + } + public function updateStatusOrderData(Cart $cart, Request $request) + { + + $cart = Cart::where("id", $cart->id)->firstOrFail(); + $cart->status = $request->input("status"); + $cart->save(); + + return response()->json(['status' => 'Success', 'cart' => $cart], 200); + } + public function PatientAppointment() + { + $user = Auth::user(); + $data = []; + $appointments = Appointment::select('patient_id')->where('telemed_pros_id', $user->id)->groupBy('patient_id')->get(); + foreach ($appointments as $appointment) { + $patient = Patient::where('id', $appointment->patient_id)->first(); + array_push($data, $patient->toArray()); + } + return response()->json(['status' => 'Success', 'patient' => $data], 200); + } +} diff --git a/app/Http/Controllers/Agent/Auth/ConfirmPasswordController.php b/app/Http/Controllers/Agent/Auth/ConfirmPasswordController.php new file mode 100644 index 0000000..138c1f0 --- /dev/null +++ b/app/Http/Controllers/Agent/Auth/ConfirmPasswordController.php @@ -0,0 +1,40 @@ +middleware('auth'); + } +} diff --git a/app/Http/Controllers/Agent/Auth/ForgotPasswordController.php b/app/Http/Controllers/Agent/Auth/ForgotPasswordController.php new file mode 100644 index 0000000..465c39c --- /dev/null +++ b/app/Http/Controllers/Agent/Auth/ForgotPasswordController.php @@ -0,0 +1,22 @@ +middleware('guest')->except('logout'); + } + + public function showLoginForm() + { + return view('agent.auth.login'); + } + + protected function login(Request $request) + { + $credentials = $request->only('email', 'password'); + + $user = Telemedpro::where($this->username(), $credentials['email'])->first(); + + if ($user && Hash::check($credentials['password'], $user->password)) { + // Auth::guard('agent')->login($user, $request->has('remember')); + if ($this->guard('agent')->attempt( + $this->credentials($request), + $request->has('remember') + )) { + // dd($this->guard()); + return redirect($this->redirectTo); + } + dd($this->guard()); + // dd($user && Hash::check($credentials['password'], $user->password)); + return redirect($this->redirectTo); + } + + return back()->withErrors(['email' => 'Invalid credentials']); + } + + public function redirectPath() + { + return "/agent"; + } + public function loginAgent(Request $request) + { + $validatedData = $request->validate([ + 'email' => 'required|email', + 'password' => 'required' + ]); + + $patient = Telemedpro::where('email', $validatedData['email'])->first(); + + if (!$patient || !Hash::check($validatedData['password'], $patient->password)) { + return response()->json([ + 'message' => 'Invalid credentials' + ], 422); + } + if (!$patient || $patient->status == 0) { + return response()->json([ + 'message' => 'Your account is undergoing verification.' + ], 422); + } + $token = $patient->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'data' => $patient, + 'access_token' => $token, + 'token_type' => 'Bearer', + ]); + } +} diff --git a/app/Http/Controllers/Agent/Auth/RegisterController.php b/app/Http/Controllers/Agent/Auth/RegisterController.php new file mode 100644 index 0000000..529de29 --- /dev/null +++ b/app/Http/Controllers/Agent/Auth/RegisterController.php @@ -0,0 +1,80 @@ +middleware('guest'); + } + + public function showRegisterForm() + { + return view('agent.auth.register'); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @return \App\Models\Telemedpro + */ + protected function register(Request $request) + { + Telemedpro::create([ + 'name' => $request->input('name'), + 'email' => $request->input('email'), + 'password' => bcrypt($request->input('password')), + ]); + return back(); + } +} diff --git a/app/Http/Controllers/Agent/Auth/ResetPasswordController.php b/app/Http/Controllers/Agent/Auth/ResetPasswordController.php new file mode 100644 index 0000000..b1726a3 --- /dev/null +++ b/app/Http/Controllers/Agent/Auth/ResetPasswordController.php @@ -0,0 +1,30 @@ +middleware('auth'); + $this->middleware('signed')->only('verify'); + $this->middleware('throttle:6,1')->only('verify', 'resend'); + } +} diff --git a/app/Http/Controllers/Agent/DashboardController.php b/app/Http/Controllers/Agent/DashboardController.php new file mode 100644 index 0000000..79730f9 --- /dev/null +++ b/app/Http/Controllers/Agent/DashboardController.php @@ -0,0 +1,204 @@ +middleware('auth'); + // if (isset(Auth::guard('agent')->user()->id)) + // $this->user_id = Auth::guard('agent')->user()->id; + $this->url = $url; + } + public function index() + { + return view('agent.dashboard'); + } + public function register(Request $request) + { + // Validate the request data + $validator = Validator::make($request->all(), [ + 'first_name' => ['required', 'string', 'max:255'], + 'last_name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8'], + ]); + + if ($validator->fails()) { + return response()->json([ + 'errors' => $validator->errors() + ], 422); + } + $first_name = $request->input('first_name'); + $last_name = $request->input('last_name'); + $email = $request->input('email'); + $digits = 4; + $code = rand(pow(10, $digits - 1), pow(10, $digits) - 1); + // Create the new user + $setting = Setting::find(1); + Mail::send('emails.providerVerificationEmail', ['name' => $first_name . " " . $last_name, "code" => $code, 'setting' => $setting], function ($message) use ($first_name, $last_name, $email) { + $message->to($email, $first_name . " " . $last_name) + ->subject('Verify Your Email '); + }); + $user = Telemedpro::create([ + 'name' => $first_name . " " . $last_name, + 'first_name' => $request->input('first_name'), + 'last_name' => $request->input('last_name'), + 'email' => $request->input('email'), + 'password' => Hash::make($request->input('password')), + 'status' => 0, + "email_verification" => $code + ]); + $token = $user->createToken('auth_token')->plainTextToken; + return response()->json([ + 'user' => $user, + 'message' => 'User registered successfully', + ], 201); + } + + public function emailVerify($id, Request $request) + { + + $providerData = Telemedpro::where('email_verification', $request->get('token'))->where('id', $id)->first(); + + if ($providerData) { + $providerData->email_verification = ''; + $providerData->email_verified_at = Carbon::now()->format('Y-m-d H:i:s'); + $providerData->save(); + return response()->json([ + 'message' => 'Account verified', + ]); + } else { + return response()->json([ + 'message' => 'already verified', + ], 422); + } + } + public function saveProfile($id, Request $request) + { + $providerData = Telemedpro::find($id); + if (!$providerData) { + return response()->json([ + 'message' => 'Provider not found', + ], 404); + } + $providerData->practice_state = $request->input('practice_state'); + $providerData->phone_number = $request->input('phone'); + $providerData->gender = $request->input('gender'); + $providerData->specialty = $request->input('specialty'); + $availabilityFrom = $request->input('availabilityFrom'); + if ($availabilityFrom && Carbon::hasFormat($availabilityFrom, 'H:i')) { + $providerData->availability_from = Carbon::createFromFormat('H:i', $availabilityFrom)->format('H:i:s'); + } else { + return response()->json([ + 'message' => 'Invalid format for availability_from', + ], 400); + } + + // Validate and format availability_to + $availability_to = $request->input('availabilityTo'); + if ($availability_to && Carbon::hasFormat($availability_to, 'H:i')) { + $providerData->availability_to = Carbon::createFromFormat('H:i', $availability_to)->format('H:i:s'); + } else { + return response()->json([ + 'message' => 'Invalid format for availability_to', + ], 400); + } + $providerData->home_address = $request->input('home_address'); + $providerData->medical_license_number = $request->input('medical_license_number'); + $providerData->years_of_experience = $request->input('years_of_experience'); + $providerData->city = $request->input('city'); + $providerData->state = $request->input('state'); + $providerData->zip_code = $request->input('zip_code'); + $providerData->save(); + $this->saveLicenseNumber($id, $request->input('medical_license_number')); + return response()->json([ + 'user' => $providerData, + 'message' => 'Data saved ', + ], 201); + } + public function saveLicenseNumber($id, $licences) + { + foreach ($licences as $key => $value) { + LicenseNumberModel::create([ + "provider_id" => $id, + "state" => $key, + "license_number" => $value, + "status" => 1 + ]); + } + } + public function resendCode($id) + { + $digits = 4; + $code = rand(pow(10, $digits - 1), pow(10, $digits) - 1); + $providerData = Telemedpro::find($id); + $providerData->email_verification = $code; //update code in database + $email = $providerData->email; + $first_name = $providerData->first_name; + $last_name = $providerData->last_time; + $setting = Setting::find(1); + Mail::send('emails.providerVerificationEmail', ['name' => $first_name . " " . $last_name, "code" => $code, 'setting' => $setting], function ($message) use ($first_name, $last_name, $email) { + $message->to($email, $first_name . " " . $last_name) + ->subject('Verify Your Email '); + }); + $providerData->save(); + return response()->json([ + 'message' => 'Verification code sent! ', + ], 201); + } + public function getProviderMeetings() + { + $appointments = Appointment::select( + "patients.profile_picture", + "patients.first_name", + "patients.last_name", + "appointments.id as appointment_id", + "appointments.start_time", + "appointments.end_time", + "appointments.timezone", + "appointments.duration", + "appointments.appointment_date", + "appointments.appointment_time", + "appointments.status as appointment_status", + "appointments.patient_name", + "carts.id as order_id", + "appointments.id as appointment_id", + "appointments.patient_id" + ) + ->leftJoin("patients", "patients.id", "appointments.patient_id") + ->leftJoin("carts", "carts.appointment_id", "appointments.id") + ->where('telemed_pros_id', Auth::guard('agent')->user()->id) + ->where('start_time', "!=", null) + ->where('end_time', "!=", null) + ->orderBy('appointments.created_at', 'desc') + ->get(); + + foreach ($appointments as $appointment) { + if ($appointment->profile_picture) + $appointment->profile_picture = $this->url->to("storage/profile_pictures/", $appointment->profile_picture); + else + $appointment->profile_picture = asset('img/avatars/1.png'); + } + + return response()->json($appointments, 200); + } +} diff --git a/app/Http/Controllers/Agent/HomeController.php b/app/Http/Controllers/Agent/HomeController.php new file mode 100644 index 0000000..4573225 --- /dev/null +++ b/app/Http/Controllers/Agent/HomeController.php @@ -0,0 +1,14 @@ + '0014470a9fa14b598a73cc0133acca8c', + ])->post('https://api.assemblyai.com/v2/realtime/token', [ + 'expires_in' => 1600 + ]); + + return $response->body(); + } + public function show($meeting_id) + { + return view('agent.jetsi', compact('meeting_id')); + } + public function joinMeeting($meeting_id) + { + $jassToken = JassJWT::generate(); + $appoinement = Appointment::where("meeting_id", $meeting_id)->firstOrFail(); + + return view('agent.join-meeting', compact('meeting_id'), compact('jassToken')); + } + public function startCall($patient_id, $agent_id, $appointment_id, Request $request) + { + $call_type = $request->input('call_type'); + $telemed_pro = Telemedpro::where("id", $agent_id)->firstOrFail(); + $appointment = Appointment::find($appointment_id); + $appointment->in_call = 1; + $appointment->start_time = Carbon::now()->format('Y-m-d H:i:s'); + $appointment_booking_tokens = $this->bookAppointmentApi($appointment, $telemed_pro); + $appointment->agent_call_token = $appointment_booking_tokens['tokenAgent']; + $appointment->patient_call_token = $appointment_booking_tokens['tokenPatient']; + $appointment->telemed_pros_id = $agent_id; + $appointment->save(); + + event(new AppointmentCreated($patient_id, $appointment->id, $appointment->patient_call_token, $call_type)); + + return response()->json([ + 'message' => 'Appointment created!', + 'appointment_id' => $appointment->id, + 'meeting_id' => $appointment->agent_call_token, + 'call_type' => $call_type + ], 200); + } + + + public function startRecording(Appointment $appointment) + { + $telemed = Telemedpro::find($appointment->telemed_pros_id); + if ($telemed->recording_switch == 0) { + return response()->json([ + 'message' => "Recording Off", + 'Response' => 'Error', + ], 200); + } + $roomName = 'appointment-' . $appointment->id; + $video_token = md5(rand(100000000, 999999999) . rand(1, 9)); + $appointment->video_token = $video_token; + $appointment->save(); + + $client = new EgressServiceClient("https://plugnmeet.codelfi.com", config('app.LK_API_KEY'), config('app.LK_API_SECRET')); + try { + $info = $client->startRoomCompositeEgress($roomName, 'speaker', new EncodedFileOutput([ + "filepath" => "/out/recordings/" . $video_token . ".mp4", + "file_type" => EncodedFileType::MP4, + ])); + } catch (Exception $e) { + return response()->json([ + 'message' => $e->getMessage(), + 'Response' => 'Error', + ], 200); + } + return response()->json([ + 'message' => "Success", + 'Response' => 'Success', + ], 200); + } + public function bookAppointmentApi($appointment, $availableTelemedPros) + { + $roomName = 'appointment-' . $appointment->id . "-" . uniqid(); + $opts = (new RoomCreateOptions()) + ->setName($roomName) + ->setEmptyTimeout(30) + ->setMaxParticipants(5); + $host = "https://plugnmeet.codelfi.com"; + $svc = new RoomServiceClient($host, config('app.LK_API_KEY'), config('app.LK_API_SECRET')); + try { + $svc->deleteRoom($roomName); + } catch (Exception | Error $e) { + } + + $room = $svc->createRoom($opts); + + $participantPatientName = "patient-" . uniqid() . $appointment->patient->first_name . " " . $appointment->patient->last_name; + + $tokenOptionsPatient = (new AccessTokenOptions()) + ->setIdentity($participantPatientName); + $videoGrantPatient = (new VideoGrant()) + ->setRoomJoin() + ->setRoomName($roomName); + $tokenPatient = (new AccessToken(config('app.LK_API_KEY'), config('app.LK_API_SECRET'))) + ->init($tokenOptionsPatient) + ->setGrant($videoGrantPatient) + ->toJwt(); + + $participantAgentName = "agent-" . uniqid() . $availableTelemedPros->name; + $tokenOptionsAgent = (new AccessTokenOptions()) + ->setIdentity($participantAgentName); + $videoGrantAgent = (new VideoGrant()) + ->setRoomJoin() + ->setRoomName($roomName); + $tokenAgent = (new AccessToken(config('app.LK_API_KEY'), config('app.LK_API_SECRET'))) + ->init($tokenOptionsAgent) + ->setGrant($videoGrantAgent) + ->toJwt(); + return [ + 'tokenPatient' => $tokenPatient, + 'tokenAgent' => $tokenAgent, + ]; + } + public function endCall($patient_id, $appointment_id) + { + $appointment = Appointment::find($appointment_id); + $appointment->in_call = 0; + $appointment->end_time = Carbon::now()->format('Y-m-d H:i:s'); + $appointment->save(); + + event(new AppointmentCallEnded($patient_id, $appointment->id)); + + return response()->json([ + 'message' => 'Call ended', + 'appointment_id' => $appointment->id, + ], 200); + } + + public function searchLabsByAddress(Request $request) + { + $address = $request->input('address'); + + try { + $labs = Lab::where('address', 'like', '%' . $address . '%') + ->orWhere('city', 'like', '%' . $address . '%') + ->orWhere('state', 'like', '%' . $address . '%') + ->orWhere('zip_code', 'like', '%' . $address . '%') + ->get(['id', 'name', 'city', 'state', 'zip_code', 'lang', 'lat']); + + return response()->json($labs, 200); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to search labs'], 500); + } + } + public function bookAppointment(Request $request) + { + $validatedData = $request->validate([ + 'telemed_pros_id' => 'required|exists:telemed_pros,id', + 'patient_id' => 'required|exists:patients,id', + 'doctor_id' => 'required|exists:doctors,id', + 'appointment_id' => 'required', + 'appointment_time' => 'required|date_format:Y-m-d H:i:s', + ]); + + $appointment = DoctorAppointment::create($validatedData); + + + + + return response()->json([ + 'message' => 'Appointment booked successfully', + 'meeting_id' => $appointment->meeting_id, + 'appointment_time' => $validatedData['appointment_time'] + ]); + } + public function updateInfo(Request $request, $patientId) + { + try { + $patient = Patient::find($patientId); + $patient->update($request->only(['city', 'state', 'address', 'zip_code', 'dob', 'country'])); + $patient->save(); + + return response()->json(['message' => 'Patient address updated successfully'], 200); + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 400); + } + } + public function getInfo(Request $request, $patientId) + { + try { + $patient = Patient::find($patientId)->makeHidden(['password', 'remember_token']); + if ($patient->dob) { + $birthDate = new DateTime($patient->dob); + $today = new DateTime(date('Y-m-d')); + $age = $today->diff($birthDate)->y; + $patient->age = $age; + } else { + $patient->age = 0; + } + + return response()->json($patient); + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 400); + } + } + public function getDoctorList() + { + try { + // Fetch only the necessary columns for efficiency + $doctors = Doctor::select('id', 'name', 'designation')->get()->makeHidden(['password', 'remember_token']); + + // Return a successful JSON response with the doctors + return response()->json($doctors, 200); + } catch (\Exception $e) { + // Handle exceptions gracefully + return response()->json(['error' => $e->getMessage()], 500); + } + } + public function getAppointmentList() + { + try { + $appointments = Appointment::select("patients.first_name", "patients.last_name", "telemed_pros.name as agent_name", "appointments.*") // Eager load the associated telemed pro + ->leftJoin("telemed_pros", "telemed_pros.id", "appointments.telemed_pros_id") + ->leftJoin("patients", "patients.id", "appointments.patient_id") + /* ->orderBy('appointment_time', 'desc') */ // Optional: sort by appointment time + ->get(); + + return response()->json($appointments, 200); + } catch (\Exception $e) { + + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } + public function getDoctorAppointmentList() + { + try { + $appointments = DoctorAppointment::select("patients.first_name", "patients.last_name", "doctor_appointments.*") // Eager load the associated telemed pro + ->leftJoin("patients", "patients.id", "doctor_appointments.patient_id") + ->orderBy('doctor_appointments.created_at', 'desc') // Optional: sort by appointment time + ->get(); + + return response()->json($appointments, 200); + } catch (\Exception $e) { + + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } + public function availableSlots($date) + { + // Ensure date is in a valid format + $date = Carbon::parse($date); + + // Generate all possible 30-minute slots between 9 AM and 4 PM + $slots = collect(); + $startTime = Carbon::parse($date)->setTime(9, 0, 0); + $endTime = Carbon::parse($date)->setTime(16, 0, 0); + while ($startTime < $endTime) { + $slots->push($startTime->format('Y-m-d H:i:s')); + $startTime->addMinutes(30); + } + // Filter out booked slots + $bookedAppointments = Appointment::where('appointment_date', '>=', $date->format('Y-m-d')) + ->where('appointment_date', '<', $date->addDay()->format('Y-m-d')) + ->pluck('appointment_date'); + $availableSlots = $slots->diff($bookedAppointments); + + $formattedSlots = $availableSlots->map(function ($slot) { + + $start = Carbon::parse($slot); + + // Add AM/PM + $startTime = $start->format('g:i A'); + + $end = (clone $start)->addMinutes(29); + + // Add AM/PM + $endTime = $end->format('g:i A'); + + return $startTime /* . ' - ' . $endTime */; + }); + + // Additional checking if slot is booked + $formattedSlots = $formattedSlots->filter(function ($slot) { + + $startTime = Carbon::parse($slot); + /*$startTime = Carbon::parse(explode(' - ', $slot)[0]); + $endTime = Carbon::parse(explode(' - ', $slot)[1]); */ + + //return !Appointment::whereBetween('appointment_time', [$startTime/* , $endTime */])->exists(); + return !Appointment::where('appointment_time', $startTime)->exists(); + }); + + return response()->json([ + 'available_slots' => $formattedSlots->toArray() + ]); + } + public function appointmentDetail(Appointment $appointment) + { + $patient = Patient::find($appointment->patient_id); + $telemedPro = Telemedpro::find($appointment->telemed_pros_id); + $doctor_appointment = DoctorAppointment::select("id", "appointment_date", "appointment_time", "appointment_id")->where('appointment_id', $appointment->id)->first(); + if (!$doctor_appointment) + $doctor_appointment = []; + else + $doctor_appointment = $doctor_appointment->toArray(); + + if ($patient) { + if ($patient->dob) { + $birthDate = new DateTime($patient->dob); + $today = new DateTime(date('Y-m-d')); + $age = $today->diff($birthDate)->y; + $patient->age = $age; + } else { + $patient->age = 0; + } + } + + return response()->json([ + 'patient' => $patient->toArray() ?? [], + 'telemedPro' => $telemedPro->toArray() ?? [], + 'doctor_appointment' => $doctor_appointment ?? [], + 'agent_appointment' => $appointment, + 'video_url' => "https://plugnmeet.codelfi.com/recordings/" . $appointment->video_token . ".mp4", + ]); + } + public function labDetail(Appointment $appointment) + { + $lab = Lab::where("appointment_id", $appointment->id)->first(); + return response()->json([ + 'lab_name' => $lab->name, + 'lab_address' => $lab->address, + 'lab_city' => $lab->city, + 'lab_state' => $lab->state, + 'lab_distance' => $lab->distance, + 'lab_contact_no' => $lab->contact_no, + 'lab_lang' => $lab->lang, + 'lab_lat' => $lab->lat, + 'slot_date' => $lab->slot_date, + 'slot_time' => $lab->slot_time, + 'booking_time' => $lab->booking_time, + ]); + } + public function getRoomList() + { + $svc = new RoomServiceClient("https://plugnmeet.codelfi.com", config('app.LK_API_KEY'), config('app.LK_API_SECRET')); + + // List rooms. + $rooms = $svc->listRooms(); + + dd($rooms); + } + + public function addNotePatient(Request $request) + { + // Validation (adjust as needed) + + $patient = Auth::guard('patient')->user(); + + $addNotePatient = PatientNote::create([ + 'note' => $request->input('note'), + 'note_type' => $request->input('note_type'), + 'patient_id' => $patient->id, + ]); + + return response()->json([ + 'message' => 'Note created', + 'data' => $addNotePatient + ], 200); + } + + public function getNotePatient(Request $request) + { + // Validation (adjust as needed) + + $patient = Auth::guard('patient')->user(); + + $addNotePatient = PatientNote::where("patient_id", $patient->id)->get(); + + return response()->json([ + 'message' => 'Note created', + 'data' => $addNotePatient + ], 200); + } + public function markAppointmentsStatus($id) + { + $appointment = Appointment::find($id); + $appointment->status = 'completed'; + $appointment->save(); + return response()->json([ + 'message' => 'status updated !' + ], 200); + } +} diff --git a/app/Http/Controllers/Agent/OrderController.php b/app/Http/Controllers/Agent/OrderController.php new file mode 100644 index 0000000..a0ef014 --- /dev/null +++ b/app/Http/Controllers/Agent/OrderController.php @@ -0,0 +1,195 @@ +middleware('auth'); + $this->user_id = Auth::guard('agent')->user()->id; + $this->url = $url; + } + public function orderList(Request $request) + { + + $fromDate = $request->get('from_date'); + $toDate = $request->get('to_date'); + $orderList = Cart::select("appointments.*", 'appointments.id as appointment_id', 'carts.*', 'carts.id as order_id')->leftJoin('appointments', 'appointments.id', 'carts.appointment_id') + ->where('appointments.telemed_pros_id', $this->user_id); + if ($fromDate != "") { + $from_date = Carbon::createFromFormat('m-d-Y', $fromDate)->format('Y-m-d'); + $orderList->where('created_at', ">=", $from_date . " 00:00:00"); + } + if ($toDate != "") { + $to_date = Carbon::createFromFormat('m-d-Y', $toDate)->format('Y-m-d'); + $orderList->where('created_at', "<=", $to_date . " 23:59:59"); + } + + $orderListData = $orderList->get(); + $totalPrice = 0; + $totalShippingCost = 0; + foreach ($orderListData as $order) { + $order->order_id = $order->id; + $totalPrice = 0; + $total_products = 0; + $quantity = []; + $totalShippingCost = 0; + $order->order_total_amount = $totalPrice; + $order->order_total_shipping = $totalShippingCost; + $items = Item::leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('cart_id', $order->id) + ->get(); + $appointment = Appointment::where('id', $order->appointment_id)->first(); + + $order->appointment_status = $appointment->status; + + + + $orderItems = []; + foreach ($items as $item) { + array_push($orderItems, $item->plansV1); + $totalShippingCost += $item->shipping_cost; + $item->total_price = $item->quantity * $item->price; + $totalPrice += $item->total_price; + $order->order_total_amount = $totalPrice; + $order->order_total_shipping = $totalShippingCost; + $item->plansV1->qty = $item->quantity; + $item->plansV1->status = $item->status; + } + + $order->total_items = $total_products; + $order->order_items = $orderItems; + } + return response() + ->json([ + 'order_data' => $orderListData + ]); + } + public function orderDetails($id) + { + $orderItems = $this->getOrderItems($id); + + $orderDetails = Cart::find($id); + $items = Item::where('cart_id', $orderDetails->id)->get(); + + + $appointments = Appointment::select( + 'appointments.*', + 'telemed_pros.name as provider_name', + 'telemed_pros.email as provider_email', + 'telemed_pros.phone_number as provider_phone' + ) + ->leftJoin('telemed_pros', 'telemed_pros.id', 'appointments.telemed_pros_id') + ->where('appointments.id', $orderDetails->appointment_id) + ->first(); + $prescription = PatientPrescription::select( + 'patient_prescription.direction_quantity', + 'patient_prescription.refill_quantity', + 'patient_prescription.dosage', + 'patient_prescription.status', + 'patient_prescription.direction_one', + 'patient_prescription.direction_two', + 'patient_prescription.dont_substitute', + 'patient_prescription.comments', + 'patient_prescription.brand', + 'patient_prescription.from', + 'patient_prescription.quantity', + 'patient_prescription.created_at as prescription_date', + 'prescriptions.name as prescription_name', + 'patient_prescription.prescription_id', + 'telemed_pros.name as provide_name', + 'telemed_pros.id as provider_id' + ) + ->where("appointment_id", $orderDetails->appointment_id) + ->leftJoin('appointments', 'appointments.id', 'patient_prescription.appointment_id') + ->leftJoin('prescriptions', 'prescriptions.id', 'patient_prescription.prescription_id') + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', 'telemed_pros.id') + ->get(); + $patientNotes = PatientNote::where("appointment_id", $orderDetails->appointment_id)->get(); + $appointments->provider_id = $appointments->telemed_pros_id; + $patient = $orderDetails->patient; + $patient->profile_picture = $this->url->to("storage/profile_pictures/" . $patient->profile_picture); + + return response() + ->json([ + 'order_details' => $orderDetails, + 'order_items' => $orderItems, + 'patient_details' => $patient, + 'appointment_details' => $appointments, + 'items_activity' => $this->getShippingActivity($id), + 'appointment_notes' => $patientNotes, + 'prescription' => $prescription + ]); + } + public function getOrderItems($id) + { + $items = Item::leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('cart_id', $id) + ->get(); + $totalPrice = 0; + $totalShippingCost = 0; + $total_products = 0; + foreach ($items as $item) { + + $totalShippingCost += $item->shipping_cost; + $item->total_price = $item->quantity * $item->price; + $totalPrice += $item->total_price; + $total_products += $item->quantity; + $item->image_url = $this->url->to("product/" . $item->image_url); + } + + return [ + 'items' => $items, + 'total_amount' => $totalPrice, + 'total_shipping_cost' => $totalShippingCost, + 'total_products' => $total_products, + 'total' => $totalPrice + $totalShippingCost + ]; + } + public function getShippingActivity($id) + { + $itemsHistory = ItemHistory::select('items_history.*', 'plans_v1.title as item_name') + ->where('items_history.cart_id', $id) + ->leftJoin('items', 'items.id', 'items_history.item_id') + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->get(); + return $itemsHistory; + } +} diff --git a/app/Http/Controllers/Agent/PatientProfileController.php b/app/Http/Controllers/Agent/PatientProfileController.php new file mode 100644 index 0000000..ae585c9 --- /dev/null +++ b/app/Http/Controllers/Agent/PatientProfileController.php @@ -0,0 +1,183 @@ +middleware('auth'); + $this->user_id = Auth::guard('agent')->user()->id; + $this->url = $url; + } + public function index($id) + { + $patient = Patient::where('id', $id)->first(); + if ($patient->profile_picture) + $patient->profile_picture = $this->url->to("storage/profile_pictures/", $patient->profile_picture); + else + $patient->profile_picture = asset('img/avatars/1.png');; + + $notes = PatientNote::select( + 'patient_notes.note', + 'telemed_pros.name as provider_name', + 'patient_notes.appointment_id', + 'patient_notes.created_at as note_date' + ) + ->leftJoin('telemed_pros', 'telemed_pros.id', 'patient_notes.telemed_pros_id') + ->where('patient_id', $id)->get(); + $prescriptions = PatientPrescription::select( + 'appointments.appointment_date', + 'appointments.appointment_time', + 'appointments.timezone', + 'appointments.start_time', + 'patient_prescription.direction_quantity', + 'patient_prescription.refill_quantity', + 'patient_prescription.dosage', + 'patient_prescription.status', + 'patient_prescription.direction_one', + 'patient_prescription.direction_two', + 'patient_prescription.dont_substitute', + 'patient_prescription.comments', + 'patient_prescription.brand', + 'patient_prescription.from', + 'patient_prescription.quantity', + 'patient_prescription.created_at as prescription_date', + 'telemed_pros.name', + 'telemed_pros.email as provider_email', + 'telemed_pros.gender as provider_gender', + 'telemed_pros.specialty as provider_specialty', + 'telemed_pros.years_of_experience', + 'prescriptions.name as prescription_name', + 'carts.id as order_id', + // 'prescriptions.price as prescription_price', + // 'prescriptions.shipping_cost as prescription_shipping_cost', + 'patient_prescription.prescription_id' + ) + ->leftJoin('appointments', 'appointments.id', 'patient_prescription.appointment_id') + ->leftJoin('carts', 'carts.appointment_id', '=', 'appointments.id') + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', 'telemed_pros.id') + ->leftJoin('prescriptions', 'prescriptions.id', 'patient_prescription.prescription_id') + ->where('patient_prescription.patient_id', $id)->get(); + return response()->json( + [ + 'notes_history' => $notes, + 'prescriptions' => $prescriptions, + 'patient_details' => $patient + ], + 200 + ); + } + public function labkitOrderItemStore(Request $request) + { + // Validate the request data + $validator = Validator::make($request->all(), [ + 'cart_id' => 'required|exists:carts,id', + 'item_id' => 'required|exists:items,id', + 'lab_kit_id' => 'required|exists:lab_kit,id', + + /* 'result' => 'nullable|string', */ + ]); + + if ($validator->fails()) { + return response()->json([ + 'errors' => $validator->errors(), + ], 422); + } + + // Create a new LabkitOrderItem + $labkitOrderItem = LabkitOrderItem::create([ + 'cart_id' => $request['cart_id'], + 'item_id' => $request['item_id'], + 'lab_kit_id' => $request['lab_kit_id'], + /* 'result' => $request['result'], */ + 'status' => "Ordered", + ]); + + return response()->json([ + 'message' => 'Order detail stored successfully', + 'data' => $labkitOrderItem, + ], 201); + } + public function labkitOrderItemGet(Request $request) + { + $labkitOrderItems = LabkitOrderItem::where('labkit_order_items.cart_id', $request->input('cart_id')) + ->leftJoin( + 'lab_kit', + 'labkit_order_items.lab_kit_id', + '=', + 'lab_kit.id' + ) + ->leftJoin( + 'items', + 'items.id', + 'labkit_order_items.item_id' + ) + ->leftJoin( + 'plans_v1', + 'plans_v1.id', + 'items.plans_id' + ) + ->select( + 'labkit_order_items.id', + 'labkit_order_items.status', + 'labkit_order_items.result', + 'lab_kit.name as lab_kit_name', + 'plans_v1.title as item_name' + ) + ->get(); + foreach ($labkitOrderItems as $labKit) { + + if ($labKit->result != "") + $labKit->result = $this->url->to('storage/lab_results/' . $labKit->result); + } + + return response()->json([ + 'data' => $labkitOrderItems, + ]); + } + public function getLabKit(Cart $cart, Request $request) + { + $kit = LabKit::all(); + return response()->json(['kit' => $kit], 200); + } +} diff --git a/app/Http/Controllers/Auth/ConfirmPasswordController.php b/app/Http/Controllers/Auth/ConfirmPasswordController.php new file mode 100644 index 0000000..138c1f0 --- /dev/null +++ b/app/Http/Controllers/Auth/ConfirmPasswordController.php @@ -0,0 +1,40 @@ +middleware('auth'); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php new file mode 100644 index 0000000..48b120e --- /dev/null +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -0,0 +1,87 @@ +email)->first(); + + if (!$patient) { + return response()->json(['message' => 'The specified email cannot be located.'], 404); + } + $token = base64_encode(Str::random(60)); + $tokenData = PasswordResetTokens::where('email', $request->email)->first(); + if ($tokenData) { + PasswordResetTokens::where('email', $request->email)->delete(); + } + PasswordResetTokens::create([ + 'email' => $request->email, + 'token' => $token, + 'created_at' => now() + ]); + + // Send reset link email + Mail::send('emails.password_reset', ['token' => $token], function ($message) use ($request) { + $message->to($request->email); + $message->subject('Password Reset Request'); + }); + + return response()->json(['message' => 'Password reset link sent']); + } + + public function resetPassword(Request $request) + { + + $tokenData = PasswordResetTokens::where('token', $request->token)->first(); + + if (!$tokenData || !$request->token == $tokenData->token) { + return response()->json([ + 'msg' => "Link Expired", + 'status' => 'error' + ]); + } + + $password = $request->get('password'); + $confPassword = $request->get('confirm'); + if ($confPassword != $password) { + return response()->json([ + 'msg' => "Password don no match", + 'status' => 'error' + ]); + } + + $user = Patient::where('email', $tokenData->email)->first(); + $user->password = bcrypt($password); + $user->save(); + PasswordResetTokens::where('token', $request->token)->delete(); + return response()->json([ + 'msg' => "Password updated" + ]); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..18a0d08 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,40 @@ +middleware('guest')->except('logout'); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 0000000..ed1a5e0 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,73 @@ +middleware('guest'); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @return \App\Models\User + */ + protected function create(array $data) + { + return User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + ]); + } +} diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php new file mode 100644 index 0000000..b1726a3 --- /dev/null +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -0,0 +1,30 @@ +middleware('auth'); + $this->middleware('signed')->only('verify'); + $this->middleware('throttle:6,1')->only('verify', 'resend'); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..77ec359 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,12 @@ +middleware('auth'); + } +} diff --git a/app/Http/Controllers/Doctor/Auth/ForgotPasswordController.php b/app/Http/Controllers/Doctor/Auth/ForgotPasswordController.php new file mode 100644 index 0000000..465c39c --- /dev/null +++ b/app/Http/Controllers/Doctor/Auth/ForgotPasswordController.php @@ -0,0 +1,22 @@ +middleware('guest')->except('logout'); + } + + public function showLoginForm() + { + return view('doctor.auth.login'); + } + + protected function login(Request $request) + { + $credentials = $request->only('email', 'password'); + + $user = Doctor::where($this->username(), $credentials['email'])->first(); + + if ($user && Hash::check($credentials['password'], $user->password)) { + Auth::guard('doctor')->login($user, $request->has('remember')); + return redirect($this->redirectTo); + } + + return back()->withErrors(['email' => 'Invalid credentials']); + } + + public function redirectPath() { + return "/doctor"; + } +} diff --git a/app/Http/Controllers/Doctor/Auth/RegisterController.php b/app/Http/Controllers/Doctor/Auth/RegisterController.php new file mode 100644 index 0000000..74b926a --- /dev/null +++ b/app/Http/Controllers/Doctor/Auth/RegisterController.php @@ -0,0 +1,80 @@ +middleware('guest'); + } + + public function showRegisterForm() + { + return view('doctor.auth.register'); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @return \App\Models\Doctor + */ + protected function register(Request $request) + { + Doctor::create([ + 'name' => $request->input('name'), + 'email' => $request->input('email'), + 'password' => bcrypt($request->input('password')), + ]); + return back(); + } +} diff --git a/app/Http/Controllers/Doctor/Auth/ResetPasswordController.php b/app/Http/Controllers/Doctor/Auth/ResetPasswordController.php new file mode 100644 index 0000000..b1726a3 --- /dev/null +++ b/app/Http/Controllers/Doctor/Auth/ResetPasswordController.php @@ -0,0 +1,30 @@ +middleware('auth'); + $this->middleware('signed')->only('verify'); + $this->middleware('throttle:6,1')->only('verify', 'resend'); + } +} diff --git a/app/Http/Controllers/Doctor/HomeController.php b/app/Http/Controllers/Doctor/HomeController.php new file mode 100644 index 0000000..1976384 --- /dev/null +++ b/app/Http/Controllers/Doctor/HomeController.php @@ -0,0 +1,14 @@ +middleware('auth'); + } + + /** + * Show the application dashboard. + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function index() + { + return view('home'); + } +} diff --git a/app/Http/Controllers/OpenErm/PatientController.php b/app/Http/Controllers/OpenErm/PatientController.php new file mode 100644 index 0000000..b9b4c1b --- /dev/null +++ b/app/Http/Controllers/OpenErm/PatientController.php @@ -0,0 +1,139 @@ +getAccessToken(); + + $searchParams = []; + + if ($request->has('fname')) { + $searchParams['fname'] = $request->input('fname'); + } + + if ($request->has('lname')) { + $searchParams['lname'] = $request->input('lname'); + } + + $response = Http::withHeaders([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $accessToken, + ])->get($this->baseUrl . '/apis/default/api/patient', $searchParams); + + return $response->json(); + } + public function getPatientById($puuid) + { + $accessToken = $this->getAccessToken(); + + $response = Http::withHeaders([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $accessToken, + ])->get($this->baseUrl . '/apis/default/api/patient/' . $puuid); + + return $response->json(); + } + public function registerPatient(Request $request) + { + $accessToken = $this->getAccessToken(); + + $patientData = $request->validate([ + 'title' => 'required|string', + 'fname' => 'required|string', + 'mname' => 'nullable|string', + 'lname' => 'required|string', + 'street' => 'required|string', + 'postal_code' => 'required|string', + 'city' => 'required|string', + 'state' => 'required|string', + 'country_code' => 'required|string', + 'phone_contact' => 'required|string', + 'DOB' => 'required|date', + 'sex' => 'required|string', + 'race' => 'nullable|string', + 'ethnicity' => 'nullable|string', + ]); + + $response = Http::withHeaders([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $accessToken, + ])->post($this->baseUrl . '/apis/default/api/patient', $patientData); + + return $response->json(); + } + + private function getAccessToken() + { + if (Cache::has('access_token')) { + return Cache::get('access_token'); + } + + $response = Http::asForm()->withHeaders([ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret), + ])->post($this->baseUrl . '/oauth2/default/token', [ + 'grant_type' => 'password', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'user_role' => 'users', + 'username' => $this->username, + 'password' => $this->password, + 'scope' => 'openid offline_access api:oemr user/patient.read user/patient.write', + ]); + + $tokenData = $response->json(); + + if (isset($tokenData['access_token'])) { + Cache::put('access_token', $tokenData['access_token'], now()->addSeconds($tokenData['expires_in'] - 60)); + Cache::put('refresh_token', $tokenData['refresh_token'], now()->addDays(30)); + } + + return $tokenData['access_token'] ?? null; + } + + private function refreshAccessToken() + { + $refreshToken = Cache::get('refresh_token'); + + if (!$refreshToken) { + return $this->getAccessToken(); + } + + $response = Http::asForm()->withHeaders([ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret), + ])->post($this->baseUrl . '/oauth2/default/token', [ + 'grant_type' => 'refresh_token', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'refresh_token' => $refreshToken, + ]); + + $tokenData = $response->json(); + + if (isset($tokenData['access_token'])) { + Cache::put('access_token', $tokenData['access_token'], now()->addSeconds($tokenData['expires_in'] - 60)); + if (isset($tokenData['refresh_token'])) { + Cache::put('refresh_token', $tokenData['refresh_token'], now()->addDays(30)); + } + return $tokenData['access_token']; + } + + return $this->getAccessToken(); + } +} diff --git a/app/Http/Controllers/OrderController.php b/app/Http/Controllers/OrderController.php new file mode 100644 index 0000000..90281c4 --- /dev/null +++ b/app/Http/Controllers/OrderController.php @@ -0,0 +1,248 @@ +middleware('auth'); + $this->user_id = Auth::guard('patient')->user()->id; + $this->url = $url; + } + + /** + * Show the application dashboard. + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function index() + { + return view('home'); + } + public function orderList(Request $request) + { + + $fromDate = $request->get('from_date'); + $toDate = $request->get('to_date'); + $orderList = Cart::where('carts.patient_id', $this->user_id); + if ($fromDate != "") { + $from_date = Carbon::createFromFormat('m-d-Y', $fromDate)->format('Y-m-d'); + $orderList->where('created_at', ">=", $from_date . " 00:00:00"); + } + if ($toDate != "") { + $to_date = Carbon::createFromFormat('m-d-Y', $toDate)->format('Y-m-d'); + $orderList->where('created_at', "<=", $to_date . " 23:59:59"); + } + + $orderListData = $orderList->get(); + $totalPrice = 0; + $totalShippingCost = 0; + foreach ($orderListData as $order) { + $totalPrice = 0; + $total_products = 0; + $quantity = []; + $totalShippingCost = 0; + $order->order_total_amount = $totalPrice; + $order->order_total_shipping = $totalShippingCost; + $items = Item::leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('cart_id', $order->id) + ->get(); + //$order->appointment_status = Appointment::where('id', $order->appointment_id)->first()->status; + + $orderItems = []; + foreach ($items as $item) { + array_push($orderItems, $item->plansV1); + $totalShippingCost += $item->shipping_cost; + $item->total_price = $item->quantity * $item->price; + $totalPrice += $item->total_price; + $order->order_total_amount = $totalPrice; + $order->order_total_shipping = $totalShippingCost; + $item->plansV1->qty = $item->quantity; + } + + $order->total_items = $total_products; + $order->order_items = $orderItems; + } + return response() + ->json([ + 'order_data' => $orderListData + ]); + } + + public function orderDetails($id) + { + $orderItems = $this->getOrderItems($id); + + $orderDetails = Cart::find($id); + $items = Item::where('cart_id', $orderDetails->id)->get(); + + + $appointments = Appointment::select( + 'appointments.*', + 'telemed_pros.name as provider_name', + 'telemed_pros.email as provider_email', + 'telemed_pros.phone_number as provider_phone', + 'carts.total_amount', + 'carts.shipping_amount' + ) + ->leftJoin('telemed_pros', 'telemed_pros.id', 'appointments.telemed_pros_id') + ->leftJoin('carts', 'carts.appointment_id', 'appointments.id') + + ->where('appointments.id', $orderDetails->appointment_id) + ->first(); + + $prescription = PatientPrescription::select( + 'patient_prescription.direction_quantity', + 'patient_prescription.refill_quantity', + 'patient_prescription.dosage', + 'patient_prescription.status', + 'patient_prescription.direction_one', + 'patient_prescription.direction_two', + 'patient_prescription.dont_substitute', + 'patient_prescription.comments', + 'patient_prescription.brand', + 'patient_prescription.from', + 'patient_prescription.quantity', + 'patient_prescription.created_at as prescription_date', + 'prescriptions.name as prescription_name', + 'patient_prescription.prescription_id', + 'telemed_pros.name as provide_name', + 'telemed_pros.id as provider_id' + ) + ->where("appointment_id", $orderDetails->appointment_id) + ->leftJoin('appointments', 'appointments.id', 'patient_prescription.appointment_id') + ->leftJoin('prescriptions', 'prescriptions.id', 'patient_prescription.prescription_id') + ->leftJoin('telemed_pros', 'appointments.telemed_pros_id', 'telemed_pros.id') + ->get(); + $patientNotes = PatientNote::where("appointment_id", $orderDetails->appointment_id)->get(); + if ($appointments) + $appointments->provider_id = $appointments->telemed_pros_id; + $patient = $orderDetails->patient; + $patient->profile_picture = $this->url->to("storage/profile_pictures/" . $patient->profile_picture); + + return response() + ->json([ + 'order_details' => $orderDetails, + 'order_items' => $orderItems, + 'patient_details' => $patient, + 'appointment_details' => $appointments, + 'items_activity' => $this->getShippingActivity($id), + 'appointment_notes' => $patientNotes, + 'prescription' => $prescription + ]); + } + + + public function getShippingActivity($id) + { + $itemsHistory = ItemHistory::select('items_history.*', 'plans_v1.title as item_name') + ->where('items_history.cart_id', $id) + ->leftJoin('items', 'items.id', 'items_history.item_id') + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->get(); + return $itemsHistory; + } + public function getPatientShippingActivity() + { + $orderDetails = Cart::where('patient_id', $this->user_id)->orderBy('id', 'Desc')->first(); + + $itemsHistory = ItemHistory::select('items_history.*', 'plans_v1.title as item_name') + ->where('items_history.cart_id', $orderDetails->id) + ->leftJoin('items', 'items.id', 'items_history.item_id') + ->leftJoin('plans_v1', 'plans_v1.id', 'items.plans_id') + ->get(); + + return response() + ->json([ + 'item_history' => $itemsHistory + + ]); + } + public function getOrderItems($id) + { + $items = Item::leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('cart_id', $id) + ->get(); + $totalPrice = 0; + $totalShippingCost = 0; + $total_products = 0; + foreach ($items as $item) { + + $totalShippingCost += $item->shipping_cost; + $item->total_price = $item->quantity * $item->price; + $totalPrice += $item->total_price; + $total_products += $item->quantity; + $item->image_url = $this->url->to("product/" . $item->image_url); + } + + return [ + 'items' => $items, + 'total_amount' => $totalPrice, + 'total_shipping_cost' => $totalShippingCost, + 'total_products' => $total_products, + 'total' => $totalPrice + $totalShippingCost + ]; + } + public function subscriptionList() + { + + $orderDetails = Cart::leftJoin('items', 'carts.id', 'items.cart_id') + ->leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('patient_id', $this->user_id) + ->where('plans_v1.is_prescription_required', '1') + ->get(); + foreach ($orderDetails as $details) { + $details->image_url = $this->url->to("product/" . $details->image_url); + } + + return response() + ->json([ + 'subscriptions' => $orderDetails + ]); + } + public function getSubscriptionDetails($id) + { + $orderDetails = Cart::find($id); + + $items = Item::leftJoin('plans_v1', 'items.plans_id', 'plans_v1.id') + ->where('plans_v1.is_prescription_required', '1') + ->get(); + $appointments = Appointment::find($orderDetails->appointment_id); + + $patient = $orderDetails->patient; + + $patient->profile_picture = $this->url->to("storage/profile_pictures/" . $patient->profile_picture); + + return response() + ->json([ + 'order_details' => $orderDetails, + 'order_items' => $items, + 'patient_details' => $patient, + 'appointment_details' => $appointments + + ]); + } +} diff --git a/app/Http/Controllers/PatientController.php b/app/Http/Controllers/PatientController.php new file mode 100644 index 0000000..d57b434 --- /dev/null +++ b/app/Http/Controllers/PatientController.php @@ -0,0 +1,1852 @@ +url = $url; + } + public function registerPatient(Request $request) + { + $validatedData = $request->validate([ + 'first_name' => 'required|string|max:255', + 'last_name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:patients', + 'password' => 'required', + // 'state' => 'required', + 'dob' => 'required|date_format:Y-m-d', + // 'address' => 'required', + // 'country' => 'required', + 'phone_no' => 'required' + ]); + + + $defaultPassword = $validatedData['password']; + + $patient = Patient::create([ + 'first_name' => $validatedData['first_name'], + 'last_name' => $validatedData['last_name'], + 'phone_no' => $validatedData['phone_no'], + 'email' => $validatedData['email'], + 'password' => Hash::make($defaultPassword), + // 'city' => $validatedData['city'], + // 'state' => $validatedData['state'], + 'dob' => $validatedData['dob'], + // 'zip_code' => $request->input('zip_code') ?? "", + // 'shipping_address' => $request->input('shipping_address') ?? "", + // 'shipping_city' => $request->input('shipping_city') ?? "", + // 'shipping_state' => $request->input('shipping_state') ?? "", + // 'shipping_country' => $request->input('shipping_country') ?? "", + // 'shipping_zipcode' => $request->input('shipping_zipcode') ?? "", + // 'address' => $validatedData['address'], + // 'lat' => $request->get('lat'), + // 'long' => $request->get('long'), + // 'country' => $validatedData['country'], + 'gender' => $request->input('gender') ?? "", + // 'marital_status' => $request->input('marital_status') ?? "", + // 'height' => $request->input('height') ?? "", + // 'weight' => $request->input('weight') ?? "", + ]); + + $token = $patient->createToken('auth_token')->plainTextToken; + + if ($patient->dob) { + $birthDate = new DateTime($patient->dob); + $today = new DateTime(date('Y-m-d')); + $age = $today->diff($birthDate)->y; + $patient->age = $age; + } else { + $patient->age = 0; + } + PatientRegActivity::create([ + 'patient_id' => $patient->id, + 'activity' => 'patient_registered' + ]); + $setting = Setting::find(1); + event(new PatientRegistered($patient, $validatedData)); + return response() + ->json([ + 'data' => $patient, + 'access_token' => $token, + 'token_type' => 'Bearer', + 'password' => $defaultPassword + ]); + } + //write for + + public function forgotPassword(Request $request) + { + $request->validate([ + 'email' => 'required|email', + ]); + + $patient = Patient::where('email', $request->email)->first(); + + if (!$patient) { + return response()->json(['message' => 'Email does not exist'], 404); + } + + $token = Str::random(60); + + $patient->update(['remember_token' => $token]); + + // Send reset link email + Mail::send('emails.password_reset', ['token' => $token], function ($message) use ($request) { + $message->to($request->email); + $message->subject('Password Reset Request'); + }); + + return response()->json(['message' => 'Password reset link sent']); + } + + public function resetPassword(Request $request) + { + $request->validate([ + 'token' => 'required', + 'password' => 'required|confirmed', + ]); + + $patient = Patient::where('remember_token', $request->token)->first(); + + if (!$patient) { + return response()->json(['message' => 'Invalid token'], 400); + } + + $patient->update([ + 'password' => Hash::make($request->password), + 'remember_token' => null, + ]); + + return response()->json(['message' => 'Password reset successful']); + } + public function updatePatient(Request $request) + { + $patient = Auth::guard('patient')->user(); + + // Collect only the fields that are present in the request + $dataToUpdate = array_filter($request->only([ + 'first_name', + 'last_name', + 'email', + 'phone_no', + 'city', + 'state', + 'address', + 'zip_code', + 'country', + 'dob', + 'gender', + 'marital_status', + 'height', + 'weight' + ]), function ($value) { + return $value !== null; + }); + + // Update the patient with the collected data + $patient->update($dataToUpdate); + return response()->json([ + 'data' => $patient, + 'message' => 'Patient updated successfully' + ]); + } + public function editPatient($id, Request $request) + { + $patient = Patient::where('id', $id)->first(); + if (!empty($request->input('address'))) + $patient->address = $request->input('address'); + if (!empty($request->input('city'))) + $patient->city = $request->input('city'); + if (!empty($request->input('state'))) + $patient->state = $request->input('state'); + if (!empty($request->input('zip_code'))) + $patient->zip_code = $request->input('zip_code'); + if (!empty($request->input('country'))) + $patient->country = $request->input('country'); + if (!empty($request->input('dob'))) + $patient->dob = $request->input('dob'); + if (!empty($request->input('gender'))) + $patient->gender = $request->input('gender'); + if (!empty($request->input('marital_status'))) + $patient->marital_status = $request->input('marital_status'); + if (!empty($request->input('height'))) + $patient->height = $request->input('height'); + if (!empty($request->input('weight'))) + $patient->weight = $request->input('weight'); + $patient->save(); + + + + $patient->save(); + + return response()->json([ + 'data' => $patient, + 'message' => 'Patient updated successfully' + ]); + } + + // PatientController + public function loginPatient(Request $request) + { + $validatedData = $request->validate([ + 'email' => 'required|email', + 'password' => 'required' + ]); + + $patient = Patient::where('email', $validatedData['email'])->firstOrFail(); + if ($patient->dob) { + $birthDate = new DateTime($patient->dob); + $today = new DateTime(date('Y-m-d')); + $age = $today->diff($birthDate)->y; + $patient->age = $age; + } else { + $patient->age = 0; + } + + + if (!$patient || !Hash::check($validatedData['password'], $patient->password)) { + return response([ + 'message' => 'Invalid credentials' + ], 401); + } + + $token = $patient->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'data' => $patient, + 'access_token' => $token, + 'token_type' => 'Bearer', + ]); + } + public function updatePassword(Request $request) + { + + $request->validate([ + /* 'old_password' => 'required', */ + 'new_password' => 'required|string|min:8', + ]); + + $patient = Auth::guard('patient')->user(); + // Check if the old password is correct + /* if (!Hash::check($request->old_password, $patient->password)) { + return response([ + 'message' => 'The provided old password is incorrect.' + ], 400); // Use 400 for bad request + } */ + + // Update the password + $patient->password = Hash::make($request->new_password); + $patient->save(); + + return response()->json([ + 'message' => 'Password updated successfully.' + ]); + } + + // Inside your AppointmentController class: + + public function availableSlots($date) + { + // Ensure date is in a valid format + $date = Carbon::parse($date); + $originalDate = Carbon::parse($date); + + // Generate all possible 30-minute slots between 9 AM and 4 PM + $slots = collect(); + $startTime = Carbon::parse($date)->subHours(24)->setTime(9, 0, 0); + $endTime = Carbon::parse($date)->addHours(24)->setTime(16, 0, 0); + while ($startTime < $endTime) { + $slots->push($startTime->format('Y-m-d H:i:s')); + $startTime->addMinutes(15); + } + + $user = Auth::guard('patient')->user(); + // Filter out booked slots + $bookedAppointments = Appointment::where("patient_id", $user->id) + ->where('appointment_date', '>=', $date->format('Y-m-d')) + ->where('appointment_date', '<', $date->addDay()->format('Y-m-d')) + ->pluck('appointment_date'); + + $availableSlots = $slots->diff($bookedAppointments); + + $formattedSlots = $availableSlots->map(function ($slot) { + $start = Carbon::parse($slot); + $startTime = $start->format('Y-m-d H:i:s'); + return $startTime; + }); + + // Additional checking if slot is booked + $formattedSlots = $formattedSlots->filter(function ($slot) use ($originalDate) { + $time = Carbon::parse($slot); + return !Appointment::where('appointment_time', $time->format('H:i:s')) + ->where('appointment_date', $originalDate->format('Y-m-d')) + ->exists(); + }); + + return response()->json([ + 'available_slots' => $formattedSlots->toArray() + ]); + } + + + public function bookAppointment(Request $request) + { + $validatedData = $request->validate([ + /* 'telemed_pros_id' => 'required|exists:telemed_pros,id', */ + 'patient_id' => 'required|exists:patients,id', + 'appointment_time' => 'required|date_format:H:i:s', + 'appointment_date' => 'required|date_format:Y-m-d', + 'patient_name' => 'required', + 'patient_email' => 'required', + 'timezone' => 'required', + ]); + try { + $tz = new DateTimeZone($validatedData['timezone']); + $standardTz = $tz->getName(); + } catch (Exception $e) { + return response()->json([ + 'message' => $e->getMessage() + ], 400); + } + try { + $timezoneMap = [ + 'EST' => 'America/New_York', + 'EDT' => 'America/New_York', + 'CST' => 'America/Chicago', + 'CDT' => 'America/Chicago', + 'MST' => 'America/Denver', + 'MDT' => 'America/Denver', + 'PST' => 'America/Los_Angeles', + 'PDT' => 'America/Los_Angeles', + // Add more mappings as needed + ]; + $timezone = $validatedData['timezone']; + if (array_key_exists($timezone, $timezoneMap)) { + $timezone = $timezoneMap[$timezone]; + } + + $appointmentDateTime = new DateTime( + $validatedData['appointment_date'] . ' ' . $validatedData['appointment_time'], + new DateTimeZone($timezone) + ); + + $appointmentDateTime->setTimezone(new DateTimeZone('UTC')); + + $validatedData['appointment_time'] = $appointmentDateTime->format('H:i:s'); + $validatedData['appointment_date'] = $appointmentDateTime->format('Y-m-d'); + } catch (Exception $e) { + return response()->json([ + 'message' => $e->getMessage() + ], 400); + } + + $availableTelemedPros = Telemedpro::select("telemed_pros.id", "telemed_pros.name")/* ->where('is_busy', false) */ + ->leftJoin('appointments', function ($join) use ($validatedData) { + $join->on('telemed_pros.id', '=', 'appointments.telemed_pros_id') + ->where('appointments.appointment_time', '=', $validatedData['appointment_time']) + ->where('appointments.appointment_date', '=', $validatedData['appointment_date']); + }) + ->whereNull('appointments.id') + ->first(); + + if (!$availableTelemedPros) + return response()->json([ + 'message' => 'Appointment time not available' + ], 400); + + $existingAppointment = Appointment::where('telemed_pros_id', $availableTelemedPros->id) + ->where('appointment_time', $validatedData['appointment_time']) + ->where('appointment_date', $validatedData['appointment_date']) + ->first(); + + if ($existingAppointment) { + return response()->json([ + 'message' => 'Appointment time not available' + ], 400); + } + $validatedData['telemed_pros_id'] = $availableTelemedPros->id; + $validatedData['status'] = 'pending'; + + // Create the appointment + $appointment = Appointment::create($validatedData); + //$appointment_booking_tokens = $this->bookAppointmentApi($appointment, $availableTelemedPros); + $appointment_booking_tokens = "null"; + /* $appointment->agent_call_token = $appointment_booking_tokens['tokenAgent']; + $appointment->patient_call_token = $appointment_booking_tokens['tokenPatient']; */ + $appointment->agent_call_token = "null"; + $appointment->patient_call_token = "null"; + $appointment->save(); + + PatientRegActivity::create([ + 'patient_id' => $validatedData['patient_id'], + 'activity' => 'patient_appointment_booked' + ]); + $patient = $appointment->patient; + $datetimeUtc = $appointment->appointment_date . ' ' . $appointment->appointment_time; + $dateTimeUtc = Carbon::createFromFormat('Y-m-d H:i:s', $datetimeUtc, 'UTC'); + $appointmentTimeZone = new CarbonTimeZone($appointment->timezone); + $dateTimeInAppointmentTimeZone = $dateTimeUtc->setTimezone($appointmentTimeZone); + $appointment->appointment_date = $appointmentDate = $dateTimeInAppointmentTimeZone->format('Y-m-d'); + $appointment->appointment_time = $appointmentTime = $dateTimeInAppointmentTimeZone->format('H:i:s'); + $setting = Setting::find(1); + event(new AppointmentBooked($appointment)); + $cart = Cart::find($request->input("cart_id")); + $cart->appointment_id = $appointment->id; + $cart->save(); + return response()->json([ + 'message' => 'Appointment booked successfully', + 'meeting_id' => $appointment->agent_call_token, + 'appointment' => $appointment, + 'appointment_time' => $validatedData['appointment_time'], + 'appointment_date' => $validatedData['appointment_date'] + ]); + } + + public function appointmentDetail(Appointment $appointment) + { + $patient = Patient::find($appointment->patient_id); + $telemedPro = Telemedpro::find($appointment->telemed_pros_id); + $doctor_appointment = DoctorAppointment::select('doctor_appointments.*', 'doctors.name as name')->where('appointment_id', $appointment->id) + ->leftjoin('doctors', 'doctors.id', '=', 'doctor_appointments.doctor_id') + ->first(); + if (!$doctor_appointment) + $doctor_appointment = []; + else + $doctor_appointment = $doctor_appointment->toArray(); + return response()->json([ + 'patient' => $patient->toArray() ?? [], + 'telemedPro' => $telemedPro->toArray() ?? [], + 'doctor_appointment' => $doctor_appointment, + 'video_url' => "https://plugnmeet.codelfi.com/recordings/" . $appointment->video_token . ".mp4", + ]); + } + public function bookAppointmentApi($appointment, $availableTelemedPros) + { + $roomName = 'appointment-' . $appointment->id . "-" . uniqid(); + $opts = (new RoomCreateOptions()) + ->setName($roomName) + ->setEmptyTimeout(10) + ->setMaxParticipants(5); + $host = "https://plugnmeet.codelfi.com"; + $svc = new RoomServiceClient($host, config('app.LK_API_KEY'), config('app.LK_API_SECRET')); + $room = $svc->createRoom($opts); + + $participantPatientName = "patient-" . uniqid() . $appointment->patient->first_name . " " . $appointment->patient->last_name; + + $tokenOptionsPatient = (new AccessTokenOptions()) + ->setIdentity($participantPatientName); + $videoGrantPatient = (new VideoGrant()) + ->setRoomJoin() + ->setRoomName($roomName); + $tokenPatient = (new AccessToken(config('app.LK_API_KEY'), config('app.LK_API_SECRET'))) + ->init($tokenOptionsPatient) + ->setGrant($videoGrantPatient) + ->toJwt(); + + $participantAgentName = "agent-" . uniqid() . $availableTelemedPros->name; + $tokenOptionsAgent = (new AccessTokenOptions()) + ->setIdentity($participantAgentName); + $videoGrantAgent = (new VideoGrant()) + ->setRoomJoin() + ->setRoomName($roomName); + $tokenAgent = (new AccessToken(config('app.LK_API_KEY'), config('app.LK_API_SECRET'))) + ->init($tokenOptionsAgent) + ->setGrant($videoGrantAgent) + ->toJwt(); + return [ + 'tokenPatient' => $tokenPatient, + 'tokenAgent' => $tokenAgent, + ]; + } + public function addPatientToQueue($patientId) + { + + // Try to get existing queue + $queue = Queue::where('patient_id', $patientId)->first(); + + // No existing queue + if (!$queue) { + + // Create new queue entry + $queue = new Queue; + $queue->patient_id = $patientId; + $queue->queue_number = Queue::max('queue_number') + 1; + $queue->save(); + + return response()->json([ + 'message' => 'Added to queue', + 'queue_number' => $queue->queue_number + ]); + } else { + + if ($queue->queue_number == 0) { + $appointment = Appointment::create([ + 'patient_id' => $patientId, + 'appointment_date' => Carbon::now()->format('Y-m-d'), + 'appointment_time' => Carbon::now()->format('H:i:s'), + ]); + return response()->json([ + 'message' => 'ok', + 'meeting_id' => $appointment->meeting_id, + 'queue_number' => 0 + ]); + } else { + + return response()->json([ + 'message' => 'Waiting in queue', + 'queue_number' => $queue->queue_number + ]); + } + } + } + public function getAppointmentsByPatientId(int $patientId) + { + try { + $appointments = Appointment::where('patient_id', $patientId) + ->with('telemedPro') // Eager load the associated telemed pro + /* ->orderBy('appointment_time', 'desc') */ // Optional: sort by appointment time + ->get(); + + return response()->json($appointments, 200); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } + public function getDoctorAppointmentsByPatientId(int $patientId) + { + try { + $appointments = DoctorAppointment::select("patients.first_name", "patients.last_name", "doctor_appointments.*") // Eager load the associated telemed pro + ->leftJoin("patients", "patients.id", "doctor_appointments.patient_id") + ->where('patient_id', $patientId) + /* ->orderBy('appointment_time', 'desc') */ // Optional: sort by appointment time + ->get(); + + return response()->json($appointments, 200); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to retrieve appointments'], 500); + } + } + public function getLabcorpData(Request $request) + { + $url = 'https://www.labcorp.com/labs-and-appointments/results'; + + $params = [ + 'geo_address' => $request->input('address'), + 'address_single' => $request->input('address'), + 'city' => $request->input('city'), + 'state' => $request->input('state'), + 'zip' => $request->input('zip'), + 'service' => 'ROUTINE_PHLEBOTOMY', + 'radius' => 25, + 'gps' => false, + ]; + + $response = Http::get($url, $params); + // Check if the request was successful (status code 200) + if ($response->successful()) { + // Extract JSON using regular expressions + $pattern = '/ + + + + + + diff --git a/resources/js/@core/AppDrawerHeaderSection.vue b/resources/js/@core/AppDrawerHeaderSection.vue new file mode 100644 index 0000000..acd3c03 --- /dev/null +++ b/resources/js/@core/AppDrawerHeaderSection.vue @@ -0,0 +1,28 @@ + + + diff --git a/resources/js/@core/AppStepper.vue b/resources/js/@core/AppStepper.vue new file mode 100644 index 0000000..dc0ad00 --- /dev/null +++ b/resources/js/@core/AppStepper.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/resources/js/@core/I18n.vue b/resources/js/@core/I18n.vue new file mode 100644 index 0000000..c6c1349 --- /dev/null +++ b/resources/js/@core/I18n.vue @@ -0,0 +1,56 @@ + + + diff --git a/resources/js/@core/MoreBtn.vue b/resources/js/@core/MoreBtn.vue new file mode 100644 index 0000000..7876fe6 --- /dev/null +++ b/resources/js/@core/MoreBtn.vue @@ -0,0 +1,31 @@ + + + diff --git a/resources/js/@core/Notifications.vue b/resources/js/@core/Notifications.vue new file mode 100644 index 0000000..400eaac --- /dev/null +++ b/resources/js/@core/Notifications.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/resources/js/@core/ScrollToTop.vue b/resources/js/@core/ScrollToTop.vue new file mode 100644 index 0000000..296722f --- /dev/null +++ b/resources/js/@core/ScrollToTop.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/resources/js/@core/TheCustomizer.vue b/resources/js/@core/TheCustomizer.vue new file mode 100644 index 0000000..ed9f007 --- /dev/null +++ b/resources/js/@core/TheCustomizer.vue @@ -0,0 +1,367 @@ + + + + + diff --git a/resources/js/@core/ThemeSwitcher.vue b/resources/js/@core/ThemeSwitcher.vue new file mode 100644 index 0000000..f61dc37 --- /dev/null +++ b/resources/js/@core/ThemeSwitcher.vue @@ -0,0 +1,43 @@ + + + diff --git a/resources/js/@core/app-form-elements/AppAutocomplete.vue b/resources/js/@core/app-form-elements/AppAutocomplete.vue new file mode 100644 index 0000000..fd16e6c --- /dev/null +++ b/resources/js/@core/app-form-elements/AppAutocomplete.vue @@ -0,0 +1,56 @@ + + + diff --git a/resources/js/@core/app-form-elements/AppCombobox.vue b/resources/js/@core/app-form-elements/AppCombobox.vue new file mode 100644 index 0000000..6e397c7 --- /dev/null +++ b/resources/js/@core/app-form-elements/AppCombobox.vue @@ -0,0 +1,57 @@ + + + diff --git a/resources/js/@core/app-form-elements/AppDateTimePicker.vue b/resources/js/@core/app-form-elements/AppDateTimePicker.vue new file mode 100644 index 0000000..28140b1 --- /dev/null +++ b/resources/js/@core/app-form-elements/AppDateTimePicker.vue @@ -0,0 +1,501 @@ + + + + + diff --git a/resources/js/@core/app-form-elements/AppOtpInput.vue b/resources/js/@core/app-form-elements/AppOtpInput.vue new file mode 100644 index 0000000..bd1a93e --- /dev/null +++ b/resources/js/@core/app-form-elements/AppOtpInput.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/resources/js/@core/app-form-elements/AppSelect.vue b/resources/js/@core/app-form-elements/AppSelect.vue new file mode 100644 index 0000000..e4e42f2 --- /dev/null +++ b/resources/js/@core/app-form-elements/AppSelect.vue @@ -0,0 +1,49 @@ + + + diff --git a/resources/js/@core/app-form-elements/AppTextField.vue b/resources/js/@core/app-form-elements/AppTextField.vue new file mode 100644 index 0000000..ea672b4 --- /dev/null +++ b/resources/js/@core/app-form-elements/AppTextField.vue @@ -0,0 +1,48 @@ + + + diff --git a/resources/js/@core/app-form-elements/AppTextarea.vue b/resources/js/@core/app-form-elements/AppTextarea.vue new file mode 100644 index 0000000..5609678 --- /dev/null +++ b/resources/js/@core/app-form-elements/AppTextarea.vue @@ -0,0 +1,49 @@ + + + diff --git a/resources/js/@core/app-form-elements/CustomCheckboxesWithIcon.vue b/resources/js/@core/app-form-elements/CustomCheckboxesWithIcon.vue new file mode 100644 index 0000000..aaa4489 --- /dev/null +++ b/resources/js/@core/app-form-elements/CustomCheckboxesWithIcon.vue @@ -0,0 +1,96 @@ + + + + + + + diff --git a/resources/js/@core/app-form-elements/CustomRadios.vue b/resources/js/@core/app-form-elements/CustomRadios.vue new file mode 100644 index 0000000..7187dbf --- /dev/null +++ b/resources/js/@core/app-form-elements/CustomRadios.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/resources/js/@core/app-form-elements/CustomRadiosWithIcon.vue b/resources/js/@core/app-form-elements/CustomRadiosWithIcon.vue new file mode 100644 index 0000000..29fd0af --- /dev/null +++ b/resources/js/@core/app-form-elements/CustomRadiosWithIcon.vue @@ -0,0 +1,92 @@ + + + + + + + diff --git a/resources/js/@core/app-form-elements/CustomRadiosWithImage.vue b/resources/js/@core/app-form-elements/CustomRadiosWithImage.vue new file mode 100644 index 0000000..86d4bc7 --- /dev/null +++ b/resources/js/@core/app-form-elements/CustomRadiosWithImage.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/resources/js/@core/cards/AppCardActions.vue b/resources/js/@core/cards/AppCardActions.vue new file mode 100644 index 0000000..6578818 --- /dev/null +++ b/resources/js/@core/cards/AppCardActions.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/resources/js/@core/cards/AppCardCode.vue b/resources/js/@core/cards/AppCardCode.vue new file mode 100644 index 0000000..df22c79 --- /dev/null +++ b/resources/js/@core/cards/AppCardCode.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/resources/js/@core/cards/CardStatisticsHorizontal.vue b/resources/js/@core/cards/CardStatisticsHorizontal.vue new file mode 100644 index 0000000..91ff6b1 --- /dev/null +++ b/resources/js/@core/cards/CardStatisticsHorizontal.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/js/@core/cards/CardStatisticsVertical.vue b/resources/js/@core/cards/CardStatisticsVertical.vue new file mode 100644 index 0000000..1690421 --- /dev/null +++ b/resources/js/@core/cards/CardStatisticsVertical.vue @@ -0,0 +1,61 @@ + + + diff --git a/resources/js/@core/components/MoreBtn.vue b/resources/js/@core/components/MoreBtn.vue new file mode 100644 index 0000000..a93ef06 --- /dev/null +++ b/resources/js/@core/components/MoreBtn.vue @@ -0,0 +1,28 @@ + + + diff --git a/resources/js/@core/components/Notifications.vue b/resources/js/@core/components/Notifications.vue new file mode 100644 index 0000000..43cf545 --- /dev/null +++ b/resources/js/@core/components/Notifications.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/resources/js/@core/components/ThemeSwitcher.vue b/resources/js/@core/components/ThemeSwitcher.vue new file mode 100644 index 0000000..094723a --- /dev/null +++ b/resources/js/@core/components/ThemeSwitcher.vue @@ -0,0 +1,43 @@ + + + diff --git a/resources/js/@core/components/cards/CardStatisticsHorizontal.vue b/resources/js/@core/components/cards/CardStatisticsHorizontal.vue new file mode 100644 index 0000000..610e4a7 --- /dev/null +++ b/resources/js/@core/components/cards/CardStatisticsHorizontal.vue @@ -0,0 +1,62 @@ + + + diff --git a/resources/js/@core/components/cards/CardStatisticsVertical.vue b/resources/js/@core/components/cards/CardStatisticsVertical.vue new file mode 100644 index 0000000..fb645b5 --- /dev/null +++ b/resources/js/@core/components/cards/CardStatisticsVertical.vue @@ -0,0 +1,95 @@ + + + diff --git a/resources/js/@core/components/cards/CardStatisticsWithImages.vue b/resources/js/@core/components/cards/CardStatisticsWithImages.vue new file mode 100644 index 0000000..2d3fdec --- /dev/null +++ b/resources/js/@core/components/cards/CardStatisticsWithImages.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/resources/js/@core/composable/useResponsiveSidebar.js b/resources/js/@core/composable/useResponsiveSidebar.js new file mode 100644 index 0000000..8fb15c9 --- /dev/null +++ b/resources/js/@core/composable/useResponsiveSidebar.js @@ -0,0 +1,23 @@ +import { useDisplay } from 'vuetify' + +export const useResponsiveLeftSidebar = (mobileBreakpoint = undefined) => { + const { mdAndDown, name: currentBreakpoint } = useDisplay() + const _mobileBreakpoint = mobileBreakpoint || mdAndDown + const isLeftSidebarOpen = ref(true) + + const setInitialValue = () => { + isLeftSidebarOpen.value = !_mobileBreakpoint.value + } + + + // Set the initial value of sidebar + setInitialValue() + watch(currentBreakpoint, () => { + // Reset left sidebar + isLeftSidebarOpen.value = !_mobileBreakpoint.value + }) + + return { + isLeftSidebarOpen, + } +} diff --git a/resources/js/@core/libs/apex-chart/apexCharConfig.js b/resources/js/@core/libs/apex-chart/apexCharConfig.js new file mode 100644 index 0000000..4c27a37 --- /dev/null +++ b/resources/js/@core/libs/apex-chart/apexCharConfig.js @@ -0,0 +1,681 @@ +import { hexToRgb } from '@layouts/utils' + + +// 👉 Colors variables +const colorVariables = themeColors => { + const themeSecondaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['medium-emphasis-opacity']})` + const themeDisabledTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['disabled-opacity']})` + const themeBorderColor = `rgba(${hexToRgb(String(themeColors.variables['border-color']))},${themeColors.variables['border-opacity']})` + const themePrimaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['high-emphasis-opacity']})` + + return { themeSecondaryTextColor, themeDisabledTextColor, themeBorderColor, themePrimaryTextColor } +} + +export const getScatterChartConfig = themeColors => { + const scatterColors = { + series1: '#ff9f43', + series2: '#7367f0', + series3: '#28c76f', + } + + const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + zoom: { + type: 'xy', + enabled: true, + }, + }, + legend: { + position: 'top', + horizontalAlign: 'left', + markers: { offsetX: -3 }, + labels: { colors: themeSecondaryTextColor }, + itemMargin: { + vertical: 3, + horizontal: 10, + }, + }, + colors: [scatterColors.series1, scatterColors.series2, scatterColors.series3], + grid: { + borderColor: themeBorderColor, + xaxis: { + lines: { show: true }, + }, + }, + yaxis: { + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + xaxis: { + tickAmount: 10, + axisBorder: { show: false }, + axisTicks: { color: themeBorderColor }, + crosshairs: { + stroke: { color: themeBorderColor }, + }, + labels: { + style: { colors: themeDisabledTextColor }, + formatter: val => parseFloat(val).toFixed(1), + }, + }, + } +} +export const getLineChartSimpleConfig = themeColors => { + const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + parentHeightOffset: 0, + zoom: { enabled: false }, + toolbar: { show: false }, + }, + colors: ['#ff9f43'], + stroke: { curve: 'straight' }, + dataLabels: { enabled: false }, + markers: { + strokeWidth: 7, + strokeOpacity: 1, + colors: ['#ff9f43'], + strokeColors: ['#fff'], + }, + grid: { + padding: { top: -10 }, + borderColor: themeBorderColor, + xaxis: { + lines: { show: true }, + }, + }, + tooltip: { + custom(data) { + return `
+ ${data.series[data.seriesIndex][data.dataPointIndex]}% +
` + }, + }, + yaxis: { + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + xaxis: { + axisBorder: { show: false }, + axisTicks: { color: themeBorderColor }, + crosshairs: { + stroke: { color: themeBorderColor }, + }, + labels: { + style: { colors: themeDisabledTextColor }, + }, + categories: [ + '7/12', + '8/12', + '9/12', + '10/12', + '11/12', + '12/12', + '13/12', + '14/12', + '15/12', + '16/12', + '17/12', + '18/12', + '19/12', + '20/12', + '21/12', + ], + }, + } +} +export const getBarChartConfig = themeColors => { + const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + }, + colors: ['#00cfe8'], + dataLabels: { enabled: false }, + plotOptions: { + bar: { + borderRadius: 8, + barHeight: '30%', + horizontal: true, + startingShape: 'rounded', + }, + }, + grid: { + borderColor: themeBorderColor, + xaxis: { + lines: { show: false }, + }, + padding: { + top: -10, + }, + }, + yaxis: { + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + xaxis: { + axisBorder: { show: false }, + axisTicks: { color: themeBorderColor }, + categories: ['MON, 11', 'THU, 14', 'FRI, 15', 'MON, 18', 'WED, 20', 'FRI, 21', 'MON, 23'], + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + } +} +export const getCandlestickChartConfig = themeColors => { + const candlestickColors = { + series1: '#28c76f', + series2: '#ea5455', + } + + const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + }, + plotOptions: { + bar: { columnWidth: '40%' }, + candlestick: { + colors: { + upward: candlestickColors.series1, + downward: candlestickColors.series2, + }, + }, + }, + grid: { + padding: { top: -10 }, + borderColor: themeBorderColor, + xaxis: { + lines: { show: true }, + }, + }, + yaxis: { + tooltip: { enabled: true }, + crosshairs: { + stroke: { color: themeBorderColor }, + }, + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + xaxis: { + type: 'datetime', + axisBorder: { show: false }, + axisTicks: { color: themeBorderColor }, + crosshairs: { + stroke: { color: themeBorderColor }, + }, + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + } +} +export const getRadialBarChartConfig = themeColors => { + const radialBarColors = { + series1: '#fdd835', + series2: '#32baff', + series3: '#00d4bd', + series4: '#7367f0', + series5: '#FFA1A1', + } + + const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors) + + return { + stroke: { lineCap: 'round' }, + labels: ['Comments', 'Replies', 'Shares'], + legend: { + show: true, + position: 'bottom', + labels: { + colors: themeSecondaryTextColor, + }, + markers: { + offsetX: -3, + }, + itemMargin: { + vertical: 3, + horizontal: 10, + }, + }, + colors: [radialBarColors.series1, radialBarColors.series2, radialBarColors.series4], + plotOptions: { + radialBar: { + hollow: { size: '30%' }, + track: { + margin: 15, + background: themeColors.colors['grey-100'], + }, + dataLabels: { + name: { + fontSize: '2rem', + }, + value: { + fontSize: '1rem', + color: themeSecondaryTextColor, + }, + total: { + show: true, + fontWeight: 400, + label: 'Comments', + fontSize: '1.125rem', + color: themePrimaryTextColor, + formatter(w) { + const totalValue = w.globals.seriesTotals.reduce((a, b) => { + return a + b + }, 0) / w.globals.series.length + + if (totalValue % 1 === 0) + return `${totalValue}%` + else + return `${totalValue.toFixed(2)}%` + }, + }, + }, + }, + }, + grid: { + padding: { + top: -30, + bottom: -25, + }, + }, + } +} +export const getDonutChartConfig = themeColors => { + const donutColors = { + series1: '#fdd835', + series2: '#00d4bd', + series3: '#826bf8', + series4: '#32baff', + series5: '#ffa1a1', + } + + const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors) + + return { + stroke: { width: 0 }, + labels: ['Operational', 'Networking', 'Hiring', 'R&D'], + colors: [donutColors.series1, donutColors.series5, donutColors.series3, donutColors.series2], + dataLabels: { + enabled: true, + formatter: val => `${parseInt(val, 10)}%`, + }, + legend: { + position: 'bottom', + markers: { offsetX: -3 }, + labels: { colors: themeSecondaryTextColor }, + itemMargin: { + vertical: 3, + horizontal: 10, + }, + }, + plotOptions: { + pie: { + donut: { + labels: { + show: true, + name: { + fontSize: '1.5rem', + }, + value: { + fontSize: '1.5rem', + color: themeSecondaryTextColor, + formatter: val => `${parseInt(val, 10)}`, + }, + total: { + show: true, + fontSize: '1.5rem', + label: 'Operational', + formatter: () => '31%', + color: themePrimaryTextColor, + }, + }, + }, + }, + }, + responsive: [ + { + breakpoint: 992, + options: { + chart: { + height: 380, + }, + legend: { + position: 'bottom', + }, + }, + }, + { + breakpoint: 576, + options: { + chart: { + height: 320, + }, + plotOptions: { + pie: { + donut: { + labels: { + show: true, + name: { + fontSize: '1rem', + }, + value: { + fontSize: '1rem', + }, + total: { + fontSize: '1rem', + }, + }, + }, + }, + }, + }, + }, + ], + } +} +export const getAreaChartSplineConfig = themeColors => { + const areaColors = { + series3: '#e0cffe', + series2: '#b992fe', + series1: '#ab7efd', + } + + const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + }, + tooltip: { shared: false }, + dataLabels: { enabled: false }, + stroke: { + show: false, + curve: 'straight', + }, + legend: { + position: 'top', + horizontalAlign: 'left', + labels: { colors: themeSecondaryTextColor }, + markers: { + offsetY: 1, + offsetX: -3, + }, + itemMargin: { + vertical: 3, + horizontal: 10, + }, + }, + colors: [areaColors.series3, areaColors.series2, areaColors.series1], + fill: { + opacity: 1, + type: 'solid', + }, + grid: { + show: true, + borderColor: themeBorderColor, + xaxis: { + lines: { show: true }, + }, + }, + yaxis: { + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + xaxis: { + axisBorder: { show: false }, + axisTicks: { color: themeBorderColor }, + crosshairs: { + stroke: { color: themeBorderColor }, + }, + labels: { + style: { colors: themeDisabledTextColor }, + }, + categories: [ + '7/12', + '8/12', + '9/12', + '10/12', + '11/12', + '12/12', + '13/12', + '14/12', + '15/12', + '16/12', + '17/12', + '18/12', + '19/12', + ], + }, + } +} +export const getColumnChartConfig = themeColors => { + const columnColors = { + series1: '#826af9', + series2: '#d2b0ff', + bg: '#f8d3ff', + } + + const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + offsetX: -10, + stacked: true, + parentHeightOffset: 0, + toolbar: { show: false }, + }, + fill: { opacity: 1 }, + dataLabels: { enabled: false }, + colors: [columnColors.series1, columnColors.series2], + legend: { + position: 'top', + horizontalAlign: 'left', + labels: { colors: themeSecondaryTextColor }, + markers: { + offsetY: 1, + offsetX: -3, + }, + itemMargin: { + vertical: 3, + horizontal: 10, + }, + }, + stroke: { + show: true, + colors: ['transparent'], + }, + plotOptions: { + bar: { + columnWidth: '15%', + colors: { + backgroundBarRadius: 10, + backgroundBarColors: [columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg], + }, + }, + }, + grid: { + borderColor: themeBorderColor, + xaxis: { + lines: { show: true }, + }, + }, + yaxis: { + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + xaxis: { + axisBorder: { show: false }, + axisTicks: { color: themeBorderColor }, + categories: ['7/12', '8/12', '9/12', '10/12', '11/12', '12/12', '13/12', '14/12', '15/12'], + crosshairs: { + stroke: { color: themeBorderColor }, + }, + labels: { + style: { colors: themeDisabledTextColor }, + }, + }, + responsive: [ + { + breakpoint: 600, + options: { + plotOptions: { + bar: { + columnWidth: '35%', + }, + }, + }, + }, + ], + } +} +export const getHeatMapChartConfig = themeColors => { + const { themeSecondaryTextColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + }, + dataLabels: { enabled: false }, + stroke: { + colors: [themeColors.colors.surface], + }, + legend: { + position: 'bottom', + labels: { + colors: themeSecondaryTextColor, + }, + markers: { + offsetY: 0, + offsetX: -3, + }, + itemMargin: { + vertical: 3, + horizontal: 10, + }, + }, + plotOptions: { + heatmap: { + enableShades: false, + colorScale: { + ranges: [ + { to: 10, from: 0, name: '0-10', color: '#b9b3f8' }, + { to: 20, from: 11, name: '10-20', color: '#aba4f6' }, + { to: 30, from: 21, name: '20-30', color: '#9d95f5' }, + { to: 40, from: 31, name: '30-40', color: '#8f85f3' }, + { to: 50, from: 41, name: '40-50', color: '#8176f2' }, + { to: 60, from: 51, name: '50-60', color: '#7367f0' }, + ], + }, + }, + }, + grid: { + padding: { top: -20 }, + }, + yaxis: { + labels: { + style: { + colors: themeDisabledTextColor, + }, + }, + }, + xaxis: { + labels: { show: false }, + axisTicks: { show: false }, + axisBorder: { show: false }, + }, + } +} +export const getRadarChartConfig = themeColors => { + const radarColors = { + series1: '#9b88fa', + series2: '#ffa1a1', + } + + const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors) + + return { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + dropShadow: { + top: 1, + blur: 8, + left: 1, + opacity: 0.2, + enabled: false, + }, + }, + markers: { size: 0 }, + fill: { opacity: [1, 0.8] }, + colors: [radarColors.series1, radarColors.series2], + stroke: { + width: 0, + show: false, + }, + legend: { + labels: { + colors: themeSecondaryTextColor, + }, + markers: { + offsetX: -3, + }, + itemMargin: { + vertical: 3, + horizontal: 10, + }, + }, + plotOptions: { + radar: { + polygons: { + strokeColors: themeBorderColor, + connectorColors: themeBorderColor, + }, + }, + }, + grid: { + show: false, + padding: { + top: -20, + bottom: -20, + }, + }, + yaxis: { show: false }, + xaxis: { + categories: ['Battery', 'Brand', 'Camera', 'Memory', 'Storage', 'Display', 'OS', 'Price'], + labels: { + style: { + colors: [ + themeDisabledTextColor, + themeDisabledTextColor, + themeDisabledTextColor, + themeDisabledTextColor, + themeDisabledTextColor, + themeDisabledTextColor, + themeDisabledTextColor, + themeDisabledTextColor, + ], + }, + }, + }, + } +} diff --git a/resources/js/@core/scss/base/_components.scss b/resources/js/@core/scss/base/_components.scss new file mode 100644 index 0000000..903c8fb --- /dev/null +++ b/resources/js/@core/scss/base/_components.scss @@ -0,0 +1,172 @@ +@use "mixins"; +@use "@layouts/styles/placeholders"; +@use "@layouts/styles/mixins" as layoutMixins; +@use "@configured-variables" as variables; + +// 👉 Avatar group +.v-avatar-group { + display: flex; + align-items: center; + + > * { + &:not(:first-child) { + margin-inline-start: -0.8rem; + } + + transition: transform 0.25s ease, box-shadow 0.15s ease; + + &:hover { + z-index: 2; + transform: translateY(-5px) scale(1.05); + + @include mixins.elevation(3); + } + } + + > .v-avatar { + border: 2px solid rgb(var(--v-theme-surface)); + transition: transform 0.15s ease; + } +} + +// 👉 Button outline with default color border color +.v-alert--variant-outlined, +.v-avatar--variant-outlined, +.v-btn.v-btn--variant-outlined, +.v-card--variant-outlined, +.v-chip--variant-outlined, +.v-list-item--variant-outlined { + &:not([class*="text-"]) { + border-color: rgba(var(--v-border-color), var(--v-border-opacity)); + } + + &.text-default { + border-color: rgba(var(--v-border-color), var(--v-border-opacity)); + } +} + +// 👉 Custom Input +.v-label.custom-input { + padding: 1rem; + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + opacity: 1; + white-space: normal; + + &:hover { + border-color: rgba(var(--v-border-color), 0.25); + } + + &.active { + border-color: rgb(var(--v-theme-primary)); + + .v-icon { + color: rgb(var(--v-theme-primary)) !important; + } + } +} + +// 👉 Datatable +.v-data-table-footer__pagination { + @include layoutMixins.rtl { + .v-btn { + .v-icon { + transform: rotate(180deg); + } + } + } +} + +// Dialog responsive width +.v-dialog { + // dialog custom close btn + .v-dialog-close-btn { + position: absolute; + z-index: 1; + color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important; + inset-block-start: 0.9375rem; + inset-inline-end: 0.9375rem; + + .v-btn__overlay { + display: none; + } + } + + .v-card { + @extend %style-scroll-bar; + } +} + +@media (min-width: 600px) { + .v-dialog { + &.v-dialog-sm, + &.v-dialog-lg, + &.v-dialog-xl { + .v-overlay__content { + inline-size: 565px !important; + } + } + } +} + +@media (min-width: 960px) { + .v-dialog { + &.v-dialog-lg, + &.v-dialog-xl { + .v-overlay__content { + inline-size: 865px !important; + } + } + } +} + +@media (min-width: 1264px) { + .v-dialog.v-dialog-xl { + .v-overlay__content { + inline-size: 1165px !important; + } + } +} + +// v-tab with pill support + +.v-tabs.v-tabs-pill { + .v-tab.v-btn { + border-radius: 0.25rem !important; + transition: none; + + .v-tab__slider { + visibility: hidden; + } + } +} + +// loop for all colors bg +@each $color-name in variables.$theme-colors-name { + .v-tabs.v-tabs-pill { + .v-slide-group-item--active.v-tab--selected.text-#{$color-name} { + background-color: rgb(var(--v-theme-#{$color-name})); + color: rgb(var(--v-theme-on-#{$color-name})) !important; + } + } +} + +// ℹ️ We are make even width of all v-timeline body +.v-timeline--vertical.v-timeline { + .v-timeline-item { + .v-timeline-item__body { + justify-self: stretch !important; + } + } +} + +// 👉 Switch +.v-switch .v-selection-control:not(.v-selection-control--dirty) .v-switch__thumb { + color: #fff !important; +} + +// 👉 Textarea +.v-textarea .v-field__input { + /* stylelint-disable-next-line property-no-vendor-prefix */ + -webkit-mask-image: none !important; + mask-image: none !important; +} diff --git a/resources/js/@core/scss/base/_dark.scss b/resources/js/@core/scss/base/_dark.scss new file mode 100644 index 0000000..6ae246b --- /dev/null +++ b/resources/js/@core/scss/base/_dark.scss @@ -0,0 +1,16 @@ +@use "@configured-variables" as variables; + +// ———————————————————————————————————— +// * ——— Perfect Scrollbar +// ———————————————————————————————————— + +.v-application.v-theme--dark { + .ps__rail-y, + .ps__rail-x { + background-color: transparent !important; + } + + .ps__thumb-y { + background-color: variables.$plugin-ps-thumb-y-dark; + } +} diff --git a/resources/js/@core/scss/base/_default-layout-w-vertical-nav.scss b/resources/js/@core/scss/base/_default-layout-w-vertical-nav.scss new file mode 100644 index 0000000..d415594 --- /dev/null +++ b/resources/js/@core/scss/base/_default-layout-w-vertical-nav.scss @@ -0,0 +1,103 @@ +@use "@configured-variables" as variables; +@use "@core/scss/base/placeholders" as *; +@use "@core/scss/template/placeholders" as *; +@use "misc"; +@use "@core/scss/base/mixins"; + +$header: ".layout-navbar"; + +@if variables.$layout-vertical-nav-navbar-is-contained { + $header: ".layout-navbar .navbar-content-container"; +} + +.layout-wrapper.layout-nav-type-vertical { + // SECTION Layout Navbar + // 👉 Elevated navbar + @if variables.$vertical-nav-navbar-style == "elevated" { + // Add transition + #{$header} { + transition: padding 0.2s ease, background-color 0.18s ease; + } + + // If navbar is contained => Add border radius to header + @if variables.$layout-vertical-nav-navbar-is-contained { + #{$header} { + border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness; + } + } + + // Scrolled styles for sticky navbar + @at-root { + /* ℹ️ This html selector with not selector is required when: + dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and out style broke + */ + html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-sticky, + &.window-scrolled.layout-navbar-sticky { + + #{$header} { + @extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav; + @extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled; + } + + .navbar-blur#{$header} { + @extend %blurry-bg; + } + } + } + } + + // 👉 Floating navbar + @else if variables.$vertical-nav-navbar-style == "floating" { + // ℹ️ Regardless of navbar is contained or not => Apply overlay to .layout-navbar + .layout-navbar { + &.navbar-blur { + @extend %default-layout-vertical-nav-floating-navbar-overlay; + } + } + + &:not(.layout-navbar-sticky) { + #{$header} { + margin-block-start: variables.$vertical-nav-floating-navbar-top; + } + } + + #{$header} { + @if variables.$layout-vertical-nav-navbar-is-contained { + border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness; + } + + background-color: rgb(var(--v-theme-surface)); + + @extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled; + } + + .navbar-blur#{$header} { + @extend %blurry-bg; + } + } + + // !SECTION + + // 👉 Layout footer + .layout-footer { + $ele-layout-footer: &; + + .footer-content-container { + border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness 0 0; + + // Sticky footer + @at-root { + // ℹ️ .layout-footer-sticky#{$ele-layout-footer} => .layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer + .layout-footer-sticky#{$ele-layout-footer} { + .footer-content-container { + background-color: rgb(var(--v-theme-surface)); + padding-block: 0; + padding-inline: 1.2rem; + + @include mixins.elevation(3); + } + } + } + } + } +} diff --git a/resources/js/@core/scss/base/_default-layout.scss b/resources/js/@core/scss/base/_default-layout.scss new file mode 100644 index 0000000..9794e7c --- /dev/null +++ b/resources/js/@core/scss/base/_default-layout.scss @@ -0,0 +1,16 @@ +@use "@core/scss/base/placeholders"; +@use "@core/scss/base/variables"; + +.layout-vertical-nav, +.layout-horizontal-nav { + ol, + ul { + list-style: none; + } +} + +.layout-navbar { + @if variables.$navbar-high-emphasis-text { + @extend %layout-navbar; + } +} diff --git a/resources/js/@core/scss/base/_index.scss b/resources/js/@core/scss/base/_index.scss new file mode 100644 index 0000000..9eb8a9c --- /dev/null +++ b/resources/js/@core/scss/base/_index.scss @@ -0,0 +1,40 @@ +@use "sass:map"; + +// Layout +@use "vertical-nav"; +@use "default-layout"; +@use "default-layout-w-vertical-nav"; + +// Layouts package +@use "layouts"; + +// Components +@use "components"; + +// Utilities +@use "utilities"; + +// Misc +@use "misc"; + +// Dark +@use "dark"; + +// libs +@use "libs/perfect-scrollbar"; + +a { + color: rgb(var(--v-theme-primary)); + text-decoration: none; +} + +// Vuetify 3 don't provide margin bottom style like vuetify 2 +p { + margin-block-end: 1rem; +} + +// Iconify icon size +svg.iconify { + block-size: 1em; + inline-size: 1em; +} diff --git a/resources/js/@core/scss/base/_layouts.scss b/resources/js/@core/scss/base/_layouts.scss new file mode 100644 index 0000000..91635d0 --- /dev/null +++ b/resources/js/@core/scss/base/_layouts.scss @@ -0,0 +1,63 @@ +@use "@configured-variables" as variables; + +/* ℹ️ This styles extends the existing layout package's styles for handling cases that aren't related to layouts package */ + +/* + ℹ️ When we use v-layout as immediate first child of `.page-content-container`, it adds display:flex and page doesn't get contained height +*/ +// .layout-wrapper.layout-nav-type-vertical { +// &.layout-content-height-fixed { +// .page-content-container { +// > .v-layout:first-child > :not(.v-navigation-drawer):first-child { +// flex-grow: 1; +// block-size: 100%; +// } +// } +// } +// } +.layout-wrapper.layout-nav-type-vertical { + &.layout-content-height-fixed { + .page-content-container { + > .v-layout:first-child { + overflow: hidden; + min-block-size: 100%; + + > .v-main { + // overflow-y: auto; + + .v-main__wrap > :first-child { + block-size: 100%; + overflow-y: auto; + } + } + } + } + } +} + +// ℹ️ Let div/v-layout take full height. E.g. Email App +.layout-wrapper.layout-nav-type-horizontal { + &.layout-content-height-fixed { + > .layout-page-content { + display: flex; + } + } +} + +// 👉 Floating navbar styles +@if variables.$vertical-nav-navbar-style == "floating" { + // ℹ️ Add spacing above navbar if navbar is floating (was in %layout-navbar-sticky placeholder) + .layout-wrapper.layout-nav-type-vertical.layout-navbar-sticky { + .layout-navbar { + inset-block-start: variables.$vertical-nav-floating-navbar-top; + } + + /* + ℹ️ If it's floating navbar + Add `vertical-nav-floating-navbar-top` as margin top to .layout-page-content + */ + .layout-page-content { + margin-block-start: variables.$vertical-nav-floating-navbar-top; + } + } +} diff --git a/resources/js/@core/scss/base/_misc.scss b/resources/js/@core/scss/base/_misc.scss new file mode 100644 index 0000000..cee983b --- /dev/null +++ b/resources/js/@core/scss/base/_misc.scss @@ -0,0 +1,20 @@ +// ℹ️ scrollable-content allows creating fixed header and scrollable content for VNavigationDrawer (Used when perfect scrollbar is used) +.scrollable-content { + &.v-navigation-drawer { + .v-navigation-drawer__content { + display: flex; + overflow: hidden; + flex-direction: column; + } + } +} + +// ℹ️ adding styling for code tag +code { + border-radius: 3px; + color: rgb(var(--v-code-color)); + font-size: 90%; + font-weight: 400; + padding-block: 0.2em; + padding-inline: 0.4em; +} diff --git a/resources/js/@core/scss/base/_mixins.scss b/resources/js/@core/scss/base/_mixins.scss new file mode 100644 index 0000000..bc3090c --- /dev/null +++ b/resources/js/@core/scss/base/_mixins.scss @@ -0,0 +1,63 @@ +@use "sass:map"; +@use "@styles/variables/_vuetify.scss"; + +@mixin elevation($z, $important: false) { + box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null); +} + +// #region before-pseudo +// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element +@mixin before-pseudo() { + position: relative; + + &::before { + position: absolute; + border-radius: inherit; + background: currentcolor; + block-size: 100%; + content: ""; + inline-size: 100%; + inset: 0; + opacity: 0; + pointer-events: none; + } +} + +// #endregion before-pseudo + +@mixin bordered-skin($component, $border-property: "border", $important: false) { + #{$component} { + // background-color: rgb(var(--v-theme-background)); + box-shadow: none !important; + #{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null); + } +} + +// #region selected-states +// ℹ️ Inspired from vuetify's active-states mixin +// focus => 0.12 & selected => 0.08 +@mixin selected-states($selector) { + #{$selector} { + opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier)); + } + + &:hover + #{$selector} { + opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier)); + } + + &:focus-visible + #{$selector} { + opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier)); + } + + @supports not selector(:focus-visible) { + &:focus { + #{$selector} { + opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier)); + } + } + } +} + +// #endregion selected-states diff --git a/resources/js/@core/scss/base/_utilities.scss b/resources/js/@core/scss/base/_utilities.scss new file mode 100644 index 0000000..ae2a042 --- /dev/null +++ b/resources/js/@core/scss/base/_utilities.scss @@ -0,0 +1,152 @@ +@use "@configured-variables" as variables; +@use "@layouts/styles/mixins" as layoutsMixins; + +// 👉 Demo spacers +// TODO: Use vuetify SCSS variable here +$card-spacer-content: 16px; + +.demo-space-x { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-block-start: -$card-spacer-content; + + & > * { + margin-block-start: $card-spacer-content; + margin-inline-end: $card-spacer-content; + } +} + +.demo-space-y { + & > * { + margin-block-end: $card-spacer-content; + + &:last-child { + margin-block-end: 0; + } + } +} + +// 👉 Card match height +.match-height.v-row { + .v-card { + block-size: 100%; + } +} + +// 👉 Whitespace +.whitespace-no-wrap { + white-space: nowrap; +} + +// 👉 Colors + +/* + ℹ️ Vuetify is applying `.text-white` class to badge icon but don't provide its styles + Moreover, we also use this class in some places + + ℹ️ In vuetify 2 with `$color-pack: false` SCSS var config this class was getting generated but this is not the case in v3 + + ℹ️ We also need !important to get correct color in badge icon +*/ +.text-white { + color: #fff !important; +} + +.bg-var-theme-background { + background-color: rgba(var(--v-theme-on-background), var(--v-hover-opacity)) !important; +} + +// [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })], +@each $color-name in variables.$theme-colors-name { + .bg-light-#{$color-name} { + background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important; + } +} + +// 👉 clamp text +.clamp-text { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + text-overflow: ellipsis; +} + +.leading-normal { + line-height: normal !important; +} + +// 👉 for rtl only +.flip-in-rtl { + @include layoutsMixins.rtl { + transform: scaleX(-1); + } +} + +// 👉 Carousel +.carousel-delimiter-top-end { + .v-carousel__controls { + justify-content: end; + block-size: 40px; + inset-block-start: 0; + padding-inline: 1rem; + + .v-btn--icon.v-btn--density-default { + block-size: calc(var(--v-btn-height) + -10px); + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + inline-size: calc(var(--v-btn-height) + -8px); + + &.v-btn--active { + color: #fff; + } + + .v-btn__overlay { + opacity: 0; + } + + .v-ripple__container { + display: none; + } + + .v-btn__content { + .v-icon { + block-size: 8px !important; + inline-size: 8px !important; + } + } + } + } + + @each $color-name in variables.$theme-colors-name { + + &.dots-active-#{$color-name} { + .v-carousel__controls { + .v-btn--active { + color: rgb(var(--v-theme-#{$color-name})) !important; + } + } + } + } +} + +.v-timeline-item { + .app-timeline-title { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-size: 16px; + font-weight: 500; + line-height: 1.3125rem; + } + + .app-timeline-meta { + color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)); + font-size: 12px; + line-height: 0.875rem; + } + + .app-timeline-text { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + font-size: 14px; + line-height: 1.25rem; + } +} diff --git a/resources/js/@core/scss/base/_utils.scss b/resources/js/@core/scss/base/_utils.scss new file mode 100644 index 0000000..02acb62 --- /dev/null +++ b/resources/js/@core/scss/base/_utils.scss @@ -0,0 +1,90 @@ +@use "sass:map"; +@use "sass:list"; +@use "@configured-variables" as variables; + +// Thanks: https://css-tricks.com/snippets/sass/deep-getset-maps/ +@function map-deep-get($map, $keys...) { + @each $key in $keys { + $map: map.get($map, $key); + } + + @return $map; +} + +@function map-deep-set($map, $keys, $value) { + $maps: ($map,); + $result: null; + + // If the last key is a map already + // Warn the user we will be overriding it with $value + @if type-of(nth($keys, -1)) == "map" { + @warn "The last key you specified is a map; it will be overrided with `#{$value}`."; + } + + // If $keys is a single key + // Just merge and return + @if length($keys) == 1 { + @return map-merge($map, ($keys: $value)); + } + + // Loop from the first to the second to last key from $keys + // Store the associated map to this key in the $maps list + // If the key doesn't exist, throw an error + @for $i from 1 through length($keys) - 1 { + $current-key: list.nth($keys, $i); + $current-map: list.nth($maps, -1); + $current-get: map.get($current-map, $current-key); + + @if not $current-get { + @error "Key `#{$key}` doesn't exist at current level in map."; + } + + $maps: list.append($maps, $current-get); + } + + // Loop from the last map to the first one + // Merge it with the previous one + @for $i from length($maps) through 1 { + $current-map: list.nth($maps, $i); + $current-key: list.nth($keys, $i); + $current-val: if($i == list.length($maps), $value, $result); + $result: map.map-merge($current-map, ($current-key: $current-val)); + } + + // Return result + @return $result; +} + +// font size utility classes +@each $name, $size in variables.$font-sizes { + .text-#{$name} { + font-size: $size; + line-height: map.get(variables.$font-line-height, $name); + } +} + +// truncate utility class +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// gap utility class +@each $name, $size in variables.$gap { + .gap-#{$name} { + gap: $size; + } + + .gap-x-#{$name} { + column-gap: $size; + } + + .gap-y-#{$name} { + row-gap: $size; + } +} + +.list-none { + list-style-type: none; +} diff --git a/resources/js/@core/scss/base/_variables.scss b/resources/js/@core/scss/base/_variables.scss new file mode 100644 index 0000000..7c97342 --- /dev/null +++ b/resources/js/@core/scss/base/_variables.scss @@ -0,0 +1,197 @@ +@use "vuetify/lib/styles/tools/functions" as *; + +/* + TODO: Add docs on when to use placeholder vs when to use SASS variable + + Placeholder + - When we want to keep customization to our self between templates use it + + Variables + - When we want to allow customization from both user and our side + - You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header) +*/ + +@forward "@layouts/styles/variables" with ( + // Adjust z-index so vertical nav & overlay stays on top of v-layout in v-main. E.g. Email app + $layout-vertical-nav-z-index: 1004, + $layout-overlay-z-index: 1003, +); +@use "@layouts/styles/variables" as *; + +// 👉 Default layout + +$navbar-high-emphasis-text: true !default; + +// @forward "@layouts/styles/variables" with ( +// $layout-vertical-nav-width: 350px !default, +// ); + +$theme-colors-name: ( + "primary", + "secondary", + "error", + "info", + "success", + "warning" +) !default; + +// 👉 Default layout with vertical nav + +$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default; + +// 👉 Vertical nav +$vertical-nav-background-color-rgb: var(--v-theme-background) !default; +$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default; + +// ℹ️ This is used to keep consistency between nav items and nav header left & right margin +// This is used by nav items & nav header +$vertical-nav-horizontal-spacing: 1rem !default; +$vertical-nav-horizontal-padding: 0.75rem !default; + +// Vertical nav header height. Mostly we will align it with navbar height; +$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default; +$vertical-nav-navbar-elevation: 3 !default; +$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating +$vertical-nav-floating-navbar-top: 1rem !default; + +// Vertical nav header padding +$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default; +$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default; + +// Move logo when vertical nav is mini (collapsed but not hovered) +$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default; + +// Space between logo and title +$vertical-nav-header-logo-title-spacing: 0.9rem !default; + +// Section title margin top (when its not first child) +$vertical-nav-section-title-mt: 1.5rem !default; + +// Section title margin bottom +$vertical-nav-section-title-mb: 0.5rem !default; + +// Vertical nav icons +$vertical-nav-items-icon-size: 1.5rem !default; +$vertical-nav-items-nested-icon-size: 0.9rem !default; +$vertical-nav-items-icon-margin-inline-end: 0.5rem !default; + +// Transition duration for nav group arrow +$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default; + +// Timing function for nav group arrow +$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default; + +// 👉 Horizontal nav + +/* + ❗ Heads up + ================== + Here we assume we will always use shorthand property which will apply same padding on four side + This is because this have been used as value of top property by `.popper-content` +*/ +$horizontal-nav-padding: 0.6875rem !default; + +// Gap between top level horizontal nav items +$horizontal-nav-top-level-items-gap: 4px !default; + +// Horizontal nav icons +$horizontal-nav-items-icon-size: 1.5rem !default; +$horizontal-nav-third-level-icon-size: 0.9rem !default; +$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default; + +// ℹ️ We used SCSS variable because we want to allow users to update max height of popper content +// 120px is combined height of navbar & horizontal nav +$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default; + +// ℹ️ This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values. +$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default; + +// 👉 Plugins + +$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default; + +// 👉 Vuetify + +// Used in src/@core/scss/base/libs/vuetify/_overrides.scss +$vuetify-reduce-default-compact-button-icon-size: true !default; + +// 👉 Custom variables +// for utility classes +$font-sizes: () !default; +$font-sizes: map-deep-merge( + ( + "xs": 0.75rem, + "sm": 0.875rem, + "base": 1rem, + "lg": 1.125rem, + "xl": 1.25rem, + "2xl": 1.5rem, + "3xl": 1.875rem, + "4xl": 2.25rem, + "5xl": 3rem, + "6xl": 3.75rem, + "7xl": 4.5rem, + "8xl": 6rem, + "9xl": 8rem + ), + $font-sizes +); + +// line height +$font-line-height: () !default; +$font-line-height: map-deep-merge( + ( + "xs": 1rem, + "sm": 1.25rem, + "base": 1.5rem, + "lg": 1.75rem, + "xl": 1.75rem, + "2xl": 2rem, + "3xl": 2.25rem, + "4xl": 2.5rem, + "5xl": 1, + "6xl": 1, + "7xl": 1, + "8xl": 1, + "9xl": 1 + ), + $font-line-height +); + +// gap utility class +$gap: () !default; +$gap: map-deep-merge( + ( + "0": 0, + "1": 0.25rem, + "2": 0.5rem, + "3": 0.75rem, + "4": 1rem, + "5": 1.25rem, + "6":1.5rem, + "7": 1.75rem, + "8": 2rem, + "9": 2.25rem, + "10": 2.5rem, + "11": 2.75rem, + "12": 3rem, + "14": 3.5rem, + "16": 4rem, + "20": 5rem, + "24": 6rem, + "28": 7rem, + "32": 8rem, + "36": 9rem, + "40": 10rem, + "44": 11rem, + "48": 12rem, + "52": 13rem, + "56": 14rem, + "60": 15rem, + "64": 16rem, + "72": 18rem, + "80": 20rem, + "96": 24rem + ), + $gap +); diff --git a/resources/js/@core/scss/base/_vertical-nav.scss b/resources/js/@core/scss/base/_vertical-nav.scss new file mode 100644 index 0000000..343ce52 --- /dev/null +++ b/resources/js/@core/scss/base/_vertical-nav.scss @@ -0,0 +1,250 @@ +@use "@core/scss/base/placeholders" as *; +@use "@core/scss/template/placeholders" as *; +@use "@layouts/styles/mixins" as layoutsMixins; +@use "@configured-variables" as variables; +@use "@core/scss/base/mixins" as mixins; +@use "vuetify/lib/styles/tools/states" as vuetifyStates; + +.layout-nav-type-vertical { + // 👉 Layout Vertical nav + .layout-vertical-nav { + $sl-layout-nav-type-vertical: &; + + @extend %nav; + + @at-root { + // ℹ️ Add styles for collapsed vertical nav + .layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}.hovered { + @include mixins.elevation(6); + } + } + + background-color: variables.$vertical-nav-background-color; + + // 👉 Nav header + .nav-header { + overflow: hidden; + padding: variables.$vertical-nav-header-padding; + margin-inline: variables.$vertical-nav-header-inline-spacing; + min-block-size: variables.$vertical-nav-header-height; + + // TEMPLATE: Check if we need to move this to master + .app-logo { + flex-shrink: 0; + transition: transform 0.25s ease-in-out; + + @at-root { + // Move logo a bit to align center with the icons in vertical nav mini variant + .layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}:not(.hovered) .nav-header .app-logo { + transform: translateX(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini); + + @include layoutsMixins.rtl { + transform: translateX(-(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini)); + } + } + } + } + + .app-title { + margin-inline-start: variables.$vertical-nav-header-logo-title-spacing; + } + + .header-action { + @extend %nav-header-action; + } + } + + // 👉 Nav items shadow + .vertical-nav-items-shadow { + position: absolute; + z-index: 1; + background: + linear-gradient( + rgb(#{variables.$vertical-nav-background-color-rgb}) 5%, + rgba(#{variables.$vertical-nav-background-color-rgb}, 75%) 45%, + rgba(#{variables.$vertical-nav-background-color-rgb}, 20%) 80%, + transparent + ); + block-size: 55px; + inline-size: 100%; + inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px); + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease-in-out; + will-change: opacity; + + @include layoutsMixins.rtl { + transform: translateX(8px); + } + } + + &.scrolled { + .vertical-nav-items-shadow { + opacity: 1; + } + } + + .ps__rail-y { + // ℹ️ Setting z-index: 1 will make perfect scrollbar thumb appear on top of vertical nav items shadow + z-index: 1; + } + + // 👉 Nav section title + .nav-section-title { + @extend %vertical-nav-item; + @extend %vertical-nav-section-title; + + margin-block-end: variables.$vertical-nav-section-title-mb; + + &:not(:first-child) { + margin-block-start: variables.$vertical-nav-section-title-mt; + } + + .placeholder-icon { + margin-inline: auto; + } + } + + // Nav item badge + .nav-item-badge { + @extend %vertical-nav-item-badge; + } + + // 👉 Nav group & Link + .nav-link, + .nav-group { + overflow: hidden; + + > :first-child { + @extend %vertical-nav-item; + @extend %vertical-nav-item-interactive; + } + + .nav-item-icon { + @extend %vertical-nav-items-icon; + } + + &.disabled { + opacity: var(--v-disabled-opacity); + pointer-events: none; + } + } + + // 👉 Vertical nav link + .nav-link { + @extend %nav-link; + + > .router-link-exact-active { + @extend %nav-link-active; + } + + > a { + // Adds before psudo element to style hover state + @include mixins.before-pseudo; + + // Adds vuetify states + @include vuetifyStates.states($active: false); + } + } + + // 👉 Vertical nav group + .nav-group { + // Reduce the size of icon if link/group is inside group + .nav-group, + .nav-link { + .nav-item-icon { + @extend %vertical-nav-items-nested-icon; + } + } + + // Hide icons after 2nd level + & .nav-group { + .nav-link, + .nav-group { + .nav-item-icon { + @extend %vertical-nav-items-icon-after-2nd-level; + } + } + } + + .nav-group-arrow { + flex-shrink: 0; + transform-origin: center; + transition: transform variables.$vertical-nav-nav-group-arrow-transition-duration variables.$vertical-nav-nav-group-arrow-transition-timing-function; + will-change: transform; + } + + // Rotate arrow icon if group is opened + &.open { + > .nav-group-label .nav-group-arrow { + transform: rotateZ(90deg); + } + } + + // Nav group label + > :first-child { + // Adds before psudo element to style hover state + @include mixins.before-pseudo; + + // Adds vuetify states + @include vuetifyStates.states($active: false); + } + + // Active & open states for nav group label + &.active, + &.open { + > :first-child { + @extend %vertical-nav-group-open-active; + } + } + } + } +} + +// SECTION: Transitions +.vertical-nav-section-title-enter-active, +.vertical-nav-section-title-leave-active { + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; +} + +.vertical-nav-section-title-enter-from, +.vertical-nav-section-title-leave-to { + opacity: 0; + transform: translateX(15px); + + @include layoutsMixins.rtl { + transform: translateX(-15px); + } +} + +.transition-slide-x-enter-active, +.transition-slide-x-leave-active { + transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out; +} + +.transition-slide-x-enter-from, +.transition-slide-x-leave-to { + opacity: 0; + transform: translateX(-15px); + + @include layoutsMixins.rtl { + transform: translateX(15px); + } +} + +.vertical-nav-app-title-enter-active, +.vertical-nav-app-title-leave-active { + transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out; +} + +.vertical-nav-app-title-enter-from, +.vertical-nav-app-title-leave-to { + opacity: 0; + transform: translateX(-15px); + + @include layoutsMixins.rtl { + transform: translateX(15px); + } +} + +// !SECTION diff --git a/resources/js/@core/scss/base/libs/_perfect-scrollbar.scss b/resources/js/@core/scss/base/libs/_perfect-scrollbar.scss new file mode 100644 index 0000000..ceb2d7a --- /dev/null +++ b/resources/js/@core/scss/base/libs/_perfect-scrollbar.scss @@ -0,0 +1,35 @@ +$ps-size: 0.25rem; +$ps-hover-size: 0.375rem; +$ps-track-size: 0.5rem; + +.ps__thumb-y { + inline-size: $ps-size; + inset-inline-end: 0.0625rem; +} + +.ps__thumb-x { + block-size: $ps-size !important; +} + +.ps__rail-x { + background: transparent !important; + block-size: $ps-track-size; +} + +.ps__rail-y { + background: transparent !important; + inline-size: $ps-track-size !important; + inset-inline-end: 0.125rem !important; + inset-inline-start: unset !important; +} + +.ps__rail-y.ps--clicking .ps__thumb-y, +.ps__rail-y:focus > .ps__thumb-y, +.ps__rail-y:hover > .ps__thumb-y { + inline-size: $ps-hover-size; +} + +.ps__thumb-x, +.ps__thumb-y { + background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important; +} diff --git a/resources/js/@core/scss/base/libs/vuetify/_index.scss b/resources/js/@core/scss/base/libs/vuetify/_index.scss new file mode 100644 index 0000000..f33ef3f --- /dev/null +++ b/resources/js/@core/scss/base/libs/vuetify/_index.scss @@ -0,0 +1 @@ +@use "overrides"; diff --git a/resources/js/@core/scss/base/libs/vuetify/_overrides.scss b/resources/js/@core/scss/base/libs/vuetify/_overrides.scss new file mode 100644 index 0000000..2484b56 --- /dev/null +++ b/resources/js/@core/scss/base/libs/vuetify/_overrides.scss @@ -0,0 +1,287 @@ +@use "@core/scss/base/utils"; +@use "@configured-variables" as variables; + +// 👉 Application +// ℹ️ We need accurate vh in mobile devices as well +.v-application__wrap { + /* stylelint-disable-next-line liberty/use-logical-spec */ + min-height: calc(var(--vh, 1vh) * 100); +} + +// 👉 Typography +h1, +h2, +h3, +h4, +h5, +h6, +.text-h1, +.text-h2, +.text-h3, +.text-h4, +.text-h5, +.text-h6, +.text-button, +.text-overline, +.v-card-title { + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); +} + +.v-application, +.text-body-1, +.text-body-2, +.text-subtitle-1, +.text-subtitle-2 { + color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity)); +} + +// 👉 Grid +// Remove margin-bottom of v-input_details inside grid (validation error message) +.v-row { + .v-col, + [class^="v-col-*"] { + .v-input__details { + margin-block-end: 0; + } + } +} + +// 👉 Button +@if variables.$vuetify-reduce-default-compact-button-icon-size { + .v-btn--density-compact.v-btn--size-default { + .v-btn__content > svg { + block-size: 22px; + font-size: 22px; + inline-size: 22px; + } + } +} + +// 👉 Card +// Removes padding-top for immediately placed v-card-text after itself +.v-card-text { + & + & { + padding-block-start: 0 !important; + } +} + +/* + 👉 Checkbox & Radio Ripple + + TODO Checkbox and switch component. Remove it when vuetify resolve the extra spacing: https://github.com/vuetifyjs/vuetify/issues/15519 + We need this because form elements likes checkbox and switches are by default set to height of textfield height which is way big than we want + Tested with checkbox & switches +*/ +.v-checkbox.v-input, +.v-switch.v-input { + --v-input-control-height: auto; + + flex: unset; +} + +.v-selection-control--density-comfortable { + &.v-checkbox-btn, + &.v-radio, + &.v-radio-btn { + .v-selection-control__wrapper { + margin-inline-start: -0.5625rem; + } + } +} + +.v-selection-control--density-compact { + &.v-radio, + &.v-radio-btn, + &.v-checkbox-btn { + .v-selection-control__wrapper { + margin-inline-start: -0.3125rem; + } + } +} + +.v-selection-control--density-default { + &.v-checkbox-btn, + &.v-radio, + &.v-radio-btn { + .v-selection-control__wrapper { + margin-inline-start: -0.6875rem; + } + } +} + +.v-radio-group { + .v-selection-control-group { + .v-radio:not(:last-child) { + margin-inline-end: 0.9rem; + } + } +} + +/* + 👉 Tabs + Disable tab transition + + This is for tabs where we don't have card wrapper to tabs and have multiple cards as tab content. + + This class will disable transition and adds `overflow: unset` on `VWindow` to allow spreading shadow +*/ +.disable-tab-transition { + overflow: unset !important; + + .v-window__container { + block-size: auto !important; + } + + .v-window-item:not(.v-window-item--active) { + display: none !important; + } + + .v-window__container .v-window-item { + transform: none !important; + } +} + +// 👉 List +.v-list { + // Set icons opacity to .87 + .v-list-item__prepend > .v-icon, + .v-list-item__append > .v-icon { + opacity: var(--v-high-emphasis-opacity); + } +} + +// 👉 Card list + +/* + ℹ️ Custom class + + Remove list spacing inside card + + This is because card title gets padding of 20px and list item have padding of 16px. Moreover, list container have padding-bottom as well. +*/ +.card-list { + --v-card-list-gap: 20px; + + &.v-list { + padding-block: 0; + } + + .v-list-item { + min-block-size: unset; + min-block-size: auto !important; + padding-block: 0 !important; + padding-inline: 0 !important; + + > .v-ripple__container { + opacity: 0; + } + + &:not(:last-child) { + padding-block-end: var(--v-card-list-gap) !important; + } + } + + .v-list-item:hover, + .v-list-item:focus, + .v-list-item:active, + .v-list-item.active { + > .v-list-item__overlay { + opacity: 0 !important; + } + } +} + +// 👉 Divider +.v-divider { + color: rgb(var(--v-border-color)); +} + +// 👉 DataTable +.v-data-table { + /* stylelint-disable-next-line no-descending-specificity */ + .v-checkbox-btn .v-selection-control__wrapper { + margin-inline-start: 0 !important; + } + + .v-selection-control { + display: flex !important; + } + + .v-pagination { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + } +} + +.v-data-table-footer { + margin-block-start: 1rem; +} + +// 👉 v-field +.v-field:hover .v-field__outline { + --v-field-border-opacity: var(--v-medium-emphasis-opacity); +} + +// 👉 VLabel +.v-label { + opacity: 1 !important; + + &:not(.v-field-label--floating) { + color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity)); + } +} + +// 👉 Overlay +.v-overlay__scrim, +.v-navigation-drawer__scrim { + background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity)) !important; + opacity: 1 !important; +} + +// 👉 VMessages +.v-messages { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + opacity: 1; +} + +// 👉 Alert close btn +.v-alert__close { + .v-btn--icon .v-icon { + --v-icon-size-multiplier: 1.5; + } +} + +// 👉 Badge icon alignment +.v-badge__badge { + display: flex; + align-items: center; +} + +// 👉 Btn focus outline style removed +.v-btn:focus-visible::after { + opacity: 0 !important; +} + +// .v-select chip spacing for slot +.v-input:not(.v-select--chips) .v-select__selection { + .v-chip { + margin-block: 2px var(--select-chips-margin-bottom); + } +} + +// 👉 VCard and VList subtitle color +.v-card-subtitle, +.v-list-item-subtitle { + color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity)); +} + +// 👉 placeholders +.v-field__input { + @at-root { + & input::placeholder, + input#{&}::placeholder, + textarea#{&}::placeholder { + color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important; + opacity: 1 !important; + } + } +} diff --git a/resources/js/@core/scss/base/libs/vuetify/_variables.scss b/resources/js/@core/scss/base/libs/vuetify/_variables.scss new file mode 100644 index 0000000..9b955d0 --- /dev/null +++ b/resources/js/@core/scss/base/libs/vuetify/_variables.scss @@ -0,0 +1,49 @@ +// 👉 Shadow opacities +$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity); +$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity); +$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity); + +// 👉 Card transition properties +$card-transition-property-custom: box-shadow, opacity; + +@forward "vuetify/settings" with ( + // 👉 General settings + $color-pack: false !default, + + // 👉 Shadow opacity + $shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default, + $shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default, + $shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default, + + // 👉 Card + $card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default, + $card-elevation: 6 !default, + $card-title-line-height: 1.6 !default, + $card-actions-min-height: unset !default, + $card-text-padding: 1.25rem !default, + $card-item-padding: 1.25rem !default, + $card-actions-padding: 0 12px 12px !default, + $card-transition-property: $card-transition-property-custom !default, + $card-subtitle-opacity: 1 !default, + + // 👉 Expansion Panel + $expansion-panel-active-title-min-height: 48px !default, + + // 👉 List + $list-item-icon-margin-end: 16px !default, + $list-item-icon-margin-start: 16px !default, + $list-item-subtitle-opacity: 1 !default, + + // 👉 Tooltip + $tooltip-background-color: rgba(59, 55, 68, 0.9) !default, + $tooltip-text-color: rgb(var(--v-theme-on-primary)) !default, + $tooltip-font-size: 0.75rem !default, + + $button-icon-density: ("default": 2, "comfortable": 0, "compact": -1 ) !default, + + // 👉 VTimeline + $timeline-dot-size: 34px !default, + + // 👉 VOverlay + $overlay-opacity: 1 !default, +); diff --git a/resources/js/@core/scss/base/placeholders/_default-layout-vertical-nav.scss b/resources/js/@core/scss/base/placeholders/_default-layout-vertical-nav.scss new file mode 100644 index 0000000..8bcea6e --- /dev/null +++ b/resources/js/@core/scss/base/placeholders/_default-layout-vertical-nav.scss @@ -0,0 +1,46 @@ +@use "@configured-variables" as variables; +@use "misc"; +@use "@core/scss/base/mixins"; + +%default-layout-vertical-nav-scrolled-sticky-elevated-nav { + background-color: rgb(var(--v-theme-surface)); +} + +%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled { + @include mixins.elevation(variables.$vertical-nav-navbar-elevation); + + // If navbar is contained => Squeeze navbar content on scroll + @if variables.$layout-vertical-nav-navbar-is-contained { + padding-inline: 1.2rem; + } +} + +%default-layout-vertical-nav-floating-navbar-overlay { + isolation: isolate; + + &::after { + position: absolute; + z-index: -1; + /* stylelint-disable property-no-vendor-prefix */ + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + /* stylelint-enable */ + background: + linear-gradient( + 180deg, + rgba(var(--v-theme-background), 70%) 44%, + rgba(var(--v-theme-background), 43%) 73%, + rgba(var(--v-theme-background), 0%) + ); + background-repeat: repeat; + block-size: calc(variables.$layout-vertical-nav-navbar-height + variables.$vertical-nav-floating-navbar-top + 0.5rem); + content: ""; + inset-block-start: -(variables.$vertical-nav-floating-navbar-top); + inset-inline-end: 0; + inset-inline-start: 0; + /* stylelint-disable property-no-vendor-prefix */ + -webkit-mask: linear-gradient(black, black 18%, transparent 100%); + mask: linear-gradient(black, black 18%, transparent 100%); + /* stylelint-enable */ + } +} diff --git a/resources/js/@core/scss/base/placeholders/_default-layout.scss b/resources/js/@core/scss/base/placeholders/_default-layout.scss new file mode 100644 index 0000000..8e5e990 --- /dev/null +++ b/resources/js/@core/scss/base/placeholders/_default-layout.scss @@ -0,0 +1,3 @@ +%layout-navbar { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); +} diff --git a/resources/js/@core/scss/base/placeholders/_index.scss b/resources/js/@core/scss/base/placeholders/_index.scss new file mode 100644 index 0000000..c59061a --- /dev/null +++ b/resources/js/@core/scss/base/placeholders/_index.scss @@ -0,0 +1,5 @@ +@forward "vertical-nav"; +@forward "nav"; +@forward "default-layout"; +@forward "default-layout-vertical-nav"; +@forward "misc"; diff --git a/resources/js/@core/scss/base/placeholders/_misc.scss b/resources/js/@core/scss/base/placeholders/_misc.scss new file mode 100644 index 0000000..87a3ed6 --- /dev/null +++ b/resources/js/@core/scss/base/placeholders/_misc.scss @@ -0,0 +1,7 @@ +%blurry-bg { + /* stylelint-disable property-no-vendor-prefix */ + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + /* stylelint-enable */ + background-color: rgb(var(--v-theme-surface), 0.9); +} diff --git a/resources/js/@core/scss/base/placeholders/_nav.scss b/resources/js/@core/scss/base/placeholders/_nav.scss new file mode 100644 index 0000000..655535a --- /dev/null +++ b/resources/js/@core/scss/base/placeholders/_nav.scss @@ -0,0 +1,33 @@ +@use "@core/scss/base/mixins"; + +// ℹ️ This is common style that needs to be applied to both navs +%nav { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + + .nav-item-title { + letter-spacing: 0.15px; + } + + .nav-section-title { + letter-spacing: 0.4px; + } +} + +/* + Active nav link styles for horizontal & vertical nav + + For horizontal nav it will be only applied to top level nav items + For vertical nav it will be only applied to nav links (not nav groups) +*/ +%nav-link-active { + background-color: rgb(var(--v-theme-primary)); + color: rgb(var(--v-theme-on-primary)); + + @include mixins.elevation(3); +} + +%nav-link { + a { + color: inherit; + } +} diff --git a/resources/js/@core/scss/base/placeholders/_vertical-nav.scss b/resources/js/@core/scss/base/placeholders/_vertical-nav.scss new file mode 100644 index 0000000..bf73e3e --- /dev/null +++ b/resources/js/@core/scss/base/placeholders/_vertical-nav.scss @@ -0,0 +1,81 @@ +@use "@core/scss/base/mixins"; +@use "@configured-variables" as variables; +@use "vuetify/lib/styles/tools/states" as vuetifyStates; + +%nav-header-action { + font-size: 1.25rem; +} + +// Nav items styles (including section title) +%vertical-nav-item { + margin-block: 0; + margin-inline: variables.$vertical-nav-horizontal-spacing; + padding-block: 0; + padding-inline: variables.$vertical-nav-horizontal-padding; + white-space: nowrap; +} + +// This is same as `%vertical-nav-item` except section title is excluded +%vertical-nav-item-interactive { + border-radius: 0.4rem; + block-size: 2.75rem; + + /* + ℹ️ We will use `margin-block-end` instead of `margin-block` to give more space for shadow to appear. + With `margin-block`, due to small space (space gets divided between top & bottom) shadow cuts + */ + margin-block-end: 0.375rem; +} + +// Common styles for nav item icon styles +// ℹ️ Nav group's children icon styles are not here (Adjusts height, width & margin) +%vertical-nav-items-icon { + flex-shrink: 0; + font-size: variables.$vertical-nav-items-icon-size; + margin-inline-end: variables.$vertical-nav-items-icon-margin-inline-end; +} + +// ℹ️ Icon styling for icon nested inside another nav item (2nd level) +%vertical-nav-items-nested-icon { + /* + ℹ️ `margin-inline` will be (normal icon font-size - small icon font-size) / 2 + (1.5rem - 0.9rem) / 2 => 0.6rem / 2 => 0.3rem + */ + $vertical-nav-items-nested-icon-margin-inline: calc((variables.$vertical-nav-items-icon-size - variables.$vertical-nav-items-nested-icon-size) / 2); + + font-size: variables.$vertical-nav-items-nested-icon-size; + margin-inline-end: $vertical-nav-items-nested-icon-margin-inline + variables.$vertical-nav-items-icon-margin-inline-end; + margin-inline-start: $vertical-nav-items-nested-icon-margin-inline; +} + +%vertical-nav-items-icon-after-2nd-level { + visibility: hidden; +} + +// Open & Active nav group styles +%vertical-nav-group-open-active { + @include mixins.selected-states("&::before"); +} + +// Section title +%vertical-nav-section-title { + // ℹ️ Setting height will prevent jerking when text & icon is toggled + block-size: 1.5rem; + color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)); + font-size: 0.75rem; + text-transform: uppercase; +} + +// Vertical nav item badge styles +%vertical-nav-item-badge { + display: inline-block; + border-radius: 1.5rem; + font-size: 0.8em; + font-weight: 500; + line-height: 1; + padding-block: 0.25em; + padding-inline: 0.55em; + text-align: center; + vertical-align: baseline; + white-space: nowrap; +} diff --git a/resources/js/@core/scss/template/_components.scss b/resources/js/@core/scss/template/_components.scss new file mode 100644 index 0000000..75e4a13 --- /dev/null +++ b/resources/js/@core/scss/template/_components.scss @@ -0,0 +1,69 @@ +@use "@configured-variables" as variables; +@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation; + +// 👉 VExpansionPanel +.v-expansion-panel-title, +.v-expansion-panel-title--active, +.v-expansion-panel-title:hover, +.v-expansion-panel-title:focus, +.v-expansion-panel-title:focus-visible, +.v-expansion-panel-title--active:focus, +.v-expansion-panel-title--active:hover { + .v-expansion-panel-title__overlay { + opacity: 0 !important; + } +} + +// 👉 Set Elevation +.v-expansion-panels { + .v-expansion-panel { + .v-expansion-panel__shadow { + @include mixins_elevation.elevation(3); + } + } + + .v-expansion-panel-text__wrapper { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important; + font-size: 1rem; + } +} + +// 👉 Timeline outlined variant +.v-timeline-item { + .v-timeline-divider__dot { + .v-timeline-divider__inner-dot { + box-shadow: 0 0 0 0.1875rem rgb(var(--v-theme-on-surface-variant)); + + @each $color-name in variables.$theme-colors-name { + + &.bg-#{$color-name} { + box-shadow: 0 0 0 0.1875rem rgba(var(--v-theme-#{$color-name}), 0.12); + } + } + } + } +} + +// 👉 Timeline Outlined style +.v-timeline-variant-outlined.v-timeline { + .v-timeline-divider__dot { + .v-timeline-divider__inner-dot { + box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-on-surface-variant)); + + @each $color-name in variables.$theme-colors-name { + background-color: rgb(var(--v-theme-surface)) !important; + + &.bg-#{$color-name} { + box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-#{$color-name})); + } + } + } + } +} + +// 👉 v-tab with pill support +.v-tabs.v-tabs-pill { + .v-tab.v-btn { + border-radius: 0.375rem !important; + } +} diff --git a/resources/js/@core/scss/template/_dark.scss b/resources/js/@core/scss/template/_dark.scss new file mode 100644 index 0000000..89d050d --- /dev/null +++ b/resources/js/@core/scss/template/_dark.scss @@ -0,0 +1,37 @@ +.v-application { + // vertical nav + &.v-theme--dark .layout-nav-type-vertical, + .v-theme-provider.v-theme--dark { + .layout-vertical-nav { + // nav-link and nav-group style for dark + .nav-link .router-link-exact-active, + .nav-group.active:not(.nav-group .nav-group) > :first-child { + background-color: rgb(var(--v-theme-primary)) !important; + color: rgb(var(--v-theme-on-primary)) !important; + + &::before { + z-index: -1; + color: rgb(var(--v-global-theme-primary)); + opacity: 1 !important; + } + } + + .nav-group { + .nav-link { + .router-link-exact-active { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important; + + &::before { + color: transparent; + } + + &:hover::before { + color: inherit; + opacity: var(--v-hover-opacity) !important; + } + } + } + } + } + } +} diff --git a/resources/js/@core/scss/template/_default-layout-w-vertical-nav.scss b/resources/js/@core/scss/template/_default-layout-w-vertical-nav.scss new file mode 100644 index 0000000..83b0005 --- /dev/null +++ b/resources/js/@core/scss/template/_default-layout-w-vertical-nav.scss @@ -0,0 +1,20 @@ +@use "vuetify/lib/styles/tools/elevation" as elevation; + +.layout-wrapper.layout-nav-type-vertical { + // 👉 Layout footer + .layout-footer { + $ele-layout-footer: &; + + .footer-content-container { + // Sticky footer + @at-root { + // ℹ️ .layout-footer-sticky#{$ele-layout-footer} => .layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer + .layout-footer-sticky#{$ele-layout-footer} { + .footer-content-container { + @include elevation.elevation(4); + } + } + } + } + } +} diff --git a/resources/js/@core/scss/template/_utils.scss b/resources/js/@core/scss/template/_utils.scss new file mode 100644 index 0000000..2969c21 --- /dev/null +++ b/resources/js/@core/scss/template/_utils.scss @@ -0,0 +1,41 @@ +@use "sass:string"; + +/* + ℹ️ This function is helpful when we have multi dimensional value + + Assume we have padding variable `$nav-padding-horizontal: 10px;` + With above variable let's say we use it in some style: + ```scss + .selector { + margin-left: $nav-padding-horizontal; + } + ``` + + Now, problem is we can also have value as `$nav-padding-horizontal: 10px 15px;` + In this case above style will be invalid. + + This function will extract the left most value from the variable value. + + $nav-padding-horizontal: 10px; => 10px; + $nav-padding-horizontal: 10px 15px; => 10px; + + This is safe: + ```scss + .selector { + margin-left: get-first-value($nav-padding-horizontal); + } + ``` +*/ +@function get-first-value($var) { + $start-at: string.index(#{$var}, " "); + + @if $start-at { + @return string.slice( + #{$var}, + 0, + $start-at + ); + } @else { + @return $var; + } +} diff --git a/resources/js/@core/scss/template/_variables.scss b/resources/js/@core/scss/template/_variables.scss new file mode 100644 index 0000000..a10f629 --- /dev/null +++ b/resources/js/@core/scss/template/_variables.scss @@ -0,0 +1,57 @@ +@use "sass:map"; +@use "utils"; + +$vertical-nav-horizontal-padding-margin-custom: 1.91rem; + +// ℹ️ We created this SCSS var to extract the start padding +// Docs: https://sass-lang.com/documentation/modules/string +// $vertical-nav-horizontal-padding => 0 8px; +// string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2 +// string.index(#{$vertical-nav-horizontal-padding}, " ") => 1 +// string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x + +$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-margin-custom) !default; + +@forward "@core/scss/base/variables" with ( + // 👉 Default layout with vertical nav + $default-layout-with-vertical-nav-navbar-footer-roundness: 8px !default, + + // 👉 Vertical nav + $layout-vertical-nav-collapsed-width: 84px !default, + $vertical-nav-background-color-rgb: var(--v-theme-surface) !default, + $vertical-nav-items-nested-icon-size: 0.5rem !default, + $vertical-nav-horizontal-padding: 0.9375rem 0.625rem !default, + $vertical-nav-header-inline-spacing: 0 !default, + $vertical-nav-header-padding: 1rem 2.2rem !default, + + // Section title margin top (when its not first child) + $vertical-nav-section-title-mt: 1.4rem !default, + + // Section title margin bottom + $vertical-nav-section-title-mb: 0.65rem !default, + + // Vertical nav icons + $vertical-nav-items-icon-size: 1.375rem !default, + $vertical-nav-navbar-style: "floating" !default, // options: elevated, floating + $vertical-nav-floating-navbar-top: 0.75rem !default, + $vertical-nav-items-icon-margin-inline-end: 0.625rem !default, + + // 👉 Horizontal nav + /* + ❗ Heads up + ================== + Here we assume we will always use shorthand property which will apply same padding on four side + This is because this have been used as value of top property by `.popper-content` + */ + $horizontal-nav-padding: 0.625rem !default, + + // Horizontal nav icons + $horizontal-nav-items-icon-size: 1.375rem !default, + $horizontal-nav-third-level-icon-size: 0.5rem !default, + $horizontal-nav-items-icon-margin-inline-end: 0.5rem !default, +); + +$slider-thumb-label-color: rgb(117, 117, 117) !default; + +// vertical nav header +$vertical-nav-header-margin-top: 0.75rem !default; diff --git a/resources/js/@core/scss/template/_vertical-nav.scss b/resources/js/@core/scss/template/_vertical-nav.scss new file mode 100644 index 0000000..0c2088e --- /dev/null +++ b/resources/js/@core/scss/template/_vertical-nav.scss @@ -0,0 +1,103 @@ +@use "@core/scss/template/placeholders" as *; +@use "vuetify/lib/styles/tools/elevation" as elevation; +@use "@configured-variables" as variables; + +$divider-gap: 0.75rem; + +// vertical nav app title +.layout-nav-type-vertical { + .layout-vertical-nav { + @include elevation.elevation(3); + + // 👉 Nav header + .nav-header { + margin-block-start: variables.$vertical-nav-header-margin-top; + + .app-title-wrapper { + h1 { + font-size: 28px; + } + } + } + + .nav-items { + padding-block-start: 0.25rem; + + // ℹ️ Reduce with width of the thumb in vertical nav menu so we can clearly see active indicator + .ps__thumb-y { + inline-size: 0.125rem; + } + + .ps__rail-y.ps--clicking .ps__thumb-y, + .ps__rail-y:focus > .ps__thumb-y, + .ps__rail-y:hover > .ps__thumb-y { + inline-size: 0.375rem; + } + } + + // nav-section-title's line + .title-text { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + column-gap: $divider-gap; + + &::before { + flex: 0 1 calc(variables.$vertical-nav-horizontal-padding-start - $divider-gap); + border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + content: ""; + margin-inline-start: -#{variables.$vertical-nav-horizontal-padding-start}; + } + } + + // Active status indicator + .nav-link .router-link-exact-active, + .nav-group.active:not(.nav-group .nav-group) > :first-child { + &::after { + position: absolute; + background-color: rgb(var(--v-global-theme-primary)); + block-size: 2.625rem; + border-end-start-radius: 0.375rem; + border-start-start-radius: 0.375rem; + content: ""; + inline-size: 0.25rem; + inset-inline-end: - variables.$vertical-nav-horizontal-spacing; + } + } + + // 👉 Vertical nav link + .nav-group { + .nav-link { + > .router-link-exact-active { + @extend %nav-link-nested-active; + + // active status indicator removed + &::after { + content: none; + } + } + } + + // Active & open states for nav group label + &.open:not(.active), + .nav-group.active { + > :first-child { + &.nav-group-label { + svg, + .nav-item-title { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + } + } + } + } + + // nav-group active + &.active:not(.nav-group .nav-group) { + > :first-child { + @extend %vertical-nav-group-active; + } + } + } + } +} diff --git a/resources/js/@core/scss/template/index.scss b/resources/js/@core/scss/template/index.scss new file mode 100644 index 0000000..ff01318 --- /dev/null +++ b/resources/js/@core/scss/template/index.scss @@ -0,0 +1,11 @@ +@use "@core/scss/base"; + +// Layout +@use "vertical-nav"; +@use "default-layout-w-vertical-nav"; + +// Components +@use "components"; + +// Dark +@use "dark"; diff --git a/resources/js/@core/scss/template/libs/apex-chart.scss b/resources/js/@core/scss/template/libs/apex-chart.scss new file mode 100644 index 0000000..bfc3679 --- /dev/null +++ b/resources/js/@core/scss/template/libs/apex-chart.scss @@ -0,0 +1,95 @@ +@use "@styles/variables/_vuetify.scss" as vuetify; +@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation; +@use "@layouts/styles/mixins" as layoutsMixins; + +.v-application .apexcharts-canvas { + &line[stroke="transparent"] { + display: "none"; + } + + .apexcharts-tooltip { + @include mixins_elevation.elevation(3); + + border-color: rgba(var(--v-border-color), var(--v-border-opacity)); + background: rgb(var(--v-theme-surface)); + + .apexcharts-tooltip-title { + border-color: rgba(var(--v-border-color), var(--v-border-opacity)); + background: rgb(var(--v-theme-surface)); + font-weight: 600; + } + + &.apexcharts-theme-light { + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); + } + + &.apexcharts-theme-dark { + color: white; + } + + .apexcharts-tooltip-series-group:first-of-type { + padding-block-end: 0; + } + } + + .apexcharts-xaxistooltip { + border-color: rgba(var(--v-border-color), var(--v-border-opacity)); + background: rgb(var(--v-theme-grey-50)); + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); + + &::after { + border-block-end-color: rgb(var(--v-theme-grey-50)); + } + + &::before { + border-block-end-color: rgba(var(--v-border-color), var(--v-border-opacity)); + } + } + + .apexcharts-yaxistooltip { + border-color: rgba(var(--v-border-color), var(--v-border-opacity)); + background: rgb(var(--v-theme-grey-50)); + + &::after { + border-inline-start-color: rgb(var(--v-theme-grey-50)); + } + + &::before { + border-inline-start-color: rgba(var(--v-border-color), var(--v-border-opacity)); + } + } + + .apexcharts-xaxistooltip-text, + .apexcharts-yaxistooltip-text { + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); + } + + .apexcharts-yaxis .apexcharts-yaxis-texts-g .apexcharts-yaxis-label { + @include layoutsMixins.rtl { + text-anchor: start; + } + } + + .apexcharts-text, + .apexcharts-tooltip-text, + .apexcharts-datalabel-label, + .apexcharts-datalabel, + .apexcharts-xaxistooltip-text, + .apexcharts-yaxistooltip-text, + .apexcharts-legend-text { + font-family: vuetify.$body-font-family !important; + } + + .apexcharts-pie-label { + fill: white; + filter: none; + } + + .apexcharts-marker { + box-shadow: none; + } + + .apexcharts-legend-marker { + margin-inline-end: 0.3875rem !important; + } +} diff --git a/resources/js/@core/scss/template/libs/full-calendar.scss b/resources/js/@core/scss/template/libs/full-calendar.scss new file mode 100644 index 0000000..e8a48f4 --- /dev/null +++ b/resources/js/@core/scss/template/libs/full-calendar.scss @@ -0,0 +1,267 @@ +@use "@core/scss/base/mixins"; + +.v-application .fc { + --fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04); + --fc-border-color: rgba(var(--v-border-color), var(--v-border-opacity)); + --fc-neutral-bg-color: rgb(var(--v-theme-background)); + --fc-list-event-hover-bg-color: rgba(var(--v-theme-on-surface), 0.02); + --fc-page-bg-color: rgb(var(--v-theme-surface)); + --fc-event-border-color: currentcolor; + + a { + color: inherit; + } + + .fc-timegrid-divider { + padding: 0; + } + + .fc-col-header-cell-cushion { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-size: 0.875rem; + font-weight: 600; + } + + .fc-toolbar .fc-toolbar-title { + margin-inline-start: 0.25rem; + } + + .fc-event-time { + font-size: 0.75rem; + font-weight: 500 !important; + } + + .fc-event-title { + font-size: 0.75rem; + font-weight: 500 !important; + } + + .fc-timegrid-event { + .fc-event-title { + font-size: 0.875rem; + } + } + + .fc-prev-button { + padding-inline-start: 0; + } + + .fc-prev-button, + .fc-next-button { + padding: 0.25rem; + } + + .fc-col-header .fc-col-header-cell .fc-col-header-cell-cushion { + padding: 0.5rem; + text-decoration: none !important; + } + + .fc-timegrid .fc-timegrid-slots .fc-timegrid-slot { + block-size: 3rem; + } + + // Removed double border on left in list view + .fc-list { + border-inline-start: none; + font-size: 0.875rem; + + .fc-list-day-cushion.fc-cell-shaded { + background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)); + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-weight: 600; + } + + .fc-list-event-time, + .fc-list-event-title { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + } + + .fc-list-day .fc-list-day-text, + .fc-list-day .fc-list-day-side-text { + text-decoration: none; + } + } + + .fc-timegrid-axis { + color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)); + font-size: 0.75rem; + text-transform: capitalize; + } + + .fc-timegrid-slot-label-frame { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-size: 0.75rem; + text-align: center; + text-transform: uppercase; + } + + .fc-header-toolbar { + flex-wrap: wrap; + margin: 1.25rem; + column-gap: 0.5rem; + row-gap: 1rem; + } + + .fc-toolbar-chunk { + display: flex; + align-items: center; + + .fc-button-group { + .fc-button-primary { + &, + &:hover, + &:not(.disabled):active { + border-color: transparent; + background-color: transparent; + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + } + + &:focus { + box-shadow: none !important; + } + } + } + + &:last-child { + .fc-button-group { + border: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 0.375rem; + + .fc-button { + font-size: 0.9rem; + letter-spacing: 0.0187rem; + padding-inline: 1rem; + text-transform: uppercase; + + &:not(:last-child) { + border-inline-end: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity)); + } + + &.fc-button-active { + background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity)); + color: rgb(var(--v-theme-primary)); + } + } + } + } + } + + .fc-toolbar-title { + display: inline-block; + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); + font-size: 1.25rem; + font-weight: 500; + } + + .fc-scrollgrid-section { + th { + border-inline: 0; + } + } + + // Calendar content container + .fc-view-harness { + min-block-size: 40.625rem; + } + + .fc-event { + border-color: transparent; + cursor: pointer; + margin-block-end: 0.3rem; + padding-block: 0.1875rem; + padding-inline: 0.3125rem; + } + + .fc-event-main { + color: inherit; + font-size: 0.75rem; + font-weight: 500; + padding-inline: 0.25rem; + } + + tbody[role="rowgroup"] { + > tr > td[role="presentation"] { + border: none; + } + } + + .fc-scrollgrid { + border-inline-start: none; + } + + .fc-daygrid-day { + padding: 0.3125rem; + } + + .fc-daygrid-day-number { + padding-block: 0.5rem; + padding-inline: 0.75rem; + } + + .fc-list-event-dot { + color: inherit; + + --fc-event-border-color: currentcolor; + } + + .fc-list-event { + background-color: transparent !important; + } + + .fc-popover { + @include mixins.elevation(3); + + border-radius: 6px; + + .fc-popover-header, + .fc-popover-body { + padding: 0.5rem; + } + + .fc-popover-title { + margin: 0; + font-size: 1rem; + font-weight: 500; + } + } + + // 👉 sidebar toggler + .fc-toolbar-chunk { + .fc-button-group { + align-items: center; + + .fc-button .fc-icon { + vertical-align: bottom; + } + + // ℹ️ Below two `background-image` styles contains static color due to browser limitation of not parsing the css var inside CSS url() + .fc-drawerToggler-button { + display: none; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(94,86,105,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E"); + background-position: 50%; + background-repeat: no-repeat; + block-size: 1.5625rem; + font-size: 0; + inline-size: 1.5625rem; + margin-inline-end: 0.25rem; + + @media (max-width: 1264px) { + display: block !important; + } + + .v-theme--dark & { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E"); + } + } + } + } + + // ℹ️ Workaround of https://github.com/fullcalendar/fullcalendar/issues/6407 + .fc-col-header, + .fc-daygrid-body, + .fc-scrollgrid-sync-table, + .fc-timegrid-body, + .fc-timegrid-body table { + inline-size: 100% !important; + } +} diff --git a/resources/js/@core/scss/template/libs/vuetify/_overrides.scss b/resources/js/@core/scss/template/libs/vuetify/_overrides.scss new file mode 100644 index 0000000..9eaf6f2 --- /dev/null +++ b/resources/js/@core/scss/template/libs/vuetify/_overrides.scss @@ -0,0 +1,195 @@ +@use "@configured-variables" as variables; +@use "@styles/variables/vuetify"; +@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation; + +// 👉 Typography +h1, +h2, +h3, +h4, +h5, +h6, +.text-h1, +.text-h2, +.text-h3, +.text-h4, +.text-h5, +.text-h6, +.text-body-1, +.text-subtitle-1, +.text-button, +.v-card-title { + color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)); +} + +.v-application, +.text-body-2, +.text-subtitle-2, +.text-overline { + color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity)); +} + +// 👉 Button +.v-btn { + .v-icon { + --v-icon-size-multiplier: 0.953; + } + + &--icon .v-icon { + --v-icon-size-multiplier: 1; + } +} + +// Alert +// custom icon style +$alert-prepend-icon-font-size: 1.125rem !important; + +.v-alert:not(.v-alert--prominent) { + .v-alert__prepend { + padding: 0.25rem; + border-radius: 1rem; + background-color: #fff; + + .v-icon { + block-size: $alert-prepend-icon-font-size; + font-size: $alert-prepend-icon-font-size; + inline-size: $alert-prepend-icon-font-size; + } + } +} + +@each $color-name in variables.$theme-colors-name { + .v-alert { + + &:not(.v-alert--prominent).text-#{$color-name}, + &:not(.v-alert--prominent).bg-#{$color-name} { + .v-alert__prepend { + border: 3px solid rgb(var(--v-theme-#{$color-name}), 0.4); + color: rgba(var(--v-theme-#{$color-name})) !important; + } + } + + &--variant-outlined:not(.v-alert--prominent), + &--variant-tonal:not(.v-alert--prominent), + &--variant-plain:not(.v-alert--prominent) { + &.bg-#{$color-name}, + &.text-#{$color-name} { + .v-alert__prepend { + background-color: rgb(var(--v-theme-#{$color-name})); + box-shadow: 0 0 0 3px rgba(var(--v-theme-#{$color-name}), 0.4); + color: #fff !important; + } + } + } + } +} + +// 👉 VAvatar +.v-avatar { + font-size: 1.125rem; + line-height: 1.25rem; +} + +// 👉 VChip +.v-chip { + line-height: normal; + text-transform: uppercase; +} + +.v-chip.v-chip--size-default .v-avatar { + font-size: 0.8125rem; + line-height: normal; +} + +// 👉 VTooltip +.v-tooltip { + .v-overlay__content { + font-weight: 500; + } +} + +// 👉 VMenu +.v-menu.v-overlay { + .v-overlay__content { + .v-list { + .v-list-item--density-default { + min-block-size: 2.25rem; + } + } + } +} + +// 👉 VTabs +.v-tabs--vertical:not(.v-tabs-pill) { + border-inline-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + + .v-tab__slider { + inset-inline-end: 0; + inset-inline-start: unset; + } +} + +.v-tabs.v-tabs-pill:not(.v-tabs--stacked) { + &.v-tabs--density-default { + --v-tabs-height: 38px; + } +} + +// 👉 VSliderThumb +.v-slider-thumb__surface { + border: 3px solid rgb(var(--v-theme-surface)); + + &::before { + inset: 0; + } +} + +.v-slider-thumb__label { + background: variables.$slider-thumb-label-color; + color: rgb(var(--v-theme-on-primary)); +} + +.v-slider-thumb__label::before { + color: variables.$slider-thumb-label-color; +} + +// 👉 VTimeline +.v-timeline { + .v-timeline-item:not(:last-child) { + .v-timeline-item__body { + margin-block-end: 0.95rem; + } + } +} + +// 👉 VDatatable +.v-data-table { + th { + background: rgb(var(--v-table-header-background)) !important; + font-size: 0.75rem; + font-weight: 500 !important; + letter-spacing: 0.17px !important; + text-transform: uppercase !important; + + .v-data-table-header__content { + display: flex; + justify-content: space-between; + } + } +} + +// 👉 VTable +.v-table { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important; + + th { + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important; + font-size: 0.75rem; + text-align: center !important; + text-transform: uppercase; + + &:first-child { + text-align: start !important; + } + } +} diff --git a/resources/js/@core/scss/template/libs/vuetify/_variables.scss b/resources/js/@core/scss/template/libs/vuetify/_variables.scss new file mode 100644 index 0000000..57c2f0d --- /dev/null +++ b/resources/js/@core/scss/template/libs/vuetify/_variables.scss @@ -0,0 +1,237 @@ +$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity); +$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity); +$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity); +/* stylelint-disable max-line-length */ +$font-family-custom: "Public Sans", sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + +/* stylelint-enable max-line-length */ + +@forward "../../../base/libs/vuetify/variables" with ( + // 👉 font-family + $body-font-family: $font-family-custom !default, + + // 👉 border-radius + $border-radius-root: 6px !default, + + $shadow-key-umbra: ( + 0: (0 0 0 0 var(--v-shadow-key-umbra-opacity)), + 1: (0 2px 1px -1px var(--v-shadow-key-umbra-opacity)), + 2: (0 3px 1px -2px var(--v-shadow-key-umbra-opacity)), + 3: (0 1px 6px -2px var(--v-shadow-key-umbra-opacity)), + 4: (0 1px 7px -2px var(--v-shadow-key-umbra-opacity)), + 5: (0 3px 5px -1px var(--v-shadow-key-umbra-opacity)), + 6: (0 2px 9px -2px var(--v-shadow-key-umbra-opacity)), + 7: (0 4px 5px -2px var(--v-shadow-key-umbra-opacity)), + 8: (0 5px 5px -3px var(--v-shadow-key-umbra-opacity)), + 9: (0 5px 6px -3px var(--v-shadow-key-umbra-opacity)), + 10: (0 6px 6px -3px var(--v-shadow-key-umbra-opacity)), + 11: (0 6px 7px -4px var(--v-shadow-key-umbra-opacity)), + 12: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)), + 13: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)), + 14: (0 7px 9px -4px var(--v-shadow-key-umbra-opacity)), + 15: (0 8px 9px -5px var(--v-shadow-key-umbra-opacity)), + 16: (0 8px 10px -5px var(--v-shadow-key-umbra-opacity)), + 17: (0 8px 11px -5px var(--v-shadow-key-umbra-opacity)), + 18: (0 9px 11px -5px var(--v-shadow-key-umbra-opacity)), + 19: (0 9px 12px -6px var(--v-shadow-key-umbra-opacity)), + 20: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)), + 21: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)), + 22: (0 10px 14px -6px var(--v-shadow-key-umbra-opacity)), + 23: (0 11px 14px -7px var(--v-shadow-key-umbra-opacity)), + 24: (0 11px 15px -7px var(--v-shadow-key-umbra-opacity)) + ) !default, + + $shadow-key-penumbra: ( + 0: (0 0 0 0 $shadow-key-penumbra-opacity-custom), + 1: (0 1px 1px 0 $shadow-key-penumbra-opacity-custom), + 2: (0 2px 2px 0 $shadow-key-penumbra-opacity-custom), + 3: (0 2px 6px 1px $shadow-key-penumbra-opacity-custom), + 4: (0 3px 7px 1px $shadow-key-penumbra-opacity-custom), + 5: (0 5px 8px 0 $shadow-key-penumbra-opacity-custom), + 6: (0 4px 9px 1px $shadow-key-penumbra-opacity-custom), + 7: (0 7px 10px 1px $shadow-key-penumbra-opacity-custom), + 8: (0 8px 10px 1px $shadow-key-penumbra-opacity-custom), + 9: (0 9px 12px 1px $shadow-key-penumbra-opacity-custom), + 10: (0 10px 14px 1px $shadow-key-penumbra-opacity-custom), + 11: (0 11px 15px 1px $shadow-key-penumbra-opacity-custom), + 12: (0 12px 17px 2px $shadow-key-penumbra-opacity-custom), + 13: (0 13px 19px 2px $shadow-key-penumbra-opacity-custom), + 14: (0 14px 21px 2px $shadow-key-penumbra-opacity-custom), + 15: (0 15px 22px 2px $shadow-key-penumbra-opacity-custom), + 16: (0 16px 24px 2px $shadow-key-penumbra-opacity-custom), + 17: (0 17px 26px 2px $shadow-key-penumbra-opacity-custom), + 18: (0 18px 28px 2px $shadow-key-penumbra-opacity-custom), + 19: (0 19px 29px 2px $shadow-key-penumbra-opacity-custom), + 20: (0 20px 31px 3px $shadow-key-penumbra-opacity-custom), + 21: (0 21px 33px 3px $shadow-key-penumbra-opacity-custom), + 22: (0 22px 35px 3px $shadow-key-penumbra-opacity-custom), + 23: (0 23px 36px 3px $shadow-key-penumbra-opacity-custom), + 24: (0 24px 38px 3px $shadow-key-penumbra-opacity-custom) + ) !default, + + $shadow-key-ambient: ( + 0: (0 0 0 0 $shadow-key-ambient-opacity-custom), + 1: (0 1px 3px 0 $shadow-key-ambient-opacity-custom), + 2: (0 1px 5px 0 $shadow-key-ambient-opacity-custom), + 3: (0 1px 4px 2px $shadow-key-ambient-opacity-custom), + 4: (0 1px 4px 2px $shadow-key-ambient-opacity-custom), + 5: (0 1px 14px 0 $shadow-key-ambient-opacity-custom), + 6: (0 2px 6px 4px $shadow-key-ambient-opacity-custom), + 7: (0 2px 16px 1px $shadow-key-ambient-opacity-custom), + 8: (0 3px 14px 2px $shadow-key-ambient-opacity-custom), + 9: (0 3px 16px 2px $shadow-key-ambient-opacity-custom), + 10: (0 4px 18px 3px $shadow-key-ambient-opacity-custom), + 11: (0 4px 20px 3px $shadow-key-ambient-opacity-custom), + 12: (0 5px 22px 4px $shadow-key-ambient-opacity-custom), + 13: (0 5px 24px 4px $shadow-key-ambient-opacity-custom), + 14: (0 5px 26px 4px $shadow-key-ambient-opacity-custom), + 15: (0 6px 28px 5px $shadow-key-ambient-opacity-custom), + 16: (0 6px 30px 5px $shadow-key-ambient-opacity-custom), + 17: (0 6px 32px 5px $shadow-key-ambient-opacity-custom), + 18: (0 7px 34px 6px $shadow-key-ambient-opacity-custom), + 19: (0 7px 36px 6px $shadow-key-ambient-opacity-custom), + 20: (0 8px 38px 7px $shadow-key-ambient-opacity-custom), + 21: (0 8px 40px 7px $shadow-key-ambient-opacity-custom), + 22: (0 8px 42px 7px $shadow-key-ambient-opacity-custom), + 23: (0 9px 44px 8px $shadow-key-ambient-opacity-custom), + 24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom) + ) !default, + + // 👉 Typography + $typography: ( + "h1": ( + "weight": 500, + "line-height": 7rem, + "letter-spacing": -0.0938rem, + ), + "h2": ( + "weight": 500, + "line-height": 4.5rem, + "letter-spacing": -0.0313rem, + ), + "h3": ( + "weight": 500, + "line-height": 3.5rem, + ), + "h4": ( + "weight": 500, + "letter-spacing": 0.0156rem, + ), + "h5": ( + "weight": 500, + ), + "h6": ( + "letter-spacing": 0.0094rem, + ), + "subtitle-1": ( + "letter-spacing": 0.0094rem, + ), + "subtitle-2": ( + "line-height": 1.3125rem, + "letter-spacing": 0.0063rem, + ), + "body-1": ( + "letter-spacing": 0.0094rem, + ), + "body-2": ( + "line-height": 1.3125rem, + "letter-spacing": 0.0094rem, + ), + "caption": ( + "line-height": 0.875rem, + "letter-spacing": 0.025rem, + ), + "button": ( + "line-height": 1.5rem, + "letter-spacing": 0.025rem, + ), + "overline": ( + "weight": 400, + "line-height": 0.875rem, + "letter-spacing": 0.0625rem, + ), + ) !default, + + // 👉 Alert + $alert-density: ("default": 0, "comfortable": -0.625, "compact": -2) !default, + $alert-title-font-size: 1rem !default, + $alert-title-line-height: 1.5rem !default, + $alert-prepend-margin-inline-end: 0.75rem !default, + + // 👉 Badges + $badge-dot-height: 0.5rem !default, + $badge-dot-width: 0.5rem !default, + + // 👉 Button + $button-height: 38px !default, + $button-icon-density: ("default": 2.5, "comfortable": 0, "compact": -1.5) !default, + $button-card-actions-padding: 0 12px !default, + + // 👉 Chip + $chip-font-size: 13px !default, + $chip-close-size: 22px !default, + $chip-label-border-radius: 4px !default, + $chip-density: ("default": 0, "comfortable": -1, "compact": -2) !default, + + // 👉 Dialog + $dialog-card-header-padding: 20px 20px 0 !default, + $dialog-card-text-padding: 20px !default, + $dialog-elevation: 16 !default, + + // 👉 Expansion Panel + $expansion-panel-title-padding: 14px 20px !default, + $expansion-panel-title-font-size: 1rem !default, + $expansion-panel-active-title-min-height: 51px !default, + $expansion-panel-title-min-height: 51px !default, + $expansion-panel-text-padding: 6px 20px 20px !default, + + // 👉 List + $list-item-icon-margin-end: 12px !default, + + // 👉 Pagination + $pagination-item-margin: 0.2rem !default, + + // 👉 Snackbar + $snackbar-border-radius: 8px !default, + $snackbar-btn-padding: 0 12px !default, + $snackbar-background: rgb(var(--v-snackbar-background)) !default, + $snackbar-color: rgb(var(--v-snackbar-color)) !default, + + // 👉 Tooltip + $tooltip-background-color: rgba(var(--v-tooltip-background),var(--v-tooltip-opacity)) !default, + $tooltip-padding: 4px 8px !default, + $tooltip-line-height: 16px !default, + $tooltip-font-size: 11px !default, + + // 👉 Timeline + $timeline-dot-divider-background: transparent !default, + $timeline-divider-line-thickness: 1px !default, + $timeline-item-padding: 16px !default, + + // 👉 input + $input-details-padding-above: 3px !default, + $text-field-details-padding-inline: 14px !default, + + // 👉 combobox + $combobox-content-elevation: 6 !default, + + // 👉 Range slider + $slider-track-active-size: 4px !default, + $slider-thumb-label-height: 29px !default, + $slider-thumb-label-padding: 4px 12px !default, + $slider-thumb-label-font-size: 12px !default, + $slider-track-border-radius: 12px !default, + + // 👉 Card + $card-item-padding: 1.5rem !default, + $card-border-radius: 0.5rem !default, + $card-text-padding: 1.5rem !default, + $card-text-font-size: 1rem !default, + $card-title-padding: 0.5rem 1.5rem !default, + $card-subtitle-padding: 0 1.5rem !default, + $card-prepend-padding-inline-end: 0.625rem !default, + $card-append-padding-inline-start: 0.625rem !default, + + // 👉 Button Group + $btn-group-height: 38px !default, +); diff --git a/resources/js/@core/scss/template/libs/vuetify/index.scss b/resources/js/@core/scss/template/libs/vuetify/index.scss new file mode 100644 index 0000000..deb639d --- /dev/null +++ b/resources/js/@core/scss/template/libs/vuetify/index.scss @@ -0,0 +1,2 @@ +@use "@core/scss/base/libs/vuetify"; +@use "overrides"; diff --git a/resources/js/@core/scss/template/pages/misc.scss b/resources/js/@core/scss/template/pages/misc.scss new file mode 100644 index 0000000..1454990 --- /dev/null +++ b/resources/js/@core/scss/template/pages/misc.scss @@ -0,0 +1,14 @@ +.layout-blank { + .misc-wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1.25rem; + min-block-size: calc(var(--vh, 1vh) * 100); + } + + .misc-avatar { + z-index: 1; + } +} diff --git a/resources/js/@core/scss/template/pages/page-auth.scss b/resources/js/@core/scss/template/pages/page-auth.scss new file mode 100644 index 0000000..38fca92 --- /dev/null +++ b/resources/js/@core/scss/template/pages/page-auth.scss @@ -0,0 +1,45 @@ +.layout-blank { + .auth-wrapper { + min-block-size: calc(var(--vh, 1vh) * 100); + } + + .auth-card { + z-index: 1 !important; + } +} + +.auth-title { + font-size: 28px; + font-weight: 700; +} + +.auth-v1-top-shape, +.auth-v1-bottom-shape { + position: absolute; +} + +.auth-v1-top-shape { + block-size: 148px; + inline-size: 148px; + inset-block-start: -2.5rem; + inset-inline-end: -2.5rem; +} + +.auth-v1-bottom-shape { + block-size: 240px; + inline-size: 240px; + inset-block-end: -4.5rem; + inset-inline-start: -3rem; +} + +.auth-illustration { + z-index: 1; +} + +@media (min-width: 960px) { + .skin--bordered { + .auth-card-v2 { + border-inline-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important; + } + } +} diff --git a/resources/js/@core/scss/template/placeholders/_default-layout-vertical-nav.scss b/resources/js/@core/scss/template/placeholders/_default-layout-vertical-nav.scss new file mode 100644 index 0000000..185b3ad --- /dev/null +++ b/resources/js/@core/scss/template/placeholders/_default-layout-vertical-nav.scss @@ -0,0 +1,11 @@ +@use "vuetify/lib/styles/tools/elevation" as elevation; +@use "@configured-variables" as variables; + +%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled { + // If navbar is contained => Squeeze navbar content on scroll + @if variables.$layout-vertical-nav-navbar-is-contained { + padding-inline: 1.5rem; + + @include elevation.elevation(4); + } +} diff --git a/resources/js/@core/scss/template/placeholders/_index.scss b/resources/js/@core/scss/template/placeholders/_index.scss new file mode 100644 index 0000000..6695412 --- /dev/null +++ b/resources/js/@core/scss/template/placeholders/_index.scss @@ -0,0 +1,3 @@ +@forward "vertical-nav"; +@forward "nav"; +@forward "default-layout-vertical-nav"; diff --git a/resources/js/@core/scss/template/placeholders/_nav.scss b/resources/js/@core/scss/template/placeholders/_nav.scss new file mode 100644 index 0000000..b3c4eaa --- /dev/null +++ b/resources/js/@core/scss/template/placeholders/_nav.scss @@ -0,0 +1,33 @@ +// ℹ️ This is common style that needs to be applied to both navs +%nav { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); +} + +/* + Active nav link styles for horizontal & vertical nav + + For horizontal nav it will be only applied to top level nav items + For vertical nav it will be only applied to nav links (not nav groups) +*/ +%nav-link-active { + --v-activated-opacity: 0.16; + + background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity)); + box-shadow: none; + color: rgb(var(--v-theme-primary)); +} + +// style for vertical nav nested icon +%nav-link-nested-active { + background-color: transparent !important; + box-shadow: none; + color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important; + font-weight: 600; + + // style for nested dot icon + .nav-item-icon { + color: rgb(var(--v-global-theme-primary)) !important; + filter: drop-shadow(rgb(var(--v-global-theme-primary)) 0 0 2px); + transform: scale(1.2); + } +} diff --git a/resources/js/@core/scss/template/placeholders/_vertical-nav.scss b/resources/js/@core/scss/template/placeholders/_vertical-nav.scss new file mode 100644 index 0000000..1f5874c --- /dev/null +++ b/resources/js/@core/scss/template/placeholders/_vertical-nav.scss @@ -0,0 +1,21 @@ +// Open & Active nav group styles +%vertical-nav-group-active { + --v-theme-overlay-multiplier: 2; + + color: rgb(var(--v-global-theme-primary)); + + &:hover { + --v-theme-overlay-multiplier: 4; + } +} + +// nav-group and nav-link border radius +%vertical-nav-item-interactive { + border-radius: 0.375rem; + margin-block-end: 0.125rem; +} + +// ℹ️ Icon styling for icon nested inside another nav item (2nd level) +%vertical-nav-items-nested-icon { + transition: transform 0.25s ease-in-out 0s; +} diff --git a/resources/js/@core/utils/external_api.js b/resources/js/@core/utils/external_api.js new file mode 100644 index 0000000..92b0fbe --- /dev/null +++ b/resources/js/@core/utils/external_api.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.JitsiMeetExternalAPI=t():e.JitsiMeetExternalAPI=t()}(self,(()=>(()=>{var e={372:(e,t,n)=>{"use strict";n.d(t,{default:()=>N});var r=n(620),i=n.n(r);class s extends r{constructor(){var e,t,n;super(...arguments),e=this,n={},(t=function(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t);if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e,"string");return"symbol"==typeof t?t:String(t)}(t="_storage"))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n}clear(){this._storage={}}get length(){return Object.keys(this._storage).length}getItem(e){return this._storage[e]}setItem(e,t){this._storage[e]=t}removeItem(e){delete this._storage[e]}key(e){const t=Object.keys(this._storage);if(!(t.length<=e))return t[e]}serialize(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];if(0===e.length)return JSON.stringify(this._storage);const t={...this._storage};return e.forEach((e=>{delete t[e]})),JSON.stringify(t)}}const o=new class extends r{constructor(){super();try{this._storage=window.localStorage,this._localStorageDisabled=!1}catch(e){}this._storage||(console.warn("Local storage is disabled."),this._storage=new s,this._localStorageDisabled=!0)}isLocalStorageDisabled(){return this._localStorageDisabled}setLocalStorageDisabled(e){this._localStorageDisabled=e;try{this._storage=e?new s:window.localStorage}catch(e){}this._storage||(this._storage=new s)}clear(){this._storage.clear(),this.emit("changed")}get length(){return this._storage.length}getItem(e){return this._storage.getItem(e)}setItem(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this._storage.setItem(e,t),n||this.emit("changed")}removeItem(e){this._storage.removeItem(e),this.emit("changed")}key(e){return this._storage.key(e)}serialize(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];if(this.isLocalStorageDisabled())return this._storage.serialize(e);const t=this._storage.length,n={};for(let r=0;r0&&void 0!==arguments[0]?arguments[0]:{};this.postis=function(e){var t,n=e.scope,r=e.window,i=e.windowForEventListening||window,s=e.allowedOrigin,o={},a=[],d={},l=!1,u="__ready__",p=function(e){var t;try{t=c(e.data)}catch(e){return}if((!s||e.origin===s)&&t&&t.postis&&t.scope===n){var r=o[t.method];if(r)for(var i=0;i{},this.postis.listen(v,(e=>this._receiveCallback(e)))}dispose(){this.postis.destroy()}send(e){this.postis.send({method:v,params:e})}setReceiveCallback(e){this._receiveCallback=e}}const _="request",b="response";class w{constructor(){let{backend:e}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this._listeners=new Map,this._requestID=0,this._responseHandlers=new Map,this._unprocessedMessages=new Set,this.addListener=this.on,e&&this.setBackend(e)}_disposeBackend(){this._backend&&(this._backend.dispose(),this._backend=null)}_onMessageReceived(e){if(e.type===b){const t=this._responseHandlers.get(e.id);t&&(t(e),this._responseHandlers.delete(e.id))}else e.type===_?this.emit("request",e.data,((t,n)=>{this._backend.send({type:b,error:n,id:e.id,result:t})})):this.emit("event",e.data)}dispose(){this._responseHandlers.clear(),this._unprocessedMessages.clear(),this.removeAllListeners(),this._disposeBackend()}emit(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{s=e(...n)||s})),s||this._unprocessedMessages.add(n),s}on(e,t){let n=this._listeners.get(e);return n||(n=new Set,this._listeners.set(e,n)),n.add(t),this._unprocessedMessages.forEach((e=>{t(...e)&&this._unprocessedMessages.delete(e)})),this}removeAllListeners(e){return e?this._listeners.delete(e):this._listeners.clear(),this}removeListener(e,t){const n=this._listeners.get(e);return n&&n.delete(t),this}sendEvent(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this._backend&&this._backend.send({type:"event",data:e})}sendRequest(e){if(!this._backend)return Promise.reject(new Error("No transport backend defined!"));this._requestID++;const t=this._requestID;return new Promise(((n,r)=>{this._responseHandlers.set(t,(e=>{let{error:t,result:i}=e;void 0!==i?n(i):r(void 0!==t?t:new Error("Unexpected response format!"))}));try{this._backend.send({type:_,data:e,id:t})}catch(e){this._responseHandlers.delete(t),r(e)}}))}setBackend(e){this._disposeBackend(),this._backend=e,this._backend.setReceiveCallback(this._onMessageReceived.bind(this))}}let L;try{L=function(e,t=!1,n="hash"){if(!e)return{};"string"==typeof e&&(e=new URL(e));const r="search"===n?e.search:e.hash,i={},s=r?.substr(1).split("&")||[];if("hash"===n&&1===s.length){const e=s[0];if(e.startsWith("/")&&1===e.split("&").length)return i}return s.forEach((e=>{const n=e.split("="),r=n[0];if(!r||r.split(".").some((e=>d.includes(e))))return;let s;try{if(s=n[1],!t){const e=decodeURIComponent(s).replace(/\\&/,"&");s="undefined"===e?void 0:c(e)}}catch(e){return void function(e,t=""){console.error(t,e),window.onerror?.(t,void 0,void 0,void 0,e)}(e,`Failed to parse URL parameter value: ${String(s)}`)}i[r]=s})),i}(window.location).jitsi_meet_external_api_id}catch(e){}(window.JitsiMeetJS||(window.JitsiMeetJS={}),window.JitsiMeetJS.app||(window.JitsiMeetJS.app={}),window.JitsiMeetJS.app).setExternalTransportBackend=e=>undefined.setBackend(e);var k=n(860);const C=n.n(k)().getLogger("modules/API/external/functions.js");function E(e,t){return e.sendRequest({type:"devices",name:"setDevice",device:t})}const S=["css/all.css","libs/alwaysontop.min.js"],x={addBreakoutRoom:"add-breakout-room",answerKnockingParticipant:"answer-knocking-participant",approveVideo:"approve-video",askToUnmute:"ask-to-unmute",autoAssignToBreakoutRooms:"auto-assign-to-breakout-rooms",avatarUrl:"avatar-url",cancelPrivateChat:"cancel-private-chat",closeBreakoutRoom:"close-breakout-room",displayName:"display-name",endConference:"end-conference",email:"email",grantModerator:"grant-moderator",hangup:"video-hangup",hideNotification:"hide-notification",initiatePrivateChat:"initiate-private-chat",joinBreakoutRoom:"join-breakout-room",localSubject:"local-subject",kickParticipant:"kick-participant",muteEveryone:"mute-everyone",overwriteConfig:"overwrite-config",overwriteNames:"overwrite-names",password:"password",pinParticipant:"pin-participant",rejectParticipant:"reject-participant",removeBreakoutRoom:"remove-breakout-room",resizeFilmStrip:"resize-film-strip",resizeLargeVideo:"resize-large-video",sendCameraFacingMode:"send-camera-facing-mode-message",sendChatMessage:"send-chat-message",sendEndpointTextMessage:"send-endpoint-text-message",sendParticipantToRoom:"send-participant-to-room",sendTones:"send-tones",setAssumedBandwidthBps:"set-assumed-bandwidth-bps",setFollowMe:"set-follow-me",setLargeVideoParticipant:"set-large-video-participant",setMediaEncryptionKey:"set-media-encryption-key",setNoiseSuppressionEnabled:"set-noise-suppression-enabled",setParticipantVolume:"set-participant-volume",setSubtitles:"set-subtitles",setTileView:"set-tile-view",setVideoQuality:"set-video-quality",showNotification:"show-notification",startRecording:"start-recording",startShareVideo:"start-share-video",stopRecording:"stop-recording",stopShareVideo:"stop-share-video",subject:"subject",submitFeedback:"submit-feedback",toggleAudio:"toggle-audio",toggleCamera:"toggle-camera",toggleCameraMirror:"toggle-camera-mirror",toggleChat:"toggle-chat",toggleE2EE:"toggle-e2ee",toggleFilmStrip:"toggle-film-strip",toggleLobby:"toggle-lobby",toggleModeration:"toggle-moderation",toggleNoiseSuppression:"toggle-noise-suppression",toggleParticipantsPane:"toggle-participants-pane",toggleRaiseHand:"toggle-raise-hand",toggleShareScreen:"toggle-share-screen",toggleSubtitles:"toggle-subtitles",toggleTileView:"toggle-tile-view",toggleVirtualBackgroundDialog:"toggle-virtual-background",toggleVideo:"toggle-video",toggleWhiteboard:"toggle-whiteboard"},O={"avatar-changed":"avatarChanged","audio-availability-changed":"audioAvailabilityChanged","audio-mute-status-changed":"audioMuteStatusChanged","audio-or-video-sharing-toggled":"audioOrVideoSharingToggled","breakout-rooms-updated":"breakoutRoomsUpdated","browser-support":"browserSupport","camera-error":"cameraError","chat-updated":"chatUpdated","compute-pressure-changed":"computePressureChanged","content-sharing-participants-changed":"contentSharingParticipantsChanged","data-channel-closed":"dataChannelClosed","data-channel-opened":"dataChannelOpened","device-list-changed":"deviceListChanged","display-name-change":"displayNameChange","dominant-speaker-changed":"dominantSpeakerChanged","email-change":"emailChange","error-occurred":"errorOccurred","endpoint-text-message-received":"endpointTextMessageReceived","face-landmark-detected":"faceLandmarkDetected","feedback-submitted":"feedbackSubmitted","feedback-prompt-displayed":"feedbackPromptDisplayed","filmstrip-display-changed":"filmstripDisplayChanged","incoming-message":"incomingMessage","knocking-participant":"knockingParticipant",log:"log","mic-error":"micError","moderation-participant-approved":"moderationParticipantApproved","moderation-participant-rejected":"moderationParticipantRejected","moderation-status-changed":"moderationStatusChanged","mouse-enter":"mouseEnter","mouse-leave":"mouseLeave","mouse-move":"mouseMove","non-participant-message-received":"nonParticipantMessageReceived","notification-triggered":"notificationTriggered","outgoing-message":"outgoingMessage","p2p-status-changed":"p2pStatusChanged","participant-joined":"participantJoined","participant-kicked-out":"participantKickedOut","participant-left":"participantLeft","participant-role-changed":"participantRoleChanged","participants-pane-toggled":"participantsPaneToggled","password-required":"passwordRequired","peer-connection-failure":"peerConnectionFailure","prejoin-screen-loaded":"prejoinScreenLoaded","proxy-connection-event":"proxyConnectionEvent","raise-hand-updated":"raiseHandUpdated",ready:"ready","recording-link-available":"recordingLinkAvailable","recording-status-changed":"recordingStatusChanged","participant-menu-button-clicked":"participantMenuButtonClick","video-ready-to-close":"readyToClose","video-conference-joined":"videoConferenceJoined","video-conference-left":"videoConferenceLeft","video-availability-changed":"videoAvailabilityChanged","video-mute-status-changed":"videoMuteStatusChanged","video-quality-changed":"videoQualityChanged","screen-sharing-status-changed":"screenSharingStatusChanged","subject-change":"subjectChange","suspend-detected":"suspendDetected","tile-view-changed":"tileViewChanged","toolbar-button-clicked":"toolbarButtonClicked","transcription-chunk-received":"transcriptionChunkReceived","whiteboard-status-changed":"whiteboardStatusChanged"},R={"_request-desktop-sources":"_requestDesktopSources"};let j=0;function I(e,t){e._numberOfParticipants+=t}function P(e){let t;return"string"==typeof e&&null!==String(e).match(/([0-9]*\.?[0-9]+)(em|pt|px|((d|l|s)?v)(h|w)|%)$/)?t=e:"number"==typeof e&&(t=`${e}px`),t}class N extends(i()){constructor(e){super();for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{},url:`https://${e}/#jitsi_meet_external_api_id=${j}`})}(e,{configOverwrite:d,iceServers:f,interfaceConfigOverwrite:l,jwt:u,lang:p,roomName:i,devices:v,userInfo:_,appData:{localStorageContent:C},release:L}),this._createIFrame(a,s,k),this._transport=new w({backend:new y({postisOptions:{allowedOrigin:new URL(this._url).origin,scope:`jitsi_meet_external_api_${j}`,window:this._frame.contentWindow}})}),Array.isArray(g)&&g.length>0&&this.invite(g),this._onload=h,this._tmpE2EEKey=b,this._isLargeVideoVisible=!1,this._isPrejoinVideoVisible=!1,this._numberOfParticipants=0,this._participants={},this._myUserID=void 0,this._onStageParticipant=void 0,this._setupListeners(),j++}_createIFrame(e,t,n){const r=`jitsiConferenceFrame${j}`;this._frame=document.createElement("iframe"),this._frame.allow=["autoplay","camera","clipboard-write","compute-pressure","display-capture","hid","microphone","screen-wake-lock","speaker-selection"].join("; "),this._frame.name=r,this._frame.id=r,this._setSize(e,t),this._frame.setAttribute("allowFullScreen","true"),this._frame.style.border=0,n&&(this._frame.sandbox=n),this._frame.src=this._url,this._frame=this._parentNode.appendChild(this._frame)}_getAlwaysOnTopResources(){const e=this._frame.contentWindow,t=e.document;let n="";const r=t.querySelector("base");if(r&&r.href)n=r.href;else{const{protocol:t,host:r}=e.location;n=`${t}//${r}`}return S.map((e=>new URL(e,n).href))}_getFormattedDisplayName(e){const{formattedDisplayName:t}=this._participants[e]||{};return t}_getOnStageParticipant(){return this._onStageParticipant}_getLargeVideo(){const e=this.getIFrame();if(this._isLargeVideoVisible&&e&&e.contentWindow&&e.contentWindow.document)return e.contentWindow.document.getElementById("largeVideo")}_getPrejoinVideo(){const e=this.getIFrame();if(this._isPrejoinVideoVisible&&e&&e.contentWindow&&e.contentWindow.document)return e.contentWindow.document.getElementById("prejoinVideo")}_getParticipantVideo(e){const t=this.getIFrame();if(t&&t.contentWindow&&t.contentWindow.document)return void 0===e||e===this._myUserID?t.contentWindow.document.getElementById("localVideo_container"):t.contentWindow.document.querySelector(`#participant_${e} video`)}_setSize(e,t){const n=P(e),r=P(t);void 0!==n&&(this._height=e,this._frame.style.height=n),void 0!==r&&(this._width=t,this._frame.style.width=r)}_setupListeners(){this._transport.on("event",(e=>{let{name:t,...n}=e;const r=n.id;switch(t){case"ready":var i;null===(i=this._onload)||void 0===i||i.call(this);break;case"video-conference-joined":if(void 0!==this._tmpE2EEKey){const e=e=>{const t=[];for(let n=0;n{const n=R[e.name],r={...e,name:n};n&&this.emit(n,r,t)}))}updateNumberOfParticipants(e){if(!e||!Object.keys(e).length)return;const t=Object.keys(e).reduce(((t,n)=>{var r;return null!==(r=e[n])&&void 0!==r&&r.participants?Object.keys(e[n].participants).length+t:t}),0);this._numberOfParticipants=t}async getRoomsInfo(){return this._transport.sendRequest({name:"rooms-info"})}isP2pActive(){return this._transport.sendRequest({name:"get-p2p-status"})}addEventListener(e,t){this.on(e,t)}addEventListeners(e){for(const t in e)this.addEventListener(t,e[t])}captureLargeVideoScreenshot(){return this._transport.sendRequest({name:"capture-largevideo-screenshot"})}dispose(){this.emit("_willDispose"),this._transport.dispose(),this.removeAllListeners(),this._frame&&this._frame.parentNode&&this._frame.parentNode.removeChild(this._frame)}executeCommand(e){if(e in x){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r(C.error(e),{})))}(this._transport)}getContentSharingParticipants(){return this._transport.sendRequest({name:"get-content-sharing-participants"})}getCurrentDevices(){return function(e){return e.sendRequest({type:"devices",name:"getCurrentDevices"}).catch((e=>(C.error(e),{})))}(this._transport)}getCustomAvatarBackgrounds(){return this._transport.sendRequest({name:"get-custom-avatar-backgrounds"})}getLivestreamUrl(){return this._transport.sendRequest({name:"get-livestream-url"})}getParticipantsInfo(){const e=Object.keys(this._participants),t=Object.values(this._participants);return t.forEach(((t,n)=>{t.participantId=e[n]})),t}getVideoQuality(){return this._videoQuality}isAudioAvailable(){return this._transport.sendRequest({name:"is-audio-available"})}isDeviceChangeAvailable(e){return function(e,t){return e.sendRequest({deviceType:t,type:"devices",name:"isDeviceChangeAvailable"})}(this._transport,e)}isDeviceListAvailable(){return function(e){return e.sendRequest({type:"devices",name:"isDeviceListAvailable"})}(this._transport)}isMultipleAudioInputSupported(){return function(e){return e.sendRequest({type:"devices",name:"isMultipleAudioInputSupported"})}(this._transport)}invite(e){return Array.isArray(e)&&0!==e.length?this._transport.sendRequest({name:"invite",invitees:e}):Promise.reject(new TypeError("Invalid Argument"))}isAudioMuted(){return this._transport.sendRequest({name:"is-audio-muted"})}isAudioDisabled(){return this._transport.sendRequest({name:"is-audio-disabled"})}isModerationOn(e){return this._transport.sendRequest({name:"is-moderation-on",mediaType:e})}isParticipantForceMuted(e,t){return this._transport.sendRequest({name:"is-participant-force-muted",participantId:e,mediaType:t})}isParticipantsPaneOpen(){return this._transport.sendRequest({name:"is-participants-pane-open"})}isSharingScreen(){return this._transport.sendRequest({name:"is-sharing-screen"})}isStartSilent(){return this._transport.sendRequest({name:"is-start-silent"})}getAvatarURL(e){const{avatarURL:t}=this._participants[e]||{};return t}getDeploymentInfo(){return this._transport.sendRequest({name:"deployment-info"})}getDisplayName(e){const{displayName:t}=this._participants[e]||{};return t}getEmail(e){const{email:t}=this._participants[e]||{};return t}getIFrame(){return this._frame}getNumberOfParticipants(){return this._numberOfParticipants}getSupportedCommands(){return Object.keys(x)}getSupportedEvents(){return Object.values(O)}isVideoAvailable(){return this._transport.sendRequest({name:"is-video-available"})}isVideoMuted(){return this._transport.sendRequest({name:"is-video-muted"})}listBreakoutRooms(){return this._transport.sendRequest({name:"list-breakout-rooms"})}_isNewElectronScreensharingSupported(){return this._transport.sendRequest({name:"_new_electron_screensharing_supported"})}pinParticipant(e,t){this.executeCommand("pinParticipant",e,t)}removeEventListener(e){this.removeAllListeners(e)}removeEventListeners(e){e.forEach((e=>this.removeEventListener(e)))}resizeLargeVideo(e,t){e<=this._width&&t<=this._height&&this.executeCommand("resizeLargeVideo",e,t)}sendProxyConnectionEvent(e){this._transport.sendEvent({data:[e],name:"proxy-connection-event"})}setAudioInputDevice(e,t){return function(e,t,n){return E(e,{id:n,kind:"audioinput",label:t})}(this._transport,e,t)}setAudioOutputDevice(e,t){return function(e,t,n){return E(e,{id:n,kind:"audiooutput",label:t})}(this._transport,e,t)}setLargeVideoParticipant(e,t){this.executeCommand("setLargeVideoParticipant",e,t)}setVideoInputDevice(e,t){return function(e,t,n){return E(e,{id:n,kind:"videoinput",label:t})}(this._transport,e,t)}startRecording(e){this.executeCommand("startRecording",e)}stopRecording(e){this.executeCommand("stopRecording",e)}toggleE2EE(e){this.executeCommand("toggleE2EE",e)}async setMediaEncryptionKey(e){const{key:t,index:n}=e;if(t){const e=await crypto.subtle.exportKey("raw",t);this.executeCommand("setMediaEncryptionKey",JSON.stringify({exportedKey:Array.from(new Uint8Array(e)),index:n}))}else this.executeCommand("setMediaEncryptionKey",JSON.stringify({exportedKey:!1,index:n}))}}},872:(e,t,n)=>{e.exports=n(372).default},571:(e,t)=>{"use strict";const n=/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*\:/;t.parse=function(e){const r="object"==typeof(arguments.length<=1?void 0:arguments[1])&&(arguments.length<=1?void 0:arguments[1]),i=(arguments.length<=1?0:arguments.length-1)>1||!r?arguments.length<=1?void 0:arguments[1]:void 0,s=(arguments.length<=1?0:arguments.length-1)>1&&(arguments.length<=2?void 0:arguments[2])||r||{},o=JSON.parse(e,i);return"ignore"===s.protoAction?o:o&&"object"==typeof o&&e.match(n)?(t.scan(o,s),o):o},t.scan=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=[e];for(;n.length;){const e=n;n=[];for(const r of e){if(Object.prototype.hasOwnProperty.call(r,"__proto__")){if("remove"!==t.protoAction)throw new SyntaxError("Object contains forbidden prototype property");delete r.__proto__}for(const e in r){const t=r[e];t&&"object"==typeof t&&n.push(r[e])}}}},t.safeParse=function(e,n){try{return t.parse(e,n)}catch(e){return null}}},369:(e,t,n)=>{var r=n(7);function i(e,t){this.logStorage=e,this.stringifyObjects=!(!t||!t.stringifyObjects)&&t.stringifyObjects,this.storeInterval=t&&t.storeInterval?t.storeInterval:3e4,this.maxEntryLength=t&&t.maxEntryLength?t.maxEntryLength:1e4,Object.values(r.levels).forEach(function(e){this[e]=function(){this._log.apply(this,arguments)}.bind(this,e)}.bind(this)),this.storeLogsIntervalID=null,this.queue=[],this.totalLen=0,this.outputCache=[]}i.prototype.stringify=function(e){try{return JSON.stringify(e)}catch(e){return"[object with circular refs?]"}},i.prototype.formatLogMessage=function(e){for(var t="",n=1,r=arguments.length;n=this.maxEntryLength&&this._flush(!0,!0)},i.prototype.start=function(){this._reschedulePublishInterval()},i.prototype._reschedulePublishInterval=function(){this.storeLogsIntervalID&&(window.clearTimeout(this.storeLogsIntervalID),this.storeLogsIntervalID=null),this.storeLogsIntervalID=window.setTimeout(this._flush.bind(this,!1,!0),this.storeInterval)},i.prototype.flush=function(){this._flush(!1,!0)},i.prototype._storeLogs=function(e){try{this.logStorage.storeLogs(e)}catch(e){console.error("LogCollector error when calling logStorage.storeLogs(): ",e)}},i.prototype._flush=function(e,t){var n=!1;try{n=this.logStorage.isReady()}catch(e){console.error("LogCollector error when calling logStorage.isReady(): ",e)}this.totalLen>0&&(n||e)&&(n?(this.outputCache.length&&(this.outputCache.forEach(function(e){this._storeLogs(e)}.bind(this)),this.outputCache=[]),this._storeLogs(this.queue)):this.outputCache.push(this.queue),this.queue=[],this.totalLen=0),t&&this._reschedulePublishInterval()},i.prototype.stop=function(){this._flush(!1,!1)},e.exports=i},7:e=>{var t={trace:0,debug:1,info:2,log:3,warn:4,error:5};o.consoleTransport=console;var n=[o.consoleTransport];o.addGlobalTransport=function(e){-1===n.indexOf(e)&&n.push(e)},o.removeGlobalTransport=function(e){var t=n.indexOf(e);-1!==t&&n.splice(t,1)};var r={};function i(){var e={methodName:"",fileLocation:"",line:null,column:null},t=new Error,n=t.stack?t.stack.split("\n"):[];if(!n||n.length<3)return e;var r=null;return n[3]&&(r=n[3].match(/\s*at\s*(.+?)\s*\((\S*)\s*:(\d*)\s*:(\d*)\)/)),!r||r.length<=4?(0===n[2].indexOf("log@")?e.methodName=n[3].substr(0,n[3].indexOf("@")):e.methodName=n[2].substr(0,n[2].indexOf("@")),e):(e.methodName=r[1],e.fileLocation=r[2],e.line=r[3],e.column=r[4],e)}function s(){var e=arguments[0],s=arguments[1],o=Array.prototype.slice.call(arguments,2);if(!(t[s]1&&p.push("<"+a.methodName+">: ");var h=p.concat(o);try{u.bind(l).apply(l,h)}catch(e){console.error("An error occured when trying to log with one of the available transports",e)}}}}function o(e,n,r,i){this.id=n,this.options=i||{},this.transports=r,this.transports||(this.transports=[]),this.level=t[e];for(var o=Object.keys(t),a=0;a{var r=n(7),i=n(369),s={},o=[],a=r.levels.TRACE;e.exports={addGlobalTransport:function(e){r.addGlobalTransport(e)},removeGlobalTransport:function(e){r.removeGlobalTransport(e)},setGlobalOptions:function(e){r.setGlobalOptions(e)},getLogger:function(e,t,n){var i=new r(a,e,t,n);return e?(s[e]=s[e]||[],s[e].push(i)):o.push(i),i},getUntrackedLogger:function(e,t,n){return new r(a,e,t,n)},setLogLevelById:function(e,t){for(var n=t?s[t]||[]:o,r=0;r{"use strict";var t,n="object"==typeof Reflect?Reflect:null,r=n&&"function"==typeof n.apply?n.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};t=n&&"function"==typeof n.ownKeys?n.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var i=Number.isNaN||function(e){return e!=e};function s(){s.init.call(this)}e.exports=s,e.exports.once=function(e,t){return new Promise((function(n,r){function i(n){e.removeListener(t,s),r(n)}function s(){"function"==typeof e.removeListener&&e.removeListener("error",i),n([].slice.call(arguments))}m(e,t,s,{once:!0}),"error"!==t&&function(e,t,n){"function"==typeof e.on&&m(e,"error",t,{once:!0})}(e,i)}))},s.EventEmitter=s,s.prototype._events=void 0,s.prototype._eventsCount=0,s.prototype._maxListeners=void 0;var o=10;function a(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function c(e){return void 0===e._maxListeners?s.defaultMaxListeners:e._maxListeners}function d(e,t,n,r){var i,s,o,d;if(a(n),void 0===(s=e._events)?(s=e._events=Object.create(null),e._eventsCount=0):(void 0!==s.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),s=e._events),o=s[t]),void 0===o)o=s[t]=n,++e._eventsCount;else if("function"==typeof o?o=s[t]=r?[n,o]:[o,n]:r?o.unshift(n):o.push(n),(i=c(e))>0&&o.length>i&&!o.warned){o.warned=!0;var l=new Error("Possible EventEmitter memory leak detected. "+o.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");l.name="MaxListenersExceededWarning",l.emitter=e,l.type=t,l.count=o.length,d=l,console&&console.warn&&console.warn(d)}return e}function l(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function u(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},i=l.bind(r);return i.listener=n,r.wrapFn=i,i}function p(e,t,n){var r=e._events;if(void 0===r)return[];var i=r[t];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(e){for(var t=new Array(e.length),n=0;n0&&(o=t[0]),o instanceof Error)throw o;var a=new Error("Unhandled error."+(o?" ("+o.message+")":""));throw a.context=o,a}var c=s[e];if(void 0===c)return!1;if("function"==typeof c)r(c,this,t);else{var d=c.length,l=g(c,d);for(n=0;n=0;s--)if(n[s]===t||n[s].listener===t){o=n[s].listener,i=s;break}if(i<0)return this;0===i?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return p(this,e,!0)},s.prototype.rawListeners=function(e){return p(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):h.call(e,t)},s.prototype.listenerCount=h,s.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}return n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n(872)})())); +//# sourceMappingURL=external_api.min.js.map diff --git a/resources/js/@core/utils/formatters.js b/resources/js/@core/utils/formatters.js new file mode 100644 index 0000000..2949e05 --- /dev/null +++ b/resources/js/@core/utils/formatters.js @@ -0,0 +1,46 @@ +import { isToday } from './index' + +export const avatarText = value => { + if (!value) + return '' + const nameArray = value.split(' ') + + return nameArray.map(word => word.charAt(0).toUpperCase()).join('') +} + +// TODO: Try to implement this: https://twitter.com/fireship_dev/status/1565424801216311297 +export const kFormatter = num => { + const regex = /\B(?=(\d{3})+(?!\d))/g + + return Math.abs(num) > 9999 ? `${Math.sign(num) * +((Math.abs(num) / 1000).toFixed(1))}k` : Math.abs(num).toFixed(0).replace(regex, ',') +} + +/** + * Format and return date in Humanize format + * Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format + * Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat + * @param {String} value date to format + * @param {Intl.DateTimeFormatOptions} formatting Intl object to format with + */ +export const formatDate = (value, formatting = { month: 'short', day: 'numeric', year: 'numeric' }) => { + if (!value) + return value + + return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value)) +} + +/** + * Return short human friendly month representation of date + * Can also convert date to only time if date is of today (Better UX) + * @param {String} value date to format + * @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current + */ +export const formatDateToMonthShort = (value, toTimeForCurrentDay = true) => { + const date = new Date(value) + let formatting = { month: 'short', day: 'numeric' } + if (toTimeForCurrentDay && isToday(date)) + formatting = { hour: 'numeric', minute: 'numeric' } + + return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value)) +} +export const prefixWithPlus = value => value > 0 ? `+${value}` : value diff --git a/resources/js/@core/utils/index.js b/resources/js/@core/utils/index.js new file mode 100644 index 0000000..3c15edc --- /dev/null +++ b/resources/js/@core/utils/index.js @@ -0,0 +1,31 @@ +// 👉 IsEmpty +export const isEmpty = value => { + if (value === null || value === undefined || value === '') + return true + + return !!(Array.isArray(value) && value.length === 0) +} + +// 👉 IsNullOrUndefined +export const isNullOrUndefined = value => { + return value === null || value === undefined +} + +// 👉 IsEmptyArray +export const isEmptyArray = arr => { + return Array.isArray(arr) && arr.length === 0 +} + +// 👉 IsObject +export const isObject = obj => obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj) +export const isToday = date => { + const today = new Date() + + return ( + /* eslint-disable operator-linebreak */ + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + /* eslint-enable */ + ) +} diff --git a/resources/js/@core/utils/validators.js b/resources/js/@core/utils/validators.js new file mode 100644 index 0000000..88c3f46 --- /dev/null +++ b/resources/js/@core/utils/validators.js @@ -0,0 +1,208 @@ +import { isEmpty, isEmptyArray, isNullOrUndefined } from './index'; + +// 👉 Required Validator +export const requiredValidator = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'This field is required' + + return !!String(value).trim().length || 'This field is required' +} +export const cardNumberValidator = value => { + // Adjust the regex based on your credit card number pattern + const cardNumberPattern = /^(\d{14}|\d{15}|\d{16})$/; + + return cardNumberPattern.test(value) || 'Invalid credit card number'; +}; +export const requiredGender = (value) => !!value || 'Gender is required' +export const requiredLicenseNumber = (value) => !!value || 'Medical License Number is required' +export const requiredYearsofExperience = (value) => !!value || 'Years of Experience is required' +export const requiredSpecialty = (value) => !!value || 'Practice or Provider of Specialty is required' +export const requiredFirstName = (value) => !!value || 'First Name is required' +export const requiredZip = (value) => !!value || 'Zip Code is required' + +export const expiryValidator = value => { + // Check if the format is MM/YY + const formatRegex = /^(0[1-9]|1[0-2])\/\d{2}$/; + if (!formatRegex.test(value)) { + return 'Invalid date format. Please use MM/YY'; + } + + // Check if the date is not expired (assuming the current date is 01/24 for example) + const currentDate = new Date(); + const currentYear = currentDate.getFullYear() % 100; + const currentMonth = currentDate.getMonth() + 1; + + const [inputMonth, inputYear] = value.split('/').map(Number); + + if (inputYear < currentYear || (inputYear === currentYear && inputMonth < currentMonth)) { + return 'The card has expired'; + } + + return true; +} +export const cvvValidator = value => { + return /^\d{3}$/.test(value) || 'Must be a 3-digit number'; +} +export const requiredAddress = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Address is required' + + return !!String(value).trim().length || 'Address is required' +} +export const requiredLocation = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Location is required' + + return !!String(value).trim().length || 'Location is required' +} +export const requiredCity = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'City is required' + + return !!String(value).trim().length || 'City is required' +} +export const requiredPassword = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Password field is required' + + return !!String(value).trim().length || 'Password field is required' +} +export const requiredConfirm = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Confirm Password field is required' + + return !!String(value).trim().length || ' Confirm Password field is required' +} +export const requiredName = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Name field is required' + + return !!String(value).trim().length || 'Name is required' +} +export const requiredLastName = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Last Name field is required' + + return !!String(value).trim().length || ' Last Name is required' +} +export const requiredPhone = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Phone is required' + + return !!String(value).trim().length || ' Phone is required' +} + +export const requiredEmail = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Email field is required' + + return !!String(value).trim().length || 'Email is required' +} +export const requiredState = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'State field is required' + + return !!String(value).trim().length || 'State is required' +} +export const requiredDate = value => { + if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) + return 'Date of Birth field is required' + + return !!String(value).trim().length || 'Date of Birth is required' +} +// 👉 Email Validator +export const emailValidator = value => { + if (isEmpty(value)) + return true + const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + if (Array.isArray(value)) + return value.every(val => re.test(String(val))) || 'The Email field must be a valid email' + + return re.test(String(value)) || 'The Email field must be a valid email' +} + +// 👉 Password Validator +export const passwordValidator = password => { + const regExp = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%&*()]).{8,}/ + const validPassword = regExp.test(password) + + return ( + // eslint-disable-next-line operator-linebreak + validPassword || + 'Field must contain at least one uppercase, lowercase, special character and digit with min 8 chars') +} + +// 👉 Confirm Password Validator +export const confirmedValidator = (value, target) => value === target || 'The Confirm Password field confirmation does not match' + +// 👉 Between Validator +export const betweenValidator = (value, min, max) => { + const valueAsNumber = Number(value) + + return (Number(min) <= valueAsNumber && Number(max) >= valueAsNumber) || `Enter number between ${min} and ${max}` +} + +// 👉 Integer Validator +export const integerValidator = value => { + if (isEmpty(value)) + return true + if (Array.isArray(value)) + return value.every(val => /^-?[0-9]+$/.test(String(val))) || 'This field must be an integer' + + return /^-?[0-9]+$/.test(String(value)) || 'This field must be an integer' +} + +// 👉 Regex Validator +export const regexValidator = (value, regex) => { + if (isEmpty(value)) + return true + let regeX = regex + if (typeof regeX === 'string') + regeX = new RegExp(regeX) + if (Array.isArray(value)) + return value.every(val => regexValidator(val, regeX)) + + return regeX.test(String(value)) || 'The Regex field format is invalid' +} + +// 👉 Alpha Validator +export const alphaValidator = value => { + if (isEmpty(value)) + return true + + return /^[A-Z]*$/i.test(String(value)) || 'The Alpha field may only contain alphabetic characters' +} + +// 👉 URL Validator +export const urlValidator = value => { + if (isEmpty(value)) + return true + const re = /^(http[s]?:\/\/){0,1}(www\.){0,1}[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,5}[\.]{0,1}/ + + return re.test(String(value)) || 'URL is invalid' +} + +// 👉 Length Validator +export const lengthValidator = (value, length) => { + if (isEmpty(value)) + return true + + return String(value).length === length || `The Min Character field must be at least ${length} characters` +} + +// 👉 Alpha-dash Validator +export const alphaDashValidator = value => { + if (isEmpty(value)) + return true + const valueAsString = String(value) + + return /^[0-9A-Z_-]*$/i.test(valueAsString) || 'All Character are not valid' +} + +export const validUSAPhone = value => { + if (isEmpty(value)) + return true + const valueAsString = String(value) + + return /^\(\d{3}\)\s\d{3}-\d{4}$/i.test(valueAsString) || 'Phone are not valid' +} diff --git a/resources/js/@fake-db/.DS_Store b/resources/js/@fake-db/.DS_Store new file mode 100644 index 0000000..58a2a2d Binary files /dev/null and b/resources/js/@fake-db/.DS_Store differ diff --git a/resources/js/@fake-db/app-bar-search/index.js b/resources/js/@fake-db/app-bar-search/index.js new file mode 100644 index 0000000..878774a --- /dev/null +++ b/resources/js/@fake-db/app-bar-search/index.js @@ -0,0 +1,696 @@ +// ** Mock Adapter +import mock from '@/@fake-db/mock' + +const database = [ + { + id: 1, + url: { name: 'dashboards-analytics' }, + icon: 'tabler-timeline', + title: 'Analytics Dashboard', + category: 'dashboards', + }, + { + id: 2, + url: { name: 'dashboards-ecommerce' }, + icon: 'tabler-shopping-cart', + title: 'eCommerce Dashboard', + category: 'dashboards', + }, + { + id: 3, + url: { name: 'dashboards-crm' }, + icon: 'tabler-shopping-cart', + title: 'CRM Dashboard', + category: 'dashboards', + }, + { + id: 4, + url: { name: 'apps-email' }, + icon: 'tabler-mail', + title: 'Email', + category: 'appsPages', + }, + { + id: 5, + url: { name: 'apps-chat' }, + icon: 'tabler-message', + title: 'Chat', + category: 'appsPages', + }, + { + id: 6, + url: { name: 'apps-calendar' }, + icon: 'tabler-calendar', + title: 'Calendar', + category: 'appsPages', + }, + { + id: 7, + url: { name: 'apps-invoice-list' }, + icon: 'tabler-list', + title: 'Invoice List', + category: 'appsPages', + }, + { + id: 8, + url: { name: 'apps-invoice-preview-id', params: { id: '5036' } }, + icon: 'tabler-file-description', + title: 'Invoice Preview', + category: 'appsPages', + }, + { + id: 9, + url: { name: 'apps-invoice-edit-id', params: { id: '5036' } }, + icon: 'tabler-file-pencil', + title: 'Invoice Edit', + category: 'appsPages', + }, + { + id: 11, + url: { name: 'apps-invoice-add' }, + icon: 'tabler-file-plus', + title: 'Invoice Add', + category: 'appsPages', + }, + { + id: 12, + url: { name: 'apps-user-list' }, + icon: 'tabler-user', + title: 'User List', + category: 'appsPages', + }, + { + id: 13, + url: { name: 'apps-user-view-id', params: { id: 21 } }, + icon: 'tabler-eye', + title: 'User View', + category: 'appsPages', + }, + { + id: 15, + url: { name: 'pages-help-center' }, + icon: 'tabler-help', + title: 'Help Center', + category: 'appsPages', + }, + { + id: 16, + url: { name: 'pages-user-profile-tab', params: { tab: 'profile' } }, + icon: 'tabler-user', + title: 'User Profile - Profile', + category: 'appsPages', + }, + { + id: 17, + url: { name: 'pages-account-settings-tab', params: { tab: 'account' } }, + icon: 'tabler-user', + title: 'Account Settings - Account', + category: 'appsPages', + }, + { + id: 18, + url: { name: 'pages-account-settings-tab', params: { tab: 'security' } }, + icon: 'tabler-lock-open', + title: 'Account Settings - Security', + category: 'appsPages', + }, + { + id: 19, + url: { name: 'pages-account-settings-tab', params: { tab: 'billing-plans' } }, + icon: 'tabler-currency-dollar', + title: 'Account Settings - Billing', + category: 'appsPages', + }, + { + id: 20, + url: { name: 'pages-account-settings-tab', params: { tab: 'notification' } }, + icon: 'tabler-bell', + title: 'Account Settings - Notifications', + category: 'appsPages', + }, + { + id: 21, + url: { name: 'pages-account-settings-tab', params: { tab: 'connection' } }, + icon: 'tabler-link', + title: 'Account Settings - Connections', + category: 'appsPages', + }, + { + id: 22, + url: { name: 'pages-pricing' }, + icon: 'tabler-currency-dollar', + title: 'Pricing', + category: 'appsPages', + }, + { + id: 23, + url: { name: 'pages-faq' }, + icon: 'tabler-help', + title: 'FAQ', + category: 'appsPages', + }, + { + id: 24, + url: { name: 'pages-misc-coming-soon' }, + icon: 'tabler-clock', + title: 'Coming Soon', + category: 'appsPages', + }, + { + id: 25, + url: { name: 'pages-misc-under-maintenance' }, + icon: 'tabler-settings', + title: 'Under Maintenance', + category: 'appsPages', + }, + { + id: 26, + url: { name: 'pages-misc-not-found' }, + icon: 'tabler-alert-circle', + title: 'Page Not Found - 404', + category: 'appsPages', + }, + { + id: 27, + url: { name: 'pages-misc-not-authorized' }, + icon: 'tabler-users', + title: 'Not Authorized - 401', + category: 'appsPages', + }, + { + id: 28, + url: { name: 'pages-misc-internal-server-error' }, + icon: 'tabler-list', + title: 'Server Error - 500', + category: 'appsPages', + }, + { + id: 29, + url: { name: 'pages-authentication-login-v1' }, + icon: 'tabler-login', + title: 'Login V1', + category: 'appsPages', + }, + { + id: 30, + url: { name: 'pages-authentication-login-v2' }, + icon: 'tabler-login', + title: 'Login V2', + category: 'appsPages', + }, + { + id: 31, + url: { name: 'pages-authentication-register-v1' }, + icon: 'tabler-user-plus', + title: 'Register V1', + category: 'appsPages', + }, + { + id: 32, + url: { name: 'pages-authentication-register-v2' }, + icon: 'tabler-user-plus', + title: 'Register V2', + category: 'appsPages', + }, + { + id: 42, + icon: 'tabler-mail', + category: 'appsPages', + title: 'Verify Email V1', + url: { name: 'pages-authentication-verify-email-v1' }, + }, + { + id: 43, + icon: 'tabler-mail', + category: 'appsPages', + title: 'Verify Email V2', + url: { name: 'pages-authentication-verify-email-v2' }, + }, + { + id: 35, + url: { name: 'pages-authentication-forgot-password-v1' }, + icon: 'tabler-lock', + title: 'Forgot Password V1', + category: 'appsPages', + }, + { + id: 36, + url: { name: 'pages-authentication-forgot-password-v2' }, + icon: 'tabler-lock', + title: 'Forgot Password V2', + category: 'appsPages', + }, + { + id: 37, + url: { name: 'pages-authentication-reset-password-v1' }, + icon: 'tabler-lock', + title: 'Reset Password V1', + category: 'appsPages', + }, + { + id: 38, + url: { name: 'pages-authentication-reset-password-v2' }, + icon: 'tabler-lock', + title: 'Reset Password V2', + category: 'appsPages', + }, + { + id: 48, + icon: 'tabler-devices', + category: 'appsPages', + title: 'Two Steps V1', + url: { name: 'pages-authentication-two-steps-v1' }, + }, + { + id: 49, + icon: 'tabler-devices', + category: 'appsPages', + title: 'Two Steps V2', + url: { name: 'pages-authentication-two-steps-v2' }, + }, + { + id: 41, + url: { name: 'pages-typography' }, + icon: 'tabler-baseline', + title: 'Typography', + category: 'userInterface', + }, + { + id: 42, + url: { name: 'pages-icons' }, + icon: 'tabler-brand-google', + title: 'Icons', + category: 'userInterface', + }, + { + id: 43, + url: { name: 'pages-cards-card-basic' }, + icon: 'tabler-cards', + title: 'Card Basic', + category: 'userInterface', + }, + { + id: 56, + url: { name: 'pages-cards-card-advance' }, + icon: 'tabler-cards', + title: 'Card Advance', + category: 'userInterface', + }, + { + id: 58, + url: { name: 'pages-cards-card-statistics' }, + icon: 'tabler-chart-bar', + title: 'Card Statistics', + category: 'userInterface', + }, + { + id: 59, + url: { name: 'pages-cards-card-widgets' }, + icon: 'tabler-id', + title: 'Card Widgets', + category: 'userInterface', + }, + { + id: 46, + url: { name: 'pages-cards-card-actions' }, + icon: 'tabler-square-plus', + title: 'Card Actions', + category: 'userInterface', + }, + { + id: 47, + url: { name: 'components-alert' }, + icon: 'tabler-alert-triangle', + title: 'Alerts', + category: 'userInterface', + }, + { + id: 48, + url: { name: 'components-avatar' }, + icon: 'tabler-user-circle', + title: 'Avatars', + category: 'userInterface', + }, + { + id: 49, + url: { name: 'components-badge' }, + icon: 'tabler-bell', + title: 'Badges', + category: 'userInterface', + }, + { + id: 50, + url: { name: 'components-button' }, + icon: 'tabler-hand-click', + title: 'Buttons', + category: 'userInterface', + }, + { + id: 51, + url: { name: 'components-chip' }, + icon: 'tabler-box', + title: 'Chips', + category: 'userInterface', + }, + { + id: 52, + url: { name: 'components-dialog' }, + icon: 'tabler-square', + title: 'Dialogs', + category: 'userInterface', + }, + { + id: 53, + url: { name: 'components-list' }, + icon: 'tabler-list', + title: 'List', + category: 'userInterface', + }, + { + id: 54, + url: { name: 'components-menu' }, + icon: 'tabler-menu-2', + title: 'Menu', + category: 'userInterface', + }, + { + id: 55, + url: { name: 'components-pagination' }, + icon: 'tabler-player-skip-forward', + title: 'Pagination', + category: 'userInterface', + }, + { + id: 56, + url: { name: 'components-progress-circular' }, + icon: 'tabler-adjustments-alt', + title: 'Progress Circular', + category: 'userInterface', + }, + { + id: 83, + url: { name: 'components-progress-linear' }, + icon: 'tabler-adjustments-alt', + title: 'Progress Linear', + category: 'userInterface', + }, + { + id: 57, + url: { name: 'components-expansion-panel' }, + icon: 'tabler-fold', + title: 'Expansion Panel', + category: 'userInterface', + }, + { + id: 58, + url: { name: 'components-snackbar' }, + icon: 'tabler-message', + title: 'Snackbar', + category: 'userInterface', + }, + { + id: 59, + url: { name: 'components-tabs' }, + icon: 'tabler-square-plus', + title: 'Tabs', + category: 'userInterface', + }, + { + id: 60, + url: { name: 'components-timeline' }, + icon: 'tabler-timeline-event', + title: 'Timeline', + category: 'userInterface', + }, + { + id: 61, + url: { name: 'components-tooltip' }, + icon: 'tabler-message-chatbot', + title: 'Tooltip', + category: 'userInterface', + }, + { + id: 62, + url: { name: 'forms-textfield' }, + icon: 'tabler-arrow-rotary-last-left', + title: 'TextField', + category: 'formsTables', + }, + { + id: 63, + url: { name: 'forms-select' }, + icon: 'tabler-list-check', + title: 'Select', + category: 'formsTables', + }, + { + id: 64, + url: { name: 'forms-checkbox' }, + icon: 'tabler-checkbox', + title: 'Checkbox', + category: 'formsTables', + }, + { + id: 65, + url: { name: 'forms-radio' }, + icon: 'tabler-circle-dot', + title: 'Radio', + category: 'formsTables', + }, + { + id: 66, + url: { name: 'forms-combobox' }, + icon: 'tabler-checkbox', + title: 'Combobox', + category: 'formsTables', + }, + { + id: 67, + url: { name: 'forms-date-time-picker' }, + icon: 'tabler-calendar', + title: 'Date Time picker', + category: 'formsTables', + }, + { + id: 68, + url: { name: 'forms-textarea' }, + icon: 'tabler-forms', + title: 'Textarea', + category: 'formsTables', + }, + { + id: 70, + url: { name: 'forms-switch' }, + icon: 'tabler-toggle-left', + title: 'Switch', + category: 'formsTables', + }, + { + id: 71, + url: { name: 'forms-file-input' }, + icon: 'tabler-upload', + title: 'File Input', + category: 'formsTables', + }, + { + id: 72, + url: { name: 'forms-rating' }, + icon: 'tabler-star', + title: 'Form Rating', + category: 'formsTables', + }, + { + id: 73, + url: { name: 'forms-slider' }, + icon: 'tabler-hand-click', + title: 'Slider', + category: 'formsTables', + }, + { + id: 74, + url: { name: 'forms-range-slider' }, + icon: 'tabler-adjustments', + title: 'Range Slider', + category: 'formsTables', + }, + { + id: 75, + url: { name: 'forms-form-layouts' }, + icon: 'tabler-box', + title: 'Form Layouts', + category: 'formsTables', + }, + { + id: 76, + url: { name: 'forms-form-validation' }, + icon: 'tabler-checkbox', + title: 'Form Validation', + category: 'formsTables', + }, + { + id: 77, + url: { name: 'charts-apex-chart' }, + icon: 'tabler-chart-line', + title: 'Apex Charts', + category: 'chartsMisc', + }, + { + id: 78, + url: { name: 'charts-chartjs' }, + icon: 'tabler-chart-area', + title: 'ChartJS', + category: 'chartsMisc', + }, + { + id: 79, + url: { name: 'access-control' }, + icon: 'tabler-shield', + title: 'Access Control (ACL)', + category: 'chartsMisc', + }, + { + id: 80, + url: { name: 'pages-dialog-examples' }, + icon: 'tabler-square', + title: 'Dialog Examples', + category: 'appsPages', + }, + { + id: 81, + url: { name: 'forms-custom-input' }, + icon: 'tabler-list-details', + title: 'Custom Input', + category: 'formsTables', + }, + { + id: 82, + url: { name: 'forms-autocomplete' }, + icon: 'tabler-align-left', + title: 'Autocomplete', + category: 'formsTables', + }, + { + id: 83, + url: { name: 'extensions-tour' }, + icon: 'mdi-cube-outline', + title: 'Tour', + category: 'userInterface', + }, + { + id: 84, + url: { name: 'pages-authentication-register-multi-steps' }, + icon: 'tabler-user-plus', + title: 'Register Multi-Steps', + category: 'appsPages', + }, + { + id: 85, + url: { name: 'wizard-examples-checkout' }, + icon: 'tabler-shopping-cart', + title: 'Wizard - Checkout', + category: 'appsPages', + }, + { + id: 86, + url: { name: 'wizard-examples-create-deal' }, + icon: 'tabler-gift', + title: 'Wizard - create deal', + category: 'appsPages', + }, + { + id: 87, + url: { name: 'wizard-examples-property-listing' }, + icon: 'tabler-home', + title: 'Wizard - Property Listing', + category: 'appsPages', + }, + { + id: 88, + url: { name: 'apps-roles' }, + icon: 'tabler-shield', + title: 'Roles', + category: 'appsPages', + }, + { + id: 89, + url: { name: 'apps-permissions' }, + icon: 'tabler-shield', + title: 'Permissions', + category: 'appsPages', + }, + { + id: 90, + url: { name: 'tables-data-table' }, + icon: 'mdi-table', + title: 'Data Table', + category: 'formsTables', + }, + { + id: 91, + url: { name: 'tables-simple-table' }, + icon: 'mdi-table', + title: 'Simple Table', + category: 'formsTables', + }, +] + + +// ** GET Search Data +// eslint-disable-next-line sonarjs/cognitive-complexity +mock.onGet('/app-bar/search').reply(config => { + const { q = '' } = config.params + const queryLowered = q.toLowerCase() + + const exactData = { + dashboards: [], + appsPages: [], + userInterface: [], + formsTables: [], + chartsMisc: [], + } + + const includeData = { + dashboards: [], + appsPages: [], + userInterface: [], + formsTables: [], + chartsMisc: [], + } + + database.forEach(obj => { + const isMatched = obj.title.toLowerCase().startsWith(queryLowered) + if (isMatched && exactData[obj.category].length < 5) + exactData[obj.category].push(obj) + }) + database.forEach(obj => { + const isMatched = !obj.title.toLowerCase().startsWith(queryLowered) && obj.title.toLowerCase().includes(queryLowered) + if (isMatched && includeData[obj.category].length < 5) + includeData[obj.category].push(obj) + }) + + const categoriesCheck = [] + + Object.keys(exactData).forEach(category => { + if (exactData[category].length > 0) + categoriesCheck.push(category) + }) + if (categoriesCheck.length === 0) { + Object.keys(includeData).forEach(category => { + if (includeData[category].length > 0) + categoriesCheck.push(category) + }) + } + const resultsLength = categoriesCheck.length === 1 ? 5 : 3 + const mergedData = [] + + Object.keys(exactData).forEach(element => { + if (exactData[element].length || includeData[element].length) { + const r = exactData[element].concat(includeData[element]).slice(0, resultsLength) + + r.unshift({ header: element, title: element }) + mergedData.push(...r) + } + }) + + return [200, [...mergedData]] +}) diff --git a/resources/js/@fake-db/apps/chat.js b/resources/js/@fake-db/apps/chat.js new file mode 100644 index 0000000..33f36d6 --- /dev/null +++ b/resources/js/@fake-db/apps/chat.js @@ -0,0 +1,385 @@ +import mock from '@/@fake-db/mock' +import { genId } from '@/@fake-db/utils' + +// Images +import avatar1 from '@images/avatars/avatar-1.png' +import avatar2 from '@images/avatars/avatar-2.png' +import avatar3 from '@images/avatars/avatar-3.png' +import avatar4 from '@images/avatars/avatar-4.png' +import avatar5 from '@images/avatars/avatar-5.png' +import avatar6 from '@images/avatars/avatar-6.png' +import avatar8 from '@images/avatars/avatar-8.png' + +const previousDay = new Date(new Date().getTime() - 24 * 60 * 60 * 1000) +const dayBeforePreviousDay = new Date(new Date().getTime() - 24 * 60 * 60 * 1000 * 2) + +const database = { + profileUser: { + id: 11, + avatar: avatar1, + fullName: 'John Doe', + role: 'admin', + about: 'Dessert chocolate cake lemon drops jujubes. Biscuit cupcake ice cream bear claw brownie marshmallow.', + status: 'online', + settings: { + isTwoStepAuthVerificationEnabled: true, + isNotificationsOn: false, + }, + }, + contacts: [ + { + id: 1, + fullName: 'Gavin Griffith', + role: 'Frontend Developer', + about: 'Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing', + avatar: avatar5, + status: 'offline', + }, + { + id: 2, + fullName: 'Harriet McBride', + role: 'UI/UX Designer', + about: 'Toffee caramels jelly-o tart gummi bears cake I love ice cream lollipop. Sweet liquorice croissant candy danish dessert icing. Cake macaroon gingerbread toffee sweet.', + avatar: avatar2, + status: 'busy', + }, + { + id: 3, + fullName: 'Danny Conner', + role: 'Town planner', + about: 'Soufflé soufflé caramels sweet roll. Jelly lollipop sesame snaps bear claw jelly beans sugar plum sugar plum.', + avatar: '', + status: 'busy', + }, + { + id: 4, + fullName: 'Janie West', + role: 'Data scientist', + about: 'Chupa chups candy canes chocolate bar marshmallow liquorice muffin. Lemon drops oat cake tart liquorice tart cookie. Jelly-o cookie tootsie roll halvah.', + avatar: '', + status: 'online', + }, + { + id: 5, + fullName: 'Bryan Murray', + role: 'Dietitian', + about: 'Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing', + avatar: avatar5, + status: 'busy', + }, + { + id: 6, + fullName: 'Albert Underwood', + role: 'Marketing executive', + about: 'Toffee caramels jelly-o tart gummi bears cake I love ice cream lollipop. Sweet liquorice croissant candy danish dessert icing. Cake macaroon gingerbread toffee sweet.', + avatar: avatar6, + status: 'online', + }, + { + id: 7, + fullName: 'Adele Ross', + role: 'Special educational needs teacher', + about: 'Biscuit powder oat cake donut brownie ice cream I love soufflé. I love tootsie roll I love powder tootsie roll.', + avatar: '', + status: 'online', + }, + { + id: 8, + fullName: 'Mark Berry', + role: 'Advertising copywriter', + about: 'Bear claw ice cream lollipop gingerbread carrot cake. Brownie gummi bears chocolate muffin croissant jelly I love marzipan wafer.', + avatar: avatar3, + status: 'away', + }, + { + id: 9, + fullName: 'Joseph Evans', + role: 'Designer, television/film set', + about: 'Gummies gummi bears I love candy icing apple pie I love marzipan bear claw. I love tart biscuit I love candy canes pudding chupa chups liquorice croissant.', + avatar: avatar8, + status: 'offline', + }, + { + id: 10, + fullName: 'Blake Carter', + role: 'Building surveyor', + about: 'Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing', + avatar: avatar4, + status: 'away', + }, + ], + chats: [ + { + id: 1, + userId: 2, + unseenMsgs: 0, + messages: [ + { + message: 'Hi', + time: 'Mon Dec 10 2018 07:45:00 GMT+0000 (GMT)', + senderId: 11, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'Hello. How can I help You?', + time: 'Mon Dec 11 2018 07:45:15 GMT+0000 (GMT)', + senderId: 2, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'Can I get details of my last transaction I made last month? 🤔', + time: 'Mon Dec 11 2018 07:46:10 GMT+0000 (GMT)', + senderId: 11, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'We need to check if we can provide you such information.', + time: 'Mon Dec 11 2018 07:45:15 GMT+0000 (GMT)', + senderId: 2, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'I will inform you as I get update on this.', + time: 'Mon Dec 11 2018 07:46:15 GMT+0000 (GMT)', + senderId: 2, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'If it takes long you can mail me at my mail address.', + time: String(dayBeforePreviousDay), + senderId: 11, + feedback: { + isSent: true, + isDelivered: false, + isSeen: false, + }, + }, + ], + }, + { + id: 2, + userId: 1, + unseenMsgs: 1, + messages: [ + { + message: 'How can we help? We\'re here for you!', + time: 'Mon Dec 10 2018 07:45:00 GMT+0000 (GMT)', + senderId: 11, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'Hey John, I am looking for the best admin template. Could you please help me to find it out?', + time: 'Mon Dec 10 2018 07:45:23 GMT+0000 (GMT)', + senderId: 1, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'It should use nice Framework.', + time: 'Mon Dec 10 2018 07:45:55 GMT+0000 (GMT)', + senderId: 1, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'Absolutely!', + time: 'Mon Dec 10 2018 07:46:00 GMT+0000 (GMT)', + senderId: 11, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'Our admin is the responsive admin template.!', + time: 'Mon Dec 10 2018 07:46:05 GMT+0000 (GMT)', + senderId: 11, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'Looks clean and fresh UI. 😍', + time: 'Mon Dec 10 2018 07:46:23 GMT+0000 (GMT)', + senderId: 1, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'It\'s perfect for my next project.', + time: 'Mon Dec 10 2018 07:46:33 GMT+0000 (GMT)', + senderId: 1, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'How can I purchase it?', + time: 'Mon Dec 10 2018 07:46:43 GMT+0000 (GMT)', + senderId: 1, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'Thanks, From our official site 😇', + time: 'Mon Dec 10 2018 07:46:53 GMT+0000 (GMT)', + senderId: 11, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + { + message: 'I will purchase it for sure. 👍', + time: String(previousDay), + senderId: 1, + feedback: { + isSent: true, + isDelivered: true, + isSeen: true, + }, + }, + ], + }, + ], +} + + +// ------------------------------------------------ +// GET: Return Chats Contacts and Contacts +// ------------------------------------------------ +mock.onGet('/apps/chat/chats-and-contacts').reply(config => { + const { q = '' } = config.params + const qLowered = q.toLowerCase() + + const chatsContacts = database.chats + .map(chat => { + const contact = JSON.parse(JSON.stringify(database.contacts.find(c => c.id === chat.userId))) + + contact.chat = { id: chat.id, unseenMsgs: chat.unseenMsgs, lastMessage: chat.messages.at(-1) } + + return contact + }) + .reverse() + + const profileUserData = database.profileUser + + const response = { + chatsContacts: chatsContacts.filter(c => c.fullName.toLowerCase().includes(qLowered)), + contacts: database.contacts.filter(c => c.fullName.toLowerCase().includes(qLowered)), + profileUser: profileUserData, + } + + return [200, response] +}) + +// ------------------------------------------------ +// GET: Return Single Chat +// ------------------------------------------------ +mock.onGet('/apps/chat/users/profile-user').reply(() => [200, database.profileUser]) + +// ------------------------------------------------ +// GET: Return Single Chat +// ------------------------------------------------ +mock.onGet(/\/apps\/chat\/chats\/\d+/).reply(config => { + // Get user id from URL + const userId = Number(config.url?.substring(config.url.lastIndexOf('/') + 1)) + const chat = database.chats.find(c => c.userId === userId) + if (chat) + chat.unseenMsgs = 0 + + return [ + 200, + { + chat, + contact: database.contacts.find(c => c.id === userId), + }, + ] +}) + +// ------------------------------------------------ +// POST: Add new chat message +// ------------------------------------------------ +mock.onPost(/\/apps\/chat\/chats\/\d+/).reply(config => { + // Get user id from URL + const contactId = Number(config.url?.substring(config.url.lastIndexOf('/') + 1)) + + // Get message from post data + const { message, senderId } = JSON.parse(config.data) + let activeChat = database.chats.find(chat => chat.userId === contactId) + + const newMessageData = { + message, + time: String(new Date()), + senderId, + feedback: { + isSent: true, + isDelivered: false, + isSeen: false, + }, + } + + + // If there's new chat for user create one + let isNewChat = false + if (activeChat === undefined) { + isNewChat = true + database.chats.push({ + id: genId(database.chats), + userId: contactId, + unseenMsgs: 0, + messages: [], + }) + activeChat = database.chats.at(-1) + } + else { + activeChat.messages.push(newMessageData) + } + const response = { msg: newMessageData } + if (isNewChat) + response.chat = activeChat + + return [201, response] +}) diff --git a/resources/js/@fake-db/apps/email.js b/resources/js/@fake-db/apps/email.js new file mode 100644 index 0000000..a305adf --- /dev/null +++ b/resources/js/@fake-db/apps/email.js @@ -0,0 +1,2139 @@ +import txt from '@images/icons/file/txt.png' +import avatar1 from '@images/avatars/avatar-1.png' +import avatar2 from '@images/avatars/avatar-2.png' +import avatar3 from '@images/avatars/avatar-3.png' +import avatar4 from '@images/avatars/avatar-4.png' +import avatar5 from '@images/avatars/avatar-5.png' +import avatar6 from '@images/avatars/avatar-6.png' +import avatar7 from '@images/avatars/avatar-7.png' +import avatar8 from '@images/avatars/avatar-8.png' +import mock from '@/@fake-db/mock' +import xls from '@images/icons/file/xls.png' + +const data = { + emails: [ + { + id: 50, + to: [ + { + email: 'johndoe@mail.com', + name: 'me', + }, + ], + from: { + email: 'james25@gmail.com', + name: 'Katie Brandt', + avatar: avatar8, + }, + subject: 'Bring smile discussion same boy include care.', + cc: [], + bcc: [], + message: '

Guy national course pay small per. Commercial research lose key fight marriage. Young series raise degree foot degree detail number.\nCrime gas real pass white. Television success east.

Into miss knowledge result. Seat carry tax beat line. Amount language paper machine fly.\nMusic several common former. More mouth year site move hold. Billion material born news western late.

World them term identify. Rule southern condition thought. Article successful traditional friend.\nPhone financial skill theory.\nChange Mr experience. Everyone help structure much family.\nVoice general group likely.

', + attachments: [ + { + fileName: 'log.txt', + thumbnail: txt, + url: '', + size: '5mb', + }, + { + fileName: 'performance.xls', + thumbnail: xls, + url: '', + size: '10mb', + }, + ], + isStarred: true, + labels: ['private', 'company'], + time: '2021-07-14T12:42:22', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 49, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'jamesskinner@hotmail.com', + name: 'Joshua Cline', + avatar: avatar1, + }, + subject: 'Magazine say side view.', + cc: [], + bcc: [], + message: '

Campaign even order for color. Remember card return position white argue prepare. Case fill follow then condition investment why.\nCold son pattern wife. Child name interest company thought every federal. He catch daughter design.

Affect customer a. Which difficult science.\nReality whether what animal. Call report author against season heart.\nCatch have always source response your even. Person mother whether since clearly. Cut staff work the nothing.

Cell cover along school. Method option not why laugh. Nation medical thousand world rule.\nEvening fish rich sense create. Civil family particularly day machine free read. Interesting capital owner international nor condition.

', + attachments: [], + isStarred: false, + labels: ['personal'], + time: '2021-07-16T01:23:14', + replies: [ + { + id: 74474, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'teresa54@gmail.com', + name: 'Brittany Young', + avatar: avatar2, + }, + subject: 'The beat save none make sea large number.', + cc: [], + bcc: [], + message: '

College before employee recognize. Teach central this interest service party section. Floor west break bit suggest ok everyone.\nPm quality school out form. Want case town individual.

Hundred a modern career whose know find responsibility. East option trouble next. Sport goal after race pull political common board.

Beat support exactly material fact benefit six. Time represent stuff forget plant pass team. Begin lot war field simple.\nBuilding development wear trip marriage. Economy speech be election arrive color next.

', + attachments: [], + isStarred: false, + labels: ['company', 'private', 'personal', 'important'], + time: '2021-07-21T18:43:19', + replies: [], + folder: 'inbox', + isRead: false, + isDeleted: false, + }, + { + id: 766, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'angelajimenez@yahoo.com', + name: 'Emily Moore', + avatar: avatar8, + }, + subject: 'Movement along college bad reality scientist.', + cc: [], + bcc: [], + message: '

Goal reveal past rule arrive project performance. Learn despite the way. Operation within suggest glass beautiful always really.\nLanguage although cut network conference economy long. Forward us point meet. Sing buy central quality science.

Real keep material wind drive life. Job election to determine table within expert art.\nOften ten ask city. Memory to run market.\nMove theory contain good fire. Area walk position site would.

Seem response base question tough consumer another. Sit hard deep child operation institution. Charge child picture different sense.\nMedia remain could go eight different west. Thousand fly box else.

', + attachments: [], + isStarred: true, + labels: ['personal'], + time: '2021-07-07T22:12:32', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 3718436, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'alexanderpatel@yahoo.com', + name: 'Andrew Cruz', + avatar: avatar7, + }, + subject: 'Realize agree dark spring suddenly maintain level history.', + cc: [], + bcc: [], + message: '

Actually and them time itself newspaper stand. Billion Republican manager little hot store. Pull issue many close by large seven.\nIt writer will concern community rate through factor. Reduce south director budget shake return.

Score event since campaign single conference significant. Design fall teacher.\nWhich themselves along that themselves activity.\nUntil nothing cold toward politics product. Rock enter in what option.

Relate authority agency claim protect. Task not wait respond week hotel.\nAt catch matter try boy why white physical. Section protect try kind few. Skin two author style.\nWestern simple instead strategy mention item suffer. Remain agree account.

', + attachments: [], + isStarred: true, + labels: ['company', 'private'], + time: '2021-07-18T19:27:18', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 48, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'williamsstacey@yahoo.com', + name: 'Jonathan Walker', + avatar: avatar1, + }, + subject: 'Then until task something before color impact now.', + cc: [], + bcc: [], + message: '

Head claim property experience arm remain structure. Worry do science look oil easy. His whose want.\nHuge protect foot save century somebody future. Skin building truth along sing such read speech.\nRaise argue everything send.

While attorney to power card. Agent card big nothing. Wall behavior investment stay relate stage their. Carry full rather product arrive center when.

Law chance mention sound maintain expect whose. Treatment simply if power decide bar. Theory building laugh hand manager condition true.\nFoot few eat store environment that involve man. Into report player writer yourself.

', + attachments: [], + isStarred: false, + labels: ['company', 'private', 'important'], + time: '2021-07-22T09:59:40', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 47, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'lori82@gmail.com', name: 'Kevin Evans', avatar: avatar7 }, + subject: 'Shoulder science point show human black answer anything.', + cc: [], + bcc: [], + message: '

Rate church step beat head class nor.\nLeg ten offer girl me teach. Quite could within. Bill civil situation to.\nDifference unit tax garden. Fine cause political center her. Design look free treat item ball.

Also night argue I explain time practice.\nCommercial example reveal window try door great material. Wear data loss. Visit prove either catch will.

Show young century between box. Statement go guess bad film.\nIdea voice by audience meet everyone next prove. Art leader minute build.

', + attachments: [], + isStarred: true, + labels: ['personal'], + time: '2021-07-09T15:02:15', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 46, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'crussell@hotmail.com', name: 'Alexa Burnett', avatar: avatar4 }, + subject: 'Want manager source car recognize character impact.', + cc: [], + bcc: [], + message: '

Mr attorney role meeting enter.\nMajor serve night often. Region current nation.\nHear each knowledge today. Church positive let anyone hospital member difficult color. Product difference such sea view senior.

Home require nor material current. State probably customer size soldier music site.\nSeveral east when miss partner language hotel ask. She hold turn. Century general study radio.

Old community prevent. Subject minute song sport.\nCover woman born decision agree center cold.

', + attachments: [], + isStarred: false, + labels: ['personal'], + time: '2021-07-09T06:52:08', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 45, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'kkim@yahoo.com', name: 'Dominique Paul', avatar: avatar4 }, + subject: 'Level within enjoy baby.', + cc: [], + bcc: [], + message: '

Opportunity meet author table pressure leader. Owner never investment recent nearly before. Whom ask road.\nBody attorney clear program tonight current. Name watch school hard fly.

People crime window talk. Cell should third have sit would.\nOccur hit take.\nFact go system really entire common. Fast organization could themselves continue. End ahead rather.

Action quickly hundred movie choice. Nice yes lose two. Stay practice section onto some firm little Republican.\nLarge fast politics what. Common price speak sign particularly answer. You simply certain which direction.

', + attachments: [], + isStarred: false, + labels: ['important', 'personal'], + time: '2021-07-15T14:59:01', + replies: [ + { + id: 781, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'amyconner@hotmail.com', + name: 'Michael Martinez', + avatar: avatar3, + }, + subject: 'Recent seek particularly seem southern charge.', + cc: [], + bcc: [], + message: '

Accept his citizen again anyone. Claim process watch.\nSeven court there. Local author line would real machine officer.\nPlant just benefit operation. Similar soldier wrong part hospital action drive.

Before throw enough goal different. Doctor remain Mrs political staff.\nSeem successful why check after best pass. Degree because prove church move center space often.

Might trade cell guess institution. Difference win again.\nCulture life car agency improve you. Thing also hold child apply south box appear. Education itself effort their.\nFast save pull deal his talk issue. Fall sport better step.

', + attachments: [], + isStarred: true, + labels: ['important', 'personal'], + time: '2021-07-14T21:30:32', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 6933053, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'ivanguerrero@yahoo.com', + name: 'Ashley Fuller', + avatar: avatar4, + }, + subject: 'Difference owner claim student site property would.', + cc: [], + bcc: [], + message: '

Hour town against move difference scene cause walk.\nAgreement bag accept society story generation.\nLike process floor lose.\nStop think work off once. Billion institution anyone stuff determine federal attention.

Notice ever same tonight away performance role increase. Continue best same candidate expect look. Feeling church whole case risk town boy language.\nManage may send rate among. Physical law risk final source. Matter star ago or at possible.

Hotel I energy piece drop. Learn southern by maintain often evening.\nLate rise husband top skin memory lot.\nTest compare strategy father. Everyone few actually this again minute become.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-11T10:46:10', + replies: [], + folder: 'draft', + isRead: false, + isDeleted: false, + }, + { + id: 8, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'lmoreno@yahoo.com', name: 'Ashley Lewis', avatar: avatar4 }, + subject: 'Candidate available material away.', + cc: [], + bcc: [], + message: '

Ten spend paper. Trial certain those. Make middle campaign might.\nEffect well accept great wall.\nSeem your at small. So his serious high center political man.

Guess staff argue ready trade whole including. Science four skill best level interesting prevent. Mind he recent another point understand.\nAsk daughter specific hot without body challenge. Official threat pretty left bar check believe bit.

Trouble result receive political.\nAvailable knowledge increase. Dog computer ability prove paper. Scientist either color capital fall do.\nShoulder bar small. Those thank beyond sea piece.

', + attachments: [], + isStarred: false, + labels: ['important', 'personal'], + time: '2021-07-02T03:06:42', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 784835803, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'mortiz@gmail.com', name: 'Bradley Hobbs', avatar: avatar7 }, + subject: 'Tend picture church team place show society.', + cc: [], + bcc: [], + message: '

Republican risk window. Different speak prove college from push main.\nRegion experience field wind get choose. Away drug professional memory. Nation still best fact forget election smile. Sure ready security office question.

Appear civil appear movie space.\nAmount rule meet wide exactly theory be. Pretty Republican material human that. Page war fear pay.\nAgree fall investment red nothing go also. Expect join against threat and.

Serve writer leader room.\nPurpose high west lose firm. Mouth between myself get upon avoid power low.\nSurface particular be main yeah. Huge parent morning continue research.

', + attachments: [], + isStarred: false, + labels: ['personal', 'private', 'important'], + time: '2021-07-04T08:11:01', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + ], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 44, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'ethan27@gmail.com', name: 'Daniel Sullivan', avatar: avatar7 }, + subject: 'Choose security yes relationship recognize consumer democratic international.', + cc: [], + bcc: [], + message: '

Person whom reflect prove show.\nBreak exist which prepare. Collection she population understand result business ability site.\nFact figure recent population condition. What west grow food space former.

Individual catch management her skin bag specific. Order base project under. Minute watch continue relationship state continue this store.

Recent cut organization machine.\nEnter today growth five interest some. Million official middle space return. Second cold available seven behind protect owner.

', + attachments: [], + isStarred: false, + labels: ['important', 'personal'], + time: '2021-07-12T02:53:08', + replies: [ + { + id: 23853, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'hubbardsharon@hotmail.com', + name: 'Laura Dominguez', + avatar: avatar8, + }, + subject: 'Daughter skill fact rise nice power.', + cc: [], + bcc: [], + message: '

Concern enter model team want admit detail far. West TV themselves short friend agreement service.\nAccording toward free upon draw family state. Account or action president piece.\nCause part fight second. Natural international mean.

Lay nearly center hear ten season officer water. Pattern loss window follow sure line.\nGlass analysis seat have. Ok budget among moment sing four.

Product now material play pick deal determine suffer. Most second region represent.\nRich reduce evidence home nothing yeah pressure. Rule play between fast wrong place.\nEvidence color anything because. Wall start manage style central charge beyond.

', + attachments: [], + isStarred: false, + labels: ['company', 'important'], + time: '2021-07-12T20:13:42', + replies: [], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + { + id: 317, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'jameslopez@yahoo.com', + name: 'Christopher Farrell', + avatar: avatar3, + }, + subject: 'Character fall follow.', + cc: [], + bcc: [], + message: '

Hope bar civil. Final design section those.\nBrother sit many receive vote read large. Reflect evening man realize detail. Party yeah factor never guy.\nSouthern movement everything. Play although movie effect space front.

Front first say interesting million force issue so. Enjoy least Democrat strong dark. Parent business bill surface arrive daughter.\nUntil home successful might capital. Nearly issue free customer. Carry matter executive country human shake.

Key do choose however.\nDiscuss each police modern. Apply method speech population participant.

', + attachments: [], + isStarred: true, + labels: ['personal'], + time: '2021-07-22T15:28:46', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 43, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'rmartin@gmail.com', name: 'Nicole Allen', avatar: avatar4 }, + subject: 'Ten store nature surface that seek black return.', + cc: [], + bcc: [], + message: '

Recognize section different ground million. Source court seek street.\nScience thank two capital shoulder herself certainly. Individual hair general manager why.

Live hear lawyer quickly player system. American spend ok beautiful. Shoulder drug itself wrong partner event.\nInclude account water success political. Newspaper quality really road. Short maintain raise appear.

Move cultural others protect season he future. Argue glass loss whether available size apply government.\nFood hand night particular. Change few key would thus.\nGreen talk to improve miss.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-27T07:21:36', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 42, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'adkinsryan@yahoo.com', name: 'Karen Russell', avatar: avatar6 }, + subject: 'Along represent responsibility security he leg.', + cc: [], + bcc: [], + message: '

Explain through thought forward daughter entire. Investment direction great writer thus blue provide.\nPersonal she community phone same. Remain religious meeting. Data personal meeting agreement style. Next time build.

Avoid board beautiful strong effect. We star fight quality stay sense soldier. Her social month. System professional social.\nYoung back including benefit position plan just. Line history sometimes check need remain make.

Radio should magazine yard ahead then. Student knowledge cover general use though.\nEnergy agree away team. Power whose music sort between man analysis. Boy election value.\nClearly law avoid dream. Would around role third seek world present.

', + attachments: [], + isStarred: false, + labels: ['personal'], + time: '2021-07-02T20:02:30', + replies: [ + { + id: 82117976, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'oblack@hotmail.com', + name: 'Michael Mccarthy', + avatar: avatar3, + }, + subject: 'Around impact point interest method.', + cc: [], + bcc: [], + message: '

Blood power job common. Spring success arm article. Continue manager blue new enough business six difference.\nMe finish pick energy wear him home. If affect ready east. Light enter speech many off day answer.

Quality consider statement building suddenly poor. Indeed because image month charge pressure lawyer. Color lot subject leg.\nUs cold everybody clearly evening ago apply. Run between pull. Could amount policy think second take born draw.

Rest feel forget garden tough citizen him. Sign court point recent.\nClaim wide chair plant. Smile build everyone politics run.\nFactor trip personal.

', + attachments: [], + isStarred: false, + labels: ['private', 'important', 'company'], + time: '2021-07-04T15:30:03', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 3151, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'zjackson@hotmail.com', + name: 'Don Stewart', + avatar: avatar7, + }, + subject: 'Nation campaign still never church politics business.', + cc: [], + bcc: [], + message: '

Leg simple region out compare include wide. Simply kid away person training how. Answer laugh build attention cell authority be.\nPolitical citizen soldier record score green consider. Catch result traditional debate subject finally security.

Model seek stand fish three. View might space.\nSection receive fire town prepare public camera order. Sometimes nice another realize level. Shake fill institution forward author matter same.

Too home after lay senior. Result agree strong finish should easy onto agreement. Size PM usually war recent raise tend use.\nWork section story six billion. Long would add film middle financial third. Citizen up debate room owner deal.

', + attachments: [], + isStarred: false, + labels: ['private', 'company'], + time: '2021-07-08T17:55:49', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 600, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'mary98@yahoo.com', name: 'Dana Harvey', avatar: avatar2 }, + subject: 'Both community term run maybe sort per.', + cc: [], + bcc: [], + message: '

Watch great himself all. Court such building kid from region. Reveal team poor lawyer theory listen.\nSon participant very better. Bed city dog sign.\nBall despite player whatever whatever opportunity. Training social kitchen blood fly.

May hit expert last. Attention opportunity shoulder. Agency federal just candidate study long.\nNotice first work full write recognize probably. Once writer common low last.

Hour about entire material. Various from subject military read safe seat. Truth third spend hair role home. Any herself analysis pay.\nGame get class ever enter once its. Job street student ok.

', + attachments: [], + isStarred: true, + labels: ['important', 'private', 'personal', 'company'], + time: '2021-07-27T16:48:17', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 23080678, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'ppineda@yahoo.com', + name: 'Samantha Martin', + avatar: avatar4, + }, + subject: 'Whether far ready success yes many window.', + cc: [], + bcc: [], + message: '

Cause have like. Unit nearly view feeling arrive player. Nor officer she production fly nice begin value.\nBehavior trade focus any. Or economy information class blue school structure everything. Production white although her total natural space.

Recognize section and tend. Understand box option agency event drive window. Child himself during statement financial under. Drug daughter attention magazine window go red.

Because drop measure I significant. Fall type us a staff wind court. Student discuss pattern way.\nPlan should book. Lead decide radio ok foreign behavior bit style.\nHundred no dream smile. Whose put indeed medical.

', + attachments: [], + isStarred: true, + labels: ['personal', 'company'], + time: '2021-07-04T13:28:16', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 41, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'julie16@yahoo.com', name: 'Tyler Hernandez', avatar: avatar3 }, + subject: 'Environment success however window student.', + cc: [], + bcc: [], + message: '

While million social ball surface in late.\nBudget though five so fund purpose.\nBall understand effect teach. Find charge rich child. Do require laugh everybody interesting.

Season south town performance whole political thought box. Management try just president. Finish fish strong teach enter ahead.\nBehind unit difference expert position two. Let before account baby cut should TV. Explain effort realize need.

Even item or environment save ten prepare activity. Nearly become current.\nBed nature indicate discussion least career perhaps. Head must sure. Why sea around buy. Audience politics sell strong career.

', + attachments: [], + isStarred: false, + labels: ['important', 'private', 'company'], + time: '2021-07-06T01:09:02', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 40, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'brownsandy@gmail.com', name: 'Michael Smith', avatar: avatar3 }, + subject: 'Miss strategy want author test.', + cc: [], + bcc: [], + message: '

Hear college professor see agent believe easy. Front test big black shoulder although. Candidate skill every player pressure.\nMany six reason allow kitchen. Respond us bank idea treat sure stuff tonight.

Nothing stay medical strategy early position maybe buy. Turn board early. Particularly then care value should material.\nSong doctor phone offer. Lawyer fear say discussion result represent. Performance back when cover effort.

Determine huge with newspaper computer focus detail trouble. Move support strong certainly.\nPopulation administration thing fund push movie democratic community. Town next wonder.

', + attachments: [], + isStarred: true, + labels: ['private', 'important'], + time: '2021-07-05T16:18:51', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 39, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'marissa73@hotmail.com', name: 'James Russell', avatar: avatar3 }, + subject: 'Interview some and minute.', + cc: [], + bcc: [], + message: '

Result last clearly should bad. Need management account other player. Time pressure beautiful teacher provide. Mouth senior explain official would exactly.

Management attack fight some item. Once century agent method section what. What their defense you. Factor civil significant enough plan different.

Body amount know condition own gas near state. Strong as black place service.\nSignificant all game. Drive assume from wear option.

', + attachments: [], + isStarred: false, + labels: ['company', 'private', 'important'], + time: '2021-07-19T05:03:32', + replies: [], + folder: 'inbox', + isRead: false, + isDeleted: false, + }, + { + id: 38, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'brendajames@hotmail.com', + name: 'Richard Spencer', + avatar: avatar7, + }, + subject: 'Town baby them account house save prevent.', + cc: [], + bcc: [], + message: '

Pattern sing wrong late north.\nDeal age risk yourself mission able car defense. Choice audience determine dream spend Congress. Mrs produce everyone who bed civil.

Forget top well little door at share. Money leg recently from make will radio.

Result plant rich tonight here discussion draw during. Population play serious their bill. Reduce industry right remember attorney them too.\nFirst once over yard. Standard so low.

', + attachments: [], + isStarred: true, + labels: ['company', 'important'], + time: '2021-07-16T20:40:12', + replies: [ + { + id: 1245629, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'kellyjones@yahoo.com', + name: 'Mark Martinez', + avatar: avatar5, + }, + subject: 'Movement risk cultural.', + cc: [], + bcc: [], + message: '

Both statement now painting decade guess commercial. Treatment movement over idea drop house expect. Heart sense agree live amount her.\nAuthority data Mr all day stock star. By shake seem shoulder not myself order. Out concern from reach.

Me worry field three name. Mr history when across around. Garden think rate central challenge guess structure.\nCall difficult relationship house around. Water public maintain. Our myself yet personal government condition.

Themselves final admit from staff conference no. Ask certain summer set purpose. Budget cost enter town most trip. Most your keep he the power.\nTrip news couple.

', + attachments: [], + isStarred: false, + labels: ['personal'], + time: '2021-07-11T06:55:40', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 1, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'sonyamccall@hotmail.com', + name: 'Lisa Richardson', + avatar: avatar4, + }, + subject: 'Despite produce officer ground employee president.', + cc: [], + bcc: [], + message: '

Understand conference debate. Among call fear away. Represent camera show job range street.\nInterview continue ahead believe subject. Himself sit them bit with bring. Oil particular represent wish home.

Your action note rise can food change. Eat claim plant accept wear film available few. Human wind security protect camera line.\nNotice deal to about truth forget every. Dark me camera where different better. Dog involve serve indicate do share for.

Sort all want oil travel need.\nBag contain hold deal individual pick believe ago. Middle oil receive close fact read. Offer often painting identify sure.\nLearn show next. Learn consider view face. Only life study near.

', + attachments: [], + isStarred: true, + labels: ['important', 'private', 'company'], + time: '2021-07-04T15:24:04', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + ], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 37, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'juliasosa@hotmail.com', name: 'Cheryl Wright', avatar: avatar6 }, + subject: 'Movie admit final enjoy particular.', + cc: [], + bcc: [], + message: '

Poor bad find. Report TV over long region defense.\nTwo sister according alone. Natural great before prove north assume become focus. Including work environment water poor.

Score though true evening again analysis feeling wait. Certain discover carry chance ever. Rich staff test raise discover.\nBoard federal improve bad impact eat box word. Situation blue culture environment road city soon.

Decade subject another our million or. Be stock interview easy those population maybe. Help send society. Win many team find.\nManagement about guy. Cultural resource prevent natural age tree reduce. Effect carry man.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-28T11:05:28', + replies: [ + { + id: 3558, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'dillon01@hotmail.com', + name: 'Brenda Navarro', + avatar: avatar8, + }, + subject: 'Business key Democrat sing.', + cc: [], + bcc: [], + message: '

Meeting carry shake turn. Money because radio lawyer better. World trial view benefit result someone sort expert. American while public question.

Court ask various serious safe. Cup than hot child sort.\nSmile view issue high recently develop floor. Ten science including force message. Hear room author return risk military.

Unit vote popular collection strategy group. Newspaper region fly structure seem story art. Skill ever as money meet involve.\nAs environmental sister investment film represent. Until student occur include few science.

', + attachments: [], + isStarred: true, + labels: ['important', 'personal'], + time: '2021-07-20T02:27:59', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 21238317, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'danielle69@yahoo.com', + name: 'Erica Miller', + avatar: avatar2, + }, + subject: 'Way program finish type yes then area.', + cc: [], + bcc: [], + message: '

Project growth brother. Star capital bring.\nRange movement risk perhaps loss. Team raise card bag hit.\nParticularly last lead system within walk public perhaps. Tax travel suggest physical data company. Mrs fear establish away.

Treatment fight as foot Republican. Sister happy major I well less fish. Various goal face up. Age put head hotel style tree.\nSurface list evening this stay. Doctor stage would current. Wide audience after paper. Process yard end man future lead.

Moment push store necessary program. Have health seek. Name safe young career those agent.\nBe protect whatever skin. Read by talk we start. Might author final perform. Tv own follow wife either husband.

', + attachments: [], + isStarred: false, + labels: ['company', 'private', 'important'], + time: '2021-07-06T20:33:50', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 36, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'benjamin30@gmail.com', name: 'Pamela Mueller', avatar: avatar6 }, + subject: 'Dinner start pretty.', + cc: [], + bcc: [], + message: '

Require might team under authority.\nCustomer value still number deal. Cell both type customer do. Congress opportunity subject.

Above threat security how. Worry too interesting especially government help instead.\nWide ability study oil training teach. Help lot tree recent admit lot business.\nCapital order himself fall rest room those.

Impact beat business hear pretty. Current professor nearly agency. Attorney education fish result move.\nFormer military bar middle PM back his. Play nature image matter pick. Standard job smile food.

', + attachments: [], + isStarred: false, + labels: ['company', 'personal'], + time: '2021-07-03T05:40:50', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 35, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'dana33@yahoo.com', name: 'Becky Coffey', avatar: avatar2 }, + subject: 'Less forget everything only girl.', + cc: [], + bcc: [], + message: '

His exactly require able. Team become friend chair between within. Employee program power science eight guy dark.

Crime his teacher imagine outside energy recent. Building week short brother many enter measure. Approach better them area deep.\nChild gas yard character. To management mother never own arm key. Trouble three speech cover feel listen.

Future north quite partner interesting. Interview investment clear industry Democrat investment. Even ahead identify.\nThese character threat next help include. Offer contain necessary something all. Reflect growth quickly part rate create question.

', + attachments: [], + isStarred: false, + labels: ['important', 'company'], + time: '2021-07-08T06:53:31', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 34, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'hollandjulie@yahoo.com', + name: 'Christopher Evans DDS', + avatar: avatar5, + }, + subject: 'Financial series artist region.', + cc: [], + bcc: [], + message: '

Peace approach ask course central reality. Decision PM standard production brother report federal its. Wonder common group current often vote.

Professional sure fear blood much question. Operation ever authority water the woman. Hospital second rich let.\nOpportunity actually decision positive. During beautiful today decide know those. Chance list many create including become instead with.

Feel put treat. Mention arm name bank side.\nWhy area language reach well. Mother Mr worry order example.\nBegin part stay culture tend. Strategy administration yeah woman measure air. Than exist with indeed population talk.

', + attachments: [], + isStarred: true, + labels: ['personal', 'private'], + time: '2021-07-12T11:07:10', + replies: [ + { + id: 689385, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'wilsonroy@gmail.com', + name: 'Chelsea Sims', + avatar: avatar4, + }, + subject: 'Prove rest forward wear.', + cc: [], + bcc: [], + message: '

Just trip own remember change these part. Trip success network send not room half yet. Floor pay which expert service.\nWhile both throw sister.\nCertainly remember certain country both. How seat exist. Hundred wind in page.

Trouble them least control. Forget up scene training garden. Effect for risk remain sign.\nSouthern bill blue general usually end how admit. Whom view final pay population reason. Type fire million on section individual.

Business specific prepare machine. Area stage poor pull. Performance myself dark school.\nScientist service student nation who wide market. Know clearly they finish. And maintain not soon play right.\nSign similar support cell. Meet less share pass.

', + attachments: [], + isStarred: true, + labels: ['personal', 'company', 'private'], + time: '2021-07-26T09:23:33', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 66371, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'dannynguyen@hotmail.com', + name: 'Matthew Schaefer', + avatar: avatar5, + }, + subject: 'Staff can next along long true dark.', + cc: [], + bcc: [], + message: '

Hotel only mind create soon north life. Improve pass too important those per including. System four north.\nFamily politics floor huge bad. Light look start apply forward civil agree. Later place expect at build.

First now against include time experience those and. Their these reveal guy dark. Always option fall evidence once success.\nLive sing gun meet. Spring face political voice. Blood clear couple run left available.

Visit network so total wife. People artist experience citizen maybe water good.\nHis news wonder note. Consumer kitchen him sport type.\nCandidate fall where structure. Art hour term matter look program.

', + attachments: [], + isStarred: true, + labels: ['personal', 'private'], + time: '2021-07-09T01:39:12', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + ], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 33, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'hsmith@gmail.com', name: 'Wendy Marshall', avatar: avatar8 }, + subject: 'Enjoy see man news decide build class make.', + cc: [], + bcc: [], + message: '

Because gun area better region role party. Most cultural control radio religious rule human.\nFinal cold positive country story gun.\nThey myself bed involve. Course president health might lot close. Level fine college deal.

Tree race ground customer. Window prove maybe television possible well soldier over. International run conference free white audience consider.\nInterview ball leg number blood support his turn. Care product a.

Something ahead painting then option recognize. Use force price then away.\nFind agent hospital physical his. Town money person case during body.\nFast have kitchen character a race walk. Stage bring we entire sort.

', + attachments: [], + isStarred: true, + labels: ['personal', 'company'], + time: '2021-07-10T22:42:15', + replies: [ + { + id: 301809469, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'maryatkins@gmail.com', + name: 'Kimberly Cisneros', + avatar: avatar4, + }, + subject: 'Family pretty interest decision.', + cc: [], + bcc: [], + message: '

Feeling production spend. Program look stand meet him. Ask away generation phone.\nMachine process window range serious process remain. Good charge in serious study seat. Heavy she concern door fire organization money fact.

Whether end investment pay. Happy information cup then. Edge fire suffer remain catch.\nDirector international determine might. Clearly fire something player. How that increase ready section. Visit become contain.

Decide find growth continue movie thank sort.\nPull where attention treat or. Since resource gas person trade organization crime. Growth southern lay lose president likely half.

', + attachments: [], + isStarred: true, + labels: ['private'], + time: '2021-07-23T04:23:43', + replies: [], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + { + id: 930166, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'nramirez@yahoo.com', name: 'Kenneth West', avatar: avatar1 }, + subject: 'Factor TV wife career thing loss increase.', + cc: [], + bcc: [], + message: '

Every public quality also. Almost base imagine former decade pull also the. She stage so military admit.\nSouth better general base reason employee may. Control see way end material service. Everybody fear risk party weight.

Sea line production appear them through. Late gun something power little care. Interest since test total.\nProvide as condition none wind month thus. Fly sort south artist letter health night.\nWrong group affect even. Identify way interview politics.

Risk total natural follow music drop sense hospital. Space family cover effect. Live particularly letter generation toward concern reality friend.\nOrganization bar ask great most live seat week. Against western use present.

', + attachments: [], + isStarred: true, + labels: ['private', 'company'], + time: '2021-07-14T00:55:32', + replies: [], + folder: 'sent', + isRead: false, + isDeleted: false, + }, + { + id: 324, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'tina53@hotmail.com', + name: 'Douglas George', + avatar: avatar1, + }, + subject: 'Recognize to study.', + cc: [], + bcc: [], + message: '

Perhaps pretty color walk different likely think. Southern occur soon chair leave discover heart. Rest product break member operation.

Agreement I include for.\nState anyone fight interview view west concern. Reach social reason how husband east.\nSometimes able especially simple size behavior. Talk beyond both big another often former.

Her money art involve building natural garden. Pay them respond step that. Old yourself table would agency. Pay recognize family individual.

', + attachments: [], + isStarred: false, + labels: ['personal', 'company'], + time: '2021-07-28T01:47:02', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + ], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + { + id: 32, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'jeffreystevens@yahoo.com', + name: 'Christopher Adams', + avatar: avatar3, + }, + subject: 'Finish actually parent condition business discussion later practice.', + cc: [], + bcc: [], + message: '

Design left million test bag character. Pm everybody ago. Table finish sell my pay quite. Often account cover home war.\nCourt sport difference film left guy natural understand. Across ok quite now camera rock.

Plan citizen star off often evidence remember. Describe professor economic professional represent catch. Employee stand person eye. Region address spend.

It policy beyond scene. Wide bed culture account eat. Color technology even.\nMight ready option guess. Once create ever worker paper perhaps. Show likely say produce capital court.

', + attachments: [], + isStarred: false, + labels: ['personal', 'important', 'private', 'company'], + time: '2021-07-27T09:40:52', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 31, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'maria99@hotmail.com', name: 'Theresa Schmitt', avatar: avatar8 }, + subject: 'Life store technology least under black type.', + cc: [], + bcc: [], + message: '

Main each pay bar professional blood fill. The commercial amount thousand carry. Sound ball become court relationship so white.\nFight late exactly evidence evidence art but. Congress spend camera sea other. Theory protect plant wait.

Her necessary capital around nor issue herself. Late quickly someone own painting moment participant.\nRequire civil night take. Southern cold because option report share fine who.

List black mean everything read front Mrs. Look whatever street approach fear guess once. Paper somebody hear machine.\nTogether it price world professor country. National worker specific shake. Open security tell all sure none imagine say.

', + attachments: [], + isStarred: false, + labels: ['company', 'private', 'personal'], + time: '2021-07-01T03:23:03', + replies: [], + folder: 'sent', + isRead: false, + isDeleted: false, + }, + { + id: 30, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'kempsarah@gmail.com', name: 'Sherry Guzman', avatar: avatar2 }, + subject: 'Officer population memory level foot public.', + cc: [], + bcc: [], + message: '

Summer general go happen owner last. Store live organization court think.\nDiscover second million today space activity conference. Generation young design factor interesting. Account always Mrs garden plant.

Sound discover piece people. Positive decade describe. Focus science free.\nSide mean however plan price me.\nBy later building result important down lay. Try growth structure nation above pull however those.

Wonder end value lead help quite trial. Recognize teacher establish explain. Try usually find over matter much.\nRaise son mouth.\nBase reach bit recognize focus. Stop best sea improve develop.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-20T16:43:48', + replies: [ + { + id: 76, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'jessica23@gmail.com', + name: 'Lauren Smith', + avatar: avatar8, + }, + subject: 'College community effect care.', + cc: [], + bcc: [], + message: '

Task age compare talk yard. Matter turn their price road.\nCulture four decide work chance cost include. Rock return statement. Major major several around method.\nUs would threat federal sense mean.\nCondition as why fast. Guy bit often professor.

Tell concern difference eye office trade fund fire. Lead report only star hot.\nFeel far factor current girl. Two hair fight a recent movie apply. Again series sometimes recent identify.

Perhaps agree note between house whom too. Down could important production tend figure special. West far bad impact cause great.\nRepresent green throughout never type trouble outside. Call adult would clearly. Turn stand federal.

', + attachments: [], + isStarred: false, + labels: ['private', 'important', 'personal'], + time: '2021-07-20T17:21:18', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 435260844, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'lkelley@yahoo.com', + name: 'Michael Torres', + avatar: avatar3, + }, + subject: 'Build learn audience water article ball must.', + cc: [], + bcc: [], + message: '

Method election require important majority five. Seat listen story.\nCause middle act film. Available turn gun before whole especially kind simple.\nStage wrong hot find agree suddenly. Chance source clear share stay few.

Figure activity role official. Food live personal.\nPersonal no public computer prepare when. Fish available report network if attack among decide. Seem rule inside economic door.

Budget open send wrong property. Half spend stock less. Degree act general skin these any personal per.\nUntil never state chair already. Product sign best.

', + attachments: [], + isStarred: false, + labels: ['important', 'company', 'private'], + time: '2021-07-10T07:00:15', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 7780, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'thomaspeterson@yahoo.com', + name: 'Dylan Logan', + avatar: avatar1, + }, + subject: 'Artist food section media commercial hospital.', + cc: [], + bcc: [], + message: '

Tax above either world. Candidate accept final challenge which risk. Fact book example positive follow attention.\nCost building central contain natural. Adult least by.

Fast cause environment go explain necessary. Help citizen others beat sure child. Claim inside whether approach chance always central.\nSide ten bill look fine career. Attention real little power yourself bank.

Nothing American sister truth medical matter. Use door practice feel point fear. Argue else however involve fact.\nOwn recognize save. Federal brother loss mouth painting paper.\nDemocrat crime join quality. Off politics note soon.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-26T23:06:27', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + ], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 29, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'daniel37@hotmail.com', name: 'David Cruz', avatar: avatar5 }, + subject: 'Clearly my usually billion ability response.', + cc: [], + bcc: [], + message: '

Face despite management international talk force detail. Partner score hit democratic fast life property. Age information wear grow rise hard price. Every area character first activity smile.

Station character American usually nice change young. Make perhaps happy trade since science. Fine think attack successful.\nCrime bit spring city. Lawyer light ball unit instead statement. Lose friend account buy oil ten tend.

Security identify there. Person factor item build never language.\nEnter stock military early. Wish identify level difference fire wall. Girl finish sense indicate bad.

', + attachments: [], + isStarred: false, + labels: ['private', 'company'], + time: '2021-07-01T10:33:17', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 28, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'areeves@gmail.com', name: 'Anthony Obrien', avatar: avatar5 }, + subject: 'A type network effort blood do various.', + cc: [], + bcc: [], + message: '

Own measure phone view baby officer. Detail nor television. Wear decade official long.\nCan interview point poor increase pick quickly run. General need audience foot weight firm. Month ability public. Go class let rise spring heart.

Cover attention letter later many town stuff away. Week lawyer western street.\nUnit rate reality adult. Arrive staff book me many.\nHand perhaps well thank join serious great budget. Including road upon will. Per price mission break.

Experience late nothing get baby head should. Must technology service address blood.\nChance decide else mean consumer pretty everything. Hospital couple second fly security region brother.

', + attachments: [], + isStarred: false, + labels: ['important', 'company'], + time: '2021-07-20T18:39:49', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 27, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'bnunez@hotmail.com', name: 'Jason Gonzalez', avatar: avatar5 }, + subject: 'Affect method provide break himself house.', + cc: [], + bcc: [], + message: '

Science design amount responsibility. Seem himself degree. Decade central manage. Rather four decide word.\nQuickly keep such popular different approach woman. Population body decade baby view significant can. Wish a build respond.

Site cut forget international lay he there. Tax early try authority.\nAbout term enjoy prevent affect. Even environmental kid skill.\nFirst plant number site bad interest board. Investment half so.

Method sea agent capital later just worker. Main guy cut build building. Condition similar best gun. Dinner new box major artist space in.\nRaise try science grow. House picture raise indeed light.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-23T07:42:38', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 26, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'welchcrystal@gmail.com', + name: 'Christopher Sanchez', + avatar: avatar5, + }, + subject: 'Trade science concern necessary theory option us.', + cc: [], + bcc: [], + message: '

Top foreign never recent baby girl base. Show charge senior difficult drug effect. Fear on standard doctor stop investment spring.

One long article market there into. Share nature member owner evening. Form tree real cultural.\nSecond be report teacher admit close.\nWhom skill teach. Blue song ahead weight rather walk line. Five talk require.

Rate onto nearly address rule side activity. Result ahead you hope woman worker evidence.\nCollection citizen we industry. Sister and that according organization leave. Day agency hope pick.\nEconomic him consider body four section single when.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-10T16:15:08', + replies: [], + folder: 'inbox', + isRead: false, + isDeleted: false, + }, + { + id: 25, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'fullerkatelyn@hotmail.com', name: 'Ana Short', avatar: avatar4 }, + subject: 'Follow tax officer soon our four relationship consumer.', + cc: [], + bcc: [], + message: '

Couple almost table everyone together contain. Plan fill trip. Perhaps explain college will machine mouth training popular.\nNice include wrong road alone. Could for adult perform.

Tax ahead ground general industry. Else style only Mr agent all.\nAlready walk edge might forward. Cold wind hard read. Street poor process major especially example defense.\nDecade capital question talk work box forget. Always hear Mr ago.

Apply camera white natural should another. Past event herself score. Own thus general despite pattern. Ability pressure network mouth sometimes represent.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-12T09:22:10', + replies: [ + { + id: 4556357, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'woodjames@gmail.com', + name: 'Taylor Lopez', + avatar: avatar4, + }, + subject: 'Appear imagine western.', + cc: [], + bcc: [], + message: '

Effect fall action chair candidate forward. Away character action start even focus claim address.\nJob once off according put off. Give answer near star cell expert. Use tax care month list.

Investment it check.\nPopulation oil mouth glass against. Stand all art leader agree.\nHerself only score image prevent bar table. Total treatment enjoy everything. Long later just cover or great meet.

Exist month watch wish remember simply low. Knowledge treatment maintain fine organization fall identify.\nIdea enough worry coach better stand general. Threat western language must person.

', + attachments: [], + isStarred: false, + labels: ['important', 'company'], + time: '2021-07-08T11:19:22', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 419, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'charlotte46@gmail.com', + name: 'Edwin Pena', + avatar: avatar3, + }, + subject: 'Hundred happen national measure.', + cc: [], + bcc: [], + message: '

Try high body design blue. Deep improve ahead police.\nHuman behind police international. Around would nor position particular physical break. Pm for against clearly.

Who in rock then build. Analysis produce kind senior until where. Part east understand.\nChance billion culture might so five. Particularly create story maintain article give fall. Short improve whatever new available wear affect.

Financial great impact everyone until.\nThem might try range main. Activity decade stock first stock start explain. Write phone nice increase fish several.\nNewspaper exist himself dinner choice agree hear. Great receive today identify.

', + attachments: [], + isStarred: true, + labels: ['company', 'personal'], + time: '2021-07-28T00:33:38', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 5123, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'amandawagner@yahoo.com', + name: 'Laura Montes', + avatar: avatar2, + }, + subject: 'Administration choice move against provide value none.', + cc: [], + bcc: [], + message: '

Goal throw including miss sometimes staff traditional. Material talk place point pay.\nShake popular part wind. While state light. Explain movement they.

Our herself indeed let use. Debate front within yes impact change big contain. Purpose outside nothing leg image never dark husband.\nPlant bring decision avoid ground act book. Up hold speech. Local indeed short.

Cold step herself style important. Week base tree game kid. Coach yet expect determine personal. Here happy peace have cause up.\nApply include recently reality common attention. Effort politics player though fly.

', + attachments: [], + isStarred: false, + labels: ['personal', 'company'], + time: '2021-07-23T04:17:17', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 60679807, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'clarkdwayne@hotmail.com', + name: 'Felicia Myers', + avatar: avatar2, + }, + subject: 'Me during name.', + cc: [], + bcc: [], + message: '

Miss back sing simply. Tax surface shake page so. Any rule vote for.\nSport six former simple. Daughter business push reality information.\nResource just possible rich enter. Tax full box beat. Network edge cultural among no morning.

Since but appear place. Trouble particularly paper chair commercial. Offer everyone success trip. Treatment special support resource.\nGun analysis test recently ball. Reality organization family test TV I surface.

Appear system shake charge nice foot. There our wrong author investment coach. Feel leg economy require push performance out speech. Need hair however commercial.\nLike Congress system whether skin. Research little attention art.

', + attachments: [], + isStarred: true, + labels: ['company', 'important'], + time: '2021-07-09T19:19:45', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 31103, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'hobbsjeffrey@yahoo.com', + name: 'Erica Mann', + avatar: avatar6, + }, + subject: 'Ability pretty student health current interesting even.', + cc: [], + bcc: [], + message: '

Image American daughter test animal. Somebody especially war loss name only just.\nStation such television also good away so water. Protect across television phone. Realize almost final half fight establish.

Program skill rest bed east here become law. How loss might purpose low time organization. Industry different enter share budget.\nFeel million how modern whole religious half finish. Hospital stage decision consider democratic.

Sort move scene behind. First office take together keep note break kind. Either laugh top agree prepare change.

', + attachments: [], + isStarred: false, + labels: ['personal', 'important', 'company'], + time: '2021-07-21T11:41:54', + replies: [], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + ], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 24, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'hmoran@gmail.com', name: 'Vincent Alexander', avatar: avatar1 }, + subject: 'Over tough city well first should quite.', + cc: [], + bcc: [], + message: '

Sense speech economic compare chair. Suddenly model bank add. Let church door human ready share begin sense.\nPlay weight audience call necessary reach candidate rest. Collection lead voice position news listen police.

Describe safe almost hold. Rich because trip blue. Discussion born spend because anyone need.\nWonder skill state. Movie receive guess with. Turn pressure market term experience hotel collection.\nOff staff word once money.

Response north Mrs area writer election. Include early look similar nearly be. Rate happen green not.\nRun bed where state why sit house attorney. Which allow size learn. Describe mind where speak some son herself.

', + attachments: [], + isStarred: false, + labels: ['company', 'important'], + time: '2021-07-12T13:33:33', + replies: [ + { + id: 324726, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'victorjohnson@yahoo.com', + name: 'Tamara Vega', + avatar: avatar6, + }, + subject: 'Democrat miss deal career maintain hotel.', + cc: [], + bcc: [], + message: '

Base enter whom respond throughout together. Nor generation various company bar. What consumer how.\nKid recently civil store. High hot assume gun.

Important win election center. Party less knowledge only magazine past condition yard. Sound doctor say between.\nResult process may have firm wide. Moment audience skill safe fast. Spring although member defense value job.

Nothing serve media tell network site benefit artist. Left scene strong. As community decide major.\nNearly indeed send begin read. Recent foot three letter wide spend have. Growth whether once home actually without central.

', + attachments: [], + isStarred: false, + labels: ['important', 'personal'], + time: '2021-07-13T12:43:08', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 3, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'jeremywilliams@yahoo.com', + name: 'Jason Schultz', + avatar: avatar1, + }, + subject: 'Piece effect usually everyone make.', + cc: [], + bcc: [], + message: '

Market easy before really individual window soldier garden. Better space avoid fund. Politics friend class something western model. Seem successful recently sometimes.\nServe shake try for you our. Involve organization last at inside.

Employee office list player. Pass cold charge.\nEye sometimes article pressure. Chair mission structure him owner. Fight leg common her forget across against.\nMusic national student. At part wide fund.\nReady health everybody.

Cover century him back card property success. Enter feeling light oil cell push research.\nNow drop everyone must side blood program. Factor fire dark their kind hit everyone person. How property million interesting both important.

', + attachments: [], + isStarred: false, + labels: ['personal'], + time: '2021-07-13T19:56:30', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 4, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'jasonpalmer@hotmail.com', + name: 'Deborah Tran', + avatar: avatar2, + }, + subject: 'Certain executive them health successful spring.', + cc: [], + bcc: [], + message: '

Commercial individual understand past history large strong.\nPositive summer three need evening. North between pay politics art hand ago cover.\nLevel happen start practice reach. Produce sport show condition. Individual grow education.

Return fear food enter friend. Great company opportunity nearly garden choose.\nLast capital cell true edge. Daughter cost west stage force tell.\nEvidence stop whether power. North hospital base accept. Message him likely trouble tax business part.

Just record kind drug four perhaps entire. Economic surface century individual behind understand.\nTax hair charge investment similar perhaps pay. Return room create table other foot happen approach.

', + attachments: [], + isStarred: true, + labels: ['private', 'personal'], + time: '2021-07-14T18:37:56', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 19865651, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'danny06@gmail.com', name: 'Walter Moss', avatar: avatar3 }, + subject: 'Go town spend determine we money experience partner.', + cc: [], + bcc: [], + message: '

Color onto chair very the account article different. Time however total without.\nHerself left knowledge never heart its product over. Citizen range state various same fall would day. Anyone against when grow evening.

Grow main front thing boy. Accept shake student consumer whom.\nAnyone return between apply.\nRead its prepare young. Week start for again focus doctor. Itself term until see somebody.

Trial direction idea green young. Success to light later.\nUse box sense indicate ask. Himself six five. Ready government than young represent difficult.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-08T13:58:13', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 23, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'paulbarrett@gmail.com', name: 'Robert Soto', avatar: avatar7 }, + subject: 'Apply loss always difference husband course deal.', + cc: [], + bcc: [], + message: '

Realize American professor television give.\nNice meeting individual could major instead. Late development deep. Memory main how minute reduce want whether happy.

Capital fight water page artist seem own. Make join public break. Support water analysis cup forget together.\nAgain along listen defense ground mission once region. Last ground experience hot trade free camera.

Bill floor tonight good condition. Traditional must spring onto break. Left just everybody election. Treatment foreign control dark often.

', + attachments: [], + isStarred: false, + labels: ['private', 'personal'], + time: '2021-07-06T23:12:45', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 22, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'danagriffin@gmail.com', + name: 'Alexander Alexander', + avatar: avatar7, + }, + subject: 'Lead position story common choice pay sit line.', + cc: [], + bcc: [], + message: '

Hand style bill phone day new area. Central husband measure could. Democratic health begin draw politics wear interest.\nHim avoid knowledge music. Offer forward happy easy. Just yard one light weight teacher threat.

American it feel parent protect. Center building recent politics when hand bar under. Without hard relationship issue.\nContinue friend game concern. Agency discussion simply hotel now prevent.

Sense indeed glass accept interest. Carry window dog onto involve specific.\nRadio despite police scientist economic. Fire affect your term. Send to end avoid political ability.

', + attachments: [], + isStarred: false, + labels: ['private', 'personal'], + time: '2021-07-03T07:04:27', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 21, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'bakercarrie@yahoo.com', name: 'Dawn Hall', avatar: avatar2 }, + subject: 'Magazine smile hear price.', + cc: [], + bcc: [], + message: '

Ball skin product option anyone. Away involve whatever score.\nCommon ever show all body bed already. Modern politics century sort. Half study write life certain.

Nothing little whose carry source force heavy employee. Price force leave small follow. Push enjoy down teacher among. Huge nature whose risk season east maybe peace.\nPolitics interview drop sell. Trip from simple matter event.

Brother simply structure some kitchen some expect. Family personal civil focus professional task specific cut.\nDemocrat continue cause television yourself whether. Find west particular ago stand car.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-06T12:47:33', + replies: [ + { + id: 6333, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'james94@gmail.com', + name: 'Ronald Mitchell', + avatar: avatar1, + }, + subject: 'Cost example hope modern especially language rock.', + cc: [], + bcc: [], + message: '

Mr go size financial role. Deal defense about space. Leader site water well side walk need.\nBall impact suddenly those rather have marriage first. May wear need may design.

Everyone artist run weight. State on executive travel.\nBrother instead nice while such half trial live. Policy truth animal make set them ask.\nPretty almost pick player after involve. Hot energy interview clearly however adult.

People during left particular rock design war young. Station require reflect. Later space head front within general. Program lose century stage.\nInstead very both. Owner bill tend Congress local.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-19T13:54:07', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 3539, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'averyamy@hotmail.com', + name: 'Courtney Reynolds', + avatar: avatar6, + }, + subject: 'President attack quickly religious.', + cc: [], + bcc: [], + message: '

Stop military interest. Picture his money go quickly. Possible second wide high.\nTime air somebody on development born charge. Marriage address pull. Laugh chair range standard open list consumer wide.

Dinner another but student upon out. Soldier current management hair management.\nLikely population measure Democrat serious result reflect. Property tax knowledge. Recognize top peace nature pattern.

Table teach knowledge. Economic section security she. Myself share oil decide necessary when smile difference.\nService open oil car. Be model record stuff position scene also. All professional plan as radio candidate movie.

', + attachments: [], + isStarred: false, + labels: ['company'], + time: '2021-07-06T00:53:34', + replies: [], + folder: 'sent', + isRead: false, + isDeleted: false, + }, + { + id: 132667, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'englishjohn@gmail.com', + name: 'Phillip Warner', + avatar: avatar1, + }, + subject: 'Stand never treat commercial.', + cc: [], + bcc: [], + message: '

Like begin million option dream just. Side still six truth alone exist that.\nIncluding himself movement increase significant. Police trial instead success he chair speak. Medical writer oil.

Successful compare analysis yes successful. Before sit old process similar physical.\nMedical receive debate than. Hit assume baby result place total.\nMoney discussion tax democratic surface everybody thousand. Throw six far home.

While reality along loss only alone pick current. Ok month view computer. Available drug ask knowledge add choose must.\nScene you ago laugh else city. Receive provide goal husband throughout. Focus local middle civil ever oil.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-07T04:28:47', + replies: [], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + { + id: 815966603, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'davidmckenzie@yahoo.com', + name: 'Tony Garcia', + avatar: avatar3, + }, + subject: 'However walk less use election.', + cc: [], + bcc: [], + message: '

Shake stop century indicate cut garden. Night learn should low material north economy.\nAnother soldier base whole accept. Natural two everyone television. Sure option key market method week. Mouth day look too western world.

Company first rise in. Image movement enjoy clearly work box. Process parent fear state these theory want. Close friend team put check.\nCourt practice since account way indeed. Between exactly five. Conference green fast see century notice.

South six discover college long anyone young. Her company fine hotel rise.\nIf raise long yeah direction painting. Rest tell entire machine than summer laugh list. Personal rise figure collection player yard.

', + attachments: [], + isStarred: true, + labels: ['private', 'company', 'important'], + time: '2021-07-07T09:57:28', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + ], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 20, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'sandraarroyo@hotmail.com', + name: 'Natalie Lloyd', + avatar: avatar2, + }, + subject: 'During teach truth group society enough that.', + cc: [], + bcc: [], + message: '

Price agreement more tell. Push special fine turn alone.\nVarious weight shake heavy age control side so. Determine fall family agreement pull guy easy. Sell will director experience where challenge Democrat.

Fly such evening all entire. Data cold hour.\nLocal strong article tend bag. Probably relate political sell. Service end environmental theory health. Ready think body necessary low result impact.

Agency trial address per strong bill able. Top lay chair bag positive rich partner. Interest address government argue project attention myself election.\nReach value pattern treat act result star. Staff list federal.

', + attachments: [], + isStarred: false, + labels: ['company', 'important'], + time: '2021-07-11T17:48:11', + replies: [ + { + id: 7, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'webersamuel@gmail.com', + name: 'Steven Jackson', + avatar: avatar1, + }, + subject: 'Hotel account interview begin carry everybody its.', + cc: [], + bcc: [], + message: '

Get behavior better walk claim. Material popular civil detail.\nStop strong true first. Science scientist low story. These former near represent.

Fine value happy admit. Although its four could yet call. May beyond building bank push past perform.\nEnd civil audience son our my artist make. Security wish probably cold space reach life situation.

Station per choice live safe dog without. Above according break her woman organization market.\nCareer pass race mother manage for. Summer organization stage century fact individual particular.

', + attachments: [], + isStarred: true, + labels: ['personal'], + time: '2021-07-18T23:08:26', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 378459327, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'jobush@yahoo.com', + name: 'Mrs. Pamela Riggs MD', + avatar: avatar6, + }, + subject: 'Industry difficult want without day partner road.', + cc: [], + bcc: [], + message: '

Never hospital price site without star. Agency nature resource perhaps send. Stand nice must.\nProve window individual final. Exactly collection boy picture try operation increase. About purpose American type include store determine.

Speak they reality consumer ball church.\nWorld sit price. More local clear. Camera kind food.\nShe often term somebody prove. Would low over someone law.\nInstitution any among face begin race term do. Teach language technology get animal good.

Play cell type process certain total stay. Court enough side choice again speech.\nBy alone young scientist walk individual a. Mind relate whatever fund vote contain. Reflect special hospital study may local.

', + attachments: [], + isStarred: false, + labels: ['personal', 'private'], + time: '2021-07-18T03:59:12', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + ], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + { + id: 19, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'washingtonsamantha@hotmail.com', + name: 'Jessica Johnston', + avatar: avatar6, + }, + subject: 'Exist general medical under entire radio.', + cc: [], + bcc: [], + message: '

Process book suddenly plan sense change science. Prepare air option response. Voice range human.\nYet staff back idea note his cold. Raise service about state final official.\nHair when expect ok sit food. Religious rule doctor all.

Need improve field set wrong born.\nConsider there situation also something. Glass finally must special. Region news water responsibility to my short. Deal hotel fill.

Successful apply reality think woman short. Hope various indeed onto third audience.\nWay score none. Raise budget tough dinner name. Similar something fall certain I different.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-24T18:10:41', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 18, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'mcleanrobert@hotmail.com', + name: 'Matthew Lee', + avatar: avatar1, + }, + subject: 'Travel free or write determine.', + cc: [], + bcc: [], + message: '

Edge memory where short stuff. Seven summer from sometimes body probably church.\nYeah might enough believe world person somebody. Compare summer road save magazine.

Light street wear home. Result baby my show current present. Attorney analysis rule democratic bed top.\nFace should pay side federal responsibility item. Test step safe his yourself.\nHold language interview other agency. Leg soon determine.

Make style already you physical.\nAir challenge fund dark. Myself another evening let big improve parent huge. Money fly investment practice.\nProvide feeling peace open decide course. Community attack her magazine white. Those let any beyond.

', + attachments: [], + isStarred: true, + labels: ['private', 'company', 'personal'], + time: '2021-07-24T00:15:10', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 17, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'perkinselizabeth@gmail.com', + name: 'James Wilson', + avatar: avatar7, + }, + subject: 'Always beautiful name push miss international.', + cc: [], + bcc: [], + message: '

Skin if open line speak wish. Ten size their happen trial. Will third prevent.\nPopular wall indeed memory cause generation under age. Less one pressure guy song.\nUpon theory item science speak mission. After read plan official good week yet show.

Shake trip when once break election red. Left individual store site prepare figure. Once indicate blue wear effect person catch.\nWind chance entire perhaps carry notice leg. Successful property education. Guy option include.

Author of exist no bag exactly. To impact since.\nArgue market strategy evidence start business movie. Million fire crime magazine mention.\nDeep figure full Mr. Take response four serve law. Forward late part.

', + attachments: [], + isStarred: true, + labels: ['important'], + time: '2021-07-07T22:14:25', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 16, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'garcialauren@hotmail.com', + name: 'Gregory Allen', + avatar: avatar3, + }, + subject: 'Test look option movement position card cause.', + cc: [], + bcc: [], + message: '

Key rather religious director week inside campaign. Sport fast activity.\nCamera go sing development up pay. Product toward well.\nRepresent appear civil skill son city leg. Best road attorney religious. Issue collection who peace morning director.

Above know trip beyond smile science. Part sport behavior notice establish. Recent direction similar everything admit pretty.\nBehind a knowledge second sound. Body soldier begin word site.

Sense policy rule after no response itself. Have magazine draw should bit often food. Car start that trade person.\nLeft pattern PM identify before executive Mr. State two your meeting task different.

', + attachments: [], + isStarred: false, + labels: ['personal', 'company', 'important'], + time: '2021-07-11T00:14:13', + replies: [ + { + id: 744639799, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'howardjustin@gmail.com', + name: 'Rebecca Smith', + avatar: avatar6, + }, + subject: 'Region stop vote tonight partner capital us.', + cc: [], + bcc: [], + message: '

Republican ten picture although partner green.\nWrite his than another hand only. Focus night table speak ahead couple. Baby me single another already unit hand.

On alone involve.\nMusic author event story east pressure thus. Game power administration.\nNext standard boy provide although city short society. Hospital company old view.

Interest see majority ability center hope. His decision use most four return college. Born technology affect like.\nAlong your military there note great day attack. Specific I throughout. Hand month family open.

', + attachments: [], + isStarred: true, + labels: ['personal', 'important', 'private'], + time: '2021-07-05T08:12:17', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 18, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'dwelch@yahoo.com', name: 'Peter Davis', avatar: avatar1 }, + subject: 'Second ground way child seem social resource appear.', + cc: [], + bcc: [], + message: '

Building believe manage analysis artist another enough similar.\nFood provide long view civil couple. Citizen too health west culture rule finish administration. Political ever eight message specific mission.\nServe determine city stand four present.

Moment compare red or institution begin more. Nothing law long might degree. Meet relationship work money human probably head.\nForward region their high with region their. Many side goal.

Customer thousand amount ask other might. Article energy wide relationship. Prevent save himself wrong action.\nShow entire play upon at shake. Unit heavy training window probably start share. Common by allow.

', + attachments: [], + isStarred: false, + labels: ['personal'], + time: '2021-07-23T16:37:03', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 712, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'larrybrown@yahoo.com', name: 'Amy Peters', avatar: avatar4 }, + subject: 'Interesting strategy south ok recognize shoulder lead.', + cc: [], + bcc: [], + message: '

Bring dark let list then kitchen audience. Agreement raise result decision choose without.\nIndicate yet radio consider perform western. Find follow far require wish than pattern. Meeting benefit through seven service.

Question response big son student stuff. There imagine hold pick friend. For join condition try.\nAnimal foot work public one brother hit.\nWithout free business new degree. Local administration it those animal.

Simply less tax. Stuff apply member deal rather sort. Best politics project say rest.\nCare expect program break concern development care. East seat window. Kind firm cover up share perhaps.

', + attachments: [], + isStarred: false, + labels: ['personal'], + time: '2021-07-17T02:58:14', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 15, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'dreed@hotmail.com', name: 'Rhonda Hamilton', avatar: avatar4 }, + subject: 'They new police guy trade carry bad.', + cc: [], + bcc: [], + message: '

Certain operation woman production especially second. To answer main good democratic move likely radio. Down rise human model land culture the.

Ten actually feeling call blue human. Less forward star another something he.\nUsually scene door enjoy heavy view management. Eye data conference. Attention traditional especially star else federal course. Speak position season stage head when.

Foot face beautiful little seven former you usually. Candidate hotel help.\nKitchen heavy she. Agent put move sister much. Hit some baby have fight.

', + attachments: [], + isStarred: false, + labels: ['company', 'private', 'important', 'personal'], + time: '2021-07-18T12:06:21', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 14, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'andersonkatrina@hotmail.com', + name: 'Richard Buckley', + avatar: avatar7, + }, + subject: 'Hospital small technology defense affect car.', + cc: [], + bcc: [], + message: '

Finish race write suggest visit pay east. Might point heavy care.\nSociety who happen stock over toward account. Question shake city share marriage drug.\nEvery test total agency another like. Wall day word camera art.

As thus necessary degree always support fall. Leader town agree improve check career. Later service when artist customer blood.\nEasy daughter tend no raise. Throw glass various among nearly act if. Than area sort trial many marriage old decision.

Worker coach together raise civil term themselves. Television something ok thank.\nAlmost song task there budget quite process than. Sell which apply environmental.\nDrop mind computer increase born.\nAuthor with will time. Garden others agency wall.

', + attachments: [], + isStarred: false, + labels: ['personal', 'company', 'private'], + time: '2021-07-09T06:36:05', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 13, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'kimberlyrobinson@hotmail.com', + name: 'James Brady', + avatar: avatar3, + }, + subject: 'There bar risk bring.', + cc: [], + bcc: [], + message: '

Thought prepare want hand character design most. Run result attack before.\nVoice return give right way along. He lose change season less cell moment use. Today benefit would somebody.

Million area million across near company heart. Happen official knowledge look. Turn class interesting.\nGive product fund would factor into hope. Everyone painting program forget including.

Begin force foreign degree detail oil such.\nFirm scene individual here point. Particular interview before people last shoulder. Appear until spend under along magazine.

', + attachments: [], + isStarred: true, + labels: ['personal'], + time: '2021-07-06T02:27:43', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 12, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'ugray@gmail.com', name: 'Jane Buckley', avatar: avatar2 }, + subject: 'Picture everything throw happen nothing social.', + cc: [], + bcc: [], + message: '

Cell role hundred husband president figure. Make how real again.\nDevelopment image develop Republican. Military head drop. Relate wait able art.\nPolice response range establish back. Chance assume subject stock appear good research.

Thousand PM speech hear three yard should for.\nMachine crime too represent campaign book. According call each.\nPicture site create sister. Opportunity become who never bed number develop set. Major finish everyone meet vote letter across.

Reality send American. Democratic serious event oil lose. Tax position down front service improve election.\nThreat heavy over. Each leave several writer card politics. Question feel technology many thank.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-18T13:42:20', + replies: [], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + { + id: 11, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'morrisjaclyn@gmail.com', + name: 'Kathryn Smith', + avatar: avatar6, + }, + subject: 'Green attorney government same course join in woman.', + cc: [], + bcc: [], + message: '

Employee society live back. Bar woman film education.\nImportant report avoid. Wait nor goal. As morning say clear.\nBody strong of alone camera fall. Civil program particular first garden. Social become voice law quality.

Mouth whole for positive. Certain tough especially nature claim box.\nFill space allow second second cut. Bank want why decide recognize space.

Outside ability second whose second. Point stand bank list defense understand seat.\nClear finish follow media sing type. Technology white practice miss price.\nDifference establish some nation western job meeting. Give article beautiful.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-08T14:51:49', + replies: [ + { + id: 133615687, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'juan31@gmail.com', + name: 'Jennifer Robinson', + avatar: avatar8, + }, + subject: 'Beautiful despite note couple pretty issue near.', + cc: [], + bcc: [], + message: '

Store use cultural human smile. Subject trip that laugh.\nWalk sense a operation about window small southern. Show road them movement.

Water behind do else just. Reach mean science yet among what.\nGreen modern design us know use others weight. Recently wonder soldier within plan.\nRoom test story see southern special nice. Drop take mind plant throw American my. A husband sit thing.

There performance fine coach way majority truth. House beyond candidate beyond debate painting alone. There significant poor something chance spring. Yeah worry white Democrat.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-13T14:02:08', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 867, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'gflores@hotmail.com', + name: 'Cindy Hernandez', + avatar: avatar2, + }, + subject: 'Watch vote decide compare start.', + cc: [], + bcc: [], + message: '

Choice race different. Yard case newspaper wide series growth identify.\nBeyond go rest read me. Though quite industry method animal organization leave quality.

Back music theory fund produce. Foreign hard board learn home add. Data political buy budget think.\nBook consumer future writer. Bag evidence thus school.\nDifficult my accept yard. Million loss officer person language to. Television room feeling.

Country myself current tough image school. Court activity catch low value. Hotel local through.\nFocus attorney computer evening you always. Guess require event picture director. Garden floor month husband mention.

', + attachments: [], + isStarred: true, + labels: ['company', 'important', 'private'], + time: '2021-07-12T14:38:42', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + { + id: 7, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'judyvillarreal@hotmail.com', + name: 'Amy Chavez', + avatar: avatar8, + }, + subject: 'Member around task woman as.', + cc: [], + bcc: [], + message: '

Police physical down generation condition throw foot relate. Table experience represent practice development.\nOrder option success thank miss. Tree knowledge light police service remain during. Entire respond join hit kind enjoy language.

Modern page social decide though small realize impact. Around special difficult level organization course her.\nMr tree three former this husband hold. Local expert especially should writer visit moment. Quite move travel less.

Nearly loss those democratic bring production. Ago economic method consider discuss.\nCapital approach red but reveal successful. Middle television treatment. Turn recent reflect interview.

', + attachments: [], + isStarred: true, + labels: ['company', 'private'], + time: '2021-07-17T02:32:58', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 59708653, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'mwalker@hotmail.com', + name: 'Marcus Campbell', + avatar: avatar3, + }, + subject: 'Woman ability middle choose vote few ability.', + cc: [], + bcc: [], + message: '

Because structure put. Face business possible light box.\nSmile group six history financial. General try financial either discuss like million. Begin create fill series age.\nExist control popular begin deep. Sit another health live.

Politics side finally senior sit here activity protect. Heavy major control education. Bad involve want skill project feel.\nNone usually kid study eight. Civil consider effort. Marriage front their live eye significant far.

Scene keep major bank up prepare others. Change century brother media energy alone. Life range explain interest address.\nMedical account indicate hit start live support. Prove popular claim direction college.

', + attachments: [], + isStarred: false, + labels: ['private'], + time: '2021-07-04T03:37:29', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 804622, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'lauramartin@hotmail.com', + name: 'Connie Osborne', + avatar: avatar8, + }, + subject: 'Heavy ball debate style message main rate.', + cc: [], + bcc: [], + message: '

Feeling ability finish kitchen majority same moment. Decision money compare really education deal. Officer get be food ahead compare stay.\nDeep teacher state. Guy purpose too remain help enough.

Cut city father while green both information.\nLetter left fall body general. Very exactly common though policy star. Former health arm respond treatment.\nEnter industry will trouble day authority agree blood. Indeed air until but idea nor enter.

Site direction lay hotel these. Role focus affect focus before. Gas fill figure rise marriage like offer child.\nAgainst wall either. Mind one ready total. Fly food why part for again season.

', + attachments: [], + isStarred: true, + labels: ['company', 'important', 'private'], + time: '2021-07-02T03:33:03', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 10, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'sean36@gmail.com', name: 'Ronald Buckley', avatar: avatar5 }, + subject: 'Blue both light anyone trial nor approach tough.', + cc: [], + bcc: [], + message: '

Take anything season ok. Nor than war fine speak happen. Where business hold continue message state for.\nMorning southern allow. Mission color camera how Republican behind. Learn five break suffer.

Over born sure continue. Option show meet however.\nModel no mean us. Enough as space herself article bring others. Place them need drive cost decide.

Million friend remain product eye Congress. Education near amount middle.\nSay key past if shoulder rule. Others mean behind case interesting bag near option. Step why example mean thus. Fish forget turn never kind boy anyone.

', + attachments: [], + isStarred: true, + labels: ['personal', 'important'], + time: '2021-07-11T11:09:30', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 9, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'wkline@yahoo.com', name: 'Jennifer Garcia', avatar: avatar6 }, + subject: 'Simply idea project health prevent beyond both after.', + cc: [], + bcc: [], + message: '

Second again well doctor because election necessary point. Campaign about from western themselves particular loss popular. During garden star couple water simply area.

Worker leave know mission southern. Sea eye walk moment.\nCamera executive education wall marriage say. Man tend perform. Issue area great financial note other guess.

Likely market physical heavy quite we.\nRecent how room page sit fast Congress fight. Interview establish watch water.\nLoss family picture mind consumer about PM. Safe natural size. Character recognize painting movie.

', + attachments: [], + isStarred: false, + labels: ['personal', 'company'], + time: '2021-07-17T20:25:41', + replies: [], + folder: 'inbox', + isRead: false, + isDeleted: false, + }, + { + id: 8, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'tolson@gmail.com', name: 'Lindsey Melton', avatar: avatar2 }, + subject: 'Amount collection marriage price.', + cc: [], + bcc: [], + message: '

Join list dog rate doctor surface share. Meeting beat particular sing apply space.\nClear down thought magazine meet.\nWould better sport wide personal matter. Analysis effort school officer such. Age blue future her start marriage.

Material year close beat rest happy. Interview material over thought. Win until morning certainly.\nDevelopment personal direction game present.

Accept wall price hair garden staff. Enough off rest. Beyond half small lay agency.\nOption in hand charge direction least message. Safe minute situation just floor. Guess month than already.

', + attachments: [], + isStarred: false, + labels: ['company'], + time: '2021-07-25T05:19:46', + replies: [], + folder: 'inbox', + isRead: false, + isDeleted: false, + }, + { + id: 7, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'jeffrey89@gmail.com', name: 'Amanda Pratt', avatar: avatar4 }, + subject: 'Pull clear protect start exactly purpose scientist food.', + cc: [], + bcc: [], + message: '

See beautiful necessary hold. Marriage TV cut look chance whom.\nHeavy girl like only special position hot throw.\nReligious someone value girl save avoid. Market soon against central baby. So follow paper run along bag.

Worry provide form. Walk receive adult.\nMind style campaign blood. Public sign allow history nature customer. Offer how answer join.\nDiscussion blue Congress half important beat without. Authority key personal forget quickly model quickly really.

Better know magazine. Attention discuss staff turn affect tough.\nSo later whose reveal follow. Almost someone end. Rate necessary dog strategy.\nHope administration born his. Upon foot vote ability medical. Poor behind stage opportunity.

', + attachments: [], + isStarred: false, + labels: ['company'], + time: '2021-07-12T16:41:20', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 6, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'gentryjeff@yahoo.com', name: 'Joseph Clark', avatar: avatar3 }, + subject: 'Grow seat discover.', + cc: [], + bcc: [], + message: '

Become laugh and up onto. Sister raise pretty material picture. Own middle region open.\nProcess rock throw kind.\nQuestion them interest some international notice agreement. Control remember purpose.

Level consumer contain process rise system. Ten responsibility finally detail development else.\nRace well letter. Over receive it might.\nDifferent use send than he everyone. Drive answer develop bad past budget.

Increase prove theory million lose down quickly.\nMoment young just position information.\nName discover different majority use seek. Religious world discover never pressure ok develop. Name also all. Drug city program way.

', + attachments: [], + isStarred: true, + labels: ['personal', 'private'], + time: '2021-07-20T13:44:07', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + { + id: 5, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'maciaspatricia@hotmail.com', + name: 'Alisha Hughes', + avatar: avatar4, + }, + subject: 'Play hope gas military that.', + cc: [], + bcc: [], + message: '

International hundred anything see ten but long. Collection edge difference turn other let price. Would ahead commercial may scene develop minute.\nOnly film avoid. Last dark party store. Collection another three movement network ready hit.

Report keep probably individual argue.\nKid activity style million. Late stage lawyer answer.\nReligious both opportunity wide. Once television amount necessary so line. Now simple shoulder ground.

Radio idea glass realize research floor. Why range brother baby own impact century. Believe service doctor once.\nKnowledge finally anything sea. Across certainly reality provide. Past center feeling financial.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-04T09:53:05', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 4, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'rvalenzuela@hotmail.com', + name: 'Michelle Murphy', + avatar: avatar6, + }, + subject: 'Anyone want yet forget effect.', + cc: [], + bcc: [], + message: '

Expert space school material success security interest. Realize size seem growth game evidence. Time itself fine travel.\nCup reason environmental analysis.

Chance election look. Pretty job they officer other.\nBrother challenge military dark. Decade behavior several few race ball along. Amount rich suddenly stand. Mention street local site.

Join thus employee determine degree lead player. Color room ever soon easy. Administration toward experience why.\nSea hard detail rule. Strong factor language enjoy find.

', + attachments: [], + isStarred: false, + labels: ['company', 'important'], + time: '2021-07-17T15:51:47', + replies: [], + folder: 'spam', + isRead: false, + isDeleted: false, + }, + { + id: 3, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'gbeltran@gmail.com', name: 'Charles Cooper', avatar: avatar1 }, + subject: 'Fight account night short.', + cc: [], + bcc: [], + message: '

Get through stay. On difficult popular.\nFine turn mean artist. President explain turn professor fly prove cultural. Moment field front.\nSuccess almost various week. North message herself front eight. Final huge right happy.

Analysis rise son let. Age specific against visit.\nPerhaps series unit center total. Bed hour sense. Star morning history design late.\nOnce but fund share education. Majority face what year interest wish financial pretty.

Class treat enjoy stock seven natural establish indeed.\nHelp eat figure rich. Although bill discover build town.\nAsk continue claim here hand surface. Success foot action close treat.

', + attachments: [ + { + fileName: 'log.txt', + thumbnail: txt, + url: '', + size: '5mb', + }, + { + fileName: 'performance.xls', + thumbnail: xls, + url: '', + size: '10mb', + }, + ], + isStarred: true, + labels: ['important', 'company'], + time: '2021-07-22T19:12:31', + replies: [ + { + id: 756051771, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'rwhitehead@yahoo.com', + name: 'Bruce Johnson', + avatar: avatar3, + }, + subject: 'Guy someone wind.', + cc: [], + bcc: [], + message: '

Century those system character. Enter mind she baby compare movie. Soldier reality guy end meeting go.\nPositive only our important. Month world century impact nothing such bar. Term their himself safe its deep.

Coach bank agent value glass race. Instead reason suffer bar role action finally town. Political market window of although least will.\nGuess thought chance term.

Pressure tonight beyond because wait early leader prove. Ground reality court event bar. Behind manage really so four vote.\nSecond series score thus.\nRealize move around baby interview clear.

', + attachments: [], + isStarred: true, + labels: ['personal', 'private'], + time: '2021-07-22T16:03:07', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 4255040, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'perezannette@gmail.com', + name: 'Kyle Christensen', + avatar: avatar7, + }, + subject: 'Each close probably.', + cc: [], + bcc: [], + message: '

Late contain dream why ready go spring to. Against page medical wonder just fall card four. Unit live manager within feeling.\nSupport democratic lose list law. Baby address inside area or. Little individual remain sister area since thousand.

Culture effect similar clear population stuff himself quite. Trade story quality quite successful such.\nEven might his continue necessary thousand give. Record former tend determine true population reflect.

Dream when TV try loss central. Billion direction up run reduce that record. Ability then best draw.\nRich second yourself deep about foreign impact. Crime military appear shoulder bed. West job call home health woman lot.

', + attachments: [], + isStarred: true, + labels: ['personal'], + time: '2021-07-15T20:54:36', + replies: [], + folder: 'draft', + isRead: false, + isDeleted: false, + }, + { + id: 946586133, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'ramirezsarah@yahoo.com', + name: 'Tammy Lloyd', + avatar: avatar8, + }, + subject: 'Security set letter once.', + cc: [], + bcc: [], + message: '

Hair kind piece main want evening career. Water artist source ago south design father. Mention movie number house yeah some government.\nScore rock idea seven establish of. Candidate oil fact about to spend about.

Not both energy key.\nMust face those idea address pull.\nLet look cover star place later. Personal student both window agency produce.\nRemember cause hour explain box worry. One upon might soon enter baby car consumer.

Character service your idea. Adult guess stay us. Law would improve.\nWithin official anyone Mr. Difference before record treatment perhaps audience culture. Along present experience because history challenge detail.

', + attachments: [], + isStarred: true, + labels: ['company', 'important'], + time: '2021-07-20T05:34:05', + replies: [], + folder: 'spam', + isRead: true, + isDeleted: false, + }, + { + id: 182449812, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'evansantonio@yahoo.com', + name: 'Shawn Flores', + avatar: avatar5, + }, + subject: 'Card yeah need shake.', + cc: [], + bcc: [], + message: '

Fine wonder sister order rock conference lose should. Personal party drug sense way north. Hear stock political pick model.

Focus population expert sense past green. Call community property tough news instead bad. War explain former quite else explain next guy. Education like send method method.

Necessary detail teacher company discuss world activity. And me get star eat power. Read sound wish already culture seek because face. Attorney purpose green.

', + attachments: [], + isStarred: false, + labels: ['personal', 'private'], + time: '2021-07-22T14:31:03', + replies: [], + folder: 'inbox', + isRead: true, + isDeleted: true, + }, + ], + folder: 'inbox', + isRead: true, + isDeleted: false, + }, + { + id: 2, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { + email: 'wilsonwilliam@yahoo.com', + name: 'Rachel Palmer', + avatar: avatar2, + }, + subject: 'Account base lose detail.', + cc: [], + bcc: [], + message: '

Religious system evidence star meeting notice draw. Garden audience sometimes strong imagine vote free.\nLow Republican nice. Toward fund decade ever. Likely itself serve camera risk adult imagine.

Main nice environmental address defense. Toward movie inside every. Else event message continue.\nReturn rise attorney black role. Individual build tonight soldier return environment successful. Dinner learn rock mother wife all.

Yard but card her then. Foreign evening ability my president dog guess. Leave husband south.\nHealth leg represent yeah. Turn sell onto kid several. Morning degree few.\nStart dark measure big end role. Property attention walk eye exist.

', + attachments: [], + isStarred: false, + labels: ['important'], + time: '2021-07-10T01:13:20', + replies: [], + folder: 'draft', + isRead: true, + isDeleted: false, + }, + { + id: 1, + to: [{ email: 'johndoe@mail.com', name: 'me' }], + from: { email: 'edavid@yahoo.com', name: 'Wendy Harris', avatar: avatar2 }, + subject: 'Step face collection heart light cultural prepare.', + cc: [], + bcc: [], + message: '

Suddenly man team would nor piece. Miss democratic receive.\nWindow measure drug success recent necessary group mission. Exist school under student rock trial treatment.\nRun season there social. Visit staff floor network improve home the.

Lay laugh sea sit food parent. Line move scientist floor establish like production. Decade PM exist moment.\nBeat under campaign say. Term gun local Congress democratic.

Chance poor attack far kitchen will. Appear thing also child whom manage hospital. Federal trouble fear between receive such involve here.\nSeek wife increase draw hair. Onto style minute democratic. Clearly music outside standard.

', + attachments: [], + isStarred: true, + labels: ['important', 'private'], + time: '2021-07-18T11:43:46', + replies: [], + folder: 'sent', + isRead: true, + isDeleted: false, + }, + ], +} + + +// ------------------------------------------------ +// GET: Return Emails +// ------------------------------------------------ +mock.onGet('/apps/email/emails').reply(config => { + const { q = '', filter = 'inbox', label } = config.params + const queryLowered = q.toLowerCase() + function isInFolder(email) { + if (filter === 'trashed') + return email.isDeleted + if (filter === 'starred') + return email.isStarred && !email.isDeleted + + return email.folder === (filter || email.folder) && !email.isDeleted + } + + const filteredData = data.emails.filter(email => (email.from.name.toLowerCase().includes(queryLowered) || email.subject.toLowerCase().includes(queryLowered)) + && isInFolder(email) + && (label ? email.labels.includes(label) : true)) + + + // ------------------------------------------------ + // Email Meta + // ------------------------------------------------ + const emailsMeta = { + inbox: data.emails.filter(email => !email.isDeleted && !email.isRead && email.folder === 'inbox').length, + draft: data.emails.filter(email => email.folder === 'draft').length, + spam: data.emails.filter(email => !email.isDeleted && !email.isRead && email.folder === 'spam').length, + } + + return [ + 200, + { + emails: filteredData.reverse(), + emailsMeta, + }, + ] +}) + +// ------------------------------------------------ +// POST: Update Email +// ------------------------------------------------ +mock.onPost('/apps/email/update-emails/').reply(config => { + const { ids: emailIds, data: dataToUpdate } = JSON.parse(config.data) + function updateMailData(email) { + Object.assign(email, dataToUpdate) + } + data.emails.forEach(email => { + if (emailIds.includes(email.id)) + updateMailData(email) + }) + + return [200] +}) + +// ------------------------------------------------ +// POST: Update Emails Label +// ------------------------------------------------ +mock.onPost('/apps/email/update-emails-label').reply(config => { + const { ids: emailIds, label } = JSON.parse(config.data) + function updateMailLabels(email) { + const labelIndex = email.labels.indexOf(label) + if (labelIndex === -1) + email.labels.push(label) + else + email.labels.splice(labelIndex, 1) + } + data.emails.forEach(email => { + if (emailIds.includes(email.id)) + updateMailLabels(email) + }) + + return [200] +}) + +// ------------------------------------------------ +// GET: Paginate Existing Email +// ------------------------------------------------ +// mock.onGet('/apps/email/paginate-email').reply(config => { +// const { dir, emailId } = config.params +// const currentEmailIndex = data.emails.findIndex(e => e.id === emailId) +// const newEmailIndex = dir === 'previous' ? currentEmailIndex - 1 : currentEmailIndex + 1 +// const newEmail = data.emails[newEmailIndex] +// return newEmail ? [200, newEmail] : [404] +// }) diff --git a/resources/js/@fake-db/apps/invoice.js b/resources/js/@fake-db/apps/invoice.js new file mode 100644 index 0000000..fd8896b --- /dev/null +++ b/resources/js/@fake-db/apps/invoice.js @@ -0,0 +1,1041 @@ +import mock from '@/@fake-db/mock' +import { paginateArray } from '@/@fake-db/utils' +import avatar1 from '@images/avatars/avatar-1.png' +import avatar2 from '@images/avatars/avatar-2.png' +import avatar3 from '@images/avatars/avatar-3.png' +import avatar4 from '@images/avatars/avatar-4.png' +import avatar5 from '@images/avatars/avatar-5.png' +import avatar6 from '@images/avatars/avatar-6.png' +import avatar7 from '@images/avatars/avatar-7.png' +import avatar8 from '@images/avatars/avatar-8.png' + +const now = new Date() +const currentMonth = now.toLocaleString('default', { month: '2-digit' }) + +const database = [ + { + id: 4987, + issuedDate: `${now.getFullYear()}-${currentMonth}-13`, + client: { + address: '7777 Mendez Plains', + company: 'Hall-Robbins PLC', + companyEmail: 'don85@johnson.com', + country: 'USA', + contact: '(616) 865-4180', + name: 'Jordan Stevenson', + }, + service: 'Software Development', + total: 3428, + avatar: '', + invoiceStatus: 'Paid', + balance: 724, + dueDate: `${now.getFullYear()}-${currentMonth}-23`, + }, + { + id: 4988, + issuedDate: `${now.getFullYear()}-${currentMonth}-17`, + client: { + address: '04033 Wesley Wall Apt. 961', + company: 'Mccann LLC and Sons', + companyEmail: 'brenda49@taylor.info', + country: 'Haiti', + contact: '(226) 204-8287', + name: 'Stephanie Burns', + }, + service: 'UI/UX Design & Development', + total: 5219, + avatar: avatar1, + invoiceStatus: 'Downloaded', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-15`, + }, + { + id: 4989, + issuedDate: `${now.getFullYear()}-${currentMonth}-19`, + client: { + address: '5345 Robert Squares', + company: 'Leonard-Garcia and Sons', + companyEmail: 'smithtiffany@powers.com', + country: 'Denmark', + contact: '(955) 676-1076', + name: 'Tony Herrera', + }, + service: 'Unlimited Extended License', + total: 3719, + invoiceStatus: 'Paid', + avatar: avatar2, + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-03`, + }, + { + id: 4990, + issuedDate: `${now.getFullYear()}-${currentMonth}-06`, + client: { + address: '19022 Clark Parks Suite 149', + company: 'Smith, Miller and Henry LLC', + companyEmail: 'mejiageorge@lee-perez.com', + country: 'Cambodia', + contact: '(832) 323-6914', + name: 'Kevin Patton', + }, + service: 'Software Development', + total: 4749, + avatar: avatar3, + invoiceStatus: 'Sent', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-11`, + }, + { + id: 4991, + issuedDate: `${now.getFullYear()}-${currentMonth}-08`, + client: { + address: '8534 Saunders Hill Apt. 583', + company: 'Garcia-Cameron and Sons', + companyEmail: 'brandon07@pierce.com', + country: 'Martinique', + contact: '(970) 982-3353', + name: 'Mrs. Julie Donovan MD', + }, + service: 'UI/UX Design & Development', + total: 4056, + avatar: avatar4, + invoiceStatus: 'Draft', + balance: 815, + dueDate: `${now.getFullYear()}-${currentMonth}-30`, + }, + { + id: 4992, + issuedDate: `${now.getFullYear()}-${currentMonth}-26`, + client: { + address: '661 Perez Run Apt. 778', + company: 'Burnett-Young PLC', + companyEmail: 'guerrerobrandy@beasley-harper.com', + country: 'Botswana', + contact: '(511) 938-9617', + name: 'Amanda Phillips', + }, + service: 'UI/UX Design & Development', + total: 2771, + avatar: '', + invoiceStatus: 'Paid', + balance: 2771, + dueDate: `${now.getFullYear()}-${currentMonth}-24`, + }, + { + id: 4993, + issuedDate: `${now.getFullYear()}-${currentMonth}-17`, + client: { + address: '074 Long Union', + company: 'Wilson-Lee LLC', + companyEmail: 'williamshenry@moon-smith.com', + country: 'Montserrat', + contact: '(504) 859-2893', + name: 'Christina Collier', + }, + service: 'UI/UX Design & Development', + total: 2713, + avatar: '', + invoiceStatus: 'Draft', + balance: 407, + dueDate: `${now.getFullYear()}-${currentMonth}-22`, + }, + { + id: 4994, + issuedDate: `${now.getFullYear()}-${currentMonth}-11`, + client: { + address: '5225 Ford Cape Apt. 840', + company: 'Schwartz, Henry and Rhodes Group', + companyEmail: 'margaretharvey@russell-murray.com', + country: 'Oman', + contact: '(758) 403-7718', + name: 'David Flores', + }, + service: 'Template Customization', + total: 4309, + avatar: avatar5, + invoiceStatus: 'Paid', + balance: -205, + dueDate: `${now.getFullYear()}-${currentMonth}-13`, + }, + { + id: 4995, + issuedDate: `${now.getFullYear()}-${currentMonth}-16`, + client: { + address: '23717 James Club Suite 277', + company: 'Henderson-Holder PLC', + companyEmail: 'dianarodriguez@villegas.com', + country: 'Cambodia', + contact: '(292) 873-8254', + name: 'Valerie Perez', + }, + service: 'Software Development', + total: 3367, + avatar: avatar6, + invoiceStatus: 'Downloaded', + balance: 3367, + dueDate: `${now.getFullYear()}-${currentMonth}-24`, + }, + { + id: 4996, + issuedDate: `${now.getFullYear()}-${currentMonth}-15`, + client: { + address: '4528 Myers Gateway', + company: 'Page-Wise PLC', + companyEmail: 'bwilson@norris-brock.com', + country: 'Guam', + contact: '(956) 803-2008', + name: 'Susan Dickerson', + }, + service: 'Software Development', + total: 4776, + avatar: avatar7, + invoiceStatus: 'Downloaded', + balance: 305, + dueDate: `${now.getFullYear()}-${currentMonth}-02`, + }, + { + id: 4997, + issuedDate: `${now.getFullYear()}-${currentMonth}-27`, + client: { + address: '4234 Mills Club Suite 107', + company: 'Turner PLC Inc', + companyEmail: 'markcampbell@bell.info', + country: 'United States Virgin Islands', + contact: '(716) 962-8635', + name: 'Kelly Smith', + }, + service: 'Unlimited Extended License', + total: 3789, + avatar: avatar8, + invoiceStatus: 'Partial Payment', + balance: 666, + dueDate: `${now.getFullYear()}-${currentMonth}-18`, + }, + { + id: 4998, + issuedDate: `${now.getFullYear()}-${currentMonth}-31`, + client: { + address: '476 Keith Meadow', + company: 'Levine-Dorsey PLC', + companyEmail: 'mary61@rosario.com', + country: 'Syrian Arab Republic', + contact: '(523) 449-0782', + name: 'Jamie Jones', + }, + service: 'Unlimited Extended License', + total: 5200, + avatar: avatar2, + invoiceStatus: 'Partial Payment', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-17`, + }, + { + id: 4999, + issuedDate: `${now.getFullYear()}-${currentMonth}-14`, + client: { + address: '56381 Ashley Village Apt. 332', + company: 'Hall, Thompson and Ramirez LLC', + companyEmail: 'sean22@cook.com', + country: 'Ukraine', + contact: '(583) 470-8356', + name: 'Ruben Garcia', + }, + service: 'Software Development', + total: 4558, + avatar: avatar1, + invoiceStatus: 'Paid', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-01`, + }, + { + id: 5000, + issuedDate: `${now.getFullYear()}-${currentMonth}-21`, + client: { + address: '6946 Gregory Plaza Apt. 310', + company: 'Lambert-Thomas Group', + companyEmail: 'mccoymatthew@lopez-jenkins.net', + country: 'Vanuatu', + contact: '(366) 906-6467', + name: 'Ryan Meyer', + }, + service: 'Template Customization', + total: 3503, + avatar: avatar7, + invoiceStatus: 'Paid', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-22`, + }, + { + id: 5001, + issuedDate: `${now.getFullYear()}-${currentMonth}-30`, + client: { + address: '64351 Andrew Lights', + company: 'Gregory-Haynes PLC', + companyEmail: 'novakshannon@mccarty-murillo.com', + country: 'Romania', + contact: '(320) 616-3915', + name: 'Valerie Valdez', + }, + service: 'Unlimited Extended License', + total: 5285, + avatar: avatar6, + invoiceStatus: 'Partial Payment', + balance: -202, + dueDate: `${now.getFullYear()}-${currentMonth}-02`, + }, + { + id: 5002, + issuedDate: `${now.getFullYear()}-${currentMonth}-21`, + client: { + address: '5702 Sarah Heights', + company: 'Wright-Schmidt LLC', + companyEmail: 'smithrachel@davis-rose.net', + country: 'Costa Rica', + contact: '(435) 899-1963', + name: 'Melissa Wheeler', + }, + service: 'UI/UX Design & Development', + total: 3668, + avatar: avatar5, + invoiceStatus: 'Downloaded', + balance: 731, + dueDate: `${now.getFullYear()}-${currentMonth}-15`, + }, + { + id: 5003, + issuedDate: `${now.getFullYear()}-${currentMonth}-30`, + client: { + address: '668 Robert Flats', + company: 'Russell-Abbott Ltd', + companyEmail: 'scott96@mejia.net', + country: 'Congo', + contact: '(254) 399-4728', + name: 'Alan Jimenez', + }, + service: 'Unlimited Extended License', + total: 4372, + avatar: '', + invoiceStatus: 'Sent', + balance: -344, + dueDate: `${now.getFullYear()}-${currentMonth}-17`, + }, + { + id: 5004, + issuedDate: `${now.getFullYear()}-${currentMonth}-27`, + client: { + address: '55642 Chang Extensions Suite 373', + company: 'Williams LLC Inc', + companyEmail: 'cramirez@ross-bass.biz', + country: 'Saint Pierre and Miquelon', + contact: '(648) 500-4338', + name: 'Jennifer Morris', + }, + service: 'Template Customization', + total: 3198, + avatar: avatar4, + invoiceStatus: 'Partial Payment', + balance: -253, + dueDate: `${now.getFullYear()}-${currentMonth}-16`, + }, + { + id: 5005, + issuedDate: `${now.getFullYear()}-${currentMonth}-30`, + client: { + address: '56694 Eric Orchard', + company: 'Hudson, Bell and Phillips PLC', + companyEmail: 'arielberg@wolfe-smith.com', + country: 'Uruguay', + contact: '(896) 544-3796', + name: 'Timothy Stevenson', + }, + service: 'Unlimited Extended License', + total: 5293, + avatar: '', + invoiceStatus: 'Past Due', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-01`, + }, + { + id: 5006, + issuedDate: `${now.getFullYear()}-${currentMonth}-10`, + client: { + address: '3727 Emma Island Suite 879', + company: 'Berry, Gonzalez and Heath Inc', + companyEmail: 'yrobinson@nichols.com', + country: 'Israel', + contact: '(236) 784-5142', + name: 'Erik Hayden', + }, + service: 'Template Customization', + total: 5612, + avatar: avatar3, + invoiceStatus: 'Downloaded', + balance: 883, + dueDate: `${now.getFullYear()}-${currentMonth}-12`, + }, + { + id: 5007, + issuedDate: `${now.getFullYear()}-${currentMonth}-01`, + client: { + address: '953 Miller Common Suite 580', + company: 'Martinez, Fuller and Chavez and Sons', + companyEmail: 'tatejennifer@allen.net', + country: 'Cook Islands', + contact: '(436) 717-2419', + name: 'Katherine Kennedy', + }, + service: 'Software Development', + total: 2230, + avatar: avatar2, + invoiceStatus: 'Sent', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-19`, + }, + { + id: 5008, + issuedDate: `${now.getFullYear()}-${currentMonth}-22`, + client: { + address: '808 Sullivan Street Apt. 135', + company: 'Wilson and Sons LLC', + companyEmail: 'gdurham@lee.com', + country: 'Nepal', + contact: '(489) 946-3041', + name: 'Monica Fuller', + }, + service: 'Unlimited Extended License', + total: 2032, + avatar: avatar1, + invoiceStatus: 'Partial Payment', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-30`, + }, + { + id: 5009, + issuedDate: `${now.getFullYear()}-${currentMonth}-30`, + client: { + address: '25135 Christopher Creek', + company: 'Hawkins, Johnston and Mcguire PLC', + companyEmail: 'jenny96@lawrence-thompson.com', + country: 'Kiribati', + contact: '(274) 246-3725', + name: 'Stacey Carter', + }, + service: 'UI/UX Design & Development', + total: 3128, + avatar: avatar8, + invoiceStatus: 'Paid', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-10`, + }, + { + id: 5010, + issuedDate: `${now.getFullYear()}-${currentMonth}-06`, + client: { + address: '81285 Rebecca Estates Suite 046', + company: 'Huynh-Mills and Sons', + companyEmail: 'jgutierrez@jackson.com', + country: 'Swaziland', + contact: '(258) 211-5970', + name: 'Chad Davis', + }, + service: 'Software Development', + total: 2060, + avatar: avatar7, + invoiceStatus: 'Downloaded', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-08`, + }, + { + id: 5011, + issuedDate: `${now.getFullYear()}-${currentMonth}-01`, + client: { + address: '3102 Briggs Dale Suite 118', + company: 'Jones-Cooley and Sons', + companyEmail: 'hunter14@jones.com', + country: 'Congo', + contact: '(593) 965-4100', + name: 'Chris Reyes', + }, + service: 'UI/UX Design & Development', + total: 4077, + avatar: '', + invoiceStatus: 'Draft', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-01`, + }, + { + id: 5012, + issuedDate: `${now.getFullYear()}-${currentMonth}-30`, + client: { + address: '811 Jill Skyway', + company: 'Jones PLC Ltd', + companyEmail: 'pricetodd@johnson-jenkins.com', + country: 'Brazil', + contact: '(585) 829-2603', + name: 'Laurie Summers', + }, + service: 'Template Customization', + total: 2872, + avatar: avatar6, + invoiceStatus: 'Partial Payment', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-18`, + }, + { + id: 5013, + issuedDate: `${now.getFullYear()}-${currentMonth}-05`, + client: { + address: '2223 Brandon Inlet Suite 597', + company: 'Jordan, Gomez and Ross Group', + companyEmail: 'perrydavid@chapman-rogers.com', + country: 'Congo', + contact: '(527) 351-5517', + name: 'Lindsay Wilson', + }, + service: 'Software Development', + total: 3740, + avatar: avatar4, + invoiceStatus: 'Draft', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-01`, + }, + { + id: 5014, + issuedDate: `${now.getFullYear()}-${currentMonth}-01`, + client: { + address: '08724 Barry Causeway', + company: 'Gonzalez, Moody and Glover LLC', + companyEmail: 'leahgriffin@carpenter.com', + country: 'Equatorial Guinea', + contact: '(628) 903-0132', + name: 'Jenna Castro', + }, + service: 'Unlimited Extended License', + total: 3623, + avatar: '', + invoiceStatus: 'Downloaded', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-23`, + }, + { + id: 5015, + issuedDate: `${now.getFullYear()}-${currentMonth}-16`, + client: { + address: '073 Holt Ramp Apt. 755', + company: 'Ashley-Pacheco Ltd', + companyEmail: 'esparzadaniel@allen.com', + country: 'Seychelles', + contact: '(847) 396-9904', + name: 'Wendy Weber', + }, + service: 'Software Development', + total: 2477, + avatar: avatar5, + invoiceStatus: 'Draft', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-01`, + }, + { + id: 5016, + issuedDate: `${now.getFullYear()}-${currentMonth}-24`, + client: { + address: '984 Sherry Trail Apt. 953', + company: 'Berry PLC Group', + companyEmail: 'todd34@owens-morgan.com', + country: 'Ireland', + contact: '(852) 249-4539', + name: 'April Yates', + }, + service: 'Unlimited Extended License', + total: 3904, + avatar: '', + invoiceStatus: 'Paid', + balance: 951, + dueDate: `${now.getFullYear()}-${currentMonth}-30`, + }, + { + id: 5017, + issuedDate: `${now.getFullYear()}-${currentMonth}-24`, + client: { + address: '093 Jonathan Camp Suite 953', + company: 'Allen Group Ltd', + companyEmail: 'roydavid@bailey.com', + country: 'Netherlands', + contact: '(917) 984-2232', + name: 'Daniel Marshall PhD', + }, + service: 'UI/UX Design & Development', + total: 3102, + avatar: avatar3, + invoiceStatus: 'Partial Payment', + balance: -153, + dueDate: `${now.getFullYear()}-${currentMonth}-25`, + }, + { + id: 5018, + issuedDate: `${now.getFullYear()}-${currentMonth}-29`, + client: { + address: '4735 Kristie Islands Apt. 259', + company: 'Chapman-Schneider LLC', + companyEmail: 'baldwinjoel@washington.com', + country: 'Cocos (Keeling) Islands', + contact: '(670) 409-3703', + name: 'Randy Rich', + }, + service: 'UI/UX Design & Development', + total: 2483, + avatar: avatar2, + invoiceStatus: 'Draft', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-10`, + }, + { + id: 5019, + issuedDate: `${now.getFullYear()}-${currentMonth}-07`, + client: { + address: '92218 Andrew Radial', + company: 'Mcclure, Hernandez and Simon Ltd', + companyEmail: 'psmith@morris.info', + country: 'Macao', + contact: '(646) 263-0257', + name: 'Mrs. Jodi Chapman', + }, + service: 'Unlimited Extended License', + total: 2825, + avatar: avatar1, + invoiceStatus: 'Partial Payment', + balance: -459, + dueDate: `${now.getFullYear()}-${currentMonth}-14`, + }, + { + id: 5020, + issuedDate: `${now.getFullYear()}-${currentMonth}-10`, + client: { + address: '2342 Michelle Valley', + company: 'Hamilton PLC and Sons', + companyEmail: 'lori06@morse.com', + country: 'Somalia', + contact: '(751) 213-4288', + name: 'Steven Myers', + }, + service: 'Unlimited Extended License', + total: 2029, + avatar: avatar2, + invoiceStatus: 'Past Due', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-28`, + }, + { + id: 5021, + issuedDate: `${now.getFullYear()}-${currentMonth}-02`, + client: { + address: '16039 Brittany Terrace Apt. 128', + company: 'Silva-Reeves LLC', + companyEmail: 'zpearson@miller.com', + country: 'Slovakia (Slovak Republic)', + contact: '(655) 649-7872', + name: 'Charles Alexander', + }, + service: 'Software Development', + total: 3208, + avatar: '', + invoiceStatus: 'Sent', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-06`, + }, + { + id: 5022, + issuedDate: `${now.getFullYear()}-${currentMonth}-02`, + client: { + address: '37856 Olsen Lakes Apt. 852', + company: 'Solis LLC Ltd', + companyEmail: 'strongpenny@young.net', + country: 'Brazil', + contact: '(402) 935-0735', + name: 'Elizabeth Jones', + }, + service: 'Software Development', + total: 3077, + avatar: '', + invoiceStatus: 'Sent', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-09`, + }, + { + id: 5023, + issuedDate: `${now.getFullYear()}-${currentMonth}-23`, + client: { + address: '11489 Griffin Plaza Apt. 927', + company: 'Munoz-Peters and Sons', + companyEmail: 'carrietorres@acosta.com', + country: 'Argentina', + contact: '(915) 448-6271', + name: 'Heidi Walton', + }, + service: 'Software Development', + total: 5578, + avatar: avatar4, + invoiceStatus: 'Draft', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-23`, + }, + { + id: 5024, + issuedDate: `${now.getFullYear()}-${currentMonth}-28`, + client: { + address: '276 Michael Gardens Apt. 004', + company: 'Shea, Velez and Garcia LLC', + companyEmail: 'zjohnson@nichols-powers.com', + country: 'Philippines', + contact: '(817) 700-2984', + name: 'Christopher Allen', + }, + service: 'Software Development', + total: 2787, + avatar: avatar5, + invoiceStatus: 'Partial Payment', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-25`, + }, + { + id: 5025, + issuedDate: `${now.getFullYear()}-${currentMonth}-21`, + client: { + address: '633 Bell Well Apt. 057', + company: 'Adams, Simmons and Brown Group', + companyEmail: 'kayla09@thomas.com', + country: 'Martinique', + contact: '(266) 611-9482', + name: 'Joseph Oliver', + }, + service: 'UI/UX Design & Development', + total: 5591, + avatar: '', + invoiceStatus: 'Downloaded', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-07`, + }, + { + id: 5026, + issuedDate: `${now.getFullYear()}-${currentMonth}-24`, + client: { + address: '1068 Lopez Fall', + company: 'Williams-Lawrence and Sons', + companyEmail: 'melvindavis@allen.info', + country: 'Mexico', + contact: '(739) 745-9728', + name: 'Megan Roberts', + }, + service: 'Template Customization', + total: 2783, + avatar: avatar6, + invoiceStatus: 'Draft', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-22`, + }, + { + id: 5027, + issuedDate: `${now.getFullYear()}-${currentMonth}-13`, + client: { + address: '86691 Mackenzie Light Suite 568', + company: 'Deleon Inc LLC', + companyEmail: 'gjordan@fernandez-coleman.com', + country: 'Costa Rica', + contact: '(682) 804-6506', + name: 'Mary Garcia', + }, + service: 'Template Customization', + total: 2719, + avatar: '', + invoiceStatus: 'Sent', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-04`, + }, + { + id: 5028, + issuedDate: `${now.getFullYear()}-${currentMonth}-18`, + client: { + address: '86580 Sarah Bridge', + company: 'Farmer, Johnson and Anderson Group', + companyEmail: 'robertscott@garcia.com', + country: 'Cameroon', + contact: '(775) 366-0411', + name: 'Crystal Mays', + }, + service: 'Template Customization', + total: 3325, + avatar: '', + invoiceStatus: 'Paid', + balance: 361, + dueDate: `${now.getFullYear()}-${currentMonth}-02`, + }, + { + id: 5029, + issuedDate: `${now.getFullYear()}-${currentMonth}-29`, + client: { + address: '49709 Edwin Ports Apt. 353', + company: 'Sherman-Johnson PLC', + companyEmail: 'desiree61@kelly.com', + country: 'Macedonia', + contact: '(510) 536-6029', + name: 'Nicholas Tanner', + }, + service: 'Template Customization', + total: 3851, + avatar: '', + invoiceStatus: 'Paid', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-25`, + }, + { + id: 5030, + issuedDate: `${now.getFullYear()}-${currentMonth}-07`, + client: { + address: '3856 Mathis Squares Apt. 584', + company: 'Byrd LLC PLC', + companyEmail: 'jeffrey25@martinez-hodge.com', + country: 'Congo', + contact: '(253) 230-4657', + name: 'Justin Richardson', + }, + service: 'Template Customization', + total: 5565, + avatar: '', + invoiceStatus: 'Draft', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-06`, + }, + { + id: 5031, + issuedDate: `${now.getFullYear()}-${currentMonth}-21`, + client: { + address: '141 Adrian Ridge Suite 550', + company: 'Stone-Zimmerman Group', + companyEmail: 'john77@anderson.net', + country: 'Falkland Islands (Malvinas)', + contact: '(612) 546-3485', + name: 'Jennifer Summers', + }, + service: 'Template Customization', + total: 3313, + avatar: avatar7, + invoiceStatus: 'Partial Payment', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-09`, + }, + { + id: 5032, + issuedDate: `${now.getFullYear()}-${currentMonth}-31`, + client: { + address: '01871 Kristy Square', + company: 'Yang, Hansen and Hart PLC', + companyEmail: 'ywagner@jones.com', + country: 'Germany', + contact: '(203) 601-8603', + name: 'Richard Payne', + }, + service: 'Template Customization', + total: 5181, + avatar: '', + invoiceStatus: 'Past Due', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-29`, + }, + { + id: 5033, + issuedDate: `${now.getFullYear()}-${currentMonth}-12`, + client: { + address: '075 Smith Views', + company: 'Jenkins-Rosales Inc', + companyEmail: 'calvin07@joseph-edwards.org', + country: 'Colombia', + contact: '(895) 401-4255', + name: 'Lori Wells', + }, + service: 'Template Customization', + total: 2869, + avatar: avatar4, + invoiceStatus: 'Partial Payment', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-22`, + }, + { + id: 5034, + issuedDate: `${now.getFullYear()}-${currentMonth}-10`, + client: { + address: '2577 Pearson Overpass Apt. 314', + company: 'Mason-Reed PLC', + companyEmail: 'eric47@george-castillo.com', + country: 'Paraguay', + contact: '(602) 336-9806', + name: 'Tammy Sanchez', + }, + service: 'Unlimited Extended License', + total: 4836, + avatar: '', + invoiceStatus: 'Paid', + balance: 0, + dueDate: `${now.getFullYear()}-${currentMonth}-22`, + }, + { + id: 5035, + issuedDate: `${now.getFullYear()}-${currentMonth}-20`, + client: { + address: '1770 Sandra Mountains Suite 636', + company: 'Foster-Pham PLC', + companyEmail: 'jamesjoel@chapman.net', + country: 'Western Sahara', + contact: '(936) 550-1638', + name: 'Dana Carey', + }, + service: 'UI/UX Design & Development', + total: 4263, + avatar: '', + invoiceStatus: 'Draft', + balance: 762, + dueDate: `${now.getFullYear()}-${currentMonth}-12`, + }, + { + id: 5036, + issuedDate: `${now.getFullYear()}-${currentMonth}-19`, + client: { + address: '78083 Laura Pines', + company: 'Richardson and Sons LLC', + companyEmail: 'pwillis@cross.org', + country: 'Bhutan', + contact: '(687) 660-2473', + name: 'Andrew Burns', + }, + service: 'Unlimited Extended License', + total: 3171, + avatar: avatar3, + invoiceStatus: 'Paid', + balance: -205, + dueDate: `${now.getFullYear()}-${currentMonth}-25`, + }, +] + + +// 👉 Get invoice list +// eslint-disable-next-line sonarjs/cognitive-complexity +mock.onGet('/apps/invoices').reply(config => { + const { q = '', status = null, startDate = '', endDate = '', options = {} } = config.params ?? {} + const { sortBy = '', page = 1, itemsPerPage = 10 } = options + const sort = JSON.parse(JSON.stringify(sortBy)) + const queryLowered = q.toLowerCase() + + // Filtering invoices + let filteredInvoices = database.filter(invoice => ((invoice.client.name.toLowerCase().includes(queryLowered) + || invoice.client.companyEmail.toLowerCase().includes(queryLowered) + || invoice.total.toString().includes(queryLowered) + || invoice.issuedDate.toString().includes(queryLowered) + || invoice.id.toString().includes(queryLowered)) + && invoice.invoiceStatus === (status || invoice.invoiceStatus))).reverse() + + // Sorting invoices + if (sort.length) { + if (sort[0]?.key === 'client') { + filteredInvoices = filteredInvoices.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.client.name.localeCompare(b.client.name) + + return b.client.name.localeCompare(a.client.name) + }) + } + else if (sort[0]?.key === 'total') { + filteredInvoices = filteredInvoices.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.total - b.total + + return b.total - a.total + }) + } + else if (sort[0]?.key === 'id') { + filteredInvoices = filteredInvoices.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.id - b.id + + return b.id - a.id + }) + } + else if (sort[0]?.key === 'date') { + filteredInvoices = filteredInvoices.sort((a, b) => { + if (sort[0]?.order === 'asc') + return new Date(a.issuedDate).getTime() - new Date(b.issuedDate).getTime() + + return new Date(b.issuedDate).getTime() - new Date(a.issuedDate).getTime() + }) + } + else if (sort[0]?.key === 'balance') { + filteredInvoices = filteredInvoices.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.balance - b.balance + + return b.balance - a.balance + }) + } + } + + // filtering invoices by date + if (startDate && endDate) { + filteredInvoices = filteredInvoices.filter(invoiceObj => { + const start = new Date(startDate).getTime() + const end = new Date(endDate).getTime() + const issuedDate = new Date(invoiceObj.issuedDate).getTime() + + return issuedDate >= start && issuedDate <= end + }) + } + const totalInvoices = filteredInvoices.length + + return [200, { invoices: paginateArray(filteredInvoices, itemsPerPage, page), totalInvoices, page: page > Math.ceil(totalInvoices / itemsPerPage) ? 1 : page }] +}) + +// 👉 Get a single invoice +mock.onGet(/\/apps\/invoices\/\d+/).reply(config => { + // Get event id from URL + const invoiceId = config.url?.substring(config.url.lastIndexOf('/') + 1) + + // Convert Id to number + const id = Number(invoiceId) + const invoice = database.find(e => e.id === id) + if (!invoice) + return [404, { message: 'Unable to find the requested invoice' }] + + const responseData = { + invoice, + paymentDetails: { + totalDue: '$12,110.55', + bankName: 'American Bank', + country: 'United States', + iban: 'ETD95476213874685', + swiftCode: 'BR91905', + }, + } + + return [200, responseData] +}) + +// 👉 Get Client +mock.onGet('/apps/invoice/clients').reply(() => { + const clients = database.map(invoice => invoice.client) + + return [200, clients.slice(0, 5)] +}) + +// 👉 Delete Invoice +mock.onDelete(/\/apps\/invoices\/\d+/).reply(config => { + // Get event id from URL + const invoiceId = config.url?.substring(config.url.lastIndexOf('/') + 1) + + // Convert Id to number + const id = Number(invoiceId) + const invoiceIndex = database.findIndex(e => e.id === id) + if (invoiceIndex >= 0) { + database.splice(invoiceIndex, 1) + + return [200] + } + + return [400] +}) diff --git a/resources/js/@fake-db/apps/permissions.js b/resources/js/@fake-db/apps/permissions.js new file mode 100644 index 0000000..c0d1bcd --- /dev/null +++ b/resources/js/@fake-db/apps/permissions.js @@ -0,0 +1,97 @@ +import mock from '@/@fake-db/mock' +import { paginateArray } from '@/@fake-db/utils' + +const data = { + permissions: [ + { + id: 1, + name: 'Management', + assignedTo: ['administrator'], + createdDate: '14 Apr 2021, 8:43 PM', + }, + { + id: 2, + assignedTo: ['administrator'], + name: 'Manage Billing & Roles', + createdDate: '16 Sep 2021, 5:20 PM', + }, + { + id: 3, + name: 'Add & Remove Users', + createdDate: '14 Oct 2021, 10:20 AM', + assignedTo: ['administrator', 'manager'], + }, + { + id: 4, + name: 'Project Planning', + createdDate: '14 Oct 2021, 10:20 AM', + assignedTo: ['administrator', 'users', 'support'], + }, + { + id: 5, + name: 'Manage Email Sequences', + createdDate: '23 Aug 2021, 2:00 PM', + assignedTo: ['administrator', 'users', 'support'], + }, + { + id: 6, + name: 'Client Communication', + createdDate: '15 Apr 2021, 11:30 AM', + assignedTo: ['administrator', 'manager'], + }, + { + id: 7, + name: 'Only View', + createdDate: '04 Dec 2021, 8:15 PM', + assignedTo: ['administrator', 'restricted-user'], + }, + { + id: 8, + name: 'Financial Management', + createdDate: '25 Feb 2021, 10:30 AM', + assignedTo: ['administrator', 'manager'], + }, + { + id: 9, + name: 'Manage Others\' Tasks', + createdDate: '04 Nov 2021, 11:45 AM', + assignedTo: ['administrator', 'support'], + }, + ], +} + + +// ------------------------------------------------ +// GET: Return Permissions List +// ------------------------------------------------ +mock.onGet('/apps/permissions/data').reply(config => { + const { q = '', options = {} } = config.params ?? {} + const { sortBy = '', page = 1, itemsPerPage = 10 } = options + const sort = JSON.parse(JSON.stringify(sortBy)) + const queryLowered = q.toLowerCase() + let filteredData = data.permissions.filter(permissions => permissions.name.toLowerCase().includes(queryLowered) + || permissions.createdDate.toLowerCase().includes(queryLowered) + || permissions.assignedTo.some(i => i.toLowerCase().startsWith(queryLowered))) + + // Sorting invoices + if (sort.length && sort[0]?.key === 'name') { + filteredData = filteredData.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.name.localeCompare(b.name) + + return b.name.localeCompare(a.name) + }) + } + + // total pages + const totalPages = Math.ceil(filteredData.length / itemsPerPage) + + return [ + 200, + { + permissions: paginateArray(filteredData, itemsPerPage, page), + totalPermissions: filteredData.length, + totalPages, + }, + ] +}) diff --git a/resources/js/@fake-db/apps/user-list.js b/resources/js/@fake-db/apps/user-list.js new file mode 100644 index 0000000..2e180a2 --- /dev/null +++ b/resources/js/@fake-db/apps/user-list.js @@ -0,0 +1,773 @@ +import mock from '@/@fake-db/mock' +import { genId, paginateArray } from '@/@fake-db/utils' +import avatar1 from '@images/avatars/avatar-1.png' +import avatar2 from '@images/avatars/avatar-2.png' +import avatar3 from '@images/avatars/avatar-3.png' +import avatar4 from '@images/avatars/avatar-4.png' +import avatar5 from '@images/avatars/avatar-5.png' +import avatar6 from '@images/avatars/avatar-6.png' +import avatar7 from '@images/avatars/avatar-7.png' +import avatar8 from '@images/avatars/avatar-8.png' + +const users = [ + { + id: 1, + fullName: 'Galen Slixby', + company: 'Yotz PVT LTD', + role: 'editor', + country: 'El Salvador', + contact: '(479) 232-9151', + email: 'gslixby0@abc.net.au', + currentPlan: 'enterprise', + status: 'inactive', + billing: 'Auto Debit', + avatar: '', + }, + { + id: 2, + fullName: 'Halsey Redmore', + company: 'Skinder PVT LTD', + role: 'author', + country: 'Albania', + contact: '(472) 607-9137', + email: 'hredmore1@imgur.com', + currentPlan: 'team', + status: 'pending', + avatar: avatar1, + billing: 'Manual - Paypal', + }, + { + id: 3, + fullName: 'Marjory Sicely', + company: 'Oozz PVT LTD', + role: 'maintainer', + country: 'Russia', + contact: '(321) 264-4599', + email: 'msicely2@who.int', + currentPlan: 'enterprise', + status: 'active', + avatar: avatar1, + billing: 'Manual - Cash', + }, + { + id: 4, + fullName: 'Cyrill Risby', + company: 'Oozz PVT LTD', + role: 'maintainer', + country: 'China', + contact: '(923) 690-6806', + email: 'crisby3@wordpress.com', + currentPlan: 'team', + status: 'inactive', + avatar: avatar3, + billing: 'Manual - Credit Card', + }, + { + id: 5, + fullName: 'Maggy Hurran', + company: 'Aimbo PVT LTD', + role: 'subscriber', + country: 'Pakistan', + contact: '(669) 914-1078', + email: 'mhurran4@yahoo.co.jp', + currentPlan: 'enterprise', + status: 'pending', + avatar: avatar1, + billing: 'Auto Debit', + }, + { + id: 6, + fullName: 'Silvain Halstead', + company: 'Jaxbean PVT LTD', + role: 'author', + country: 'China', + contact: '(958) 973-3093', + email: 'shalstead5@shinystat.com', + currentPlan: 'company', + status: 'active', + avatar: '', + billing: 'Manual - Cash', + }, + { + id: 7, + fullName: 'Breena Gallemore', + company: 'Jazzy PVT LTD', + role: 'subscriber', + country: 'Canada', + contact: '(825) 977-8152', + email: 'bgallemore6@boston.com', + currentPlan: 'company', + status: 'pending', + avatar: '', + billing: 'Manual - Cash', + }, + { + id: 8, + fullName: 'Kathryne Liger', + company: 'Pixoboo PVT LTD', + role: 'author', + country: 'France', + contact: '(187) 440-0934', + email: 'kliger7@vinaora.com', + currentPlan: 'enterprise', + status: 'pending', + avatar: avatar4, + billing: 'Manual - Cash', + }, + { + id: 9, + fullName: 'Franz Scotfurth', + company: 'Tekfly PVT LTD', + role: 'subscriber', + country: 'China', + contact: '(978) 146-5443', + email: 'fscotfurth8@dailymotion.com', + currentPlan: 'team', + status: 'pending', + avatar: avatar2, + billing: 'Manual - Cash', + }, + { + id: 10, + fullName: 'Jillene Bellany', + company: 'Gigashots PVT LTD', + role: 'maintainer', + country: 'Jamaica', + contact: '(589) 284-6732', + email: 'jbellany9@kickstarter.com', + currentPlan: 'company', + status: 'inactive', + avatar: avatar5, + billing: 'Manual - Cash', + }, + { + id: 11, + fullName: 'Jonah Wharlton', + company: 'Eare PVT LTD', + role: 'subscriber', + country: 'United States', + contact: '(176) 532-6824', + email: 'jwharltona@oakley.com', + currentPlan: 'team', + status: 'inactive', + avatar: avatar4, + billing: 'Manual - Cash', + }, + { + id: 12, + fullName: 'Seth Hallam', + company: 'Yakitri PVT LTD', + role: 'subscriber', + country: 'Peru', + contact: '(234) 464-0600', + email: 'shallamb@hugedomains.com', + currentPlan: 'team', + status: 'pending', + avatar: avatar5, + billing: 'Manual - Cash', + }, + { + id: 13, + fullName: 'Yoko Pottie', + company: 'Leenti PVT LTD', + role: 'subscriber', + country: 'Philippines', + contact: '(907) 284-5083', + email: 'ypottiec@privacy.gov.au', + currentPlan: 'basic', + status: 'inactive', + avatar: avatar7, + billing: 'Manual - Cash', + }, + { + id: 14, + fullName: 'Maximilianus Krause', + company: 'Digitube PVT LTD', + role: 'author', + country: 'Democratic Republic of the Congo', + contact: '(167) 135-7392', + email: 'mkraused@stanford.edu', + currentPlan: 'team', + status: 'active', + avatar: avatar6, + billing: 'Manual - Cash', + }, + { + id: 15, + fullName: 'Zsazsa McCleverty', + company: 'Kaymbo PVT LTD', + role: 'maintainer', + country: 'France', + contact: '(317) 409-6565', + email: 'zmcclevertye@soundcloud.com', + currentPlan: 'enterprise', + status: 'active', + avatar: avatar2, + billing: 'Manual - Cash', + }, + { + id: 16, + fullName: 'Bentlee Emblin', + company: 'Yambee PVT LTD', + role: 'author', + country: 'Spain', + contact: '(590) 606-1056', + email: 'bemblinf@wired.com', + currentPlan: 'company', + status: 'active', + avatar: avatar6, + billing: 'Manual - Cash', + }, + { + id: 17, + fullName: 'Brockie Myles', + company: 'Wikivu PVT LTD', + role: 'maintainer', + country: 'Poland', + contact: '(553) 225-9905', + email: 'bmylesg@amazon.com', + currentPlan: 'basic', + status: 'active', + avatar: '', + billing: 'Manual - Cash', + }, + { + id: 18, + fullName: 'Bertha Biner', + company: 'Twinte PVT LTD', + role: 'editor', + country: 'Yemen', + contact: '(901) 916-9287', + email: 'bbinerh@mozilla.com', + currentPlan: 'team', + status: 'active', + avatar: avatar7, + billing: 'Manual - Cash', + }, + { + id: 19, + fullName: 'Travus Bruntjen', + company: 'Cogidoo PVT LTD', + role: 'admin', + country: 'France', + contact: '(524) 586-6057', + email: 'tbruntjeni@sitemeter.com', + currentPlan: 'enterprise', + status: 'active', + avatar: '', + billing: 'Auto Debit', + }, + { + id: 20, + fullName: 'Wesley Burland', + company: 'Bubblemix PVT LTD', + role: 'editor', + country: 'Honduras', + contact: '(569) 683-1292', + email: 'wburlandj@uiuc.edu', + currentPlan: 'team', + status: 'inactive', + avatar: avatar6, + billing: 'Manual - Cash', + }, + { + id: 21, + fullName: 'Selina Kyle', + company: 'Wayne Enterprises', + role: 'admin', + country: 'USA', + contact: '(829) 537-0057', + email: 'irena.dubrovna@wayne.com', + currentPlan: 'team', + status: 'active', + avatar: avatar1, + billing: 'Manual - Cash', + }, + { + id: 22, + fullName: 'Jameson Lyster', + company: 'Quaxo PVT LTD', + role: 'editor', + country: 'Ukraine', + contact: '(593) 624-0222', + email: 'jlysterl@guardian.co.uk', + currentPlan: 'company', + status: 'inactive', + avatar: avatar8, + billing: 'Auto Debit', + }, + { + id: 23, + fullName: 'Kare Skitterel', + company: 'Ainyx PVT LTD', + role: 'maintainer', + country: 'Poland', + contact: '(254) 845-4107', + email: 'kskitterelm@ainyx.com', + currentPlan: 'basic', + status: 'pending', + avatar: avatar3, + billing: 'Manual - Cash', + }, + { + id: 24, + fullName: 'Cleavland Hatherleigh', + company: 'Flipopia PVT LTD', + role: 'admin', + country: 'Brazil', + contact: '(700) 783-7498', + email: 'chatherleighn@washington.edu', + currentPlan: 'team', + status: 'pending', + avatar: avatar2, + billing: 'Manual - Cash', + }, + { + id: 25, + fullName: 'Adeline Micco', + company: 'Topicware PVT LTD', + role: 'admin', + country: 'France', + contact: '(227) 598-1841', + email: 'amiccoo@whitehouse.gov', + currentPlan: 'enterprise', + status: 'pending', + avatar: '', + billing: 'Manual - Cash', + }, + { + id: 26, + fullName: 'Hugh Hasson', + company: 'Skinix PVT LTD', + role: 'admin', + country: 'China', + contact: '(582) 516-1324', + email: 'hhassonp@bizjournals.com', + currentPlan: 'basic', + status: 'inactive', + avatar: avatar4, + billing: 'Auto Debit', + }, + { + id: 27, + fullName: 'Germain Jacombs', + company: 'Youopia PVT LTD', + role: 'editor', + country: 'Zambia', + contact: '(137) 467-5393', + email: 'gjacombsq@jigsy.com', + currentPlan: 'enterprise', + status: 'active', + avatar: avatar5, + billing: 'Auto Debit', + }, + { + id: 28, + fullName: 'Bree Kilday', + company: 'Jetpulse PVT LTD', + role: 'maintainer', + country: 'Portugal', + contact: '(412) 476-0854', + email: 'bkildayr@mashable.com', + currentPlan: 'team', + status: 'active', + avatar: '', + billing: 'Auto Debit', + }, + { + id: 29, + fullName: 'Candice Pinyon', + company: 'Kare PVT LTD', + role: 'maintainer', + country: 'Sweden', + contact: '(170) 683-1520', + email: 'cpinyons@behance.net', + currentPlan: 'team', + status: 'active', + avatar: avatar7, + billing: 'Auto Debit', + }, + { + id: 30, + fullName: 'Isabel Mallindine', + company: 'Voomm PVT LTD', + role: 'subscriber', + country: 'Slovenia', + contact: '(332) 803-1983', + email: 'imallindinet@shinystat.com', + currentPlan: 'team', + status: 'pending', + avatar: '', + billing: 'Auto Debit', + }, + { + id: 31, + fullName: 'Gwendolyn Meineken', + company: 'Oyondu PVT LTD', + role: 'admin', + country: 'Moldova', + contact: '(551) 379-7460', + email: 'gmeinekenu@hc360.com', + currentPlan: 'basic', + status: 'pending', + avatar: avatar1, + billing: 'Auto Debit', + }, + { + id: 32, + fullName: 'Rafaellle Snowball', + company: 'Fivespan PVT LTD', + role: 'editor', + country: 'Philippines', + contact: '(974) 829-0911', + email: 'rsnowballv@indiegogo.com', + currentPlan: 'basic', + status: 'pending', + avatar: avatar5, + billing: 'Auto Debit', + }, + { + id: 33, + fullName: 'Rochette Emer', + company: 'Thoughtworks PVT LTD', + role: 'admin', + country: 'North Korea', + contact: '(841) 889-3339', + email: 'remerw@blogtalkradio.com', + currentPlan: 'basic', + status: 'active', + avatar: avatar8, + billing: 'Manual - Paypal', + }, + { + id: 34, + fullName: 'Ophelie Fibbens', + company: 'Jaxbean PVT LTD', + role: 'subscriber', + country: 'Indonesia', + contact: '(764) 885-7351', + email: 'ofibbensx@booking.com', + currentPlan: 'company', + status: 'active', + avatar: avatar4, + billing: 'Manual - Paypal', + }, + { + id: 35, + fullName: 'Stephen MacGilfoyle', + company: 'Browseblab PVT LTD', + role: 'maintainer', + country: 'Japan', + contact: '(350) 589-8520', + email: 'smacgilfoyley@bigcartel.com', + currentPlan: 'company', + status: 'pending', + avatar: '', + billing: 'Manual - Paypal', + }, + { + id: 36, + fullName: 'Bradan Rosebotham', + company: 'Agivu PVT LTD', + role: 'subscriber', + country: 'Belarus', + contact: '(882) 933-2180', + email: 'brosebothamz@tripadvisor.com', + currentPlan: 'team', + status: 'inactive', + avatar: '', + billing: 'Manual - Paypal', + }, + { + id: 37, + fullName: 'Skip Hebblethwaite', + company: 'Katz PVT LTD', + role: 'admin', + country: 'Canada', + contact: '(610) 343-1024', + email: 'shebblethwaite10@arizona.edu', + currentPlan: 'company', + status: 'inactive', + avatar: avatar1, + billing: 'Manual - Paypal', + }, + { + id: 38, + fullName: 'Moritz Piccard', + company: 'Twitternation PVT LTD', + role: 'maintainer', + country: 'Croatia', + contact: '(365) 277-2986', + email: 'mpiccard11@vimeo.com', + currentPlan: 'enterprise', + status: 'inactive', + avatar: avatar1, + billing: 'Auto Debit', + }, + { + id: 39, + fullName: 'Tyne Widmore', + company: 'Yombu PVT LTD', + role: 'subscriber', + country: 'Finland', + contact: '(531) 731-0928', + email: 'twidmore12@bravesites.com', + currentPlan: 'team', + status: 'pending', + avatar: '', + billing: 'Manual - Credit Card', + }, + { + id: 40, + fullName: 'Florenza Desporte', + company: 'Kamba PVT LTD', + role: 'author', + country: 'Ukraine', + contact: '(312) 104-2638', + email: 'fdesporte13@omniture.com', + currentPlan: 'company', + status: 'active', + avatar: avatar6, + billing: 'Manual - Credit Card', + }, + { + id: 41, + fullName: 'Edwina Baldetti', + company: 'Dazzlesphere PVT LTD', + role: 'maintainer', + country: 'Haiti', + contact: '(315) 329-3578', + email: 'ebaldetti14@theguardian.com', + currentPlan: 'team', + status: 'pending', + avatar: '', + billing: 'Manual - Cash', + }, + { + id: 42, + fullName: 'Benedetto Rossiter', + company: 'Mybuzz PVT LTD', + role: 'editor', + country: 'Indonesia', + contact: '(323) 175-6741', + email: 'brossiter15@craigslist.org', + currentPlan: 'team', + status: 'inactive', + avatar: '', + billing: 'Manual - Credit Card', + }, + { + id: 43, + fullName: 'Micaela McNirlan', + company: 'Tambee PVT LTD', + role: 'admin', + country: 'Indonesia', + contact: '(242) 952-0916', + email: 'mmcnirlan16@hc360.com', + currentPlan: 'basic', + status: 'inactive', + avatar: '', + billing: 'Manual - Credit Card', + }, + { + id: 44, + fullName: 'Vladamir Koschek', + company: 'Centimia PVT LTD', + role: 'author', + country: 'Guatemala', + contact: '(531) 758-8335', + email: 'vkoschek17@abc.net.au', + currentPlan: 'team', + status: 'active', + avatar: '', + billing: 'Auto Debit', + }, + { + id: 45, + fullName: 'Corrie Perot', + company: 'Flipopia PVT LTD', + role: 'subscriber', + country: 'China', + contact: '(659) 385-6808', + email: 'cperot18@goo.ne.jp', + currentPlan: 'team', + status: 'pending', + avatar: avatar3, + billing: 'Manual - Paypal', + }, + { + id: 46, + fullName: 'Saunder Offner', + company: 'Skalith PVT LTD', + role: 'maintainer', + country: 'Poland', + contact: '(200) 586-2264', + email: 'soffner19@mac.com', + currentPlan: 'enterprise', + status: 'pending', + avatar: '', + billing: 'Auto Debit', + }, + { + id: 47, + fullName: 'Karena Courtliff', + company: 'Feedfire PVT LTD', + role: 'admin', + country: 'China', + contact: '(478) 199-0020', + email: 'kcourtliff1a@bbc.co.uk', + currentPlan: 'basic', + status: 'active', + avatar: avatar1, + billing: 'Manual - Credit Card', + }, + { + id: 48, + fullName: 'Onfre Wind', + company: 'Thoughtmix PVT LTD', + role: 'admin', + country: 'Ukraine', + contact: '(344) 262-7270', + email: 'owind1b@yandex.ru', + currentPlan: 'basic', + status: 'pending', + avatar: '', + billing: 'Manual - Credit Card', + }, + { + id: 49, + fullName: 'Paulie Durber', + company: 'Babbleblab PVT LTD', + role: 'subscriber', + country: 'Sweden', + contact: '(694) 676-1275', + email: 'pdurber1c@gov.uk', + currentPlan: 'team', + status: 'inactive', + avatar: '', + billing: 'Auto Debit', + }, + { + id: 50, + fullName: 'Beverlie Krabbe', + company: 'Kaymbo PVT LTD', + role: 'editor', + country: 'China', + contact: '(397) 294-5153', + email: 'bkrabbe1d@home.pl', + currentPlan: 'company', + status: 'active', + avatar: avatar2, + billing: 'Manual - Credit Card', + }, +] + + +// 👉 return users +// eslint-disable-next-line sonarjs/cognitive-complexity +mock.onGet('/apps/users/list').reply(config => { + const { q = '', role = null, plan = null, status = null, options = {} } = config.params ?? {} + const { sortBy = '', itemsPerPage = 10, page = 1 } = options + const queryLower = q.toLowerCase() + + // filter users + let filteredUsers = users.filter(user => ((user.fullName.toLowerCase().includes(queryLower) || user.email.toLowerCase().includes(queryLower)) && user.role === (role || user.role) && user.currentPlan === (plan || user.currentPlan) && user.status === (status || user.status))).reverse() + + // sort users + const sort = JSON.parse(JSON.stringify(sortBy)) + if (sort.length) { + if (sort[0]?.key === 'user') { + filteredUsers = filteredUsers.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.fullName.localeCompare(b.fullName) + else + return b.fullName.localeCompare(a.fullName) + }) + } + if (sort[0]?.key === 'billing') { + filteredUsers = filteredUsers.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.billing.localeCompare(b.billing) + else + return b.billing.localeCompare(a.billing) + }) + } + if (sort[0]?.key === 'role') { + filteredUsers = filteredUsers.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.role.localeCompare(b.role) + else + return b.role.localeCompare(a.role) + }) + } + if (sort[0]?.key === 'plan') { + filteredUsers = filteredUsers.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.currentPlan.localeCompare(b.currentPlan) + else + return b.currentPlan.localeCompare(a.currentPlan) + }) + } + if (sort[0]?.key === 'status') { + filteredUsers = filteredUsers.sort((a, b) => { + if (sort[0]?.order === 'asc') + return a.status.localeCompare(b.status) + else + return b.status.localeCompare(a.status) + }) + } + } + const totalUsers = filteredUsers.length + + // total pages + const totalPages = Math.ceil(totalUsers / itemsPerPage) + + return [200, { users: paginateArray(filteredUsers, itemsPerPage, page), totalPages, totalUsers, page: page > Math.ceil(totalUsers / itemsPerPage) ? 1 : page }] +}) + +// 👉 Add user +mock.onPost('/apps/users/user').reply(config => { + const { user } = JSON.parse(config.data) + + user.id = genId(users) + users.push(user) + + return [201, { user }] +}) + +// 👉 Get Single user +mock.onGet(/\/apps\/users\/\d+/).reply(config => { + // Get event id from URL + const userId = config.url?.substring(config.url.lastIndexOf('/') + 1) + + // Convert Id to number + const Id = Number(userId) + const userIndex = users.findIndex(e => e.id === Id) + const user = users[userIndex] + + Object.assign(user, { + taskDone: 1230, + projectDone: 568, + taxId: 'Tax-8894', + language: 'English', + }) + if (user) + return [200, user] + + return [404] +}) +mock.onDelete(/\/apps\/users\/\d+/).reply(config => { + // Get user id from URL + const userId = config.url?.substring(config.url.lastIndexOf('/') + 1) + + // Convert Id to number + const Id = Number(userId) + const userIndex = users.findIndex(e => e.id === Id) + if (userIndex >= 0) { + users.splice(userIndex, 1) + + return [200] + } + + return [400] +}) diff --git a/resources/js/@fake-db/db.js b/resources/js/@fake-db/db.js new file mode 100644 index 0000000..53a391a --- /dev/null +++ b/resources/js/@fake-db/db.js @@ -0,0 +1,17 @@ +import './app-bar-search' +import './apps/user-list' +import './jwt' +import mock from './mock' +import './pages/datatable' + + +// Apps +import './apps/chat' + +import './apps/invoice' +import './apps/permissions' + +// Dashboard + +// forwards the matched request over network +mock.onAny().passThrough() diff --git a/resources/js/@fake-db/jwt/index.js b/resources/js/@fake-db/jwt/index.js new file mode 100644 index 0000000..4287a6a --- /dev/null +++ b/resources/js/@fake-db/jwt/index.js @@ -0,0 +1,162 @@ +import mock from '@/@fake-db/mock' +import { genId } from '@/@fake-db/utils' +import avatar1 from '@images/avatars/avatar-1.png' +import avatar2 from '@images/avatars/avatar-2.png' + + +// TODO: Use jsonwebtoken pkg +// ℹ️ Created from https://jwt.io/ using HS256 algorithm +// ℹ️ We didn't created it programmatically because jsonwebtoken package have issues with esm support. View Issues: https://github.com/auth0/node-jsonwebtoken/issues/655 +const userTokens = [ + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.fhc3wykrAnRpcKApKhXiahxaOe8PSHatad31NuIZ0Zg', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.cat2xMrZLn0FwicdGtZNzL7ifDTAKWB0k1RurSWjdnw', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.PGOfMaZA_T9W05vMj5FYXG5d47soSPJD1WuxeUfw4L4', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NH0.d_9aq2tpeA9-qpqO0X4AmW6gU2UpWkXwc04UJYFWiZE', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NX0.ocO77FbjOSU1-JQ_BilEZq2G_M8bCiB10KYqtfkv1ss', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Nn0.YgQILRqZy8oefhTZgJJfiEzLmhxQT_Bd2510OvrrwB8', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6N30.KH9RmOWIYv_HONxajg7xBIJXHEUvSdcBygFtS2if8Jk', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OH0.shrp-oMHkVAkiMkv_aIvSx3k6Jk-X7TrH5UeufChz_g', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OX0.9JD1MR3ZkwHzhl4mOHH6lGG8hOVNZqDNH6UkFzjCqSE', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTB9.txWLuN4QT5PqTtgHmlOiNerIu5Do51PpYOiZutkyXYg', +] + + +// ❗ These two secrets shall be in .env file and not in any other file +// const jwtSecret = 'dd5f3089-40c3-403d-af14-d0c228b05cb4' +const database = [ + { + id: 1, + fullName: 'John Doe', + username: 'johndoe', + password: 'admin', + avatar: avatar1, + email: 'admin@demo.com', + role: 'admin', + abilities: [ + { + action: 'manage', + subject: 'all', + }, + ], + }, + { + id: 2, + fullName: 'Jane Doe', + username: 'janedoe', + password: 'client', + avatar: avatar2, + email: 'client@demo.com', + role: 'client', + abilities: [ + { + action: 'read', + subject: 'Auth', + }, + { + action: 'read', + subject: 'AclDemo', + }, + ], + }, +] + +mock.onPost('/auth/login').reply(request => { + const { email, password } = JSON.parse(request.data) + let errors = { + email: ['Something went wrong'], + } + const user = database.find(u => u.email === email && u.password === password) + if (user) { + try { + const accessToken = userTokens[user.id] + + // We are duplicating user here + const userData = { ...user } + + const userOutData = Object.fromEntries(Object.entries(userData) + .filter(([key, _]) => !(key === 'password' || key === 'abilities'))) + + const response = { + userAbilities: userData.abilities, + accessToken, + userData: userOutData, + } + + + // const accessToken = jwt.sign({ id: user.id }, jwtSecret) + return [200, response] + } + catch (e) { + errors = { email: [e] } + } + } + else { + errors = { + email: ['Email or Password is Invalid'], + } + } + + return [400, { errors }] +}) +mock.onPost('/auth/register').reply(request => { + const { username, email, password } = JSON.parse(request.data) + + // If not any of data is missing return 400 + if (!(username && email && password)) + return [400] + const isEmailAlreadyInUse = database.find(user => user.email === email) + const isUsernameAlreadyInUse = database.find(user => user.username === username) + + const errors = { + password: !password ? ['Please enter password'] : null, + email: (() => { + if (!email) + return ['Please enter your email.'] + if (isEmailAlreadyInUse) + return ['This email is already in use.'] + + return null + })(), + username: (() => { + if (!username) + return ['Please enter your username.'] + if (isUsernameAlreadyInUse) + return ['This username is already in use.'] + + return null + })(), + } + + if (!errors.username && !errors.email) { + // Calculate user id + const userData = { + id: genId(database), + email, + password, + username, + fullName: '', + role: 'admin', + abilities: [ + { + action: 'manage', + subject: 'all', + }, + ], + } + + database.push(userData) + + const accessToken = userTokens[userData.id] + const { password: _, abilities, ...user } = userData + + const response = { + userData: user, + accessToken, + userAbilities: abilities, + } + + return [200, response] + } + + return [400, { error: errors }] +}) diff --git a/resources/js/@fake-db/mock.js b/resources/js/@fake-db/mock.js new file mode 100644 index 0000000..b5192d2 --- /dev/null +++ b/resources/js/@fake-db/mock.js @@ -0,0 +1,6 @@ +import MockAdapter from 'axios-mock-adapter' +import axios from '@axios' + +// This sets the mock adapter on the axios instance +const mock = new MockAdapter(axios) +export default mock diff --git a/resources/js/@fake-db/pages/datatable.js b/resources/js/@fake-db/pages/datatable.js new file mode 100644 index 0000000..a2d86e4 --- /dev/null +++ b/resources/js/@fake-db/pages/datatable.js @@ -0,0 +1,1310 @@ +import mock from '@/@fake-db/mock' +import avatar1 from '@images/avatars/avatar-1.png' +import avatar2 from '@images/avatars/avatar-2.png' +import avatar3 from '@images/avatars/avatar-3.png' +import avatar4 from '@images/avatars/avatar-4.png' +import avatar5 from '@images/avatars/avatar-5.png' +import avatar6 from '@images/avatars/avatar-6.png' +import avatar7 from '@images/avatars/avatar-7.png' +import avatar8 from '@images/avatars/avatar-8.png' +import product10 from '@images/eCommerce/10.png' +import product11 from '@images/eCommerce/11.png' +import product13 from '@images/eCommerce/13.png' +import product14 from '@images/eCommerce/14.png' +import product15 from '@images/eCommerce/15.png' +import product16 from '@images/eCommerce/16.png' +import product17 from '@images/eCommerce/17.png' +import product18 from '@images/eCommerce/18.png' +import product19 from '@images/eCommerce/19.png' +import product20 from '@images/eCommerce/20.png' +import product23 from '@images/eCommerce/23.png' +import product24 from '@images/eCommerce/24.png' +import product25 from '@images/eCommerce/25.png' +import product26 from '@images/eCommerce/26.png' +import product3 from '@images/eCommerce/3.png' +import product4 from '@images/eCommerce/4.png' +import product5 from '@images/eCommerce/5.png' +import product6 from '@images/eCommerce/6.png' +import product7 from '@images/eCommerce/7.png' +import product8 from '@images/eCommerce/8.png' +import product9 from '@images/eCommerce/9.png' + +const data = [ + { + product: { + id: 19, + name: 'OnePlus 7 Pro ', + slug: 'one-plus-7-pro-19', + brand: 'Philips', + category: 'Smart Phone', + price: 14.99, + image: product9, + hasFreeShipping: false, + rating: 4, + description: 'The OnePlus 7 Pro features a brand new design, with a glass back and front and curved sides. The phone feels\n very premium but\u2019s it\u2019s also very heavy. The Nebula Blue variant looks slick but it\u2019s quite slippery, which\n makes single-handed use a real challenge. It has a massive 6.67-inch \u2018Fluid AMOLED\u2019 display with a QHD+\n resolution, 90Hz refresh rate and support for HDR 10+ content. The display produces vivid colours, deep blacks\n and has good viewing angles.', + }, + date: '30 Apr 2020', + buyer: { + name: 'Ana Smith', + avatar: avatar3, + }, + payment: { + total: 984, + received_payment_status: 'Fully Paid', + paid_amount: 984, + status: 'Completed', + }, + }, + { + product: { + id: 21, + name: 'Google - Google Home', + slug: 'google-google-home-white-slate-fabric-21', + brand: 'Google', + category: 'Google Home', + price: 129.29, + image: product7, + hasFreeShipping: true, + rating: 4, + description: 'Simplify your everyday life with the Google Home, a voice-activated speaker powered by the Google Assistant. Use\n voice commands to enjoy music, get answers from Google and manage everyday tasks. Google Home is compatible with\n Android and iOS operating systems, and can control compatible smart devices such as Chromecast or Nest.', + }, + date: '11 Jul 2020', + buyer: { + name: 'Lindsay Green', + avatar: avatar8, + }, + payment: { + total: 1101, + received_payment_status: 'Fully Paid', + paid_amount: 1101, + status: 'Completed', + }, + }, + { + product: { + id: 17, + name: 'Nike Air Max', + slug: '72-9301-speaker-wire-harness-adapter-for-most-plymouth-dodge-and-mitsubishi-vehicles-multi-17', + description: 'With a bold application of colorblocking inspired by modern art styles, the Nike Air Max 270 React sneaker is constructed with layers of lightweight material to achieve its artful look and comfortable feel.', + brand: 'Nike', + category: 'Shoes', + price: 81.99, + image: product11, + hasFreeShipping: true, + rating: 5, + }, + date: '06 Jan 2021', + buyer: { + name: 'Ethan Lee', + avatar: avatar1, + }, + payment: { + total: 726, + received_payment_status: 'Partially Paid', + paid_amount: 126, + status: 'Confirmed', + }, + }, + { + product: { + id: 2, + name: 'Bose Frames Tenor', + slug: 'bose-frames-tenor-rectangular-polarized-bluetooth-audio-sunglasses-2', + description: 'Redesigned for luxury \u2014 Thoughtfully refined and strikingly elegant, the latest Bose sunglasses blend enhanced features and designs for an elevated way to listen', + brand: 'Bose', + category: 'Glass', + price: 249, + image: product26, + hasFreeShipping: false, + rating: 4, + }, + date: '21 Aug 2020', + buyer: { + name: 'Scott Miller', + avatar: avatar7, + }, + payment: { + total: 646, + received_payment_status: 'Partially Paid', + paid_amount: 345, + status: 'Confirmed', + }, + }, + { + product: { + id: 25, + name: 'Apple iMac 27-inch', + slug: 'apple-i-mac-27-inch-25', + brand: 'Apple', + category: 'IMac', + price: 999.99, + image: product3, + hasFreeShipping: true, + rating: 4, + description: 'The all-in-one for all. If you can dream it, you can do it on iMac. It\u2019s beautifully & incredibly intuitive and\n packed with tools that let you take any idea to the next level. And the new 27-inch model elevates the\n experience in way, with faster processors and graphics, expanded memory and storage, enhanced audio and video\n capabilities, and an even more stunning Retina 5K display. It\u2019s the desktop that does it all \u2014 better and faster\n than ever.', + }, + date: '21 Aug 2020', + buyer: { + name: 'Brandon Brooks', + avatar: avatar5, + }, + payment: { + total: 1005, + received_payment_status: 'Partially Paid', + paid_amount: 21, + status: 'Confirmed', + }, + }, + { + product: { + id: 12, + name: 'Adidas Mens Tech Response Shoes', + slug: 'adidas-mens-tech-response-shoes-12', + description: 'Comfort + performance. Designed with materials that are durable, lightweight and extremely comfortable. Core performance delivers the perfect mix of fit, style and all-around performance.', + brand: 'Adidas', + category: 'Shoes', + price: 54.59, + image: product16, + hasFreeShipping: false, + rating: 5, + }, + date: '10 Mar 2021', + buyer: { + name: 'Henry Mann', + avatar: avatar6, + }, + payment: { + total: 1114, + received_payment_status: 'Partially Paid', + paid_amount: 814, + status: 'Confirmed', + }, + }, + { + product: { + id: 25, + name: 'Apple iMac 27-inch', + slug: 'apple-i-mac-27-inch-25', + brand: 'Apple', + category: 'IMac', + price: 999.99, + image: product3, + hasFreeShipping: true, + rating: 4, + description: 'The all-in-one for all. If you can dream it, you can do it on iMac. It\u2019s beautifully & incredibly intuitive and\n packed with tools that let you take any idea to the next level. And the new 27-inch model elevates the\n experience in way, with faster processors and graphics, expanded memory and storage, enhanced audio and video\n capabilities, and an even more stunning Retina 5K display. It\u2019s the desktop that does it all \u2014 better and faster\n than ever.', + }, + date: '21 Aug 2020', + buyer: { + name: 'Brandon Brooks', + avatar: avatar5, + }, + payment: { + total: 1005, + received_payment_status: 'Partially Paid', + paid_amount: 21, + status: 'Confirmed', + }, + }, + { + product: { + id: 24, + name: 'OneOdio A71 Wired Headphones', + slug: 'one-odio-a71-wired-headphones-24', + brand: 'OneOdio', + category: 'Headphone', + price: 49.99, + image: product4, + hasFreeShipping: true, + rating: 3, + description: 'Omnidirectional detachable boom mic upgrades the headphones into a professional headset for gaming, business,\n podcasting and taking calls on the go. Better pick up your voice. Control most electric devices through voice\n activation, or schedule a ride with Uber and order a pizza. OneOdio A71 Wired Headphones voice-controlled device\n turns any home into a smart device on a smartphone or tablet.', + }, + date: '12 Nov 2020', + buyer: { + name: 'Grant Wright', + avatar: avatar2, + }, + payment: { + total: 207, + received_payment_status: 'Fully Paid', + paid_amount: 207, + status: 'Completed', + }, + }, + { + product: { + id: 20, + name: 'Sony 4K Ultra HD LED TV ', + slug: 'sony-4-k-ultra-hd-led-tv-20', + brand: 'Apple', + category: 'Smart TV', + price: 7999.99, + image: product8, + hasFreeShipping: false, + rating: 5, + description: 'Sony 4K Ultra HD LED TV has 4K HDR Support. The TV provides clear visuals and provides distinct sound quality\n and an immersive experience. This TV has Yes HDMI ports & Yes USB ports. Connectivity options included are HDMI.\n You can connect various gadgets such as your laptop using the HDMI port. The TV comes with a 1 Year warranty.', + }, + date: '19 Apr 2021', + buyer: { + name: 'Amanda Sanchez', + avatar: avatar2, + }, + payment: { + total: 1119, + received_payment_status: 'Fully Paid', + paid_amount: 1119, + status: 'Completed', + }, + }, + { + product: { + id: 23, + name: 'Apple - MacBook Air\u00AE', + slug: 'apple-mac-book-air-latest-model-13-3-display-silver-23', + brand: 'Apple', + category: 'Mac', + price: 999.99, + image: product5, + hasFreeShipping: false, + rating: 4, + description: 'MacBook Air is a thin, lightweight laptop from Apple. MacBook Air features up to 8GB of memory, a\n fifth-generation Intel Core processor, Thunderbolt 2, great built-in apps, and all-day battery life.1 Its thin,\n light, and durable enough to take everywhere you go-and powerful enough to do everything once you get there,\n better.', + }, + date: '25 Dec 2020', + buyer: { + name: 'Kathy Estrada', + avatar: avatar2, + }, + payment: { + total: 1221, + received_payment_status: 'Partially Paid', + paid_amount: 1025, + status: 'Confirmed', + }, + }, + { + product: { + id: 25, + name: 'Apple iMac 27-inch', + slug: 'apple-i-mac-27-inch-25', + brand: 'Apple', + category: 'IMac', + price: 999.99, + image: product3, + hasFreeShipping: true, + rating: 4, + description: 'The all-in-one for all. If you can dream it, you can do it on iMac. It\u2019s beautifully & incredibly intuitive and\n packed with tools that let you take any idea to the next level. And the new 27-inch model elevates the\n experience in way, with faster processors and graphics, expanded memory and storage, enhanced audio and video\n capabilities, and an even more stunning Retina 5K display. It\u2019s the desktop that does it all \u2014 better and faster\n than ever.', + }, + date: '19 May 2020', + buyer: { + name: 'William Lopez', + avatar: avatar2, + }, + payment: { + total: 973, + received_payment_status: 'Partially Paid', + paid_amount: 374, + status: 'Confirmed', + }, + }, + { + product: { + id: 8, + name: 'PlayStation 4 Console', + slug: 'play-station-4-console-8', + description: 'All the greatest, games, TV, music and more. Connect with your friends to broadcast and celebrate your epic moments at the press of the Share button to Twitch, YouTube, Facebook and Twitter.', + brand: 'Sony', + category: 'Gaming', + price: 339.95, + image: product20, + hasFreeShipping: false, + rating: 4, + }, + date: '27 Mar 2021', + buyer: { + name: 'Colleen Taylor', + avatar: avatar2, + }, + payment: { + total: 1235, + received_payment_status: 'Fully Paid', + paid_amount: 1235, + status: 'Completed', + }, + }, + { + product: { + id: 5, + name: 'Toshiba Canvio External Hard Drive', + slug: 'toshiba-canvio-advance-2-tb-portable-external-hard-drive-5', + description: 'Up to 3TB of storage capacity to store your growing files and content', + brand: 'Toshiba', + category: 'Storage Device', + price: 69.99, + image: product23, + hasFreeShipping: true, + rating: 2, + }, + date: '21 Jun 2020', + buyer: { + name: 'Melanie Olson', + avatar: avatar6, + }, + payment: { + total: 780, + received_payment_status: 'Fully Paid', + paid_amount: 780, + status: 'Completed', + }, + }, + { + product: { + id: 19, + name: 'OnePlus 7 Pro ', + slug: 'one-plus-7-pro-19', + brand: 'Philips', + category: 'Smart Phone', + price: 14.99, + image: product9, + hasFreeShipping: false, + rating: 4, + description: 'The OnePlus 7 Pro features a brand new design, with a glass back and front and curved sides. The phone feels\n very premium but\u2019s it\u2019s also very heavy. The Nebula Blue variant looks slick but it\u2019s quite slippery, which\n makes single-handed use a real challenge. It has a massive 6.67-inch \u2018Fluid AMOLED\u2019 display with a QHD+\n resolution, 90Hz refresh rate and support for HDR 10+ content. The display produces vivid colours, deep blacks\n and has good viewing angles.', + }, + date: '28 Jan 2021', + buyer: { + name: 'Cynthia Cannon', + avatar: avatar7, + }, + payment: { + total: 1073, + received_payment_status: 'Partially Paid', + paid_amount: 871, + status: 'Confirmed', + }, + }, + { + product: { + id: 23, + name: 'Apple - MacBook Air\u00AE', + slug: 'apple-mac-book-air-latest-model-13-3-display-silver-23', + brand: 'Apple', + category: 'Mac', + price: 999.99, + image: product5, + hasFreeShipping: false, + rating: 4, + description: 'MacBook Air is a thin, lightweight laptop from Apple. MacBook Air features up to 8GB of memory, a\n fifth-generation Intel Core processor, Thunderbolt 2, great built-in apps, and all-day battery life.1 Its thin,\n light, and durable enough to take everywhere you go-and powerful enough to do everything once you get there,\n better.', + }, + date: '20 Aug 2020', + buyer: { + name: 'David Archer', + avatar: avatar5, + }, + payment: { + total: 224, + received_payment_status: 'Fully Paid', + paid_amount: 224, + status: 'Completed', + }, + }, + { + product: { + id: 9, + name: 'Giotto 32oz Leakproof BPA Free Drinking Water', + slug: 'giotto-32oz-leakproof-bpa-free-drinking-water-9', + description: 'With unique inspirational quote and time markers on it,this water bottle is great for measuring your daily intake of water,reminding you stay hydrated and drink enough water throughout the day.A must have for any fitness goals including weight loss,appetite control and overall health.', + brand: '3M', + category: 'Home', + price: 16.99, + image: product19, + hasFreeShipping: true, + rating: 4, + }, + date: '29 Dec 2020', + buyer: { + name: 'Michael Cervantes', + avatar: avatar8, + }, + payment: { + total: 960, + received_payment_status: 'Partially Paid', + paid_amount: 866, + status: 'Confirmed', + }, + }, + { + product: { + id: 13, + name: 'Laptop Bag', + slug: 'laptop-bag-13', + description: 'TSA FRIENDLY- A separate DIGI SMART compartment can hold 15.6 inch Laptop as well as 15 inch, 14 inch Macbook, 12.9 inch iPad, and tech accessories like charger for quick TSA checkpoint when traveling', + brand: 'TAS', + category: 'Bag', + price: 29.99, + image: product15, + hasFreeShipping: true, + rating: 5, + }, + date: '15 Aug 2020', + buyer: { + name: 'Nathaniel Marshall', + avatar: avatar6, + }, + payment: { + total: 1423, + received_payment_status: 'Unpaid', + paid_amount: 0, + status: 'Cancelled', + }, + }, + { + product: { + id: 5, + name: 'Toshiba Canvio External Hard Drive', + slug: 'toshiba-canvio-advance-2-tb-portable-external-hard-drive-5', + description: 'Up to 3TB of storage capacity to store your growing files and content', + brand: 'Toshiba', + category: 'Storage Device', + price: 69.99, + image: product23, + hasFreeShipping: true, + rating: 2, + }, + date: '03 Jan 2021', + buyer: { + name: 'Tiffany Ross', + avatar: avatar4, + }, + payment: { + total: 663, + received_payment_status: 'Partially Paid', + paid_amount: 285, + status: 'Confirmed', + }, + }, + { + product: { + id: 14, + name: 'Wireless Charger 5W Max', + slug: 'wireless-charger-5-w-max-14', + description: 'Charge with case: transmits charging power directly through protective cases. Rubber/plastic/TPU cases under 5 mm thickness . Do not use any magnetic and metal attachments or cards, or it will prevent charging.', + brand: '3M', + category: 'Electronics', + price: 10.83, + image: product14, + hasFreeShipping: true, + rating: 3, + }, + date: '20 Dec 2020', + buyer: { + name: 'Philip Walters', + avatar: null, + }, + payment: { + total: 1112, + received_payment_status: 'Partially Paid', + paid_amount: 426, + status: 'Confirmed', + }, + }, + { + product: { + id: 15, + name: 'Vankyo leisure 3 mini projector', + slug: '3-m-filtrete-vacuum-belt-for-select-hoover-t-series-upright-vacuums-15', + description: 'SUPERIOR VIEWING EXPERIENCE: Supporting 1920x1080 resolution, VANKYO Leisure 3 projector is powered by MStar Advanced Color Engine, which is ideal for home entertainment. 2020 upgraded LED lighting provides a superior viewing experience for you.', + brand: 'Vankyo Store', + category: 'Projector', + price: 99.99, + image: product13, + hasFreeShipping: true, + rating: 2, + }, + date: '02 Jul 2020', + buyer: { + name: 'Pamela Smith', + avatar: null, + }, + payment: { + total: 462, + received_payment_status: 'Partially Paid', + paid_amount: 383, + status: 'Confirmed', + }, + }, + { + product: { + id: 12, + name: 'Adidas Mens Tech Response Shoes', + slug: 'adidas-mens-tech-response-shoes-12', + description: 'Comfort + performance. Designed with materials that are durable, lightweight and extremely comfortable. Core performance delivers the perfect mix of fit, style and all-around performance.', + brand: 'Adidas', + category: 'Shoes', + price: 54.59, + image: product16, + hasFreeShipping: false, + rating: 5, + }, + date: '24 Jul 2020', + buyer: { + name: 'Kara Gonzalez', + avatar: avatar3, + }, + payment: { + total: 1325, + received_payment_status: 'Partially Paid', + paid_amount: 792, + status: 'Confirmed', + }, + }, + { + product: { + id: 18, + name: 'Logitech K380 Wireless Keyboard', + slug: 'acer-11-6-chromebook-intel-celeron-2-gb-memory-16-gb-e-mmc-flash-memory-moonstone-white-18', + description: 'Logitech K380 Bluetooth Wireless Keyboard gives you the comfort and convenience of desktop typing on your smartphone, and tablet. It is a wireless keyboard that connects to all Bluetooth wireless devices that support external keyboards. Take this compact, lightweight, Bluetooth keyboard anywhere in your home. Type wherever you like, on any compatible computer, phone or tablet.', + brand: 'Logitech', + category: 'Keyboard', + price: 81.99, + image: product10, + hasFreeShipping: false, + rating: 4, + }, + date: '07 Jan 2021', + buyer: { + name: 'Katherine Tate', + avatar: avatar8, + }, + payment: { + total: 582, + received_payment_status: 'Partially Paid', + paid_amount: 234, + status: 'Confirmed', + }, + }, + { + product: { + id: 3, + name: 'Willful Smart Watch for Men Women 2020,', + slug: 'willful-smart-watch-for-men-women-2020-3', + description: 'Are you looking for a smart watch, which can not only easily keep tracking of your steps, calories, heart rate and sleep quality, but also keep you informed of incoming calls.', + brand: 'Willful', + category: 'Smart Watch', + price: 29.99, + image: product25, + hasFreeShipping: true, + rating: 5, + }, + date: '29 Aug 2020', + buyer: { + name: 'Ashley Douglas DDS', + avatar: avatar3, + }, + payment: { + total: 1092, + received_payment_status: 'Fully Paid', + paid_amount: 1092, + status: 'Completed', + }, + }, + { + product: { + id: 22, + name: 'Switch Pro Controller', + slug: 'switch-pro-controller-22', + brand: 'Sharp', + category: 'Gaming', + price: 429.99, + image: product6, + hasFreeShipping: false, + rating: 3, + description: 'The Nintendo Switch Pro Controller is one of the priciest \'baseline\' controllers in the current console\n generation, but it\'s also sturdy, feels good to play with, has an excellent direction pad, and features\n impressive motion sensors and vibration systems. On top of all of that, it uses Bluetooth, so you don\'t need an\n adapter to use it with your PC.', + }, + date: '09 Jan 2021', + buyer: { + name: 'Eric Gregory', + avatar: avatar3, + }, + payment: { + total: 939, + received_payment_status: 'Fully Paid', + paid_amount: 939, + status: 'Completed', + }, + }, + { + product: { + id: 4, + name: 'Ronyes Unisex College Bag Bookbags for Women', + slug: 'ronyes-unisex-college-bag-bookbags-for-women-4', + description: 'Made of high quality water-resistant material; padded and adjustable shoulder straps; external USB with built-in charging cable offers a convenient charging', + brand: 'Ronyes', + category: 'Bag', + price: 23.99, + image: product24, + hasFreeShipping: true, + rating: 2, + }, + date: '06 May 2020', + buyer: { + name: 'Taylor Hernandez', + avatar: avatar3, + }, + payment: { + total: 1129, + received_payment_status: 'Unpaid', + paid_amount: 0, + status: 'Cancelled', + }, + }, + { + product: { + id: 10, + name: 'Oculus Quest All-in-one VR', + slug: 'oculus-quest-all-in-one-vr-10', + description: 'All-in-one VR: No PC. No wires. No limits. Oculus quest is an all-in-one gaming system built for virtual reality. Now you can play almost anywhere with just a VR headset and controllers. Oculus touch controllers: arm yourself with the award-winning Oculus touch controllers. Your slashes, throws and grab appear in VR with intuitive, realistic Precision, transporting your hands and gestures right into the game', + brand: 'Oculus', + category: 'VR', + price: 645, + image: product18, + hasFreeShipping: false, + rating: 1, + }, + date: '29 Dec 2020', + buyer: { + name: 'Justin Patterson', + avatar: avatar3, + }, + payment: { + total: 252, + received_payment_status: 'Fully Paid', + paid_amount: 252, + status: 'Completed', + }, + }, + { + product: { + id: 11, + name: 'Handbags for Women Large Designer bag', + slug: 'handbags-for-women-large-designer-bag-11', + description: 'Classic Hobo Purse: Top zipper closure, with 2 side zipper pockets design and elegant tassels decoration, fashionable and practical handbags for women, perfect for shopping, dating, travel and business', + brand: 'Hobo', + category: 'Bag', + price: 39.99, + image: product17, + hasFreeShipping: true, + rating: 3, + }, + date: '19 Dec 2020', + buyer: { + name: 'Judy Cummings', + avatar: avatar3, + }, + payment: { + total: 1369, + received_payment_status: 'Fully Paid', + paid_amount: 1369, + status: 'Completed', + }, + }, + { + product: { + id: 18, + name: 'Logitech K380 Wireless Keyboard', + slug: 'acer-11-6-chromebook-intel-celeron-2-gb-memory-16-gb-e-mmc-flash-memory-moonstone-white-18', + description: 'Logitech K380 Bluetooth Wireless Keyboard gives you the comfort and convenience of desktop typing on your smartphone, and tablet. It is a wireless keyboard that connects to all Bluetooth wireless devices that support external keyboards. Take this compact, lightweight, Bluetooth keyboard anywhere in your home. Type wherever you like, on any compatible computer, phone or tablet.', + brand: 'Logitech', + category: 'Keyboard', + price: 81.99, + image: product10, + hasFreeShipping: false, + rating: 4, + }, + date: '02 Jan 2021', + buyer: { + name: 'Linda Buchanan', + avatar: avatar7, + }, + payment: { + total: 351, + received_payment_status: 'Fully Paid', + paid_amount: 351, + status: 'Completed', + }, + }, + { + product: { + id: 21, + name: 'Google - Google Home', + slug: 'google-google-home-white-slate-fabric-21', + brand: 'Google', + category: 'Google Home', + price: 129.29, + image: product7, + hasFreeShipping: true, + rating: 4, + description: 'Simplify your everyday life with the Google Home, a voice-activated speaker powered by the Google Assistant. Use\n voice commands to enjoy music, get answers from Google and manage everyday tasks. Google Home is compatible with\n Android and iOS operating systems, and can control compatible smart devices such as Chromecast or Nest.', + }, + date: '25 Feb 2021', + buyer: { + name: 'Brian Perez', + avatar: avatar8, + }, + payment: { + total: 506, + received_payment_status: 'Partially Paid', + paid_amount: 497, + status: 'Confirmed', + }, + }, + { + product: { + id: 3, + name: 'Willful Smart Watch for Men Women 2020,', + slug: 'willful-smart-watch-for-men-women-2020-3', + description: 'Are you looking for a smart watch, which can not only easily keep tracking of your steps, calories, heart rate and sleep quality, but also keep you informed of incoming calls.', + brand: 'Willful', + category: 'Smart Watch', + price: 29.99, + image: product25, + hasFreeShipping: true, + rating: 5, + }, + date: '13 Sep 2020', + buyer: { + name: 'Amy White', + avatar: null, + }, + payment: { + total: 195, + received_payment_status: 'Fully Paid', + paid_amount: 195, + status: 'Completed', + }, + }, + { + product: { + id: 18, + name: 'Logitech K380 Wireless Keyboard', + slug: 'acer-11-6-chromebook-intel-celeron-2-gb-memory-16-gb-e-mmc-flash-memory-moonstone-white-18', + description: 'Logitech K380 Bluetooth Wireless Keyboard gives you the comfort and convenience of desktop typing on your smartphone, and tablet. It is a wireless keyboard that connects to all Bluetooth wireless devices that support external keyboards. Take this compact, lightweight, Bluetooth keyboard anywhere in your home. Type wherever you like, on any compatible computer, phone or tablet.', + brand: 'Logitech', + category: 'Keyboard', + price: 81.99, + image: product10, + hasFreeShipping: false, + rating: 4, + }, + date: '30 Sep 2020', + buyer: { + name: 'Katherine Clark', + avatar: avatar1, + }, + payment: { + total: 1246, + received_payment_status: 'Partially Paid', + paid_amount: 475, + status: 'Confirmed', + }, + }, + { + product: { + id: 14, + name: 'Wireless Charger 5W Max', + slug: 'wireless-charger-5-w-max-14', + description: 'Charge with case: transmits charging power directly through protective cases. Rubber/plastic/TPU cases under 5 mm thickness . Do not use any magnetic and metal attachments or cards, or it will prevent charging.', + brand: '3M', + category: 'Electronics', + price: 10.83, + image: product14, + hasFreeShipping: true, + rating: 3, + }, + date: '26 Mar 2021', + buyer: { + name: 'Jose Murphy', + avatar: avatar5, + }, + payment: { + total: 383, + received_payment_status: 'Fully Paid', + paid_amount: 383, + status: 'Completed', + }, + }, + { + product: { + id: 2, + name: 'Bose Frames Tenor', + slug: 'bose-frames-tenor-rectangular-polarized-bluetooth-audio-sunglasses-2', + description: 'Redesigned for luxury \u2014 Thoughtfully refined and strikingly elegant, the latest Bose sunglasses blend enhanced features and designs for an elevated way to listen', + brand: 'Bose', + category: 'Glass', + price: 249, + image: product26, + hasFreeShipping: false, + rating: 4, + }, + date: '01 Dec 2020', + buyer: { + name: 'Jeffrey Rose', + avatar: avatar5, + }, + payment: { + total: 902, + received_payment_status: 'Fully Paid', + paid_amount: 902, + status: 'Completed', + }, + }, + { + product: { + id: 24, + name: 'OneOdio A71 Wired Headphones', + slug: 'one-odio-a71-wired-headphones-24', + brand: 'OneOdio', + category: 'Headphone', + price: 49.99, + image: product4, + hasFreeShipping: true, + rating: 3, + description: 'Omnidirectional detachable boom mic upgrades the headphones into a professional headset for gaming, business,\n podcasting and taking calls on the go. Better pick up your voice. Control most electric devices through voice\n activation, or schedule a ride with Uber and order a pizza. OneOdio A71 Wired Headphones voice-controlled device\n turns any home into a smart device on a smartphone or tablet.', + }, + date: '15 Sep 2020', + buyer: { + name: 'Amber Hunt', + avatar: avatar7, + }, + payment: { + total: 379, + received_payment_status: 'Partially Paid', + paid_amount: 174, + status: 'Confirmed', + }, + }, + { + product: { + id: 2, + name: 'Bose Frames Tenor', + slug: 'bose-frames-tenor-rectangular-polarized-bluetooth-audio-sunglasses-2', + description: 'Redesigned for luxury \u2014 Thoughtfully refined and strikingly elegant, the latest Bose sunglasses blend enhanced features and designs for an elevated way to listen', + brand: 'Bose', + category: 'Glass', + price: 249, + image: product26, + hasFreeShipping: false, + rating: 4, + }, + date: '08 Apr 2021', + buyer: { + name: 'Christopher Haas', + avatar: avatar2, + }, + payment: { + total: 7, + received_payment_status: 'Unpaid', + paid_amount: 0, + status: 'Confirmed', + }, + }, + { + product: { + id: 2, + name: 'Bose Frames Tenor', + slug: 'bose-frames-tenor-rectangular-polarized-bluetooth-audio-sunglasses-2', + description: 'Redesigned for luxury \u2014 Thoughtfully refined and strikingly elegant, the latest Bose sunglasses blend enhanced features and designs for an elevated way to listen', + brand: 'Bose', + category: 'Glass', + price: 249, + image: product26, + hasFreeShipping: false, + rating: 4, + }, + date: '21 Oct 2020', + buyer: { + name: 'Stephen Mccormick', + avatar: avatar6, + }, + payment: { + total: 186, + received_payment_status: 'Partially Paid', + paid_amount: 81, + status: 'Confirmed', + }, + }, + { + product: { + id: 19, + name: 'OnePlus 7 Pro ', + slug: 'one-plus-7-pro-19', + brand: 'Philips', + category: 'Smart Phone', + price: 14.99, + image: product9, + hasFreeShipping: false, + rating: 4, + description: 'The OnePlus 7 Pro features a brand new design, with a glass back and front and curved sides. The phone feels\n very premium but\u2019s it\u2019s also very heavy. The Nebula Blue variant looks slick but it\u2019s quite slippery, which\n makes single-handed use a real challenge. It has a massive 6.67-inch \u2018Fluid AMOLED\u2019 display with a QHD+\n resolution, 90Hz refresh rate and support for HDR 10+ content. The display produces vivid colours, deep blacks\n and has good viewing angles.', + }, + date: '21 Oct 2020', + buyer: { + name: 'Matthew Reyes', + avatar: avatar3, + }, + payment: { + total: 198, + received_payment_status: 'Fully Paid', + paid_amount: 198, + status: 'Completed', + }, + }, + { + product: { + id: 4, + name: 'Ronyes Unisex College Bag Bookbags for Women', + slug: 'ronyes-unisex-college-bag-bookbags-for-women-4', + description: 'Made of high quality water-resistant material; padded and adjustable shoulder straps; external USB with built-in charging cable offers a convenient charging', + brand: 'Ronyes', + category: 'Bag', + price: 23.99, + image: product24, + hasFreeShipping: true, + rating: 2, + }, + date: '16 May 2020', + buyer: { + name: 'Ricardo Morgan', + avatar: avatar5, + }, + payment: { + total: 519, + received_payment_status: 'Partially Paid', + paid_amount: 447, + status: 'Confirmed', + }, + }, + { + product: { + id: 20, + name: 'Sony 4K Ultra HD LED TV ', + slug: 'sony-4-k-ultra-hd-led-tv-20', + brand: 'Apple', + category: 'Smart TV', + price: 7999.99, + image: product8, + hasFreeShipping: false, + rating: 5, + description: 'Sony 4K Ultra HD LED TV has 4K HDR Support. The TV provides clear visuals and provides distinct sound quality\n and an immersive experience. This TV has Yes HDMI ports & Yes USB ports. Connectivity options included are HDMI.\n You can connect various gadgets such as your laptop using the HDMI port. The TV comes with a 1 Year warranty.', + }, + date: '01 Jul 2020', + buyer: { + name: 'William Castillo', + avatar: avatar4, + }, + payment: { + total: 10, + received_payment_status: 'Partially Paid', + paid_amount: 6, + status: 'Confirmed', + }, + }, + { + product: { + id: 11, + name: 'Handbags for Women Large Designer bag', + slug: 'handbags-for-women-large-designer-bag-11', + description: 'Classic Hobo Purse: Top zipper closure, with 2 side zipper pockets design and elegant tassels decoration, fashionable and practical handbags for women, perfect for shopping, dating, travel and business', + brand: 'Hobo', + category: 'Bag', + price: 39.99, + image: product17, + hasFreeShipping: true, + rating: 3, + }, + date: '04 Jul 2020', + buyer: { + name: 'James Coleman', + avatar: avatar8, + }, + payment: { + total: 897, + received_payment_status: 'Partially Paid', + paid_amount: 677, + status: 'Confirmed', + }, + }, + { + product: { + id: 18, + name: 'Logitech K380 Wireless Keyboard', + slug: 'acer-11-6-chromebook-intel-celeron-2-gb-memory-16-gb-e-mmc-flash-memory-moonstone-white-18', + description: 'Logitech K380 Bluetooth Wireless Keyboard gives you the comfort and convenience of desktop typing on your smartphone, and tablet. It is a wireless keyboard that connects to all Bluetooth wireless devices that support external keyboards. Take this compact, lightweight, Bluetooth keyboard anywhere in your home. Type wherever you like, on any compatible computer, phone or tablet.', + brand: 'Logitech', + category: 'Keyboard', + price: 81.99, + image: product10, + hasFreeShipping: false, + rating: 4, + }, + date: '19 Feb 2021', + buyer: { + name: 'Michael Summers', + avatar: avatar3, + }, + payment: { + total: 653, + received_payment_status: 'Fully Paid', + paid_amount: 653, + status: 'Completed', + }, + }, + { + product: { + id: 3, + name: 'Willful Smart Watch for Men Women 2020,', + slug: 'willful-smart-watch-for-men-women-2020-3', + description: 'Are you looking for a smart watch, which can not only easily keep tracking of your steps, calories, heart rate and sleep quality, but also keep you informed of incoming calls.', + brand: 'Willful', + category: 'Smart Watch', + price: 29.99, + image: product25, + hasFreeShipping: true, + rating: 5, + }, + date: '03 Mar 2021', + buyer: { + name: 'Jeremiah Espinoza', + avatar: avatar2, + }, + payment: { + total: 913, + received_payment_status: 'Partially Paid', + paid_amount: 468, + status: 'Confirmed', + }, + }, + { + product: { + id: 2, + name: 'Bose Frames Tenor', + slug: 'bose-frames-tenor-rectangular-polarized-bluetooth-audio-sunglasses-2', + description: 'Redesigned for luxury \u2014 Thoughtfully refined and strikingly elegant, the latest Bose sunglasses blend enhanced features and designs for an elevated way to listen', + brand: 'Bose', + category: 'Glass', + price: 249, + image: product26, + hasFreeShipping: false, + rating: 4, + }, + date: '03 Mar 2021', + buyer: { + name: 'Tyler Brooks', + avatar: null, + }, + payment: { + total: 1123, + received_payment_status: 'Fully Paid', + paid_amount: 1123, + status: 'Completed', + }, + }, + { + product: { + id: 17, + name: 'Nike Air Max', + slug: '72-9301-speaker-wire-harness-adapter-for-most-plymouth-dodge-and-mitsubishi-vehicles-multi-17', + description: 'With a bold application of colorblocking inspired by modern art styles, the Nike Air Max 270 React sneaker is constructed with layers of lightweight material to achieve its artful look and comfortable feel.', + brand: 'Nike', + category: 'Shoes', + price: 81.99, + image: product11, + hasFreeShipping: true, + rating: 5, + }, + date: '29 Dec 2020', + buyer: { + name: 'Juan Wilson', + avatar: avatar3, + }, + payment: { + total: 779, + received_payment_status: 'Fully Paid', + paid_amount: 779, + status: 'Completed', + }, + }, + { + product: { + id: 15, + name: 'Vankyo leisure 3 mini projector', + slug: '3-m-filtrete-vacuum-belt-for-select-hoover-t-series-upright-vacuums-15', + description: 'SUPERIOR VIEWING EXPERIENCE: Supporting 1920x1080 resolution, VANKYO Leisure 3 projector is powered by MStar Advanced Color Engine, which is ideal for home entertainment. 2020 upgraded LED lighting provides a superior viewing experience for you.', + brand: 'Vankyo Store', + category: 'Projector', + price: 99.99, + image: product13, + hasFreeShipping: true, + rating: 2, + }, + date: '03 Dec 2020', + buyer: { + name: 'Marvin Duran', + avatar: null, + }, + payment: { + total: 594, + received_payment_status: 'Unpaid', + paid_amount: 0, + status: 'Cancelled', + }, + }, + { + product: { + id: 22, + name: 'Switch Pro Controller', + slug: 'switch-pro-controller-22', + brand: 'Sharp', + category: 'Gaming', + price: 429.99, + image: product6, + hasFreeShipping: false, + rating: 3, + description: 'The Nintendo Switch Pro Controller is one of the priciest \'baseline\' controllers in the current console\n generation, but it\'s also sturdy, feels good to play with, has an excellent direction pad, and features\n impressive motion sensors and vibration systems. On top of all of that, it uses Bluetooth, so you don\'t need an\n adapter to use it with your PC.', + }, + date: '28 May 2020', + buyer: { + name: 'Jessica Glass', + avatar: avatar5, + }, + payment: { + total: 1065, + received_payment_status: 'Partially Paid', + paid_amount: 844, + status: 'Confirmed', + }, + }, + { + product: { + id: 18, + name: 'Logitech K380 Wireless Keyboard', + slug: 'acer-11-6-chromebook-intel-celeron-2-gb-memory-16-gb-e-mmc-flash-memory-moonstone-white-18', + description: 'Logitech K380 Bluetooth Wireless Keyboard gives you the comfort and convenience of desktop typing on your smartphone, and tablet. It is a wireless keyboard that connects to all Bluetooth wireless devices that support external keyboards. Take this compact, lightweight, Bluetooth keyboard anywhere in your home. Type wherever you like, on any compatible computer, phone or tablet.', + brand: 'Logitech', + category: 'Keyboard', + price: 81.99, + image: product10, + hasFreeShipping: false, + rating: 4, + }, + date: '17 May 2020', + buyer: { + name: 'Gary Herman', + avatar: avatar8, + }, + payment: { + total: 432, + received_payment_status: 'Partially Paid', + paid_amount: 64, + status: 'Confirmed', + }, + }, + { + product: { + id: 19, + name: 'OnePlus 7 Pro ', + slug: 'one-plus-7-pro-19', + brand: 'Philips', + category: 'Smart Phone', + price: 14.99, + image: product9, + hasFreeShipping: false, + rating: 4, + description: 'The OnePlus 7 Pro features a brand new design, with a glass back and front and curved sides. The phone feels\n very premium but\u2019s it\u2019s also very heavy. The Nebula Blue variant looks slick but it\u2019s quite slippery, which\n makes single-handed use a real challenge. It has a massive 6.67-inch \u2018Fluid AMOLED\u2019 display with a QHD+\n resolution, 90Hz refresh rate and support for HDR 10+ content. The display produces vivid colours, deep blacks\n and has good viewing angles.', + }, + date: '25 Mar 2021', + buyer: { + name: 'Adam Williams', + avatar: avatar2, + }, + payment: { + total: 1402, + received_payment_status: 'Partially Paid', + paid_amount: 434, + status: 'Confirmed', + }, + }, + { + product: { + id: 20, + name: 'Sony 4K Ultra HD LED TV ', + slug: 'sony-4-k-ultra-hd-led-tv-20', + brand: 'Apple', + category: 'Smart TV', + price: 7999.99, + image: product8, + hasFreeShipping: false, + rating: 5, + description: 'Sony 4K Ultra HD LED TV has 4K HDR Support. The TV provides clear visuals and provides distinct sound quality\n and an immersive experience. This TV has Yes HDMI ports & Yes USB ports. Connectivity options included are HDMI.\n You can connect various gadgets such as your laptop using the HDMI port. The TV comes with a 1 Year warranty.', + }, + date: '13 Apr 2021', + buyer: { + name: 'Bobby Brown', + avatar: null, + }, + payment: { + total: 100, + received_payment_status: 'Partially Paid', + paid_amount: 65, + status: 'Confirmed', + }, + }, + { + product: { + id: 14, + name: 'Wireless Charger 5W Max', + slug: 'wireless-charger-5-w-max-14', + description: 'Charge with case: transmits charging power directly through protective cases. Rubber/plastic/TPU cases under 5 mm thickness . Do not use any magnetic and metal attachments or cards, or it will prevent charging.', + brand: '3M', + category: 'Electronics', + price: 10.83, + image: product14, + hasFreeShipping: true, + rating: 3, + }, + date: '07 Aug 2020', + buyer: { + name: 'Sharon Moss', + avatar: avatar8, + }, + payment: { + total: 823, + received_payment_status: 'Unpaid', + paid_amount: 0, + status: 'Cancelled', + }, + }, + { + product: { + id: 15, + name: 'Vankyo leisure 3 mini projector', + slug: '3-m-filtrete-vacuum-belt-for-select-hoover-t-series-upright-vacuums-15', + description: 'SUPERIOR VIEWING EXPERIENCE: Supporting 1920x1080 resolution, VANKYO Leisure 3 projector is powered by MStar Advanced Color Engine, which is ideal for home entertainment. 2020 upgraded LED lighting provides a superior viewing experience for you.', + brand: 'Vankyo Store', + category: 'Projector', + price: 99.99, + image: product13, + hasFreeShipping: true, + rating: 2, + }, + date: '23 Feb 2021', + buyer: { + name: 'Scott Buchanan', + avatar: avatar5, + }, + payment: { + total: 183, + received_payment_status: 'Unpaid', + paid_amount: 0, + status: 'Cancelled', + }, + }, +] + +mock.onGet('/pages/datatables').reply(() => [200, data]) diff --git a/resources/js/@fake-db/pages/help-center.js b/resources/js/@fake-db/pages/help-center.js new file mode 100644 index 0000000..e3c828e --- /dev/null +++ b/resources/js/@fake-db/pages/help-center.js @@ -0,0 +1,576 @@ +import mock from '@/@fake-db/mock' +import { themeConfig } from '@themeConfig' + +// Images +import discord from '@images/svg/discord.svg' +import gift from '@images/svg/gift.svg' +import keyboard from '@images/svg/keyboard.svg' +import laptop from '@images/svg/laptop.svg' +import lightbulb from '@images/svg/lightbulb.svg' +import rocket from '@images/svg/rocket.svg' + +const data = { + popularArticles: [ + { + slug: 'getting-started', + title: 'Getting Started', + img: rocket, + subtitle: 'Whether you\'re new or you\'re a power user, this article will', + }, + { + slug: 'first-steps', + title: 'First Steps', + img: gift, + subtitle: 'Are you a new customer wondering how to get started?', + }, + { + slug: 'external-content', + title: 'Add External Content', + img: keyboard, + subtitle: 'This article will show you how to expand the functionality of App', + }, + ], + categories: [ + { + icon: 'tabler-rocket', + avatarColor: 'success', + slug: 'getting-started', + title: 'Getting Started', + subCategories: [ + { + slug: 'account', + icon: 'tabler-box', + title: 'Account', + articles: [ + { + slug: 'changing-your-username', + title: 'Changing your username?', + content: '

You can change your username to another username that is not currently in use. If the username you want is not available, consider other names or unique variations. Using a number, hyphen, or an alternative spelling might help you find a similar username that\'s still available.

After changing your username, your old username becomes available for anyone else to claim. Most references to your repositories under the old username automatically change to the new username. However, some links to your profile won\'t automatically redirect.

You can change your username to another username that is not currently in use. If the username you want is not available, consider other names or unique variations. Using a number, hyphen, or an alternative spelling might help you find a similar username that\'s still available.

After changing your username, your old username becomes available for anyone else to claim. Most references to your repositories under the old username automatically change to the new username. However, some links to your profile won\'t automatically redirect.

', + }, + { + slug: 'changing-your-primary-email-address', + title: 'Changing your primary email address?', + content: '

You can change the email address associated with your personal account at any time from account settings.

Note: You cannot change your primary email address to an email that is already set to be your backup email address.

You can change the email address associated with your personal account at any time from account settings.

Note: You cannot change your primary email address to an email that is already set to be your backup email address.

', + }, + { + slug: 'changing-your-profile-picture', + title: 'Changing your profile picture?', + content: '

You can change your profile from account settings any time.

Note: Your profile picture should be a PNG, JPG, or GIF file, and it must be less than 1 MB in size and smaller than 3000 by 3000 pixels. For the best quality rendering, we recommend keeping the image at about 500 by 500 pixels.

You can change your profile from account settings any time.

Note: Your profile picture should be a PNG, JPG, or GIF file, and it must be less than 1 MB in size and smaller than 3000 by 3000 pixels. For the best quality rendering, we recommend keeping the image at about 500 by 500 pixels.', + }, + { + slug: 'setting-your-profile-to-private', + title: 'Setting your profile to private?', + content: '

A private profile displays only limited information, and hides some activity.

To hide parts of your profile page, you can make your profile private. This also hides your activity in various social features on the website. A private profile hides information from all users, and there is currently no option to allow specified users to see your activity.

You can change your profile to private in account settings.

A private profile displays only limited information, and hides some activity.

To hide parts of your profile page, you can make your profile private. This also hides your activity in various social features on the website. A private profile hides information from all users, and there is currently no option to allow specified users to see your activity.

You can change your profile to private in account settings.

', + }, + { + slug: 'deleting-your-personal-account', + title: 'Deleting your personal account?', + content: '

Deleting your personal account removes data associated with your account.

When you delete your account we stop billing you. The email address associated with the account becomes available for use with a different account on website. After 90 days, the account name also becomes available to anyone else to use on a new account.

Deleting your personal account removes data associated with your account.

When you delete your account we stop billing you. The email address associated with the account becomes available for use with a different account on website. After 90 days, the account name also becomes available to anyone else to use on a new account.

', + }, + ], + }, + { + slug: 'authentication', + title: 'Authentication', + icon: 'tabler-lock', + articles: [ + { + slug: 'how-to-create-a-strong-password', + title: 'How to create a strong password?', + content: '

A strong password is a unique word or phrase a hacker cannot easily guess or crack.

To keep your account secure, we recommend you to have a password with at least Eight characters, a number, a lowercase letter & an uppercase character.

A strong password is a unique word or phrase a hacker cannot easily guess or crack.

To keep your account secure, we recommend you to have a password with at least Eight characters, a number, a lowercase letter & an uppercase character.

', + }, + { + slug: 'what-is-2FA', + title: 'What is Two-Factor Authentication?', + content: '

Two-factor authentication (2FA) is an extra layer of security used when logging into websites or apps. With 2FA, you have to log in with your username and password and provide another form of authentication that only you know or have access to.

For our app, the second form of authentication is a code that\'s generated by an application on your mobile device or sent as a text message (SMS). After you enable 2FA, App generates an authentication code any time someone attempts to sign into your account. The only way someone can sign into your account is if they know both your password and have access to the authentication code on your phone.

Two-factor authentication (2FA) is an extra layer of security used when logging into websites or apps. With 2FA, you have to log in with your username and password and provide another form of authentication that only you know or have access to.

For our app, the second form of authentication is a code that\'s generated by an application on your mobile device or sent as a text message (SMS). After you enable 2FA, App generates an authentication code any time someone attempts to sign into your account. The only way someone can sign into your account is if they know both your password and have access to the authentication code on your phone.

', + }, + { + slug: 'how-to-recover-account-if-you-lose-your-2fa-credentials', + title: 'How to recover account if you lose your 2fa credentials?', + content: '

If you lose access to your two-factor authentication credentials, you can use your recovery codes, or another recovery option, to regain access to your account.

Warning: For security reasons, Our Support may not be able to restore access to accounts with two-factor authentication enabled if you lose your two-factor authentication credentials or lose access to your account recovery methods.

If you lose access to your two-factor authentication credentials, you can use your recovery codes, or another recovery option, to regain access to your account.

Warning: For security reasons, Our Support may not be able to restore access to accounts with two-factor authentication enabled if you lose your two-factor authentication credentials or lose access to your account recovery methods.

', + }, + { + slug: 'how-to-review-security-logs', + title: 'How to review security logs?', + content: '

You can review the security log for your personal account to better understand actions you\'ve performed and actions others have performed that involve you.

You can refer your security log from the settings.

You can review the security log for your personal account to better understand actions you\'ve performed and actions others have performed that involve you.

You can refer your security log from the settings.

', + }, + ], + }, + { + slug: 'billing', + title: 'Billing', + icon: 'tabler-currency-dollar', + articles: [ + { + slug: 'how-to-update-payment-method', + title: 'How to update payment method?', + content: '

You can add a payment method to your account or update your account\'s existing payment method at any time.

You can pay with a credit card or with a PayPal account. When you update your payment method for your account\'s subscription, your new payment method is automatically added to your other subscriptions for paid products.

You can add a payment method to your account or update your account\'s existing payment method at any time.

You can pay with a credit card or with a PayPal account. When you update your payment method for your account\'s subscription, your new payment method is automatically added to your other subscriptions for paid products.

', + }, + { + slug: 'how-to-check-billing-date', + title: 'How to check billing date?', + content: '

You can view your account\'s subscription, your other paid features and products, and your next billing date in your account\'s billing settings.

You can view your account\'s subscription, your other paid features and products, and your next billing date in your account\'s billing settings.

', + }, + { + slug: 'how-to-change-billing-cycle', + title: 'How to change billing cycle?', + content: '

You can change your billing cycle from the account settings billing section.

When you change your billing cycle\'s duration, your GitHub subscription, along with any other paid features and products, will be moved to your new billing cycle on your next billing date.

You can change your billing cycle from the account settings billing section.

When you change your billing cycle\'s duration, your GitHub subscription, along with any other paid features and products, will be moved to your new billing cycle on your next billing date.

', + }, + { + slug: 'where-can-i-view-and-download-payment-receipt', + title: 'Where can i view and download payment receipt?', + content: '

You can view your payment from the account settings billing section.

You\'ll also a have a option to download or share your payment receipt from the billing section.

You can view your payment from the account settings billing section.

You\'ll also a have a option to download or share your payment receipt from the billing section.

', + }, + { + slug: 'how-to-set-billing-email', + title: 'How to set billing email?', + content: '

Your personal account\'s primary email is where we send receipts and other billing-related communication.

Your primary email address is the first email listed in your account email settings. We also use your primary email address as our billing email address.

If you\'d like to change your billing email you can do it from account settings.

Your personal account\'s primary email is where we send receipts and other billing-related communication.

Your primary email address is the first email listed in your account email settings. We also use your primary email address as our billing email address.

If you\'d like to change your billing email you can do it from account settings.

', + }, + ], + }, + ], + }, + { + slug: 'orders', + title: 'Orders', + avatarColor: 'info', + icon: 'tabler-box', + subCategories: [ + { + slug: 'processing-orders', + title: 'Processing orders', + icon: 'tabler-box', + articles: [ + { + slug: 'what-happens-when-you-receive-an-online-order', + title: 'What happens when you receive an online order?', + content: '

When you receive an online order, you\'ll receive a new order notification by email.

You\'ll be able to see that order on the orders page.

When you receive an online order, you\'ll receive a new order notification by email.

You\'ll be able to see that order on the orders page.

', + }, + { + slug: 'what-happens-when-you-process-an-order', + title: 'What happens when you process an order?', + content: '

When you process an order, The Orders page will show the order with a payment status of Paid or Partially paid.

If the customer provided their email address, then they receive a receipt by email.

When you process an order, The Orders page will show the order with a payment status of Paid or Partially paid.

If the customer provided their email address, then they receive a receipt by email.

', + }, + { + slug: 'how-to-cancel-an-order', + title: 'How to cancel an order?', + content: '

Canceling an order indicates that you are halting order processing. For example, if a customer requests a cancellation or you suspect the order is fraudulent, then you can cancel the order to help prevent staff or fulfillment services from continuing work on the order. You can also cancel an order if an item was ordered and isn\'t available.

You can cancel an order by clicking the cancel button on orders page.

Canceling an order indicates that you are halting order processing. For example, if a customer requests a cancellation or you suspect the order is fraudulent, then you can cancel the order to help prevent staff or fulfillment services from continuing work on the order. You can also cancel an order if an item was ordered and isn\'t available.

You can cancel an order by clicking the cancel button on orders page.

', + }, + { + slug: 'whats-the-status-of-my-order', + title: 'What\'s the Status of My Order?', + content: '

You can check the shipping status of your order on website or the app. If the seller added a tracking number, you can use that to get detailed information about the package\'s movement through the shipping carrier.

You\'ll see the shipping status on the orders page. You\'ll also see an estimated delivery date which should give you an idea of when you can expect the order to arrive, and a tracking number if it\'s available for your order.

You can check the shipping status of your order on website or the app. If the seller added a tracking number, you can use that to get detailed information about the package\'s movement through the shipping carrier.

You\'ll see the shipping status on the orders page. You\'ll also see an estimated delivery date which should give you an idea of when you can expect the order to arrive, and a tracking number if it\'s available for your order.

', + }, + { + slug: 'how-to-return-or-exchange-an-item', + title: 'How to Return or Exchange an Item?', + content: '

If you need to return or exchange an item, the seller you purchased your order from is the best person to help you. Each seller manages their own orders, and makes decisions about cancellations, refunds, and returns.

Sellers aren\'t required to accept returns, exchanges, or provide a refund unless stated in their shop policies. Go to the shop\'s homepage and scroll to the bottom to see the shop\'s policies.

If you need to return or exchange an item, the seller you purchased your order from is the best person to help you. Each seller manages their own orders, and makes decisions about cancellations, refunds, and returns.

Sellers aren\'t required to accept returns, exchanges, or provide a refund unless stated in their shop policies. Go to the shop\'s homepage and scroll to the bottom to see the shop\'s policies.

', + }, + ], + }, + { + slug: 'payments', + title: 'Payments', + icon: 'tabler-currency-dollar', + articles: [ + { + slug: 'how-do-i-get-paid', + title: 'How do i get paid?', + content: '

When you set up a payment provider to accept credit card payments, each payment must be processed, so there is usually a delay between when the customer pays for their order and when you receive the payment. After the payment is processed, the purchase amount will be transferred to your merchant account.

When you set up a payment provider to accept credit card payments, each payment must be processed, so there is usually a delay between when the customer pays for their order and when you receive the payment. After the payment is processed, the purchase amount will be transferred to your merchant account.

', + }, + { + slug: 'how-often-do-i-get-paid', + title: 'How often do I get paid?', + content: '

If you use our payment system, then you can check your pay period to see when you receive payouts from credit card orders. Other payment providers have their own rules on when you receive payouts for credit card orders. Check with your provider to find out how often you will be paid.

After the payout is sent, it might not be received by your bank right away. It can take a few days after the payout is sent for it to be deposited into your bank account. Check with your bank if you find your payouts are being delayed.

If you use our payment system, then you can check your pay period to see when you receive payouts from credit card orders. Other payment providers have their own rules on when you receive payouts for credit card orders. Check with your provider to find out how often you will be paid.

After the payout is sent, it might not be received by your bank right away. It can take a few days after the payout is sent for it to be deposited into your bank account. Check with your bank if you find your payouts are being delayed.

', + }, + { + slug: 'how-much-do-i-get-paid', + title: 'How much do I get paid?', + content: '

You can be charged several third-party transaction fees for online transactions. For credit card transactions, the issuer, the acquirer, and the credit card company all charge a small fee for using their services.

You aren\'t charged third-party transaction fees for orders processed through our payment system. You pay credit card processing fees, depending on your subscription plan. If you\'re using a third-party payment provider with us, then you\'re charged a third-party transaction fee.

You can be charged several third-party transaction fees for online transactions. For credit card transactions, the issuer, the acquirer, and the credit card company all charge a small fee for using their services.

You aren\'t charged third-party transaction fees for orders processed through our payment system. You pay credit card processing fees, depending on your subscription plan. If you\'re using a third-party payment provider with us, then you\'re charged a third-party transaction fee.

', + }, + { + slug: 'cant-complete-payment-on-paypal', + title: 'Can\'t Complete Payment on PayPal?', + content: '

PayPal uses various security measures to protect their users. Because of this, PayPal may occasionally prohibit a buyer from submitting payment to a seller through PayPal.

If you\'re ultimately unable to submit payment, try working with the seller to determine an alternative payment method. Learn how to contact a seller.

PayPal uses various security measures to protect their users. Because of this, PayPal may occasionally prohibit a buyer from submitting payment to a seller through PayPal.

If you\'re ultimately unable to submit payment, try working with the seller to determine an alternative payment method. Learn how to contact a seller.

', + }, + { + slug: 'why-is-my-order-is-still-processing', + title: 'Why is my order is still processing?', + content: '

If you received an email saying that your order is still processing, it means that your purchase is being screened by our third-party partner. All Payments orders are screened to ensure that the orders are legitimate and to protect from possible fraud.

Most orders are processed in under 72 hours. You\'ll receive a confirmation email when the review is complete.

If you received an email saying that your order is still processing, it means that your purchase is being screened by our third-party partner. All Payments orders are screened to ensure that the orders are legitimate and to protect from possible fraud.

Most orders are processed in under 72 hours. You\'ll receive a confirmation email when the review is complete.

', + }, + ], + }, + { + icon: 'tabler-refresh', + slug: 'returns-refunds-replacements', + title: 'Returns, Refunds and Replacements', + articles: [ + { + slug: 'what-can-i-return', + title: 'What can I return?', + content: '

You may request returns for most items you buy from the sellers listed on the website. However, you can only return items explicitly identified as "returnable" on the product detail page and/or our policy and within the ‘return window\' period.

Please refer to the website Returns policy. to know which categories are "non-returnable" and the specific return windows for categories eligible for return.

  • Physically damaged
  • Has missing parts or accessories
  • Defective
  • Different from its description on the product detail page on the website

You may request returns for most items you buy from the sellers listed on the website. However, you can only return items explicitly identified as "returnable" on the product detail page and/or our policy and within the \'return window\' period.

Please refer to the website Returns policy. to know which categories are "non-returnable" and the specific return windows for categories eligible for return.

  • Physically damaged
  • Has missing parts or accessories
  • Defective
  • Different from its description on the product detail page on the website
', + }, + { + slug: 'when-will-i-get-my-refund', + title: 'When will I get my refund?', + content: '

Following are the refund processing timelines after the item is received by Amazon or the Seller notifies us of the receipt of the return:

  • Wallet: 2 hours
  • Credit/Debit Card: 2-4 Business Days
  • Bank Account: 2-4 Business Days

Following are the refund processing timelines after the item is received by Amazon or the Seller notifies us of the receipt of the return:

  • Wallet: 2 hours
  • Credit/Debit Card: 2-4 Business Days
  • Bank Account: 2-4 Business Days
', + }, + { + slug: 'can-my-order-be-replaced', + title: 'Can my order be replaced?', + content: '

If the item you ordered arrived in a physically damaged/ defective condition or is different from their description on the product detail page, or has missing parts or accessories, it will be eligible for a free replacement as long as the exact item is available with the same seller.

If the item you ordered arrived in a physically damaged/ defective condition or is different from their description on the product detail page, or has missing parts or accessories, it will be eligible for a free replacement as long as the exact item is available with the same seller.

', + }, + ], + }, + ], + }, + { + icon: 'tabler-users', + slug: 'safety-security', + avatarColor: 'primary', + title: 'Safety and Security', + subCategories: [ + { + slug: 'hacked-accounts', + icon: 'tabler-shield', + title: 'Security and hacked accounts', + articles: [ + { + slug: 'has-my-account-been-compromised', + title: 'Has my account been compromised?', + content: '

Have you:

  • Noticed unexpected posts by your account
  • Seen unintended Direct Messages sent from your account
  • Observed other account behaviors you didn\'t make or approve (like following, unfollowing, or blocking)
.

If you\'ve answered yes to any of the above, please change your password and Revoke connections to third-party applications

Have you:

  • Noticed unexpected posts by your account
  • Seen unintended Direct Messages sent from your account
  • Observed other account behaviors you didn\'t make or approve (like following, unfollowing, or blocking)
.

If you\'ve answered yes to any of the above, please change your password and Revoke connections to third-party applications

', + }, + { + slug: 'how-to-keep-my-account-safe', + title: 'How to keep my account safe?', + content: '

To help keep your account secure, we recommend the following best practices:

  • Use a strong password that you don\'t reuse on other websites.
  • Use two-factor authentication.
  • Require email and phone number to request a reset password link or code.
  • Be cautious of suspicious links and always make sure you\'re on our website before you enter your login information.
  • Never give your username and password out to third parties, especially those promising to get you followers, make you money, or verify you.

To help keep your account secure, we recommend the following best practices:

  • Use a strong password that you don\'t reuse on other websites.
  • Use two-factor authentication.
  • Require email and phone number to request a reset password link or code.
  • Be cautious of suspicious links and always make sure you\'re on our website before you enter your login information.
  • Never give your username and password out to third parties, especially those promising to get you followers, make you money, or verify you.
', + }, + { + slug: 'help-with-my-hacked-account', + title: 'Help with my hacked account', + content: '

If you think you\'ve been hacked and you\'re unable to log in with your username and password, please take the following two steps:

  1. Request a password reset

    Reset your password by requesting an email from the password reset form. Try entering both your username and email address, and be sure to check for the reset email at the address associated with your account.

  2. Contact Support if you still require assistance

    If you still can\'t log in, contact us by submitting a Support Request. Be sure to use the email address you associated with the hacked account; we\'ll then send additional information and instructions to that email address. When submitting your support request please Include both your username and the date you last had access to your account.

If you think you\'ve been hacked and you\'re unable to log in with your username and password, please take the following two steps:

  1. Request a password reset

    Reset your password by requesting an email from the password reset form. Try entering both your username and email address, and be sure to check for the reset email at the address associated with your account.

  2. Contact Support if you still require assistance

    If you still can\'t log in, contact us by submitting a Support Request. Be sure to use the email address you associated with the hacked account; we\'ll then send additional information and instructions to that email address. When submitting your support request please Include both your username and the date you last had access to your account.

', + }, + ], + }, + { + slug: 'privacy', + title: 'Privacy', + icon: 'tabler-lock', + articles: [ + { + slug: 'what-is-visible-on-my-profile', + title: 'What is visible on my profile?', + content: '

Most of the profile information you provide us is always public, like your biography, location, website, and picture. For certain profile information fields we provide you with visibility settings to select who can see this information in your profile.

If you provide us with profile information and you don\'t see a visibility setting, that information is public.

Most of the profile information you provide us is always public, like your biography, location, website, and picture. For certain profile information fields we provide you with visibility settings to select who can see this information in your profile.

If you provide us with profile information and you don\'t see a visibility setting, that information is public.

', + }, + { + slug: 'should-i-turn-on-precise-location', + title: 'Should I turn on precise location?', + content: '

Enabling precise location through our official app allows us to collect, store, and use your precise location, such as GPS information. This allows us to provide, develop, and improve a variety of our services, including but not limited to:

  • Delivery of content, including posts and advertising, that is better tailored to your location.
  • Delivery of location-specific trends.
  • Showing your followers the location you are posting from as part of your post, if you decide to geo-tag your post.

Enabling precise location through our official app allows us to collect, store, and use your precise location, such as GPS information. This allows us to provide, develop, and improve a variety of our services, including but not limited to:

  • Delivery of content, including posts and advertising, that is better tailored to your location.
  • Delivery of location-specific trends.
  • Showing your followers the location you are posting from as part of your post, if you decide to geo-tag your post.
', + }, + { + slug: 'what-location-information-is-displayed', + title: 'What location information is displayed?', + content: '
  • All geolocation information begins as a location (latitude and longitude), sent from your browser or device. We won\'t show any location information unless you\'ve opted in to the feature, and have allowed your device or browser to transmit your coordinates to us.
  • If you have chosen to attach location information to your Posts, your selected location label is displayed underneath the text of the Post.
  • When you use the in-app camera on iOS and Android to attach a photo or video to your post and toggle on the option to tag your precise location, that post will include both the location label of your choice and your device\'s precise location (latitude and longitude), which can be found via API. Your precise location may be more specific than the location label you select. This is helpful when sharing on-the-ground moments.
  • All geolocation information begins as a location (latitude and longitude), sent from your browser or device. We won\'t show any location information unless you\'ve opted in to the feature, and have allowed your device or browser to transmit your coordinates to us.
  • If you have chosen to attach location information to your Posts, your selected location label is displayed underneath the text of the Post.
  • When you use the in-app camera on iOS and Android to attach a photo or video to your post and toggle on the option to tag your precise location, that post will include both the location label of your choice and your device\'s precise location (latitude and longitude), which can be found via API. Your precise location may be more specific than the location label you select. This is helpful when sharing on-the-ground moments.
', + }, + ], + }, + { + slug: 'spam-fake-accounts', + title: 'Spam and fake accounts', + icon: 'tabler-mail', + articles: [ + { + slug: 'how-to-detect-fake-email', + title: 'How to detect fake email?', + content: `

We will only send you emails from @${themeConfig.app.title}.com or @t.${themeConfig.app.title}.com. However, some people may receive fake or suspicious emails that look like they were sent by US. These emails might include malicious attachments or links to spam or phishing websites. Please know that we will never send emails with attachments or request your password by email.

We will only send you emails from @${themeConfig.app.title}.com or @t.${themeConfig.app.title}.com. However, some people may receive fake or suspicious emails that look like they were sent by US. These emails might include malicious attachments or links to spam or phishing websites. Please know that we will never send emails with attachments or request your password by email.

`, + }, + { + slug: 'how-do-I-report-an-impersonation-violation', + title: 'How do I report an impersonation violation?', + content: '

If you believe an account is posing as you or your brand, you or your authorized representative can file a report in our support Center.

If you believe an account is misusing the identity of somebody else, you can flag it as a bystander by reporting directly from the account\'s profile.

If you believe an account is posing as you or your brand, you or your authorized representative can file a report in our support Center.

If you believe an account is misusing the identity of somebody else, you can flag it as a bystander by reporting directly from the account\'s profile.

', + }, + { + slug: 'someone-is-using-my-email-address-what-can-i-do', + title: 'Someone is using my email address, what can I do?', + content: '

Are you trying to create a new account, but you\'re told your email address or phone number is already in use? This support article outlines how your email address may already be in use and how to resolve the issue.

Are you trying to create a new account, but you\'re told your email address or phone number is already in use? This support article outlines how your email address may already be in use and how to resolve the issue.

', + }, + ], + }, + ], + }, + { + avatarColor: 'error', + icon: 'tabler-clipboard', + slug: 'rules-policies', + title: 'Rules and Policies', + subCategories: [ + { + slug: 'general', + title: 'General', + icon: 'tabler-globe', + articles: [ + { + slug: 'our-rules', + title: 'Our Rules', + content: '

Our purpose is to serve the public conversation. Violence, harassment and other similar types of behavior discourage people from expressing themselves, and ultimately diminish the value of global public conversation. Our rules are to ensure all people can participate in the public conversation freely and safely.

Our purpose is to serve the public conversation. Violence, harassment and other similar types of behavior discourage people from expressing themselves, and ultimately diminish the value of global public conversation. Our rules are to ensure all people can participate in the public conversation freely and safely.

', + }, + { + slug: 'what-is-username-squatting-policy', + title: 'What is username squatting policy?', + content: '

Username squatting is prohibited by the Rules.

Please note that if an account has had no updates, no profile image, and there is no intent to mislead, it typically means there\'s no name-squatting or impersonation. Note that we will not release squatted usernames except in cases of trademark infringement. If your report involves trademark infringement, please consult those policies for instructions for reporting these accounts.

Attempts to sell, buy, or solicit other forms of payment in exchange for usernames are also violations and may result in permanent account suspension.

Username squatting is prohibited by the Rules.

Please note that if an account has had no updates, no profile image, and there is no intent to mislead, it typically means there\'s no name-squatting or impersonation. Note that we will not release squatted usernames except in cases of trademark infringement. If your report involves trademark infringement, please consult those policies for instructions for reporting these accounts.

Attempts to sell, buy, or solicit other forms of payment in exchange for usernames are also violations and may result in permanent account suspension.

', + }, + { + slug: 'third-party-advertising-in-video-content', + title: 'Third-party advertising in video content', + content: '

You may not submit, post, or display any video content on or through our services that includes third-party advertising, such as pre-roll video ads or sponsorship graphics, without our prior consent.

Note: we may need to change these rules from time to time in order to support our goal of promoting a healthy public conversation

You may not submit, post, or display any video content on or through our services that includes third-party advertising, such as pre-roll video ads or sponsorship graphics, without our prior consent.

Note: we may need to change these rules from time to time in order to support our goal of promoting a healthy public conversation

', + }, + ], + }, + { + icon: 'tabler-registered', + slug: 'intellectual-property', + title: 'Intellectual property', + articles: [ + { + slug: 'what-is-your-trademark-policy', + title: 'What is your trademark policy?', + content: '

You may not violate others\' intellectual property rights, including copyright and trademark.

A trademark is a word, logo, phrase, or device that distinguishes a trademark holder\'s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner.

You may not violate others\' intellectual property rights, including copyright and trademark.

A trademark is a word, logo, phrase, or device that distinguishes a trademark holder\'s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner.

', + }, + { + slug: 'what-are-counterfeit-goods', + title: 'What are counterfeit goods?', + content: '

Counterfeit goods are goods, including digital goods, that are promoted, sold, or otherwise distributed using a trademark or brand that is identical to, or substantially indistinguishable from, the registered trademark or brand of another, without authorization from the trademark or brand owner. Counterfeit goods attempt to deceive consumers into believing the counterfeit is a genuine product of the brand owner, or to represent themselves as faux, replicas or imitations of the genuine product.

Counterfeit goods are goods, including digital goods, that are promoted, sold, or otherwise distributed using a trademark or brand that is identical to, or substantially indistinguishable from, the registered trademark or brand of another, without authorization from the trademark or brand owner. Counterfeit goods attempt to deceive consumers into believing the counterfeit is a genuine product of the brand owner, or to represent themselves as faux, replicas or imitations of the genuine product.

', + }, + { + slug: 'what-types-of-copyright-complaints-do-you-respond-to', + title: 'What types of copyright complaints do you respond to?', + content: '

We respond to copyright complaints submitted under the Digital Millennium Copyright Act (“DMCA”). Section 512 of the DMCA outlines the statutory requirements necessary for formally reporting copyright infringement, as well as providing instructions on how an affected party can appeal a removal by submitting a compliant counter-notice.

If you are concerned about the use of your brand or entity\'s name, please review our trademark policy. If you are concerned about a parody, newsfeed, commentary, or fan account, please see the relevant policy here. These are generally not copyright issues.

We respond to copyright complaints submitted under the Digital Millennium Copyright Act (“DMCA”). Section 512 of the DMCA outlines the statutory requirements necessary for formally reporting copyright infringement, as well as providing instructions on how an affected party can appeal a removal by submitting a compliant counter-notice.

If you are concerned about the use of your brand or entity\'s name, please review our trademark policy. If you are concerned about a parody, newsfeed, commentary, or fan account, please see the relevant policy here. These are generally not copyright issues.

', + }, + ], + }, + { + icon: 'tabler-clipboard', + slug: 'guidelines-for-law-enforcement', + title: 'Guidelines for law enforcement', + articles: [ + { + slug: 'does-we-have-access-to-user-generated-photos-or-videos', + title: 'Does we have access to user-generated photos or videos?', + content: `

We provide photo hosting for some image uploads (i.e., pic.${themeConfig.app.title}.com images) as well as account profile photos, and header photos. However, We are not the sole photo provider for images that may appear on the platform. More information about posting photos on platform.

We provide photo hosting for some image uploads (i.e., pic.${themeConfig.app.title}.com images) as well as account profile photos, and header photos. However, We are not the sole photo provider for images that may appear on the platform. More information about posting photos on platform.

`, + }, + { + slug: 'data-controller', + title: 'Data Controller', + content: '

For people who live in the United States or any other country outside of the European Union or the European Economic Area, the data controller responsible for personal data, Inc. based in San Francisco, California. For people who live in the European Union or the European Economic Area, the data controller is our International Unlimited Company based in Dublin, Ireland.

For people who live in the United States or any other country outside of the European Union or the European Economic Area, the data controller responsible for personal data, Inc. based in San Francisco, California. For people who live in the European Union or the European Economic Area, the data controller is our International Unlimited Company based in Dublin, Ireland.

', + }, + { + slug: 'requests-for-Twitter-account-information', + title: 'Requests for Twitter account information', + content: '

Requests for user account information from law enforcement should be directed to us, Inc. in San Francisco, California or Twitter International Unlimited Company in Dublin, Ireland. We respond to valid legal process issued in compliance with applicable law.

Requests for user account information from law enforcement should be directed to us, Inc. in San Francisco, California or Twitter International Unlimited Company in Dublin, Ireland. We respond to valid legal process issued in compliance with applicable law.

', + }, + ], + }, + ], + }, + { + slug: 'chats', + title: 'Chats', + avatarColor: 'warning', + icon: 'tabler-message', + subCategories: [ + { + slug: 'general', + title: 'General', + icon: 'tabler-globe', + articles: [ + { + slug: 'what-is-forwarding-limits', + title: 'What is forwarding limit?', + content: '

You can forward a message with up to five chats at one time. If a message has already been forwarded, you can forward it to up to five chats, including a maximum of one group.

However, when a message is forwarded through a chain of five or more chats, meaning it\'s at least five forwards away from its original sender, a double arrow icon and "Forwarded many times" label will be displayed. These messages can only be forwarded to one chat at a time, as a way to help keep conversations on platform intimate and personal. This also helps slow down the spread of rumors, viral messages, and fake news.

You can forward a message with up to five chats at one time. If a message has already been forwarded, you can forward it to up to five chats, including a maximum of one group.

However, when a message is forwarded through a chain of five or more chats, meaning it\'s at least five forwards away from its original sender, a double arrow icon and "Forwarded many times" label will be displayed. These messages can only be forwarded to one chat at a time, as a way to help keep conversations on platform intimate and personal. This also helps slow down the spread of rumors, viral messages, and fake news.

', + }, + { + slug: 'what-is-last-seen-and-online', + title: 'What is last seen & online?', + content: '

Last seen and online tell you the last time your contacts used the app, or if they\'re online.

If a contact is online, they have th app open in the foreground on their device and are connected to the Internet. However, it doesn\'t necessarily mean the contact has read your message.

Last seen and online tell you the last time your contacts used the app, or if they\'re online.

If a contact is online, they have th app open in the foreground on their device and are connected to the Internet. However, it doesn\'t necessarily mean the contact has read your message.

', + }, + { + slug: 'how-to-reply-to-a-message', + title: 'How to reply to a message?', + content: '

You can use the reply feature when responding to a specific message in an individual or group chat.

Tap and hold the message, then tap Reply. Enter your response and tap Send. Alternatively, swipe right on the message to reply.

You can use the reply feature when responding to a specific message in an individual or group chat.

Tap and hold the message, then tap Reply. Enter your response and tap Send. Alternatively, swipe right on the message to reply.

', + }, + ], + }, + { + slug: 'features', + title: 'Features', + icon: 'tabler-star', + articles: [ + { + slug: 'how-to-send-disappearing-messages', + title: 'How to send disappearing messages?', + content: '

Disappearing messages is an optional feature you can turn on for more privacy.

When you enable disappearing messages, you can set messages to disappear 24 hours, 7 days, or 90 days after the time they are sent. The most recent selection only controls new messages in the chat. You can choose to turn disappearing messages on for all of your chats, or select specific chats. This setting won\'t affect messages you previously sent or received in the chat. In an individual chat, either user can turn disappearing messages on or off. In a group chat, any group participants can turn disappearing messages on or off. However, a group admin can change group settings to allow only admins to turn disappearing messages on or off.

Disappearing messages is an optional feature you can turn on for more privacy.

When you enable disappearing messages, you can set messages to disappear 24 hours, 7 days, or 90 days after the time they are sent. The most recent selection only controls new messages in the chat. You can choose to turn disappearing messages on for all of your chats, or select specific chats. This setting won\'t affect messages you previously sent or received in the chat. In an individual chat, either user can turn disappearing messages on or off. In a group chat, any group participants can turn disappearing messages on or off. However, a group admin can change group settings to allow only admins to turn disappearing messages on or off.

', + }, + { + slug: 'can-i-send-view-once-messages', + title: 'Can I send view once messages?', + content: '

For added privacy, you can now send photos and videos that disappear from your chat after the recipient has opened them once. To use view once, please update the app to the latest version available for your device.

For added privacy, you can now send photos and videos that disappear from your chat after the recipient has opened them once. To use view once, please update the app to the latest version available for your device.

', + }, + { + slug: 'how-to-pin-a-chat', + title: 'How to pin a chat?', + content: '

The pin chat feature allows you to pin up to three specific chats to the top of your chats list so you can quickly find them.

On iPhone: Swipe right on the chat you want to pin, then tap Pin.

On Android: Tap and hold the chat you want to pin, then tap Pin chat

The pin chat feature allows you to pin up to three specific chats to the top of your chats list so you can quickly find them.

On iPhone: Swipe right on the chat you want to pin, then tap Pin.

On Android: Tap and hold the chat you want to pin, then tap Pin chat

', + }, + ], + }, + { + slug: 'encryption', + title: 'Encryption', + icon: 'tabler-lock', + articles: [ + { + slug: 'what-is-end-to-end-encrypted-backup', + title: 'What is end-to-end encrypted backup?', + content: '

End-to-end encryption ensures only you and the person you\'re communicating with can read or listen to what is sent, and nobody in between, not even us. With end-to-end encrypted backup, you can also add that same layer of protection to your backup on iCloud or Google Drive.

End-to-end encryption ensures only you and the person you\'re communicating with can read or listen to what is sent, and nobody in between, not even us. With end-to-end encrypted backup, you can also add that same layer of protection to your backup on iCloud or Google Drive.

', + }, + { + slug: 'can-i-change-password-for-end-to-end-encrypted-backup', + title: 'Can I change password for end-to-end encrypted backup?', + content: '

When you create an end-to-end encrypted backup, your messages and media are stored in the cloud and secured by a password or a 64-digit encryption key. Your password can be changed at any time as long as you have access to your previous password or key.

Note: You won\'t be able to restore your backup if you lose your chats and forget your password or key. We can\'t reset your password or restore your backup for you.

When you create an end-to-end encrypted backup, your messages and media are stored in the cloud and secured by a password or a 64-digit encryption key. Your password can be changed at any time as long as you have access to your previous password or key.

Note: You won\'t be able to restore your backup if you lose your chats and forget your password or key. We can\'t reset your password or restore your backup for you.

', + }, + { + slug: 'can-i-turnoff-end-to-end-encrypted-backup', + title: 'Can I turnoff end-to-end encrypted backup?', + content: '

You can choose to turn off end-to-end encrypted backup by using your password or key, or by authenticating with your biometrics or device PIN. If you turn off end-to-end encrypted backup, your messages and media will no longer back up to the cloud unless you set them up to do so.

You can choose to turn off end-to-end encrypted backup by using your password or key, or by authenticating with your biometrics or device PIN. If you turn off end-to-end encrypted backup, your messages and media will no longer back up to the cloud unless you set them up to do so.

', + }, + ], + }, + ], + }, + { + slug: 'connections', + title: 'Connections', + avatarColor: 'secondary', + icon: 'tabler-link', + subCategories: [ + { + slug: 'conversations', + title: 'Conversations', + icon: 'tabler-message', + articles: [ + { + slug: 'how-to-send-messages-to-connections', + title: 'How to send messages to connections?', + content: '

You can send a message to your connections directly from the messaging page or connections page.

The sent message will be visible in the recipient\'s message list and possibly in their email, depending on their app notification settings.

You can send a message to your connections directly from the messaging page or connections page.

The sent message will be visible in the recipient\'s message list and possibly in their email, depending on their app notification settings.

', + }, + { + slug: 'how-to-edit-or-delete-a-sent-message-within-a-conversation', + title: 'How to edit or delete a sent message within a conversation?', + content: '

You can edit or delete a text only message you send on app.

Note:You can only edit or delete a message within 60 minutes of sending the message.

You can edit or delete a text only message you send on app.

Note:You can only edit or delete a message within 60 minutes of sending the message.

', + }, + { + slug: 'how-to-delete-a-message', + title: 'How to delete a message?', + content: '

A conversation thread starts when a message is sent to one or more people via app messaging. You can delete conversation threads individually or in bulk.

Important:You can\'t restore or access deleted messages. The conversation thread will only be deleted from your inbox and not from the recipient\'s.

A conversation thread starts when a message is sent to one or more people via app messaging. You can delete conversation threads individually or in bulk.

Important:You can\'t restore or access deleted messages. The conversation thread will only be deleted from your inbox and not from the recipient\'s.

', + }, + ], + }, + { + slug: 'jobs', + title: 'Jobs', + icon: 'tabler-briefcase', + articles: [ + { + slug: 'find-relevant-jobs-through-social-hiring-and-meeting-the-team', + title: 'Find relevant jobs through social hiring and meeting the team?', + content: '

We have introduced two features that will help both job seekers and hirers fully engage with the power of their platform.

  • The #social hiring feature will notify members when a first- or second-degree connection is hiring for a relevant job. When a network connection posts a relevant job on app or adds a #hiring frame to their profile picture, app will notify the job seeker. From there, job seekers will be able to view open jobs that people in their network are hiring for.
  • When a member clicks on the job\'s details page, they will see the “Meet the Hiring Team” feature. Members will be able to connect and message the entire team listed in this section, including the job poster.

These features will allow members to find jobs through their connections and stand out to the hiring team. As a result, the hiring team will also be able to reach more potential candidates through their network.

We have introduced two features that will help both job seekers and hirers fully engage with the power of their platform.

  • The #social hiring feature will notify members when a first- or second-degree connection is hiring for a relevant job. When a network connection posts a relevant job on app or adds a #hiring frame to their profile picture, app will notify the job seeker. From there, job seekers will be able to view open jobs that people in their network are hiring for.
  • When a member clicks on the job\'s details page, they will see the “Meet the Hiring Team” feature. Members will be able to connect and message the entire team listed in this section, including the job poster.

These features will allow members to find jobs through their connections and stand out to the hiring team. As a result, the hiring team will also be able to reach more potential candidates through their network.

', + }, + { + slug: 'how-does-the-app-determine-when-a-job-is-relevant', + title: 'How does the app determine when a job is relevant?', + content: '

We will notify job seekers when someone in their network is hiring for a job that matches their current job title or industry listed in your profile or open to work preferences.

We will notify job seekers when someone in their network is hiring for a job that matches their current job title or industry listed in your profile or open to work preferences.

', + }, + { + slug: 'how-can-job-seekers-receive-these-notifications', + title: 'How can job seekers receive these notifications?', + content: '

Members will automatically receive notifications without having to opt in. To turn off the notification, click the three dots next to the notification and select Turn off.

Members will automatically receive notifications without having to opt in. To turn off the notification, click the three dots next to the notification and select Turn off.

', + }, + ], + }, + { + slug: 'people', + title: 'People', + icon: 'tabler-users', + articles: [ + { + slug: 'how-to-import-and-invite-your-email-contacts', + title: 'How to import and invite your email contacts?', + content: '

You can build your network by importing a list of your contacts you already know on the app. This will run a one-time upload of your address book contacts, as well as their detailed contact information. We periodically import and store details about your address book contacts to suggest relevant contacts for you to connect with, to show you relevant updates, and for other uses explained in our Privacy Policy. We\'ll never email anyone without your permission.

You can build your network by importing a list of your contacts you already know on the app. This will run a one-time upload of your address book contacts, as well as their detailed contact information. We periodically import and store details about your address book contacts to suggest relevant contacts for you to connect with, to show you relevant updates, and for other uses explained in our Privacy Policy. We\'ll never email anyone without your permission.

', + }, + { + slug: 'various-ways-to-connect-with-people', + title: 'Various ways to connect with people?', + content: '

Building your network is a great way to stay in touch with alumni, colleagues, and recruiters, as well as connect with new, professional opportunities. A primary email address is mandatory to send invitations. Members become 1st-degree connections when they accept your invitation.

First-degree connections are given access to any information you\'ve displayed on your profile. To ensure an optimal site experience, the members can have a maximum of 30,000 1st-degree connections.

Building your network is a great way to stay in touch with alumni, colleagues, and recruiters, as well as connect with new, professional opportunities. A primary email address is mandatory to send invitations. Members become 1st-degree connections when they accept your invitation.

First-degree connections are given access to any information you\'ve displayed on your profile. To ensure an optimal site experience, the members can have a maximum of 30,000 1st-degree connections.

', + }, + { + slug: 'how-to-follow-or-unfollow-people', + title: 'How to follow or unfollow people?', + content: '

When you follow someone, new content posted or shared by the person will be displayed in your feed. If you no longer wish to see the content of someone in your feed, you can always unfollow this person.

You can find people to follow from your feed, the Notifications tab, My Network page, or from the Search bar at the top of the page.

Unfollowing a person will hide all updates from them on your feed. If you\'re connected to a person and choose to unfollow them, you\'ll remain connected, but won\'t see their updates. They won\'t be notified that you\'ve unfollowed them. The members will receive a notification if you begin following them again.

When you follow someone, new content posted or shared by the person will be displayed in your feed. If you no longer wish to see the content of someone in your feed, you can always unfollow this person.

You can find people to follow from your feed, the Notifications tab, My Network page, or from the Search bar at the top of the page.

Unfollowing a person will hide all updates from them on your feed. If you\'re connected to a person and choose to unfollow them, you\'ll remain connected, but won\'t see their updates. They won\'t be notified that you\'ve unfollowed them. The members will receive a notification if you begin following them again.

', + }, + ], + }, + ], + }, + ], + keepLearning: [ + { + slug: 'blogging', + title: 'Blogging', + img: laptop, + subtitle: 'Expert tips & tools to improve your website or online store using blog.', + }, + { + slug: 'inspiration-center', + title: 'Inspiration Center', + img: lightbulb, + subtitle: 'inspiration from experts to help you start and grow your big ideas.', + }, + { + slug: 'community', + title: 'Community', + img: discord, + subtitle: 'A group of people living in the same place or having a particular.', + }, + ], +} + +mock.onGet('/pages/help-center/landing').reply(() => { + const allArticles = [] + + data.categories.map(category => category.subCategories.map(subCategory => subCategory.articles.map(article => allArticles.push(article)))) + + return [ + 200, + { allArticles, categories: data.categories, popularArticles: data.popularArticles, keepLearning: data.keepLearning }, + ] +}) +mock.onGet('/pages/help-center/subcategory').reply(config => { + const { category, subcategory } = config.params + const filteredData = data.categories.filter(item => item.slug === category) + + return [ + 200, + { + data: filteredData[0], + categories: data.categories, + activeTab: subcategory || filteredData[0].subCategories[0].slug, + }, + ] +}) +mock.onGet('/pages/help-center/article').reply(config => { + const { article, category, subcategory } = config.params + const activeCategory = data.categories.filter(item => item.slug === category)[0] + const activeSubcategory = activeCategory.subCategories.filter(item => item.slug === subcategory)[0] || activeCategory.subCategories[0] + const activeArticle = activeSubcategory.articles.filter(item => item.slug === article)[0] + + return [200, { activeArticle, activeSubcategory, categories: data.categories, articles: activeSubcategory.articles }] +}) diff --git a/resources/js/@fake-db/utils.js b/resources/js/@fake-db/utils.js new file mode 100644 index 0000000..a4bbc45 --- /dev/null +++ b/resources/js/@fake-db/utils.js @@ -0,0 +1,19 @@ +export const paginateArray = (array, perPage, page) => array.slice((page - 1) * perPage, page * perPage) + +// pagination meta +export const paginationMeta = computed(() => { + return (options, total) => { + const start = (options.page - 1) * options.itemsPerPage + 1 + const end = Math.min(options.page * options.itemsPerPage, total) + + return `Showing ${start} to ${end} of ${total} entries` + } +}) +export const genId = array => { + const { length } = array + let lastIndex = 0 + if (length) + lastIndex = Number(array[length - 1]?.id) + 1 + + return lastIndex || (length + 1) +} diff --git a/resources/js/@iconify/build-icons.js b/resources/js/@iconify/build-icons.js new file mode 100644 index 0000000..2ea8dd6 --- /dev/null +++ b/resources/js/@iconify/build-icons.js @@ -0,0 +1,244 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * This is an advanced example for creating icon bundles for Iconify SVG Framework. + * + * It creates a bundle from: + * - All SVG files in a directory. + * - Custom JSON files. + * - Iconify icon sets. + * - SVG framework. + * + * This example uses Iconify Tools to import and clean up icons. + * For Iconify Tools documentation visit https://docs.iconify.design/tools/tools2/ + */ +const node_fs_1 = require("node:fs"); +const node_path_1 = require("node:path"); +// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify +const tools_1 = require("@iconify/tools"); +const utils_1 = require("@iconify/utils"); +const sources = { + svg: [ + { + dir: 'resources/images/iconify-svg', + monotone: false, + prefix: 'custom', + }, + // { + // dir: 'emojis', + // monotone: false, + // prefix: 'emoji', + // }, + ], + icons: [ + // 'mdi:home', + // 'mdi:account', + // 'mdi:login', + // 'mdi:logout', + // 'octicon:book-24', + // 'octicon:code-square-24', + ], + json: [ + // Custom JSON file + // 'json/gg.json', + // Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename) + require.resolve('@iconify-json/bx/icons.json'), + require.resolve('@iconify-json/bxs/icons.json'), + require.resolve('@iconify-json/bxl/icons.json'), + { + filename: require.resolve('@iconify-json/mdi/icons.json'), + icons: [ + 'file-remove-outline', + 'translate', + 'vuetify', + 'information-variant', + 'arrow-top-right', + 'arrow-bottom-right', + 'arrow-bottom-left', + 'arrow-top-left', + 'arrow-collapse-all', + 'arrow-down-left', + 'web', + 'cpu-32-bit', + 'alpha-r', + 'alpha-g', + 'alpha-b', + 'map-marker-off-outline', + 'alpha-t-box-outline', + 'form-select', + 'account-cog-outline', + 'laptop', + ], + }, + // Custom file with only few icons + // { + // filename: require.resolve('@iconify-json/line-md/icons.json'), + // icons: [ + // 'home-twotone-alt', + // 'github', + // 'document-list', + // 'document-code', + // 'image-twotone', + // ], + // }, + ], +}; +// Iconify component (this changes import statement in generated file) +// Available options: '@iconify/react' for React, '@iconify/vue' for Vue 3, '@iconify/vue2' for Vue 2, '@iconify/svelte' for Svelte +const component = '@iconify/vue'; +// Set to true to use require() instead of import +const commonJS = false; +// File to save bundle to +const target = (0, node_path_1.join)(__dirname, 'icons-bundle.js'); +/** + * Do stuff! + */ +// eslint-disable-next-line sonarjs/cognitive-complexity +(async function () { + let bundle = commonJS + ? `const { addCollection } = require('${component}');\n\n` + : `import { addCollection } from '${component}';\n\n`; + // Create directory for output if missing + const dir = (0, node_path_1.dirname)(target); + try { + await node_fs_1.promises.mkdir(dir, { + recursive: true, + }); + } + catch (err) { + // + } + /** + * Convert sources.icons to sources.json + */ + if (sources.icons) { + const sourcesJSON = sources.json ? sources.json : (sources.json = []); + // Sort icons by prefix + const organizedList = organizeIconsList(sources.icons); + for (const prefix in organizedList) { + const filename = require.resolve(`@iconify/json/json/${prefix}.json`); + sourcesJSON.push({ + filename, + icons: organizedList[prefix], + }); + } + } + /** + * Bundle JSON files + */ + if (sources.json) { + for (let i = 0; i < sources.json.length; i++) { + const item = sources.json[i]; + // Load icon set + const filename = typeof item === 'string' ? item : item.filename; + let content = JSON.parse(await node_fs_1.promises.readFile(filename, 'utf8')); + // Filter icons + if (typeof item !== 'string' && item.icons?.length) { + const filteredContent = (0, utils_1.getIcons)(content, item.icons); + if (!filteredContent) + throw new Error(`Cannot find required icons in ${filename}`); + content = filteredContent; + } + // Remove metadata and add to bundle + removeMetaData(content); + (0, utils_1.minifyIconSet)(content); + bundle += `addCollection(${JSON.stringify(content)});\n`; + console.log(`Bundled icons from ${filename}`); + } + } + /** + * Custom SVG + */ + if (sources.svg) { + for (let i = 0; i < sources.svg.length; i++) { + const source = sources.svg[i]; + // Import icons + const iconSet = await (0, tools_1.importDirectory)(source.dir, { + prefix: source.prefix, + }); + // Validate, clean up, fix palette and optimise + await iconSet.forEach(async (name, type) => { + if (type !== 'icon') + return; + // Get SVG instance for parsing + const svg = iconSet.toSVG(name); + if (!svg) { + // Invalid icon + iconSet.remove(name); + return; + } + // Clean up and optimise icons + try { + // Clean up icon code + await (0, tools_1.cleanupSVG)(svg); + if (source.monotone) { + // Replace color with currentColor, add if missing + // If icon is not monotone, remove this code + await (0, tools_1.parseColors)(svg, { + defaultColor: 'currentColor', + callback: (attr, colorStr, color) => { + return (!color || (0, tools_1.isEmptyColor)(color)) + ? colorStr + : 'currentColor'; + }, + }); + } + // Optimise + await (0, tools_1.runSVGO)(svg); + } + catch (err) { + // Invalid icon + console.error(`Error parsing ${name} from ${source.dir}:`, err); + iconSet.remove(name); + return; + } + // Update icon from SVG instance + iconSet.fromSVG(name, svg); + }); + console.log(`Bundled ${iconSet.count()} icons from ${source.dir}`); + // Export to JSON + const content = iconSet.export(); + bundle += `addCollection(${JSON.stringify(content)});\n`; + } + } + // Save to file + await node_fs_1.promises.writeFile(target, bundle, 'utf8'); + console.log(`Saved ${target} (${bundle.length} bytes)`); +})().catch(err => { + console.error(err); +}); +/** + * Remove metadata from icon set + */ +function removeMetaData(iconSet) { + const props = [ + 'info', + 'chars', + 'categories', + 'themes', + 'prefixes', + 'suffixes', + ]; + props.forEach(prop => { + delete iconSet[prop]; + }); +} +/** + * Sort icon names by prefix + */ +function organizeIconsList(icons) { + const sorted = Object.create(null); + icons.forEach(icon => { + const item = (0, utils_1.stringToIcon)(icon); + if (!item) + return; + const prefix = item.prefix; + const prefixList = sorted[prefix] + ? sorted[prefix] + : (sorted[prefix] = []); + const name = item.name; + if (!prefixList.includes(name)) + prefixList.push(name); + }); + return sorted; +} diff --git a/resources/js/@iconify/icons-bundle.js b/resources/js/@iconify/icons-bundle.js new file mode 100644 index 0000000..e397f7e --- /dev/null +++ b/resources/js/@iconify/icons-bundle.js @@ -0,0 +1,7 @@ +import { addCollection } from '@iconify/vue'; + +addCollection({"prefix":"bx","icons":{"abacus":{"body":""},"accessibility":{"body":""},"add-to-queue":{"body":""},"adjust":{"body":""},"alarm":{"body":""},"alarm-add":{"body":""},"alarm-exclamation":{"body":""},"alarm-off":{"body":""},"alarm-snooze":{"body":""},"album":{"body":""},"align-justify":{"body":""},"align-left":{"body":""},"align-middle":{"body":""},"align-right":{"body":""},"analyse":{"body":""},"anchor":{"body":""},"angry":{"body":""},"aperture":{"body":""},"arch":{"body":""},"archive":{"body":""},"archive-in":{"body":""},"archive-out":{"body":""},"area":{"body":""},"arrow-back":{"body":""},"arrow-from-bottom":{"body":""},"arrow-from-left":{"body":""},"arrow-from-right":{"body":""},"arrow-from-top":{"body":""},"arrow-to-bottom":{"body":""},"arrow-to-left":{"body":""},"arrow-to-right":{"body":""},"arrow-to-top":{"body":""},"at":{"body":""},"atom":{"body":""},"award":{"body":""},"badge":{"body":""},"badge-check":{"body":""},"baguette":{"body":""},"ball":{"body":""},"band-aid":{"body":""},"bar-chart":{"body":""},"bar-chart-alt":{"body":""},"bar-chart-alt-2":{"body":""},"bar-chart-square":{"body":""},"barcode":{"body":""},"barcode-reader":{"body":""},"baseball":{"body":""},"basket":{"body":""},"basketball":{"body":""},"bath":{"body":""},"battery":{"body":""},"bed":{"body":""},"been-here":{"body":""},"beer":{"body":""},"bell":{"body":""},"bell-minus":{"body":""},"bell-off":{"body":""},"bell-plus":{"body":""},"bible":{"body":""},"bitcoin":{"body":""},"blanket":{"body":""},"block":{"body":""},"bluetooth":{"body":""},"body":{"body":""},"bold":{"body":""},"bolt-circle":{"body":""},"bomb":{"body":""},"bone":{"body":""},"bong":{"body":""},"book":{"body":""},"book-add":{"body":""},"book-alt":{"body":""},"book-bookmark":{"body":""},"book-content":{"body":""},"book-heart":{"body":""},"book-open":{"body":""},"book-reader":{"body":""},"bookmark":{"body":""},"bookmark-alt":{"body":""},"bookmark-alt-minus":{"body":""},"bookmark-alt-plus":{"body":""},"bookmark-heart":{"body":""},"bookmark-minus":{"body":""},"bookmark-plus":{"body":""},"bookmarks":{"body":""},"border-all":{"body":""},"border-bottom":{"body":""},"border-inner":{"body":""},"border-left":{"body":""},"border-none":{"body":""},"border-outer":{"body":""},"border-radius":{"body":""},"border-right":{"body":""},"border-top":{"body":""},"bot":{"body":""},"bowl-hot":{"body":""},"bowl-rice":{"body":""},"bowling-ball":{"body":""},"box":{"body":""},"bracket":{"body":""},"braille":{"body":""},"brain":{"body":""},"briefcase":{"body":""},"briefcase-alt":{"body":""},"briefcase-alt-2":{"body":""},"brightness":{"body":""},"brightness-half":{"body":""},"broadcast":{"body":""},"brush":{"body":""},"brush-alt":{"body":""},"bug":{"body":""},"bug-alt":{"body":""},"building":{"body":""},"building-house":{"body":""},"buildings":{"body":""},"bulb":{"body":""},"bullseye":{"body":""},"buoy":{"body":""},"bus":{"body":""},"bus-school":{"body":""},"bxl-500px":{"body":"","hidden":true},"bxl-99designs":{"body":"","hidden":true},"bxl-adobe":{"body":"","hidden":true},"bxl-airbnb":{"body":"","hidden":true},"bxl-algolia":{"body":"","hidden":true},"bxl-amazon":{"body":"","hidden":true},"bxl-android":{"body":"","hidden":true},"bxl-angular":{"body":"","hidden":true},"bxl-apple":{"body":"","hidden":true},"bxl-audible":{"body":"","hidden":true},"bxl-aws":{"body":"","hidden":true},"bxl-baidu":{"body":"","hidden":true},"bxl-behance":{"body":"","hidden":true},"bxl-bing":{"body":"","hidden":true},"bxl-bitcoin":{"body":"","hidden":true},"bxl-blender":{"body":"","hidden":true},"bxl-blogger":{"body":"","hidden":true},"bxl-bootstrap":{"body":"","hidden":true},"bxl-c-plus-plus":{"body":"","hidden":true},"bxl-chrome":{"body":"","hidden":true},"bxl-codepen":{"body":"","hidden":true},"bxl-creative-commons":{"body":"","hidden":true},"bxl-css3":{"body":"","hidden":true},"bxl-dailymotion":{"body":"","hidden":true},"bxl-dev-to":{"body":"","hidden":true},"bxl-deviantart":{"body":"","hidden":true},"bxl-digg":{"body":"","hidden":true},"bxl-digitalocean":{"body":"","hidden":true},"bxl-discord":{"body":"","hidden":true},"bxl-discord-alt":{"body":"","hidden":true},"bxl-discourse":{"body":"","hidden":true},"bxl-django":{"body":"","hidden":true},"bxl-docker":{"body":"","hidden":true},"bxl-dribbble":{"body":"","hidden":true},"bxl-dropbox":{"body":"","hidden":true},"bxl-drupal":{"body":"","hidden":true},"bxl-ebay":{"body":"","hidden":true},"bxl-edge":{"body":"","hidden":true},"bxl-etsy":{"body":"","hidden":true},"bxl-facebook":{"body":"","hidden":true},"bxl-facebook-circle":{"body":"","hidden":true},"bxl-facebook-square":{"body":"","hidden":true},"bxl-figma":{"body":"","hidden":true},"bxl-firebase":{"body":"","hidden":true},"bxl-firefox":{"body":"","hidden":true},"bxl-flask":{"body":"","hidden":true},"bxl-flickr":{"body":"","hidden":true},"bxl-flickr-square":{"body":"","hidden":true},"bxl-flutter":{"body":"","hidden":true},"bxl-foursquare":{"body":"","hidden":true},"bxl-git":{"body":"","hidden":true},"bxl-github":{"body":"","hidden":true},"bxl-gitlab":{"body":"","hidden":true},"bxl-gmail":{"body":"","hidden":true},"bxl-go-lang":{"body":"","hidden":true},"bxl-google":{"body":"","hidden":true},"bxl-google-cloud":{"body":"","hidden":true},"bxl-google-plus":{"body":"","hidden":true},"bxl-google-plus-circle":{"body":"","hidden":true},"bxl-heroku":{"body":"","hidden":true},"bxl-html5":{"body":"","hidden":true},"bxl-imdb":{"body":"","hidden":true},"bxl-instagram":{"body":"","hidden":true},"bxl-instagram-alt":{"body":"","hidden":true},"bxl-internet-explorer":{"body":"","hidden":true},"bxl-invision":{"body":"","hidden":true},"bxl-java":{"body":"","hidden":true},"bxl-javascript":{"body":"","hidden":true},"bxl-joomla":{"body":"","hidden":true},"bxl-jquery":{"body":"","hidden":true},"bxl-jsfiddle":{"body":"","hidden":true},"bxl-kickstarter":{"body":"","hidden":true},"bxl-kubernetes":{"body":"","hidden":true},"bxl-less":{"body":"","hidden":true},"bxl-linkedin":{"body":"","hidden":true},"bxl-linkedin-square":{"body":"","hidden":true},"bxl-magento":{"body":"","hidden":true},"bxl-mailchimp":{"body":"","hidden":true},"bxl-markdown":{"body":"","hidden":true},"bxl-mastercard":{"body":"","hidden":true},"bxl-mastodon":{"body":"","hidden":true},"bxl-medium":{"body":"","hidden":true},"bxl-medium-old":{"body":"","hidden":true},"bxl-medium-square":{"body":"","hidden":true},"bxl-messenger":{"body":"","hidden":true},"bxl-meta":{"body":"","hidden":true},"bxl-microsoft":{"body":"","hidden":true},"bxl-microsoft-teams":{"body":"","hidden":true},"bxl-netlify":{"body":"","hidden":true},"bxl-nodejs":{"body":"","hidden":true},"bxl-ok-ru":{"body":"","hidden":true},"bxl-opera":{"body":"","hidden":true},"bxl-patreon":{"body":"","hidden":true},"bxl-paypal":{"body":"","hidden":true},"bxl-periscope":{"body":"","hidden":true},"bxl-php":{"body":"","hidden":true},"bxl-pinterest":{"body":"","hidden":true},"bxl-pinterest-alt":{"body":"","hidden":true},"bxl-play-store":{"body":"","hidden":true},"bxl-pocket":{"body":"","hidden":true},"bxl-product-hunt":{"body":"","hidden":true},"bxl-python":{"body":"","hidden":true},"bxl-quora":{"body":"","hidden":true},"bxl-react":{"body":"","hidden":true},"bxl-redbubble":{"body":"","hidden":true},"bxl-reddit":{"body":"","hidden":true},"bxl-redux":{"body":"","hidden":true},"bxl-sass":{"body":"","hidden":true},"bxl-shopify":{"body":"","hidden":true},"bxl-sketch":{"body":"","hidden":true},"bxl-skype":{"body":"","hidden":true},"bxl-slack":{"body":"","hidden":true},"bxl-slack-old":{"body":"","hidden":true},"bxl-snapchat":{"body":"","hidden":true},"bxl-soundcloud":{"body":"","hidden":true},"bxl-spotify":{"body":"","hidden":true},"bxl-spring-boot":{"body":"","hidden":true},"bxl-squarespace":{"body":"","hidden":true},"bxl-stack-overflow":{"body":"","hidden":true},"bxl-steam":{"body":"","hidden":true},"bxl-stripe":{"body":"","hidden":true},"bxl-tailwind-css":{"body":"","hidden":true},"bxl-telegram":{"body":"","hidden":true},"bxl-tiktok":{"body":"","hidden":true},"bxl-trello":{"body":"","hidden":true},"bxl-trip-advisor":{"body":"","hidden":true},"bxl-tumblr":{"body":"","hidden":true},"bxl-tux":{"body":"","hidden":true},"bxl-twitch":{"body":"","hidden":true},"bxl-twitter":{"body":"","hidden":true},"bxl-unity":{"body":"","hidden":true},"bxl-unsplash":{"body":"","hidden":true},"bxl-upwork":{"body":"","hidden":true},"bxl-venmo":{"body":"","hidden":true},"bxl-vimeo":{"body":"","hidden":true},"bxl-visa":{"body":"","hidden":true},"bxl-visual-studio":{"body":"","hidden":true},"bxl-vk":{"body":"","hidden":true},"bxl-vuejs":{"body":"","hidden":true},"bxl-whatsapp":{"body":"","hidden":true},"bxl-whatsapp-square":{"body":"","hidden":true},"bxl-wikipedia":{"body":"","hidden":true},"bxl-windows":{"body":"","hidden":true},"bxl-wix":{"body":"","hidden":true},"bxl-wordpress":{"body":"","hidden":true},"bxl-yahoo":{"body":"","hidden":true},"bxl-yelp":{"body":"","hidden":true},"bxl-youtube":{"body":"","hidden":true},"bxl-zoom":{"body":"","hidden":true},"bxs-add-to-queue":{"body":"","hidden":true},"bxs-adjust":{"body":"","hidden":true},"bxs-adjust-alt":{"body":"","hidden":true},"bxs-alarm":{"body":"","hidden":true},"bxs-alarm-add":{"body":"","hidden":true},"bxs-alarm-exclamation":{"body":"","hidden":true},"bxs-alarm-off":{"body":"","hidden":true},"bxs-alarm-snooze":{"body":"","hidden":true},"bxs-album":{"body":"","hidden":true},"bxs-ambulance":{"body":"","hidden":true},"bxs-analyse":{"body":"","hidden":true},"bxs-angry":{"body":"","hidden":true},"bxs-arch":{"body":"","hidden":true},"bxs-archive":{"body":"","hidden":true},"bxs-archive-in":{"body":"","hidden":true},"bxs-archive-out":{"body":"","hidden":true},"bxs-area":{"body":"","hidden":true},"bxs-arrow-from-bottom":{"body":"","hidden":true},"bxs-arrow-from-left":{"body":"","hidden":true},"bxs-arrow-from-right":{"body":"","hidden":true},"bxs-arrow-from-top":{"body":"","hidden":true},"bxs-arrow-to-bottom":{"body":"","hidden":true},"bxs-arrow-to-left":{"body":"","hidden":true},"bxs-arrow-to-right":{"body":"","hidden":true},"bxs-arrow-to-top":{"body":"","hidden":true},"bxs-award":{"body":"","hidden":true},"bxs-baby-carriage":{"body":"","hidden":true},"bxs-backpack":{"body":"","hidden":true},"bxs-badge":{"body":"","hidden":true},"bxs-badge-check":{"body":"","hidden":true},"bxs-badge-dollar":{"body":"","hidden":true},"bxs-baguette":{"body":"","hidden":true},"bxs-ball":{"body":"","hidden":true},"bxs-band-aid":{"body":"","hidden":true},"bxs-bank":{"body":"","hidden":true},"bxs-bar-chart-alt-2":{"body":"","hidden":true},"bxs-bar-chart-square":{"body":"","hidden":true},"bxs-barcode":{"body":"","hidden":true},"bxs-baseball":{"body":"","hidden":true},"bxs-basket":{"body":"","hidden":true},"bxs-basketball":{"body":"","hidden":true},"bxs-bath":{"body":"","hidden":true},"bxs-battery":{"body":"","hidden":true},"bxs-battery-charging":{"body":"","hidden":true},"bxs-battery-full":{"body":"","hidden":true},"bxs-battery-low":{"body":"","hidden":true},"bxs-bed":{"body":"","hidden":true},"bxs-been-here":{"body":"","hidden":true},"bxs-beer":{"body":"","hidden":true},"bxs-bell":{"body":"","hidden":true},"bxs-bell-minus":{"body":"","hidden":true},"bxs-bell-off":{"body":"","hidden":true},"bxs-bell-plus":{"body":"","hidden":true},"bxs-bell-ring":{"body":"","hidden":true},"bxs-bible":{"body":"","hidden":true},"bxs-binoculars":{"body":"","hidden":true},"bxs-blanket":{"body":"","hidden":true},"bxs-bolt":{"body":"","hidden":true},"bxs-bolt-circle":{"body":"","hidden":true},"bxs-bomb":{"body":"","hidden":true},"bxs-bone":{"body":"","hidden":true},"bxs-bong":{"body":"","hidden":true},"bxs-book":{"body":"","hidden":true},"bxs-book-add":{"body":"","hidden":true},"bxs-book-alt":{"body":"","hidden":true},"bxs-book-bookmark":{"body":"","hidden":true},"bxs-book-content":{"body":"","hidden":true},"bxs-book-heart":{"body":"","hidden":true},"bxs-book-open":{"body":"","hidden":true},"bxs-book-reader":{"body":"","hidden":true},"bxs-bookmark":{"body":"","hidden":true},"bxs-bookmark-alt":{"body":"","hidden":true},"bxs-bookmark-alt-minus":{"body":"","hidden":true},"bxs-bookmark-alt-plus":{"body":"","hidden":true},"bxs-bookmark-heart":{"body":"","hidden":true},"bxs-bookmark-minus":{"body":"","hidden":true},"bxs-bookmark-plus":{"body":"","hidden":true},"bxs-bookmark-star":{"body":"","hidden":true},"bxs-bookmarks":{"body":"","hidden":true},"bxs-bot":{"body":"","hidden":true},"bxs-bowl-hot":{"body":"","hidden":true},"bxs-bowl-rice":{"body":"","hidden":true},"bxs-bowling-ball":{"body":"","hidden":true},"bxs-box":{"body":"","hidden":true},"bxs-brain":{"body":"","hidden":true},"bxs-briefcase":{"body":"","hidden":true},"bxs-briefcase-alt":{"body":"","hidden":true},"bxs-briefcase-alt-2":{"body":"","hidden":true},"bxs-brightness":{"body":"","hidden":true},"bxs-brightness-half":{"body":"","hidden":true},"bxs-brush":{"body":"","hidden":true},"bxs-brush-alt":{"body":"","hidden":true},"bxs-bug":{"body":"","hidden":true},"bxs-bug-alt":{"body":"","hidden":true},"bxs-building":{"body":"","hidden":true},"bxs-building-house":{"body":"","hidden":true},"bxs-buildings":{"body":"","hidden":true},"bxs-bulb":{"body":"","hidden":true},"bxs-bullseye":{"body":"","hidden":true},"bxs-buoy":{"body":"","hidden":true},"bxs-bus":{"body":"","hidden":true},"bxs-bus-school":{"body":"","hidden":true},"bxs-business":{"body":"","hidden":true},"bxs-cabinet":{"body":"","hidden":true},"bxs-cable-car":{"body":"","hidden":true},"bxs-cake":{"body":"","hidden":true},"bxs-calculator":{"body":"","hidden":true},"bxs-calendar":{"body":"","hidden":true},"bxs-calendar-alt":{"body":"","hidden":true},"bxs-calendar-check":{"body":"","hidden":true},"bxs-calendar-edit":{"body":"","hidden":true},"bxs-calendar-event":{"body":"","hidden":true},"bxs-calendar-exclamation":{"body":"","hidden":true},"bxs-calendar-heart":{"body":"","hidden":true},"bxs-calendar-minus":{"body":"","hidden":true},"bxs-calendar-plus":{"body":"","hidden":true},"bxs-calendar-star":{"body":"","hidden":true},"bxs-calendar-week":{"body":"","hidden":true},"bxs-calendar-x":{"body":"","hidden":true},"bxs-camera":{"body":"","hidden":true},"bxs-camera-home":{"body":"","hidden":true},"bxs-camera-movie":{"body":"","hidden":true},"bxs-camera-off":{"body":"","hidden":true},"bxs-camera-plus":{"body":"","hidden":true},"bxs-capsule":{"body":"","hidden":true},"bxs-captions":{"body":"","hidden":true},"bxs-car":{"body":"","hidden":true},"bxs-car-battery":{"body":"","hidden":true},"bxs-car-crash":{"body":"","hidden":true},"bxs-car-garage":{"body":"","hidden":true},"bxs-car-mechanic":{"body":"","hidden":true},"bxs-car-wash":{"body":"","hidden":true},"bxs-card":{"body":"","hidden":true},"bxs-caret-down-circle":{"body":"","hidden":true},"bxs-caret-down-square":{"body":"","hidden":true},"bxs-caret-left-circle":{"body":"","hidden":true},"bxs-caret-left-square":{"body":"","hidden":true},"bxs-caret-right-circle":{"body":"","hidden":true},"bxs-caret-right-square":{"body":"","hidden":true},"bxs-caret-up-circle":{"body":"","hidden":true},"bxs-caret-up-square":{"body":"","hidden":true},"bxs-carousel":{"body":"","hidden":true},"bxs-cart":{"body":"","hidden":true},"bxs-cart-add":{"body":"","hidden":true},"bxs-cart-alt":{"body":"","hidden":true},"bxs-cart-download":{"body":"","hidden":true},"bxs-cat":{"body":"","hidden":true},"bxs-category":{"body":"","hidden":true},"bxs-category-alt":{"body":"","hidden":true},"bxs-cctv":{"body":"","hidden":true},"bxs-certification":{"body":"","hidden":true},"bxs-chalkboard":{"body":"","hidden":true},"bxs-chart":{"body":"","hidden":true},"bxs-chat":{"body":"","hidden":true},"bxs-check-circle":{"body":"","hidden":true},"bxs-check-shield":{"body":"","hidden":true},"bxs-check-square":{"body":"","hidden":true},"bxs-checkbox":{"body":"","hidden":true},"bxs-checkbox-checked":{"body":"","hidden":true},"bxs-checkbox-minus":{"body":"","hidden":true},"bxs-chess":{"body":"","hidden":true},"bxs-chevron-down":{"body":"","hidden":true},"bxs-chevron-down-circle":{"body":"","hidden":true},"bxs-chevron-down-square":{"body":"","hidden":true},"bxs-chevron-left":{"body":"","hidden":true},"bxs-chevron-left-circle":{"body":"","hidden":true},"bxs-chevron-left-square":{"body":"","hidden":true},"bxs-chevron-right":{"body":"","hidden":true},"bxs-chevron-right-circle":{"body":"","hidden":true},"bxs-chevron-right-square":{"body":"","hidden":true},"bxs-chevron-up":{"body":"","hidden":true},"bxs-chevron-up-circle":{"body":"","hidden":true},"bxs-chevron-up-square":{"body":"","hidden":true},"bxs-chevrons-down":{"body":"","hidden":true},"bxs-chevrons-left":{"body":"","hidden":true},"bxs-chevrons-right":{"body":"","hidden":true},"bxs-chevrons-up":{"body":"","hidden":true},"bxs-chip":{"body":"","hidden":true},"bxs-church":{"body":"","hidden":true},"bxs-circle":{"body":"","hidden":true},"bxs-circle-half":{"body":"","hidden":true},"bxs-circle-quarter":{"body":"","hidden":true},"bxs-circle-three-quarter":{"body":"","hidden":true},"bxs-city":{"body":"","hidden":true},"bxs-clinic":{"body":"","hidden":true},"bxs-cloud":{"body":"","hidden":true},"bxs-cloud-download":{"body":"","hidden":true},"bxs-cloud-lightning":{"body":"","hidden":true},"bxs-cloud-rain":{"body":"","hidden":true},"bxs-cloud-upload":{"body":"","hidden":true},"bxs-coffee":{"body":"","hidden":true},"bxs-coffee-alt":{"body":"","hidden":true},"bxs-coffee-togo":{"body":"","hidden":true},"bxs-cog":{"body":"","hidden":true},"bxs-coin":{"body":"","hidden":true},"bxs-coin-stack":{"body":"","hidden":true},"bxs-collection":{"body":"","hidden":true},"bxs-color-fill":{"body":"","hidden":true},"bxs-comment":{"body":"","hidden":true},"bxs-comment-add":{"body":"","hidden":true},"bxs-comment-check":{"body":"","hidden":true},"bxs-comment-detail":{"body":"","hidden":true},"bxs-comment-dots":{"body":"","hidden":true},"bxs-comment-edit":{"body":"","hidden":true},"bxs-comment-error":{"body":"","hidden":true},"bxs-comment-minus":{"body":"","hidden":true},"bxs-comment-x":{"body":"","hidden":true},"bxs-compass":{"body":"","hidden":true},"bxs-component":{"body":"","hidden":true},"bxs-confused":{"body":"","hidden":true},"bxs-contact":{"body":"","hidden":true},"bxs-conversation":{"body":"","hidden":true},"bxs-cookie":{"body":"","hidden":true},"bxs-cool":{"body":"","hidden":true},"bxs-copy":{"body":"","hidden":true},"bxs-copy-alt":{"body":"","hidden":true},"bxs-copyright":{"body":"","hidden":true},"bxs-coupon":{"body":"","hidden":true},"bxs-credit-card":{"body":"","hidden":true},"bxs-credit-card-alt":{"body":"","hidden":true},"bxs-credit-card-front":{"body":"","hidden":true},"bxs-cricket-ball":{"body":"","hidden":true},"bxs-crop":{"body":"","hidden":true},"bxs-crown":{"body":"","hidden":true},"bxs-cube":{"body":"","hidden":true},"bxs-cube-alt":{"body":"","hidden":true},"bxs-cuboid":{"body":"","hidden":true},"bxs-customize":{"body":"","hidden":true},"bxs-cylinder":{"body":"","hidden":true},"bxs-dashboard":{"body":"","hidden":true},"bxs-data":{"body":"","hidden":true},"bxs-detail":{"body":"","hidden":true},"bxs-devices":{"body":"","hidden":true},"bxs-diamond":{"body":"","hidden":true},"bxs-dice-1":{"body":"","hidden":true},"bxs-dice-2":{"body":"","hidden":true},"bxs-dice-3":{"body":"","hidden":true},"bxs-dice-4":{"body":"","hidden":true},"bxs-dice-5":{"body":"","hidden":true},"bxs-dice-6":{"body":"","hidden":true},"bxs-direction-left":{"body":"","hidden":true},"bxs-direction-right":{"body":"","hidden":true},"bxs-directions":{"body":"","hidden":true},"bxs-disc":{"body":"","hidden":true},"bxs-discount":{"body":"","hidden":true},"bxs-dish":{"body":"","hidden":true},"bxs-dislike":{"body":"","hidden":true},"bxs-dizzy":{"body":"","hidden":true},"bxs-dock-bottom":{"body":"","hidden":true},"bxs-dock-left":{"body":"","hidden":true},"bxs-dock-right":{"body":"","hidden":true},"bxs-dock-top":{"body":"","hidden":true},"bxs-dog":{"body":"","hidden":true},"bxs-dollar-circle":{"body":"","hidden":true},"bxs-donate-blood":{"body":"","hidden":true},"bxs-donate-heart":{"body":"","hidden":true},"bxs-door-open":{"body":"","hidden":true},"bxs-doughnut-chart":{"body":"","hidden":true},"bxs-down-arrow":{"body":"","hidden":true},"bxs-down-arrow-alt":{"body":"","hidden":true},"bxs-down-arrow-circle":{"body":"","hidden":true},"bxs-down-arrow-square":{"body":"","hidden":true},"bxs-download":{"body":"","hidden":true},"bxs-downvote":{"body":"","hidden":true},"bxs-drink":{"body":"","hidden":true},"bxs-droplet":{"body":"","hidden":true},"bxs-droplet-half":{"body":"","hidden":true},"bxs-dryer":{"body":"","hidden":true},"bxs-duplicate":{"body":"","hidden":true},"bxs-edit":{"body":"","hidden":true},"bxs-edit-alt":{"body":"","hidden":true},"bxs-edit-location":{"body":"","hidden":true},"bxs-eject":{"body":"","hidden":true},"bxs-envelope":{"body":"","hidden":true},"bxs-envelope-open":{"body":"","hidden":true},"bxs-eraser":{"body":"","hidden":true},"bxs-error":{"body":"","hidden":true},"bxs-error-alt":{"body":"","hidden":true},"bxs-error-circle":{"body":"","hidden":true},"bxs-ev-station":{"body":"","hidden":true},"bxs-exit":{"body":"","hidden":true},"bxs-extension":{"body":"","hidden":true},"bxs-eyedropper":{"body":"","hidden":true},"bxs-face":{"body":"","hidden":true},"bxs-face-mask":{"body":"","hidden":true},"bxs-factory":{"body":"","hidden":true},"bxs-fast-forward-circle":{"body":"","hidden":true},"bxs-file":{"body":"","hidden":true},"bxs-file-archive":{"body":"","hidden":true},"bxs-file-blank":{"body":"","hidden":true},"bxs-file-css":{"body":"","hidden":true},"bxs-file-doc":{"body":"","hidden":true},"bxs-file-export":{"body":"","hidden":true},"bxs-file-find":{"body":"","hidden":true},"bxs-file-gif":{"body":"","hidden":true},"bxs-file-html":{"body":"","hidden":true},"bxs-file-image":{"body":"","hidden":true},"bxs-file-import":{"body":"","hidden":true},"bxs-file-jpg":{"body":"","hidden":true},"bxs-file-js":{"body":"","hidden":true},"bxs-file-json":{"body":"","hidden":true},"bxs-file-md":{"body":"","hidden":true},"bxs-file-pdf":{"body":"","hidden":true},"bxs-file-plus":{"body":"","hidden":true},"bxs-file-png":{"body":"","hidden":true},"bxs-file-txt":{"body":"","hidden":true},"bxs-film":{"body":"","hidden":true},"bxs-filter-alt":{"body":"","hidden":true},"bxs-first-aid":{"body":"","hidden":true},"bxs-flag":{"body":"","hidden":true},"bxs-flag-alt":{"body":"","hidden":true},"bxs-flag-checkered":{"body":"","hidden":true},"bxs-flame":{"body":"","hidden":true},"bxs-flask":{"body":"","hidden":true},"bxs-florist":{"body":"","hidden":true},"bxs-folder":{"body":"","hidden":true},"bxs-folder-minus":{"body":"","hidden":true},"bxs-folder-open":{"body":"","hidden":true},"bxs-folder-plus":{"body":"","hidden":true},"bxs-food-menu":{"body":"","hidden":true},"bxs-fridge":{"body":"","hidden":true},"bxs-game":{"body":"","hidden":true},"bxs-gas-pump":{"body":"","hidden":true},"bxs-ghost":{"body":"","hidden":true},"bxs-gift":{"body":"","hidden":true},"bxs-graduation":{"body":"","hidden":true},"bxs-grid":{"body":"","hidden":true},"bxs-grid-alt":{"body":"","hidden":true},"bxs-group":{"body":"","hidden":true},"bxs-guitar-amp":{"body":"","hidden":true},"bxs-hand":{"body":"","hidden":true},"bxs-hand-down":{"body":"","hidden":true},"bxs-hand-left":{"body":"","hidden":true},"bxs-hand-right":{"body":"","hidden":true},"bxs-hand-up":{"body":"","hidden":true},"bxs-happy":{"body":"","hidden":true},"bxs-happy-alt":{"body":"","hidden":true},"bxs-happy-beaming":{"body":"","hidden":true},"bxs-happy-heart-eyes":{"body":"","hidden":true},"bxs-hdd":{"body":"","hidden":true},"bxs-heart":{"body":"","hidden":true},"bxs-heart-circle":{"body":"","hidden":true},"bxs-heart-square":{"body":"","hidden":true},"bxs-help-circle":{"body":"","hidden":true},"bxs-hide":{"body":"","hidden":true},"bxs-home":{"body":"","hidden":true},"bxs-home-circle":{"body":"","hidden":true},"bxs-home-heart":{"body":"","hidden":true},"bxs-home-smile":{"body":"","hidden":true},"bxs-hot":{"body":"","hidden":true},"bxs-hotel":{"body":"","hidden":true},"bxs-hourglass":{"body":"","hidden":true},"bxs-hourglass-bottom":{"body":"","hidden":true},"bxs-hourglass-top":{"body":"","hidden":true},"bxs-id-card":{"body":"","hidden":true},"bxs-image":{"body":"","hidden":true},"bxs-image-add":{"body":"","hidden":true},"bxs-image-alt":{"body":"","hidden":true},"bxs-inbox":{"body":"","hidden":true},"bxs-info-circle":{"body":"","hidden":true},"bxs-info-square":{"body":"","hidden":true},"bxs-injection":{"body":"","hidden":true},"bxs-institution":{"body":"","hidden":true},"bxs-invader":{"body":"","hidden":true},"bxs-joystick":{"body":"","hidden":true},"bxs-joystick-alt":{"body":"","hidden":true},"bxs-joystick-button":{"body":"","hidden":true},"bxs-key":{"body":"","hidden":true},"bxs-keyboard":{"body":"","hidden":true},"bxs-label":{"body":"","hidden":true},"bxs-landmark":{"body":"","hidden":true},"bxs-landscape":{"body":"","hidden":true},"bxs-laugh":{"body":"","hidden":true},"bxs-layer":{"body":"","hidden":true},"bxs-layer-minus":{"body":"","hidden":true},"bxs-layer-plus":{"body":"","hidden":true},"bxs-layout":{"body":"","hidden":true},"bxs-leaf":{"body":"","hidden":true},"bxs-left-arrow":{"body":"","hidden":true},"bxs-left-arrow-alt":{"body":"","hidden":true},"bxs-left-arrow-circle":{"body":"","hidden":true},"bxs-left-arrow-square":{"body":"","hidden":true},"bxs-left-down-arrow-circle":{"body":"","hidden":true},"bxs-left-top-arrow-circle":{"body":"","hidden":true},"bxs-lemon":{"body":"","hidden":true},"bxs-like":{"body":"","hidden":true},"bxs-location-plus":{"body":"","hidden":true},"bxs-lock":{"body":"","hidden":true},"bxs-lock-alt":{"body":"","hidden":true},"bxs-lock-open":{"body":"","hidden":true},"bxs-lock-open-alt":{"body":"","hidden":true},"bxs-log-in":{"body":"","hidden":true},"bxs-log-in-circle":{"body":"","hidden":true},"bxs-log-out":{"body":"","hidden":true},"bxs-log-out-circle":{"body":"","hidden":true},"bxs-low-vision":{"body":"","hidden":true},"bxs-magic-wand":{"body":"","hidden":true},"bxs-magnet":{"body":"","hidden":true},"bxs-map":{"body":"","hidden":true},"bxs-map-alt":{"body":"","hidden":true},"bxs-map-pin":{"body":"","hidden":true},"bxs-mask":{"body":"","hidden":true},"bxs-medal":{"body":"","hidden":true},"bxs-megaphone":{"body":"","hidden":true},"bxs-meh":{"body":"","hidden":true},"bxs-meh-alt":{"body":"","hidden":true},"bxs-meh-blank":{"body":"","hidden":true},"bxs-memory-card":{"body":"","hidden":true},"bxs-message":{"body":"","hidden":true},"bxs-message-add":{"body":"","hidden":true},"bxs-message-alt":{"body":"","hidden":true},"bxs-message-alt-add":{"body":"","hidden":true},"bxs-message-alt-check":{"body":"","hidden":true},"bxs-message-alt-detail":{"body":"","hidden":true},"bxs-message-alt-dots":{"body":"","hidden":true},"bxs-message-alt-edit":{"body":"","hidden":true},"bxs-message-alt-error":{"body":"","hidden":true},"bxs-message-alt-minus":{"body":"","hidden":true},"bxs-message-alt-x":{"body":"","hidden":true},"bxs-message-check":{"body":"","hidden":true},"bxs-message-detail":{"body":"","hidden":true},"bxs-message-dots":{"body":"","hidden":true},"bxs-message-edit":{"body":"","hidden":true},"bxs-message-error":{"body":"","hidden":true},"bxs-message-minus":{"body":"","hidden":true},"bxs-message-rounded":{"body":"","hidden":true},"bxs-message-rounded-add":{"body":"","hidden":true},"bxs-message-rounded-check":{"body":"","hidden":true},"bxs-message-rounded-detail":{"body":"","hidden":true},"bxs-message-rounded-dots":{"body":"","hidden":true},"bxs-message-rounded-edit":{"body":"","hidden":true},"bxs-message-rounded-error":{"body":"","hidden":true},"bxs-message-rounded-minus":{"body":"","hidden":true},"bxs-message-rounded-x":{"body":"","hidden":true},"bxs-message-square":{"body":"","hidden":true},"bxs-message-square-add":{"body":"","hidden":true},"bxs-message-square-check":{"body":"","hidden":true},"bxs-message-square-detail":{"body":"","hidden":true},"bxs-message-square-dots":{"body":"","hidden":true},"bxs-message-square-edit":{"body":"","hidden":true},"bxs-message-square-error":{"body":"","hidden":true},"bxs-message-square-minus":{"body":"","hidden":true},"bxs-message-square-x":{"body":"","hidden":true},"bxs-message-x":{"body":"","hidden":true},"bxs-meteor":{"body":"","hidden":true},"bxs-microchip":{"body":"","hidden":true},"bxs-microphone":{"body":"","hidden":true},"bxs-microphone-alt":{"body":"","hidden":true},"bxs-microphone-off":{"body":"","hidden":true},"bxs-minus-circle":{"body":"","hidden":true},"bxs-minus-square":{"body":"","hidden":true},"bxs-mobile":{"body":"","hidden":true},"bxs-mobile-vibration":{"body":"","hidden":true},"bxs-moon":{"body":"","hidden":true},"bxs-mouse":{"body":"","hidden":true},"bxs-mouse-alt":{"body":"","hidden":true},"bxs-movie":{"body":"","hidden":true},"bxs-movie-play":{"body":"","hidden":true},"bxs-music":{"body":"","hidden":true},"bxs-navigation":{"body":"","hidden":true},"bxs-network-chart":{"body":"","hidden":true},"bxs-news":{"body":"","hidden":true},"bxs-no-entry":{"body":"","hidden":true},"bxs-note":{"body":"","hidden":true},"bxs-notepad":{"body":"","hidden":true},"bxs-notification":{"body":"","hidden":true},"bxs-notification-off":{"body":"","hidden":true},"bxs-offer":{"body":"","hidden":true},"bxs-package":{"body":"","hidden":true},"bxs-paint":{"body":"","hidden":true},"bxs-paint-roll":{"body":"","hidden":true},"bxs-palette":{"body":"","hidden":true},"bxs-paper-plane":{"body":"","hidden":true},"bxs-parking":{"body":"","hidden":true},"bxs-party":{"body":"","hidden":true},"bxs-paste":{"body":"","hidden":true},"bxs-pen":{"body":"","hidden":true},"bxs-pencil":{"body":"","hidden":true},"bxs-phone":{"body":"","hidden":true},"bxs-phone-call":{"body":"","hidden":true},"bxs-phone-incoming":{"body":"","hidden":true},"bxs-phone-off":{"body":"","hidden":true},"bxs-phone-outgoing":{"body":"","hidden":true},"bxs-photo-album":{"body":"","hidden":true},"bxs-piano":{"body":"","hidden":true},"bxs-pie-chart":{"body":"","hidden":true},"bxs-pie-chart-alt":{"body":"","hidden":true},"bxs-pie-chart-alt-2":{"body":"","hidden":true},"bxs-pin":{"body":"","hidden":true},"bxs-pizza":{"body":"","hidden":true},"bxs-plane":{"body":"","hidden":true},"bxs-plane-alt":{"body":"","hidden":true},"bxs-plane-land":{"body":"","hidden":true},"bxs-plane-take-off":{"body":"","hidden":true},"bxs-planet":{"body":"","hidden":true},"bxs-playlist":{"body":"","hidden":true},"bxs-plug":{"body":"","hidden":true},"bxs-plus-circle":{"body":"","hidden":true},"bxs-plus-square":{"body":"","hidden":true},"bxs-pointer":{"body":"","hidden":true},"bxs-polygon":{"body":"","hidden":true},"bxs-popsicle":{"body":"","hidden":true},"bxs-printer":{"body":"","hidden":true},"bxs-purchase-tag":{"body":"","hidden":true},"bxs-purchase-tag-alt":{"body":"","hidden":true},"bxs-pyramid":{"body":"","hidden":true},"bxs-quote-alt-left":{"body":"","hidden":true},"bxs-quote-alt-right":{"body":"","hidden":true},"bxs-quote-left":{"body":"","hidden":true},"bxs-quote-right":{"body":"","hidden":true},"bxs-quote-single-left":{"body":"","hidden":true},"bxs-quote-single-right":{"body":"","hidden":true},"bxs-radiation":{"body":"","hidden":true},"bxs-radio":{"body":"","hidden":true},"bxs-receipt":{"body":"","hidden":true},"bxs-rectangle":{"body":"","hidden":true},"bxs-registered":{"body":"","hidden":true},"bxs-rename":{"body":"","hidden":true},"bxs-report":{"body":"","hidden":true},"bxs-rewind-circle":{"body":"","hidden":true},"bxs-right-arrow":{"body":"","hidden":true},"bxs-right-arrow-alt":{"body":"","hidden":true},"bxs-right-arrow-circle":{"body":"","hidden":true},"bxs-right-arrow-square":{"body":"","hidden":true},"bxs-right-down-arrow-circle":{"body":"","hidden":true},"bxs-right-top-arrow-circle":{"body":"","hidden":true},"bxs-rocket":{"body":"","hidden":true},"bxs-ruler":{"body":"","hidden":true},"bxs-sad":{"body":"","hidden":true},"bxs-save":{"body":"","hidden":true},"bxs-school":{"body":"","hidden":true},"bxs-search":{"body":"","hidden":true},"bxs-search-alt-2":{"body":"","hidden":true},"bxs-select-multiple":{"body":"","hidden":true},"bxs-send":{"body":"","hidden":true},"bxs-server":{"body":"","hidden":true},"bxs-shapes":{"body":"","hidden":true},"bxs-share":{"body":"","hidden":true},"bxs-share-alt":{"body":"","hidden":true},"bxs-shield":{"body":"","hidden":true},"bxs-shield-alt-2":{"body":"","hidden":true},"bxs-shield-x":{"body":"","hidden":true},"bxs-ship":{"body":"","hidden":true},"bxs-shocked":{"body":"","hidden":true},"bxs-shopping-bag":{"body":"","hidden":true},"bxs-shopping-bag-alt":{"body":"","hidden":true},"bxs-shopping-bags":{"body":"","hidden":true},"bxs-show":{"body":"","hidden":true},"bxs-skip-next-circle":{"body":"","hidden":true},"bxs-skip-previous-circle":{"body":"","hidden":true},"bxs-skull":{"body":"","hidden":true},"bxs-sleepy":{"body":"","hidden":true},"bxs-slideshow":{"body":"","hidden":true},"bxs-smile":{"body":"","hidden":true},"bxs-sort-alt":{"body":"","hidden":true},"bxs-spa":{"body":"","hidden":true},"bxs-speaker":{"body":"","hidden":true},"bxs-spray-can":{"body":"","hidden":true},"bxs-spreadsheet":{"body":"","hidden":true},"bxs-square":{"body":"","hidden":true},"bxs-square-rounded":{"body":"","hidden":true},"bxs-star":{"body":"","hidden":true},"bxs-star-half":{"body":"","hidden":true},"bxs-sticker":{"body":"","hidden":true},"bxs-stopwatch":{"body":"","hidden":true},"bxs-store":{"body":"","hidden":true},"bxs-store-alt":{"body":"","hidden":true},"bxs-sun":{"body":"","hidden":true},"bxs-t-shirt":{"body":"","hidden":true},"bxs-tachometer":{"body":"","hidden":true},"bxs-tag":{"body":"","hidden":true},"bxs-tag-alt":{"body":"","hidden":true},"bxs-tag-x":{"body":"","hidden":true},"bxs-taxi":{"body":"","hidden":true},"bxs-tennis-ball":{"body":"","hidden":true},"bxs-terminal":{"body":"","hidden":true},"bxs-thermometer":{"body":"","hidden":true},"bxs-time":{"body":"","hidden":true},"bxs-time-five":{"body":"","hidden":true},"bxs-timer":{"body":"","hidden":true},"bxs-tired":{"body":"","hidden":true},"bxs-to-top":{"body":"","hidden":true},"bxs-toggle-left":{"body":"","hidden":true},"bxs-toggle-right":{"body":"","hidden":true},"bxs-tone":{"body":"","hidden":true},"bxs-torch":{"body":"","hidden":true},"bxs-traffic":{"body":"","hidden":true},"bxs-traffic-barrier":{"body":"","hidden":true},"bxs-traffic-cone":{"body":"","hidden":true},"bxs-train":{"body":"","hidden":true},"bxs-trash":{"body":"","hidden":true},"bxs-trash-alt":{"body":"","hidden":true},"bxs-tree":{"body":"","hidden":true},"bxs-tree-alt":{"body":"","hidden":true},"bxs-trophy":{"body":"","hidden":true},"bxs-truck":{"body":"","hidden":true},"bxs-tv":{"body":"","hidden":true},"bxs-up-arrow":{"body":"","hidden":true},"bxs-up-arrow-alt":{"body":"","hidden":true},"bxs-up-arrow-circle":{"body":"","hidden":true},"bxs-up-arrow-square":{"body":"","hidden":true},"bxs-upside-down":{"body":"","hidden":true},"bxs-upvote":{"body":"","hidden":true},"bxs-user":{"body":"","hidden":true},"bxs-user-account":{"body":"","hidden":true},"bxs-user-badge":{"body":"","hidden":true},"bxs-user-check":{"body":"","hidden":true},"bxs-user-circle":{"body":"","hidden":true},"bxs-user-detail":{"body":"","hidden":true},"bxs-user-minus":{"body":"","hidden":true},"bxs-user-pin":{"body":"","hidden":true},"bxs-user-plus":{"body":"","hidden":true},"bxs-user-rectangle":{"body":"","hidden":true},"bxs-user-voice":{"body":"","hidden":true},"bxs-user-x":{"body":"","hidden":true},"bxs-vector":{"body":"","hidden":true},"bxs-vial":{"body":"","hidden":true},"bxs-video":{"body":"","hidden":true},"bxs-video-off":{"body":"","hidden":true},"bxs-video-plus":{"body":"","hidden":true},"bxs-video-recording":{"body":"","hidden":true},"bxs-videos":{"body":"","hidden":true},"bxs-virus":{"body":"","hidden":true},"bxs-virus-block":{"body":"","hidden":true},"bxs-volume":{"body":"","hidden":true},"bxs-volume-full":{"body":"","hidden":true},"bxs-volume-low":{"body":"","hidden":true},"bxs-volume-mute":{"body":"","hidden":true},"bxs-wallet":{"body":"","hidden":true},"bxs-wallet-alt":{"body":"","hidden":true},"bxs-washer":{"body":"","hidden":true},"bxs-watch":{"body":"","hidden":true},"bxs-watch-alt":{"body":"","hidden":true},"bxs-webcam":{"body":"","hidden":true},"bxs-widget":{"body":"","hidden":true},"bxs-window-alt":{"body":"","hidden":true},"bxs-wine":{"body":"","hidden":true},"bxs-wink-smile":{"body":"","hidden":true},"bxs-wink-tongue":{"body":"","hidden":true},"bxs-wrench":{"body":"","hidden":true},"bxs-x-circle":{"body":"","hidden":true},"bxs-x-square":{"body":"","hidden":true},"bxs-yin-yang":{"body":"","hidden":true},"bxs-zap":{"body":"","hidden":true},"bxs-zoom-in":{"body":"","hidden":true},"bxs-zoom-out":{"body":"","hidden":true},"cabinet":{"body":""},"cable-car":{"body":""},"cake":{"body":""},"calculator":{"body":""},"calendar":{"body":""},"calendar-alt":{"body":""},"calendar-check":{"body":""},"calendar-edit":{"body":""},"calendar-event":{"body":""},"calendar-exclamation":{"body":""},"calendar-heart":{"body":""},"calendar-minus":{"body":""},"calendar-plus":{"body":""},"calendar-star":{"body":""},"calendar-week":{"body":""},"calendar-x":{"body":""},"camera":{"body":""},"camera-home":{"body":""},"camera-movie":{"body":""},"camera-off":{"body":""},"candles":{"body":""},"capsule":{"body":""},"captions":{"body":""},"car":{"body":""},"card":{"body":""},"caret-down":{"body":""},"caret-down-circle":{"body":""},"caret-down-square":{"body":""},"caret-left":{"body":""},"caret-left-circle":{"body":""},"caret-left-square":{"body":""},"caret-right":{"body":""},"caret-right-circle":{"body":""},"caret-right-square":{"body":""},"caret-up":{"body":""},"caret-up-circle":{"body":""},"caret-up-square":{"body":""},"carousel":{"body":""},"cart":{"body":""},"cart-add":{"body":""},"cart-alt":{"body":""},"cart-download":{"body":""},"cast":{"body":""},"category":{"body":""},"category-alt":{"body":""},"cctv":{"body":""},"certification":{"body":""},"chair":{"body":""},"chalkboard":{"body":""},"chart":{"body":""},"chat":{"body":""},"check":{"body":""},"check-circle":{"body":""},"check-double":{"body":""},"check-shield":{"body":""},"check-square":{"body":""},"checkbox":{"body":""},"checkbox-checked":{"body":""},"checkbox-minus":{"body":""},"checkbox-square":{"body":""},"cheese":{"body":""},"chevron-down":{"body":""},"chevron-down-circle":{"body":""},"chevron-down-square":{"body":""},"chevron-left":{"body":""},"chevron-left-circle":{"body":""},"chevron-left-square":{"body":""},"chevron-right":{"body":""},"chevron-right-circle":{"body":""},"chevron-right-square":{"body":""},"chevron-up":{"body":""},"chevron-up-circle":{"body":""},"chevron-up-square":{"body":""},"chevrons-down":{"body":""},"chevrons-left":{"body":""},"chevrons-right":{"body":""},"chevrons-up":{"body":""},"child":{"body":""},"chip":{"body":""},"church":{"body":""},"circle":{"body":""},"circle-half":{"body":""},"circle-quarter":{"body":""},"circle-three-quarter":{"body":""},"clinic":{"body":""},"clipboard":{"body":""},"closet":{"body":""},"cloud":{"body":""},"cloud-download":{"body":""},"cloud-drizzle":{"body":""},"cloud-light-rain":{"body":""},"cloud-lightning":{"body":""},"cloud-rain":{"body":""},"cloud-snow":{"body":""},"cloud-upload":{"body":""},"code":{"body":""},"code-alt":{"body":""},"code-block":{"body":""},"code-curly":{"body":""},"coffee":{"body":""},"coffee-togo":{"body":""},"cog":{"body":""},"coin":{"body":""},"coin-stack":{"body":""},"collapse":{"body":""},"collapse-alt":{"body":""},"collapse-horizontal":{"body":""},"collapse-vertical":{"body":""},"collection":{"body":""},"color":{"body":""},"color-fill":{"body":""},"columns":{"body":""},"command":{"body":""},"comment":{"body":""},"comment-add":{"body":""},"comment-check":{"body":""},"comment-detail":{"body":""},"comment-dots":{"body":""},"comment-edit":{"body":""},"comment-error":{"body":""},"comment-minus":{"body":""},"comment-x":{"body":""},"compass":{"body":""},"confused":{"body":""},"conversation":{"body":""},"cookie":{"body":""},"cool":{"body":""},"copy":{"body":""},"copy-alt":{"body":""},"copyright":{"body":""},"credit-card":{"body":""},"credit-card-alt":{"body":""},"credit-card-front":{"body":""},"cricket-ball":{"body":""},"crop":{"body":""},"cross":{"body":""},"crosshair":{"body":""},"crown":{"body":""},"cube":{"body":""},"cube-alt":{"body":""},"cuboid":{"body":""},"current-location":{"body":""},"customize":{"body":""},"cut":{"body":""},"cycling":{"body":""},"cylinder":{"body":""},"data":{"body":""},"desktop":{"body":""},"detail":{"body":""},"devices":{"body":""},"dialpad":{"body":""},"dialpad-alt":{"body":""},"diamond":{"body":""},"dice-1":{"body":""},"dice-2":{"body":""},"dice-3":{"body":""},"dice-4":{"body":""},"dice-5":{"body":""},"dice-6":{"body":""},"directions":{"body":""},"disc":{"body":""},"dish":{"body":""},"dislike":{"body":""},"dizzy":{"body":""},"dna":{"body":""},"dock-bottom":{"body":""},"dock-left":{"body":""},"dock-right":{"body":""},"dock-top":{"body":""},"dollar":{"body":""},"dollar-circle":{"body":""},"donate-blood":{"body":""},"donate-heart":{"body":""},"door-open":{"body":""},"dots-horizontal":{"body":""},"dots-horizontal-rounded":{"body":""},"dots-vertical":{"body":""},"dots-vertical-rounded":{"body":""},"doughnut-chart":{"body":""},"down-arrow":{"body":""},"down-arrow-alt":{"body":""},"down-arrow-circle":{"body":""},"download":{"body":""},"downvote":{"body":""},"drink":{"body":""},"droplet":{"body":""},"dumbbell":{"body":""},"duplicate":{"body":""},"edit":{"body":""},"edit-alt":{"body":""},"envelope":{"body":""},"envelope-open":{"body":""},"equalizer":{"body":""},"eraser":{"body":""},"error":{"body":""},"error-alt":{"body":""},"error-circle":{"body":""},"euro":{"body":""},"exclude":{"body":""},"exit":{"body":""},"exit-fullscreen":{"body":""},"expand":{"body":""},"expand-alt":{"body":""},"expand-horizontal":{"body":""},"expand-vertical":{"body":""},"export":{"body":""},"extension":{"body":""},"face":{"body":""},"fast-forward":{"body":""},"fast-forward-circle":{"body":""},"female":{"body":""},"female-sign":{"body":""},"file":{"body":""},"file-blank":{"body":""},"file-find":{"body":""},"film":{"body":""},"filter":{"body":""},"filter-alt":{"body":""},"fingerprint":{"body":""},"first-aid":{"body":""},"first-page":{"body":""},"flag":{"body":""},"folder":{"body":""},"folder-minus":{"body":""},"folder-open":{"body":""},"folder-plus":{"body":""},"font":{"body":""},"font-color":{"body":""},"font-family":{"body":""},"font-size":{"body":""},"food-menu":{"body":""},"food-tag":{"body":""},"football":{"body":""},"fork":{"body":""},"fridge":{"body":""},"fullscreen":{"body":""},"game":{"body":""},"gas-pump":{"body":""},"ghost":{"body":""},"gift":{"body":""},"git-branch":{"body":""},"git-commit":{"body":""},"git-compare":{"body":""},"git-merge":{"body":""},"git-pull-request":{"body":""},"git-repo-forked":{"body":""},"glasses":{"body":""},"glasses-alt":{"body":""},"globe":{"body":""},"globe-alt":{"body":""},"grid":{"body":""},"grid-alt":{"body":""},"grid-horizontal":{"body":""},"grid-small":{"body":""},"grid-vertical":{"body":""},"group":{"body":""},"handicap":{"body":""},"happy":{"body":""},"happy-alt":{"body":""},"happy-beaming":{"body":""},"happy-heart-eyes":{"body":""},"hard-hat":{"body":""},"hash":{"body":""},"hdd":{"body":""},"heading":{"body":""},"headphone":{"body":""},"health":{"body":""},"heart":{"body":""},"heart-circle":{"body":""},"heart-square":{"body":""},"help-circle":{"body":""},"hide":{"body":""},"highlight":{"body":""},"history":{"body":""},"hive":{"body":""},"home":{"body":""},"home-alt":{"body":""},"home-alt-2":{"body":""},"home-circle":{"body":""},"home-heart":{"body":""},"home-smile":{"body":""},"horizontal-center":{"body":""},"horizontal-left":{"body":""},"horizontal-right":{"body":""},"hotel":{"body":""},"hourglass":{"body":""},"id-card":{"body":""},"image":{"body":""},"image-add":{"body":""},"image-alt":{"body":""},"images":{"body":""},"import":{"body":""},"infinite":{"body":""},"info-circle":{"body":""},"info-square":{"body":""},"injection":{"body":""},"intersect":{"body":""},"italic":{"body":""},"joystick":{"body":""},"joystick-alt":{"body":""},"joystick-button":{"body":""},"key":{"body":""},"knife":{"body":""},"label":{"body":""},"landscape":{"body":""},"laptop":{"body":""},"last-page":{"body":""},"laugh":{"body":""},"layer":{"body":""},"layer-minus":{"body":""},"layer-plus":{"body":""},"layout":{"body":""},"leaf":{"body":""},"left-arrow":{"body":""},"left-arrow-alt":{"body":""},"left-arrow-circle":{"body":""},"left-down-arrow-circle":{"body":""},"left-indent":{"body":""},"left-top-arrow-circle":{"body":""},"lemon":{"body":""},"library":{"body":""},"like":{"body":""},"line-chart":{"body":""},"line-chart-down":{"body":""},"link":{"body":""},"link-alt":{"body":""},"link-external":{"body":""},"lira":{"body":""},"list-check":{"body":""},"list-minus":{"body":""},"list-ol":{"body":""},"list-plus":{"body":""},"list-ul":{"body":""},"loader":{"body":""},"loader-alt":{"body":""},"loader-circle":{"body":""},"location-plus":{"body":""},"lock":{"body":""},"lock-alt":{"body":""},"lock-open":{"body":""},"lock-open-alt":{"body":""},"log-in":{"body":""},"log-in-circle":{"body":""},"log-out":{"body":""},"log-out-circle":{"body":""},"low-vision":{"body":""},"magnet":{"body":""},"mail-send":{"body":""},"male":{"body":""},"male-female":{"body":""},"male-sign":{"body":""},"map":{"body":""},"map-alt":{"body":""},"map-pin":{"body":""},"mask":{"body":""},"math":{"body":""},"medal":{"body":""},"meh":{"body":""},"meh-alt":{"body":""},"meh-blank":{"body":""},"memory-card":{"body":""},"menu":{"body":""},"menu-alt-left":{"body":""},"menu-alt-right":{"body":""},"merge":{"body":""},"message":{"body":""},"message-add":{"body":""},"message-alt":{"body":""},"message-alt-add":{"body":""},"message-alt-check":{"body":""},"message-alt-detail":{"body":""},"message-alt-dots":{"body":""},"message-alt-edit":{"body":""},"message-alt-error":{"body":""},"message-alt-minus":{"body":""},"message-alt-x":{"body":""},"message-check":{"body":""},"message-detail":{"body":""},"message-dots":{"body":""},"message-edit":{"body":""},"message-error":{"body":""},"message-minus":{"body":""},"message-rounded":{"body":""},"message-rounded-add":{"body":""},"message-rounded-check":{"body":""},"message-rounded-detail":{"body":""},"message-rounded-dots":{"body":""},"message-rounded-edit":{"body":""},"message-rounded-error":{"body":""},"message-rounded-minus":{"body":""},"message-rounded-x":{"body":""},"message-square":{"body":""},"message-square-add":{"body":""},"message-square-check":{"body":""},"message-square-detail":{"body":""},"message-square-dots":{"body":""},"message-square-edit":{"body":""},"message-square-error":{"body":""},"message-square-minus":{"body":""},"message-square-x":{"body":""},"message-x":{"body":""},"meteor":{"body":""},"microchip":{"body":""},"microphone":{"body":""},"microphone-off":{"body":""},"minus":{"body":""},"minus-back":{"body":""},"minus-circle":{"body":""},"minus-front":{"body":""},"mobile":{"body":""},"mobile-alt":{"body":""},"mobile-landscape":{"body":""},"mobile-vibration":{"body":""},"money":{"body":""},"money-withdraw":{"body":""},"moon":{"body":""},"mouse":{"body":""},"mouse-alt":{"body":""},"move":{"body":""},"move-horizontal":{"body":""},"move-vertical":{"body":""},"movie":{"body":""},"movie-play":{"body":""},"music":{"body":""},"navigation":{"body":""},"network-chart":{"body":""},"news":{"body":""},"no-entry":{"body":""},"no-signal":{"body":""},"note":{"body":""},"notepad":{"body":""},"notification":{"body":""},"notification-off":{"body":""},"objects-horizontal-center":{"body":""},"objects-horizontal-left":{"body":""},"objects-horizontal-right":{"body":""},"objects-vertical-bottom":{"body":""},"objects-vertical-center":{"body":""},"objects-vertical-top":{"body":""},"outline":{"body":""},"package":{"body":""},"paint":{"body":""},"paint-roll":{"body":""},"palette":{"body":""},"paper-plane":{"body":""},"paperclip":{"body":""},"paragraph":{"body":""},"party":{"body":""},"paste":{"body":""},"pause":{"body":""},"pause-circle":{"body":""},"pen":{"body":""},"pencil":{"body":""},"phone":{"body":""},"phone-call":{"body":""},"phone-incoming":{"body":""},"phone-off":{"body":""},"phone-outgoing":{"body":""},"photo-album":{"body":""},"pie-chart":{"body":""},"pie-chart-alt":{"body":""},"pie-chart-alt-2":{"body":""},"pin":{"body":""},"planet":{"body":""},"play":{"body":""},"play-circle":{"body":""},"plug":{"body":""},"plus":{"body":""},"plus-circle":{"body":""},"plus-medical":{"body":""},"podcast":{"body":""},"pointer":{"body":""},"poll":{"body":""},"polygon":{"body":""},"popsicle":{"body":""},"pound":{"body":""},"power-off":{"body":""},"printer":{"body":""},"pulse":{"body":""},"purchase-tag":{"body":""},"purchase-tag-alt":{"body":""},"pyramid":{"body":""},"qr":{"body":""},"qr-scan":{"body":""},"question-mark":{"body":""},"radar":{"body":""},"radio":{"body":""},"radio-circle":{"body":""},"radio-circle-marked":{"body":""},"receipt":{"body":""},"rectangle":{"body":""},"recycle":{"body":""},"redo":{"body":""},"reflect-horizontal":{"body":""},"reflect-vertical":{"body":""},"refresh":{"body":""},"registered":{"body":""},"rename":{"body":""},"repeat":{"body":""},"reply":{"body":""},"reply-all":{"body":""},"repost":{"body":""},"reset":{"body":""},"restaurant":{"body":""},"revision":{"body":""},"rewind":{"body":""},"rewind-circle":{"body":""},"rfid":{"body":""},"right-arrow":{"body":""},"right-arrow-alt":{"body":""},"right-arrow-circle":{"body":""},"right-down-arrow-circle":{"body":""},"right-indent":{"body":""},"right-top-arrow-circle":{"body":""},"rocket":{"body":""},"rotate-left":{"body":""},"rotate-right":{"body":""},"rss":{"body":""},"ruble":{"body":""},"ruler":{"body":""},"run":{"body":""},"rupee":{"body":""},"sad":{"body":""},"save":{"body":""},"scan":{"body":""},"scatter-chart":{"body":""},"screenshot":{"body":""},"search":{"body":""},"search-alt":{"body":""},"search-alt-2":{"body":""},"select-multiple":{"body":""},"selection":{"body":""},"send":{"body":""},"server":{"body":""},"shape-circle":{"body":""},"shape-polygon":{"body":""},"shape-square":{"body":""},"shape-triangle":{"body":""},"share":{"body":""},"share-alt":{"body":""},"shekel":{"body":""},"shield":{"body":""},"shield-alt":{"body":""},"shield-alt-2":{"body":""},"shield-minus":{"body":""},"shield-plus":{"body":""},"shield-quarter":{"body":""},"shield-x":{"body":""},"shocked":{"body":""},"shopping-bag":{"body":""},"show":{"body":""},"show-alt":{"body":""},"shower":{"body":""},"shuffle":{"body":""},"sidebar":{"body":""},"signal-1":{"body":""},"signal-2":{"body":""},"signal-3":{"body":""},"signal-4":{"body":""},"signal-5":{"body":""},"sitemap":{"body":""},"skip-next":{"body":""},"skip-next-circle":{"body":""},"skip-previous":{"body":""},"skip-previous-circle":{"body":""},"sleepy":{"body":""},"slider":{"body":""},"slider-alt":{"body":""},"slideshow":{"body":""},"smile":{"body":""},"sort":{"body":""},"sort-a-z":{"body":""},"sort-alt-2":{"body":""},"sort-down":{"body":""},"sort-up":{"body":""},"sort-z-a":{"body":""},"spa":{"body":""},"space-bar":{"body":""},"speaker":{"body":""},"spray-can":{"body":""},"spreadsheet":{"body":""},"square":{"body":""},"square-rounded":{"body":""},"star":{"body":""},"station":{"body":""},"stats":{"body":""},"sticker":{"body":""},"stop":{"body":""},"stop-circle":{"body":""},"stopwatch":{"body":""},"store":{"body":""},"store-alt":{"body":""},"street-view":{"body":""},"strikethrough":{"body":""},"subdirectory-left":{"body":""},"subdirectory-right":{"body":""},"sun":{"body":""},"support":{"body":""},"sushi":{"body":""},"swim":{"body":""},"sync":{"body":""},"tab":{"body":""},"table":{"body":""},"tachometer":{"body":""},"tag":{"body":""},"tag-alt":{"body":""},"target-lock":{"body":""},"task":{"body":""},"task-x":{"body":""},"taxi":{"body":""},"tennis-ball":{"body":""},"terminal":{"body":""},"test-tube":{"body":""},"text":{"body":""},"time":{"body":""},"time-five":{"body":""},"timer":{"body":""},"tired":{"body":""},"toggle-left":{"body":""},"toggle-right":{"body":""},"tone":{"body":""},"traffic-cone":{"body":""},"train":{"body":""},"transfer":{"body":""},"transfer-alt":{"body":""},"trash":{"body":""},"trash-alt":{"body":""},"trending-down":{"body":""},"trending-up":{"body":""},"trim":{"body":""},"trip":{"body":""},"trophy":{"body":""},"tv":{"body":""},"underline":{"body":""},"undo":{"body":""},"unite":{"body":""},"universal-access":{"body":""},"unlink":{"body":""},"up-arrow":{"body":""},"up-arrow-alt":{"body":""},"up-arrow-circle":{"body":""},"upload":{"body":""},"upside-down":{"body":""},"upvote":{"body":""},"usb":{"body":""},"user":{"body":""},"user-check":{"body":""},"user-circle":{"body":""},"user-minus":{"body":""},"user-pin":{"body":""},"user-plus":{"body":""},"user-voice":{"body":""},"user-x":{"body":""},"vector":{"body":""},"vertical-bottom":{"body":""},"vertical-center":{"body":""},"vertical-top":{"body":""},"vial":{"body":""},"video":{"body":""},"video-off":{"body":""},"video-plus":{"body":""},"video-recording":{"body":""},"voicemail":{"body":""},"volume":{"body":""},"volume-full":{"body":""},"volume-low":{"body":""},"volume-mute":{"body":""},"walk":{"body":""},"wallet":{"body":""},"wallet-alt":{"body":""},"water":{"body":""},"webcam":{"body":""},"wifi":{"body":""},"wifi-0":{"body":""},"wifi-1":{"body":""},"wifi-2":{"body":""},"wifi-off":{"body":""},"wind":{"body":""},"window":{"body":""},"window-alt":{"body":""},"window-close":{"body":""},"window-open":{"body":""},"windows":{"body":""},"wine":{"body":""},"wink-smile":{"body":""},"wink-tongue":{"body":""},"won":{"body":""},"world":{"body":""},"wrench":{"body":""},"x":{"body":""},"x-circle":{"body":""},"yen":{"body":""},"zoom-in":{"body":""},"zoom-out":{"body":""}},"aliases":{"bx-abacus":{"parent":"abacus"},"bx-accessibility":{"parent":"accessibility"},"bx-add-to-queue":{"parent":"add-to-queue"},"bx-adjust":{"parent":"adjust"},"bx-alarm":{"parent":"alarm"},"bx-alarm-add":{"parent":"alarm-add"},"bx-alarm-exclamation":{"parent":"alarm-exclamation"},"bx-alarm-off":{"parent":"alarm-off"},"bx-alarm-snooze":{"parent":"alarm-snooze"},"bx-album":{"parent":"album"},"bx-align-justify":{"parent":"align-justify"},"bx-align-left":{"parent":"align-left"},"bx-align-middle":{"parent":"align-middle"},"bx-align-right":{"parent":"align-right"},"bx-analyse":{"parent":"analyse"},"bx-anchor":{"parent":"anchor"},"bx-angry":{"parent":"angry"},"bx-aperture":{"parent":"aperture"},"bx-arch":{"parent":"arch"},"bx-archive":{"parent":"archive"},"bx-archive-in":{"parent":"archive-in"},"bx-archive-out":{"parent":"archive-out"},"bx-area":{"parent":"area"},"bx-arrow-back":{"parent":"arrow-back"},"bx-arrow-from-bottom":{"parent":"arrow-from-bottom"},"bx-arrow-from-left":{"parent":"arrow-from-left"},"bx-arrow-from-right":{"parent":"arrow-from-right"},"bx-arrow-from-top":{"parent":"arrow-from-top"},"bx-arrow-to-bottom":{"parent":"arrow-to-bottom"},"bx-arrow-to-left":{"parent":"arrow-to-left"},"bx-arrow-to-right":{"parent":"arrow-to-right"},"bx-arrow-to-top":{"parent":"arrow-to-top"},"bx-at":{"parent":"at"},"bx-atom":{"parent":"atom"},"bx-award":{"parent":"award"},"bx-badge":{"parent":"badge"},"bx-badge-check":{"parent":"badge-check"},"bx-baguette":{"parent":"baguette"},"bx-ball":{"parent":"ball"},"bx-band-aid":{"parent":"band-aid"},"bx-bar-chart":{"parent":"bar-chart"},"bx-bar-chart-alt":{"parent":"bar-chart-alt"},"bx-bar-chart-alt-2":{"parent":"bar-chart-alt-2"},"bx-bar-chart-square":{"parent":"bar-chart-square"},"bx-barcode":{"parent":"barcode"},"bx-barcode-reader":{"parent":"barcode-reader"},"bx-baseball":{"parent":"baseball"},"bx-basket":{"parent":"basket"},"bx-basketball":{"parent":"basketball"},"bx-bath":{"parent":"bath"},"bx-battery":{"parent":"battery"},"bx-bed":{"parent":"bed"},"bx-been-here":{"parent":"been-here"},"bx-beer":{"parent":"beer"},"bx-bell":{"parent":"bell"},"bx-bell-minus":{"parent":"bell-minus"},"bx-bell-off":{"parent":"bell-off"},"bx-bell-plus":{"parent":"bell-plus"},"bx-bible":{"parent":"bible"},"bx-bitcoin":{"parent":"bitcoin"},"bx-blanket":{"parent":"blanket"},"bx-block":{"parent":"block"},"bx-bluetooth":{"parent":"bluetooth"},"bx-body":{"parent":"body"},"bx-bold":{"parent":"bold"},"bx-bolt-circle":{"parent":"bolt-circle"},"bx-bomb":{"parent":"bomb"},"bx-bone":{"parent":"bone"},"bx-bong":{"parent":"bong"},"bx-book":{"parent":"book"},"bx-book-add":{"parent":"book-add"},"bx-book-alt":{"parent":"book-alt"},"bx-book-bookmark":{"parent":"book-bookmark"},"bx-book-content":{"parent":"book-content"},"bx-book-heart":{"parent":"book-heart"},"bx-book-open":{"parent":"book-open"},"bx-book-reader":{"parent":"book-reader"},"bx-bookmark":{"parent":"bookmark"},"bx-bookmark-alt":{"parent":"bookmark-alt"},"bx-bookmark-alt-minus":{"parent":"bookmark-alt-minus"},"bx-bookmark-alt-plus":{"parent":"bookmark-alt-plus"},"bx-bookmark-heart":{"parent":"bookmark-heart"},"bx-bookmark-minus":{"parent":"bookmark-minus"},"bx-bookmark-plus":{"parent":"bookmark-plus"},"bx-bookmarks":{"parent":"bookmarks"},"bx-border-all":{"parent":"border-all"},"bx-border-bottom":{"parent":"border-bottom"},"bx-border-inner":{"parent":"border-inner"},"bx-border-left":{"parent":"border-left"},"bx-border-none":{"parent":"border-none"},"bx-border-outer":{"parent":"border-outer"},"bx-border-radius":{"parent":"border-radius"},"bx-border-right":{"parent":"border-right"},"bx-border-top":{"parent":"border-top"},"bx-bot":{"parent":"bot"},"bx-bowl-hot":{"parent":"bowl-hot"},"bx-bowl-rice":{"parent":"bowl-rice"},"bx-bowling-ball":{"parent":"bowling-ball"},"bx-box":{"parent":"box"},"bx-bracket":{"parent":"bracket"},"bx-braille":{"parent":"braille"},"bx-brain":{"parent":"brain"},"bx-briefcase":{"parent":"briefcase"},"bx-briefcase-alt":{"parent":"briefcase-alt"},"bx-briefcase-alt-2":{"parent":"briefcase-alt-2"},"bx-brightness":{"parent":"brightness"},"bx-brightness-half":{"parent":"brightness-half"},"bx-broadcast":{"parent":"broadcast"},"bx-brush":{"parent":"brush"},"bx-brush-alt":{"parent":"brush-alt"},"bx-bug":{"parent":"bug"},"bx-bug-alt":{"parent":"bug-alt"},"bx-building":{"parent":"building"},"bx-building-house":{"parent":"building-house"},"bx-buildings":{"parent":"buildings"},"bx-bulb":{"parent":"bulb"},"bx-bullseye":{"parent":"bullseye"},"bx-buoy":{"parent":"buoy"},"bx-bus":{"parent":"bus"},"bx-bus-school":{"parent":"bus-school"},"bx-cabinet":{"parent":"cabinet"},"bx-cable-car":{"parent":"cable-car"},"bx-cake":{"parent":"cake"},"bx-calculator":{"parent":"calculator"},"bx-calendar":{"parent":"calendar"},"bx-calendar-alt":{"parent":"calendar-alt"},"bx-calendar-check":{"parent":"calendar-check"},"bx-calendar-edit":{"parent":"calendar-edit"},"bx-calendar-event":{"parent":"calendar-event"},"bx-calendar-exclamation":{"parent":"calendar-exclamation"},"bx-calendar-heart":{"parent":"calendar-heart"},"bx-calendar-minus":{"parent":"calendar-minus"},"bx-calendar-plus":{"parent":"calendar-plus"},"bx-calendar-star":{"parent":"calendar-star"},"bx-calendar-week":{"parent":"calendar-week"},"bx-calendar-x":{"parent":"calendar-x"},"bx-camera":{"parent":"camera"},"bx-camera-home":{"parent":"camera-home"},"bx-camera-movie":{"parent":"camera-movie"},"bx-camera-off":{"parent":"camera-off"},"bx-candles":{"parent":"candles"},"bx-capsule":{"parent":"capsule"},"bx-captions":{"parent":"captions"},"bx-car":{"parent":"car"},"bx-card":{"parent":"card"},"bx-caret-down":{"parent":"caret-down"},"bx-caret-down-circle":{"parent":"caret-down-circle"},"bx-caret-down-square":{"parent":"caret-down-square"},"bx-caret-left":{"parent":"caret-left"},"bx-caret-left-circle":{"parent":"caret-left-circle"},"bx-caret-left-square":{"parent":"caret-left-square"},"bx-caret-right":{"parent":"caret-right"},"bx-caret-right-circle":{"parent":"caret-right-circle"},"bx-caret-right-square":{"parent":"caret-right-square"},"bx-caret-up":{"parent":"caret-up"},"bx-caret-up-circle":{"parent":"caret-up-circle"},"bx-caret-up-square":{"parent":"caret-up-square"},"bx-carousel":{"parent":"carousel"},"bx-cart":{"parent":"cart"},"bx-cart-add":{"parent":"cart-add"},"bx-cart-alt":{"parent":"cart-alt"},"bx-cart-download":{"parent":"cart-download"},"bx-cast":{"parent":"cast"},"bx-category":{"parent":"category"},"bx-category-alt":{"parent":"category-alt"},"bx-cctv":{"parent":"cctv"},"bx-certification":{"parent":"certification"},"bx-chair":{"parent":"chair"},"bx-chalkboard":{"parent":"chalkboard"},"bx-chart":{"parent":"chart"},"bx-chat":{"parent":"chat"},"bx-check":{"parent":"check"},"bx-check-circle":{"parent":"check-circle"},"bx-check-double":{"parent":"check-double"},"bx-check-shield":{"parent":"check-shield"},"bx-check-square":{"parent":"check-square"},"bx-checkbox":{"parent":"checkbox"},"bx-checkbox-checked":{"parent":"checkbox-checked"},"bx-checkbox-minus":{"parent":"checkbox-minus"},"bx-checkbox-square":{"parent":"checkbox-square"},"bx-cheese":{"parent":"cheese"},"bx-chevron-down":{"parent":"chevron-down"},"bx-chevron-down-circle":{"parent":"chevron-down-circle"},"bx-chevron-down-square":{"parent":"chevron-down-square"},"bx-chevron-left":{"parent":"chevron-left"},"bx-chevron-left-circle":{"parent":"chevron-left-circle"},"bx-chevron-left-square":{"parent":"chevron-left-square"},"bx-chevron-right":{"parent":"chevron-right"},"bx-chevron-right-circle":{"parent":"chevron-right-circle"},"bx-chevron-right-square":{"parent":"chevron-right-square"},"bx-chevron-up":{"parent":"chevron-up"},"bx-chevron-up-circle":{"parent":"chevron-up-circle"},"bx-chevron-up-square":{"parent":"chevron-up-square"},"bx-chevrons-down":{"parent":"chevrons-down"},"bx-chevrons-left":{"parent":"chevrons-left"},"bx-chevrons-right":{"parent":"chevrons-right"},"bx-chevrons-up":{"parent":"chevrons-up"},"bx-child":{"parent":"child"},"bx-chip":{"parent":"chip"},"bx-church":{"parent":"church"},"bx-circle":{"parent":"circle"},"bx-circle-half":{"parent":"circle-half"},"bx-circle-quarter":{"parent":"circle-quarter"},"bx-circle-three-quarter":{"parent":"circle-three-quarter"},"bx-clinic":{"parent":"clinic"},"bx-clipboard":{"parent":"clipboard"},"bx-closet":{"parent":"closet"},"bx-cloud":{"parent":"cloud"},"bx-cloud-download":{"parent":"cloud-download"},"bx-cloud-drizzle":{"parent":"cloud-drizzle"},"bx-cloud-light-rain":{"parent":"cloud-light-rain"},"bx-cloud-lightning":{"parent":"cloud-lightning"},"bx-cloud-rain":{"parent":"cloud-rain"},"bx-cloud-snow":{"parent":"cloud-snow"},"bx-cloud-upload":{"parent":"cloud-upload"},"bx-code":{"parent":"code"},"bx-code-alt":{"parent":"code-alt"},"bx-code-block":{"parent":"code-block"},"bx-code-curly":{"parent":"code-curly"},"bx-coffee":{"parent":"coffee"},"bx-coffee-togo":{"parent":"coffee-togo"},"bx-cog":{"parent":"cog"},"bx-coin":{"parent":"coin"},"bx-coin-stack":{"parent":"coin-stack"},"bx-collapse":{"parent":"collapse"},"bx-collapse-alt":{"parent":"collapse-alt"},"bx-collapse-horizontal":{"parent":"collapse-horizontal"},"bx-collapse-vertical":{"parent":"collapse-vertical"},"bx-collection":{"parent":"collection"},"bx-color":{"parent":"color"},"bx-color-fill":{"parent":"color-fill"},"bx-columns":{"parent":"columns"},"bx-command":{"parent":"command"},"bx-comment":{"parent":"comment"},"bx-comment-add":{"parent":"comment-add"},"bx-comment-check":{"parent":"comment-check"},"bx-comment-detail":{"parent":"comment-detail"},"bx-comment-dots":{"parent":"comment-dots"},"bx-comment-edit":{"parent":"comment-edit"},"bx-comment-error":{"parent":"comment-error"},"bx-comment-minus":{"parent":"comment-minus"},"bx-comment-x":{"parent":"comment-x"},"bx-compass":{"parent":"compass"},"bx-confused":{"parent":"confused"},"bx-conversation":{"parent":"conversation"},"bx-cookie":{"parent":"cookie"},"bx-cool":{"parent":"cool"},"bx-copy":{"parent":"copy"},"bx-copy-alt":{"parent":"copy-alt"},"bx-copyright":{"parent":"copyright"},"bx-credit-card":{"parent":"credit-card"},"bx-credit-card-alt":{"parent":"credit-card-alt"},"bx-credit-card-front":{"parent":"credit-card-front"},"bx-cricket-ball":{"parent":"cricket-ball"},"bx-crop":{"parent":"crop"},"bx-cross":{"parent":"cross"},"bx-crosshair":{"parent":"crosshair"},"bx-crown":{"parent":"crown"},"bx-cube":{"parent":"cube"},"bx-cube-alt":{"parent":"cube-alt"},"bx-cuboid":{"parent":"cuboid"},"bx-current-location":{"parent":"current-location"},"bx-customize":{"parent":"customize"},"bx-cut":{"parent":"cut"},"bx-cycling":{"parent":"cycling"},"bx-cylinder":{"parent":"cylinder"},"bx-data":{"parent":"data"},"bx-desktop":{"parent":"desktop"},"bx-detail":{"parent":"detail"},"bx-devices":{"parent":"devices"},"bx-dialpad":{"parent":"dialpad"},"bx-dialpad-alt":{"parent":"dialpad-alt"},"bx-diamond":{"parent":"diamond"},"bx-dice-1":{"parent":"dice-1"},"bx-dice-2":{"parent":"dice-2"},"bx-dice-3":{"parent":"dice-3"},"bx-dice-4":{"parent":"dice-4"},"bx-dice-5":{"parent":"dice-5"},"bx-dice-6":{"parent":"dice-6"},"bx-directions":{"parent":"directions"},"bx-disc":{"parent":"disc"},"bx-dish":{"parent":"dish"},"bx-dislike":{"parent":"dislike"},"bx-dizzy":{"parent":"dizzy"},"bx-dna":{"parent":"dna"},"bx-dock-bottom":{"parent":"dock-bottom"},"bx-dock-left":{"parent":"dock-left"},"bx-dock-right":{"parent":"dock-right"},"bx-dock-top":{"parent":"dock-top"},"bx-dollar":{"parent":"dollar"},"bx-dollar-circle":{"parent":"dollar-circle"},"bx-donate-blood":{"parent":"donate-blood"},"bx-donate-heart":{"parent":"donate-heart"},"bx-door-open":{"parent":"door-open"},"bx-dots-horizontal":{"parent":"dots-horizontal"},"bx-dots-horizontal-rounded":{"parent":"dots-horizontal-rounded"},"bx-dots-vertical":{"parent":"dots-vertical"},"bx-dots-vertical-rounded":{"parent":"dots-vertical-rounded"},"bx-doughnut-chart":{"parent":"doughnut-chart"},"bx-down-arrow":{"parent":"down-arrow"},"bx-down-arrow-alt":{"parent":"down-arrow-alt"},"bx-down-arrow-circle":{"parent":"down-arrow-circle"},"bx-download":{"parent":"download"},"bx-downvote":{"parent":"downvote"},"bx-drink":{"parent":"drink"},"bx-droplet":{"parent":"droplet"},"bx-dumbbell":{"parent":"dumbbell"},"bx-duplicate":{"parent":"duplicate"},"bx-edit":{"parent":"edit"},"bx-edit-alt":{"parent":"edit-alt"},"bx-envelope":{"parent":"envelope"},"bx-envelope-open":{"parent":"envelope-open"},"bx-equalizer":{"parent":"equalizer"},"bx-eraser":{"parent":"eraser"},"bx-error":{"parent":"error"},"bx-error-alt":{"parent":"error-alt"},"bx-error-circle":{"parent":"error-circle"},"bx-euro":{"parent":"euro"},"bx-exclude":{"parent":"exclude"},"bx-exit":{"parent":"exit"},"bx-exit-fullscreen":{"parent":"exit-fullscreen"},"bx-expand":{"parent":"expand"},"bx-expand-alt":{"parent":"expand-alt"},"bx-expand-horizontal":{"parent":"expand-horizontal"},"bx-expand-vertical":{"parent":"expand-vertical"},"bx-export":{"parent":"export"},"bx-extension":{"parent":"extension"},"bx-face":{"parent":"face"},"bx-fast-forward":{"parent":"fast-forward"},"bx-fast-forward-circle":{"parent":"fast-forward-circle"},"bx-female":{"parent":"female"},"bx-female-sign":{"parent":"female-sign"},"bx-file":{"parent":"file"},"bx-file-blank":{"parent":"file-blank"},"bx-file-find":{"parent":"file-find"},"bx-film":{"parent":"film"},"bx-filter":{"parent":"filter"},"bx-filter-alt":{"parent":"filter-alt"},"bx-fingerprint":{"parent":"fingerprint"},"bx-first-aid":{"parent":"first-aid"},"bx-first-page":{"parent":"first-page"},"bx-flag":{"parent":"flag"},"bx-folder":{"parent":"folder"},"bx-folder-minus":{"parent":"folder-minus"},"bx-folder-open":{"parent":"folder-open"},"bx-folder-plus":{"parent":"folder-plus"},"bx-font":{"parent":"font"},"bx-font-color":{"parent":"font-color"},"bx-font-family":{"parent":"font-family"},"bx-font-size":{"parent":"font-size"},"bx-food-menu":{"parent":"food-menu"},"bx-food-tag":{"parent":"food-tag"},"bx-football":{"parent":"football"},"bx-fork":{"parent":"fork"},"bx-fridge":{"parent":"fridge"},"bx-fullscreen":{"parent":"fullscreen"},"bx-game":{"parent":"game"},"bx-gas-pump":{"parent":"gas-pump"},"bx-ghost":{"parent":"ghost"},"bx-gift":{"parent":"gift"},"bx-git-branch":{"parent":"git-branch"},"bx-git-commit":{"parent":"git-commit"},"bx-git-compare":{"parent":"git-compare"},"bx-git-merge":{"parent":"git-merge"},"bx-git-pull-request":{"parent":"git-pull-request"},"bx-git-repo-forked":{"parent":"git-repo-forked"},"bx-glasses":{"parent":"glasses"},"bx-glasses-alt":{"parent":"glasses-alt"},"bx-globe":{"parent":"globe"},"bx-globe-alt":{"parent":"globe-alt"},"bx-grid":{"parent":"grid"},"bx-grid-alt":{"parent":"grid-alt"},"bx-grid-horizontal":{"parent":"grid-horizontal"},"bx-grid-small":{"parent":"grid-small"},"bx-grid-vertical":{"parent":"grid-vertical"},"bx-group":{"parent":"group"},"bx-handicap":{"parent":"handicap"},"bx-happy":{"parent":"happy"},"bx-happy-alt":{"parent":"happy-alt"},"bx-happy-beaming":{"parent":"happy-beaming"},"bx-happy-heart-eyes":{"parent":"happy-heart-eyes"},"bx-hard-hat":{"parent":"hard-hat"},"bx-hash":{"parent":"hash"},"bx-hdd":{"parent":"hdd"},"bx-heading":{"parent":"heading"},"bx-headphone":{"parent":"headphone"},"bx-health":{"parent":"health"},"bx-heart":{"parent":"heart"},"bx-heart-circle":{"parent":"heart-circle"},"bx-heart-square":{"parent":"heart-square"},"bx-help-circle":{"parent":"help-circle"},"bx-hide":{"parent":"hide"},"bx-highlight":{"parent":"highlight"},"bx-history":{"parent":"history"},"bx-hive":{"parent":"hive"},"bx-home":{"parent":"home"},"bx-home-alt":{"parent":"home-alt"},"bx-home-alt-2":{"parent":"home-alt-2"},"bx-home-circle":{"parent":"home-circle"},"bx-home-heart":{"parent":"home-heart"},"bx-home-smile":{"parent":"home-smile"},"bx-horizontal-center":{"parent":"horizontal-center"},"bx-horizontal-left":{"parent":"horizontal-left"},"bx-horizontal-right":{"parent":"horizontal-right"},"bx-hotel":{"parent":"hotel"},"bx-hourglass":{"parent":"hourglass"},"bx-id-card":{"parent":"id-card"},"bx-image":{"parent":"image"},"bx-image-add":{"parent":"image-add"},"bx-image-alt":{"parent":"image-alt"},"bx-images":{"parent":"images"},"bx-import":{"parent":"import"},"bx-infinite":{"parent":"infinite"},"bx-info-circle":{"parent":"info-circle"},"bx-info-square":{"parent":"info-square"},"bx-injection":{"parent":"injection"},"bx-intersect":{"parent":"intersect"},"bx-italic":{"parent":"italic"},"bx-joystick":{"parent":"joystick"},"bx-joystick-alt":{"parent":"joystick-alt"},"bx-joystick-button":{"parent":"joystick-button"},"bx-key":{"parent":"key"},"bx-knife":{"parent":"knife"},"bx-label":{"parent":"label"},"bx-landscape":{"parent":"landscape"},"bx-laptop":{"parent":"laptop"},"bx-last-page":{"parent":"last-page"},"bx-laugh":{"parent":"laugh"},"bx-layer":{"parent":"layer"},"bx-layer-minus":{"parent":"layer-minus"},"bx-layer-plus":{"parent":"layer-plus"},"bx-layout":{"parent":"layout"},"bx-leaf":{"parent":"leaf"},"bx-left-arrow":{"parent":"left-arrow"},"bx-left-arrow-alt":{"parent":"left-arrow-alt"},"bx-left-arrow-circle":{"parent":"left-arrow-circle"},"bx-left-down-arrow-circle":{"parent":"left-down-arrow-circle"},"bx-left-indent":{"parent":"left-indent"},"bx-left-top-arrow-circle":{"parent":"left-top-arrow-circle"},"bx-lemon":{"parent":"lemon"},"bx-library":{"parent":"library"},"bx-like":{"parent":"like"},"bx-line-chart":{"parent":"line-chart"},"bx-line-chart-down":{"parent":"line-chart-down"},"bx-link":{"parent":"link"},"bx-link-alt":{"parent":"link-alt"},"bx-link-external":{"parent":"link-external"},"bx-lira":{"parent":"lira"},"bx-list-check":{"parent":"list-check"},"bx-list-minus":{"parent":"list-minus"},"bx-list-ol":{"parent":"list-ol"},"bx-list-plus":{"parent":"list-plus"},"bx-list-ul":{"parent":"list-ul"},"bx-loader":{"parent":"loader"},"bx-loader-alt":{"parent":"loader-alt"},"bx-loader-circle":{"parent":"loader-circle"},"bx-location-plus":{"parent":"location-plus"},"bx-lock":{"parent":"lock"},"bx-lock-alt":{"parent":"lock-alt"},"bx-lock-open":{"parent":"lock-open"},"bx-lock-open-alt":{"parent":"lock-open-alt"},"bx-log-in":{"parent":"log-in"},"bx-log-in-circle":{"parent":"log-in-circle"},"bx-log-out":{"parent":"log-out"},"bx-log-out-circle":{"parent":"log-out-circle"},"bx-low-vision":{"parent":"low-vision"},"bx-magnet":{"parent":"magnet"},"bx-mail-send":{"parent":"mail-send"},"bx-male":{"parent":"male"},"bx-male-female":{"parent":"male-female"},"bx-male-sign":{"parent":"male-sign"},"bx-map":{"parent":"map"},"bx-map-alt":{"parent":"map-alt"},"bx-map-pin":{"parent":"map-pin"},"bx-mask":{"parent":"mask"},"bx-math":{"parent":"math"},"bx-medal":{"parent":"medal"},"bx-meh":{"parent":"meh"},"bx-meh-alt":{"parent":"meh-alt"},"bx-meh-blank":{"parent":"meh-blank"},"bx-memory-card":{"parent":"memory-card"},"bx-menu":{"parent":"menu"},"bx-menu-alt-left":{"parent":"menu-alt-left"},"bx-menu-alt-right":{"parent":"menu-alt-right"},"bx-merge":{"parent":"merge"},"bx-message":{"parent":"message"},"bx-message-add":{"parent":"message-add"},"bx-message-alt":{"parent":"message-alt"},"bx-message-alt-add":{"parent":"message-alt-add"},"bx-message-alt-check":{"parent":"message-alt-check"},"bx-message-alt-detail":{"parent":"message-alt-detail"},"bx-message-alt-dots":{"parent":"message-alt-dots"},"bx-message-alt-edit":{"parent":"message-alt-edit"},"bx-message-alt-error":{"parent":"message-alt-error"},"bx-message-alt-minus":{"parent":"message-alt-minus"},"bx-message-alt-x":{"parent":"message-alt-x"},"bx-message-check":{"parent":"message-check"},"bx-message-detail":{"parent":"message-detail"},"bx-message-dots":{"parent":"message-dots"},"bx-message-edit":{"parent":"message-edit"},"bx-message-error":{"parent":"message-error"},"bx-message-minus":{"parent":"message-minus"},"bx-message-rounded":{"parent":"message-rounded"},"bx-message-rounded-add":{"parent":"message-rounded-add"},"bx-message-rounded-check":{"parent":"message-rounded-check"},"bx-message-rounded-detail":{"parent":"message-rounded-detail"},"bx-message-rounded-dots":{"parent":"message-rounded-dots"},"bx-message-rounded-edit":{"parent":"message-rounded-edit"},"bx-message-rounded-error":{"parent":"message-rounded-error"},"bx-message-rounded-minus":{"parent":"message-rounded-minus"},"bx-message-rounded-x":{"parent":"message-rounded-x"},"bx-message-square":{"parent":"message-square"},"bx-message-square-add":{"parent":"message-square-add"},"bx-message-square-check":{"parent":"message-square-check"},"bx-message-square-detail":{"parent":"message-square-detail"},"bx-message-square-dots":{"parent":"message-square-dots"},"bx-message-square-edit":{"parent":"message-square-edit"},"bx-message-square-error":{"parent":"message-square-error"},"bx-message-square-minus":{"parent":"message-square-minus"},"bx-message-square-x":{"parent":"message-square-x"},"bx-message-x":{"parent":"message-x"},"bx-meteor":{"parent":"meteor"},"bx-microchip":{"parent":"microchip"},"bx-microphone":{"parent":"microphone"},"bx-microphone-off":{"parent":"microphone-off"},"bx-minus":{"parent":"minus"},"bx-minus-back":{"parent":"minus-back"},"bx-minus-circle":{"parent":"minus-circle"},"bx-minus-front":{"parent":"minus-front"},"bx-mobile":{"parent":"mobile"},"bx-mobile-alt":{"parent":"mobile-alt"},"bx-mobile-landscape":{"parent":"mobile-landscape"},"bx-mobile-vibration":{"parent":"mobile-vibration"},"bx-money":{"parent":"money"},"bx-money-withdraw":{"parent":"money-withdraw"},"bx-moon":{"parent":"moon"},"bx-mouse":{"parent":"mouse"},"bx-mouse-alt":{"parent":"mouse-alt"},"bx-move":{"parent":"move"},"bx-move-horizontal":{"parent":"move-horizontal"},"bx-move-vertical":{"parent":"move-vertical"},"bx-movie":{"parent":"movie"},"bx-movie-play":{"parent":"movie-play"},"bx-music":{"parent":"music"},"bx-navigation":{"parent":"navigation"},"bx-network-chart":{"parent":"network-chart"},"bx-news":{"parent":"news"},"bx-no-entry":{"parent":"no-entry"},"bx-no-signal":{"parent":"no-signal"},"bx-note":{"parent":"note"},"bx-notepad":{"parent":"notepad"},"bx-notification":{"parent":"notification"},"bx-notification-off":{"parent":"notification-off"},"bx-objects-horizontal-center":{"parent":"objects-horizontal-center"},"bx-objects-horizontal-left":{"parent":"objects-horizontal-left"},"bx-objects-horizontal-right":{"parent":"objects-horizontal-right"},"bx-objects-vertical-bottom":{"parent":"objects-vertical-bottom"},"bx-objects-vertical-center":{"parent":"objects-vertical-center"},"bx-objects-vertical-top":{"parent":"objects-vertical-top"},"bx-outline":{"parent":"outline"},"bx-package":{"parent":"package"},"bx-paint":{"parent":"paint"},"bx-paint-roll":{"parent":"paint-roll"},"bx-palette":{"parent":"palette"},"bx-paper-plane":{"parent":"paper-plane"},"bx-paperclip":{"parent":"paperclip"},"bx-paragraph":{"parent":"paragraph"},"bx-party":{"parent":"party"},"bx-paste":{"parent":"paste"},"bx-pause":{"parent":"pause"},"bx-pause-circle":{"parent":"pause-circle"},"bx-pen":{"parent":"pen"},"bx-pencil":{"parent":"pencil"},"bx-phone":{"parent":"phone"},"bx-phone-call":{"parent":"phone-call"},"bx-phone-incoming":{"parent":"phone-incoming"},"bx-phone-off":{"parent":"phone-off"},"bx-phone-outgoing":{"parent":"phone-outgoing"},"bx-photo-album":{"parent":"photo-album"},"bx-pie-chart":{"parent":"pie-chart"},"bx-pie-chart-alt":{"parent":"pie-chart-alt"},"bx-pie-chart-alt-2":{"parent":"pie-chart-alt-2"},"bx-pin":{"parent":"pin"},"bx-planet":{"parent":"planet"},"bx-play":{"parent":"play"},"bx-play-circle":{"parent":"play-circle"},"bx-plug":{"parent":"plug"},"bx-plus":{"parent":"plus"},"bx-plus-circle":{"parent":"plus-circle"},"bx-plus-medical":{"parent":"plus-medical"},"bx-podcast":{"parent":"podcast"},"bx-pointer":{"parent":"pointer"},"bx-poll":{"parent":"poll"},"bx-polygon":{"parent":"polygon"},"bx-popsicle":{"parent":"popsicle"},"bx-pound":{"parent":"pound"},"bx-power-off":{"parent":"power-off"},"bx-printer":{"parent":"printer"},"bx-pulse":{"parent":"pulse"},"bx-purchase-tag":{"parent":"purchase-tag"},"bx-purchase-tag-alt":{"parent":"purchase-tag-alt"},"bx-pyramid":{"parent":"pyramid"},"bx-qr":{"parent":"qr"},"bx-qr-scan":{"parent":"qr-scan"},"bx-question-mark":{"parent":"question-mark"},"bx-radar":{"parent":"radar"},"bx-radio":{"parent":"radio"},"bx-radio-circle":{"parent":"radio-circle"},"bx-radio-circle-marked":{"parent":"radio-circle-marked"},"bx-receipt":{"parent":"receipt"},"bx-rectangle":{"parent":"rectangle"},"bx-recycle":{"parent":"recycle"},"bx-redo":{"parent":"redo"},"bx-reflect-horizontal":{"parent":"reflect-horizontal"},"bx-reflect-vertical":{"parent":"reflect-vertical"},"bx-refresh":{"parent":"refresh"},"bx-registered":{"parent":"registered"},"bx-rename":{"parent":"rename"},"bx-repeat":{"parent":"repeat"},"bx-reply":{"parent":"reply"},"bx-reply-all":{"parent":"reply-all"},"bx-repost":{"parent":"repost"},"bx-reset":{"parent":"reset"},"bx-restaurant":{"parent":"restaurant"},"bx-revision":{"parent":"revision"},"bx-rewind":{"parent":"rewind"},"bx-rewind-circle":{"parent":"rewind-circle"},"bx-rfid":{"parent":"rfid"},"bx-right-arrow":{"parent":"right-arrow"},"bx-right-arrow-alt":{"parent":"right-arrow-alt"},"bx-right-arrow-circle":{"parent":"right-arrow-circle"},"bx-right-down-arrow-circle":{"parent":"right-down-arrow-circle"},"bx-right-indent":{"parent":"right-indent"},"bx-right-top-arrow-circle":{"parent":"right-top-arrow-circle"},"bx-rocket":{"parent":"rocket"},"bx-rotate-left":{"parent":"rotate-left"},"bx-rotate-right":{"parent":"rotate-right"},"bx-rss":{"parent":"rss"},"bx-ruble":{"parent":"ruble"},"bx-ruler":{"parent":"ruler"},"bx-run":{"parent":"run"},"bx-rupee":{"parent":"rupee"},"bx-sad":{"parent":"sad"},"bx-save":{"parent":"save"},"bx-scan":{"parent":"scan"},"bx-scatter-chart":{"parent":"scatter-chart"},"bx-screenshot":{"parent":"screenshot"},"bx-search":{"parent":"search"},"bx-search-alt":{"parent":"search-alt"},"bx-search-alt-2":{"parent":"search-alt-2"},"bx-select-multiple":{"parent":"select-multiple"},"bx-selection":{"parent":"selection"},"bx-send":{"parent":"send"},"bx-server":{"parent":"server"},"bx-shape-circle":{"parent":"shape-circle"},"bx-shape-polygon":{"parent":"shape-polygon"},"bx-shape-square":{"parent":"shape-square"},"bx-shape-triangle":{"parent":"shape-triangle"},"bx-share":{"parent":"share"},"bx-share-alt":{"parent":"share-alt"},"bx-shekel":{"parent":"shekel"},"bx-shield":{"parent":"shield"},"bx-shield-alt":{"parent":"shield-alt"},"bx-shield-alt-2":{"parent":"shield-alt-2"},"bx-shield-minus":{"parent":"shield-minus"},"bx-shield-plus":{"parent":"shield-plus"},"bx-shield-quarter":{"parent":"shield-quarter"},"bx-shield-x":{"parent":"shield-x"},"bx-shocked":{"parent":"shocked"},"bx-shopping-bag":{"parent":"shopping-bag"},"bx-show":{"parent":"show"},"bx-show-alt":{"parent":"show-alt"},"bx-shower":{"parent":"shower"},"bx-shuffle":{"parent":"shuffle"},"bx-sidebar":{"parent":"sidebar"},"bx-signal-1":{"parent":"signal-1"},"bx-signal-2":{"parent":"signal-2"},"bx-signal-3":{"parent":"signal-3"},"bx-signal-4":{"parent":"signal-4"},"bx-signal-5":{"parent":"signal-5"},"bx-sitemap":{"parent":"sitemap"},"bx-skip-next":{"parent":"skip-next"},"bx-skip-next-circle":{"parent":"skip-next-circle"},"bx-skip-previous":{"parent":"skip-previous"},"bx-skip-previous-circle":{"parent":"skip-previous-circle"},"bx-sleepy":{"parent":"sleepy"},"bx-slider":{"parent":"slider"},"bx-slider-alt":{"parent":"slider-alt"},"bx-slideshow":{"parent":"slideshow"},"bx-smile":{"parent":"smile"},"bx-sort":{"parent":"sort"},"bx-sort-a-z":{"parent":"sort-a-z"},"bx-sort-alt-2":{"parent":"sort-alt-2"},"bx-sort-down":{"parent":"sort-down"},"bx-sort-up":{"parent":"sort-up"},"bx-sort-z-a":{"parent":"sort-z-a"},"bx-spa":{"parent":"spa"},"bx-space-bar":{"parent":"space-bar"},"bx-speaker":{"parent":"speaker"},"bx-spray-can":{"parent":"spray-can"},"bx-spreadsheet":{"parent":"spreadsheet"},"bx-square":{"parent":"square"},"bx-square-rounded":{"parent":"square-rounded"},"bx-star":{"parent":"star"},"bx-station":{"parent":"station"},"bx-stats":{"parent":"stats"},"bx-sticker":{"parent":"sticker"},"bx-stop":{"parent":"stop"},"bx-stop-circle":{"parent":"stop-circle"},"bx-stopwatch":{"parent":"stopwatch"},"bx-store":{"parent":"store"},"bx-store-alt":{"parent":"store-alt"},"bx-street-view":{"parent":"street-view"},"bx-strikethrough":{"parent":"strikethrough"},"bx-subdirectory-left":{"parent":"subdirectory-left"},"bx-subdirectory-right":{"parent":"subdirectory-right"},"bx-sun":{"parent":"sun"},"bx-support":{"parent":"support"},"bx-sushi":{"parent":"sushi"},"bx-swim":{"parent":"swim"},"bx-sync":{"parent":"sync"},"bx-tab":{"parent":"tab"},"bx-table":{"parent":"table"},"bx-tachometer":{"parent":"tachometer"},"bx-tag":{"parent":"tag"},"bx-tag-alt":{"parent":"tag-alt"},"bx-target-lock":{"parent":"target-lock"},"bx-task":{"parent":"task"},"bx-task-x":{"parent":"task-x"},"bx-taxi":{"parent":"taxi"},"bx-tennis-ball":{"parent":"tennis-ball"},"bx-terminal":{"parent":"terminal"},"bx-test-tube":{"parent":"test-tube"},"bx-text":{"parent":"text"},"bx-time":{"parent":"time"},"bx-time-five":{"parent":"time-five"},"bx-timer":{"parent":"timer"},"bx-tired":{"parent":"tired"},"bx-toggle-left":{"parent":"toggle-left"},"bx-toggle-right":{"parent":"toggle-right"},"bx-tone":{"parent":"tone"},"bx-traffic-cone":{"parent":"traffic-cone"},"bx-train":{"parent":"train"},"bx-transfer":{"parent":"transfer"},"bx-transfer-alt":{"parent":"transfer-alt"},"bx-trash":{"parent":"trash"},"bx-trash-alt":{"parent":"trash-alt"},"bx-trending-down":{"parent":"trending-down"},"bx-trending-up":{"parent":"trending-up"},"bx-trim":{"parent":"trim"},"bx-trip":{"parent":"trip"},"bx-trophy":{"parent":"trophy"},"bx-tv":{"parent":"tv"},"bx-underline":{"parent":"underline"},"bx-undo":{"parent":"undo"},"bx-unite":{"parent":"unite"},"bx-universal-access":{"parent":"universal-access"},"bx-unlink":{"parent":"unlink"},"bx-up-arrow":{"parent":"up-arrow"},"bx-up-arrow-alt":{"parent":"up-arrow-alt"},"bx-up-arrow-circle":{"parent":"up-arrow-circle"},"bx-upload":{"parent":"upload"},"bx-upside-down":{"parent":"upside-down"},"bx-upvote":{"parent":"upvote"},"bx-usb":{"parent":"usb"},"bx-user":{"parent":"user"},"bx-user-check":{"parent":"user-check"},"bx-user-circle":{"parent":"user-circle"},"bx-user-minus":{"parent":"user-minus"},"bx-user-pin":{"parent":"user-pin"},"bx-user-plus":{"parent":"user-plus"},"bx-user-voice":{"parent":"user-voice"},"bx-user-x":{"parent":"user-x"},"bx-vector":{"parent":"vector"},"bx-vertical-bottom":{"parent":"vertical-bottom"},"bx-vertical-center":{"parent":"vertical-center"},"bx-vertical-top":{"parent":"vertical-top"},"bx-vial":{"parent":"vial"},"bx-video":{"parent":"video"},"bx-video-off":{"parent":"video-off"},"bx-video-plus":{"parent":"video-plus"},"bx-video-recording":{"parent":"video-recording"},"bx-voicemail":{"parent":"voicemail"},"bx-volume":{"parent":"volume"},"bx-volume-full":{"parent":"volume-full"},"bx-volume-low":{"parent":"volume-low"},"bx-volume-mute":{"parent":"volume-mute"},"bx-walk":{"parent":"walk"},"bx-wallet":{"parent":"wallet"},"bx-wallet-alt":{"parent":"wallet-alt"},"bx-water":{"parent":"water"},"bx-webcam":{"parent":"webcam"},"bx-wifi":{"parent":"wifi"},"bx-wifi-0":{"parent":"wifi-0"},"bx-wifi-1":{"parent":"wifi-1"},"bx-wifi-2":{"parent":"wifi-2"},"bx-wifi-off":{"parent":"wifi-off"},"bx-wind":{"parent":"wind"},"bx-window":{"parent":"window"},"bx-window-alt":{"parent":"window-alt"},"bx-window-close":{"parent":"window-close"},"bx-window-open":{"parent":"window-open"},"bx-windows":{"parent":"windows"},"bx-wine":{"parent":"wine"},"bx-wink-smile":{"parent":"wink-smile"},"bx-wink-tongue":{"parent":"wink-tongue"},"bx-won":{"parent":"won"},"bx-world":{"parent":"world"},"bx-wrench":{"parent":"wrench"},"bx-x":{"parent":"x"},"bx-x-circle":{"parent":"x-circle"},"bx-yen":{"parent":"yen"},"bx-zoom-in":{"parent":"zoom-in"},"bx-zoom-out":{"parent":"zoom-out"}},"lastModified":1702311649,"width":24,"height":24}); +addCollection({"prefix":"bxs","icons":{"add-to-queue":{"body":""},"adjust":{"body":""},"adjust-alt":{"body":""},"alarm":{"body":""},"alarm-add":{"body":""},"alarm-exclamation":{"body":""},"alarm-off":{"body":""},"alarm-snooze":{"body":""},"album":{"body":""},"ambulance":{"body":""},"analyse":{"body":""},"angry":{"body":""},"arch":{"body":""},"archive":{"body":""},"archive-in":{"body":""},"archive-out":{"body":""},"area":{"body":""},"arrow-from-bottom":{"body":""},"arrow-from-left":{"body":""},"arrow-from-right":{"body":""},"arrow-from-top":{"body":""},"arrow-to-bottom":{"body":""},"arrow-to-left":{"body":""},"arrow-to-right":{"body":""},"arrow-to-top":{"body":""},"award":{"body":""},"baby-carriage":{"body":""},"backpack":{"body":""},"badge":{"body":""},"badge-check":{"body":""},"badge-dollar":{"body":""},"baguette":{"body":""},"ball":{"body":""},"balloon":{"body":""},"band-aid":{"body":""},"bank":{"body":""},"bar-chart-alt-2":{"body":""},"bar-chart-square":{"body":""},"barcode":{"body":""},"baseball":{"body":""},"basket":{"body":""},"basketball":{"body":""},"bath":{"body":""},"battery":{"body":""},"battery-charging":{"body":""},"battery-full":{"body":""},"battery-low":{"body":""},"bed":{"body":""},"been-here":{"body":""},"beer":{"body":""},"bell":{"body":""},"bell-minus":{"body":""},"bell-off":{"body":""},"bell-plus":{"body":""},"bell-ring":{"body":""},"bible":{"body":""},"binoculars":{"body":""},"blanket":{"body":""},"bolt":{"body":""},"bolt-circle":{"body":""},"bomb":{"body":""},"bone":{"body":""},"bong":{"body":""},"book":{"body":""},"book-add":{"body":""},"book-alt":{"body":""},"book-bookmark":{"body":""},"book-content":{"body":""},"book-heart":{"body":""},"book-open":{"body":""},"book-reader":{"body":""},"bookmark":{"body":""},"bookmark-alt":{"body":""},"bookmark-alt-minus":{"body":""},"bookmark-alt-plus":{"body":""},"bookmark-heart":{"body":""},"bookmark-minus":{"body":""},"bookmark-plus":{"body":""},"bookmark-star":{"body":""},"bookmarks":{"body":""},"bot":{"body":""},"bowl-hot":{"body":""},"bowl-rice":{"body":""},"bowling-ball":{"body":""},"box":{"body":""},"brain":{"body":""},"briefcase":{"body":""},"briefcase-alt":{"body":""},"briefcase-alt-2":{"body":""},"brightness":{"body":""},"brightness-half":{"body":""},"brush":{"body":""},"brush-alt":{"body":""},"bug":{"body":""},"bug-alt":{"body":""},"building":{"body":""},"building-house":{"body":""},"buildings":{"body":""},"bulb":{"body":""},"bullseye":{"body":""},"buoy":{"body":""},"bus":{"body":""},"bus-school":{"body":""},"business":{"body":""},"cabinet":{"body":""},"cable-car":{"body":""},"cake":{"body":""},"calculator":{"body":""},"calendar":{"body":""},"calendar-alt":{"body":""},"calendar-check":{"body":""},"calendar-edit":{"body":""},"calendar-event":{"body":""},"calendar-exclamation":{"body":""},"calendar-heart":{"body":""},"calendar-minus":{"body":""},"calendar-plus":{"body":""},"calendar-star":{"body":""},"calendar-week":{"body":""},"calendar-x":{"body":""},"camera":{"body":""},"camera-home":{"body":""},"camera-movie":{"body":""},"camera-off":{"body":""},"camera-plus":{"body":""},"capsule":{"body":""},"captions":{"body":""},"car":{"body":""},"car-battery":{"body":""},"car-crash":{"body":""},"car-garage":{"body":""},"car-mechanic":{"body":""},"car-wash":{"body":""},"card":{"body":""},"caret-down-circle":{"body":""},"caret-down-square":{"body":""},"caret-left-circle":{"body":""},"caret-left-square":{"body":""},"caret-right-circle":{"body":""},"caret-right-square":{"body":""},"caret-up-circle":{"body":""},"caret-up-square":{"body":""},"carousel":{"body":""},"cart":{"body":""},"cart-add":{"body":""},"cart-alt":{"body":""},"cart-download":{"body":""},"castle":{"body":""},"cat":{"body":""},"category":{"body":""},"category-alt":{"body":""},"cctv":{"body":""},"certification":{"body":""},"chalkboard":{"body":""},"chart":{"body":""},"chat":{"body":""},"check-circle":{"body":""},"check-shield":{"body":""},"check-square":{"body":""},"checkbox":{"body":""},"checkbox-checked":{"body":""},"checkbox-minus":{"body":""},"cheese":{"body":""},"chess":{"body":""},"chevron-down":{"body":""},"chevron-down-circle":{"body":""},"chevron-down-square":{"body":""},"chevron-left":{"body":""},"chevron-left-circle":{"body":""},"chevron-left-square":{"body":""},"chevron-right":{"body":""},"chevron-right-circle":{"body":""},"chevron-right-square":{"body":""},"chevron-up":{"body":""},"chevron-up-circle":{"body":""},"chevron-up-square":{"body":""},"chevrons-down":{"body":""},"chevrons-left":{"body":""},"chevrons-right":{"body":""},"chevrons-up":{"body":""},"chip":{"body":""},"church":{"body":""},"circle":{"body":""},"circle-half":{"body":""},"circle-quarter":{"body":""},"circle-three-quarter":{"body":""},"city":{"body":""},"clinic":{"body":""},"cloud":{"body":""},"cloud-download":{"body":""},"cloud-lightning":{"body":""},"cloud-rain":{"body":""},"cloud-upload":{"body":""},"coffee":{"body":""},"coffee-alt":{"body":""},"coffee-bean":{"body":""},"coffee-togo":{"body":""},"cog":{"body":""},"coin":{"body":""},"coin-stack":{"body":""},"collection":{"body":""},"color":{"body":""},"color-fill":{"body":""},"comment":{"body":""},"comment-add":{"body":""},"comment-check":{"body":""},"comment-detail":{"body":""},"comment-dots":{"body":""},"comment-edit":{"body":""},"comment-error":{"body":""},"comment-minus":{"body":""},"comment-x":{"body":""},"compass":{"body":""},"component":{"body":""},"confused":{"body":""},"contact":{"body":""},"conversation":{"body":""},"cookie":{"body":""},"cool":{"body":""},"copy":{"body":""},"copy-alt":{"body":""},"copyright":{"body":""},"coupon":{"body":""},"credit-card":{"body":""},"credit-card-alt":{"body":""},"credit-card-front":{"body":""},"cricket-ball":{"body":""},"crop":{"body":""},"crown":{"body":""},"cube":{"body":""},"cube-alt":{"body":""},"cuboid":{"body":""},"customize":{"body":""},"cylinder":{"body":""},"dashboard":{"body":""},"data":{"body":""},"detail":{"body":""},"devices":{"body":""},"diamond":{"body":""},"dice-1":{"body":""},"dice-2":{"body":""},"dice-3":{"body":""},"dice-4":{"body":""},"dice-5":{"body":""},"dice-6":{"body":""},"direction-left":{"body":""},"direction-right":{"body":""},"directions":{"body":""},"disc":{"body":""},"discount":{"body":""},"dish":{"body":""},"dislike":{"body":""},"dizzy":{"body":""},"dock-bottom":{"body":""},"dock-left":{"body":""},"dock-right":{"body":""},"dock-top":{"body":""},"dog":{"body":""},"dollar-circle":{"body":""},"donate-blood":{"body":""},"donate-heart":{"body":""},"door-open":{"body":""},"doughnut-chart":{"body":""},"down-arrow":{"body":""},"down-arrow-alt":{"body":""},"down-arrow-circle":{"body":""},"down-arrow-square":{"body":""},"download":{"body":""},"downvote":{"body":""},"drink":{"body":""},"droplet":{"body":""},"droplet-half":{"body":""},"dryer":{"body":""},"duplicate":{"body":""},"edit":{"body":""},"edit-alt":{"body":""},"edit-location":{"body":""},"eject":{"body":""},"envelope":{"body":""},"envelope-open":{"body":""},"eraser":{"body":""},"error":{"body":""},"error-alt":{"body":""},"error-circle":{"body":""},"ev-station":{"body":""},"exit":{"body":""},"extension":{"body":""},"eyedropper":{"body":""},"face":{"body":""},"face-mask":{"body":""},"factory":{"body":""},"fast-forward-circle":{"body":""},"file":{"body":""},"file-archive":{"body":""},"file-blank":{"body":""},"file-css":{"body":""},"file-doc":{"body":""},"file-export":{"body":""},"file-find":{"body":""},"file-gif":{"body":""},"file-html":{"body":""},"file-image":{"body":""},"file-import":{"body":""},"file-jpg":{"body":""},"file-js":{"body":""},"file-json":{"body":""},"file-md":{"body":""},"file-pdf":{"body":""},"file-plus":{"body":""},"file-png":{"body":""},"file-txt":{"body":""},"film":{"body":""},"filter-alt":{"body":""},"first-aid":{"body":""},"flag":{"body":""},"flag-alt":{"body":""},"flag-checkered":{"body":""},"flame":{"body":""},"flask":{"body":""},"florist":{"body":""},"folder":{"body":""},"folder-minus":{"body":""},"folder-open":{"body":""},"folder-plus":{"body":""},"food-menu":{"body":""},"fridge":{"body":""},"game":{"body":""},"gas-pump":{"body":""},"ghost":{"body":""},"gift":{"body":""},"graduation":{"body":""},"grid":{"body":""},"grid-alt":{"body":""},"group":{"body":""},"guitar-amp":{"body":""},"hand":{"body":""},"hand-down":{"body":""},"hand-left":{"body":""},"hand-right":{"body":""},"hand-up":{"body":""},"happy":{"body":""},"happy-alt":{"body":""},"happy-beaming":{"body":""},"happy-heart-eyes":{"body":""},"hard-hat":{"body":""},"hdd":{"body":""},"heart":{"body":""},"heart-circle":{"body":""},"heart-square":{"body":""},"help-circle":{"body":""},"hide":{"body":""},"home":{"body":""},"home-alt-2":{"body":""},"home-circle":{"body":""},"home-heart":{"body":""},"home-smile":{"body":""},"hot":{"body":""},"hotel":{"body":""},"hourglass":{"body":""},"hourglass-bottom":{"body":""},"hourglass-top":{"body":""},"id-card":{"body":""},"image":{"body":""},"image-add":{"body":""},"image-alt":{"body":""},"inbox":{"body":""},"info-circle":{"body":""},"info-square":{"body":""},"injection":{"body":""},"institution":{"body":""},"invader":{"body":""},"joystick":{"body":""},"joystick-alt":{"body":""},"joystick-button":{"body":""},"key":{"body":""},"keyboard":{"body":""},"label":{"body":""},"landmark":{"body":""},"landscape":{"body":""},"laugh":{"body":""},"layer":{"body":""},"layer-minus":{"body":""},"layer-plus":{"body":""},"layout":{"body":""},"leaf":{"body":""},"left-arrow":{"body":""},"left-arrow-alt":{"body":""},"left-arrow-circle":{"body":""},"left-arrow-square":{"body":""},"left-down-arrow-circle":{"body":""},"left-top-arrow-circle":{"body":""},"lemon":{"body":""},"like":{"body":""},"location-plus":{"body":""},"lock":{"body":""},"lock-alt":{"body":""},"lock-open":{"body":""},"lock-open-alt":{"body":""},"log-in":{"body":""},"log-in-circle":{"body":""},"log-out":{"body":""},"log-out-circle":{"body":""},"low-vision":{"body":""},"magic-wand":{"body":""},"magnet":{"body":""},"map":{"body":""},"map-alt":{"body":""},"map-pin":{"body":""},"mask":{"body":""},"medal":{"body":""},"megaphone":{"body":""},"meh":{"body":""},"meh-alt":{"body":""},"meh-blank":{"body":""},"memory-card":{"body":""},"message":{"body":""},"message-add":{"body":""},"message-alt":{"body":""},"message-alt-add":{"body":""},"message-alt-check":{"body":""},"message-alt-detail":{"body":""},"message-alt-dots":{"body":""},"message-alt-edit":{"body":""},"message-alt-error":{"body":""},"message-alt-minus":{"body":""},"message-alt-x":{"body":""},"message-check":{"body":""},"message-detail":{"body":""},"message-dots":{"body":""},"message-edit":{"body":""},"message-error":{"body":""},"message-minus":{"body":""},"message-rounded":{"body":""},"message-rounded-add":{"body":""},"message-rounded-check":{"body":""},"message-rounded-detail":{"body":""},"message-rounded-dots":{"body":""},"message-rounded-edit":{"body":""},"message-rounded-error":{"body":""},"message-rounded-minus":{"body":""},"message-rounded-x":{"body":""},"message-square":{"body":""},"message-square-add":{"body":""},"message-square-check":{"body":""},"message-square-detail":{"body":""},"message-square-dots":{"body":""},"message-square-edit":{"body":""},"message-square-error":{"body":""},"message-square-minus":{"body":""},"message-square-x":{"body":""},"message-x":{"body":""},"meteor":{"body":""},"microchip":{"body":""},"microphone":{"body":""},"microphone-alt":{"body":""},"microphone-off":{"body":""},"minus-circle":{"body":""},"minus-square":{"body":""},"mobile":{"body":""},"mobile-vibration":{"body":""},"moon":{"body":""},"mouse":{"body":""},"mouse-alt":{"body":""},"movie":{"body":""},"movie-play":{"body":""},"music":{"body":""},"navigation":{"body":""},"network-chart":{"body":""},"news":{"body":""},"no-entry":{"body":""},"note":{"body":""},"notepad":{"body":""},"notification":{"body":""},"notification-off":{"body":""},"objects-horizontal-center":{"body":""},"objects-horizontal-left":{"body":""},"objects-horizontal-right":{"body":""},"objects-vertical-bottom":{"body":""},"objects-vertical-center":{"body":""},"objects-vertical-top":{"body":""},"offer":{"body":""},"package":{"body":""},"paint":{"body":""},"paint-roll":{"body":""},"palette":{"body":""},"paper-plane":{"body":""},"parking":{"body":""},"party":{"body":""},"paste":{"body":""},"pear":{"body":""},"pen":{"body":""},"pencil":{"body":""},"phone":{"body":""},"phone-call":{"body":""},"phone-incoming":{"body":""},"phone-off":{"body":""},"phone-outgoing":{"body":""},"photo-album":{"body":""},"piano":{"body":""},"pie-chart":{"body":""},"pie-chart-alt":{"body":""},"pie-chart-alt-2":{"body":""},"pin":{"body":""},"pizza":{"body":""},"plane":{"body":""},"plane-alt":{"body":""},"plane-land":{"body":""},"plane-take-off":{"body":""},"planet":{"body":""},"playlist":{"body":""},"plug":{"body":""},"plus-circle":{"body":""},"plus-square":{"body":""},"pointer":{"body":""},"polygon":{"body":""},"popsicle":{"body":""},"printer":{"body":""},"purchase-tag":{"body":""},"purchase-tag-alt":{"body":""},"pyramid":{"body":""},"quote-alt-left":{"body":""},"quote-alt-right":{"body":""},"quote-left":{"body":""},"quote-right":{"body":""},"quote-single-left":{"body":""},"quote-single-right":{"body":""},"radiation":{"body":""},"radio":{"body":""},"receipt":{"body":""},"rectangle":{"body":""},"registered":{"body":""},"rename":{"body":""},"report":{"body":""},"rewind-circle":{"body":""},"right-arrow":{"body":""},"right-arrow-alt":{"body":""},"right-arrow-circle":{"body":""},"right-arrow-square":{"body":""},"right-down-arrow-circle":{"body":""},"right-top-arrow-circle":{"body":""},"rocket":{"body":""},"ruler":{"body":""},"sad":{"body":""},"save":{"body":""},"school":{"body":""},"search":{"body":""},"search-alt-2":{"body":""},"select-multiple":{"body":""},"send":{"body":""},"server":{"body":""},"shapes":{"body":""},"share":{"body":""},"share-alt":{"body":""},"shield":{"body":""},"shield-alt-2":{"body":""},"shield-minus":{"body":""},"shield-plus":{"body":""},"shield-x":{"body":""},"ship":{"body":""},"shocked":{"body":""},"shopping-bag":{"body":""},"shopping-bag-alt":{"body":""},"shopping-bags":{"body":""},"show":{"body":""},"shower":{"body":""},"skip-next-circle":{"body":""},"skip-previous-circle":{"body":""},"skull":{"body":""},"sleepy":{"body":""},"slideshow":{"body":""},"smile":{"body":""},"sort-alt":{"body":""},"spa":{"body":""},"speaker":{"body":""},"spray-can":{"body":""},"spreadsheet":{"body":""},"square":{"body":""},"square-rounded":{"body":""},"star":{"body":""},"star-half":{"body":""},"sticker":{"body":""},"stopwatch":{"body":""},"store":{"body":""},"store-alt":{"body":""},"sun":{"body":""},"sushi":{"body":""},"t-shirt":{"body":""},"tachometer":{"body":""},"tag":{"body":""},"tag-alt":{"body":""},"tag-x":{"body":""},"taxi":{"body":""},"tennis-ball":{"body":""},"terminal":{"body":""},"thermometer":{"body":""},"time":{"body":""},"time-five":{"body":""},"timer":{"body":""},"tired":{"body":""},"to-top":{"body":""},"toggle-left":{"body":""},"toggle-right":{"body":""},"tone":{"body":""},"torch":{"body":""},"traffic":{"body":""},"traffic-barrier":{"body":""},"traffic-cone":{"body":""},"train":{"body":""},"trash":{"body":""},"trash-alt":{"body":""},"tree":{"body":""},"tree-alt":{"body":""},"trophy":{"body":""},"truck":{"body":""},"tv":{"body":""},"universal-access":{"body":""},"up-arrow":{"body":""},"up-arrow-alt":{"body":""},"up-arrow-circle":{"body":""},"up-arrow-square":{"body":""},"upside-down":{"body":""},"upvote":{"body":""},"user":{"body":""},"user-account":{"body":""},"user-badge":{"body":""},"user-check":{"body":""},"user-circle":{"body":""},"user-detail":{"body":""},"user-minus":{"body":""},"user-pin":{"body":""},"user-plus":{"body":""},"user-rectangle":{"body":""},"user-voice":{"body":""},"user-x":{"body":""},"vector":{"body":""},"vial":{"body":""},"video":{"body":""},"video-off":{"body":""},"video-plus":{"body":""},"video-recording":{"body":""},"videos":{"body":""},"virus":{"body":""},"virus-block":{"body":""},"volume":{"body":""},"volume-full":{"body":""},"volume-low":{"body":""},"volume-mute":{"body":""},"wallet":{"body":""},"wallet-alt":{"body":""},"washer":{"body":""},"watch":{"body":""},"watch-alt":{"body":""},"webcam":{"body":""},"widget":{"body":""},"window-alt":{"body":""},"wine":{"body":""},"wink-smile":{"body":""},"wink-tongue":{"body":""},"wrench":{"body":""},"x-circle":{"body":""},"x-square":{"body":""},"yin-yang":{"body":""},"zap":{"body":""},"zoom-in":{"body":""},"zoom-out":{"body":""}},"lastModified":1702311659,"width":24,"height":24}); +addCollection({"prefix":"bxl","icons":{"500px":{"body":""},"99designs":{"body":""},"adobe":{"body":""},"airbnb":{"body":""},"algolia":{"body":""},"amazon":{"body":""},"android":{"body":""},"angular":{"body":""},"apple":{"body":""},"audible":{"body":""},"aws":{"body":""},"baidu":{"body":""},"behance":{"body":""},"bing":{"body":""},"bitcoin":{"body":""},"blender":{"body":""},"blogger":{"body":""},"bootstrap":{"body":""},"c-plus-plus":{"body":""},"chrome":{"body":""},"codepen":{"body":""},"creative-commons":{"body":""},"css3":{"body":""},"dailymotion":{"body":""},"deezer":{"body":""},"dev-to":{"body":""},"deviantart":{"body":""},"digg":{"body":""},"digitalocean":{"body":""},"discord":{"body":""},"discord-alt":{"body":""},"discourse":{"body":""},"django":{"body":""},"docker":{"body":""},"dribbble":{"body":""},"dropbox":{"body":""},"drupal":{"body":""},"ebay":{"body":""},"edge":{"body":""},"etsy":{"body":""},"facebook":{"body":""},"facebook-circle":{"body":""},"facebook-square":{"body":""},"figma":{"body":""},"firebase":{"body":""},"firefox":{"body":""},"flask":{"body":""},"flickr":{"body":""},"flickr-square":{"body":""},"flutter":{"body":""},"foursquare":{"body":""},"git":{"body":""},"github":{"body":""},"gitlab":{"body":""},"gmail":{"body":""},"go-lang":{"body":""},"google":{"body":""},"google-cloud":{"body":""},"google-plus":{"body":""},"google-plus-circle":{"body":""},"graphql":{"body":""},"heroku":{"body":""},"html5":{"body":""},"imdb":{"body":""},"instagram":{"body":""},"instagram-alt":{"body":""},"internet-explorer":{"body":""},"invision":{"body":""},"java":{"body":""},"javascript":{"body":""},"joomla":{"body":""},"jquery":{"body":""},"jsfiddle":{"body":""},"kickstarter":{"body":""},"kubernetes":{"body":""},"less":{"body":""},"linkedin":{"body":""},"linkedin-square":{"body":""},"magento":{"body":""},"mailchimp":{"body":""},"markdown":{"body":""},"mastercard":{"body":""},"mastodon":{"body":""},"medium":{"body":""},"medium-old":{"body":""},"medium-square":{"body":""},"messenger":{"body":""},"meta":{"body":""},"microsoft":{"body":""},"microsoft-teams":{"body":""},"mongodb":{"body":""},"netlify":{"body":""},"nodejs":{"body":""},"ok-ru":{"body":""},"opera":{"body":""},"patreon":{"body":""},"paypal":{"body":""},"periscope":{"body":""},"php":{"body":""},"pinterest":{"body":""},"pinterest-alt":{"body":""},"play-store":{"body":""},"pocket":{"body":""},"postgresql":{"body":""},"product-hunt":{"body":""},"python":{"body":""},"quora":{"body":""},"react":{"body":""},"redbubble":{"body":""},"reddit":{"body":""},"redux":{"body":""},"sass":{"body":""},"shopify":{"body":""},"sketch":{"body":""},"skype":{"body":""},"slack":{"body":""},"slack-old":{"body":""},"snapchat":{"body":""},"soundcloud":{"body":""},"spotify":{"body":""},"spring-boot":{"body":""},"squarespace":{"body":""},"stack-overflow":{"body":""},"steam":{"body":""},"stripe":{"body":""},"tailwind-css":{"body":""},"telegram":{"body":""},"tiktok":{"body":""},"trello":{"body":""},"trip-advisor":{"body":""},"tumblr":{"body":""},"tux":{"body":""},"twitch":{"body":""},"twitter":{"body":""},"typescript":{"body":""},"unity":{"body":""},"unsplash":{"body":""},"upwork":{"body":""},"venmo":{"body":""},"vimeo":{"body":""},"visa":{"body":""},"visual-studio":{"body":""},"vk":{"body":""},"vuejs":{"body":""},"whatsapp":{"body":""},"whatsapp-square":{"body":""},"wikipedia":{"body":""},"windows":{"body":""},"wix":{"body":""},"wordpress":{"body":""},"xing":{"body":""},"yahoo":{"body":""},"yelp":{"body":""},"youtube":{"body":""},"zoom":{"body":""}},"lastModified":1702311653,"width":24,"height":24}); +addCollection({"prefix":"mdi","icons":{"file-remove-outline":{"body":""},"translate":{"body":""},"vuetify":{"body":""},"information-variant":{"body":""},"arrow-top-right":{"body":""},"arrow-bottom-right":{"body":""},"arrow-bottom-left":{"body":""},"arrow-top-left":{"body":""},"arrow-collapse-all":{"body":""},"arrow-down-left":{"body":""},"web":{"body":""},"cpu-32-bit":{"body":""},"alpha-r":{"body":""},"alpha-g":{"body":""},"alpha-b":{"body":""},"map-marker-off-outline":{"body":""},"alpha-t-box-outline":{"body":""},"form-select":{"body":""},"account-cog-outline":{"body":""},"laptop":{"body":""}},"lastModified":1704178618,"width":24,"height":24}); +addCollection({"prefix":"custom","lastModified":1727366370,"icons":{"checked-checkbox":{"body":""},"checked-radio":{"body":""},"indeterminate-checkbox":{"body":""},"javascript":{"body":""},"typescript":{"body":""},"unchecked-checkbox":{"body":""},"unchecked-radio":{"body":""}},"width":24,"height":24}); diff --git a/resources/js/@iconify/tsconfig.json b/resources/js/@iconify/tsconfig.json new file mode 100644 index 0000000..49d06d9 --- /dev/null +++ b/resources/js/@iconify/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS", + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "composite": false, + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true + }, + "exclude": [ + "./*.js" + ] +} \ No newline at end of file diff --git a/resources/js/@layouts/components/TransitionExpand.vue b/resources/js/@layouts/components/TransitionExpand.vue new file mode 100644 index 0000000..d2c249e --- /dev/null +++ b/resources/js/@layouts/components/TransitionExpand.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/resources/js/@layouts/components/VerticalNav.vue b/resources/js/@layouts/components/VerticalNav.vue new file mode 100644 index 0000000..f96cb21 --- /dev/null +++ b/resources/js/@layouts/components/VerticalNav.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/resources/js/@layouts/components/VerticalNavLayout.vue b/resources/js/@layouts/components/VerticalNavLayout.vue new file mode 100644 index 0000000..fee91c5 --- /dev/null +++ b/resources/js/@layouts/components/VerticalNavLayout.vue @@ -0,0 +1,184 @@ + + + diff --git a/resources/js/@layouts/components/VerticalNavLink.vue b/resources/js/@layouts/components/VerticalNavLink.vue new file mode 100644 index 0000000..c174f9a --- /dev/null +++ b/resources/js/@layouts/components/VerticalNavLink.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/resources/js/@layouts/components/VerticalNavSectionTitle.vue b/resources/js/@layouts/components/VerticalNavSectionTitle.vue new file mode 100644 index 0000000..ef49a03 --- /dev/null +++ b/resources/js/@layouts/components/VerticalNavSectionTitle.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/js/@layouts/styles/_classes.scss b/resources/js/@layouts/styles/_classes.scss new file mode 100644 index 0000000..70074d6 --- /dev/null +++ b/resources/js/@layouts/styles/_classes.scss @@ -0,0 +1,3 @@ +.cursor-pointer { + cursor: pointer; +} diff --git a/resources/js/@layouts/styles/_default-layout.scss b/resources/js/@layouts/styles/_default-layout.scss new file mode 100644 index 0000000..7f12d9a --- /dev/null +++ b/resources/js/@layouts/styles/_default-layout.scss @@ -0,0 +1,35 @@ +// These are styles which are both common in layout w/ vertical nav & horizontal nav +@use "@layouts/styles/rtl"; +@use "@layouts/styles/placeholders"; +@use "@layouts/styles/mixins"; +@use "@configured-variables" as variables; + +html, +body { + min-block-size: 100%; +} + +.layout-page-content { + @include mixins.boxed-content(true); + + flex-grow: 1; + + // TODO: Use grid gutter variable here + padding-block: 1.5rem; +} + +.layout-footer { + .footer-content-container { + block-size: variables.$layout-vertical-nav-footer-height; + } + + .layout-footer-sticky & { + position: sticky; + inset-block-end: 0; + will-change: transform; + } + + .layout-footer-hidden & { + display: none; + } +} diff --git a/resources/js/@layouts/styles/_global.scss b/resources/js/@layouts/styles/_global.scss new file mode 100644 index 0000000..eb6d834 --- /dev/null +++ b/resources/js/@layouts/styles/_global.scss @@ -0,0 +1,10 @@ +*, +::before, +::after { + box-sizing: inherit; + background-repeat: no-repeat; +} + +html { + box-sizing: border-box; +} diff --git a/resources/js/@layouts/styles/_mixins.scss b/resources/js/@layouts/styles/_mixins.scss new file mode 100644 index 0000000..0eeca06 --- /dev/null +++ b/resources/js/@layouts/styles/_mixins.scss @@ -0,0 +1,28 @@ +@use "placeholders"; +@use "@configured-variables" as variables; + +@mixin rtl { + @if variables.$enable-rtl-styles { + [dir="rtl"] & { + @content; + } + } +} + +@mixin boxed-content($nest-selector: false) { + & { + @extend %boxed-content-spacing; + + @at-root { + @if $nest-selector == false { + .layout-content-width-boxed#{&} { + @extend %boxed-content; + } + } @else { + .layout-content-width-boxed & { + @extend %boxed-content; + } + } + } + } +} diff --git a/resources/js/@layouts/styles/_placeholders.scss b/resources/js/@layouts/styles/_placeholders.scss new file mode 100644 index 0000000..c10e25b --- /dev/null +++ b/resources/js/@layouts/styles/_placeholders.scss @@ -0,0 +1,53 @@ +// placeholders +@use "@configured-variables" as variables; + +%boxed-content { + @at-root #{&}-spacing { + // TODO: Use grid gutter variable here + padding-inline: 1.5rem; + } + + inline-size: 100%; + margin-inline: auto; + max-inline-size: variables.$layout-boxed-content-width; +} + +%layout-navbar-hidden { + display: none; +} + +// ℹ️ We created this placeholder even it is being used in just layout w/ vertical nav because in future we might apply style to both navbar & horizontal nav separately +%layout-navbar-sticky { + position: sticky; + inset-block-start: 0; + + // will-change: transform; + // inline-size: 100%; +} + +%style-scroll-bar { + /* width */ + + &::-webkit-scrollbar { + background: rgb(var(--v-theme-surface)); + block-size: 8px; + border-end-end-radius: 14px; + border-start-end-radius: 14px; + inline-size: 4px; + } + + /* Track */ + &::-webkit-scrollbar-track { + background: transparent; + } + + /* Handle */ + &::-webkit-scrollbar-thumb { + border-radius: 0.5rem; + background: rgb(var(--v-theme-perfect-scrollbar-thumb)); + } + + &::-webkit-scrollbar-corner { + display: none; + } +} diff --git a/resources/js/@layouts/styles/_rtl.scss b/resources/js/@layouts/styles/_rtl.scss new file mode 100644 index 0000000..5031d56 --- /dev/null +++ b/resources/js/@layouts/styles/_rtl.scss @@ -0,0 +1,7 @@ +@use "./mixins"; + +.layout-vertical-nav .nav-group-arrow { + @include mixins.rtl { + transform: rotate(180deg); + } +} diff --git a/resources/js/@layouts/styles/_variables.scss b/resources/js/@layouts/styles/_variables.scss new file mode 100644 index 0000000..0166907 --- /dev/null +++ b/resources/js/@layouts/styles/_variables.scss @@ -0,0 +1,28 @@ +// @use "@styles/style.scss"; + +// 👉 Vertical nav +$layout-vertical-nav-z-index: 12 !default; +$layout-vertical-nav-width: 260px !default; +$layout-vertical-nav-collapsed-width: 80px !default; + +// 👉 Horizontal nav +$layout-horizontal-nav-z-index: 11 !default; +$layout-horizontal-nav-navbar-height: 64px !default; + +// 👉 Navbar +$layout-vertical-nav-navbar-height: 64px !default; +$layout-vertical-nav-navbar-is-contained: true !default; +$layout-vertical-nav-layout-navbar-z-index: 11 !default; +$layout-horizontal-nav-layout-navbar-z-index: 11 !default; + +// 👉 Main content +$layout-boxed-content-width: 1440px !default; + +// 👉Footer +$layout-vertical-nav-footer-height: 56px !default; + +// 👉 Layout overlay +$layout-overlay-z-index: 11 !default; + +// 👉 RTL +$enable-rtl-styles: true !default; diff --git a/resources/js/@layouts/styles/index.scss b/resources/js/@layouts/styles/index.scss new file mode 100644 index 0000000..4eb5800 --- /dev/null +++ b/resources/js/@layouts/styles/index.scss @@ -0,0 +1,3 @@ +@use "_global"; +@use "vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.min.css"; +@use "_classes"; diff --git a/resources/js/@layouts/symbols.js b/resources/js/@layouts/symbols.js new file mode 100644 index 0000000..f9ebd4b --- /dev/null +++ b/resources/js/@layouts/symbols.js @@ -0,0 +1 @@ +export const injectionKeyIsVerticalNavHovered = Symbol('isVerticalNavHovered') diff --git a/resources/js/@layouts/utils.js b/resources/js/@layouts/utils.js new file mode 100644 index 0000000..8e114cd --- /dev/null +++ b/resources/js/@layouts/utils.js @@ -0,0 +1,12 @@ +export const hexToRgb = hex => { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i + + hex = hex.replace(shorthandRegex, (m, r, g, b) => { + return r + r + g + g + b + b + }) + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + + return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : null +} diff --git a/resources/js/App.vue b/resources/js/App.vue new file mode 100644 index 0000000..5fe4396 --- /dev/null +++ b/resources/js/App.vue @@ -0,0 +1,229 @@ + + + + diff --git a/resources/js/AudioTrackProcess.js b/resources/js/AudioTrackProcess.js new file mode 100644 index 0000000..ba51b4e --- /dev/null +++ b/resources/js/AudioTrackProcess.js @@ -0,0 +1,53 @@ + +export default class AudioTrackProcess { + name= "" + + source= null + + sourceSettings = null + + processor = null + + trackGenerator= null; + + processedTrack= null; + + constructor(name) { + this.name = name; + } + + async init(opts) { + this.source = opts.track + this.sourceSettings = this.source.getSettings(); + this.processor = new MediaStreamTrackProcessor({ track: this.source }); + this.trackGenerator = new MediaStreamTrackGenerator({ + kind: 'audio', + signalTarget: this.source, + }); + let readableStream = this.processor.readable; + this.transformer = new TransformStream({ + transform: (frame, controller) => this.transform(frame, controller), + }); + readableStream = readableStream.pipeThrough(this.transformer); + readableStream + .pipeTo(this.trackGenerator.writable) + .catch((e) => console.error('error when trying to pipe', e)) + .finally(() => this.destroy()); + this.processedTrack = this.trackGenerator + } + + async transform(data, controller) { + console.log('transformer', data); + controller.enqueue(data); + } + + async restart(opts) { + await this.destroy(); + return this.init(opts); + } + + async destroy() { + + this.trackGenerator?.stop(); + } +} diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..846d350 --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,32 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo'; + +// import Pusher from 'pusher-js'; +// window.Pusher = Pusher; + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: import.meta.env.VITE_PUSHER_APP_KEY, +// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', +// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, +// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, +// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, +// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', +// enabledTransports: ['ws', 'wss'], +// }); diff --git a/resources/js/components/ErrorHeader.vue b/resources/js/components/ErrorHeader.vue new file mode 100644 index 0000000..bdd5d7a --- /dev/null +++ b/resources/js/components/ErrorHeader.vue @@ -0,0 +1,37 @@ + + + diff --git a/resources/js/components/UpgradeToPro.vue b/resources/js/components/UpgradeToPro.vue new file mode 100644 index 0000000..958094e --- /dev/null +++ b/resources/js/components/UpgradeToPro.vue @@ -0,0 +1,66 @@ + + + diff --git a/resources/js/layouts/agentCallDefault.vue b/resources/js/layouts/agentCallDefault.vue new file mode 100644 index 0000000..dbdbe1a --- /dev/null +++ b/resources/js/layouts/agentCallDefault.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/resources/js/layouts/blank.vue b/resources/js/layouts/blank.vue new file mode 100644 index 0000000..8816b73 --- /dev/null +++ b/resources/js/layouts/blank.vue @@ -0,0 +1,11 @@ + + + diff --git a/resources/js/layouts/components/DefaultLayoutWithVerticalNav.vue b/resources/js/layouts/components/DefaultLayoutWithVerticalNav.vue new file mode 100644 index 0000000..2924335 --- /dev/null +++ b/resources/js/layouts/components/DefaultLayoutWithVerticalNav.vue @@ -0,0 +1,723 @@ + + + + + diff --git a/resources/js/layouts/components/DefinedSteps.vue b/resources/js/layouts/components/DefinedSteps.vue new file mode 100644 index 0000000..156d50c --- /dev/null +++ b/resources/js/layouts/components/DefinedSteps.vue @@ -0,0 +1,46 @@ + + diff --git a/resources/js/layouts/components/Footer.vue b/resources/js/layouts/components/Footer.vue new file mode 100644 index 0000000..b90a64c --- /dev/null +++ b/resources/js/layouts/components/Footer.vue @@ -0,0 +1,40 @@ + + + diff --git a/resources/js/layouts/components/NavBarNotifications.vue b/resources/js/layouts/components/NavBarNotifications.vue new file mode 100644 index 0000000..3ba7f85 --- /dev/null +++ b/resources/js/layouts/components/NavBarNotifications.vue @@ -0,0 +1,157 @@ + + + diff --git a/resources/js/layouts/components/NavbarThemeSwitcher.vue b/resources/js/layouts/components/NavbarThemeSwitcher.vue new file mode 100644 index 0000000..dd55797 --- /dev/null +++ b/resources/js/layouts/components/NavbarThemeSwitcher.vue @@ -0,0 +1,16 @@ + + + diff --git a/resources/js/layouts/components/PatientPrescriptionDetail.vue b/resources/js/layouts/components/PatientPrescriptionDetail.vue new file mode 100644 index 0000000..c513f76 --- /dev/null +++ b/resources/js/layouts/components/PatientPrescriptionDetail.vue @@ -0,0 +1,411 @@ + + + diff --git a/resources/js/layouts/components/ProviderInfo.vue b/resources/js/layouts/components/ProviderInfo.vue new file mode 100644 index 0000000..0b255d7 --- /dev/null +++ b/resources/js/layouts/components/ProviderInfo.vue @@ -0,0 +1,58 @@ + + + + + \ No newline at end of file diff --git a/resources/js/layouts/components/QueueComponent.vue b/resources/js/layouts/components/QueueComponent.vue new file mode 100644 index 0000000..e9b38e9 --- /dev/null +++ b/resources/js/layouts/components/QueueComponent.vue @@ -0,0 +1,2301 @@ + + +