Compare commits

...

516 Commits

Author SHA1 Message Date
Gregor Vostrak
4ff8a72f0b add comprehensive 2 factor authentication e2e tests 2026-06-10 14:13:21 +02:00
Gregor Vostrak
4790693017 add back destroy other browser sessions endpoint (jetstream migration) 2026-06-10 13:28:35 +02:00
Gregor Vostrak
3caf7438b5 update e2e test setup to use user settings api endpoint 2026-06-09 16:24:52 +02:00
Gregor Vostrak
d929d31847 change redirects and references to new organization routes 2026-06-09 13:08:22 +02:00
Gregor Vostrak
d7bb36d50f add currency to organization update endpoint 2026-06-09 12:37:20 +02:00
Gregor Vostrak
b3785f0aa6 replace hardcoded inertia props with organization delete/update perms 2026-06-09 01:43:13 +02:00
Gregor Vostrak
8e47f07f09 remove unused inertia organization page props 2026-06-09 00:32:44 +02:00
Gregor Vostrak
da611086e8 fix inertia backend role data structure after jetstream migration 2026-06-09 00:19:36 +02:00
Gregor Vostrak
a220d0e592 call api for organization create/update/delete and switch 2026-06-09 00:12:55 +02:00
Constantin Graf
0e2c4431a0 Fixed current organization after normal registration 2026-06-08 23:06:07 +02:00
Constantin Graf
2f4c079f9f Added tests 2026-06-08 22:57:02 +02:00
Gregor Vostrak
f826474f88 add switch current organization endpoint 2026-06-08 18:57:23 +02:00
Constantin Graf
98bbe800f1 Removed Laravel Jetstream 2026-06-08 17:34:55 +02:00
Gregor Vostrak
7035d5fd6e remove jetstream inertia properties; remove unused ApiTokenManager; 2026-06-05 16:43:01 +02:00
Gregor Vostrak
f32ec59bb5 move banners on login and register cards into the cards 2026-05-29 17:40:16 +02:00
Gregor Vostrak
d2b6be137f add pending email cancel button 2026-05-29 17:40:16 +02:00
Constantin Graf
dc082b2b19 Replaces all Jetstream model trait functions and relations 2026-05-29 17:40:16 +02:00
Constantin Graf
82ad8ee316 Add reset pending email endpoint to user controller 2026-05-29 17:40:16 +02:00
Gregor Vostrak
117c3c4b6c move user delete to api endpoint 2026-05-29 17:40:16 +02:00
Gregor Vostrak
4c2586936d use api routes for profile information updates 2026-05-29 17:40:16 +02:00
Gregor Vostrak
ca843168f6 show null billable rate as empty not as 0 to avoid confusion 2026-05-29 17:40:16 +02:00
Gregor Vostrak
67dcf77635 fix e2e selectors to adapt to reka-ui change; 2026-05-29 17:40:16 +02:00
Gregor Vostrak
dcd21345b2 add pending email to UserResource and update openapi client 2026-05-29 17:40:16 +02:00
Gregor Vostrak
1f832a24a0 update ui package dependencies; update lucide imports 2026-05-29 17:40:16 +02:00
Gregor Vostrak
07cf3f7405 add user endpoint tests for idempotence email update, unauthenticated
update and invalid email
2026-05-29 17:37:14 +02:00
Gregor Vostrak
a880ccb32c update npm dependencies 2026-05-29 17:37:13 +02:00
Gregor Vostrak
5a41c356d4 add profile page e2e tests 2026-05-29 17:27:16 +02:00
Gregor Vostrak
72bddfba8b update email address change info to use session based banners 2026-05-29 17:27:16 +02:00
Gregor Vostrak
34a1a89c30 add 1MB photo upload limit 2026-05-29 17:27:15 +02:00
Gregor Vostrak
77e4d768d4 add photo delete logic to user update endpoint 2026-05-29 17:27:15 +02:00
Constantin Graf
d42e3ffff0 Updated composer dependencies 2026-05-29 17:27:15 +02:00
Constantin Graf
4e26c8ad6d Add more tests 2026-05-29 17:27:15 +02:00
Constantin Graf
57794940f1 Add migration to lower case the user emails 2026-05-29 17:27:15 +02:00
Constantin Graf
09827d3d83 Migrate permission away from Jetstream; Moved update user to REST API 2026-05-29 17:27:15 +02:00
Gregor Vostrak
64c5da5223 rephrase logged out user invite accept message to clarify that the
invite was accepted
2026-05-29 17:27:15 +02:00
Gregor Vostrak
983e6c3815 add banners for invitation accept 2026-05-29 17:27:15 +02:00
Constantin Graf
f34b60874e Updated invitation flow, Moved jetstream function to REST endpoints; Lower case email 2026-05-29 17:27:15 +02:00
Gregor Vostrak
8eab0485c9 revert reka-ui update; fix DST cellMath; 2026-05-29 17:14:52 +02:00
Gregor Vostrak
0aa0f0bd77 use cn helper for alert-dialog modals 2026-05-29 17:14:52 +02:00
Gregor Vostrak
eb63c4ef03 fix light mode timesheet background and add missing aria-label 2026-05-29 17:14:52 +02:00
Gregor Vostrak
54fffd07bc add timesheet unit and e2e tests; add unit test CI setup 2026-05-29 17:14:52 +02:00
Gregor Vostrak
da235dfdc8 remove special “Add new project” state in TimeTrackerProjectTaskDropdown 2026-05-29 17:14:52 +02:00
Gregor Vostrak
0debdddef9 set min release age for npm packages to 7 days to prevent supply chain attacks 2026-05-29 17:14:52 +02:00
Gregor Vostrak
62354cfe8b remove timetrackerprojecttaskdropdown test without setup 2026-05-29 17:14:52 +02:00
Gregor Vostrak
396e7b2b6b fix DST boundary issue in timesheets 2026-05-29 17:14:52 +02:00
Gregor Vostrak
221889ff87 fix "No project" duplicating rows, unify no project senitel to null 2026-05-29 17:14:52 +02:00
Gregor Vostrak
7ce3fa2740 change TimeEntryFilter start filter to be inclusive 2026-05-29 17:14:52 +02:00
Gregor Vostrak
df34014bfe fix e2e tests 2026-05-29 17:14:52 +02:00
Gregor Vostrak
faf3ee471c fix formatting 2026-05-29 17:14:52 +02:00
Gregor Vostrak
866e5d8594 clamp running time entry duration to min 0 for FullCalendarHeaderDuration calc 2026-05-29 17:14:52 +02:00
Gregor Vostrak
72cd0b6f05 fix formatting 2026-05-29 17:14:52 +02:00
Gregor Vostrak
6d93e48b1d add missing dayjs plugins for isSameOrBefore and isSameOrAfter 2026-05-29 17:14:52 +02:00
Gregor Vostrak
09af0f775f add timesheets page 2026-05-29 17:14:52 +02:00
Gregor Vostrak
1cc000a584 fix local storage filter migration state for visibility filter 2026-05-26 11:37:24 +02:00
Gregor Vostrak
1a754f6756 improve modal and field group spacing for project modal layout 2026-05-26 11:15:15 +02:00
Gregor Vostrak
d69d25d059 add project table visibility filter 2026-05-26 11:15:15 +02:00
Gregor Vostrak
0e15d9d9c2 add project visibility ui 2026-05-26 11:15:15 +02:00
dependabot[bot]
7d9ecd9526 Bump aglipanci/laravel-pint-action from 2.5 to 2.6
Bumps [aglipanci/laravel-pint-action](https://github.com/aglipanci/laravel-pint-action) from 2.5 to 2.6.
- [Release notes](https://github.com/aglipanci/laravel-pint-action/releases)
- [Commits](https://github.com/aglipanci/laravel-pint-action/compare/2.5...2.6)

---
updated-dependencies:
- dependency-name: aglipanci/laravel-pint-action
  dependency-version: '2.6'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 15:28:48 +02:00
dependabot[bot]
3a17f80f99 Bump codecov/codecov-action from 5.4.3 to 5.5.1
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.3 to 5.5.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.4.3...v5.5.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 15:14:44 +02:00
dependabot[bot]
e29ea2ea42 Bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 15:13:14 +02:00
dependabot[bot]
fb6e4639ce Bump actions/download-artifact from 4 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 15:12:01 +02:00
dependabot[bot]
69bc41988a Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 15:11:19 +02:00
Gregor Vostrak
f7663b1c8b Clarify out of scope items for vulnerability reports
Added out of scope section for vulnerability reporting.
2026-05-18 19:21:32 +02:00
Gregor Vostrak
793bd11dcf remove member, invitation, and owner email disclosure from Teams/Show inertia props
The Teams/Show Inertia page serialized members, pending invitations, and the
owner email into props using only a belongsToTeam authorization gate, while
the corresponding API endpoints correctly enforced members:view and
invitations:view. The serialized data was unused by the live UI (the
TeamMemberManager partial that referenced it was orphaned), so dropping the
fields removes the disclosure surface without functional impact. The owner
card retains name and photo.
2026-05-18 19:04:57 +02:00
Gregor Vostrak
77a62afd69 add alphabetic sorting to multiselect dropdowns 2026-04-29 18:32:05 +02:00
Gregor Vostrak
b73aa543fd Merge commit from fork 2026-04-21 21:12:30 +02:00
Gregor Vostrak
2d6f9e514f add groupSimilarTimeEntries to TimeEntryGroupedTable 2026-04-21 20:44:33 +02:00
Gregor Vostrak
f8e668790b Fix typo in project name in README.md 2026-04-18 04:27:50 +02:00
utlark
77a5e979c6 Added the ability to disable group similar time entries (#1054)
* Added the ability to disable group similar time entries

* Fix E2E test for Group similar time entries

* Simplify `TimeEntryGroupedTable` by replacing ternary with early return logic

* Refactor time entry grouping settings: rename storage key, move logic into a dedicated module

* Replace fixed `waitForTimeout` calls in E2E tests with element-based waits and assertions

* Run frontend linting and formatting for changes
2026-04-17 16:44:59 +02:00
Gregor Vostrak
353a579850 chore: bump ui package version 2026-04-17 14:46:36 +02:00
Gregor Vostrak
bd44a2b376 fix e2e tests for new duration reporting format logic 2026-04-17 14:36:56 +02:00
Gregor Vostrak
277dbaf6eb promote duration formats that omit seconds to HH:mm:ss in reporting
views and exports
2026-04-17 12:15:26 +02:00
Gregor Vostrak
1cf33ddb3f improve dark mode color palette; rework font weights throughout the
interface
2026-04-15 15:35:20 +02:00
Gregor Vostrak
84cd0d572d bump ui package version 2026-04-08 23:18:29 +02:00
Gregor Vostrak
f37b86f377 chore: remove unused formatActivityDuration function 2026-04-08 14:49:37 +02:00
Gregor Vostrak
1e7364fc4b show calendar activities more prominently when no time entry exists 2026-04-08 14:43:09 +02:00
Gregor Vostrak
8cbc9838c9 fix minimal layout shift on time entry select and migrate to ui button 2026-04-07 21:42:34 +02:00
Gregor Vostrak
71c8992e31 Fix getLocalizedDayJsFromMinutes handling negative minute values 2026-03-31 13:56:30 +02:00
Gregor Vostrak
53d91b65d6 fix: use timezoned dates in public report endpoint tests
Replace travelTo + now() with Carbon::now($timezone)->startOfDay() to eliminate flakiness when tests run near midnight UTC, where the UTC and Vienna dates can differ.
2026-03-31 13:21:54 +02:00
Gregor Vostrak
0c88a10eb5 improve calendar current day styling 2026-03-30 00:58:40 +02:00
Gregor Vostrak
dd7b23958a fix gotenberg url in CI 2026-03-30 00:07:57 +02:00
Gregor Vostrak
1eb066f5aa Add E2E test for project name prefill 2026-03-29 23:55:10 +02:00
ShrootBuck
b1287c6a0a Prefill project name in create modal
Add optional initialProjectName prop to ProjectCreateModal and use it
to initialize the project's name. Pass the TimeTracker dropdown's
searchValue as initial-project-name so the create form is prefilled.
2026-03-29 23:55:10 +02:00
Gregor Vostrak
815abb5980 improve drag handle hit area and activity tooltip placement 2026-03-29 23:14:01 +02:00
Gregor Vostrak
e2f859be27 fix calendar scroll down on load; bump ui package version 2026-03-29 23:02:22 +02:00
Gregor Vostrak
3d26fcaefe Fix DST-related timezone offset when creating/resizing/dragging calendar
events
2026-03-29 22:55:50 +02:00
Gregor Vostrak
1e73a90f9d chore: bump ui version 2026-03-29 22:09:01 +02:00
Gregor Vostrak
0f8f906e5c clarify naming on activity type 2026-03-27 00:37:29 +01:00
Gregor Vostrak
797fddf638 chore: Add zod/type deps and tighten TimeTracker types 2026-03-24 17:41:26 +01:00
Gregor Vostrak
d07294ae7c add zodios to external ui package dependencies 2026-03-23 19:55:26 +01:00
Gregor Vostrak
1f49940805 Use Bundler moduleResolution and add PostCSS config for ui package 2026-03-23 19:38:07 +01:00
Gregor Vostrak
6be6a48e0d Use relative cn imports in UI package to improve isolation and fix
package build
2026-03-23 19:16:31 +01:00
Gregor Vostrak
b94a04dca0 Move useCssVariable into ui package 2026-03-23 19:02:20 +01:00
Gregor Vostrak
bd3b8f265f chore: cleanup old tabs reexports and ui version bump 2026-03-23 17:57:28 +01:00
Gregor Vostrak
c19a0f9acc Move tabs and TabBar into UI package 2026-03-23 17:43:46 +01:00
Gregor Vostrak
5c6d84dc38 fix e2e tests timing issues with cut off time entries at the start of
the day
2026-03-23 17:43:46 +01:00
Gregor Vostrak
5c67709746 Add clearable DatePicker and report tests 2026-03-23 17:43:46 +01:00
Gregor Vostrak
a2b0828c54 Fix flaky e2e tests for calendar and projects 2026-03-23 17:43:46 +01:00
Gregor Vostrak
b94872b07b Add size prop to DatePicker and fix range end 2026-03-23 17:43:46 +01:00
Gregor Vostrak
12bbbf64e9 Add context menu actions and tests 2026-03-23 17:43:46 +01:00
Gregor Vostrak
c07ac4b0e4 add random identifier to exports to avoid path conflicts, fixes #1035 2026-03-23 17:43:46 +01:00
Gregor Vostrak
a58566d002 fix design inconsistencies in time entry edit modal 2026-03-23 17:43:46 +01:00
Gregor Vostrak
57ed6036e6 Add context menu to time entry rows 2026-03-23 17:43:46 +01:00
Gregor Vostrak
ef7569b63b only show calendar toolbar after load complete to avoid layout shift 2026-03-23 17:43:46 +01:00
Gregor Vostrak
19c789b78e fix flaky firefox e2e test 2026-03-23 17:43:46 +01:00
Gregor Vostrak
49548037b3 fix calendar and calendar settings e2e test regressions after migration 2026-03-23 17:43:46 +01:00
Gregor Vostrak
97df779d1e Use locale-aware parseTimeInput for duration inputs 2026-03-23 17:43:46 +01:00
Gregor Vostrak
a1d5563fc4 fix window type error for activity test data injection 2026-03-23 17:43:46 +01:00
Gregor Vostrak
c94ca804f8 add Progress component and Reorganize UI exports 2026-03-23 17:43:46 +01:00
Gregor Vostrak
189682cfaf Replace FullCalendar with custom calendar UI 2026-03-23 17:43:46 +01:00
Gregor Vostrak
8d16503541 Adjust UI sizing and spacing 2026-03-23 17:43:46 +01:00
Gregor Vostrak
e43ce477b8 externalize npm packages in ui package 2026-03-23 17:43:46 +01:00
Gregor Vostrak
5646aedb25 add lucide-vue-next to peer dependencies 2026-03-23 17:43:46 +01:00
Gregor Vostrak
2b46e568e0 Use nearest-grid snapping for event resize 2026-03-23 17:43:46 +01:00
Gregor Vostrak
89a4a1962a Replace fullcalendar calendar header with custom toolbar 2026-03-23 17:43:46 +01:00
Gregor Vostrak
c581ad8854 move calendar, dropdown-menu, select, dialog, number-field components to
the ui package
2026-03-23 17:43:46 +01:00
Gregor Vostrak
bce6cb9395 Move dropdown menu into UI package 2026-03-23 17:43:46 +01:00
Gregor Vostrak
1cdae98ed9 Add context menu actions for running entries in calendar 2026-03-23 17:43:46 +01:00
Gregor Vostrak
02f6436fd0 keep calendar event data while resizing event 2026-03-23 17:43:46 +01:00
Gregor Vostrak
452acca942 add context menus to calendar view + ui package 2026-03-23 17:43:46 +01:00
Gregor Vostrak
192c8c3b88 fix IDOR private projects 2026-03-19 13:52:28 +01:00
Gregor Vostrak
6218ffceb5 update composer dependencies 2026-03-03 12:27:42 +01:00
Gregor Vostrak
ba32be0543 update npm dependencies 2026-03-02 18:19:11 +01:00
Gregor Vostrak
bd817db06f only use xsrf token for organization requests 2026-03-02 17:18:21 +01:00
Gregor Vostrak
97f4bce676 bump retries and wait for networkidle in retry 2026-03-02 17:18:21 +01:00
Gregor Vostrak
6962b668fb add retries to api data token setup and xsrf token fallback 2026-03-02 17:18:21 +01:00
Gregor Vostrak
be8091296c use api tokens to create e2e test data 2026-03-02 17:18:21 +01:00
Gregor Vostrak
84c4750c9b Add warning for AI slop pull requests
Added a warning about AI slop pull requests and potential bans.
2026-02-27 20:18:44 +01:00
Gregor Vostrak
f582adab0d fix time entries incorrectly not updating in calendar
the synced snapDuration cause incorrect noops on updates f.e. 15:55-16:00 on a 15 minute snap
2026-02-24 19:38:55 +01:00
Gregor Vostrak
c60cff04ce fix calendar flickering on move for non-aligned entries
this is a trade-off where for non grid aligned entries, the cursor position is a bit off, but data and visual are stil in sync. otherwise fc overrides height on drag, causing flickers.
2026-02-24 15:30:18 +01:00
Gregor Vostrak
cae41e4b4f improve visual snapping boundaries 2026-02-24 14:02:18 +01:00
Gregor Vostrak
8973be9dab filament minor version update 2026-02-24 13:43:21 +01:00
Gregor Vostrak
2a0b8d31e6 add calendar settings + custom visual snapping 2026-02-24 12:41:15 +01:00
Gregor Vostrak
d2f3fe411a add missing query invalidation after report create 2026-02-18 23:58:39 +01:00
Gregor Vostrak
f880f9f730 fix firefox flaky input in e2e test 2026-02-18 23:22:04 +01:00
Gregor Vostrak
556bbedeca add dynamic loading of paginated endpoints above page_limit
add request classes and fix collection typing for clients, tasks and tags
2026-02-18 22:32:56 +01:00
Gregor Vostrak
eed638d0aa add default sorting to task, project, member, invitation, api token endpoints 2026-02-18 19:16:14 +01:00
Gregor Vostrak
864f41bda6 fix project member query invalidations after update, query key change regression 2026-02-18 18:51:21 +01:00
Gregor Vostrak
26524c5f40 fix member edit modal ui regression from field component migration 2026-02-18 17:57:11 +01:00
Gregor Vostrak
cf98fabe0a add table sorting to members, clients and tags table 2026-02-18 17:41:36 +01:00
Gregor Vostrak
88c0c334e9 add project progress sorting and fix direction ui for number based
columns in the project table
2026-02-18 16:45:17 +01:00
Gregor Vostrak
0fc325363d update query keys to include org id, preventing stale data after organization switch 2026-02-18 12:53:22 +01:00
Gregor Vostrak
1afc16573a cleanup postcss config dependency in ui package 2026-02-17 18:06:35 +01:00
Gregor Vostrak
147514a606 convert billable query string to boolean for shared report + e2e tests #876 2026-02-17 17:08:38 +01:00
Gregor Vostrak
435522b502 make OrganizationPolicy use “organizations:update” to remove jetstream inconsistencies
The frontend did not show organization settings for admin users because of the team ownership check
2026-02-17 14:35:52 +01:00
Gregor Vostrak
f1d001e03e add lazy loading to modals and dropdowns to improve time page render performance 2026-02-17 13:54:26 +01:00
Gregor Vostrak
7f145cf1c2 make sure cost column shows in shared report view, #1019 2026-02-17 13:42:22 +01:00
Gregor Vostrak
b579ed1075 bump ui package version to 0.0.16 2026-02-16 18:31:11 +01:00
Gregor Vostrak
ed2b7476ae clear inertia cache on organization change to fix wrongly loaded stale pages 2026-02-16 16:44:20 +01:00
Gregor Vostrak
8107c6a208 improve activitygraphcard chart coloring steps 2026-02-16 15:29:46 +01:00
Gregor Vostrak
6dc517e07d make sure days with low tracked time are clearly distinguished from no time in activity graph, fixes #447 2026-02-16 15:24:50 +01:00
Gregor Vostrak
2c60d04ba4 override current_team_id in makeMemberToPlaceholder to avoid fk constraint error on user delete, fixes #989 2026-02-16 15:02:42 +01:00
Gregor Vostrak
2c222f3f67 fix time loading spinner flashing “no time entries” on direct load 2026-02-13 15:35:54 +01:00
Gregor Vostrak
c5c1a7af13 add project and task prefetches to the dashboard prefetch 2026-02-13 13:52:49 +01:00
Gregor Vostrak
22cf7cf74d limit initially loaded time entries on the time page to 50 2026-02-13 13:44:33 +01:00
Gregor Vostrak
cfbfbd4b6a remove no tags option from timetracker tag dropdown 2026-02-13 12:30:54 +01:00
Gregor Vostrak
6629482a0e set maximum-scale=1 to prevent weird ios zoom behaviours 2026-02-12 18:12:05 +01:00
Gregor Vostrak
38457cae4d make sure e2e tests use the visible timer button only 2026-02-12 17:43:04 +01:00
Gregor Vostrak
0e63ecb520 improve timetracker on mobile; fix select all checkbox with 0 time
entries; add minimal padding to mobile dialogs
2026-02-12 17:06:20 +01:00
Gregor Vostrak
6f207a4926 hide "All time entries are loaded" when no time entries are created yet 2026-02-12 13:58:08 +01:00
Gregor Vostrak
052424a581 add animation to the mobile sidebar 2026-02-12 13:51:53 +01:00
Gregor Vostrak
b258717211 improve reporting page responsive layout; standardize button sizing;
prevent mobile input zoom; increase CI playwright shards
2026-02-12 13:30:11 +01:00
Gregor Vostrak
685cc29282 improve layout consistency between project and project show page, fix
client status indicator, fixes #814
2026-02-11 18:17:08 +01:00
Gregor Vostrak
c78c681ec4 Conditionally show cost column in report tables; Task/Project Modal
Field cleanup; improve estimated time UX
2026-02-11 17:29:41 +01:00
Gregor Vostrak
2d9f33387e improve format settings e2e test consistency; improve euro icon sizing
consistency
2026-02-11 17:29:41 +01:00
Gregor Vostrak
b68d68a2a2 make sure that 404 current time entry requests do not override local
state while preparing new time entry
2026-02-11 17:29:41 +01:00
Gregor Vostrak
a9e03f3b29 responsive time entry modal fixes 2026-02-11 17:29:41 +01:00
Gregor Vostrak
474b294a18 fix reporting tab selectors in e2e test 2026-02-11 17:29:41 +01:00
Gregor Vostrak
334a98016f use frankenphp in the playwright tests CI to handle parallel requests
better
2026-02-11 17:29:41 +01:00
Gregor Vostrak
8be55359ce add e2e tests for employee restrictions 2026-02-11 17:29:41 +01:00
Gregor Vostrak
e45662c715 add sharding for e2e tests in CI 2026-02-11 17:29:41 +01:00
Gregor Vostrak
f3217baed1 Add Tag Edit Modal and UI 2026-02-11 17:29:41 +01:00
Gregor Vostrak
562ee234a8 Add Euro Symbol as Billable Icon when EUR is the organization currency.
fixes #423
2026-02-11 17:29:41 +01:00
Gregor Vostrak
15e61e9789 Add Field component system and migrate UI 2026-02-11 17:29:41 +01:00
Gregor Vostrak
125f6f062f Expand e2e test coverage migrate to API-based data setup 2026-02-11 17:29:41 +01:00
Gregor Vostrak
f75a19bccd improve time estimate input, responsive time entry create modal fixes,
fixes #460, #800
2026-02-11 17:29:41 +01:00
Gregor Vostrak
c17d87b710 Allow updating public_until on already-public reports 2026-02-11 17:29:41 +01:00
Gregor Vostrak
a154293348 migrate datepickers to shadcn, Fixes #877, #807 2026-02-11 17:29:41 +01:00
Gregor Vostrak
9832c688fe fix desync of checkboxes on the reporting detailed page, fixes #892 2026-02-11 17:29:41 +01:00
Gregor Vostrak
6804eb098d Make sure that time entry billable status updates when project changes,
fixes #981
2026-02-11 17:29:41 +01:00
Gregor Vostrak
531443f0df fix admin panel time entry save and update, fixes #997 2026-02-11 17:29:41 +01:00
Gregor Vostrak
bd2d57dfd1 Improve Time page responsiveness and compact tags, fixes #896 2026-02-11 17:29:41 +01:00
Gregor Vostrak
73c92fad47 fix responsive issues in timetracker recently tracked entries dropdown 2026-02-11 17:29:41 +01:00
Gregor Vostrak
537a023ab9 Add calendar query prefetch 2026-02-11 17:29:41 +01:00
Gregor Vostrak
28fc324c6a Allow NONE filter value to shared reports and add shared-report tests 2026-02-11 17:29:41 +01:00
Gregor Vostrak
9379c191be Add Mailpit SMTP and refine Playwright tests 2026-02-11 17:29:41 +01:00
Gregor Vostrak
ff06d4d2f3 fix Y-Label ui regression from echarts update 2026-02-11 17:29:41 +01:00
Gregor Vostrak
7efb7e6071 Enable npm workspaces and update dependencies 2026-02-11 17:29:41 +01:00
Gregor Vostrak
b2af9c6bf1 Add client_ids filter to time entry export 2026-02-11 17:29:41 +01:00
Gregor Vostrak
73b4d66386 Add reporting e2e helpers and detailed tests 2026-02-11 17:29:41 +01:00
Gregor Vostrak
cb7baef0ba Update openapi api client spec 2026-02-11 17:29:41 +01:00
Gregor Vostrak
dd75a80df7 add no project, no task, no client, no task, no tag support to the API 2026-02-11 17:29:41 +01:00
Gregor Vostrak
bc562bf76f refactor: extract ReportingFilterBar and migrate reporting to TanStack Query 2026-02-11 17:29:41 +01:00
Gregor Vostrak
756b423295 migrate select/multiselect components to Radix Vue primitives 2026-02-11 17:29:41 +01:00
Gregor Vostrak
3707f2469c fix styling inconsistencies 2026-02-11 17:29:41 +01:00
Gregor Vostrak
c6c1434430 fix: display custom billable rate correctly on project detail page 2026-02-11 17:29:41 +01:00
Gregor Vostrak
70b78e41c3 add command palette 2026-02-11 17:29:41 +01:00
Gregor Vostrak
8c16302f17 add outline and secondary variants to TimeTrackerStartStop button to reduce visual complexity 2026-02-11 17:29:41 +01:00
Gregor Vostrak
bfc369794e remove redundant projects pinia store after tanstack query migration 2026-02-11 17:29:41 +01:00
Gregor Vostrak
3c2ea0e645 load time entries above pagination limit for calendar, fixes #995 2026-02-11 17:29:41 +01:00
Gregor Vostrak
b0d28f2f6d fix e2e project filtering in reporting e2e test 2026-02-11 17:29:41 +01:00
Gregor Vostrak
6555bca5f1 use tanstack query in ProjectMultiselectDropdown, ClientTableRow and ProjectDropdown; fix e2e 2026-02-11 17:29:41 +01:00
Gregor Vostrak
81d9561656 refactor timeentries queries and mutations, improve activitygraph, add dashboard reporting table 2026-02-11 17:29:41 +01:00
Gregor Vostrak
0a6bde8bc6 upgrade inertia v2; add prefetching; migrate queries to tanstack query
vue
2026-02-11 17:29:41 +01:00
Constantin Graf
51af3db305 Add test to TimeEntryEndpointTest 2026-01-28 12:56:58 +01:00
Gregor Vostrak
f242ce48b5 change rounding up on boundaries so it does not round up but keeps the value, fixes #994 2026-01-28 12:56:58 +01:00
Gregor Vostrak
19064cdc3d make time entry calendar use seconds as a duration basis, fixes #996 2026-01-15 17:07:50 +01:00
Gregor Vostrak
5a05ee35e0 change dashboard card colors and input background colors 2026-01-09 01:16:23 +01:00
Gregor Vostrak
00d9d1488e improve time entry heading contrast in light mode 2026-01-08 20:17:54 +01:00
Gregor Vostrak
9bbbfdfafe improve visual hierarchy in time view 2026-01-08 19:53:53 +01:00
Gregor Vostrak
d27f023e16 refactor BaseFilterBadge to use DropdownMenuTrigger directly and avoid class merging conflicts 2026-01-08 19:14:59 +01:00
Gregor Vostrak
db57055941 add filters and sorting to projects table 2026-01-08 18:07:17 +01:00
Gregor Vostrak
743c64909a restrict time entries create endpoints for employees to only projects where they have access to 2025-12-17 12:54:07 +01:00
Gregor Vostrak
de97d15925 add tailwind theme and css variables to files export, bump ui package version 2025-12-09 16:44:55 +01:00
Gregor Vostrak
0691fe10ef add direct axios dependency to package, bump package versions 2025-12-09 16:44:55 +01:00
Gregor Vostrak
513b2048ee move TimezonMismatchModal to ui package 2025-12-09 16:44:55 +01:00
Gregor Vostrak
3acf9b8b07 add support for window activities in the calendar view plugin 2025-12-09 16:44:55 +01:00
Gregor Vostrak
814d539fb0 move rangecalendar, popover and daterangepicker to ui package 2025-12-09 16:44:55 +01:00
Gregor Vostrak
7a51fca2f9 only show Weekly Billable Amount of current organization on dashboard, fixes #977 2025-12-02 13:30:08 +01:00
Gregor Vostrak
280032ee02 allow employee manage task setting to organization 2025-11-25 15:39:20 +01:00
Gregor Vostrak
b1bb7245b0 use default api limit for fetching time entries 2025-11-20 17:30:13 +01:00
Gregor Vostrak
6f37ad500a limit initially loaded time entries on time page 2025-11-20 16:58:53 +01:00
Gregor Vostrak
500ccd5719 fix container queries for time entry rows 2025-11-20 16:52:08 +01:00
Gregor Vostrak
bacd6f4222 include the currently running time entry in the calendar header 2025-11-20 13:17:48 +01:00
Gregor Vostrak
022caf59ee bump solidtime ui package version to 0.0.13 2025-11-19 17:34:21 +01:00
Gregor Vostrak
f955ab3135 fix display problems caused by minimum height of calendar events 2025-11-19 17:34:21 +01:00
Gregor Vostrak
5b491b0da2 add support for currently running time entry 2025-11-19 17:34:21 +01:00
Gregor Vostrak
249ab67ac8 improve idle indicator colors, fix typescript issues 2025-11-19 17:34:21 +01:00
Gregor Vostrak
1bd2c28b37 add tooltips to idlestatus indicators 2025-11-19 17:34:21 +01:00
Gregor Vostrak
33ac994cc0 add activity status plugin to calendar 2025-11-19 17:34:21 +01:00
Gregor Vostrak
8d3ee58bed improve initial mount performance for groupedtimeentrytable by streaming in the rows
mounting the rows mounts lots of nested components which results in a delay on the initial mount.
2025-11-19 17:34:21 +01:00
Gregor Vostrak
8a2c260533 use container queries for time entry table 2025-11-19 17:34:21 +01:00
Gregor Vostrak
95ab1699c4 make sure that CreateTimeEntry modal always starts with times that have 0 seconds 2025-11-19 17:34:21 +01:00
Gregor Vostrak
306a081a3d prevent seconds update on timepicker when nothing else changes 2025-11-19 17:34:21 +01:00
Gregor Vostrak
878ac4ab81 add tooltip component 2025-11-19 17:34:21 +01:00
Gregor Vostrak
947550d639 move css variables and tailwind theme config into ui package 2025-11-19 17:34:21 +01:00
Gregor Vostrak
09fb5aa48e make sure that timepicker and calendar set seconds to 0 on update, fixes #968 2025-11-19 17:34:21 +01:00
Gregor Vostrak
9b9371e5a5 move button component to ui package 2025-11-19 17:34:21 +01:00
Gregor Vostrak
0648437478 design fixes, improve component encapsulation 2025-11-19 17:34:21 +01:00
Gregor Vostrak
8ba04eca0c move currency and cancreateproject permission to props to decouple TimeEntryCreateModal from web 2025-11-19 17:34:21 +01:00
Gregor Vostrak
8a2f35de0c fix package build error dependencies 2025-11-19 17:34:21 +01:00
Gregor Vostrak
b7dafb0892 bump api and ui package versions 2025-11-19 17:34:21 +01:00
Gregor Vostrak
6eca0c2c76 fix archived_at timestamp of client in exporter 2025-11-11 12:55:33 +01:00
Gregor Vostrak
3417b60585 only run self-hosting update and telemetry scheduler when app_key is set 2025-11-04 13:35:12 +01:00
Constantin Graf
0f21fabd37 Spread self-hosting update and telemetry requests over the day 2025-11-03 20:24:52 +01:00
Gregor Vostrak
df00200464 load current member time entries in calendar, to be consistent with time view 2025-10-22 14:36:21 +02:00
Gregor Vostrak
3b41de7135 remove project default listener in timeentry edit modal 2025-10-22 13:55:06 +02:00
Gregor Vostrak
9fe0ea5a0f add support for HH:mm:ss format for input time fields 2025-10-22 13:54:14 +02:00
Gregor Vostrak
f8f708a664 add set end time functionality to timetracker component 2025-10-21 17:24:46 +02:00
Gregor Vostrak
c359259e45 fix TimeRangeSelector dropdown behaviour when clicking after other input was focused before 2025-10-21 13:50:30 +02:00
Gregor Vostrak
55d12aaae1 add discard option for running timer 2025-10-21 12:49:49 +02:00
Alexander Groß
9a1dd4861c Extend description to 5000 chars, closes #914 2025-10-21 12:36:32 +02:00
Gregor Vostrak
1e985b71ec move Client visibleByEmployee logic from controller to model 2025-10-21 12:22:17 +02:00
Alexander Groß
93d6a86f74 Show clients that are assigned to the employee, closes #893 2025-10-21 12:20:28 +02:00
Gregor Vostrak
19a206d57c add prevent_overlapping_time_entries setting to organization
when enabled users are blocked from creating or editing new time entries that are overlapping with other time entries
2025-10-13 14:23:41 +02:00
Gregor Vostrak
c0788c270b fix typescript openapi mapping types 2025-10-07 17:42:44 +02:00
Gregor Vostrak
7765056074 add tag grouping 2025-10-07 17:15:20 +02:00
Kaspar Rosin
639f5332e4 feat: add duplicate time entry fields 2025-10-07 17:10:22 +02:00
Gregor Vostrak
4a50145329 fix calendar header timezone issue 2025-10-06 19:30:58 +02:00
Gregor Vostrak
8aabffd1e7 fix race condition in UserTimezoneMismatchModal 2025-10-06 18:33:57 +02:00
Gregor Vostrak
b373427dc7 add feedback button in sidebar 2025-10-01 13:20:23 +02:00
Gregor Vostrak
d2a4d60441 clarify UserSettingsIcon Dropdown Profile Settings Item Description 2025-10-01 13:20:23 +02:00
Gregor Vostrak
c3305b3df6 remove bottom padding for toast container
This became redundant due to the floating feedback bubble removal
2025-10-01 13:20:23 +02:00
Gregor Vostrak
7584e59d0b improve focus states and keyboard navigation for organization switcher and user settings dropdown 2025-10-01 13:20:23 +02:00
Gregor Vostrak
d2f75cca6e update organization switcher to use shadcn dropdownmenu 2025-10-01 13:20:23 +02:00
Gregor Vostrak
250379d4bd change profile dropdown to shadcn, add feedback entry 2025-10-01 13:20:23 +02:00
Gregor Vostrak
7f89fd8ea1 fix overflow issues in short calendar events 2025-09-29 12:19:27 +02:00
Gregor Vostrak
0b45f3b473 change create bucket script to work with new minio client versions 2025-09-29 12:09:15 +02:00
Gregor Vostrak
9827a74ae2 lock caddy version to 2.10 to fix docker buiilds 2025-09-08 13:49:43 +02:00
Gregor Vostrak
3425847a44 make time entry create in calendar use minimal interval instead of 1h duration 2025-09-08 13:28:36 +02:00
Gregor Vostrak
47b778fab9 make sure that 0 duration entries are shown correctly in calendar 2025-09-08 13:28:36 +02:00
Gregor Vostrak
85d69f1f16 fix scroll overflow issue in calendar with banner 2025-09-08 13:28:36 +02:00
Gregor Vostrak
fca55fe0e1 improve calendar fetching behaviour to always include prev/next period 2025-09-08 13:28:36 +02:00
Gregor Vostrak
f19abb9db6 make calendar fetch time ranges respect user timezone 2025-09-08 13:28:36 +02:00
Gregor Vostrak
e3bd50ed6b improve contrast of calendar events 2025-09-08 13:28:36 +02:00
Gregor Vostrak
c582530899 add edit time entry dropdown option to timeentryrow 2025-09-08 13:28:36 +02:00
Gregor Vostrak
fb5185a32f fix card background active color contrast in light mode 2025-09-08 13:28:36 +02:00
Gregor Vostrak
0a0854f771 fix recently tracked time entries card placeholders 2025-09-08 13:28:36 +02:00
Gregor Vostrak
4e635cde83 add support for week_start and time_format in calendar
also rename them so that they do not conflict with the datepicker calendar component
2025-09-08 13:28:36 +02:00
Gregor Vostrak
9fa9522237 add calendar view 2025-09-08 13:28:36 +02:00
Gregor Vostrak
04c44097d0 fix duplicated borders in time and detailed reporting view 2025-09-08 13:28:36 +02:00
Gregor Vostrak
3d5a0cb974 add timezone mismatch modal 2025-09-08 13:28:36 +02:00
Constantin Graf
da98e0571c Add on premise build 2025-08-12 16:59:52 +02:00
Constantin Graf
f68f05d1aa Updated the PR template 2025-07-31 14:01:17 +02:00
Gregor Vostrak
8fdc4c1219 add contributing notice that you need to run the format command 2025-07-31 14:01:17 +02:00
Gregor Vostrak
93148299a9 add CONTRIBUTING.md 2025-07-31 14:01:17 +02:00
Constantin Graf
78d2ea1a25 Add API doc description for chart endpoints 2025-07-31 13:43:00 +02:00
Constantin Graf
14f559c4c2 Removed FORWARD_WEB_PORT from local setup 2025-07-31 13:42:37 +02:00
Gregor Vostrak
61fd2b1187 update font-face file names for font loading 2025-07-31 12:08:51 +02:00
Gregor Vostrak
9ea3c5dc29 fix font embeds #864 2025-07-31 11:53:32 +02:00
Gregor Vostrak
cb30487a21 add format check, update prettier rules, apply rules consistently 2025-07-31 11:53:00 +02:00
Constantin Graf
b11672732b Fixed modules service providers 2025-07-23 16:11:34 +02:00
Gregor Vostrak
97dcadc795 add frontend blocking for rounding for non-premium users 2025-07-23 16:09:36 +02:00
Constantin Graf
e7fa414c06 Restrict rounding to premium users 2025-07-23 16:09:36 +02:00
Gregor Vostrak
43073b5be2 fix design inconsistency in timeentryaggregaterow 2025-07-18 16:38:09 +02:00
Gregor Vostrak
9589c9106d e2e: make sure reporting tests do not check the dropdown values when verifying table results 2025-07-17 18:41:48 +02:00
Gregor Vostrak
8a0d2235a8 fix flakyness in e2e tests for reporting 2025-07-17 18:38:21 +02:00
Gregor Vostrak
38f38790d5 change font to inter, scale down fonts, improve rounding/filter elements 2025-07-17 18:38:21 +02:00
Gregor Vostrak
e3cfc155b8 add rounding frontend to reports, and support for shared reports 2025-07-17 18:38:21 +02:00
Constantin Graf
4b726635b2 Add rounding feature 2025-07-17 18:38:21 +02:00
Constantin Graf
e1185af281 Fixed failing tests because of legacy currency codes 2025-07-17 18:16:25 +02:00
Constantin Graf
f9c0d64f82 Add email notifications for expiring api tokens 2025-07-17 18:16:25 +02:00
Constantin Graf
3d58f570bd Fixed Laravel passport migrations 2025-07-17 11:47:34 +02:00
Constantin Graf
400bc434b9 Updated docker image 2025-07-17 11:47:34 +02:00
Constantin Graf
2ab28001be Updated dependencies; Major update laravel passport 2025-07-17 11:47:34 +02:00
Gregor Vostrak
62d2f4bf4e fix broken light mode on oauth page #842 2025-07-15 15:52:55 +02:00
Gregor Vostrak
3d4b20f7c8 make sure time entry information remains visible on mobile views 2025-07-08 18:22:18 +02:00
Gregor Vostrak
155ed62fcc add clearable option to calendardateinput, fix format, add paid_date 2025-07-08 18:22:18 +02:00
Gregor Vostrak
5daa6f2a25 fix last 7 days statistic labels 2025-07-08 18:22:18 +02:00
Constantin Graf
47aa65d959 Add checks for placeholder invitation; Fixed bug in member deletion 2025-07-08 16:49:05 +02:00
Gregor Vostrak
b0e638c28b fix daterange presets, fix e2e test 2025-06-30 12:54:22 +02:00
Gregor Vostrak
24b62d4643 add information about placeholders in delete modal 2025-06-30 12:54:22 +02:00
Gregor Vostrak
dd928508fd add delete modal for member delete with relations
allow admins to delete members
fix Dialog cloes on click outside of content
2025-06-30 12:54:22 +02:00
Constantin Graf
ead9cf2185 Add option to delete members with relations 2025-06-30 12:54:22 +02:00
Gregor Vostrak
7578beb271 fix css variables not updating correctly when system theme changes 2025-06-24 15:43:49 +02:00
Constantin Graf
dc21ac8352 Switch organization after accepting invitation 2025-06-10 11:23:53 +02:00
Constantin Graf
4de7868851 Add postgres version matrix to phpunit tests 2025-06-04 21:43:35 +02:00
dependabot[bot]
ffc016a1ec Bump codecov/codecov-action from 5.4.2 to 5.4.3
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.4.2...v5.4.3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-22 18:32:13 +02:00
Constantin Graf
be69626970 Add permissions to all GitHub actions 2025-05-22 11:04:37 +02:00
Gregor Vostrak
f1dce88dab fix time zone issue in daterangepicker 2025-05-21 12:34:02 -07:00
Constantin Graf
15411ec0c8 Add resend verification email to filament resource 2025-05-19 11:50:40 +02:00
Constantin Graf
48f09421d0 Fixed time entries exports for employees #2 2025-05-16 15:14:22 +02:00
Constantin Graf
36caadeb14 Fixed time entries exports for employees 2025-05-16 13:20:23 +02:00
Gregor Vostrak
b4edcaa2dc hide shared reports create for employees, fix export request for employees 2025-05-16 13:20:23 +02:00
Constantin Graf
a3dda8b03c Fixed text for clockify import 2025-05-16 13:03:47 +02:00
Constantin Graf
d64f0c52be Fixed bugs in current organization; Add database consistency checks; Add foreign key 2025-05-16 13:03:47 +02:00
Gregor Vostrak
c80d51c2e1 fix sub_group empty type placeholders showing parent type in shared reports view 2025-05-15 13:34:27 +02:00
Gregor Vostrak
46dea00b34 fix user name not displayed correctly for employee users in reporting 2025-05-15 12:54:30 +02:00
Constantin Graf
16fed4a2b7 Add base request class with generic rule sets 2025-05-14 21:07:54 +02:00
Gregor Vostrak
9a2af2e743 respect organization time format settings in api tokens section 2025-05-14 16:21:37 +02:00
Gregor Vostrak
2e3a517502 improve positioning and overflow behaviour of dialogs 2025-05-14 16:03:32 +02:00
Gregor Vostrak
a69fb9c551 make client deselectable for projects, fixes #333 2025-05-14 15:27:28 +02:00
Gregor Vostrak
62b5730fa8 fix contrast on select and dropdown foreground colors, add missing placeholder in billable input 2025-05-14 14:09:19 +02:00
Gregor Vostrak
098ead8da6 change billable rate input to use shadcn component 2025-05-13 18:51:36 +02:00
Constantin Graf
d49082d7f3 Fixed localization in PDF reports 2025-05-13 18:48:37 +02:00
Gregor Vostrak
cc88f034c7 fix sharedreport date_format provide 2025-05-13 17:45:02 +02:00
Gregor Vostrak
9620c89545 migrate daterange picker to shadcn component 2025-05-13 16:32:11 +02:00
Gregor Vostrak
f9c3f42289 improve time entry range design issue in 12-h format 2025-05-13 16:32:11 +02:00
Gregor Vostrak
fca4c26cfc add support for timeFormat in the frontend 2025-05-13 16:32:11 +02:00
Gregor Vostrak
d8f4ba1517 add format options for number field component 2025-05-13 16:32:11 +02:00
Constantin Graf
284d8cd786 Add unit test for currency endpoint 2025-05-13 16:32:11 +02:00
Gregor Vostrak
411fc6ea5e add e2e tests for organization format settings 2025-05-13 16:32:11 +02:00
Gregor Vostrak
02a8367d16 change e2e tests to use organization default values for money formatting 2025-05-13 16:32:11 +02:00
Gregor Vostrak
68f636c8ff fix shared report endpoint test to check new structure that includes organization format properties, format 2025-05-13 16:32:11 +02:00
Gregor Vostrak
9c44abf7aa update api client, add api types, fix activitygraphcard formatting 2025-05-13 16:32:11 +02:00
Gregor Vostrak
b1ff97a82f add frontend support for the date formatting option 2025-05-13 16:32:11 +02:00
Gregor Vostrak
ed32c6b217 add frontend format support for currencies, add currencies endpoint 2025-05-13 16:32:11 +02:00
Gregor Vostrak
8b950d99d6 add support for interval / duration format in frontend views 2025-05-13 16:32:11 +02:00
Constantin Graf
e374d8b3de Fixed typos in organization format settings 2025-05-13 16:32:11 +02:00
Gregor Vostrak
301d09e830 add formating options to organization settings 2025-05-13 16:32:11 +02:00
Constantin Graf
49af3d4371 Fixed missing time in pdf report 2025-05-07 22:13:27 +02:00
Gregor Vostrak
b4a6145f40 fix tanstack query store invalidation on detailed view update 2025-05-07 15:21:23 +02:00
Gregor Vostrak
06c6c874eb respect organization currency setting in shared report 2025-05-06 12:51:28 +02:00
Gregor Vostrak
b796d232f5 add reporting tests for detailed, project filter, billable filter, tag filter 2025-05-05 21:30:18 +02:00
Gregor Vostrak
26c50867b3 fix layout shift in shared reporting view 2025-05-01 12:35:51 +02:00
Constantin Graf
b8110e222a Fixed descriptions and billable in shared reports 2025-04-30 13:36:21 +02:00
Gregor Vostrak
7673b365ca fix light/dark theme not currectly initializing on shared report, unify logic 2025-04-30 13:32:25 +02:00
Gregor Vostrak
da5fc3f113 only show invoicing tab when module is activated 2025-04-30 12:06:48 +02:00
Gregor Vostrak
8c66068663 update openapi api client 2025-04-29 16:38:34 +02:00
Constantin Graf
dd0cc0d60b Add more validation for clockify importer 2025-04-29 16:38:08 +02:00
Gregor Vostrak
3a482c1e6a fix reporting not updating and client ui cue #458 2025-04-28 13:34:08 +02:00
Constantin Graf
ef9f353047 Fixed data type of project and task spend time 2025-04-25 22:32:37 +02:00
Constantin Graf
f1a1d2a266 Project name is now unique per client and organization 2025-04-25 17:55:29 +02:00
Constantin Graf
f5efbad703 Api docs for date time format 2025-04-25 17:55:29 +02:00
Constantin Graf
17242188c2 Updated composer dependencies 2025-04-25 17:55:29 +02:00
dependabot[bot]
0a376b1caa Bump codecov/codecov-action from 5.4.0 to 5.4.2
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-25 11:54:43 +02:00
Gregor Vostrak
10a8310e37 fix stage name in private build action 2025-04-23 15:54:33 +02:00
Gregor Vostrak
89131b9e77 prevent billable rate change modals from immediately sumbitting when pressing enter on the previous form 2025-04-23 14:33:32 +02:00
Gregor Vostrak
c17c5dc6c0 fix escape handling in tagdropdown and timetrackerprojecttaskdropdown after changing to radix dropdowns 2025-04-23 14:33:32 +02:00
Constantin Graf
3444281703 Add composer dependency “league/iso3166” 2025-04-23 14:33:32 +02:00
Gregor Vostrak
84e2365a6d add invoicing extension to private build action 2025-04-23 14:33:32 +02:00
Gregor Vostrak
92ac9948a0 add accordion component and countries api route 2025-04-23 14:33:32 +02:00
Gregor Vostrak
8da358dbe6 fix timeentry checkboxes 2025-04-23 14:33:32 +02:00
Gregor Vostrak
b7b9092e64 update api client, and report empty state improvement 2025-04-23 14:33:32 +02:00
Gregor Vostrak
15ac3e9a43 fix tests, add autofocus disable option for dropdown 2025-04-23 14:33:32 +02:00
Constantin Graf
d03dd60864 Add composer package korridor/laravel-has-many-sync 2025-04-23 14:33:32 +02:00
Constantin Graf
827e0fe377 Fixes for invoice feature 2025-04-23 14:33:32 +02:00
Gregor Vostrak
e78a551098 refactor to shadcn components, dynamically load extension frontend
add jetstream permissions, add dynamic inertia module loading, add shadcn components, change modals and dropdowns to shadcn dismissable layer,
2025-04-23 14:33:32 +02:00
Constantin Graf
ae00fdb0e9 Add localization settings 2025-04-23 14:33:32 +02:00
Constantin Graf
3c9160a08a Removed external font 2025-04-02 13:00:53 +02:00
Constantin Graf
4fb744db1d Fixed timezone issue in PDF reports 2025-04-02 13:00:53 +02:00
Gregor Vostrak
bc9b104c3f fix dropdown highlight color in dark mode 2025-04-01 19:31:01 +02:00
Gregor Vostrak
880c363ae4 fix light mode text color in some 2fa and auth views 2025-04-01 14:47:36 +02:00
Gregor Vostrak
8e6d1abbf3 raise sidebar title contrast, fix profile text colors in light mode 2025-03-30 17:11:30 +02:00
Gregor Vostrak
d202bd9c47 fix light mode icon colors on primary buttons 2025-03-30 16:48:00 +02:00
Gregor Vostrak
992d8945df fix vertical alignment of dropdown triggers (time entry row more) 2025-03-30 16:25:02 +02:00
Gregor Vostrak
df2fe1da1e add light mode 2025-03-28 14:54:31 +01:00
Gregor Vostrak
7339b79e35 invalidate time entries on time tracker stop, fix task text overflow dashboard 2025-03-20 16:47:21 +01:00
Gregor Vostrak
6deb281565 add task information to recently time entries dashboard card 2025-03-20 15:18:12 +01:00
Gregor Vostrak
6ba0b19d40 change dashboard ui to use api instead of inertia props 2025-03-19 15:42:25 +01:00
Constantin Graf
01f6f0f5ea Add chart endpoints 2025-03-19 15:42:25 +01:00
Constantin Graf
aa3c64e496 Allow members:make-placeholder for admins 2025-03-10 16:26:08 +01:00
Gregor Vostrak
eee13897c9 add frontend to deactivate user 2025-03-10 15:43:08 +01:00
Gregor Vostrak
ac6e2b8079 fetch tasks on project show page, fixes #253 2025-03-10 15:43:08 +01:00
Gregor Vostrak
50cc7053e4 hide total billable amounts from employees when employees_can_see_billable_rates is disabled 2025-03-10 15:43:08 +01:00
Constantin Graf
73ce5f793d Fixed problem with merge into when project members already exist in destination member 2025-03-10 15:42:43 +01:00
Constantin Graf
02a716897d Fixed bug in merge into 2025-03-06 15:38:35 -05:00
Gregor Vostrak
e5ec11af44 add member merge frontend modal 2025-03-06 14:44:11 -05:00
Constantin Graf
ab263e725f Fixed bugs in member endpoints; Added merge-into member endpoint 2025-03-06 14:44:11 -05:00
Constantin Graf
f93c5370bf Add harvest and generic imports 2025-03-06 14:44:11 -05:00
dependabot[bot]
9faa8fe6e1 Bump codecov/codecov-action from 5.3.1 to 5.4.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:03:57 -05:00
Gregor Vostrak
9948cb1fc1 add focus loop to tag dropdown to improve focus management 2025-03-05 12:03:37 +01:00
Gregor Vostrak
3026edd27b fix datepicker dropdown and taborder in create time entry 2025-03-05 11:22:57 +01:00
Constantin Graf
b6bbcd7097 Fixed bug in toggl data importer if import contains invalid timezone 2025-03-04 17:08:28 -05:00
Constantin Graf
0d4ffa1061 Fixed GitHub issue templates 2025-02-18 12:21:53 -05:00
Constantin Graf
b7abe3738e Added GitHub issue templates 2025-02-18 11:55:48 -05:00
Constantin Graf
128a21ba63 Fix docker for ARM 2025-02-18 11:55:48 -05:00
Constantin Graf
e25461a439 Fix desktop auth 2025-02-14 10:55:20 -05:00
Gregor Vostrak
ba8751c7c4 add api key e2e tests and improve labels 2025-02-13 17:04:18 -05:00
Gregor Vostrak
21b33a0028 add api token expiry information notices 2025-02-13 17:04:18 -05:00
Gregor Vostrak
97585b5771 fix inconsistencies in dropdown highlighted item, indirectly fix flaky project member test 2025-02-13 17:04:18 -05:00
Constantin Graf
ae76135373 Add filament resource for tokens; Ignore non-personal tokens in API token endpoints 2025-02-13 17:04:18 -05:00
Constantin Graf
69a8c8bb2b Fixed api token endpoint documentation 2025-02-13 17:04:18 -05:00
Gregor Vostrak
4ea55e5867 add frontend support for api token create, delete and revoke 2025-02-13 17:04:18 -05:00
Constantin Graf
bbed618fdc Added API endpoints for user API tokens 2025-02-13 17:04:18 -05:00
Constantin Graf
d924fa74ec Moved force https logic to a middleware; Changed default for config session.secure 2025-02-08 10:40:15 -05:00
Constantin Graf
adf0d35c11 Fix docker image 2025-02-07 17:05:53 -05:00
Gregor Vostrak
4ed8f16ae3 remove duplicates from recently tracked dropdown, improve focus handling 2025-02-07 16:39:39 +01:00
Constantin Graf
0a956fd9e7 Fixed user create in filament 2025-02-06 14:20:37 -05:00
Constantin Graf
09b168cddb Update composer dependencies - minor 2025-02-06 14:00:30 -05:00
Gregor Vostrak
31b9659f7e start time entry on click in recently tracked time entries dropdown 2025-02-06 18:36:16 +01:00
Gregor Vostrak
db7111da44 add recently tracked timeentries dropdown to timetracker 2025-02-06 18:36:16 +01:00
Gregor Vostrak
18ab1f714b update dependencies, update eslint config, update optional ts props types 2025-02-06 18:36:16 +01:00
Gregor Vostrak
00e2518196 fix TimeTrackerRangeSelector detection so it does not open the Dropdown again after pressing Escape 2025-02-06 18:36:16 +01:00
Gregor Vostrak
6f6e5fb4c3 fix time update test to respect new taborder logic 2025-02-06 18:36:16 +01:00
Gregor Vostrak
68228bccb2 fix enter submits in the time range dropdown 2025-02-06 18:36:16 +01:00
Gregor Vostrak
2dd80ba6cc fix focus state for dropdowns, fix taborder for timerange select in timetracker and timeentryrows 2025-02-06 18:36:16 +01:00
Gregor Vostrak
b783ea9ecd improve focus state styling 2025-02-06 18:36:16 +01:00
Constantin Graf
dce608e403 Add more tests; Add filter in filament resource; Added options for user create command 2025-02-06 12:22:19 -05:00
Constantin Graf
84c9cfe2f2 Fixed bugs causing incorrect computed attributes in imported data 2025-02-06 12:22:19 -05:00
Constantin Graf
f14bd6413a Add missing serve option to local filesystem disk 2025-02-06 12:22:19 -05:00
Constantin Graf
eb19199bc6 Updated composer dependencies 2025-02-06 12:22:19 -05:00
Constantin Graf
0252d984cb Added estimated time to clockify project import 2025-02-06 12:22:19 -05:00
Constantin Graf
18162b0ff5 Fixed timezones in unit tests 2025-02-06 12:22:19 -05:00
Constantin Graf
3dab7440dd Updated composer dependencies 2025-02-06 12:22:19 -05:00
Constantin Graf
713e12e54e Fixed reports in deletion service 2025-02-06 12:22:19 -05:00
Constantin Graf
fc0a840ded Deactivated registration 2025-02-06 12:22:19 -05:00
dependabot[bot]
28904b650e Bump aglipanci/laravel-pint-action from 2.4 to 2.5
Bumps [aglipanci/laravel-pint-action](https://github.com/aglipanci/laravel-pint-action) from 2.4 to 2.5.
- [Release notes](https://github.com/aglipanci/laravel-pint-action/releases)
- [Commits](https://github.com/aglipanci/laravel-pint-action/compare/2.4...2.5)

---
updated-dependencies:
- dependency-name: aglipanci/laravel-pint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 12:02:07 -05:00
dependabot[bot]
1d34a77eb2 Bump codecov/codecov-action from 5.1.2 to 5.3.1
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.2 to 5.3.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.1.2...v5.3.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 11:34:37 -05:00
Constantin Graf
49e045809b Enhanced description for Clockify imports 2024-12-20 19:57:50 -05:00
Constantin Graf
e90fa8307f Fixed timezones in unit tests 2024-12-20 19:57:50 -05:00
dependabot[bot]
895540d0a9 Bump codecov/codecov-action from 4.5.0 to 5.1.2
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 5.1.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v5.1.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 19:29:52 -05:00
Constantin Graf
62270382dc Fixed import lock 2024-12-18 11:26:49 -05:00
Constantin Graf
29929467f6 Fixed overlapping labels in PDF report 2024-12-18 11:20:32 -05:00
Gregor Vostrak
02fe89dfdf Update README.md 2024-12-17 17:38:34 +01:00
Gregor Vostrak
03550a0ca6 add request free trial text to upgrade modal 2024-12-17 17:03:54 +01:00
Gregor Vostrak
2f1056dddb change report default to public 2024-12-17 15:21:23 +01:00
Gregor Vostrak
6e226cd743 hide report table for users that do not already have reports and cannot report new ones 2024-12-17 13:03:59 +01:00
Gregor Vostrak
19ed966504 fix icons alignment in billing upgrade buttons 2024-12-17 12:55:29 +01:00
Gregor Vostrak
33818f10b3 improve detailed report so that the table header has a border on the new page 2024-12-09 17:29:44 +01:00
Gregor Vostrak
ee9d818d75 add name of shared report to title attribute 2024-12-09 17:24:04 +01:00
Gregor Vostrak
e3d8457523 add week_start default for unauthenticated shared reports view 2024-12-09 17:11:56 +01:00
Gregor Vostrak
67e42a0a54 improve pdf index export to prevent overflows 2024-12-09 16:58:06 +01:00
Gregor Vostrak
fdbf88a9a6 fix selects inside of focus trap not working on click select 2024-12-09 16:33:57 +01:00
Gregor Vostrak
c4daca32c5 add modal focus trap & fix design bug in project billable section 2024-12-09 15:45:28 +01:00
Gregor Vostrak
4e10f9538f add export modal to prevent firefox popup blocking behaviour 2024-12-09 15:29:44 +01:00
Gregor Vostrak
959cad8f74 fix main chart label not cutting off for big numbers on the top 2024-12-09 12:57:25 +01:00
Gregor Vostrak
e308ca78b1 improve design for time entries index export 2024-12-09 12:57:25 +01:00
Gregor Vostrak
4281736a6d automatically set the project billable default in time entry create modal 2024-12-09 12:57:25 +01:00
Gregor Vostrak
9b0cf37bc7 improve aggregated pdf design 2024-12-09 12:57:25 +01:00
Constantin Graf
a4f3e014d9 Add debug flag to pdf export 2024-12-09 12:57:25 +01:00
Gregor Vostrak
32bce2f749 fix reporting descriptions for nested group 2024-12-09 12:57:25 +01:00
Gregor Vostrak
ae7f5a98e7 add Today option to Date Range Picker 2024-12-09 12:57:25 +01:00
Gregor Vostrak
e3f981aac2 add missing data to public shared reports, add premium restrictions, add pdf download 2024-12-09 12:57:25 +01:00
Constantin Graf
bcb298bd6d Updated dedoc/scramble composer dependency 2024-12-09 12:57:25 +01:00
Constantin Graf
620c4c97dc Updated PDF footer and added pie chart to aggregate report 2024-12-09 12:57:25 +01:00
Constantin Graf
05da595470 Add wait for report with chart 2024-12-09 12:57:25 +01:00
Constantin Graf
a4d8a02b80 Updated PDF reports 2024-12-09 12:57:25 +01:00
Constantin Graf
0860aa9d24 Added shareable reports 2024-12-09 12:57:25 +01:00
Gregor Vostrak
9c82efdf07 add reporting submenus to navbar 2024-12-09 12:57:25 +01:00
Gregor Vostrak
2560619c15 add shared reports section in the frontend 2024-12-09 12:57:25 +01:00
Constantin Graf
c03aad1abd Added shareable reports 2024-12-09 12:57:25 +01:00
Constantin Graf
0ee0175f04 Prevent stray requests in unit tests 2024-12-02 17:40:01 +01:00
Constantin Graf
0c1f06face Change default generate key env to single line 2024-12-02 15:00:29 +01:00
Gregor Vostrak
86d625b18a add discount banner 2024-11-25 13:21:35 +01:00
Constantin Graf
83e17d4a40 Updated composer dependencies 2024-11-16 16:18:06 +01:00
Gregor Vostrak
5b27853546 Add e2e test for live timer 2024-11-15 18:04:39 +01:00
Gregor Vostrak
f49f7b2c9b fix live timer after reload 2024-11-15 16:48:02 +01:00
Constantin Graf
9e77500d94 Extended healthcheck debug in debug mode 2024-11-15 13:17:33 +01:00
Constantin Graf
2cf9b3aa8f Fix force https for some reverse proxies 2024-11-12 21:50:26 +01:00
Constantin Graf
64b41e3018 Fix force https for some reverse proxies, Add url and path to debug endpoint 2024-11-12 19:03:36 +01:00
Gregor Vostrak
31014c1e29 fix type import api reference 2024-11-12 18:58:59 +01:00
Gregor Vostrak
d880717749 add TimeEntryCreateModal and MoreOptionsDropdown to ui package 2024-11-12 18:54:54 +01:00
Gregor Vostrak
df0f3b2680 patch new time entries into existing store when stores are refreshed on focus 2024-11-12 17:38:04 +01:00
Gregor Vostrak
4b0cb2e282 improve time picker parsing, fix nested escape listeners, change project member select 2024-11-12 16:07:51 +01:00
Gregor Vostrak
d5699da234 improve manual time entry modal, improve time picker, add human duration input 2024-11-12 16:07:51 +01:00
Constantin Graf
96f06bae1d Update README.md 2024-11-12 13:52:31 +01:00
Gregor Vostrak
e1243178fe Update README.md 2024-11-12 13:50:33 +01:00
Gregor Vostrak
cfbc98705a add bug report and feature request rules to the README 2024-11-12 13:48:04 +01:00
Gregor Vostrak
f0d6b234e5 add github sponsor information 2024-11-11 17:23:23 +01:00
Constantin Graf
4b622afcfc Change logic of tags_ids filter from AND to OR 2024-11-08 13:28:26 +01:00
Constantin Graf
45daeead61 Fix billable contract for self-hosting 2024-11-07 16:12:42 +01:00
Constantin Graf
95c1bcd4cb Change precheck order in migrations 2024-11-05 12:32:51 +01:00
Constantin Graf
3b3f593080 Fix foreign keys and deletion service 2024-11-05 12:09:04 +01:00
Constantin Graf
4224fdd57e Fixed report for query with no entries 2024-11-01 13:46:22 +01:00
Constantin Graf
f4cfeaa718 Fixed issue with daylight saving time in chart 2024-10-30 17:40:46 +01:00
Constantin Graf
04fcc1e3ae Fixed timezones in detailed export reports #2 2024-10-29 18:25:42 +01:00
Constantin Graf
f145e821a8 Fix incorrect grouping by billable in export report 2024-10-29 18:09:22 +01:00
Constantin Graf
eaaa83406d Fixed timezones in detailed export reports 2024-10-29 18:09:22 +01:00
Constantin Graf
9a60e2b911 Add tests for export endpoints 2024-10-29 17:20:21 +01:00
Gregor Vostrak
5a1e05374c disable pdf export button 2024-10-29 17:20:21 +01:00
Gregor Vostrak
ab4dbd64df add support for history_group and loading indicators to export buttons 2024-10-29 17:20:21 +01:00
Constantin Graf
8712cfb9dc Add report exports 2024-10-29 17:20:21 +01:00
Gregor Vostrak
7c1fe35754 add export buttons for aggregated export and pdf export 2024-10-29 17:20:21 +01:00
Constantin Graf
b0bcc4f330 Add pdf detailed report and placeholder for aggregate endpoint 2024-10-29 17:20:21 +01:00
Gregor Vostrak
5593d141ea automatically select project after create in time tracker component, fixes ST-457 2024-10-29 17:20:21 +01:00
Gregor Vostrak
d080b07e60 add Export download buttons 2024-10-29 17:20:21 +01:00
Constantin Graf
64535ceea6 Add report exports 2024-10-29 17:20:21 +01:00
Gregor Vostrak
e54df74d5d improve typing in solidtime ui package 2024-10-28 14:54:48 +01:00
Constantin Graf
27b40d863e Make email validation on registration stricter 2024-10-28 14:32:27 +01:00
Gregor Vostrak
b41d20839e improve empty state texts for employees 2024-10-28 14:24:40 +01:00
Gregor Vostrak
7acadda6d8 bump ui and api package versions 2024-10-28 14:14:50 +01:00
Gregor Vostrak
cd7573dcf1 hide create project buttons and modal depending on the permission 2024-10-28 14:14:50 +01:00
Gregor Vostrak
eb4debe481 move time entry mass updates to ui package and remove its dependencies 2024-10-28 14:14:50 +01:00
Constantin Graf
fd77e1e901 Fix logo for email client with no SVG support like Gmail 2024-10-28 12:21:57 +01:00
Constantin Graf
401cd4be0a Fixed setting multiple time entry description to an empty string 2024-10-22 16:45:21 +02:00
1052 changed files with 84179 additions and 23968 deletions

67
.env.ci
View File

@@ -1,57 +1,66 @@
# Application
APP_NAME=solidtime
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_FORCE_HTTPS=false
SESSION_SECURE_COOKIE=false
APP_ENABLE_REGISTRATION=true
# Logging
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
# Database
DB_CONNECTION=pgsql_test
DB_TEST_HOST=127.0.0.1
DB_TEST_PORT=5432
DB_TEST_DATABASE=laravel
DB_TEST_USERNAME=root
DB_TEST_PASSWORD=root
BROADCAST_DRIVER=log
# Broadcasting
BROADCAST_DRIVER=null
# Cache
CACHE_DRIVER=file
FILESYSTEM_DISK=local
# Queue
QUEUE_CONNECTION=sync
# Session
SESSION_DRIVER=database
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
# Mail
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
MAIL_FROM_NAME="solidtime"
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
MAIL_REPLY_TO_NAME="solidtime"
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
# Filesystems
FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=public
MAIL_MAILER=log
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
# Passport
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="9e27f54d-5dfb-4dde-99d7-834518236c92"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="EL5mXp3aF8ITjcwoOXRpbSK7zGrWhW4zTDpQXTkf"
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_REGION=us-east-1
S3_BUCKET=
S3_USE_PATH_STYLE_ENDPOINT=false
# Auditing
AUDITING_ENABLED=true
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
# Telescope
TELESCOPE_ENABLED=false
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Services
GOTENBERG_URL=http://localhost:3000
# Octane
OCTANE_SERVER=frankenphp

View File

@@ -1,18 +1,21 @@
# Application
APP_NAME=solidtime
APP_ENV=local
APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
APP_DEBUG=true
APP_URL=https://solidtime.test
AUDITING_ENABLED=true
APP_FORCE_HTTPS=false
APP_ENABLE_REGISTRATION=true
SUPER_ADMINS=admin@example.com
PAGINATION_PER_PAGE_DEFAULT=500
# Logging
LOG_CHANNEL=single
LOG_DEPRECATIONS_CHANNEL=deprecation
LOG_LEVEL=debug
# Database
DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=laravel
@@ -25,18 +28,20 @@ DB_TEST_DATABASE=laravel
DB_TEST_USERNAME=root
DB_TEST_PASSWORD=root
BROADCAST_DRIVER=log
# Broadcasting
BROADCAST_DRIVER=null
# Cache
CACHE_DRIVER=file
# Queue
QUEUE_CONNECTION=sync
# Session
SESSION_DRIVER=database
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
# Mail
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
@@ -44,17 +49,11 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
MAIL_FROM_NAME="${APP_NAME}"
MAIL_FROM_NAME="solidtime"
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
MAIL_REPLY_TO_NAME="solidtime"
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
# Storage
# Filesystems
FILESYSTEM_DISK=s3
PUBLIC_FILESYSTEM_DISK=s3
S3_ACCESS_KEY_ID=sail
@@ -65,18 +64,26 @@ S3_URL=http://storage.solidtime.test/local
S3_ENDPOINT=http://storage.solidtime.test
S3_USE_PATH_STYLE_ENDPOINT=true
VITE_HOST_NAME=vite.solidtime.test
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Passport
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="9e27f54d-5dfb-4dde-99d7-834518236c92"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="EL5mXp3aF8ITjcwoOXRpbSK7zGrWhW4zTDpQXTkf"
# Auditing
AUDITING_ENABLED=true
# Telescope
TELESCOPE_ENABLED=false
# Services
GOTENBERG_URL=http://gotenberg:3000
# Octane
OCTANE_SERVER=frankenphp
# Local setup
NGINX_HOST_NAME=solidtime.test
NETWORK_NAME=reverse-proxy-docker-traefik_routing
FORWARD_DB_PORT=5432
FORWARD_WEB_PORT=8083
PAGINATION_PER_PAGE_DEFAULT=500
FORWARD_DB_PORT=54329
VITE_HOST_NAME=vite.solidtime.test
VITE_APP_NAME="${APP_NAME}"
#SAIL_XDEBUG_MODE=develop,debug,coverage

View File

@@ -5,7 +5,6 @@ VITE_APP_NAME=solidtime
APP_ENV=production
APP_DEBUG=false
APP_FORCE_HTTPS=true
SESSION_SECURE_COOKIE=true
OCTANE_SERVER=frankenphp
PAGINATION_PER_PAGE_DEFAULT=500

View File

@@ -1,13 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
extends: ['plugin:vue/vue3-essential', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier'],
rules: {
'vue/multi-word-component-names': 'off',
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": "error",
},
plugins: ['unused-imports'],
}

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: solidtime-io

47
.github/ISSUE_TEMPLATE/1_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Bug Report
description: "Report a bug"
body:
- type: markdown
attributes:
value: |
Before creating a new bug report, please check that there isn't already a similar issue.
- type: textarea
attributes:
label: Description
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: "Steps To Reproduce"
description: How do you trigger this bug? Please walk us through it step by step.
value: |
1.
2.
3.
...
validations:
required: false
- type: dropdown
attributes:
label: "Self-hosted or Cloud?"
options:
- Self-Hosted
- solidtime Cloud
- Both
- type: input
attributes:
label: "Version of solidtime: (for self-hosted)"
validations:
required: false
- type: input
attributes:
label: "solidtime self-hosting guide: (for self-hosted)"
description: "Did you use the official guide to self-host solidtime? If yes, which one?"
validations:
required: false

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 🚀 Feature Request
url: https://github.com/solidtime-io/solidtime/discussions/new?category=feature-requests
about: Share ideas for new features
- name: ❓ Ask a Question
url: https://github.com/solidtime-io/solidtime/discussions/new?category=general
about: Ask the community for help

11
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,11 @@
## What does this PR do?
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
- Fixes #XXXX (GitHub issue number)
## Checklist (DO NOT REMOVE)
- [ ] I read the [contributing guide](https://github.com/solidtime-io/solidtime/blob/main/CONTRIBUTING.md)
- [ ] I signed the [Contributor License Agreement](https://cla-assistant.io/solidtime-io/solidtime).
- [ ] I commented my code, particularly in hard-to-understand areas

216
.github/workflows/build-onpremise.yml vendored Normal file
View File

@@ -0,0 +1,216 @@
on:
push:
branches:
- main
- develop
tags:
- '*'
pull_request:
paths:
- '.github/workflows/build-onpremise.yml'
- 'docker/prod/**'
workflow_dispatch:
permissions:
packages: write
contents: read
attestations: write
id-token: write
env:
DOCKER_REPO: registry.on-premise.solidtime.io/solidtime/solidtime
name: Build - On Premise
jobs:
build:
strategy:
matrix:
include:
- runs-on: "ubuntu-24.04-arm"
platform: "linux/arm64"
- runs-on: "ubuntu-24.04"
platform: "linux/amd64"
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 90
steps:
- name: "Check out code"
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
- name: "Get build"
id: release-build
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
prefix: "v"
- name: "Get version"
id: release-version
run: |
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
else
echo "ERROR: No previous tag found";
exit 1;
fi
else
version=$(echo "${{ github.ref }}" | cut -c 12-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
fi
- name: "Copy .env template for production"
run: |
cp .env.production .env
rm .env.production .env.ci .env.example
- name: "Add version to .env"
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.release-version.outputs.app_version }}/g' .env
- name: "Add build to .env"
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.release-build.outputs.build }}/g' .env
- name: "Output .env"
run: cat .env
- name: "Setup PHP with PECL extension"
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, dom, fileinfo, pgsql
- name: "Install dependencies"
run: composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
- name: "Use Node.js"
uses: actions/setup-node@v6
with:
node-version: '20.x'
- name: "Checkout invoicing extension"
uses: actions/checkout@v4
with:
repository: solidtime-io/extension-invoicing
path: extensions/Invoicing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
- name: "Install composer dependencies in invoicing extension"
run: cd extensions/Invoicing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
- name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci
- name: "Activate invoicing extension"
run: php artisan module:enable Invoicing
- name: "Install npm dependencies"
run: npm ci
- name: "Build"
run: npm run build
- name: "Prepare"
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: "Docker meta"
id: "meta"
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKER_REPO }}
- name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v3
with:
registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }}
password: ${{ secrets.ONPREMISE_TOKEN }}
- name: "Set up QEMU"
uses: docker/setup-qemu-action@v3
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
- name: "Build and push by digest"
id: build
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
build-args: |
DOCKER_FILES_BASE_PATH=docker/prod/
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.DOCKER_REPO }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: "Export digest"
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: "Upload digest"
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
timeout-minutes: 90
needs:
- build
steps:
- name: "Download digests"
uses: actions/download-artifact@v6
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v3
with:
registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }}
password: ${{ secrets.ONPREMISE_TOKEN }}
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
- name: "Docker meta"
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKER_REPO }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: "Create manifest list and push"
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
- name: "Inspect image"
run: |
docker buildx imagetools inspect ${{ env.DOCKER_REPO }}:${{ steps.meta.outputs.version }}

