├── .editorconfig ├── .env.example ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .prettierrc ├── README.md ├── app ├── Actions │ ├── Client │ │ ├── CreateClient.php │ │ └── UpdateClient.php │ ├── ClientCompany │ │ ├── CreateClientCompany.php │ │ └── UpdateClientCompany.php │ ├── Invoice │ │ ├── CreateInvoice.php │ │ └── UpdateInvoice.php │ ├── OwnerCompany │ │ └── UpdateOwnerCompany.php │ ├── Task │ │ ├── CreateTask.php │ │ └── UpdateTask.php │ └── User │ │ ├── CreateUser.php │ │ ├── UpdateAuthUser.php │ │ └── UpdateUser.php ├── Console │ ├── Commands │ │ ├── PruneActivities.php │ │ └── PruneNotifications.php │ └── Kernel.php ├── Enums │ ├── Invoice.php │ └── Queue.php ├── Events │ ├── Task │ │ ├── AttachmentDeleted.php │ │ ├── AttachmentsUploaded.php │ │ ├── CommentCreated.php │ │ ├── TaskCreated.php │ │ ├── TaskDeleted.php │ │ ├── TaskGroupChanged.php │ │ ├── TaskOrderChanged.php │ │ ├── TaskRestored.php │ │ ├── TaskUpdated.php │ │ ├── TimeLogCreated.php │ │ └── TimeLogDeleted.php │ ├── TaskGroup │ │ ├── TaskGroupCreated.php │ │ ├── TaskGroupDeleted.php │ │ ├── TaskGroupOrderChanged.php │ │ ├── TaskGroupRestored.php │ │ └── TaskGroupUpdated.php │ └── UserCreated.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ ├── Account │ │ │ ├── NotificationController.php │ │ │ └── ProfileController.php │ │ ├── Auth │ │ │ ├── AuthenticationController.php │ │ │ ├── GoogleSocialiteController.php │ │ │ ├── NewPasswordController.php │ │ │ └── ResetPasswordController.php │ │ ├── Client │ │ │ ├── ClientCompanyController.php │ │ │ └── ClientUserController.php │ │ ├── Controller.php │ │ ├── DashboardController.php │ │ ├── DropdownValuesController.php │ │ ├── Invoice │ │ │ └── InvoiceTasksController.php │ │ ├── InvoiceController.php │ │ ├── MyWork │ │ │ ├── ActivityController.php │ │ │ └── MyWorkTaskController.php │ │ ├── ProjectController.php │ │ ├── ReportController.php │ │ ├── Settings │ │ │ ├── LabelController.php │ │ │ ├── OwnerCompanyController.php │ │ │ └── RoleController.php │ │ ├── Task │ │ │ ├── AttachmentController.php │ │ │ ├── CommentController.php │ │ │ ├── GroupController.php │ │ │ └── TimeLogController.php │ │ ├── TaskController.php │ │ └── UserController.php │ ├── Kernel.php │ ├── Middleware │ │ ├── Authenticate.php │ │ ├── EncryptCookies.php │ │ ├── HandleInertiaRequests.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── TrustProxies.php │ │ ├── ValidateSignature.php │ │ └── VerifyCsrfToken.php │ ├── Requests │ │ ├── Auth │ │ │ └── LoginRequest.php │ │ ├── Client │ │ │ ├── StoreClientRequest.php │ │ │ └── UpdateClientRequest.php │ │ ├── ClientCompany │ │ │ ├── StoreClientCompanyRequest.php │ │ │ └── UpdateClientCompanyRequest.php │ │ ├── Comment │ │ │ └── StoreCommentRequest.php │ │ ├── Invoice │ │ │ ├── StoreInvoiceRequest.php │ │ │ └── UpdateInvoiceRequest.php │ │ ├── Label │ │ │ ├── StoreLabelRequest.php │ │ │ └── UpdateLabelRequest.php │ │ ├── OwnerCompany │ │ │ └── UpdateOwnerCompanyRequest.php │ │ ├── Project │ │ │ ├── StoreProjectRequest.php │ │ │ └── UpdateProjectRequest.php │ │ ├── Role │ │ │ ├── StoreRoleRequest.php │ │ │ └── UpdateRoleRequest.php │ │ ├── Task │ │ │ ├── StoreTaskRequest.php │ │ │ └── UpdateTaskRequest.php │ │ ├── TaskGroup │ │ │ ├── StoreTaskGroupRequest.php │ │ │ └── UpdateTaskGroupRequest.php │ │ ├── TimeLog │ │ │ └── StoreTimeLogRequest.php │ │ └── User │ │ │ ├── StoreUserRequest.php │ │ │ ├── UpdateAuthUserRequest.php │ │ │ └── UpdateUserRequest.php │ └── Resources │ │ ├── Activity │ │ └── ActivityGroupedByDateCollection.php │ │ ├── Client │ │ └── ClientResource.php │ │ ├── ClientCompany │ │ └── ClientCompanyResource.php │ │ ├── Invoice │ │ └── InvoiceResource.php │ │ ├── Label │ │ └── LabelResource.php │ │ ├── Notification │ │ ├── NotificationGroupedByDateCollection.php │ │ └── NotificationResource.php │ │ ├── Project │ │ └── ProjectResource.php │ │ ├── Role │ │ └── RoleResource.php │ │ └── User │ │ ├── AuthUserResource.php │ │ └── UserResource.php ├── Listeners │ ├── NotifyTaskSubscribers.php │ └── SendEmailWithCredentials.php ├── Models │ ├── Activity.php │ ├── Attachment.php │ ├── ClientCompany.php │ ├── Comment.php │ ├── Country.php │ ├── Currency.php │ ├── Filters │ │ ├── IsNullFilter.php │ │ ├── TaskCompletedFilter.php │ │ ├── TaskOverdueFilter.php │ │ ├── WhereHasFilter.php │ │ └── WhereInFilter.php │ ├── Invoice.php │ ├── Label.php │ ├── OwnerCompany.php │ ├── Permission.php │ ├── Project.php │ ├── Role.php │ ├── Task.php │ ├── TaskGroup.php │ ├── TimeLog.php │ └── User.php ├── Notifications │ ├── CommentCreatedMentionedUserNotification.php │ ├── CommentCreatedNotification.php │ ├── TaskCreatedMentionedUserNotification.php │ ├── TaskCreatedNotification.php │ └── UserCreatedNotification.php ├── Observers │ ├── CommentObserver.php │ ├── ProjectObserver.php │ └── TaskObserver.php ├── Policies │ ├── ClientCompanyPolicy.php │ ├── CommentPolicy.php │ ├── InvoicePolicy.php │ ├── LabelPolicy.php │ ├── OwnerCompanyPolicy.php │ ├── ProjectPolicy.php │ ├── RolePolicy.php │ ├── TaskGroupPolicy.php │ ├── TaskPolicy.php │ ├── TimeLogPolicy.php │ └── UserPolicy.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── Rules │ └── HexColor.php └── Services │ ├── InvoiceService.php │ ├── NotificationService.php │ ├── PermissionService.php │ ├── ProjectService.php │ ├── UserMentionService.php │ └── UserService.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── audit.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── favorite.php ├── filesystems.php ├── hashing.php ├── logging.php ├── mail.php ├── permission.php ├── queue.php ├── sanctum.php ├── services.php ├── session.php └── view.php ├── database ├── .gitignore ├── factories │ ├── ClientCompanyFactory.php │ ├── ClientFactory.php │ └── UserFactory.php ├── migrations │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_reset_tokens_table.php │ ├── 2018_12_14_000000_create_favorites_table.php │ ├── 2023_10_26_165503_create_permission_tables.php │ ├── 2023_10_31_105255_create_jobs_table.php │ ├── 2023_10_31_113749_create_failed_jobs_table.php │ ├── 2023_11_01_120111_create_labels_table.php │ ├── 2023_11_01_182514_add_archived_at_to_roles.php │ ├── 2023_11_02_153937_create_owner_companies_table.php │ ├── 2023_11_02_165827_create_currencies_table.php │ ├── 2023_11_03_134217_create_countries_table.php │ ├── 2023_11_03_190241_create_client_companies_table.php │ ├── 2023_11_04_104543_create_client_company_pivot_table.php │ ├── 2023_11_06_094257_create_projects_table.php │ ├── 2023_11_06_153749_create_project_user_access.php │ ├── 2023_11_07_131704_create_task_groups_table.php │ ├── 2023_11_07_192734_create_tasks_table.php │ ├── 2023_11_10_144123_create_label_task_pivot_table.php │ ├── 2023_11_15_220141_create_subscribe_task.php │ ├── 2023_11_15_220222_create_attachments.php │ ├── 2023_11_16_144304_create_notifications_table.php │ ├── 2023_11_17_211110_create_time_logs_table.php │ ├── 2023_11_18_193550_create_comments_table.php │ ├── 2023_11_28_142456_create_audits_table.php │ ├── 2023_11_28_155542_create_activities.php │ ├── 2023_12_04_145458_create_invoices_table.php │ └── 2024_01_30_190158_add_rate_to_projects.php └── seeders │ ├── ClientCompanySeeder.php │ ├── ClientSeeder.php │ ├── CountrySeeder.php │ ├── CurrencySeeder.php │ ├── DatabaseSeeder.php │ ├── LabelSeeder.php │ ├── OwnerCompanySeeder.php │ ├── PermissionSeeder.php │ ├── ProductionSeeder.php │ ├── ProjectSeeder.php │ ├── RoleSeeder.php │ ├── TaskGroupSeeder.php │ ├── TasksSeeder.php │ └── UserSeeder.php ├── docker-compose.yml ├── jsconfig.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── pint.json ├── postcss.config.js ├── public ├── .htaccess ├── favicon.ico ├── index.php ├── robots.txt └── vendor │ └── telescope │ ├── app-dark.css │ ├── app.css │ ├── app.js │ ├── favicon.ico │ └── mix-manifest.json ├── resources ├── css │ └── app.css ├── docs │ ├── banner.afphoto │ ├── banner.jpg │ └── screenshots │ │ ├── Activity - dark.jpeg │ │ ├── Activity - light.jpeg │ │ ├── Dashboard - dark.jpeg │ │ ├── Dashboard - light.jpeg │ │ ├── Invoice - dark.jpeg │ │ ├── Invoice - light.jpeg │ │ ├── My tasks - dark.jpeg │ │ ├── My tasks - light.jpeg │ │ ├── Project tasks - dark.jpeg │ │ ├── Project tasks - light.jpeg │ │ ├── Projects - dark.jpeg │ │ ├── Projects - light.jpeg │ │ ├── Task - dark.jpeg │ │ └── Task - light.jpeg ├── js │ ├── app.jsx │ ├── bootstrap.js │ ├── components │ │ ├── ActionButton.jsx │ │ ├── ArchivedFilterButton.jsx │ │ ├── BackButton.jsx │ │ ├── Card.jsx │ │ ├── ClearFiltersButton.jsx │ │ ├── ConfirmModal.jsx │ │ ├── Dropzone.jsx │ │ ├── EmptyResult.jsx │ │ ├── EmptyWithIcon.jsx │ │ ├── FileThumbnail.jsx │ │ ├── FlashNotification.jsx │ │ ├── ImageModal.jsx │ │ ├── Label.jsx │ │ ├── Logo.jsx │ │ ├── Notification.jsx │ │ ├── Pagination.jsx │ │ ├── RichTextEditor.jsx │ │ ├── RichTextEditor │ │ │ └── Mention │ │ │ │ ├── MentionList.jsx │ │ │ │ ├── css │ │ │ │ └── MentionList.css │ │ │ │ └── suggestion.js │ │ ├── RoleBadge.jsx │ │ ├── SearchInput.jsx │ │ ├── TableHead.jsx │ │ ├── TableHeaderCell.jsx │ │ ├── TableRowActions.jsx │ │ ├── TableRowEmpty.jsx │ │ ├── TaskGroupLabel.jsx │ │ ├── UserInfoCard.jsx │ │ └── css │ │ │ ├── Card.module.css │ │ │ ├── FileThumbnail.module.css │ │ │ ├── FlashNotification.module.css │ │ │ ├── Notification.module.css │ │ │ ├── RichTextEditor.module.css │ │ │ ├── TableHeader.module.css │ │ │ └── UserInfoCard.module.css │ ├── hooks │ │ ├── store │ │ │ ├── taskGroups │ │ │ │ └── TaskGroupWebSocketUpdatesSlice.js │ │ │ ├── tasks │ │ │ │ ├── TaskAttachmentsSlice.js │ │ │ │ ├── TaskCommentsSlice.js │ │ │ │ ├── TaskTimeLogsSlice.js │ │ │ │ └── TaskWebSocketUpdatesSlice.js │ │ │ ├── useNavigationStore.js │ │ │ ├── useNotificationsStore.js │ │ │ ├── useTaskDrawerStore.js │ │ │ ├── useTaskFiltersStore.js │ │ │ ├── useTaskGroupsStore.js │ │ │ └── useTasksStore.js │ │ ├── useAuthorization.js │ │ ├── useForm.js │ │ ├── useImageLoader.js │ │ ├── usePreferences.js │ │ ├── useRoles.js │ │ ├── useSorting.js │ │ ├── useTimer.js │ │ └── useWebSockets.js │ ├── icons │ │ └── GoogleIcon.jsx │ ├── layouts │ │ ├── ContainerBox.jsx │ │ ├── GuestLayout.jsx │ │ ├── MainLayout.jsx │ │ ├── NavBarNested.jsx │ │ ├── NavbarLinksGroup.jsx │ │ ├── Notifications.jsx │ │ ├── UserButton.jsx │ │ └── css │ │ │ ├── ContainerBox.module.css │ │ │ ├── NavBarNested.module.css │ │ │ ├── NavbarLinksGroup.module.css │ │ │ ├── Notifications.module.css │ │ │ └── UserButton.module.css │ ├── pages │ │ ├── Account │ │ │ ├── Notifications │ │ │ │ ├── Index.jsx │ │ │ │ └── css │ │ │ │ │ └── Index.module.css │ │ │ └── Profile │ │ │ │ └── Edit.jsx │ │ ├── Auth │ │ │ ├── ForgotPassword.jsx │ │ │ ├── Login.jsx │ │ │ ├── LoginNotification.jsx │ │ │ ├── ResetPassword.jsx │ │ │ └── css │ │ │ │ ├── ForgotPassword.module.css │ │ │ │ ├── Login.module.css │ │ │ │ └── ResetPassword.module.css │ │ ├── Clients │ │ │ ├── Companies │ │ │ │ ├── Create.jsx │ │ │ │ ├── Edit.jsx │ │ │ │ ├── Index.jsx │ │ │ │ └── TableRow.jsx │ │ │ └── Users │ │ │ │ ├── Create.jsx │ │ │ │ ├── Edit.jsx │ │ │ │ ├── Index.jsx │ │ │ │ └── TableRow.jsx │ │ ├── Dashboard │ │ │ ├── Cards │ │ │ │ ├── OverdueTasks.jsx │ │ │ │ ├── ProjectCard.jsx │ │ │ │ ├── RecentComments.jsx │ │ │ │ ├── RecentlyAssignedTasks.jsx │ │ │ │ └── css │ │ │ │ │ ├── OverdueTasks.module.css │ │ │ │ │ ├── ProjectCard.module.css │ │ │ │ │ ├── RecentComments.module.css │ │ │ │ │ └── RecentlyAssignedTasks.module.css │ │ │ ├── Index.jsx │ │ │ └── css │ │ │ │ └── Index.module.css │ │ ├── Error.jsx │ │ ├── Invoices │ │ │ ├── Create.jsx │ │ │ ├── Edit.jsx │ │ │ ├── Index.jsx │ │ │ ├── InvoiceCreate.jsx │ │ │ ├── StatusDropdown.jsx │ │ │ ├── TableRow.jsx │ │ │ ├── Task.jsx │ │ │ └── css │ │ │ │ └── Task.module.css │ │ ├── MyWork │ │ │ ├── Activity │ │ │ │ └── Index.jsx │ │ │ └── Tasks │ │ │ │ ├── Index.jsx │ │ │ │ ├── Task.jsx │ │ │ │ └── css │ │ │ │ ├── Index.module.css │ │ │ │ └── Task.module.css │ │ ├── Projects │ │ │ ├── Create.jsx │ │ │ ├── Edit.jsx │ │ │ ├── Index.jsx │ │ │ ├── Index │ │ │ │ ├── FavoriteToggle.jsx │ │ │ │ ├── Modals │ │ │ │ │ └── UserAccessModal.jsx │ │ │ │ ├── ProjectCard.jsx │ │ │ │ ├── ProjectCardActions.jsx │ │ │ │ └── css │ │ │ │ │ ├── FavoriteToggle.module.css │ │ │ │ │ └── ProjectCard.module.css │ │ │ └── Tasks │ │ │ │ ├── Drawers │ │ │ │ ├── Comments.jsx │ │ │ │ ├── CreateTaskDrawer.jsx │ │ │ │ ├── EditTaskDrawer.jsx │ │ │ │ ├── LabelsDropdown.jsx │ │ │ │ ├── Timer.jsx │ │ │ │ └── css │ │ │ │ │ ├── Comments.module.css │ │ │ │ │ ├── TaskDrawer.module.css │ │ │ │ │ └── Timer.module.css │ │ │ │ ├── Index.jsx │ │ │ │ ├── Index │ │ │ │ ├── Archive │ │ │ │ │ ├── ArchivedItems.jsx │ │ │ │ │ ├── ArchivedTask.jsx │ │ │ │ │ └── ArchivedTaskGroup.jsx │ │ │ │ ├── Filters.jsx │ │ │ │ ├── Filters │ │ │ │ │ ├── FilterButton.jsx │ │ │ │ │ └── css │ │ │ │ │ │ └── FilterButton.module.css │ │ │ │ ├── FiltersDrawer.jsx │ │ │ │ ├── Header.jsx │ │ │ │ ├── Modals │ │ │ │ │ ├── CreateTasksGroupModal.jsx │ │ │ │ │ └── EditTasksGroupModal.jsx │ │ │ │ ├── Task.jsx │ │ │ │ ├── Task │ │ │ │ │ ├── TaskCard.jsx │ │ │ │ │ ├── TaskRow.jsx │ │ │ │ │ └── css │ │ │ │ │ │ ├── TaskCard.module.css │ │ │ │ │ │ └── TaskRow.module.css │ │ │ │ ├── TaskActions.jsx │ │ │ │ ├── TaskGroup.jsx │ │ │ │ ├── TaskGroupActions.jsx │ │ │ │ └── css │ │ │ │ │ └── TaskGroup.module.css │ │ │ │ └── css │ │ │ │ └── Index.module.css │ │ ├── Reports │ │ │ ├── DailyLoggedTime.jsx │ │ │ └── LoggedTimeSum.jsx │ │ ├── Settings │ │ │ ├── Company │ │ │ │ └── Edit.jsx │ │ │ ├── Labels │ │ │ │ ├── Create.jsx │ │ │ │ ├── Edit.jsx │ │ │ │ ├── Index.jsx │ │ │ │ └── TableRow.jsx │ │ │ └── Roles │ │ │ │ ├── Create.jsx │ │ │ │ ├── Edit.jsx │ │ │ │ ├── Index.jsx │ │ │ │ └── TableRow.jsx │ │ ├── Users │ │ │ ├── Create.jsx │ │ │ ├── Edit.jsx │ │ │ ├── Index.jsx │ │ │ └── TableRow.jsx │ │ └── css │ │ │ └── Error.module.css │ ├── types.ts │ └── utils │ │ ├── axios.js │ │ ├── currency.js │ │ ├── datetime.js │ │ ├── domEvents.js │ │ ├── file.js │ │ ├── reorder.js │ │ ├── route.js │ │ ├── table.js │ │ ├── task.js │ │ ├── timer.js │ │ └── user.js └── views │ ├── app.blade.php │ └── vendor │ ├── invoices │ └── templates │ │ └── default.blade.php │ ├── mail │ ├── html │ │ ├── button.blade.php │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ ├── layout.blade.php │ │ ├── message.blade.php │ │ ├── panel.blade.php │ │ ├── subcopy.blade.php │ │ ├── table.blade.php │ │ └── themes │ │ │ └── default.css │ └── text │ │ ├── button.blade.php │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ ├── layout.blade.php │ │ ├── message.blade.php │ │ ├── panel.blade.php │ │ ├── subcopy.blade.php │ │ └── table.blade.php │ └── notifications │ └── email.blade.php ├── routes ├── api.php ├── auth.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ ├── .gitignore │ │ └── avatars │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── CreatesApplication.php ├── Feature │ └── ExampleTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length=100 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{yml,yaml}] 16 | indent_size = 2 17 | 18 | [*.{js,jsx}] 19 | indent_size = 2 20 | 21 | [docker-compose.yml] 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:react-hooks/recommended", 10 | "plugin:react/jsx-runtime" 11 | ], 12 | "overrides": [ 13 | { 14 | "env": { 15 | "node": true 16 | }, 17 | "files": [ 18 | ".eslintrc.{js,cjs}" 19 | ], 20 | "parserOptions": { 21 | "sourceType": "script" 22 | } 23 | } 24 | ], 25 | "parserOptions": { 26 | "ecmaVersion": "latest", 27 | "sourceType": "module" 28 | }, 29 | "plugins": [ 30 | "react", 31 | "react-hooks" 32 | ], 33 | "rules": { 34 | "react/prop-types": 0, 35 | "react-hooks/exhaustive-deps": 0 36 | }, 37 | "globals": { 38 | "route": false, 39 | "can": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | Homestead.json 13 | Homestead.yaml 14 | auth.json 15 | npm-debug.log 16 | yarn-error.log 17 | /.fleet 18 | /.idea 19 | /.vscode 20 | /storage/clockwork 21 | .pnpm-lock.yaml 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleAttributePerLine": true, 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "bracketSameLine": false, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "trailingComma": "es5", 9 | "arrowParens": "avoid" 10 | } -------------------------------------------------------------------------------- /app/Actions/Client/CreateClient.php: -------------------------------------------------------------------------------- 1 | $data['name'], 18 | 'job_title' => 'Client', 19 | 'phone' => $data['phone'], 20 | 'rate' => null, 21 | 'email' => $data['email'], 22 | 'password' => Hash::make($data['password']), 23 | ]); 24 | 25 | $user->update(['avatar' => UserService::storeOrFetchAvatar($user, $data['avatar'])]); 26 | 27 | $user->assignRole('client'); 28 | 29 | if (! empty($data['companies'])) { 30 | $user->clientCompanies()->attach($data['companies']); 31 | } 32 | 33 | UserCreated::dispatch($user, $data['password']); 34 | 35 | return $user; 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Actions/Client/UpdateClient.php: -------------------------------------------------------------------------------- 1 | $data['name'], 14 | 'phone' => $data['phone'], 15 | 'email' => $data['email'], 16 | ]; 17 | 18 | if ($user->avatar === null || $data['avatar']) { 19 | $newData['avatar'] = UserService::storeOrFetchAvatar($user, $data['avatar']); 20 | } 21 | 22 | if (! empty($data['password'])) { 23 | $newData['password'] = Hash::make($data['password']); 24 | } 25 | 26 | if (! empty($data['companies'])) { 27 | $user->clientCompanies()->sync($data['companies']); 28 | } 29 | 30 | return $user->update($newData); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Actions/ClientCompany/CreateClientCompany.php: -------------------------------------------------------------------------------- 1 | clients()->attach($data['clients']); 17 | } 18 | 19 | return $clientCompany; 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Actions/ClientCompany/UpdateClientCompany.php: -------------------------------------------------------------------------------- 1 | clients()->sync($data['clients']); 13 | } 14 | 15 | return $clientCompany->update($data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Actions/OwnerCompany/UpdateOwnerCompany.php: -------------------------------------------------------------------------------- 1 | except(['logo']); 14 | 15 | if ($request->hasFile('logo')) { 16 | $logo = $request->file('logo'); 17 | $logo->storePubliclyAs('public/company', 'logo.'.$logo->clientExtension()); 18 | $data['logo'] = '/storage/company/logo.'.$logo->clientExtension(); 19 | } 20 | 21 | $data['tax'] = intval($data['tax']) * 100; 22 | 23 | return $ownerCompany->update($data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Actions/Task/UpdateTask.php: -------------------------------------------------------------------------------- 1 | update($data); 16 | 17 | if ($updateField === 'group_id') { 18 | $task->update(['order_column' => 0]); 19 | } 20 | } 21 | 22 | if ($updateField === 'subscribed_users') { 23 | $task->subscribedUsers()->sync($data['subscribed_users']); 24 | } 25 | 26 | if ($updateField === 'labels') { 27 | $task->labels()->sync($data['labels']); 28 | } 29 | 30 | TaskUpdated::dispatch($task, $updateField); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Actions/User/CreateUser.php: -------------------------------------------------------------------------------- 1 | $data['name'], 18 | 'job_title' => $data['job_title'], 19 | 'phone' => $data['phone'], 20 | 'rate' => $data['rate'] * 100, 21 | 'email' => $data['email'], 22 | 'password' => Hash::make($data['password']), 23 | ]); 24 | 25 | $user->update(['avatar' => UserService::storeOrFetchAvatar($user, $data['avatar'])]); 26 | 27 | $user->assignRole($data['roles']); 28 | 29 | UserCreated::dispatch($user, $data['password']); 30 | 31 | return $user; 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Actions/User/UpdateAuthUser.php: -------------------------------------------------------------------------------- 1 | $data['name'], 14 | 'job_title' => $data['job_title'], 15 | 'phone' => $data['phone'], 16 | 'email' => $data['email'], 17 | ]; 18 | 19 | if ($user->avatar === null || $data['avatar']) { 20 | $newData['avatar'] = UserService::storeOrFetchAvatar($user, $data['avatar']); 21 | } 22 | 23 | if (! empty($data['password'])) { 24 | $newData['password'] = Hash::make($data['password']); 25 | } 26 | 27 | return $user->update($newData); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Actions/User/UpdateUser.php: -------------------------------------------------------------------------------- 1 | $data['name'], 14 | 'job_title' => $data['job_title'], 15 | 'phone' => $data['phone'], 16 | 'rate' => $data['rate'] * 100, 17 | 'email' => $data['email'], 18 | ]; 19 | 20 | $user->syncRoles($data['roles']); 21 | 22 | if ($user->avatar === null || $data['avatar']) { 23 | $newData['avatar'] = UserService::storeOrFetchAvatar($user, $data['avatar']); 24 | } 25 | 26 | if (! empty($data['password'])) { 27 | $newData['password'] = Hash::make($data['password']); 28 | } 29 | 30 | return $user->update($newData); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Console/Commands/PruneActivities.php: -------------------------------------------------------------------------------- 1 | each(function (Project $project) use ($skip) { 34 | $count = Activity::where('project_id', $project->id)->count(); 35 | 36 | if ($count > $skip) { 37 | Activity::where('project_id', $project->id) 38 | ->latest() 39 | ->skip($skip) 40 | ->take($count - $skip) 41 | ->delete(); 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Console/Commands/PruneNotifications.php: -------------------------------------------------------------------------------- 1 | each(function (User $user) use ($skip) { 33 | $count = $user->notifications()->count(); 34 | 35 | if ($count > $skip) { 36 | $user->notifications() 37 | ->latest() 38 | ->skip($skip) 39 | ->take($count - $skip) 40 | ->delete(); 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('auth:clear-resets')->everyFifteenMinutes(); 16 | 17 | $schedule->command('project:prune-activities')->dailyAt('03:00'); 18 | $schedule->command('user:prune-notifications')->dailyAt('03:05'); 19 | } 20 | 21 | /** 22 | * Register the commands for the application. 23 | */ 24 | protected function commands(): void 25 | { 26 | $this->load(__DIR__.'/Commands'); 27 | 28 | require base_path('routes/console.php'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Enums/Invoice.php: -------------------------------------------------------------------------------- 1 | taskId = $task->id; 26 | } 27 | 28 | /** 29 | * Get the channels the event should broadcast on. 30 | * 31 | * @return array 32 | */ 33 | public function broadcastOn(): array 34 | { 35 | return [ 36 | new PrivateChannel("App.Models.Project.{$this->task->project_id}"), 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Events/Task/AttachmentsUploaded.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function broadcastOn(): array 31 | { 32 | return [ 33 | new PrivateChannel("App.Models.Project.{$this->task->project_id}"), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Events/Task/CommentCreated.php: -------------------------------------------------------------------------------- 1 | comment = $comment->load('user:id,name,avatar,job_title'); 24 | } 25 | 26 | /** 27 | * Get the channels the event should broadcast on. 28 | * 29 | * @return array 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel("App.Models.Task.{$this->comment->task_id}"), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/Task/TaskCreated.php: -------------------------------------------------------------------------------- 1 | task = $task->loadDefault(); 24 | } 25 | 26 | /** 27 | * Get the channels the event should broadcast on. 28 | * 29 | * @return array 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel("App.Models.Project.{$this->task->project_id}"), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/Task/TaskDeleted.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function broadcastOn(): array 29 | { 30 | return [ 31 | new PrivateChannel("App.Models.Project.{$this->projectId}"), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Events/Task/TaskGroupChanged.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel("App.Models.Project.{$this->projectId}"), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/Task/TaskOrderChanged.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function broadcastOn(): array 31 | { 32 | return [ 33 | new PrivateChannel("App.Models.Project.{$this->projectId}"), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Events/Task/TaskRestored.php: -------------------------------------------------------------------------------- 1 | task = $task->loadDefault(); 26 | $this->groupId = $task->group_id; 27 | } 28 | 29 | /** 30 | * Get the channels the event should broadcast on. 31 | * 32 | * @return array 33 | */ 34 | public function broadcastOn(): array 35 | { 36 | return [ 37 | new PrivateChannel("App.Models.Project.{$this->task->project_id}"), 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Events/Task/TimeLogCreated.php: -------------------------------------------------------------------------------- 1 | timeLog = $timeLog->load(['user:id,name']); 27 | } 28 | 29 | /** 30 | * Get the channels the event should broadcast on. 31 | * 32 | * @return array 33 | */ 34 | public function broadcastOn(): array 35 | { 36 | return [ 37 | new PrivateChannel("App.Models.Project.{$this->task->project_id}"), 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Events/Task/TimeLogDeleted.php: -------------------------------------------------------------------------------- 1 | taskId = $task->id; 26 | } 27 | 28 | /** 29 | * Get the channels the event should broadcast on. 30 | * 31 | * @return array 32 | */ 33 | public function broadcastOn(): array 34 | { 35 | return [ 36 | new PrivateChannel("App.Models.Project.{$this->task->project_id}"), 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Events/TaskGroup/TaskGroupCreated.php: -------------------------------------------------------------------------------- 1 | taskGroup = $taskGroup; 24 | } 25 | 26 | /** 27 | * Get the channels the event should broadcast on. 28 | * 29 | * @return array 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel("App.Models.Project.{$this->taskGroup->project_id}"), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/TaskGroup/TaskGroupDeleted.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function broadcastOn(): array 29 | { 30 | return [ 31 | new PrivateChannel("App.Models.Project.{$this->projectId}"), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Events/TaskGroup/TaskGroupOrderChanged.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function broadcastOn(): array 29 | { 30 | return [ 31 | new PrivateChannel("App.Models.Project.{$this->projectId}"), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Events/TaskGroup/TaskGroupRestored.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function broadcastOn(): array 29 | { 30 | return [ 31 | new PrivateChannel("App.Models.Project.{$this->taskGroup->project_id}"), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Events/TaskGroup/TaskGroupUpdated.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function broadcastOn(): array 29 | { 30 | return [ 31 | new PrivateChannel("App.Models.Project.{$this->taskGroup->project_id}"), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Events/UserCreated.php: -------------------------------------------------------------------------------- 1 | new NotificationGroupedByDateCollection( 17 | auth() 18 | ->user() 19 | ->notifications() 20 | ->latest() 21 | ->get() 22 | ), 23 | ]); 24 | } 25 | 26 | public function read(DatabaseNotification $notification) 27 | { 28 | $notification->markAsRead(); 29 | 30 | return response()->json(); 31 | } 32 | 33 | public function readAll() 34 | { 35 | auth()->user()->unreadNotifications()->update(['read_at' => now()]); 36 | 37 | return response()->json(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/Account/ProfileController.php: -------------------------------------------------------------------------------- 1 | new AuthUserResource(auth()->user()), 17 | ]); 18 | } 19 | 20 | public function update(UpdateAuthUserRequest $request) 21 | { 22 | (new UpdateAuthUser)->update($request->user(), $request->validated()); 23 | 24 | return redirect()->back()->success('User updated', 'The user was successfully updated.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticationController.php: -------------------------------------------------------------------------------- 1 | session('notify')]); 18 | } 19 | 20 | public function store(LoginRequest $request): RedirectResponse 21 | { 22 | $request->authenticate(); 23 | 24 | $request->session()->regenerate(); 25 | 26 | return redirect()->intended(RouteServiceProvider::HOME); 27 | } 28 | 29 | public function destroy(Request $request): RedirectResponse 30 | { 31 | auth()->guard('web')->logout(); 32 | 33 | $request->session()->invalidate(); 34 | $request->session()->regenerateToken(); 35 | 36 | return redirect()->route('auth.login.form'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | when($request->has('users'), function (Collection $collection) { 19 | return $collection->put('users', User::userDropdownValues()); 20 | }); 21 | 22 | $dropdowns->when($request->has('clients'), function (Collection $collection) { 23 | return $collection->put('clients', User::clientDropdownValues()); 24 | }); 25 | 26 | $dropdowns->when($request->has('mentionProjectUsers'), function (Collection $collection) use ($request) { 27 | $project = Project::findOrFail($request->projectId); 28 | $users = PermissionService::usersWithAccessToProject($project); 29 | 30 | return $collection->put('mentionProjectUsers', $users->pluck('name')); 31 | }); 32 | 33 | return response()->json($dropdowns); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/Invoice/InvoiceTasksController.php: -------------------------------------------------------------------------------- 1 | json([ 15 | 'projectTasks' => Project::whereIn('id', $request->get('projectIds', [])) 16 | ->with(['tasks' => function ($query) use ($request) { 17 | $query->whereNotNull('completed_at') 18 | ->where('billable', true) 19 | ->where(function ($query) use ($request) { 20 | $query->whereNull('invoice_id') 21 | ->when($request->invoiceId, fn ($query) => $query->orWhere('invoice_id', $request->invoiceId)); 22 | }) 23 | ->with(['labels:id,name,color']) 24 | ->withSum('timeLogs AS total_minutes', 'minutes'); 25 | }]) 26 | ->get(), 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Settings/OwnerCompanyController.php: -------------------------------------------------------------------------------- 1 | authorize('view', OwnerCompany::class); 18 | 19 | return Inertia::render('Settings/Company/Edit', [ 20 | 'item' => OwnerCompany::first(), 21 | 'dropdowns' => [ 22 | 'countries' => Country::dropdownValues(), 23 | 'currencies' => Currency::dropdownValues(), 24 | ], 25 | ]); 26 | } 27 | 28 | public function update(UpdateOwnerCompanyRequest $request) 29 | { 30 | $this->authorize('update', OwnerCompany::class); 31 | 32 | (new UpdateOwnerCompany)->update($request); 33 | 34 | return redirect()->back()->success('Company updated', 'The company was successfully updated.'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/Task/AttachmentController.php: -------------------------------------------------------------------------------- 1 | uploadAttachments($task, $request->attachments); 20 | 21 | return response()->json(['files' => $files]); 22 | } 23 | 24 | public function destroy(Project $project, Task $task, Attachment $attachment): JsonResponse 25 | { 26 | File::delete(public_path($attachment->path)); 27 | File::delete(public_path($attachment->thumb)); 28 | 29 | $attachment->delete(); 30 | 31 | AttachmentDeleted::dispatch($task, $attachment->id); 32 | 33 | return response()->json(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/Task/CommentController.php: -------------------------------------------------------------------------------- 1 | authorize('viewAny', [Comment::class, $project]); 17 | 18 | return response()->json( 19 | $task->comments()->with(['user:id,name,avatar,job_title'])->latest()->get(), 20 | ); 21 | } 22 | 23 | public function store(StoreCommentRequest $request, Project $project, Task $task) 24 | { 25 | $this->authorize('create', [Comment::class, $project]); 26 | 27 | $comment = $task->comments()->create( 28 | $request->validated() + ['user_id' => auth()->id()] 29 | ); 30 | 31 | CommentCreated::dispatch($comment); 32 | 33 | return response()->json(['comment' => $comment->load(['user:id,name,avatar,job_title'])]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('auth.login.form'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 24 | return redirect(RouteServiceProvider::HOME); 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts(): array 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Client/StoreClientRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'name' => 'required|string', 28 | 'phone' => 'string|nullable', 29 | 'email' => ['required', 'email', Rule::unique('users')], 30 | 'password' => 'required|min:8|confirmed', 31 | 'avatar' => [File::image(), 'nullable'], 32 | 'companies' => 'array', 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Requests/Client/UpdateClientRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'name' => 'required|string', 28 | 'phone' => 'string|nullable', 29 | 'email' => ['required', 'email', Rule::unique('users')->ignore($this->route('user')->id)], 30 | 'password' => 'nullable|min:8|confirmed', 31 | 'avatar' => [File::image(), 'nullable'], 32 | 'companies' => 'required|array|min:1', 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Requests/Comment/StoreCommentRequest.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'content' => ['required', 'min:8'], 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Requests/Label/StoreLabelRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => 'required|string', 27 | 'color' => ['required', 'string', new HexColor], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Requests/Label/UpdateLabelRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => 'required|string', 27 | 'color' => ['required', 'string', new HexColor], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Requests/OwnerCompany/UpdateOwnerCompanyRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'name' => 'required|string', 28 | 'logo' => [ 29 | File::image() 30 | ->max(12 * 1024) 31 | ->dimensions(Rule::dimensions()->ratio(15 / 4)), 32 | 'nullable', 33 | ], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Requests/Project/StoreProjectRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => ['required', 'string', Rule::unique('projects', 'name')], 27 | 'description' => 'string|nullable', 28 | 'client_company_id' => 'required|integer|exists:client_companies,id', 29 | 'rate' => 'numeric|min:0|nullable', 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Requests/Project/UpdateProjectRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => ['required', 'string', Rule::unique('projects', 'name')->ignore($this->route('project')->id)], 27 | 'description' => 'string|nullable', 28 | 'client_company_id' => 'required|integer|exists:client_companies,id', 29 | 'rate' => 'numeric|min:0|nullable', 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Requests/Role/StoreRoleRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => ['required', 'string', Rule::unique('roles')], 27 | 'permissions' => ['required', 'array'], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Requests/Role/UpdateRoleRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => ['required', 'string', Rule::unique('roles')->ignore($this->route('role')->id)], 27 | 'permissions' => ['required', 'array'], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Requests/Task/StoreTaskRequest.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'name' => ['required', 'string:255'], 26 | 'group_id' => ['required', 'exists:task_groups,id'], 27 | 'assigned_to_user_id' => ['nullable', 'exists:users,id'], 28 | 'description' => ['nullable'], 29 | 'estimation' => ['nullable'], 30 | 'due_on' => ['nullable'], 31 | 'hidden_from_clients' => ['required', 'boolean'], 32 | 'billable' => ['required', 'boolean'], 33 | 'subscribed_users' => ['array'], 34 | 'labels' => ['array'], 35 | 'attachments' => ['array'], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Requests/Task/UpdateTaskRequest.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'name' => ['string:255'], 26 | 'group_id' => ['exists:task_groups,id'], 27 | 'assigned_to_user_id' => ['nullable', 'exists:users,id'], 28 | 'description' => ['nullable'], 29 | 'estimation' => ['nullable'], 30 | 'due_on' => ['nullable'], 31 | 'hidden_from_clients' => ['boolean'], 32 | 'billable' => ['boolean'], 33 | 'subscribed_users' => ['array'], 34 | 'labels' => ['array'], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Requests/TaskGroup/StoreTaskGroupRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => [ 27 | 'required', 28 | 'string', 29 | Rule::unique('task_groups', 'name') 30 | ->where('project_id', $this->route('project')->id), 31 | ], 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Requests/TaskGroup/UpdateTaskGroupRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => [ 27 | 'required', 28 | 'string', 29 | Rule::unique('task_groups', 'name') 30 | ->where('project_id', $this->route('project')->id) 31 | ->ignore($this->route('taskGroup')->id), 32 | ], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Requests/TimeLog/StoreTimeLogRequest.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'minutes' => ['required', 'integer'], 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Requests/User/StoreUserRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'job_title' => 'required|string', 28 | 'name' => 'required|string', 29 | 'phone' => 'string|nullable', 30 | 'rate' => 'numeric|min:0', 31 | 'email' => ['required', 'email', Rule::unique('users')], 32 | 'password' => 'required|min:8|confirmed', 33 | 'roles' => 'required|array|min:1', 34 | 'avatar' => [File::image(), 'nullable'], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Requests/User/UpdateAuthUserRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'job_title' => 'required|string', 28 | 'name' => 'required|string', 29 | 'phone' => 'string|nullable', 30 | 'email' => ['required', 'email', Rule::unique('users')->ignore(auth()->id())], 31 | 'password' => 'nullable|min:8|confirmed', 32 | 'avatar' => [File::image(), 'nullable'], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Requests/User/UpdateUserRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'job_title' => 'required|string', 28 | 'name' => 'required|string', 29 | 'phone' => 'string|nullable', 30 | 'rate' => 'numeric|min:0', 31 | 'email' => ['required', 'email', Rule::unique('users')->ignore($this->route('user')->id)], 32 | 'password' => 'nullable|min:8|confirmed', 33 | 'roles' => 'required|array|min:1', 34 | 'avatar' => [File::image(), 'nullable'], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Resources/Activity/ActivityGroupedByDateCollection.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return $this 18 | ->collection 19 | ->map(function ($activity) { 20 | return [ 21 | 'id' => $activity->id, 22 | 'activity_capable' => $activity->activityCapable, 23 | 'project' => $activity->project, 24 | 'title' => $activity->title, 25 | 'subtitle' => $activity->subtitle, 26 | 'created_at' => $activity->created_at, 27 | 'date' => $activity->created_at->format('F j, Y'), 28 | ]; 29 | }) 30 | ->groupBy('date') 31 | ->toArray(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Resources/Client/ClientResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->email, 21 | 'companies' => $this->clientCompanies->map->only(['id', 'name']), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Resources/ClientCompany/ClientCompanyResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->email, 21 | 'address' => $this->address, 22 | 'postal_code' => $this->postal_code, 23 | 'city' => $this->city, 24 | 'country_id' => $this->country_id, 25 | 'currency_id' => $this->currency_id, 26 | 'phone' => $this->phone, 27 | 'web' => $this->web, 28 | 'iban' => $this->iban, 29 | 'swift' => $this->swift, 30 | 'business_id' => $this->business_id, 31 | 'tax_id' => $this->tax_id, 32 | 'vat' => $this->vat, 33 | 'rate' => $this->rate, 34 | 'currency' => $this->currency, 35 | 'clients' => $this->clients->map->only(['id', 'name']), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Resources/Invoice/InvoiceResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'number' => $this->number, 20 | 'client_company' => $this->clientCompany, 21 | 'created_by_user' => $this->createdByUser, 22 | 'status' => $this->status, 23 | 'type' => $this->type, 24 | 'amount' => $this->amount, 25 | 'amount_with_tax' => $this->amount_with_tax, 26 | 'hourly_rate' => $this->hourly_rate, 27 | 'due_date' => $this->due_date, 28 | 'note' => $this->note, 29 | 'filename' => $this->filename, 30 | 'created_at' => $this->created_at, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Resources/Label/LabelResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'name' => $this->name, 20 | 'color' => $this->color, 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Resources/Notification/NotificationGroupedByDateCollection.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return $this 18 | ->collection 19 | ->map(function ($notification) { 20 | return [ 21 | 'id' => $notification->id, 22 | 'title' => $notification->data['title'], 23 | 'subtitle' => $notification->data['subtitle'], 24 | 'link' => $notification->data['link'], 25 | 'read_at' => $notification->read_at, 26 | 'created_at' => $notification->created_at, 27 | 'date' => $notification->created_at->format('F j, Y'), 28 | ]; 29 | }) 30 | ->groupBy('date') 31 | ->toArray(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Resources/Notification/NotificationResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'title' => $this->data['title'], 20 | 'subtitle' => $this->data['subtitle'], 21 | 'link' => $this->data['link'], 22 | 'read_at' => $this->read_at, 23 | 'created_at' => $this->created_at, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Resources/Project/ProjectResource.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function toArray(Request $request): array 17 | { 18 | return [ 19 | 'id' => $this->id, 20 | 'name' => $this->name, 21 | 'description' => $this->description, 22 | 'favorite' => $this->favorite, 23 | 'client_company' => $this->clientCompany->only(['id', 'name']), 24 | 'users_with_access' => PermissionService::usersWithAccessToProject($this), 25 | 'all_tasks_count' => $this->all_tasks_count, 26 | 'completed_tasks_count' => $this->completed_tasks_count, 27 | 'overdue_tasks_count' => $this->overdue_tasks_count, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Resources/Role/RoleResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'name' => $this->name, 20 | 'permissions_count' => $this->permissions_count, 21 | 'permissions' => $this->permissions->pluck('name'), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Resources/User/AuthUserResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->email, 21 | 'job_title' => $this->job_title, 22 | 'avatar' => $this->avatar, 23 | 'phone' => $this->phone, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Resources/User/UserResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->email, 21 | 'job_title' => $this->job_title, 22 | 'avatar' => $this->avatar, 23 | 'phone' => $this->phone, 24 | 'rate' => $this->rate, 25 | 'roles' => $this->roles->map->only('name')->flatten()->toArray(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Listeners/SendEmailWithCredentials.php: -------------------------------------------------------------------------------- 1 | user->notify( 16 | new UserCreatedNotification($event->password) 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Models/Activity.php: -------------------------------------------------------------------------------- 1 | setQueryName('project'), 30 | ]); 31 | } 32 | 33 | public function project(): BelongsTo 34 | { 35 | return $this->belongsTo(Project::class); 36 | } 37 | 38 | public function user(): BelongsTo 39 | { 40 | return $this->belongsTo(User::class); 41 | } 42 | 43 | public function activityCapable(): MorphTo 44 | { 45 | return $this->morphTo(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Models/Attachment.php: -------------------------------------------------------------------------------- 1 | belongsTo(Task::class); 23 | } 24 | 25 | public function user(): BelongsTo 26 | { 27 | return $this->belongsTo(User::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Models/Comment.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 20 | } 21 | 22 | public function task(): BelongsTo 23 | { 24 | return $this->belongsTo(Task::class); 25 | } 26 | 27 | public function activities(): MorphMany 28 | { 29 | return $this->morphMany(Activity::class, 'activity_capable'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Models/Country.php: -------------------------------------------------------------------------------- 1 | get(['id', 'name']) 13 | ->map(fn ($i) => ['value' => (string) $i->id, 'label' => $i->name]) 14 | ->toArray(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Models/Currency.php: -------------------------------------------------------------------------------- 1 | hasMany(ClientCompany::class); 13 | } 14 | 15 | public static function dropdownValues($options = []): array 16 | { 17 | return self::orderBy('name') 18 | ->when(isset($options['with']), fn ($query) => $query->with($options['with'])) 19 | ->get() 20 | ->map(fn ($i) => array_merge([ 21 | 'value' => (string) $i->id, 22 | 'label' => "{$i->name} ({$i->symbol})", 23 | ], isset($options['with']) ? $i->toArray() : [])) 24 | ->toArray(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Models/Filters/IsNullFilter.php: -------------------------------------------------------------------------------- 1 | whereNull($this->field); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Models/Filters/TaskCompletedFilter.php: -------------------------------------------------------------------------------- 1 | values[0]) { 15 | 'completed' => $query->whereNotNull('completed_at'), 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Models/Filters/TaskOverdueFilter.php: -------------------------------------------------------------------------------- 1 | whereDate($this->field, '<', now()) 15 | ->whereNull('completed_at'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Models/Filters/WhereHasFilter.php: -------------------------------------------------------------------------------- 1 | whereHas($this->relation, function (Builder $query) { 15 | $query->whereIn('id', $this->values); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Models/Filters/WhereInFilter.php: -------------------------------------------------------------------------------- 1 | whereIn($this->field, $this->values); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Models/Label.php: -------------------------------------------------------------------------------- 1 | 'asc', 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /app/Models/Permission.php: -------------------------------------------------------------------------------- 1 | 'asc', 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /app/Models/TimeLog.php: -------------------------------------------------------------------------------- 1 | 'integer', 22 | ]; 23 | 24 | public function user(): BelongsTo 25 | { 26 | return $this->belongsTo(User::class); 27 | } 28 | 29 | public function task(): BelongsTo 30 | { 31 | return $this->belongsTo(Task::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Observers/CommentObserver.php: -------------------------------------------------------------------------------- 1 | activities()->create([ 15 | 'project_id' => $comment->task->project_id, 16 | 'user_id' => auth()->id(), 17 | 'title' => 'New comment', 18 | 'subtitle' => auth()->user()->name." left a comment on \"{$comment->task->name}\" task", 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Policies/CommentPolicy.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo('view comments') && $user->hasProjectAccess($project); 16 | } 17 | 18 | /** 19 | * Determine whether the user can create models. 20 | */ 21 | public function create(User $user, Project $project): bool 22 | { 23 | return $user->hasPermissionTo('view comments') && $user->hasProjectAccess($project); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Policies/LabelPolicy.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo('view labels'); 16 | } 17 | 18 | /** 19 | * Determine whether the user can create models. 20 | */ 21 | public function create(User $user): bool 22 | { 23 | return $user->hasPermissionTo('create label'); 24 | } 25 | 26 | /** 27 | * Determine whether the user can update the model. 28 | */ 29 | public function update(User $user, Label $label): bool 30 | { 31 | return $user->hasPermissionTo('edit label'); 32 | } 33 | 34 | /** 35 | * Determine whether the user can delete the model. 36 | */ 37 | public function delete(User $user, Label $label): bool 38 | { 39 | return $user->hasPermissionTo('archive label'); 40 | } 41 | 42 | /** 43 | * Determine whether the user can restore the model. 44 | */ 45 | public function restore(User $user, Label $label): bool 46 | { 47 | return $user->hasPermissionTo('restore label'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Policies/OwnerCompanyPolicy.php: -------------------------------------------------------------------------------- 1 | can('view owner company'); 15 | } 16 | 17 | /** 18 | * Determine whether the user can update the model. 19 | */ 20 | public function update(User $user): bool 21 | { 22 | return $user->can('edit owner company'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Policies/RolePolicy.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo('view roles'); 16 | } 17 | 18 | /** 19 | * Determine whether the user can create models. 20 | */ 21 | public function create(User $user): bool 22 | { 23 | return $user->hasPermissionTo('create role'); 24 | } 25 | 26 | /** 27 | * Determine whether the user can update the model. 28 | */ 29 | public function update(User $user, Role $role): bool 30 | { 31 | return $user->hasPermissionTo('edit role') && $role->name !== 'admin'; 32 | } 33 | 34 | /** 35 | * Determine whether the user can delete the model. 36 | */ 37 | public function delete(User $user, Role $role): bool 38 | { 39 | return $user->hasPermissionTo('archive role') && $role->name !== 'admin'; 40 | } 41 | 42 | /** 43 | * Determine whether the user can restore the model. 44 | */ 45 | public function restore(User $user, Role $role): bool 46 | { 47 | return $user->hasPermissionTo('restore role'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Policies/TimeLogPolicy.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo('add time log') && $user->hasProjectAccess($project); 17 | } 18 | 19 | /** 20 | * Determine whether the user can delete the model. 21 | */ 22 | public function delete(User $user, TimeLog $timeLog, Project $project): bool 23 | { 24 | return $user->hasPermissionTo('delete time log') && $user->hasProjectAccess($project); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Policies/UserPolicy.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo('view users'); 15 | } 16 | 17 | /** 18 | * Determine whether the user can create models. 19 | */ 20 | public function create(User $user): bool 21 | { 22 | return $user->hasPermissionTo('create user'); 23 | } 24 | 25 | /** 26 | * Determine whether the user can update the model. 27 | */ 28 | public function update(User $user, User $model): bool 29 | { 30 | return $user->hasPermissionTo('edit user'); 31 | } 32 | 33 | /** 34 | * Determine whether the user can delete the model. 35 | */ 36 | public function delete(User $user, User $model): bool 37 | { 38 | return $user->hasPermissionTo('archive user'); 39 | } 40 | 41 | /** 42 | * Determine whether the user can restore the model. 43 | */ 44 | public function restore(User $user, User $model): bool 45 | { 46 | return $user->hasPermissionTo('restore user'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | with('flash', ['type' => $type, 'title' => $title, 'message' => $message]); 31 | } 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | protected $policies = [ 19 | // 20 | ]; 21 | 22 | /** 23 | * Register any authentication / authorization services. 24 | */ 25 | public function boot(): void 26 | { 27 | // Gate::before(function ($user, $ability) { 28 | // return $user->hasRole('admin') ? true : null; 29 | // }); 30 | 31 | ResetPassword::createUrlUsing(function (User $user, string $token) { 32 | return route('auth.newPassword.form', ['token' => $token]); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | isValid($value)) { 18 | $fail('The hex color is not valid.'); 19 | } 20 | } 21 | 22 | private function isValid(string $value): bool 23 | { 24 | return (bool) preg_match('/^#([a-fA-F0-9]{6})$/', $value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Services/NotificationService.php: -------------------------------------------------------------------------------- 1 | check()) { 10 | return null; 11 | } 12 | /** @var User */ 13 | $user = auth()->user(); 14 | 15 | return $user 16 | ->notifications() 17 | ->latest() 18 | ->limit($limit) 19 | ->get() 20 | ->map(function ($notification) { 21 | return [ 22 | ...$notification->data, 23 | 'id' => $notification->id, 24 | 'read_at' => $notification->read_at, 25 | ]; 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Services/ProjectService.php: -------------------------------------------------------------------------------- 1 | where('project_id', $this->project->id) 16 | ->delete(); 17 | 18 | return DB::table('project_user_access') 19 | ->insert( 20 | collect($userIds) 21 | ->map(fn ($id) => ['user_id' => $id, 'project_id' => $this->project->id]) 22 | ->toArray() 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Services/UserMentionService.php: -------------------------------------------------------------------------------- 1 | contains('data-type="mention"'); 17 | } 18 | 19 | public static function getUsersFromMentions(string $content, Project $project): Collection 20 | { 21 | $users = PermissionService::usersWithAccessToProject($project); 22 | 23 | $foundUsers = $users->filter(function ($user) use ($content) { 24 | return Str::of($content)->contains("@{$user['name']}"); 25 | }); 26 | 27 | return User::findMany($foundUsers->pluck('id')); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/favorite.php: -------------------------------------------------------------------------------- 1 | false, 8 | 9 | /* 10 | * User tables foreign key name. 11 | */ 12 | 'user_foreign_key' => 'user_id', 13 | 14 | /* 15 | * Table name for favorites records. 16 | */ 17 | 'favorites_table' => 'favorites', 18 | 19 | /* 20 | * Model name for favorite record. 21 | */ 22 | 'favorite_model' => Overtrue\LaravelFavorite\Favorite::class, 23 | ]; 24 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | 'google' => [ 35 | 'client_id' => env('GOOGLE_CLIENT_ID'), 36 | 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 37 | 'redirect' => env('GOOGLE_REDIRECT_URI'), 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/ClientCompanyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ClientCompanyFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake()->company, 21 | 'address' => fake()->address, 22 | 'postal_code' => fake()->postcode, 23 | 'city' => fake()->city, 24 | 'country_id' => fake()->numberBetween(1, 249), 25 | 'currency_id' => 97, 26 | 'phone' => fake()->phoneNumber, 27 | 'web' => 'https://company.com', 28 | 'email' => fake()->email, 29 | 'iban' => fake()->iban, 30 | 'swift' => fake()->swiftBicNumber, 31 | 'business_id' => '111111111', 32 | 'tax_id' => '222222222', 33 | 'vat' => '333333333', 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/factories/ClientFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ClientFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | // 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UserFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition(): array 19 | { 20 | return [ 21 | 'name' => fake()->name(), 22 | 'email' => fake()->unique()->safeEmail(), 23 | 'phone' => fake()->phoneNumber(), 24 | 'rate' => fake()->numberBetween(10, 50) * 100, 25 | 'job_title' => fake()->randomElement(['Frontend Developer', 'Backend Developer', 'Fullstack Developer', 'Designer', 'Manager', 'Client']), 26 | 'avatar' => null, 27 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 28 | 'remember_token' => Str::random(10), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->string('password'); 19 | $table->string('avatar')->nullable(); 20 | $table->string('phone')->nullable(); 21 | $table->string('job_title')->nullable(); 22 | $table->unsignedInteger('rate')->nullable(); 23 | $table->string('google_id')->nullable(); 24 | $table->rememberToken(); 25 | $table->timestamps(); 26 | $table->archivedAt(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('users'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 16 | $table->string('token'); 17 | $table->timestamp('created_at')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('password_reset_tokens'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2018_12_14_000000_create_favorites_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->unsignedBigInteger(config('favorite.user_foreign_key'))->index()->comment('user_id'); 17 | $table->morphs('favoriteable'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down() 26 | { 27 | Schema::dropIfExists(config('favorite.favorites_table')); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2023_10_31_105255_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('jobs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_10_31_113749_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('uuid')->unique(); 17 | $table->text('connection'); 18 | $table->text('queue'); 19 | $table->longText('payload'); 20 | $table->longText('exception'); 21 | $table->timestamp('failed_at')->useCurrent(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('failed_jobs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_11_01_120111_create_labels_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('color'); 18 | $table->archivedAt(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('labels'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_11_01_182514_add_archived_at_to_roles.php: -------------------------------------------------------------------------------- 1 | archivedAt(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('roles', function (Blueprint $table) { 25 | $table->dropColumn('archived_at'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2023_11_02_165827_create_currencies_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name', 120); 17 | $table->string('code', 3); 18 | $table->string('symbol', 5); 19 | $table->smallInteger('decimals'); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('currencies'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_11_03_134217_create_countries_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('countries'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2023_11_04_104543_create_client_company_pivot_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('client_id'); 16 | $table->unsignedBigInteger('client_company_id'); 17 | 18 | $table->foreign('client_id')->references('id')->on('users'); 19 | $table->foreign('client_company_id')->references('id')->on('client_companies'); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('client_company'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_11_06_094257_create_projects_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->unsignedBigInteger('client_company_id'); 17 | $table->string('name'); 18 | $table->text('description')->nullable(); 19 | $table->timestamps(); 20 | $table->archivedAt(); 21 | 22 | $table->foreign('client_company_id')->references('id')->on('client_companies'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('projects'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2023_11_06_153749_create_project_user_access.php: -------------------------------------------------------------------------------- 1 | foreignId('user_id'); 16 | $table->foreignId('project_id'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('project_user_access'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2023_11_07_131704_create_task_groups_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('project_id'); 17 | $table->string('name'); 18 | $table->unsignedInteger('order_column'); 19 | $table->archivedAt(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('task_groups'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_11_10_144123_create_label_task_pivot_table.php: -------------------------------------------------------------------------------- 1 | foreignId('label_id'); 16 | $table->foreignId('task_id'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('label_task'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2023_11_15_220141_create_subscribe_task.php: -------------------------------------------------------------------------------- 1 | foreignId('user_id'); 16 | $table->foreignId('task_id'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('subscribe_task'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2023_11_15_220222_create_attachments.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('task_id'); 17 | $table->foreignId('user_id'); 18 | $table->string('name'); 19 | $table->string('path'); 20 | $table->string('thumb')->nullable(); 21 | $table->string('type'); 22 | $table->integer('size'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('attachments'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_11_16_144304_create_notifications_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->string('type'); 17 | $table->morphs('notifiable'); 18 | $table->text('data'); 19 | $table->timestamp('read_at')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('notifications'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2023_11_17_211110_create_time_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('task_id'); 17 | $table->foreignId('user_id'); 18 | $table->unsignedSmallInteger('minutes')->nullable(); 19 | $table->unsignedInteger('timer_start')->nullable(); 20 | $table->unsignedInteger('timer_stop')->nullable(); 21 | $table->timestamp('created_at')->nullable(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('time_logs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_11_18_193550_create_comments_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id'); 17 | $table->foreignId('task_id'); 18 | $table->text('content'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('comments'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_11_28_155542_create_activities.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('project_id'); 17 | $table->foreignId('user_id'); 18 | $table->string('title'); 19 | $table->string('subtitle'); 20 | $table->foreignId('activity_capable_id'); 21 | $table->string('activity_capable_type'); 22 | $table->timestamp('created_at'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('activities'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2024_01_30_190158_add_rate_to_projects.php: -------------------------------------------------------------------------------- 1 | unsignedInteger('rate')->nullable()->after('description'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('projects', function (Blueprint $table) { 25 | $table->dropColumn('rate'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/seeders/ClientCompanySeeder.php: -------------------------------------------------------------------------------- 1 | get() 18 | ->each(function (User $client) { 19 | ClientCompany::factory()->create()->clients()->attach($client); 20 | }); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/seeders/ClientSeeder.php: -------------------------------------------------------------------------------- 1 | create(['job_title' => 'Client']) 17 | ->each 18 | ->assignRole('client'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 17 | RoleSeeder::class, 18 | PermissionSeeder::class, 19 | LabelSeeder::class, 20 | CurrencySeeder::class, 21 | CountrySeeder::class, 22 | ]); 23 | 24 | if ($this->command->confirm('Seed development data?', false)) { 25 | $this->call([ 26 | UserSeeder::class, 27 | OwnerCompanySeeder::class, 28 | ClientSeeder::class, 29 | ClientCompanySeeder::class, 30 | ]); 31 | 32 | auth()->setUser(User::role('admin')->first()); 33 | 34 | $this->call([ 35 | ProjectSeeder::class, 36 | TaskGroupSeeder::class, 37 | TasksSeeder::class, 38 | ]); 39 | } else { 40 | $this->call([ProductionSeeder::class]); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/seeders/LabelSeeder.php: -------------------------------------------------------------------------------- 1 | 'Confirmed', 'color' => '#37B24D'], 17 | ['name' => 'Estimate', 'color' => '#AE3EC9'], 18 | ['name' => 'Blocked', 'color' => '#F03E3E'], 19 | ['name' => 'Bug', 'color' => '#D6336C'], 20 | ['name' => 'Rework', 'color' => '#F76707'], 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/seeders/OwnerCompanySeeder.php: -------------------------------------------------------------------------------- 1 | fake()->company, 17 | 'logo' => null, 18 | 'address' => fake()->streetAddress, 19 | 'postal_code' => fake()->postcode, 20 | 'city' => fake()->city, 21 | 'country_id' => fake()->numberBetween(1, 249), 22 | 'currency_id' => 97, 23 | 'phone' => fake()->phoneNumber, 24 | 'web' => 'https://company.com', 25 | 'tax' => 1000, // 10% 26 | 'email' => fake()->email, 27 | 'iban' => fake()->iban, 28 | 'swift' => fake()->swiftBicNumber, 29 | 'business_id' => '111111111', 30 | 'tax_id' => '222222222', 31 | 'vat' => '333333333', 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/seeders/ProjectSeeder.php: -------------------------------------------------------------------------------- 1 | 'Demo Project', 20 | 'description' => fake()->sentence(), 21 | 'client_company_id' => ClientCompany::first()->id, 22 | ]); 23 | 24 | $projects[] = Project::create([ 25 | 'name' => 'Demo Project 2', 26 | 'description' => fake()->sentence(), 27 | 'client_company_id' => ClientCompany::oldest()->first()->id, 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/seeders/RoleSeeder.php: -------------------------------------------------------------------------------- 1 | $role]); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/seeders/TaskGroupSeeder.php: -------------------------------------------------------------------------------- 1 | taskGroups()->createMany([ 19 | ['name' => 'Backlog'], 20 | ['name' => 'Todo'], 21 | ['name' => 'In progress'], 22 | ['name' => 'QA'], 23 | ['name' => 'Done'], 24 | ['name' => 'Deployed'], 25 | ]); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "public"] 9 | } 10 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel" 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {}, 4 | 'postcss-preset-mantine': {}, 5 | 'postcss-simple-vars': { 6 | variables: { 7 | 'mantine-breakpoint-xs': '36em', 8 | 'mantine-breakpoint-sm': '48em', 9 | 'mantine-breakpoint-md': '62em', 10 | 'mantine-breakpoint-lg': '75em', 11 | 'mantine-breakpoint-xl': '88em', 12 | }, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=140ed4bc5b10bc99492b97668c59272d", 3 | "/app-dark.css": "/app-dark.css?id=b11fa9a28e9d3aeb8c92986f319b3c44", 4 | "/app.css": "/app.css?id=b3ccfbe68f24cff776f83faa8dead721" 5 | } 6 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | :root[data-mantine-color-scheme=light] { 2 | --mantine-color-text: #343d43; 3 | --mantine-color-gray-filled: var(--mantine-color-gray-3) !important; 4 | --mantine-color-body: var(--mantine-color-gray-0); 5 | --mantine-color-anchor: var(--mantine-color-blue-7); 6 | } -------------------------------------------------------------------------------- /resources/docs/banner.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/banner.afphoto -------------------------------------------------------------------------------- /resources/docs/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/banner.jpg -------------------------------------------------------------------------------- /resources/docs/screenshots/Activity - dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Activity - dark.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Activity - light.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Activity - light.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Dashboard - dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Dashboard - dark.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Dashboard - light.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Dashboard - light.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Invoice - dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Invoice - dark.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Invoice - light.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Invoice - light.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/My tasks - dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/My tasks - dark.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/My tasks - light.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/My tasks - light.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Project tasks - dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Project tasks - dark.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Project tasks - light.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Project tasks - light.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Projects - dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Projects - dark.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Projects - light.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Projects - light.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Task - dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Task - dark.jpeg -------------------------------------------------------------------------------- /resources/docs/screenshots/Task - light.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/lara-collab-react-laravel/1cd1cd9faad3954a3a2a87b35d09f34db37f6eec/resources/docs/screenshots/Task - light.jpeg -------------------------------------------------------------------------------- /resources/js/components/ActionButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mantine/core"; 2 | 3 | export default function ActionButton({ children, ...props }) { 4 | return ( 5 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /resources/js/components/ArchivedFilterButton.jsx: -------------------------------------------------------------------------------- 1 | import { reloadWithQuery, reloadWithoutQueryParams } from "@/utils/route"; 2 | import { ActionIcon, Tooltip } from "@mantine/core"; 3 | import { useDidUpdate, useDisclosure } from "@mantine/hooks"; 4 | import { IconArchive } from "@tabler/icons-react"; 5 | 6 | export default function ArchivedFilterButton() { 7 | const [selected, { toggle }] = useDisclosure( 8 | route().params?.archived !== undefined, 9 | ); 10 | 11 | useDidUpdate(() => { 12 | if (selected) reloadWithQuery({ archived: 1 }); 13 | else reloadWithoutQueryParams({ exclude: ["archived"] }); 14 | }, [selected]); 15 | 16 | return ( 17 | 18 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /resources/js/components/BackButton.jsx: -------------------------------------------------------------------------------- 1 | import { redirectTo } from "@/utils/route"; 2 | import { Button } from "@mantine/core"; 3 | import { IconChevronLeft } from "@tabler/icons-react"; 4 | 5 | export default function BackButton({ route }) { 6 | return ( 7 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /resources/js/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import { Card as MantineCard } from "@mantine/core"; 2 | import classes from "./css/Card.module.css"; 3 | 4 | export default function Card({ children, ...props }) { 5 | return ( 6 | 7 | {children} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /resources/js/components/ClearFiltersButton.jsx: -------------------------------------------------------------------------------- 1 | import useTaskFiltersStore from "@/hooks/store/useTaskFiltersStore"; 2 | import { ActionIcon, Tooltip } from "@mantine/core"; 3 | import { IconX } from "@tabler/icons-react"; 4 | 5 | export default function ClearFiltersButton() { 6 | const { hasUrlParams, clearFilters } = useTaskFiltersStore(); 7 | const usingFilters = hasUrlParams(["archived"]); 8 | 9 | return ( 10 | 11 | clearFilters({ keep: ["archived"] })} 16 | > 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/components/ConfirmModal.jsx: -------------------------------------------------------------------------------- 1 | import { Text } from "@mantine/core"; 2 | import { openConfirmModal as openMantineConfirmModal } from "@mantine/modals"; 3 | 4 | export const openConfirmModal = ({ 5 | type = "info", 6 | title, 7 | content, 8 | confirmLabel, 9 | cancelLabel = "Cancel", 10 | ...props 11 | }) => { 12 | const typeColors = { 13 | info: "blue", 14 | warning: "orange", 15 | danger: "red", 16 | }; 17 | 18 | openMantineConfirmModal({ 19 | title: ( 20 | 21 | {title} 22 | 23 | ), 24 | centered: true, 25 | overlayProps: { backgroundOpacity: 0.55, blur: 3 }, 26 | children: {content}, 27 | labels: { confirm: confirmLabel, cancel: cancelLabel }, 28 | confirmProps: { color: typeColors[type] }, 29 | ...props, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /resources/js/components/EmptyResult.jsx: -------------------------------------------------------------------------------- 1 | import { Flex, Text, Title, useComputedColorScheme } from "@mantine/core"; 2 | 3 | export function EmptyResult({ title, subtitle }) { 4 | const computedColorScheme = useComputedColorScheme(); 5 | 6 | return ( 7 | 16 | 17 | {title} 18 | 19 | 20 | {subtitle} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /resources/js/components/EmptyWithIcon.jsx: -------------------------------------------------------------------------------- 1 | import { Group, Text, rem } from "@mantine/core"; 2 | 3 | export default function EmptyWithIcon({ 4 | title, 5 | subtitle, 6 | icon: Icon, 7 | titleFontSize = 22, 8 | subtitleFontSize = 14, 9 | iconSize = 50, 10 | opacity = 0.6, 11 | }) { 12 | return ( 13 | 14 | 20 |
21 | 22 | {title} 23 | 24 | 25 | {subtitle} 26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /resources/js/components/ImageModal.jsx: -------------------------------------------------------------------------------- 1 | import useImageLoader from "@/hooks/useImageLoader"; 2 | import { Center, Image, Loader, Modal } from "@mantine/core"; 3 | import { useEffect } from "react"; 4 | 5 | export default function ImageModal({ image, opened, close }) { 6 | const { loadImage, loading } = useImageLoader(); 7 | 8 | useEffect(() => { 9 | image && loadImage(image.path); 10 | }, [image]); 11 | 12 | return ( 13 | 24 | {loading ? ( 25 |
26 | 27 |
28 | ) : ( 29 | 36 | )} 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /resources/js/components/Label.jsx: -------------------------------------------------------------------------------- 1 | import { ColorSwatch, Group, Text } from "@mantine/core"; 2 | 3 | export function Label({ name, color, size = 10, dot = true }) { 4 | return ( 5 | 6 | {dot === true && } 7 | 8 | {name} 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /resources/js/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import { Center, Group, Text, rem, useComputedColorScheme } from "@mantine/core"; 2 | import { IconChartArcs } from "@tabler/icons-react"; 3 | 4 | export default function Logo(props) { 5 | const computedColorScheme = useComputedColorScheme(); 6 | 7 | return ( 8 | 9 |
14 | 15 |
16 | 17 | LaraCollab 18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/components/Notification.jsx: -------------------------------------------------------------------------------- 1 | import { dateTime } from "@/utils/datetime"; 2 | import { Group, Text, rem } from "@mantine/core"; 3 | import { IconMessage } from "@tabler/icons-react"; 4 | import classes from "./css/Notification.module.css"; 5 | 6 | export default function Notification({ title, subtitle, datetime, read }) { 7 | return ( 8 | 9 | 13 |
14 | 15 | {title} 16 | 17 | 18 | {`${subtitle}, ${dateTime(datetime)}`} 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/components/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import { Pagination as MantinePagination, Group } from "@mantine/core"; 2 | import { router } from "@inertiajs/react"; 3 | 4 | export default function Pagination({ current, pages, routeName = null }) { 5 | if (!routeName) { 6 | routeName = route().current(); 7 | } 8 | 9 | return ( 10 | 15 | router.get(route(routeName, { ...route().params, page })) 16 | } 17 | > 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /resources/js/components/RichTextEditor/Mention/css/MentionList.css: -------------------------------------------------------------------------------- 1 | .items { 2 | color: light-dark(var(--mantine-color-dark-6), var(--mantine-color-white)); 3 | background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-4)); 4 | border-radius: var(--mantine-radius-md); 5 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1); 6 | font-size: rem(14px); 7 | overflow: hidden; 8 | padding: rem(5px); 9 | position: relative; 10 | } 11 | 12 | .item { 13 | background: transparent; 14 | border: 1px solid transparent; 15 | border-radius: 0.4rem; 16 | display: block; 17 | margin: 0; 18 | padding: rem(3px) rem(8px); 19 | text-align: left; 20 | width: 100%; 21 | } 22 | 23 | .item.is-selected { 24 | border: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7)); 25 | background: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-3)); 26 | } 27 | 28 | .mention { 29 | color: var(--mantine-color-blue-8); 30 | box-decoration-break: clone; 31 | padding: 0 rem(1px); 32 | } -------------------------------------------------------------------------------- /resources/js/components/RoleBadge.jsx: -------------------------------------------------------------------------------- 1 | import useRoles from "@/hooks/useRoles"; 2 | import { Badge } from "@mantine/core"; 3 | 4 | export default function RoleBadge({ role }) { 5 | const { getColor } = useRoles(); 6 | 7 | return ( 8 | 9 | {role} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /resources/js/components/TableHead.jsx: -------------------------------------------------------------------------------- 1 | import { Table } from "@mantine/core"; 2 | import TableHeaderCell from "./TableHeaderCell"; 3 | import useSorting from "@/hooks/useSorting"; 4 | 5 | export default function TableHead({ columns, sort }) { 6 | const [sortBy, reverseSortDirection, setSorting] = useSorting(sort); 7 | 8 | return ( 9 | 10 | 11 | {columns.map((item) => ( 12 | setSorting(item.column)} 19 | > 20 | {item.label} 21 | 22 | ))} 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /resources/js/components/TableRowEmpty.jsx: -------------------------------------------------------------------------------- 1 | import { Table, Text } from "@mantine/core"; 2 | 3 | export default function TableRowEmpty(props) { 4 | return ( 5 | 6 | 7 | 8 | No items were found 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /resources/js/components/TaskGroupLabel.jsx: -------------------------------------------------------------------------------- 1 | import { Pill, useComputedColorScheme } from "@mantine/core"; 2 | import { forwardRef } from "react"; 3 | 4 | export default forwardRef(function TaskGroupLabel(props, ref) { 5 | const computedColorScheme = useComputedColorScheme(); 6 | 7 | return ( 8 | 16 | {props.children} 17 | 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /resources/js/components/css/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | transition: 0.3s ease all; 3 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-body)) !important; 4 | 5 | @mixin hover { 6 | border: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7)); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/js/components/css/FileThumbnail.module.css: -------------------------------------------------------------------------------- 1 | .file { 2 | padding: var(--mantine-spacing-sm); 3 | border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); 4 | border-radius: var(--mantine-radius-md); 5 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7)); 6 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); 7 | 8 | @mixin hover { 9 | color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-white)); 10 | border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); 11 | } 12 | } 13 | 14 | .text { 15 | max-width: calc(95% - 50px - var(--mantine-spacing-sm)); 16 | } 17 | 18 | .iconContainer { 19 | position: relative; 20 | width: rem(45); 21 | height: rem(45); 22 | 23 | @mixin hover { 24 | .remove { 25 | display: block; 26 | } 27 | 28 | .icon { 29 | opacity: 0.15; 30 | } 31 | } 32 | } 33 | 34 | .icon { 35 | opacity: 1; 36 | } 37 | 38 | .remove { 39 | display: none; 40 | position: absolute; 41 | top: 2px; 42 | left: 2px; 43 | width: rem(42); 44 | height: rem(42); 45 | color: var(--mantine-color-red-7); 46 | cursor: pointer; 47 | } 48 | -------------------------------------------------------------------------------- /resources/js/components/css/FlashNotification.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: fixed; 3 | top: 15px; 4 | left: 50%; 5 | z-index: 99999; 6 | min-width: 400px; 7 | max-width: 500px; 8 | } 9 | 10 | .icon { 11 | margin-top: 0; 12 | align-items: normal; 13 | width: calc(2.5rem * var(--mantine-scale)); 14 | height: calc(2.5rem * var(--mantine-scale)); 15 | } 16 | 17 | .title { 18 | margin-bottom: 2px; 19 | } 20 | 21 | .label { 22 | font-size: var(--mantine-font-size-md); 23 | user-select: none; 24 | } 25 | 26 | .message { 27 | font-size: var(--mantine-font-size-xs); 28 | user-select: none; 29 | } 30 | -------------------------------------------------------------------------------- /resources/js/components/css/Notification.module.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | color: var(--mantine-color-blue-7); 3 | } -------------------------------------------------------------------------------- /resources/js/components/css/RichTextEditor.module.css: -------------------------------------------------------------------------------- 1 | .content div { 2 | min-height: var(--rich-text-editor-height); 3 | font-size: rem(14); 4 | line-height: rem(18); 5 | } 6 | -------------------------------------------------------------------------------- /resources/js/components/css/TableHeader.module.css: -------------------------------------------------------------------------------- 1 | .th { 2 | padding: 0; 3 | } 4 | 5 | .control { 6 | width: 100%; 7 | padding: var(--mantine-spacing-xs) var(--mantine-spacing-md); 8 | 9 | @mixin hover { 10 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 11 | } 12 | } 13 | 14 | .icon { 15 | width: rem(21px); 16 | height: rem(21px); 17 | border-radius: rem(21px); 18 | } -------------------------------------------------------------------------------- /resources/js/components/css/UserInfoCard.module.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); 3 | } 4 | 5 | .name { 6 | font-family: Greycliff CF, var(--mantine-font-family); 7 | } -------------------------------------------------------------------------------- /resources/js/hooks/store/taskGroups/TaskGroupWebSocketUpdatesSlice.js: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | 3 | const createTaskWebSocketUpdatesSlice = (set, get) => ({ 4 | addTaskGroupLocally: (taskGroup) => { 5 | return set(produce(state => { 6 | state.groups = [...state.groups, taskGroup]; 7 | })); 8 | }, 9 | updateTaskGroupLocally: (taskGroup) => { 10 | return set(produce(state => { 11 | const index = get().groups.findIndex(i => i.id === taskGroup.id); 12 | state.groups[index] = taskGroup; 13 | })); 14 | }, 15 | removeTaskGroupLocally: (taskGroupId) => { 16 | return set(produce(state => { 17 | state.groups = state.groups.filter(i => i.id !== taskGroupId); 18 | })); 19 | }, 20 | restoreTaskGroupLocally: (taskGroup) => { 21 | return set(produce(state => { 22 | state.groups = [ 23 | taskGroup, 24 | ...state.groups.filter(i => i.id !== taskGroup.id) 25 | ].sort((a, b) => (a.order_column > b.order_column ? 1 : -1)); 26 | })); 27 | }, 28 | reorderTaskGroupLocally: (ids) => { 29 | return set(produce(state => { 30 | state.groups = state.groups.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); 31 | })); 32 | }, 33 | }); 34 | 35 | export default createTaskWebSocketUpdatesSlice; 36 | -------------------------------------------------------------------------------- /resources/js/hooks/store/tasks/TaskCommentsSlice.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { produce } from "immer"; 3 | 4 | const createTaskCommentsSlice = (set, get) => ({ 5 | comments: [], 6 | fetchComments: async (task, onFinish) => { 7 | try { 8 | const { data } = await axios.get(route("projects.tasks.comments", [task.project_id, task.id])); 9 | onFinish(); 10 | 11 | return set(produce(state => {state.comments = data})); 12 | } catch (e) { 13 | onFinish(); 14 | console.error(e); 15 | alert("Failed to load comments"); 16 | } 17 | }, 18 | saveComment: async (task, comment, onFinish) => { 19 | try { 20 | const { data } = await axios.post( 21 | route("projects.tasks.comments.store", [task.project_id, task.id]), 22 | { content: comment }, 23 | { progress: true } 24 | ); 25 | onFinish(); 26 | 27 | return set(produce(state => { 28 | state.comments = [ 29 | data.comment, 30 | ...state.comments, 31 | ]; 32 | })); 33 | } catch (e) { 34 | onFinish(); 35 | console.error(e); 36 | alert("Failed to save comment"); 37 | } 38 | }, 39 | }); 40 | 41 | export default createTaskCommentsSlice; 42 | -------------------------------------------------------------------------------- /resources/js/hooks/store/useTaskDrawerStore.js: -------------------------------------------------------------------------------- 1 | import { replaceUrlWithoutReload } from '@/utils/route'; 2 | import { produce } from 'immer'; 3 | import { create } from 'zustand'; 4 | 5 | const useTaskDrawerStore = create((set, get) => ({ 6 | create: { 7 | opened: false, 8 | group_id: null, 9 | }, 10 | edit: { 11 | opened: false, 12 | task: {}, 13 | }, 14 | openCreateTask: (groupId = null) => { 15 | return set(produce(state => { 16 | state.create.opened = true; 17 | state.create.group_id = groupId; 18 | })); 19 | }, 20 | closeCreateTask: () => { 21 | return set(produce(state => { 22 | state.create.opened = false; 23 | state.create.group_id = null; 24 | })); 25 | }, 26 | openEditTask: (task) => { 27 | replaceUrlWithoutReload( 28 | route("projects.tasks.open", [task.project_id, task.id]) 29 | ); 30 | 31 | return set(produce(state => { 32 | state.edit.opened = true; 33 | state.edit.task = task; 34 | })); 35 | }, 36 | closeEditTask: () => { 37 | replaceUrlWithoutReload(route("projects.tasks", get().edit.task.project_id)); 38 | 39 | return set(produce(state => { 40 | state.edit.opened = false; 41 | })); 42 | }, 43 | })); 44 | 45 | export default useTaskDrawerStore; 46 | -------------------------------------------------------------------------------- /resources/js/hooks/store/useTaskGroupsStore.js: -------------------------------------------------------------------------------- 1 | import createTaskGroupWebSocketUpdatesSlice from '@/hooks/store/taskGroups/TaskGroupWebSocketUpdatesSlice'; 2 | import { reorder } from '@/utils/reorder'; 3 | import axios from 'axios'; 4 | import { create } from 'zustand'; 5 | 6 | const useTaskGroupsStore = create((set, get) => ({ 7 | ...createTaskGroupWebSocketUpdatesSlice(set, get), 8 | 9 | groups: [], 10 | setGroups: (groups) => set(() => ({ groups: [...groups] })), 11 | findTaskGroup: (id) => { 12 | return get().groups.find((i) => i.id === id); 13 | }, 14 | reorderGroup: (fromIndex, toIndex) => { 15 | const result = reorder(get().groups, fromIndex, toIndex); 16 | 17 | axios 18 | .post(route("projects.task-groups.reorder", route().params.project), {ids: result.map((i) => i.id)}) 19 | .catch(() => alert("Failed to save task group reorder action")); 20 | 21 | return set(() => ({ groups: [...result] })); 22 | }, 23 | })); 24 | 25 | export default useTaskGroupsStore; 26 | -------------------------------------------------------------------------------- /resources/js/hooks/useAuthorization.js: -------------------------------------------------------------------------------- 1 | import { usePage } from "@inertiajs/react"; 2 | 3 | export default function useAuthorization() { 4 | const {auth} = usePage().props; 5 | 6 | const can = (permission) => { 7 | return auth.user.permissions.includes(permission); 8 | }; 9 | 10 | const isAdmin = () => { 11 | return auth.user.roles.includes('admin'); 12 | }; 13 | 14 | return {can, isAdmin}; 15 | } 16 | -------------------------------------------------------------------------------- /resources/js/hooks/useForm.js: -------------------------------------------------------------------------------- 1 | import { useScrollIntoView } from "@mantine/hooks"; 2 | import { useForm as usePrecognitionForm } from "laravel-precognition-react-inertia"; 3 | import { isObject } from "lodash"; 4 | 5 | export default function useForm(method, url, data) { 6 | const form = usePrecognitionForm(method, url, data); 7 | 8 | const { scrollIntoView } = useScrollIntoView({ duration: 1000 }); 9 | 10 | const submit = (e, props) => { 11 | e.preventDefault(); 12 | 13 | form.submit({ 14 | preserveScroll: false, 15 | onError: () => { 16 | scrollIntoView({ 17 | target: document.querySelector('[data-error="true"]'), 18 | }); 19 | }, 20 | 21 | ...props, 22 | }); 23 | }; 24 | 25 | const updateValue = (field, value) => { 26 | if (isObject(field)) { 27 | form.setData(field); 28 | form.clearErrors(); 29 | } else { 30 | form.setData(field, value); 31 | form.forgetError(field); 32 | } 33 | }; 34 | 35 | return [form, submit, updateValue]; 36 | } 37 | -------------------------------------------------------------------------------- /resources/js/hooks/useImageLoader.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function useImageLoader() { 4 | const [loadingState, setLoadingState] = useState({ 5 | loading: true, 6 | error: null, 7 | image: null, 8 | }); 9 | 10 | const loadImage = (url) => { 11 | const image = new Image(); 12 | 13 | image.onload = () => { 14 | setLoadingState({ 15 | loading: false, 16 | error: null, 17 | image, 18 | }); 19 | }; 20 | 21 | image.onerror = () => { 22 | setLoadingState({ 23 | loading: false, 24 | error: 'Failed to load image', 25 | image: null, 26 | }); 27 | }; 28 | 29 | image.src = url; 30 | 31 | return () => { 32 | image.onload = null; 33 | image.onerror = null; 34 | }; 35 | }; 36 | 37 | return { ...loadingState, loadImage }; 38 | } 39 | -------------------------------------------------------------------------------- /resources/js/hooks/usePreferences.js: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from "@mantine/hooks"; 2 | 3 | export default function usePreferences() { 4 | const [tasksView, setTasksView] = useLocalStorage({ 5 | key: "tasks-view", 6 | defaultValue: "list", 7 | getInitialValueInEffect: false, 8 | }); 9 | 10 | return {tasksView, setTasksView}; 11 | } 12 | -------------------------------------------------------------------------------- /resources/js/hooks/useRoles.js: -------------------------------------------------------------------------------- 1 | import { usePage } from '@inertiajs/react'; 2 | import upperFirst from 'lodash/upperFirst'; 3 | 4 | export default function useRoles() { 5 | const roles = usePage().props.shared.roles; 6 | const roleColors = {}; 7 | 8 | const colors = [ 9 | 'grape', 10 | 'yellow', 11 | 'indigo', 12 | 'lime', 13 | 'cyan', 14 | 'violet', 15 | 'orange', 16 | 'pink', 17 | ]; 18 | 19 | roles.forEach((role, index) => roleColors[role.name] = colors[index % colors.length]); 20 | 21 | 22 | const getColor = (role) => { 23 | return roleColors[role]; 24 | }; 25 | 26 | const getDropdownValues = ({except = []}) => { 27 | return roles 28 | .filter(i => !except.includes(i.name)) 29 | .map(role => ({value: role.name, label: upperFirst(role.name)})); 30 | } 31 | 32 | return {getColor, getDropdownValues}; 33 | } 34 | -------------------------------------------------------------------------------- /resources/js/hooks/useSorting.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export default function useSorting(sort) { 4 | const [sortBy, setSortBy] = useState(null); 5 | const [reverseSortDirection, setReverseSortDirection] = useState(false); 6 | 7 | const setSorting = (field) => { 8 | const reversed = field === sortBy ? !reverseSortDirection : false; 9 | setReverseSortDirection(reversed); 10 | setSortBy(field); 11 | 12 | sort({sort: {[field]: reversed ? 'desc' : 'asc'}}); 13 | }; 14 | 15 | useEffect(() => { 16 | const currentSort = route().params.sort; 17 | 18 | if (currentSort) { 19 | const key = Object.keys(currentSort)[0]; 20 | setSortBy(key); 21 | setReverseSortDirection(currentSort[key] === 'desc'); 22 | } 23 | }, []); 24 | 25 | return [sortBy, reverseSortDirection, setSorting]; 26 | } 27 | -------------------------------------------------------------------------------- /resources/js/layouts/ContainerBox.jsx: -------------------------------------------------------------------------------- 1 | import { Paper } from "@mantine/core"; 2 | import classes from "./css/ContainerBox.module.css"; 3 | 4 | export default function ContainerBox({ children, ...props }) { 5 | return ( 6 | 7 | {children} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /resources/js/layouts/GuestLayout.jsx: -------------------------------------------------------------------------------- 1 | import FlashNotification from "@/components/FlashNotification"; 2 | import { Head } from "@inertiajs/react"; 3 | import { Container } from "@mantine/core"; 4 | 5 | export default function GuestLayout({ title, children }) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /resources/js/layouts/css/ContainerBox.module.css: -------------------------------------------------------------------------------- 1 | .box { 2 | 3 | @mixin light { 4 | background-color: var(--mantine-color-white); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /resources/js/layouts/css/Notifications.module.css: -------------------------------------------------------------------------------- 1 | .indicator div { 2 | font-weight: 700; 3 | font-size: 11px; 4 | pointer-events: none; 5 | } 6 | 7 | .notification { 8 | margin: rem(10px) 0; 9 | transition: 0.3s ease opacity; 10 | 11 | @mixin hover { 12 | opacity: 1 !important; 13 | } 14 | } 15 | 16 | .link { 17 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); 18 | opacity: 0.6; 19 | 20 | @mixin hover { 21 | opacity: 0.9; 22 | text-decoration: underline; 23 | } 24 | } -------------------------------------------------------------------------------- /resources/js/layouts/css/UserButton.module.css: -------------------------------------------------------------------------------- 1 | .user { 2 | display: block; 3 | width: 100%; 4 | padding: var(--mantine-spacing-md); 5 | color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-0)); 6 | transition: 0.3s ease background-color; 7 | 8 | @mixin hover { 9 | background-color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-dark-8)) !important; 10 | } 11 | } -------------------------------------------------------------------------------- /resources/js/pages/Account/Notifications/css/Index.module.css: -------------------------------------------------------------------------------- 1 | .notification { 2 | border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); 3 | padding: 15px; 4 | border-radius: var(--mantine-radius-sm); 5 | transition: 0.3s ease all; 6 | 7 | @mixin hover { 8 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); 9 | opacity: 1 !important; 10 | } 11 | } -------------------------------------------------------------------------------- /resources/js/pages/Auth/LoginNotification.jsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "@mantine/core"; 2 | import { 3 | IconInfoCircle, 4 | IconAlertTriangle, 5 | IconExclamationCircle, 6 | } from "@tabler/icons-react"; 7 | 8 | export default function LoginNotification({ notify }) { 9 | return ( 10 |
11 | {notify === "password-reset" && ( 12 | }> 13 | Your password was successfully updated, you may use it to login. 14 | 15 | )} 16 | {notify === "social-login-user-not-found" && ( 17 | } 21 | color="orange" 22 | > 23 | No user was found with your Google email address. 24 | 25 | )} 26 | {notify === "social-login-failed" && ( 27 | } 31 | color="red" 32 | > 33 | Unexpected error has occurred, please try logging in with your email 34 | and password. 35 | 36 | )} 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /resources/js/pages/Auth/css/ForgotPassword.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: rem(26px); 3 | font-weight: 900; 4 | font-family: Greycliff CF, var(--mantine-font-family); 5 | } 6 | 7 | .controls { 8 | @media (max-width: $mantine-breakpoint-xs) { 9 | flex-direction: column-reverse; 10 | } 11 | } 12 | 13 | .control { 14 | @media (max-width: $mantine-breakpoint-xs) { 15 | width: 100%; 16 | text-align: center; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/pages/Auth/css/Login.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-family: 3 | Greycliff CF, 4 | var(--mantine-font-family); 5 | font-weight: 900; 6 | } 7 | -------------------------------------------------------------------------------- /resources/js/pages/Auth/css/ResetPassword.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: rem(26px); 3 | font-weight: 900; 4 | font-family: Greycliff CF, var(--mantine-font-family); 5 | } 6 | 7 | .controls { 8 | @media (max-width: $mantine-breakpoint-xs) { 9 | flex-direction: column-reverse; 10 | } 11 | } 12 | 13 | .control { 14 | @media (max-width: $mantine-breakpoint-xs) { 15 | width: 100%; 16 | text-align: center; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/pages/Dashboard/Cards/css/OverdueTasks.module.css: -------------------------------------------------------------------------------- 1 | .task { 2 | transition: 0.3s ease color, 0.3s ease background-color; 3 | padding: 5px 10px 10px 15px; 4 | border-radius: var(--mantine-radius-md); 5 | 6 | @mixin hover { 7 | color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-white)); 8 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-gray-9)); 9 | } 10 | } 11 | 12 | .link { 13 | cursor: pointer; 14 | } 15 | 16 | .due { 17 | text-wrap: nowrap; 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/pages/Dashboard/Cards/css/ProjectCard.module.css: -------------------------------------------------------------------------------- 1 | .link { 2 | @mixin hover { 3 | cursor: pointer; 4 | color: var(--mantine-color-blue-6); 5 | } 6 | } -------------------------------------------------------------------------------- /resources/js/pages/Dashboard/Cards/css/RecentComments.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | transition: 0.3s ease color, 0.3s ease background-color; 3 | padding: 6px 10px 7px 10px; 4 | border-radius: var(--mantine-radius-md); 5 | cursor: pointer; 6 | 7 | @mixin hover { 8 | color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-white)); 9 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-gray-9)); 10 | } 11 | } 12 | 13 | .comment { 14 | font-size: rem(14); 15 | line-height: rem(14); 16 | 17 | p { 18 | margin: 0; 19 | } 20 | } -------------------------------------------------------------------------------- /resources/js/pages/Dashboard/Cards/css/RecentlyAssignedTasks.module.css: -------------------------------------------------------------------------------- 1 | .task { 2 | transition: 0.3s ease color, 0.3s ease background-color; 3 | padding: 5px 10px 10px 15px; 4 | border-radius: var(--mantine-radius-md); 5 | 6 | @mixin hover { 7 | color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-white)); 8 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-gray-9)); 9 | } 10 | } 11 | 12 | .link { 13 | cursor: pointer; 14 | } 15 | 16 | .due { 17 | text-wrap: nowrap; 18 | } -------------------------------------------------------------------------------- /resources/js/pages/Dashboard/css/Index.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | transition: 0.3s ease all; 3 | 4 | @mixin hover { 5 | border: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7)); 6 | 7 | .title { 8 | color: var(--mantine-color-blue-5); 9 | } 10 | } 11 | } 12 | 13 | .link { 14 | @mixin hover { 15 | cursor: pointer; 16 | } 17 | } 18 | 19 | .myMasonryGrid { 20 | display: flex; 21 | width: auto; 22 | } 23 | 24 | .myMasonryGridColumn { 25 | padding-left: rem(20px); 26 | background-clip: padding-box; 27 | 28 | &:first-child { 29 | padding-left: 0; 30 | } 31 | 32 | >div { 33 | margin-bottom: rem(20px); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/js/pages/Invoices/css/Task.module.css: -------------------------------------------------------------------------------- 1 | .checkbox input { 2 | cursor: pointer; 3 | } 4 | 5 | .task { 6 | transition: 0.3s ease color, 0.3s ease background-color; 7 | padding: 5px 10px 5px 15px; 8 | margin: 5px -15px; 9 | border-radius: var(--mantine-radius-md); 10 | 11 | @mixin hover { 12 | color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-white)); 13 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-gray-9)); 14 | } 15 | } 16 | 17 | .name { 18 | cursor: pointer; 19 | } -------------------------------------------------------------------------------- /resources/js/pages/MyWork/Tasks/css/Index.module.css: -------------------------------------------------------------------------------- 1 | .accordionControl:not([data-active="true"]) { 2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); 3 | border-radius: var(--mantine-radius-md); 4 | border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-6)); 5 | transition: 0.3s ease border, 0.3s ease color; 6 | } -------------------------------------------------------------------------------- /resources/js/pages/MyWork/Tasks/css/Task.module.css: -------------------------------------------------------------------------------- 1 | .user { 2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); 3 | border-radius: var(--mantine-radius-xl); 4 | border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6)); 5 | transition: 0.3s ease border, 0.3s ease color; 6 | 7 | @mixin hover { 8 | color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-white)); 9 | border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); 10 | } 11 | 12 | &>span { 13 | cursor: pointer; 14 | } 15 | } 16 | 17 | .task { 18 | transition: 0.3s ease color, 0.3s ease background-color; 19 | padding: 5px 10px 5px 15px; 20 | border-radius: var(--mantine-radius-md); 21 | 22 | @mixin hover { 23 | color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-white)); 24 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-gray-9)); 25 | } 26 | } 27 | 28 | .name { 29 | cursor: pointer; 30 | } 31 | 32 | .completed { 33 | opacity: 0.35; 34 | } -------------------------------------------------------------------------------- /resources/js/pages/Projects/Index/FavoriteToggle.jsx: -------------------------------------------------------------------------------- 1 | import { UnstyledButton, rem } from "@mantine/core"; 2 | import { IconStar, IconStarFilled } from "@tabler/icons-react"; 3 | import { useForm } from "laravel-precognition-react-inertia"; 4 | import classes from "./css/FavoriteToggle.module.css"; 5 | 6 | export default function ToggleFavorite({ item }) { 7 | const favorite = useForm("put", route("projects.favorite.toggle", item.id)); 8 | 9 | return ( 10 | favorite.submit({ preserveScroll: true })} 12 | className={classes.button} 13 | data-ignore-link 14 | > 15 | {item.favorite ? ( 16 | 24 | ) : ( 25 | 32 | )} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Index/css/FavoriteToggle.module.css: -------------------------------------------------------------------------------- 1 | .button svg { 2 | transform: scale(1); 3 | 4 | @mixin hover { 5 | transform: scale(1.07); 6 | } 7 | } -------------------------------------------------------------------------------- /resources/js/pages/Projects/Index/css/ProjectCard.module.css: -------------------------------------------------------------------------------- 1 | .link { 2 | text-decoration: none; 3 | } 4 | 5 | .card { 6 | transition: 0.3s ease all; 7 | 8 | @mixin hover { 9 | border: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7)); 10 | 11 | .title { 12 | color: var(--mantine-color-blue-5); 13 | } 14 | } 15 | } 16 | 17 | .title { 18 | color: var(--mantine-color-blue-6); 19 | transition: 0.2s ease all; 20 | } 21 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Drawers/css/Comments.module.css: -------------------------------------------------------------------------------- 1 | .comment { 2 | font-size: rem(14); 3 | line-height: rem(14); 4 | 5 | p { 6 | margin: 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Drawers/css/TaskDrawer.module.css: -------------------------------------------------------------------------------- 1 | .inner { 2 | padding: 0 rem(20); 3 | display: flex; 4 | column-gap: rem(30); 5 | } 6 | 7 | .content { 8 | padding: rem(5); 9 | width: 70%; 10 | flex-grow: 1; 11 | 12 | @media (max-width: $mantine-breakpoint-md) { 13 | width: 100%; 14 | } 15 | } 16 | 17 | .sidebar { 18 | padding: rem(5); 19 | width: 30%; 20 | flex-grow: 1; 21 | 22 | @media (max-width: $mantine-breakpoint-md) { 23 | width: 100%; 24 | } 25 | } 26 | 27 | .checkbox input { 28 | cursor: pointer; 29 | } 30 | 31 | .disabledCheckbox input { 32 | cursor: default; 33 | pointer-events: none; 34 | } 35 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Index/Archive/ArchivedItems.jsx: -------------------------------------------------------------------------------- 1 | import { EmptyResult } from "@/components/EmptyResult"; 2 | import { Text } from "@mantine/core"; 3 | import ArchivedTask from "./ArchivedTask"; 4 | import ArchivedTaskGroup from "./ArchivedTaskGroup"; 5 | 6 | export default function ArchivedItems({ groups, tasks }) { 7 | const hasTasks = Object.keys(tasks).some((key) => tasks[key].length > 0); 8 | 9 | return groups.length || hasTasks ? ( 10 | <> 11 | {hasTasks && ( 12 | <> 13 | 14 | Tasks 15 | 16 | {Object.keys(tasks).map((key) => 17 | tasks[key].map((task) => ), 18 | )} 19 | 20 | )} 21 | {groups.length > 0 && ( 22 | <> 23 | 24 | Task groups 25 | 26 | {groups.map((group) => ( 27 | 28 | ))} 29 | 30 | )} 31 | 32 | ) : ( 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Index/Archive/ArchivedTaskGroup.jsx: -------------------------------------------------------------------------------- 1 | import { Group, Text } from "@mantine/core"; 2 | import TaskGroupActions from "../TaskGroupActions"; 3 | import classes from "../css/TaskGroup.module.css"; 4 | 5 | export default function ArchivedTaskGroup({ group }) { 6 | return ( 7 |
8 | 9 | 10 | {group.name} 11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Index/Filters/FilterButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button, rem } from "@mantine/core"; 2 | import { IconCheck } from "@tabler/icons-react"; 3 | import classes from "./css/FilterButton.module.css"; 4 | 5 | export default function FilterButton({ selected, children, ...props }) { 6 | return ( 7 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Index/Filters/css/FilterButton.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6)); 3 | 4 | @mixin hover { 5 | border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Index/FiltersDrawer.jsx: -------------------------------------------------------------------------------- 1 | import useTaskFiltersStore from "@/hooks/store/useTaskFiltersStore"; 2 | import { Drawer, Text, rem } from "@mantine/core"; 3 | import Filters from "./Filters"; 4 | 5 | export default function FiltersDrawer() { 6 | const { openedDrawer, closeDrawer } = useTaskFiltersStore(); 7 | 8 | return ( 9 | closeDrawer()} 12 | title={ 13 | 14 | Filters 15 | 16 | } 17 | position="right" 18 | size={300} 19 | overlayProps={{ backgroundOpacity: 0.4 }} 20 | transitionProps={{ 21 | transition: "slide-left", 22 | duration: 400, 23 | timingFunction: "ease", 24 | }} 25 | > 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Index/Task.jsx: -------------------------------------------------------------------------------- 1 | import usePreferences from "@/hooks/usePreferences"; 2 | import TaskCard from "./Task/TaskCard"; 3 | import TaskRow from "./Task/TaskRow"; 4 | 5 | export default function Task({ task, index }) { 6 | const { tasksView } = usePreferences(); 7 | 8 | return tasksView === "list" ? ( 9 | 10 | ) : ( 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/Index/Task/css/TaskCard.module.css: -------------------------------------------------------------------------------- 1 | .task { 2 | transition: 0.3s ease color, 0.3s ease background-color; 3 | padding: rem(12px) rem(12px) rem(8px) rem(12px); 4 | margin-bottom: rem(8px); 5 | position: relative; 6 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 7 | border-radius: var(--mantine-radius-md); 8 | border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6)); 9 | 10 | @mixin hover { 11 | color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-white)); 12 | background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6)); 13 | 14 | .actions { 15 | opacity: 0.5; 16 | } 17 | 18 | .user { 19 | border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); 20 | } 21 | } 22 | } 23 | 24 | .name { 25 | cursor: pointer; 26 | padding-right: rem(20px); 27 | } 28 | 29 | .actions { 30 | opacity: 0; 31 | transition: 0.3s ease opacity; 32 | position: absolute; 33 | top: rem(5px); 34 | right: rem(5px); 35 | } 36 | 37 | .completed { 38 | opacity: 0.35; 39 | } -------------------------------------------------------------------------------- /resources/js/pages/Projects/Tasks/css/Index.module.css: -------------------------------------------------------------------------------- 1 | :global(.kanban-view) { 2 | .viewport { 3 | display: flex; 4 | gap: rem(15px); 5 | 6 | padding-left: 4rem; 7 | padding-right: 4rem; 8 | 9 | height: calc(100dvh - 147px); 10 | } 11 | 12 | overflow-x: auto; 13 | margin-left: -4rem; 14 | margin-right: -4rem; 15 | margin-bottom: -4rem; 16 | } -------------------------------------------------------------------------------- /resources/js/pages/css/Error.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding-top: rem(80px); 3 | padding-bottom: rem(80px); 4 | } 5 | 6 | .inner { 7 | position: relative; 8 | } 9 | 10 | .image { 11 | position: absolute; 12 | left: 50%; 13 | top: 50%; 14 | translate: -50% -50%; 15 | opacity: 0.75; 16 | color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 17 | font-size: 45vw; 18 | letter-spacing: rem(-10px); 19 | font-weight: 700; 20 | text-align: center; 21 | user-select: none; 22 | } 23 | 24 | .content { 25 | padding-top: rem(220px); 26 | position: relative; 27 | z-index: 1; 28 | 29 | @media (max-width: $mantine-breakpoint-sm) { 30 | padding-top: rem(120px); 31 | } 32 | } 33 | 34 | .title { 35 | font-family: Greycliff CF, var(--mantine-font-family); 36 | text-align: center; 37 | font-weight: 900; 38 | font-size: rem(38px); 39 | 40 | @media (max-width: $mantine-breakpoint-sm) { 41 | font-size: rem(32px); 42 | } 43 | } 44 | 45 | .description { 46 | max-width: rem(540px); 47 | margin: auto; 48 | margin-top: var(--mantine-spacing-xl); 49 | margin-bottom: calc(var(--mantine-spacing-xl) * 1.5); 50 | } -------------------------------------------------------------------------------- /resources/js/types.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: BigInteger; 3 | name: String; 4 | email: String; 5 | phone: String; 6 | job_title: String; 7 | avatar: String; 8 | rate: BigInteger; 9 | created_at: String; 10 | }; 11 | -------------------------------------------------------------------------------- /resources/js/utils/axios.js: -------------------------------------------------------------------------------- 1 | import nProgress from "nprogress"; 2 | 3 | export const onUploadProgress = (progressEvent) => { 4 | nProgress.set(progressEvent.progress); 5 | 6 | if (progressEvent.progress === 1) { 7 | nProgress.done(); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /resources/js/utils/currency.js: -------------------------------------------------------------------------------- 1 | export const money = (amount, currency = 'USD', minimumFractionDigits = 2) => { 2 | const formatter = new Intl.NumberFormat('en-US', { 3 | style: 'currency', 4 | currency, 5 | minimumFractionDigits, 6 | }); 7 | 8 | return formatter.format(amount / 100); 9 | }; 10 | -------------------------------------------------------------------------------- /resources/js/utils/datetime.js: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | export const date = (date) => { 4 | return dayjs(date).format("D. MMM YYYY"); 5 | }; 6 | 7 | export const time = (date) => { 8 | return dayjs(date).format("H:mm") + 'h'; 9 | }; 10 | 11 | export const day = (date) => { 12 | return dayjs(date).format("dddd"); 13 | }; 14 | 15 | export const dateTime = (datetime) => { 16 | return dayjs(datetime).format("D. MMM YYYY H:mm") + 'h'; 17 | }; 18 | 19 | export const diffForHumans = (datetime, withoutSuffix = false) => { 20 | return dayjs(datetime).fromNow(withoutSuffix); 21 | }; 22 | -------------------------------------------------------------------------------- /resources/js/utils/domEvents.js: -------------------------------------------------------------------------------- 1 | export const stopOnIgnoreLink = (event) => { 2 | if ( 3 | event.target.dataset.ignoreLink || 4 | event.target.parentNode.dataset.ignoreLink 5 | ) 6 | event.preventDefault(); 7 | }; 8 | -------------------------------------------------------------------------------- /resources/js/utils/reorder.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from "lodash/cloneDeep"; 2 | 3 | export const reorder = (list, startIndex, endIndex) => { 4 | const result = Array.from(list); 5 | const [removed] = result.splice(startIndex, 1); 6 | result.splice(endIndex, 0, removed); 7 | 8 | return result; 9 | }; 10 | 11 | export const move = (tasks, sourceGroupId, destinationGroupId, sourceIndex, destinationIndex) => { 12 | const sourceClone = cloneDeep(tasks[sourceGroupId]); 13 | const destClone = Array.from(tasks[destinationGroupId] || []); 14 | const [removed] = sourceClone.splice(sourceIndex, 1); 15 | 16 | removed.group_id = destinationGroupId; 17 | 18 | destClone.splice(destinationIndex, 0, removed); 19 | 20 | const result = {}; 21 | 22 | result[sourceGroupId] = sourceClone; 23 | result[destinationGroupId] = destClone; 24 | 25 | return result; 26 | }; 27 | -------------------------------------------------------------------------------- /resources/js/utils/table.js: -------------------------------------------------------------------------------- 1 | export const prepareColumns = (columns) => { 2 | return columns.filter((c) => c.visible !== false); 3 | }; 4 | 5 | export const actionColumnVisibility = (name) => { 6 | return (can(`edit ${name}`) && !route().params.archived) || 7 | (can(`archive ${name}`) && !route().params.archived) || 8 | (can(`restore ${name}`) && route().params.archived); 9 | }; 10 | -------------------------------------------------------------------------------- /resources/js/utils/task.js: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | export const isOverdue = (task) => { 4 | return dayjs().isAfter(task.due_on); 5 | }; 6 | -------------------------------------------------------------------------------- /resources/js/utils/timer.js: -------------------------------------------------------------------------------- 1 | 2 | export const humanReadableTime = (minutes) => { 3 | let formattedMinutes = (minutes % 60).toString(); 4 | 5 | if (formattedMinutes.length === 1) { 6 | formattedMinutes = `0${formattedMinutes}`; 7 | } 8 | return `${Math.floor(minutes / 60)}:${formattedMinutes}`; 9 | }; 10 | 11 | export const convertToMinutes = (value) => { 12 | if (value.includes(':')) { // 10:30 13 | return (value.split(':')[0] * 60) + parseInt(value.split(':')[1]); 14 | } else if (value.includes('.')) { // 1.5 or 1.75 15 | let remainder = value.split('.')[1]; 16 | 17 | if (remainder.length === 1) { 18 | remainder += '0'; 19 | } 20 | return (value.split('.')[0] * 60) + (remainder * 0.6); 21 | } else if (value.includes(',')) { // 1,5 or 1,75 22 | let remainder = value.split(',')[1]; 23 | 24 | if (remainder.length === 1) { 25 | remainder += '0'; 26 | } 27 | return (value.split(',')[0] * 60) + (remainder * 0.6); 28 | } 29 | return value * 60; 30 | } 31 | 32 | export const isTimeValueValid = (value) => { 33 | // valid values: 1:00, 10:30, 28:59, 1,5, 2.5 34 | return /^(\d{1,2}:[0-5]{1}[0-9]{1})$|^(\d{1,3}\.\d{0,2})$|^(\d{1,3},\d{0,2})$|^(\d{1,3})$/.test( 35 | value, 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /resources/js/utils/user.js: -------------------------------------------------------------------------------- 1 | export const getInitials = (name) => { 2 | if (!name.includes(" ")) { 3 | return name.slice(0, 2).toUpperCase(); 4 | } 5 | const [firstname, lastname] = name.split(" "); 6 | 7 | if (!lastname) { 8 | return firstname.slice(0, 2).toUpperCase(); 9 | } 10 | 11 | return (firstname[0] + lastname[0]).toUpperCase(); 12 | }; 13 | 14 | export const shortName = (name) => { 15 | if (!name.includes(" ")) { 16 | return name; 17 | } 18 | const [firstname, lastname] = name.split(" "); 19 | 20 | return firstname + ' ' + lastname[0] + '.'; 21 | }; 22 | 23 | export const hasRoles = (user, roles) => { 24 | return user.roles.find((i) => roles.includes(i.name)) !== undefined; 25 | }; 26 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @routes 16 | @viteReactRefresh 17 | @vite(['resources/js/app.jsx']) 18 | @inertiaHead 19 | 20 | 21 | 22 | @inertia 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'url', 3 | 'color' => 'primary', 4 | 'align' => 'center', 5 | ]) 6 | 7 | 8 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | @props(['url']) 2 | 3 | 4 | 5 | {{ $slot }} 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/layout.blade.php: -------------------------------------------------------------------------------- 1 | {!! strip_tags($header ?? '') !!} 2 | 3 | {!! strip_tags($slot) !!} 4 | @isset($subcopy) 5 | 6 | {!! strip_tags($subcopy) !!} 7 | @endisset 8 | 9 | {!! strip_tags($footer ?? '') !!} 10 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/user', function (Request $request) { 18 | return $request->user(); 19 | }); 20 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 16 | }); 17 | 18 | Broadcast::channel('App.Models.Project.{id}', function ($user, int $id) { 19 | $users = PermissionService::usersWithAccessToProject( 20 | Project::findOrFail($id) 21 | ); 22 | 23 | return $users->contains(fn ($u) => $u['id'] === $user->id); 24 | }); 25 | 26 | Broadcast::channel('App.Models.Task.{id}', function ($user, int $id) { 27 | $task = Task::findOrFail($id); 28 | $users = PermissionService::usersWithAccessToProject($task->project); 29 | 30 | return $users->contains(fn ($u) => $u['id'] === $user->id); 31 | }); 32 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !avatars 4 | -------------------------------------------------------------------------------- /storage/app/public/avatars/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | laravel({ 8 | input: 'resources/js/app.jsx', 9 | refresh: true, 10 | }), 11 | react(), 12 | ], 13 | }); 14 | --------------------------------------------------------------------------------