View File

@@ -10,6 +10,8 @@ on:
- '.github/workflows/build-private.yml'
- 'docker/prod/**'
workflow_dispatch:
permissions:
contents: read
name: Build - Private
jobs:
@@ -17,9 +19,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: "Check out code"
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
@@ -65,12 +68,12 @@ jobs:
run: cat .env
- name: "Use Node.js"
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'
- name: "Checkout billing extension"
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: solidtime-io/extension-billing
path: extensions/Billing
@@ -90,7 +93,7 @@ jobs:
run: cd extensions/Billing && npm ci
- name: "Checkout services extension"
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: solidtime-io/extension-services
path: extensions/Services
@@ -107,6 +110,24 @@ jobs:
- name: "Install npm dependencies in services extension"
run: cd extensions/Services && npm ci
- name: "Checkout invoicing extension"
uses: actions/checkout@v5
with:
repository: solidtime-io/extension-invoicing
path: extensions/Invoicing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
- name: "Install composer dependencies in invoicing extension"
uses: php-actions/composer@v6
with:
working_dir: "extensions/Invoicing"
command: install
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
php_version: 8.3
- name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci
- name: "Setup PHP with PECL extension"
uses: shivammathur/setup-php@v2
with:
@@ -127,6 +148,9 @@ jobs:
- name: "Activate services extension"
run: php artisan module:enable Services
- name: "Activate invoicing extension"
run: php artisan module:enable Invoicing
- name: "Install npm dependencies"
run: npm ci

View File

@@ -11,25 +11,37 @@ on:
- 'docker/prod/**'
workflow_dispatch:
permissions:
packages: write
contents: read
attestations: write
id-token: write
env:
DOCKERHUB_REPO: solidtime/solidtime
GHCR_REPO: ghcr.io/solidtime-io/solidtime
name: Build - Public
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
strategy:
matrix:
include:
- runs-on: "ubuntu-24.04-arm"
platform: "linux/arm64"
- runs-on: "ubuntu-24.04"
platform: "linux/amd64"
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 90
steps:
- name: "Check out code"
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
- name: "Get build"
id: build
id: release-build
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
- name: "Get Previous tag (normal push)"
@@ -40,7 +52,7 @@ jobs:
prefix: "v"
- name: "Get version"
id: version
id: release-version
run: |
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
@@ -61,24 +73,26 @@ jobs:
rm .env.production .env.ci .env.example
- name: "Add version to .env"
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.release-version.outputs.app_version }}/g' .env
- name: "Add build to .env"
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.release-build.outputs.build }}/g' .env
- name: "Output .env"
run: cat .env
- name: "Install dependencies"
uses: php-actions/composer@v6
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
- name: "Setup PHP with PECL extension"
uses: shivammathur/setup-php@v2
with:
command: install
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
php_version: 8.3
php-version: '8.3'
extensions: mbstring, dom, fileinfo, pgsql
- name: "Install dependencies"
run: composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
- name: "Use Node.js"
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'
@@ -88,29 +102,31 @@ jobs:
- name: "Build"
run: npm run build
- name: "Login to GitHub Container Registry"
- name: "Prepare"
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: "Docker meta"
id: "meta"
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_REPO }}
${{ env.GHCR_REPO }}
- name: "Login to Docker Hub Container Registry"
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Docker meta"
id: "meta"
uses: docker/metadata-action@v5
with:
images: |
solidtime/solidtime
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: "Set up QEMU"
uses: docker/setup-qemu-action@v3
@@ -118,16 +134,85 @@ jobs:
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
- name: "Build and push"
- name: "Build and push by digest"
id: build
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
build-args: |
DOCKER_FILES_BASE_PATH=docker/prod/
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: "Export digest"
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: "Upload digest"
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
timeout-minutes: 90
needs:
- build
steps:
- name: "Download digests"
uses: actions/download-artifact@v6
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: "Login to Docker Hub"
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GHCR"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
- name: "Docker meta"
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_REPO }}
${{ env.GHCR_REPO }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: "Create manifest list and push"
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
- name: "Inspect image"
run: |
docker buildx imagetools inspect ${{ env.DOCKERHUB_REPO }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.GHCR_REPO }}:${{ steps.meta.outputs.version }}

View File

@@ -3,6 +3,9 @@ on:
push:
branches:
- main
permissions:
contents: read
jobs:
api_docs:
runs-on: ubuntu-latest
@@ -26,7 +29,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: "Setup PHP"
uses: shivammathur/setup-php@v2

View File

@@ -1,6 +1,8 @@
name: NPM Build
on: [push]
permissions:
contents: read
jobs:
build:
@@ -9,7 +11,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: "Setup PHP (for Ziggy)"
uses: shivammathur/setup-php@v2
@@ -22,7 +24,7 @@ jobs:
run: composer install -n --prefer-dist
- name: "Use Node.js"
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'

23
.github/workflows/npm-format-check.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: NPM Format Check
on: [push]
jobs:
format-check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: "Checkout code"
uses: actions/checkout@v5
- name: "Use Node.js"
uses: actions/setup-node@v6
with:
node-version: '20.x'
- name: "Install npm dependencies"
run: npm ci
- name: "Check code formatting"
run: npm run format:check

View File

@@ -1,6 +1,8 @@
name: NPM Lint
on: [push]
permissions:
contents: read
jobs:
build:
@@ -9,10 +11,10 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: "Use Node.js"
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'

View File

@@ -1,6 +1,8 @@
name: Publish API package to NPM
on:
workflow_dispatch
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -8,11 +10,12 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: "Checkout code"
uses: actions/checkout@v5
# Setup .npmrc file to publish to npm
- name: Install root project dependencies
run: npm ci
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'

View File

@@ -1,6 +1,8 @@
name: Publish UI package to NPM
on:
workflow_dispatch
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -8,9 +10,10 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: "Checkout code"
uses: actions/checkout@v5
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'

27
.github/workflows/npm-test-unit.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: NPM Test Unit
on: [push]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
TZ: UTC
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Use Node.js"
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: "Install npm dependencies"
run: npm ci
- name: "Run vitest"
run: npm run test:unit

View File

@@ -1,7 +1,8 @@
name: NPM Typecheck
on: [push]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -9,7 +10,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: "Setup PHP (for Ziggy)"
uses: shivammathur/setup-php@v2
@@ -22,7 +23,7 @@ jobs:
run: composer install -n --prefer-dist
- name: "Use Node.js"
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'

View File

@@ -1,5 +1,7 @@
name: Static code analysis (PHPStan)
on: push
permissions:
contents: read
jobs:
phpstan:
runs-on: ubuntu-latest
@@ -7,7 +9,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: "Setup PHP"
uses: shivammathur/setup-php@v2

View File

@@ -1,13 +1,18 @@
name: PHPUnit Tests
on: push
permissions:
contents: read
jobs:
phpunit:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
postgres_version: [ 15, 16, 17 ]
services:
pgsql_test:
image: postgres:15
image: postgres:${{ matrix.postgres_version }}
env:
PGPASSWORD: 'root'
POSTGRES_DB: 'laravel'
@@ -20,10 +25,18 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
gotenberg:
image: gotenberg/gotenberg:8
ports:
- 3000:3000
options: >-
--health-cmd "curl --silent --fail http://localhost:3000/health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
@@ -35,7 +48,7 @@ jobs:
- name: "Run composer install"
run: composer install -n --prefer-dist
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: '20.x'
@@ -55,7 +68,7 @@ jobs:
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
- name: "Upload coverage reports to Codecov"
uses: codecov/codecov-action@v4.5.0
uses: codecov/codecov-action@v5.5.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: solidtime-io/solidtime

View File

@@ -1,5 +1,7 @@
name: PHP Linting
on: push
permissions:
contents: read
jobs:
pint:
runs-on: ubuntu-latest
@@ -7,9 +9,9 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: "Check code style"
uses: aglipanci/laravel-pint-action@2.4
uses: aglipanci/laravel-pint-action@2.6
with:
configPath: "pint.json"

View File

@@ -1,13 +1,23 @@
name: Playwright Tests
on: [push]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
services:
mailpit:
image: 'axllent/mailpit:latest'
ports:
- 1025:1025
- 8025:8025
pgsql_test:
image: postgres:15
env:
@@ -25,50 +35,93 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v5
- uses: actions/setup-node@v4
- name: "Setup node"
uses: actions/setup-node@v6
with:
node-version: '20.x'
- name: Setup PHP
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: none
- name: Run composer install
- name: "Run composer install"
run: composer install -n --prefer-dist
- name: Prepare Laravel Application
- name: "Prepare Laravel Application"
run: |
cp .env.ci .env
php artisan key:generate
php artisan migrate --seed
php artisan passport:keys
php artisan migrate --seed
- name: Install dependencies
- name: "Install dependencies"
run: npm ci
- name: Build Frontend
- name: "Build Frontend"
run: npm run build
- name: Run Laravel Server
run: php artisan serve > /dev/null 2>&1 &
- name: "Install FrankenPHP"
run: |
ARCH="$(uname -m)"
curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-${ARCH}" -o /usr/local/bin/frankenphp
chmod +x /usr/local/bin/frankenphp
- name: Install Playwright Browsers
- name: "Run Laravel Octane Server"
run: php artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000 --workers=4 --max-requests=500 > /dev/null 2>&1 &
env:
OCTANE_SERVER: frankenphp
- name: "Install Playwright Browsers"
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: "Run Playwright tests"
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
MAILPIT_BASE_URL: 'http://localhost:8025'
- uses: actions/upload-artifact@v4
- name: "Upload blob report"
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/
retention-days: 30
name: blob-report-${{ matrix.shardIndex }}
path: blob-report/
retention-days: 7
merge-reports:
if: always()
needs: [test]
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Setup node"
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: "Install dependencies"
run: npm ci
- name: "Download blob reports"
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: "Merge reports"
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: "Upload merged HTML report"
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ yarn-error.log
/data
/config/caddy
/config/composer
/AGENTS.md

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
min-release-age=7

27
.prettierignore Normal file
View File

@@ -0,0 +1,27 @@
# Ignore build outputs
node_modules/
vendor/
storage/
bootstrap/cache/
public/build/
public/hot/
# Ignore lock files
package-lock.json
composer.lock
# Ignore generated files
*.min.js
*.min.css
# Ignore test results
test-results/
playwright-report/
# Ignore IDE files
.idea/
.vscode/
# Ignore OS files
.DS_Store
Thumbs.db

View File

@@ -3,5 +3,6 @@
"tabWidth": 4,
"singleQuote": true,
"bracketSameLine": true,
"quoteProps": "preserve"
"quoteProps": "preserve",
"printWidth": 100
}

81
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,81 @@
# Contributing to solidtime
Contributions are greatly apprecited, please make sure to read the rules and vision for solidtime before contributing.
## Rules
### Issues for Bugs, Discussions for Feature requests
In order to keep the issues of the repository clean we decided to only use them for bugs. Feature Requests and enhancement are handled in discussions. This also helps us to see which feature requests are popular as they can be upvoted.
### Only work on approved issues
To respect your time and help us manage contributions effectively, please open an issue or start a discussion and wait for approval before submitting a pull request (PR). This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons.
### Contributor License Agreement
You'll also notice that weve set up a [Contributor License Agreement (CLA)](https://cla-assistant.io/solidtime-io/solidtime), which must be signed before any PR can be merged. Dont worry - the process is quick and only takes a few clicks.
We want to be transparent about why we require the CLA and what it means for your contributions and the codebase. Thats why weve written a few paragraphs below outlining our plans and vision for solidtime in the **Vision** part of this document.
### Prevent Duplicate Work
Before you submit a new PR, make sure that none exists already. If you plan to work on an issue, make sure to let us and others know by commenting on the issue/discussion.
### Give context
Tell us what you thinking was behind the decisions you made while drafting the PR. Treat the PR itself as documentation for everyone who wants to go back and understand why certain decisions were made.
### Summarize your PR
Please make sure to include a short summary at the top of your PR to make it easy for us to quickly check what the PR is about, without looking at the code changes.
### Use Github Keywords and Auto-Link Issues
Use phrases like "Closes #123" or "Fixes #123" in the PR description to link the PR with the issue that you are adressing.
### Mention what you tested and how
Explain how you tested and validated the implementation.
### Keep Naming consistent
Look at existing code patterns and use naming conventions that already exist in the code base.
### Testing
We have an exhaustive test-suite of PHPUnit (Backend) and Playwright (Frontend) testing. Whereever applicable please make sure to write add tests to the codebase.
### Linting & Formatting
Make sure to run linting and formatting commands before you commit the changes.
For backend changes:
```
composer fix
composer analyse
```
For frontend changes:
```
npm run lint:fix
npm run format
```
## Vision
We started solidtime to provide an open infrastructure solution for time tracking—one that empowers teams and individuals to fully own their data, instead of depending on proprietary platforms. We believe infrastructure software should be open, accessible, and built to last. However, competing with established market leaders in this space requires long-term financial sustainability.
solidtime is licensed under the AGPL, which we believe is the best available license to strike a balance between openness and financial viability. The AGPL gives us, as the copyright holders, certain exclusive rights that we plan to leverage to fund development. To ensure we retain those rights across the entire codebase, we've put a CLA in place that contributors must sign before submitting code.
One of solidtimes key advantages is that it's built to be self-hostable. This makes it a great solution for organizations like governments, healthcare providers, and enterprises that are required to keep data on their own infrastructure due to regulations or internal policies. These organizations may need custom licenses, integrations, or modifications that aren't suitable for the open-source version. To support them, we offer relicensed versions of solidtime along with support plans.
Well also provide proprietary extensions for solidtime. These will be available to enterprise customers with support plans, but also to individual users or teams who dont need support, at much more accessible price points. For companies running solidtime on their own infrastructure, this is the easiest way to support the project while gaining additional functionality. While we plan to make it easier to build custom extensions in the future, our current APIs are still highly experimental.
Finally - and perhaps most importantly - we offer a hosted SaaS version called solidtime Cloud, for users who cant or dont want to run the software themselves. This version includes proprietary extensions, always runs the latest commit, and includes monitoring and billing features available exclusively on this hosted instance. We expect solidtime Cloud to play a critical role in funding the project long-term.
Having full control over the source codes licensing also gives us the ability to change the license of the main project in the future. That said, we have no plans to do so and would only consider it in extreme cases - for example, if a malicious actor were to directly compete with our hosted service in a way that threatens the sustainability of the project, the legal interpretation of AGPL changes in a way that would make it unreasonable to use for certain companies, or a new similar license gains wide-spread adoption. Regardless, solidtime will always remain free to self-host for individuals and companies who use it as part of their work, and all previous releases will remain licensed under AGPL.
If you are using the open-source version of solidtime and want to support us, the best way to do so is to spread the word.

View File

@@ -1,4 +1,4 @@
# solidtime - The modern Open-Source Time Tracker
# solidtime - The modern Open-Source TimeTracker
[![GitHub License](https://img.shields.io/github/license/solidtime-io/solidtime?style=flat-square)](https://github.com/solidtime-io/solidtime/blob/main/LICENSE.md)
[![Codecov](https://img.shields.io/codecov/c/github/solidtime-io/solidtime?style=flat-square&logo=codecov)](https://codecov.io/gh/solidtime-io/solidtime)
@@ -13,7 +13,7 @@ solidtime is a modern open-source time tracking application for Freelancers and
- Time tracking: Track your time with a modern and easy-to-use interface
- Projects: Create and manage projects and assign project members
- Tasks: Create and manage tasks and assign tasks to project members
- Tasks: Create and manage tasks and assign tasks to projects
- Clients: Create and manage clients and assign clients to projects
- Billable rates: Set billable rates for projects, project members, organization members and organizations
- Multiple organizations: Create and manage multiple organizations with one account
@@ -28,12 +28,20 @@ We also have an examples repository [here](https://github.com/solidtime-io/self-
If you do not want to self-host solidtime or try it out you can sign up for [solidtime cloud](https://www.solidtime.io/)
## Issues & Feature Requests
If you find any **bugs in solidtime**, please feel free to [**open an issue**](https://github.com/solidtime-io/solidtime/issues/new) in this repository, with instructions on how to reproduce the bug.
If you have a **feature request**, please [**create a discussion**](https://github.com/solidtime-io/solidtime/discussions/new?category=feature-requests) in this repository.
## Contributing
This project is in a very early stage. The structure and APIs are still subject to change and not stable.
Therefore, we do not currently accept any contributions, unless you are a member of the team.
Please open an issue or start a discussion and wait for approval before submitting a pull request. This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons.
As soon as we feel comfortable enough that the application structure is stable enough, we will open up the project for contributions.
**If you submit an AI slop pull request (especially without following the proper procedure), you will be banned from future contributions to solidtime.**
Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request.
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.
## Security

View File

@@ -3,3 +3,18 @@
## Reporting a Vulnerability
If you discover a security vulnerability regarding this project, please e-mail me to [security@solidtime.io](mailto:security@solidtime.io)!
## Out of scope
Reports we typically won't issue an advisory for:
* Theoretical findings without a working PoC
* Raw scanner output without manual validation
* Missing/weak security headers in isolation (CSP, X-Frame-Options, HSTS, etc.)
* SPF/DKIM/DMARC on non-mail-sending domains; missing DNSSEC/CAA; TLS cipher preferences
* Self-XSS; CSRF on non-state-changing endpoints (logout, theme)
* CSV / spreadsheet formula injection in exports — treated as a spreadsheet-application issue
* Org owners or admins acting destructively within their own organization
* Anything requiring direct DB, shell, or filesystem access on a self-hosted instance
* Missing OAuth Scope enforcement (this is not implemented yet, but AI scanners flag it which is why it is included in this list until we actually support it)

View File

@@ -4,21 +4,18 @@ declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Enums\Role;
use App\Enums\Weekday;
use App\Events\NewsletterRegistered;
use App\Models\Organization;
use App\Models\User;
use App\Service\IpLookup\IpLookupServiceContract;
use App\Service\TimezoneService;
use App\Service\UserService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Jetstream;
use Log;
class CreateNewUser implements CreatesNewUsers
@@ -28,12 +25,18 @@ class CreateNewUser implements CreatesNewUsers
/**
* Create a newly registered user.
*
* @param array<string, string> $input
* @param array<string, mixed> $input
*
* @throws ValidationException
*/
public function create(array $input): User
{
if (! config('app.enable_registration')) {
throw ValidationException::withMessages([
'email' => [__('Registration is disabled.')],
]);
}
Validator::make($input, [
'name' => [
'required',
@@ -43,7 +46,7 @@ class CreateNewUser implements CreatesNewUsers
'email' => [
'required',
'string',
'email',
'email:rfc,strict',
'max:255',
UniqueEloquent::make(User::class, 'email', function (Builder $builder): Builder {
/** @var Builder<User> $builder */
@@ -51,7 +54,7 @@ class CreateNewUser implements CreatesNewUsers
}),
],
'password' => $this->passwordRules(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
'terms' => ['accepted', 'required'],
'newsletter_consent' => [
'boolean',
],
@@ -72,6 +75,11 @@ class CreateNewUser implements CreatesNewUsers
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
$startOfWeek = Weekday::Monday;
$numberFormat = null;
$currencyFormat = null;
$dateFormat = null;
$intervalFormat = null;
$timeFormat = null;
$currency = null;
if ($ipLookupResponse !== null) {
$startOfWeek = $ipLookupResponse->startOfWeek ?? Weekday::Monday;
@@ -81,30 +89,21 @@ class CreateNewUser implements CreatesNewUsers
$currency = $ipLookupResponse->currency;
}
$user = null;
$organization = null;
DB::transaction(function () use (&$user, &$organization, $input, $timezone, $startOfWeek, $currency): void {
$user = User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
'timezone' => $timezone ?? 'UTC',
'week_start' => $startOfWeek,
]);
$organization = new Organization;
$organization->name = explode(' ', $user->name, 2)[0]."'s Organization";
$organization->personal_team = true;
$organization->currency = $currency ?? 'EUR';
$organization->owner()->associate($user);
$organization->save();
$organization->users()->attach(
$user, [
'role' => Role::Owner->value,
]
DB::transaction(function () use (&$user, $input, $timezone, $startOfWeek, $currency, $numberFormat, $currencyFormat, $dateFormat, $intervalFormat, $timeFormat): void {
$userService = app(UserService::class);
$user = $userService->createUser(
$input['name'],
$input['email'],
$input['password'],
$timezone ?? 'UTC',
$startOfWeek,
$currency,
$numberFormat,
$currencyFormat,
$dateFormat,
$intervalFormat,
$timeFormat
);
$user->ownedTeams()->save($organization);
});
$newsletterConsent = isset($input['newsletter_consent']) && (bool) $input['newsletter_consent'];

View File

@@ -4,14 +4,9 @@ declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Enums\Weekday;
use App\Exceptions\MovedToApiException;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
@@ -25,57 +20,6 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => [
'required',
'string',
'max:255',
],
'email' => [
'required',
'email',
'max:255',
UniqueEloquent::make(User::class, 'email')->ignore($user->id)->query(function (Builder $query) {
/** @var Builder<User> $query */
return $query->where('is_placeholder', '=', false);
}),
],
'photo' => [
'nullable',
'mimes:jpg,jpeg,png',
'max:1024',
],
'timezone' => [
'required',
'timezone:all',
],
'week_start' => [
'required',
Rule::enum(Weekday::class),
],
])->validateWithBag('updateProfileInformation');
if (isset($input['photo'])) {
$user->updateProfilePhoto($input['photo']);
}
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
'timezone' => $input['timezone'],
'week_start' => $input['week_start'],
])->save();
$user->sendEmailVerificationNotification();
} else {
$user->forceFill([
'name' => $input['name'],
'timezone' => $input['timezone'],
'week_start' => $input['week_start'],
])->save();
}
throw new MovedToApiException;
}
}

View File

@@ -1,104 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Enums\Role;
use App\Models\Organization;
use App\Models\User;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
use Laravel\Jetstream\Events\AddingTeamMember;
use Laravel\Jetstream\Events\TeamMemberAdded;
class AddOrganizationMember implements AddsTeamMembers
{
/**
* Add a new team member to the given team.
*/
public function add(User $owner, Organization $organization, string $email, ?string $role = null): void
{
Gate::forUser($owner)->authorize('addTeamMember', $organization); // TODO: refactor after owner refactoring
$this->validate($organization, $email, $role);
$newOrganizationMember = User::query()
->where('email', $email)
->where('is_placeholder', '=', false)
->firstOrFail();
AddingTeamMember::dispatch($organization, $newOrganizationMember);
DB::transaction(function () use ($organization, $newOrganizationMember, $role): void {
$organization->users()->attach(
$newOrganizationMember, ['role' => $role]
);
});
TeamMemberAdded::dispatch($organization, $newOrganizationMember);
}
/**
* Validate the add member operation.
*/
protected function validate(Organization $organization, string $email, ?string $role): void
{
Validator::make([
'email' => $email,
'role' => $role,
], $this->rules())->after(
$this->ensureUserIsNotAlreadyOnTeam($organization, $email)
)->validateWithBag('addTeamMember');
}
/**
* Get the validation rules for adding a team member.
*
* @return array<string, array<ValidationRule|Rule|string|In>>
*/
protected function rules(): array
{
return array_filter([
'email' => [
'required',
'email',
ExistsEloquent::make(User::class, 'email', function (Builder $builder) {
/** @var Builder<User> $builder */
return $builder->where('is_placeholder', '=', false);
})->withMessage(__('We were unable to find a registered user with this email address.')),
],
'role' => [
'required',
'string',
Rule::in([
Role::Admin->value,
Role::Manager->value,
Role::Employee->value,
]),
],
]);
}
/**
* Ensure that the user is not already on the team.
*/
protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure
{
return function ($validator) use ($team, $email): void {
$validator->errors()->addIf(
$team->hasRealUserWithEmail($email),
'email',
__('This user already belongs to the team.')
);
};
}
}

View File

@@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Enums\Role;
use App\Events\AfterCreateOrganization;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\CreatesTeams;
use Laravel\Jetstream\Jetstream;
class CreateOrganization implements CreatesTeams
{
/**
* Validate and create a new team for the given user.
*
* @param array<string, string> $input
*
* @throws AuthorizationException
* @throws ValidationException
*/
public function create(User $user, array $input): Organization
{
Gate::forUser($user)->authorize('create', Jetstream::newTeamModel());
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
])->validateWithBag('createTeam');
$organization = new Organization;
$organization->name = $input['name'];
$organization->personal_team = false;
$organization->owner()->associate($user);
$organization->save();
$organization->users()->attach(
$user, [
'role' => Role::Owner->value,
]
);
$user->switchTeam($organization);
// Note: The refresh is necessary for currently unknown reasons. Do not remove it.
$organization = $organization->refresh();
AfterCreateOrganization::dispatch($organization);
return $organization;
}
}

View File

@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Models\Organization;
use App\Service\DeletionService;
use Laravel\Jetstream\Contracts\DeletesTeams;
class DeleteOrganization implements DeletesTeams
{
/**
* Delete the given team.
*/
public function delete(Organization $organization): void
{
/** @see ValidateOrganizationDeletion */
app(DeletionService::class)->deleteOrganization($organization);
}
}

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Exceptions\Api\ApiException;
use App\Models\User;
use App\Service\DeletionService;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\DeletesUsers;
class DeleteUser implements DeletesUsers
{
/**
* Delete the given user.
*
* @throws ValidationException
*/
public function delete(User $user): void
{
try {
app(DeletionService::class)->deleteUser($user);
} catch (ApiException $exception) {
throw ValidationException::withMessages([
'password' => $exception->getTranslatedMessage(),
]);
}
}
}

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Exceptions\MovedToApiException;
use App\Models\Organization;
use App\Models\User;
use Exception;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
class InviteOrganizationMember implements InvitesTeamMembers
{
/**
* Invite a new team member to the given team.
*
* @throws Exception
*/
public function invite(User $user, Organization $organization, string $email, ?string $role = null): void
{
throw new MovedToApiException;
}
}

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Exceptions\MovedToApiException;
use App\Models\Organization;
use App\Models\User;
use Exception;
use Laravel\Jetstream\Contracts\RemovesTeamMembers;
class RemoveOrganizationMember implements RemovesTeamMembers
{
/**
* Remove the team member from the given team.
*
* @throws Exception
*/
public function remove(User $user, Organization $organization, User $teamMember): void
{
throw new MovedToApiException;
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Enums\Role;
use App\Exceptions\MovedToApiException;
use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use Exception;
class UpdateMemberRole
{
/**
* Update the role for the given team member.
*
* @throws Exception
*/
public function update(User $actingUser, Organization $organization, string $userId, string $role): void
{
throw new MovedToApiException;
}
}

View File

@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Models\Organization;
use App\Models\User;
use App\Rules\CurrencyRule;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\UpdatesTeamNames;
class UpdateOrganization implements UpdatesTeamNames
{
/**
* Validate and update the given team's name.
*
* @param array<string, string> $input
*
* @throws AuthorizationException
* @throws ValidationException
*/
public function update(User $user, Organization $organization, array $input): void
{
Gate::forUser($user)->authorize('update', $organization);
Validator::make($input, [
'name' => [
'required',
'string',
'max:255',
],
'currency' => [
'required',
'string',
new CurrencyRule,
],
])->validateWithBag('updateTeamName');
$organization->forceFill([
'name' => $input['name'],
'currency' => $input['currency'],
])->save();
}
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Models\Organization;
use App\Models\User;
use App\Service\PermissionStore;
use Illuminate\Auth\Access\AuthorizationException;
class ValidateOrganizationDeletion
{
/**
* Validate that the team can be deleted by the given user.
*
* @param User $user Authenticated user
* @param Organization $organization Organization to be deleted
*
* @throws AuthorizationException
*/
public function validate(User $user, Organization $organization): void
{
if (! app(PermissionStore::class)->userHas($organization, $user, 'organizations:delete')) {
throw new AuthorizationException;
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Admin;
use App\Enums\Weekday;
use App\Models\Organization;
use App\Models\User;
use App\Service\UserService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use LogicException;
class UserCreateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'admin:user:create
{ name : The name of the user }
{ email : The email of the user }
{ --ask-for-password : Ask for the password, otherwise the command will generate a random one }
{ --verify-email : Verify the email address of the user }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new user';
/**
* Execute the console command.
*/
public function handle(): int
{
$name = $this->argument('name');
$email = $this->argument('email');
$askForPassword = (bool) $this->option('ask-for-password');
$verifyEmail = (bool) $this->option('verify-email');
if (User::query()->where('email', $email)->where('is_placeholder', '=', false)->exists()) {
$this->error('User with email "'.$email.'" already exists.');
return self::FAILURE;
}
if ($askForPassword) {
$outputPassword = false;
$password = $this->secret('Enter the password');
} else {
$outputPassword = true;
$password = bin2hex(random_bytes(16));
}
$user = null;
DB::transaction(function () use (&$user, $name, $email, $password, $verifyEmail): void {
$user = app(UserService::class)->createUser(
$name,
$email,
$password,
'UTC',
Weekday::Monday,
null,
verifyEmail: $verifyEmail
);
});
/** @var Organization|null $organization */
$organization = $user->ownedOrganizations->first();
if ($organization === null) {
throw new LogicException('User does not have an organization');
}
$this->info('Created user "'.$name.'" ("'.$email.'")');
$this->line('ID: '.$user->getKey());
$this->line('Name: '.$name);
$this->line('Email: '.$email);
if ($outputPassword) {
$this->line('Password: '.$password);
}
$this->line('Timezone: '.$user->timezone);
$this->line('Week start: '.$user->week_start->value);
// Organization
$this->line('Currency: '.$organization->currency);
return self::SUCCESS;
}
}

View File

@@ -35,7 +35,9 @@ class UserVerifyCommand extends Command
$this->info('Start verifying user with email "'.$email.'"');
/** @var User|null $user */
$user = User::where('email', $email)->first();
$user = User::query()->where('email', $email)
->where('is_placeholder', '=', false)
->first();
if ($user === null) {
$this->error('User with email "'.$email.'" not found.');

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Auth;
use App\Mail\AuthApiTokenExpirationReminderMail;
use App\Mail\AuthApiTokenExpiredMail;
use App\Models\Passport\Token;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
class AuthSendReminderForExpiringApiTokensCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'auth:send-mails-expiring-api-tokens '.
' { --dry-run : Do not actually send emails or save anything to the database, just output what would happen }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends emails about expiring API tokens, one week before and when they expired.';
/**
* Execute the console command.
*/
public function handle(): int
{
$dryRun = (bool) $this->option('dry-run');
if ($dryRun) {
$this->comment('Running in dry-run mode. No emails will be sent and nothing will be saved to the database.');
}
$this->comment('Sending reminder emails about expiring API tokens...');
$sentMails = 0;
Token::query()
->where('expires_at', '<=', Carbon::now()->addDays(7))
->whereNull('reminder_sent_at')
->with([
'client',
'user',
])
->whereHas('user', function (Builder $query): void {
/** @var Builder<User> $query */
$query->where('is_placeholder', '=', false);
})
->isApiToken(true)
->orderBy('created_at', 'asc')
->chunk(500, function (Collection $tokens) use ($dryRun, &$sentMails): void {
/** @var Collection<int, Token> $tokens */
foreach ($tokens as $token) {
$user = $token->user;
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') reminding about API token '.$token->getKey());
$sentMails++;
if (! $dryRun) {
Mail::to($user->email)
->queue(new AuthApiTokenExpirationReminderMail($token, $user));
$token->reminder_sent_at = Carbon::now();
$token->save();
}
}
});
$this->comment('Finished sending '.$sentMails.' expiring API token emails...');
$this->comment('Sent emails about expired API tokens');
$sentMails = 0;
Token::query()
->where('expires_at', '<=', Carbon::now())
->whereNull('expired_info_sent_at')
->with([
'client',
'user',
])
->whereHas('user', function (Builder $query): void {
/** @var Builder<User> $query */
$query->where('is_placeholder', '=', false);
})
->isApiToken(true)
->orderBy('created_at', 'asc')
->chunk(500, function (Collection $tokens) use ($dryRun, &$sentMails): void {
/** @var Collection<int, Token> $tokens */
foreach ($tokens as $token) {
$user = $token->user;
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') about expired API token '.$token->getKey());
$sentMails++;
if (! $dryRun) {
Mail::to($user->email)
->queue(new AuthApiTokenExpiredMail($token, $user));
$token->expired_info_sent_at = Carbon::now();
$token->save();
}
}
});
$this->comment('Finished sending '.$sentMails.' expired API token emails...');
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Correction;
use App\Enums\Role;
use App\Models\Member;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
class CorrectionPlaceholderMembersCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'correction:placeholder-members '.
' { --dry-run : Do not actually save anything to the database, just output what would happen }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sets all members who belong to a placeholder user to role placeholder';
/**
* Execute the console command.
*/
public function handle(): int
{
$this->comment('Sets all members who belong to a placeholder user to role placeholder...');
$dryRun = (bool) $this->option('dry-run');
if ($dryRun) {
$this->comment('Running in dry-run mode. Nothing will be saved to the database.');
}
$members = Member::query()
->where('role', '!=', Role::Placeholder->value)
->whereHas('user', function (Builder $builder): void {
/** @var Builder<User> $builder */
$builder->where('is_placeholder', '=', true);
})
->get();
foreach ($members as $member) {
/** @var Member $member */
$member->role = Role::Placeholder->value;
if (! $dryRun) {
$member->save();
}
$this->line('Set role of member (id='.$member->getKey().') to placeholder');
}
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Report;
use App\Models\Report;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use LogicException;
class ReportSetExpiredToPrivateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'report:set-expired-to-private '.
' { --dry-run : Do not actually save anything to the database, just output what would happen }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Makes public reports private if the public_until date has passed.';
/**
* Execute the console command.
*/
public function handle(): int
{
$this->comment('Makes public reports private if the public_until date has passed...');
$dryRun = (bool) $this->option('dry-run');
if ($dryRun) {
$this->comment('Running in dry-run mode. Nothing will be saved to the database.');
}
$resetReports = 0;
Report::query()
->where('public_until', '<', Carbon::now())
->orderBy('created_at', 'asc')
->chunk(500, function (Collection $reports) use ($dryRun, &$resetReports): void {
/** @var Collection<int, Report> $reports */
foreach ($reports as $report) {
$publicUntil = $report->public_until;
if ($publicUntil === null) {
throw new LogicException('public_until should not be null');
}
$this->info('Make report "'.$report->name.'" ('.$report->getKey().') private, expired: '.
$publicUntil->toIso8601ZuluString().' ('.$publicUntil->diffForHumans().')');
$resetReports++;
if (! $dryRun) {
$report->is_public = false;
$report->share_secret = null;
$report->save();
}
}
});
$this->comment('Finished setting '.$resetReports.' expired reports to private...');
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\SelfHost;
use Illuminate\Console\Command;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class SelfHostDatabaseConsistency extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'self-host:database-consistency';
/**
* The console command description.
*
* @var string
*/
protected $description = '';
/**
* Execute the console command.
*/
public function handle(): int
{
$hadAProblem = false;
// Task need to be part of project in time entries
$problems = DB::table('time_entries')
->select(['time_entries.id as id'])
->join('tasks', 'time_entries.task_id', '=', 'tasks.id')
->where('tasks.project_id', '!=', DB::raw('time_entries.project_id'))
->get();
$this->logProblems($problems, 'Time entries have a task that does not belong to the project of the time entry', $hadAProblem);
// Client id is the client id of the project
$problems = DB::table('time_entries')
->select(['time_entries.id as id'])
->join('projects', 'time_entries.project_id', '=', 'projects.id')
->where(DB::raw('coalesce(projects.client_id::varchar, \'\')'), '!=', DB::raw('coalesce(time_entries.client_id::varchar, \'\')'))
->get();
$this->logProblems($problems, 'Time entries have a client that does not match the client of the project', $hadAProblem);
// Client id can only be not null if the project id is not null
$problems = DB::table('time_entries')
->select(['time_entries.id as id'])
->whereNotNull('client_id')
->whereNull('project_id')
->get();
$this->logProblems($problems, 'Time entries have a client but no project', $hadAProblem);
// Every user needs to be a member of at least one organization
$problems = DB::table('users')
->select(['users.id as id'])
->leftJoin('members', 'users.id', '=', 'members.user_id')
->whereNull('members.id')
->get();
$this->logProblems($problems, 'Users are not member of any organization', $hadAProblem);
// Every organization needs at least an owner
$problems = DB::table('organizations')
->select(['organizations.id as id'])
->leftJoin('members', function (JoinClause $join): void {
$join->on('organizations.id', '=', 'members.organization_id')
->where('members.role', '=', 'owner');
})
->whereNull('members.id')
->get();
$this->logProblems($problems, 'Organizations without an owner', $hadAProblem);
// Every member can only have one running time entry
$problems = DB::table('time_entries')
->select(['user_id as id'])
->whereNull('end')
->groupBy('user_id')
->havingRaw('count(*) > 1')
->get(['user_id', DB::raw('count(*) as count')]);
$this->logProblems($problems, 'Users with more than one running time entry', $hadAProblem);
// Users have a current organization that they are not a member of
$problems = DB::table('users')
->select(['users.id as id'])
->whereNotNull('current_team_id')
->whereNotIn('current_team_id', function (Builder $query): void {
$query->select('organization_id')
->from('members')
->whereColumn('members.user_id', 'users.id');
})->get();
$this->logProblems($problems, 'Users have a current organization that they are not a member of', $hadAProblem);
return $hadAProblem ? self::FAILURE : self::SUCCESS;
}
/**
* @param Collection<int, \stdClass> $problems
*/
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
{
$message = 'Consistency problem: '.$message;
if ($problems->isNotEmpty()) {
$ids = $problems->pluck('id');
$hadAProblem = true;
Log::error($message, [
'ids' => $ids,
]);
$error = $message;
foreach ($ids as $id) {
$error .= "\n - ".$id;
}
$this->error($error);
}
}
}

View File

@@ -18,6 +18,7 @@ class SelfHostGenerateKeysCommand extends Command
*/
protected $signature = 'self-host:generate-keys
{ --length=4096 : The length of the passport private key }
{ --multi-line : Whether to output the keys in multiple lines }
{ --format=env : The format of the output (env, yaml) }';
/**
@@ -34,6 +35,7 @@ class SelfHostGenerateKeysCommand extends Command
{
$format = $this->option('format');
$key = RSA::createKey((int) $this->option('length'));
$multiLine = (bool) $this->option('multi-line');
$publicKey = (string) $key->getPublicKey();
$privateKey = (string) $key;
@@ -41,12 +43,17 @@ class SelfHostGenerateKeysCommand extends Command
if ($format === 'env') {
$this->line('APP_KEY="'.$appKey.'"');
$this->line('PASSPORT_PRIVATE_KEY="'.$privateKey.'"');
$this->line('PASSPORT_PUBLIC_KEY="'.$publicKey.'"');
if ($multiLine) {
$this->line('PASSPORT_PRIVATE_KEY="'.Str::replace("\r\n", "\n", $privateKey).'"');
$this->line('PASSPORT_PUBLIC_KEY="'.Str::replace("\r\n", "\n", $publicKey).'"');
} else {
$this->line('PASSPORT_PRIVATE_KEY="'.Str::replace("\r\n", '\n', $privateKey).'"');
$this->line('PASSPORT_PUBLIC_KEY="'.Str::replace("\r\n", '\n', $publicKey).'"');
}
} elseif ($format === 'yaml') {
$this->line('APP_KEY: "'.$appKey.'"');
$this->line("PASSPORT_PRIVATE_KEY: |\n ".Str::replace("\n", "\n ", $privateKey));
$this->line("PASSPORT_PUBLIC_KEY: |\n ".Str::replace("\n", "\n ", $publicKey));
$this->line("PASSPORT_PRIVATE_KEY: |\n ".Str::replace("\r\n", "\n ", $privateKey));
$this->line("PASSPORT_PUBLIC_KEY: |\n ".Str::replace("\r\n", "\n ", $publicKey));
} else {
$this->error('Invalid format');

View File

@@ -18,13 +18,35 @@ class Kernel extends ConsoleKernel
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
->everyTenMinutes();
$schedule->command('self-host:check-for-update')
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
->twiceDaily();
$schedule->command('auth:send-mails-expiring-api-tokens')
->when(fn (): bool => config('scheduling.tasks.auth_send_mails_expiring_api_tokens'))
->everyTenMinutes();
$schedule->command('self-host:telemetry')
->when(fn (): bool => config('scheduling.tasks.self_hosting_telemetry'))
->twiceDaily();
if (config('app.key') && (config('scheduling.tasks.self_hosting_check_for_update') || config('scheduling.tasks.self_hosting_telemetry'))) {
// Convert string to a stable integer for seeding
/** @var int $seed Take the first 8 hex chars → 32-bit int */
$seed = hexdec(substr(hash('md5', config('app.key')), 0, 8));
$seed = abs($seed); // Ensure it's positive
mt_srand($seed);
$firstHour = mt_rand(0, 23);
$secondHour = ($firstHour + 12) % 24;
$minuteOffset = mt_rand(0, 59);
mt_srand(null); // Reset the random number generator
if (config('scheduling.tasks.self_hosting_check_for_update')) {
$schedule->command('self-host:check-for-update')
->twiceDailyAt($firstHour, $secondHour, $minuteOffset);
}
if (config('scheduling.tasks.self_hosting_telemetry')) {
$schedule->command('self-host:telemetry')
->twiceDailyAt($firstHour, $secondHour, $minuteOffset);
}
}
$schedule->command('self-host:database-consistency')
->when(fn (): bool => config('scheduling.tasks.self_hosting_database_consistency'))
->everySixHours();
}
/**

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum CurrencyFormat: string
{
use LaravelEnumHelper;
case ISOCodeBeforeWithSpace = 'iso-code-before-with-space';
case ISOCodeAfterWithSpace = 'iso-code-after-with-space';
case SymbolBefore = 'symbol-before';
case SymbolAfter = 'symbol-after';
case SymbolBeforeWithSpace = 'symbol-before-with-space';
case SymbolAfterWithSpace = 'symbol-after-with-space';
/**
* @return array<string, string>
*/
public static function toSelectArray(): array
{
$selectArray = [];
foreach (self::values() as $value) {
$selectArray[(string) $value] = (string) __('enum.currency_format.'.$value);
}
return $selectArray;
}
}

48
app/Enums/DateFormat.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum DateFormat: string
{
use LaravelEnumHelper;
case PointSeparatedDMYYYY = 'point-separated-d-m-yyyy';
case SlashSeparatedMMDDYYYY = 'slash-separated-mm-dd-yyyy';
case SlashSeparatedDDMMYYYY = 'slash-separated-dd-mm-yyyy';
case HyphenSeparatedDDMMYYY = 'hyphen-separated-dd-mm-yyyy';
case HyphenSeparatedMMDDDYYYY = 'hyphen-separated-mm-dd-yyyy';
case HyphenSeparatedYYYYMMDD = 'hyphen-separated-yyyy-mm-dd';
public function toCarbonFormat(): string
{
return match ($this->value) {
self::PointSeparatedDMYYYY->value => 'j.n.Y',
self::SlashSeparatedMMDDYYYY->value => 'm/d/Y',
self::SlashSeparatedDDMMYYYY->value => 'd/m/Y',
self::HyphenSeparatedDDMMYYY->value => 'd-m-Y',
self::HyphenSeparatedMMDDDYYYY->value => 'm-d-Y',
self::HyphenSeparatedYYYYMMDD->value => 'Y-m-d',
};
}
/**
* @return array<string, string>
*/
public static function toSelectArray(): array
{
$selectArray = [];
foreach (self::values() as $value) {
$selectArray[(string) $value] = (string) __('enum.date_format.'.$value);
}
return $selectArray;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Maatwebsite\Excel\Excel;
enum ExportFormat: string
{
case CSV = 'csv';
case PDF = 'pdf';
case XLSX = 'xlsx';
case ODS = 'ods';
public function getFileExtension(): string
{
return match ($this) {
self::CSV => 'csv',
self::PDF => 'pdf',
self::XLSX => 'xlsx',
self::ODS => 'ods',
};
}
public function getExportPackageType(): string
{
return match ($this) {
self::CSV => Excel::CSV,
self::PDF => Excel::MPDF,
self::XLSX => Excel::XLSX,
self::ODS => Excel::ODS,
};
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum IntervalFormat: string
{
use LaravelEnumHelper;
case Decimal = 'decimal';
case HoursMinutes = 'hours-minutes';
case HoursMinutesColonSeparated = 'hours-minutes-colon-separated';
case HoursMinutesSecondsColonSeparated = 'hours-minutes-seconds-colon-separated';
/**
* @return array<string, string>
*/
public static function toSelectArray(): array
{
$selectArray = [];
foreach (self::values() as $value) {
$selectArray[(string) $value] = (string) __('enum.interval_format.'.$value);
}
return $selectArray;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
/**
* @info https://en.wikipedia.org/wiki/Decimal_separator
*/
enum NumberFormat: string
{
use LaravelEnumHelper;
case ThousandsPointDecimalComma = 'point-comma';
case ThousandsCommaDecimalPoint = 'comma-point';
case ThousandsSpaceDecimalComma = 'space-comma';
case ThousandsSpaceDecimalPoint = 'space-point';
case ThousandsApostropheDecimalPoint = 'apostrophe-point';
/**
* @return array<string, string>
*/
public static function toSelectArray(): array
{
$selectArray = [];
foreach (self::values() as $value) {
$selectArray[(string) $value] = (string) __('enum.number_format.'.$value);
}
return $selectArray;
}
}

View File

@@ -4,8 +4,12 @@ declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum Role: string
{
use LaravelEnumHelper;
case Owner = 'owner';
case Admin = 'admin';
case Manager = 'manager';

View File

@@ -4,8 +4,12 @@ declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum TimeEntryAggregationType: string
{
use LaravelEnumHelper;
case Day = 'day';
case Week = 'week';
case Month = 'month';
@@ -16,6 +20,17 @@ enum TimeEntryAggregationType: string
case Client = 'client';
case Billable = 'billable';
case Description = 'description';
case Tag = 'tag';
public static function fromInterval(TimeEntryAggregationTypeInterval $timeEntryAggregationTypeInterval): TimeEntryAggregationType
{
return match ($timeEntryAggregationTypeInterval) {
TimeEntryAggregationTypeInterval::Day => TimeEntryAggregationType::Day,
TimeEntryAggregationTypeInterval::Week => TimeEntryAggregationType::Week,
TimeEntryAggregationTypeInterval::Month => TimeEntryAggregationType::Month,
TimeEntryAggregationTypeInterval::Year => TimeEntryAggregationType::Year,
};
}
public function toInterval(): ?TimeEntryAggregationTypeInterval
{

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum TimeEntryRoundingType: string
{
use LaravelEnumHelper;
case Up = 'up';
case Down = 'down';
case Nearest = 'nearest';
}

28
app/Enums/TimeFormat.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum TimeFormat: string
{
use LaravelEnumHelper;
case TwelveHours = '12-hours';
case TwentyFourHours = '24-hours';
/**
* @return array<string, string>
*/
public static function toSelectArray(): array
{
$selectArray = [];
foreach (self::values() as $value) {
$selectArray[(string) $value] = (string) __('enum.time_format.'.$value);
}
return $selectArray;
}
}

View File

@@ -4,10 +4,13 @@ declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
use Illuminate\Support\Carbon;
enum Weekday: string
{
use LaravelEnumHelper;
case Monday = 'monday';
case Tuesday = 'tuesday';
case Wednesday = 'wednesday';

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
class DatabaseSeederAfterSeed
{
use Dispatchable;
public function __construct() {}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
class DatabaseSeederBeforeDelete
{
use Dispatchable;
public function __construct() {}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Events;
use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
class MemberAdded
{
use Dispatchable;
public Member $member;
public Organization $organization;
public User $user;
public function __construct(Member $member, Organization $organization, User $user)
{
$this->member = $member;
$this->organization = $organization;
$this->user = $user;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Events;
use App\Enums\Role;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
class MemberAdding
{
use Dispatchable;
public User $user;
public Organization $organization;
public Role $role;
public function __construct(User $user, Organization $organization, Role $role)
{
$this->user = $user;
$this->organization = $organization;
$this->role = $role;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Events;
use App\Enums\Role;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
class OrganizationInvitationAdding
{
use Dispatchable;
public Organization $organization;
public string $email;
public Role $role;
public User $inviter;
public function __construct(
Organization $organization,
string $email,
Role $role,
User $inviter
) {
$this->role = $role;
$this->email = $email;
$this->organization = $organization;
$this->inviter = $inviter;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
{
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
{
public const string KEY = 'feature_is_not_available_in_free_plan';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
{
public const string KEY = 'invitation_for_the_email_already_exists';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
{
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class OverlappingTimeEntryApiException extends ApiException
{
public const string KEY = 'overlapping_time_entry';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class PdfRendererIsNotConfiguredException extends ApiException
{
public const string KEY = 'pdf_renderer_is_not_configured';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class PersonalAccessClientIsNotConfiguredException extends ApiException
{
public const string KEY = 'personal_access_client_is_not_configured';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
{
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Api;
class UserResendEmailVerificationNoPendingEmailApiException extends ApiException
{
public const string KEY = 'user_resend_email_verification_no_pending_email';
}

View File

@@ -41,9 +41,7 @@ class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
return null;
}
if (! ($collectingType = $this->openApiTransformer->transform($collectingClassType))) {
return null;
}
$collectingType = $this->openApiTransformer->transform($collectingClassType);
$newType = new OpenApiObjectType;
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));

View File

@@ -13,7 +13,7 @@ use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Novadaemon\FilamentPrettyJson\PrettyJson;
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
class AuditResource extends Resource
{
@@ -38,8 +38,8 @@ class AuditResource extends Resource
->maxLength(255),
Forms\Components\TextInput::make('auditable_id')
->required(),
PrettyJson::make('old_values'),
PrettyJson::make('new_values'),
PrettyJsonField::make('old_values'),
PrettyJsonField::make('new_values'),
Forms\Components\Textarea::make('url'),
Forms\Components\TextInput::make('ip_address'),
Forms\Components\TextInput::make('user_agent')

View File

@@ -60,8 +60,13 @@ class ClientResource extends Resource
->defaultSort('created_at', 'desc')
->filters([
SelectFilter::make('organization')
->label('Organization')
->relationship('organization', 'name')
->searchable(),
SelectFilter::make('organization_id')
->label('Organization ID')
->relationship('organization', 'id')
->searchable(),
])
->actions([
Tables\Actions\EditAction::make(),

View File

@@ -15,7 +15,8 @@ class EditClient extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}
}

View File

@@ -15,7 +15,8 @@ class ListClients extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}
}

View File

@@ -15,12 +15,13 @@ use Filament\Resources\Resource;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Novadaemon\FilamentPrettyJson\PrettyJson;
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
/**
* @source https://gitlab.com/amvisor/filament-failed-jobs
@@ -49,8 +50,8 @@ class FailedJobResource extends Resource
TextInput::make('queue')->disabled(),
// make text a little bit smaller because often a complete Stack Trace is shown:
TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
PrettyJson::make('payload')->disabled()->columnSpan(4),
Textarea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
PrettyJsonField::make('payload')->disabled()->columnSpan(4),
])->columns(4);
}
@@ -75,7 +76,8 @@ class FailedJobResource extends Resource
->filters([])
->bulkActions([
BulkAction::make('retry')
->label('Retry')
->icon('heroicon-o-arrow-path')
->label('Retry selected')
->requiresConfirmation()
->action(function (Collection $records): void {
/** @var FailedJob $record */
@@ -87,11 +89,13 @@ class FailedJobResource extends Resource
->success()
->send();
}),
DeleteBulkAction::make(),
])
->actions([
DeleteAction::make('Delete'),
ViewAction::make('View'),
DeleteAction::make(),
ViewAction::make(),
Action::make('retry')
->icon('heroicon-o-arrow-path')
->label('Retry')
->requiresConfirmation()
->action(function (FailedJob $record): void {
@@ -109,7 +113,6 @@ class FailedJobResource extends Resource
return [
'index' => ListFailedJobs::route('/'),
'view' => ViewFailedJobs::route('/{record}'),
];
}
}

View File

@@ -6,8 +6,8 @@ namespace App\Filament\Resources\FailedJobResource\Pages;
use App\Filament\Resources\FailedJobResource;
use App\Models\FailedJob;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Actions\Action;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Support\Facades\Artisan;
@@ -19,7 +19,8 @@ class ListFailedJobs extends ListRecords
{
return [
Action::make('retry_all')
->label('Retry all failed Jobs')
->icon('heroicon-o-arrow-path')
->label('Retry all')
->requiresConfirmation()
->action(function (): void {
Artisan::call('queue:retry all');
@@ -30,7 +31,8 @@ class ListFailedJobs extends ListRecords
}),
Action::make('delete_all')
->label('Delete all failed Jobs')
->icon('heroicon-o-trash')
->label('Delete all')
->requiresConfirmation()
->color('danger')
->action(function (): void {

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Enums\Role;
use App\Filament\Resources\OrganizationInvitationResource\Pages;
use App\Models\OrganizationInvitation;
use App\Service\OrganizationInvitationService;
use Filament\Forms;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
class OrganizationInvitationResource extends Resource
{
protected static ?string $model = OrganizationInvitation::class;
protected static ?string $label = 'Invitations';
protected static ?string $navigationIcon = 'heroicon-o-user-plus';
protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 9;
public static function form(Form $form): Form
{
return $form
->columns(1)
->schema([
Forms\Components\TextInput::make('email')
->label('Email')
->disabledOn(['edit'])
->required(),
Select::make('role')
->options(Role::class),
Select::make('organization_id')
->label('Organization')
->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name'])
->disabledOn(['edit'])
->required(),
Forms\Components\DateTimePicker::make('created_at')
->label('Created At')
->hiddenOn(['create'])
->disabled(),
Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At')
->hiddenOn(['create'])
->disabled(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('organization.name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('email')
->sortable(),
Tables\Columns\TextColumn::make('role'),
Tables\Columns\TextColumn::make('created_at')
->label('Created At')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('updated_at')
->label('Updated At')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('created_at', 'desc')
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\BulkAction::make('resend')
->label('Resend')
->action(function (Collection $records): void {
foreach ($records as $organizationInvite) {
app(OrganizationInvitationService::class)->resend($organizationInvite);
}
}),
]),
]);
}
public static function getRelations(): array
{
return [
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListOrganizationInvitations::route('/'),
'edit' => Pages\EditOrganizationInvitation::route('/{record}/edit'),
'view' => Pages\ViewOrganizationInvitation::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
use App\Filament\Resources\OrganizationInvitationResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditOrganizationInvitation extends EditRecord
{
protected static string $resource = OrganizationInvitationResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
use App\Filament\Resources\OrganizationInvitationResource;
use Filament\Resources\Pages\ListRecords;
class ListOrganizationInvitations extends ListRecords
{
protected static string $resource = OrganizationInvitationResource::class;
protected function getHeaderActions(): array
{
return [
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
use App\Filament\Resources\OrganizationInvitationResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewOrganizationInvitation extends ViewRecord
{
protected static string $resource = OrganizationInvitationResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make('edit')
->icon('heroicon-s-pencil'),
];
}
}

View File

@@ -4,9 +4,16 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Enums\CurrencyFormat;
use App\Enums\DateFormat;
use App\Enums\IntervalFormat;
use App\Enums\NumberFormat;
use App\Enums\TimeFormat;
use App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
use App\Models\Organization;
use App\Service\DeletionService;
use App\Service\Export\ExportService;
use App\Service\Import\Importers\ImporterProvider;
use App\Service\Import\Importers\ImportException;
@@ -46,12 +53,30 @@ class OrganizationResource extends Resource
->maxLength(255),
Forms\Components\Toggle::make('personal_team')
->label('Is personal?')
->hiddenOn(['create'])
->required(),
Forms\Components\Select::make('user_id')
Select::make('user_id')
->label('Owner')
->relationship(name: 'owner', titleAttribute: 'email')
->searchable(['name', 'email'])
->disabledOn(['edit'])
->required(),
Forms\Components\Select::make('currency')
Select::make('date_format')
->options(DateFormat::toSelectArray())
->required(),
Select::make('currency_format')
->options(CurrencyFormat::toSelectArray())
->required(),
Select::make('interval_format')
->options(IntervalFormat::toSelectArray())
->required(),
Select::make('number_format')
->options(NumberFormat::toSelectArray())
->required(),
Select::make('time_format')
->options(TimeFormat::toSelectArray())
->required(),
Select::make('currency')
->label('Currency')
->options(function (): array {
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
@@ -62,6 +87,7 @@ class OrganizationResource extends Resource
return $select;
})
->required()
->searchable(),
Forms\Components\TextInput::make('billable_rate')
->label('Billable rate (in Cents)')
@@ -75,9 +101,11 @@ class OrganizationResource extends Resource
->numeric(),
Forms\Components\DateTimePicker::make('created_at')
->label('Created At')
->hiddenOn(['create'])
->disabled(),
Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At')
->hiddenOn(['create'])
->disabled(),
]);
}
@@ -86,22 +114,22 @@ class OrganizationResource extends Resource
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\IconColumn::make('personal_team')
->boolean()
->label('Is personal?')
->sortable(),
Tables\Columns\TextColumn::make('owner.email')
TextColumn::make('owner.email')
->sortable(),
Tables\Columns\TextColumn::make('currency'),
TextColumn::make('currency'),
TextColumn::make('billable_rate')
->money(fn (Organization $resource) => $resource->currency ?? 'EUR', divideBy: 100),
Tables\Columns\TextColumn::make('created_at')
->money(fn (Organization $resource) => $resource->currency, divideBy: 100),
TextColumn::make('created_at')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('updated_at')
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
@@ -112,6 +140,10 @@ class OrganizationResource extends Resource
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make()
->using(function (Organization $record): void {
app(DeletionService::class)->deleteOrganization($record);
}),
Action::make('Export')
->icon('heroicon-o-arrow-down-tray')
->action(function (Organization $record) {
@@ -191,7 +223,7 @@ class OrganizationResource extends Resource
return $select;
}),
Forms\Components\Select::make('timezone')
Select::make('timezone')
->label('Timezone')
->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
->searchable()
@@ -199,8 +231,6 @@ class OrganizationResource extends Resource
]),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
]),
]);
}
@@ -208,6 +238,7 @@ class OrganizationResource extends Resource
{
return [
UsersRelationManager::class,
InvitationsRelationManager::class,
];
}

View File

@@ -15,7 +15,6 @@ class DeleteOrganization extends DeleteAction
protected function setUp(): void
{
parent::setUp();
// TODO: check why setting the icon is necessary
$this->icon('heroicon-m-trash');
$this->action(function (): void {
$result = $this->process(function (Organization $record): bool {

View File

@@ -4,10 +4,33 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Enums\Role;
use App\Filament\Resources\OrganizationResource;
use App\Models\Organization;
use Filament\Resources\Pages\CreateRecord;
class CreateOrganization extends CreateRecord
{
protected static string $resource = OrganizationResource::class;
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['personal_team'] = false;
return $data;
}
protected function afterCreate(): void
{
/** @var Organization $organization */
$organization = $this->record;
$user = $organization->owner;
$organization->users()->attach(
$user, [
'role' => Role::Owner->value,
]
);
}
}

View File

@@ -15,7 +15,8 @@ class ListOrganizations extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\RelationManagers;
use App\Enums\Role;
use App\Filament\Resources\OrganizationInvitationResource;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Service\InvitationService;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
class InvitationsRelationManager extends RelationManager
{
protected static string $relationship = 'organizationInvitations';
protected static ?string $title = 'Invitations';
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('email')
->label('Email')
->disabledOn(['edit'])
->required(),
Select::make('role')
->options(Role::class)
->label('Role')
->rules([
'required',
'string',
Rule::enum(Role::class)
->except([Role::Owner, Role::Placeholder]),
])
->required(),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('email')
->modelLabel('Invitation')
->pluralModelLabel('Invitations')
->columns([
Tables\Columns\TextColumn::make('email'),
Tables\Columns\TextColumn::make('role'),
])
->headerActions([
Tables\Actions\CreateAction::make()
->icon('heroicon-s-plus')
->using(function (array $data, string $model): Model {
/** @var Organization $ownerRecord */
$ownerRecord = $this->getOwnerRecord();
return app(InvitationService::class)
->inviteUser($ownerRecord, $data['email'], Role::from($data['role']), auth()->user());
}),
])
->actions([
Action::make('view')
->icon('heroicon-o-eye')
->color('gray')
->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [
'record' => $record->getKey(),
])),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(),
]),
]);
}
}

View File

@@ -5,17 +5,24 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\RelationManagers;
use App\Enums\Role;
use App\Exceptions\Api\ApiException;
use App\Filament\Resources\UserResource;
use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use App\Service\BillableRateService;
use App\Service\MemberService;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\AttachAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Validation\Rule;
class UsersRelationManager extends RelationManager
{
@@ -36,20 +43,40 @@ class UsersRelationManager extends RelationManager
public function table(Table $table): Table
{
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name'),
Tables\Columns\TextColumn::make('role'),
TextColumn::make('name'),
TextColumn::make('role'),
TextColumn::make('billable_rate')
->money($this->getOwnerRecord()->currency ?? 'EUR', divideBy: 100),
->money($organization->currency, divideBy: 100),
])
->headerActions([
Tables\Actions\AttachAction::make()->form(fn (AttachAction $action): array => [
$action->getRecordSelect(),
Select::make('role')
->options(Role::class),
]),
AttachAction::make()
->recordTitle(fn (User $record): string => "{$record->name} ({$record->email})")
->form(fn (AttachAction $action): array => [
$action->getRecordSelect(),
Select::make('role')
->required()
->options(Role::class)
->rule([
'required',
'string',
Rule::enum(Role::class)
->except([Role::Owner, Role::Placeholder]),
]),
])
->label('Add user')
->modalHeading('Add user')
->icon('heroicon-s-plus')
->using(function (User $record, array $data): void {
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
app(MemberService::class)->addMember($record, $organization, Role::from($data['role']), true);
}),
])
->actions([
Action::make('view')
@@ -58,13 +85,55 @@ class UsersRelationManager extends RelationManager
->url(fn (User $record): string => UserResource::getUrl('view', [
'record' => $record->getKey(),
])),
Tables\Actions\EditAction::make(),
Tables\Actions\DetachAction::make(),
Tables\Actions\EditAction::make()
->using(function (User $record, array $data): User {
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
/** @var Member $member */
$member = $record->getRelation('membership');
if ($data['billable_rate'] !== $member->billable_rate) {
$member->billable_rate = $data['billable_rate'];
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
}
if ($data['role'] !== $member->role) {
try {
app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true);
} catch (ApiException $exception) {
Notification::make()
->danger()
->title('Update failed')
->body($exception->getTranslatedMessage())
->persistent()
->send();
}
}
$member->save();
return $record;
}),
Tables\Actions\DetachAction::make()
->using(function (User $record): void {
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
$member = Member::query()
->whereBelongsTo($record, 'user')
->whereBelongsTo($organization, 'organization')
->firstOrFail();
try {
app(MemberService::class)->removeMember($member, $organization);
} catch (ApiException $exception) {
Notification::make()
->danger()
->title('Delete failed')
->body($exception->getTranslatedMessage())
->persistent()
->send();
}
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(),
]),
]);
}
}

View File

@@ -15,7 +15,8 @@ class EditProjectMember extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}
}

View File

@@ -15,7 +15,8 @@ class ListProjectMembers extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}
}

View File

@@ -72,8 +72,13 @@ class ProjectResource extends Resource
])
->filters([
SelectFilter::make('organization')
->label('Organization')
->relationship('organization', 'name')
->searchable(),
SelectFilter::make('organization_id')
->label('Organization ID')
->relationship('organization', 'id')
->searchable(),
])
->defaultSort('created_at', 'desc')
->actions([

View File

@@ -15,7 +15,8 @@ class EditProject extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}
}

View File

@@ -15,7 +15,8 @@ class ListProjects extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\ReportResource\Pages;
use App\Models\Report;
use App\Service\Dto\ReportPropertiesDto;
use Filament\Forms;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
class ReportResource extends Resource
{
protected static ?string $model = Report::class;
protected static ?string $navigationIcon = 'heroicon-o-document-chart-bar';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 7;
public static function form(Form $form): Form
{
return $form
->columns(1)
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('description')
->label('Description')
->nullable()
->maxLength(255),
Toggle::make('is_public')
->label('Is public?')
->required(),
DateTimePicker::make('public_until')
->label('Public until')
->nullable(),
Forms\Components\Select::make('organization_id')
->label('Organization')
->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name'])
->disabled()
->required(),
Forms\Components\TextInput::make('share_secret')
->label('Share Secret')
->nullable(),
PrettyJsonField::make('properties')
->formatStateUsing(function (ReportPropertiesDto $state, Report $record): string {
return $record->getRawOriginal('properties');
})
->disabled(),
DateTimePicker::make('created_at')
->label('Created At')
->hiddenOn(['create'])
->disabled(),
DateTimePicker::make('updated_at')
->label('Updated At')
->hiddenOn(['create'])
->disabled(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('description')
->searchable()
->sortable(),
ToggleColumn::make('is_public')
->label('Is public?')
->sortable(),
TextColumn::make('organization.name')
->searchable()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable(),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('created_at', 'desc')
->filters([
SelectFilter::make('organization')
->label('Organization')
->relationship('organization', 'name')
->searchable(),
SelectFilter::make('organization_id')
->label('Organization ID')
->relationship('organization', 'id')
->searchable(),
])
->actions([
Action::make('public-view')
->label('Public')
->icon('heroicon-o-eye')
->color('gray')
->hidden(fn (Report $record): bool => $record->getShareableLink() === null)
->url(fn (Report $record): string => $record->getShareableLink(), true),
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
]);
}
public static function getRelations(): array
{
return [
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListReports::route('/'),
'edit' => Pages\EditReport::route('/{record}/edit'),
'view' => Pages\ViewReport::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ReportResource\Pages;
use App\Filament\Resources\ReportResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditReport extends EditRecord
{
protected static string $resource = ReportResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ReportResource\Pages;
use App\Filament\Resources\ReportResource;
use Filament\Resources\Pages\ListRecords;
class ListReports extends ListRecords
{
protected static string $resource = ReportResource::class;
protected function getHeaderActions(): array
{
return [
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ReportResource\Pages;
use App\Filament\Resources\ReportResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewReport extends ViewRecord
{
protected static string $resource = ReportResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make('edit')
->icon('heroicon-s-pencil'),
];
}
}

View File

@@ -60,8 +60,13 @@ class TagResource extends Resource
->defaultSort('created_at', 'desc')
->filters([
SelectFilter::make('organization')
->label('Organization')
->relationship('organization', 'name')
->searchable(),
SelectFilter::make('organization_id')
->label('Organization ID')
->relationship('organization', 'id')
->searchable(),
])
->actions([
Tables\Actions\EditAction::make(),

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