mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-08 05:43:18 +01:00
Compare commits
8 Commits
fix/db-ssl
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1abe73241 | ||
|
|
0ef944a896 | ||
|
|
2072223f19 | ||
|
|
9ea774c1ac | ||
|
|
d1f7e206f2 | ||
|
|
5f23ce98d7 | ||
|
|
400df23035 | ||
|
|
981bad5414 |
19
.github/workflows/build-onpremise.yml
vendored
19
.github/workflows/build-onpremise.yml
vendored
@@ -103,8 +103,6 @@ jobs:
|
|||||||
{
|
{
|
||||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||||
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
|
|
||||||
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: "Checkout invoicing extension"
|
- name: "Checkout invoicing extension"
|
||||||
@@ -121,26 +119,9 @@ jobs:
|
|||||||
- name: "Install npm dependencies in invoicing extension"
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
run: cd extensions/Invoicing && npm ci
|
run: cd extensions/Invoicing && npm ci
|
||||||
|
|
||||||
- name: "Checkout auditing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
|
|
||||||
path: extensions/Auditing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in auditing extension"
|
|
||||||
run: cd extensions/Auditing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in auditing extension"
|
|
||||||
run: cd extensions/Auditing && npm ci
|
|
||||||
|
|
||||||
- name: "Activate invoicing extension"
|
- name: "Activate invoicing extension"
|
||||||
run: php artisan module:enable Invoicing
|
run: php artisan module:enable Invoicing
|
||||||
|
|
||||||
- name: "Activate auditing extension"
|
|
||||||
run: php artisan module:enable Auditing
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
|||||||
24
.github/workflows/build-private.yml
vendored
24
.github/workflows/build-private.yml
vendored
@@ -84,8 +84,6 @@ jobs:
|
|||||||
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
||||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||||
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
|
|
||||||
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: "Checkout billing extension"
|
- name: "Checkout billing extension"
|
||||||
@@ -147,25 +145,6 @@ jobs:
|
|||||||
- name: "Install npm dependencies in invoicing extension"
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
run: cd extensions/Invoicing && npm ci
|
run: cd extensions/Invoicing && npm ci
|
||||||
|
|
||||||
- name: "Checkout auditing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
|
|
||||||
path: extensions/Auditing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in auditing extension"
|
|
||||||
uses: php-actions/composer@v6
|
|
||||||
with:
|
|
||||||
working_dir: "extensions/Auditing"
|
|
||||||
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 auditing extension"
|
|
||||||
run: cd extensions/Auditing && npm ci
|
|
||||||
|
|
||||||
- name: "Setup PHP with PECL extension"
|
- name: "Setup PHP with PECL extension"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -189,9 +168,6 @@ jobs:
|
|||||||
- name: "Activate invoicing extension"
|
- name: "Activate invoicing extension"
|
||||||
run: php artisan module:enable Invoicing
|
run: php artisan module:enable Invoicing
|
||||||
|
|
||||||
- name: "Activate auditing extension"
|
|
||||||
run: php artisan module:enable Auditing
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/npm-format-check.yml
vendored
2
.github/workflows/npm-format-check.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: NPM Format Check
|
name: NPM Format Check
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format-check:
|
format-check:
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource\Pages\CreateAudit;
|
||||||
|
use App\Filament\Resources\AuditResource\Pages\ListAudits;
|
||||||
|
use App\Filament\Resources\AuditResource\Pages\ViewAudit;
|
||||||
|
use App\Models\Audit;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||||
|
|
||||||
|
class AuditResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Audit::class;
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-archive-box';
|
||||||
|
|
||||||
|
protected static string|\UnitEnum|null $navigationGroup = 'System';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('user_type')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('user_id'),
|
||||||
|
TextInput::make('event')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('auditable_type')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('auditable_id')
|
||||||
|
->required(),
|
||||||
|
PrettyJsonField::make('old_values'),
|
||||||
|
PrettyJsonField::make('new_values'),
|
||||||
|
Textarea::make('url'),
|
||||||
|
TextInput::make('ip_address'),
|
||||||
|
TextInput::make('user_agent')
|
||||||
|
->maxLength(1023),
|
||||||
|
TextInput::make('tags')
|
||||||
|
->maxLength(255),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('user.name'),
|
||||||
|
TextColumn::make('event'),
|
||||||
|
TextColumn::make('auditable_type'),
|
||||||
|
TextColumn::make('auditable_id'),
|
||||||
|
IconColumn::make('was_command')
|
||||||
|
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
|
||||||
|
->boolean(),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
TextColumn::make('updated_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
ViewAction::make(),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListAudits::route('/'),
|
||||||
|
'create' => CreateAudit::route('/create'),
|
||||||
|
'view' => ViewAudit::route('/{record}'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateAudit extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
}
|
||||||
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal file
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListAudits extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
|
|
||||||
|
class ViewAudit extends ViewRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
}
|
||||||
@@ -88,8 +88,6 @@ class InvitationController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Remove a pending invitation
|
* Remove a pending invitation
|
||||||
*
|
*
|
||||||
* This revokes the invitation: the link in the invitation email stops working. Find the invitation ID with `GET /organizations/{organization}/invitations`.
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId removeInvitation
|
* @operationId removeInvitation
|
||||||
|
|||||||
@@ -145,9 +145,6 @@ class MemberController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Merge one member into another
|
* Merge one member into another
|
||||||
*
|
*
|
||||||
* Only placeholder members (for example people created by an import) can be merged. All time entries and other data of the placeholder
|
|
||||||
* are reassigned to the member given in `member_id`, and the placeholder is removed. Find both member IDs with `GET /organizations/{organization}/members`.
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ class ReportController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create report
|
* Create report
|
||||||
*
|
*
|
||||||
* A report is a saved set of filters. Set `is_public` to `true` to share it: the response then contains the `shareable_link` that can be opened without logging in.
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId createReport
|
* @operationId createReport
|
||||||
|
|||||||
@@ -109,14 +109,9 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get time entries in organization
|
* Get time entries in organization
|
||||||
*
|
*
|
||||||
* Without a member filter this returns the time entries of all members of the organization (for users who may view all time entries, such as owners and admins), not only your own.
|
* If you only need time entries for a specific user, you can filter by `member_id`.
|
||||||
* To get only your own time entries, pass your member ID as `member_id`. Your member ID is the `id` returned for this organization by `GET /v1/users/me/memberships`; it is not your user ID.
|
|
||||||
* Users with the permission `time-entries:view:own` can only use this endpoint with their own member ID in the member_id filter.
|
* Users with the permission `time-entries:view:own` can only use this endpoint with their own member ID in the member_id filter.
|
||||||
*
|
*
|
||||||
* The `start` and `end` filters both apply to the start time of an entry, in UTC. Convert the user's local day boundaries to UTC first.
|
|
||||||
* Results are paginated with `limit` (default 100, max 500) and `offset`; check `meta.total` and fetch further pages when needed.
|
|
||||||
* To find the running timer, use `active=true` (or `GET /v1/users/me/time-entries/active`).
|
|
||||||
*
|
|
||||||
* @return TimeEntryCollection<TimeEntryResource>
|
* @return TimeEntryCollection<TimeEntryResource>
|
||||||
*
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
@@ -356,11 +351,6 @@ class TimeEntryController extends Controller
|
|||||||
* The parameters `group` and `sub_group` allow you to group the time entries by different criteria.
|
* The parameters `group` and `sub_group` allow you to group the time entries by different criteria.
|
||||||
* If the group parameters are all set to `null` or are all missing, the endpoint will aggregate all filtered time entries.
|
* If the group parameters are all set to `null` or are all missing, the endpoint will aggregate all filtered time entries.
|
||||||
*
|
*
|
||||||
* Durations are returned in `seconds` (divide by 3600 for hours) and amounts in `cost` as cents in the organization's currency (divide by 100 for money).
|
|
||||||
* Filter by member with `member_id`. Your member ID is the `id` returned for this organization by `GET /v1/users/me/memberships`; it is not your user ID.
|
|
||||||
* Array filters use the query format `client_ids[]=<id>`.
|
|
||||||
* Example: billable hours per client for a period: `group=client&billable=true&start=...&end=...`.
|
|
||||||
*
|
|
||||||
* @operationId getAggregatedTimeEntries
|
* @operationId getAggregatedTimeEntries
|
||||||
*
|
*
|
||||||
* @return array{
|
* @return array{
|
||||||
@@ -594,12 +584,6 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create time entry
|
* Create time entry
|
||||||
*
|
*
|
||||||
* `billable` is not taken from the project. To match the web app, set it to the project's `is_billable` value (or `false` without a project).
|
|
||||||
*
|
|
||||||
* A member can only have one running time entry (an entry with `end` set to `null`). Creating a running entry while another one runs fails with `time_entry_still_running`.
|
|
||||||
* To start a new timer, first stop the running entry by updating its `end` to the current time, then create the new entry.
|
|
||||||
* To log past work, send both `start` and `end` (UTC). Create one entry per block of work.
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
* @throws TimeEntryStillRunningApiException
|
* @throws TimeEntryStillRunningApiException
|
||||||
*
|
*
|
||||||
@@ -650,8 +634,6 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Update time entry
|
* Update time entry
|
||||||
*
|
*
|
||||||
* To stop a running timer, set `end` to the stop time (UTC). Times the user gives in their own timezone must be converted to UTC first.
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException|TimeEntryCanNotBeRestartedApiException
|
* @throws AuthorizationException|TimeEntryCanNotBeRestartedApiException
|
||||||
*
|
*
|
||||||
* @operationId updateTimeEntry
|
* @operationId updateTimeEntry
|
||||||
@@ -720,10 +702,6 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Update multiple time entries
|
* Update multiple time entries
|
||||||
*
|
*
|
||||||
* Applies the same `changes` to every entry in `ids`. To find the IDs, list entries with `GET /organizations/{organization}/time-entries`
|
|
||||||
* (filtered by `member_id`, the project and the other criteria), then send their IDs here.
|
|
||||||
* When `changes.project_id` moves entries to another project, also set `changes.task_id` to a task of the new project or to `null`, because tasks belong to a project.
|
|
||||||
*
|
|
||||||
* @operationId updateMultipleTimeEntries
|
* @operationId updateMultipleTimeEntries
|
||||||
*
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class MemberUpdateRequest extends BaseFormRequest
|
|||||||
'string',
|
'string',
|
||||||
Rule::enum(Role::class),
|
Rule::enum(Role::class),
|
||||||
],
|
],
|
||||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
|
||||||
'billable_rate' => array_merge(
|
'billable_rate' => array_merge(
|
||||||
[
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class OrganizationUpdateRequest extends BaseFormRequest
|
|||||||
'string',
|
'string',
|
||||||
new CurrencyRule,
|
new CurrencyRule,
|
||||||
],
|
],
|
||||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
|
||||||
'billable_rate' => array_merge(
|
'billable_rate' => array_merge(
|
||||||
[
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class ProjectStoreRequest extends BaseFormRequest
|
|||||||
'required',
|
'required',
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
|
||||||
'billable_rate' => array_merge(
|
'billable_rate' => array_merge(
|
||||||
[
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class ProjectUpdateRequest extends BaseFormRequest
|
|||||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||||
})->uuid(),
|
})->uuid(),
|
||||||
],
|
],
|
||||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
|
||||||
'billable_rate' => array_merge([
|
'billable_rate' => array_merge([
|
||||||
'nullable',
|
'nullable',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class ProjectMemberStoreRequest extends BaseFormRequest
|
|||||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||||
})->uuid(),
|
})->uuid(),
|
||||||
],
|
],
|
||||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
|
||||||
'billable_rate' => array_merge(
|
'billable_rate' => array_merge(
|
||||||
[
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class ProjectMemberUpdateRequest extends BaseFormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
|
||||||
'billable_rate' => array_merge(
|
'billable_rate' => array_merge(
|
||||||
[
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// Filter by member ID. Without it, users who may view all time entries (owners, admins) get the entries of every member; pass your own member ID (from GET /v1/users/me/memberships) to get only yours
|
// Filter by member ID
|
||||||
'member_id' => [
|
'member_id' => [
|
||||||
'string',
|
'string',
|
||||||
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder {
|
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder {
|
||||||
@@ -155,7 +155,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
|||||||
'string',
|
'string',
|
||||||
Rule::enum(TimeEntryType::class),
|
Rule::enum(TimeEntryType::class),
|
||||||
],
|
],
|
||||||
// Limit the number of returned time entries (default: 100)
|
// Limit the number of returned time entries (default: 150)
|
||||||
'limit' => [
|
'limit' => [
|
||||||
'integer',
|
'integer',
|
||||||
'min:1',
|
'min:1',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// ID of the organization member that the time entry should belong to (a member ID from GET /v1/users/me/memberships or the members list, not a user ID)
|
// ID of the organization member that the time entry should belong to
|
||||||
'member_id' => [
|
'member_id' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
@@ -86,7 +86,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
|
|||||||
'date_format:Y-m-d\TH:i:s\Z',
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
'after_or_equal:start',
|
'after_or_equal:start',
|
||||||
],
|
],
|
||||||
// Whether time entry is billable. Not derived from the project: set it to the project's is_billable value to match the web app
|
// Whether time entry is billable
|
||||||
'billable' => [
|
'billable' => [
|
||||||
'required',
|
'required',
|
||||||
'boolean',
|
'boolean',
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use App\Models\Client;
|
|||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
use App\Models\Passport\RefreshToken;
|
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\ProjectMember;
|
use App\Models\ProjectMember;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
@@ -170,10 +169,6 @@ class DeletionService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh tokens are not linked to the user directly, so they need to be deleted via their access tokens.
|
|
||||||
// Otherwise a still-valid refresh token could be used to issue a new access token for a deleted user,
|
|
||||||
// which fails with a foreign key violation on oauth_access_tokens.user_id.
|
|
||||||
RefreshToken::query()->whereIn('access_token_id', $user->accessTokens()->pluck('id'))->delete();
|
|
||||||
$user->accessTokens()->delete();
|
$user->accessTokens()->delete();
|
||||||
$user->authCodes()->delete();
|
$user->authCodes()->delete();
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
'Project',
|
'Project',
|
||||||
'Description',
|
'Description',
|
||||||
'User',
|
'User',
|
||||||
|
'Group',
|
||||||
'Email',
|
'Email',
|
||||||
'Tags',
|
'Tags',
|
||||||
'Start Date',
|
'Start Date',
|
||||||
|
|||||||
79
composer.lock
generated
79
composer.lock
generated
@@ -429,24 +429,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
"version": "1.0.0",
|
"version": "0.19.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/brick/math.git",
|
"url": "https://github.com/brick/math.git",
|
||||||
"reference": "2effe05d2177c451b86c6a073196a4034c02f211"
|
"reference": "a89bc96a7cf3d7b59e725afe57ccb95eb03cf6ce"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/brick/math/zipball/2effe05d2177c451b86c6a073196a4034c02f211",
|
"url": "https://api.github.com/repos/brick/math/zipball/a89bc96a7cf3d7b59e725afe57ccb95eb03cf6ce",
|
||||||
"reference": "2effe05d2177c451b86c6a073196a4034c02f211",
|
"reference": "a89bc96a7cf3d7b59e725afe57ccb95eb03cf6ce",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2"
|
"php": "^8.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "2.2.13",
|
"phpstan/phpstan": "2.1.22",
|
||||||
"phpstan/phpstan-phpunit": "2.0.18",
|
|
||||||
"phpunit/phpunit": "^11.5"
|
"phpunit/phpunit": "^11.5"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
@@ -477,7 +476,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/brick/math/issues",
|
"issues": "https://github.com/brick/math/issues",
|
||||||
"source": "https://github.com/brick/math/tree/1.0.0"
|
"source": "https://github.com/brick/math/tree/0.19.1"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -485,7 +484,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-09-12T10:28:18+00:00"
|
"time": "2026-08-08T23:03:16+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/money",
|
"name": "brick/money",
|
||||||
@@ -4219,16 +4218,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/fortify",
|
"name": "laravel/fortify",
|
||||||
"version": "v1.40.0",
|
"version": "v1.39.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/fortify.git",
|
"url": "https://github.com/laravel/fortify.git",
|
||||||
"reference": "fe0fce8814660317df0684f2c7be3b573def67d3"
|
"reference": "b1fc50707bbe007fd92165d8b7d460ab549b355a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/fortify/zipball/fe0fce8814660317df0684f2c7be3b573def67d3",
|
"url": "https://api.github.com/repos/laravel/fortify/zipball/b1fc50707bbe007fd92165d8b7d460ab549b355a",
|
||||||
"reference": "fe0fce8814660317df0684f2c7be3b573def67d3",
|
"reference": "b1fc50707bbe007fd92165d8b7d460ab549b355a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4279,24 +4278,24 @@
|
|||||||
"issues": "https://github.com/laravel/fortify/issues",
|
"issues": "https://github.com/laravel/fortify/issues",
|
||||||
"source": "https://github.com/laravel/fortify"
|
"source": "https://github.com/laravel/fortify"
|
||||||
},
|
},
|
||||||
"time": "2026-09-10T11:52:08+00:00"
|
"time": "2026-08-23T07:46:41+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/framework",
|
"name": "laravel/framework",
|
||||||
"version": "v13.33.0",
|
"version": "v13.32.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/framework.git",
|
"url": "https://github.com/laravel/framework.git",
|
||||||
"reference": "91188a17ceaa3dbace6e8a5f7abd0d042e466359"
|
"reference": "cdd8b33c246719acdd118c705ce8c7ab5ef48a96"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/framework/zipball/91188a17ceaa3dbace6e8a5f7abd0d042e466359",
|
"url": "https://api.github.com/repos/laravel/framework/zipball/cdd8b33c246719acdd118c705ce8c7ab5ef48a96",
|
||||||
"reference": "91188a17ceaa3dbace6e8a5f7abd0d042e466359",
|
"reference": "cdd8b33c246719acdd118c705ce8c7ab5ef48a96",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0",
|
"brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19",
|
||||||
"composer-runtime-api": "^2.2",
|
"composer-runtime-api": "^2.2",
|
||||||
"doctrine/inflector": "^2.0.5",
|
"doctrine/inflector": "^2.0.5",
|
||||||
"dragonmantank/cron-expression": "^3.4",
|
"dragonmantank/cron-expression": "^3.4",
|
||||||
@@ -4510,20 +4509,20 @@
|
|||||||
"issues": "https://github.com/laravel/framework/issues",
|
"issues": "https://github.com/laravel/framework/issues",
|
||||||
"source": "https://github.com/laravel/framework"
|
"source": "https://github.com/laravel/framework"
|
||||||
},
|
},
|
||||||
"time": "2026-09-22T14:12:33+00:00"
|
"time": "2026-09-15T14:55:30+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/octane",
|
"name": "laravel/octane",
|
||||||
"version": "v2.20.0",
|
"version": "v2.19.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/octane.git",
|
"url": "https://github.com/laravel/octane.git",
|
||||||
"reference": "df4c38dcddc5f51e36e7f1d788841130c4fb8f45"
|
"reference": "68a2516a0318baba0de0e4648f61e335c5e69dca"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/octane/zipball/df4c38dcddc5f51e36e7f1d788841130c4fb8f45",
|
"url": "https://api.github.com/repos/laravel/octane/zipball/68a2516a0318baba0de0e4648f61e335c5e69dca",
|
||||||
"reference": "df4c38dcddc5f51e36e7f1d788841130c4fb8f45",
|
"reference": "68a2516a0318baba0de0e4648f61e335c5e69dca",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4599,7 +4598,7 @@
|
|||||||
"issues": "https://github.com/laravel/octane/issues",
|
"issues": "https://github.com/laravel/octane/issues",
|
||||||
"source": "https://github.com/laravel/octane"
|
"source": "https://github.com/laravel/octane"
|
||||||
},
|
},
|
||||||
"time": "2026-08-23T17:25:20+00:00"
|
"time": "2026-08-13T13:58:52+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/passkeys",
|
"name": "laravel/passkeys",
|
||||||
@@ -4805,16 +4804,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.1.0",
|
"version": "v2.0.16",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/serializable-closure.git",
|
"url": "https://github.com/laravel/serializable-closure.git",
|
||||||
"reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5"
|
"reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5",
|
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed",
|
||||||
"reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5",
|
"reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4862,7 +4861,7 @@
|
|||||||
"issues": "https://github.com/laravel/serializable-closure/issues",
|
"issues": "https://github.com/laravel/serializable-closure/issues",
|
||||||
"source": "https://github.com/laravel/serializable-closure"
|
"source": "https://github.com/laravel/serializable-closure"
|
||||||
},
|
},
|
||||||
"time": "2026-09-22T14:32:34+00:00"
|
"time": "2026-08-18T20:28:54+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/tinker",
|
"name": "laravel/tinker",
|
||||||
@@ -15338,16 +15337,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/sail",
|
"name": "laravel/sail",
|
||||||
"version": "v1.68.0",
|
"version": "v1.67.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/sail.git",
|
"url": "https://github.com/laravel/sail.git",
|
||||||
"reference": "2bc304083d515065b03944e425e62cb3c526c33e"
|
"reference": "639e03ac12cf23def171770bcab05758045b2642"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/sail/zipball/2bc304083d515065b03944e425e62cb3c526c33e",
|
"url": "https://api.github.com/repos/laravel/sail/zipball/639e03ac12cf23def171770bcab05758045b2642",
|
||||||
"reference": "2bc304083d515065b03944e425e62cb3c526c33e",
|
"reference": "639e03ac12cf23def171770bcab05758045b2642",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -15397,7 +15396,7 @@
|
|||||||
"issues": "https://github.com/laravel/sail/issues",
|
"issues": "https://github.com/laravel/sail/issues",
|
||||||
"source": "https://github.com/laravel/sail"
|
"source": "https://github.com/laravel/sail"
|
||||||
},
|
},
|
||||||
"time": "2026-09-18T14:34:46+00:00"
|
"time": "2026-08-12T13:55:56+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/sentinel",
|
"name": "laravel/sentinel",
|
||||||
@@ -15457,16 +15456,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/telescope",
|
"name": "laravel/telescope",
|
||||||
"version": "v5.25.0",
|
"version": "v5.24.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/telescope.git",
|
"url": "https://github.com/laravel/telescope.git",
|
||||||
"reference": "65fe6724f8688de01dfa74b28a0edc191bc3caa8"
|
"reference": "50cbcf4553ddfd8d4bd456b04f97dfed923007d9"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/telescope/zipball/65fe6724f8688de01dfa74b28a0edc191bc3caa8",
|
"url": "https://api.github.com/repos/laravel/telescope/zipball/50cbcf4553ddfd8d4bd456b04f97dfed923007d9",
|
||||||
"reference": "65fe6724f8688de01dfa74b28a0edc191bc3caa8",
|
"reference": "50cbcf4553ddfd8d4bd456b04f97dfed923007d9",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -15519,9 +15518,9 @@
|
|||||||
"monitoring"
|
"monitoring"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/laravel/telescope/tree/v5.25.0"
|
"source": "https://github.com/laravel/telescope/tree/v5.24.0"
|
||||||
},
|
},
|
||||||
"time": "2026-09-09T14:16:19+00:00"
|
"time": "2026-09-08T16:15:16+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mockery/mockery",
|
"name": "mockery/mockery",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ return [
|
|||||||
|
|
||||||
'sqlite' => [
|
'sqlite' => [
|
||||||
'driver' => 'sqlite',
|
'driver' => 'sqlite',
|
||||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
'url' => env('DB_URL'),
|
||||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
@@ -47,7 +47,7 @@ return [
|
|||||||
|
|
||||||
'pgsql' => [
|
'pgsql' => [
|
||||||
'driver' => 'pgsql',
|
'driver' => 'pgsql',
|
||||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
'url' => env('DB_URL'),
|
||||||
'host' => env('DB_HOST', '127.0.0.1'),
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
'port' => env('DB_PORT', '5432'),
|
'port' => env('DB_PORT', '5432'),
|
||||||
'database' => env('DB_DATABASE', 'forge'),
|
'database' => env('DB_DATABASE', 'forge'),
|
||||||
@@ -57,12 +57,12 @@ return [
|
|||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'search_path' => 'public',
|
'search_path' => 'public',
|
||||||
'sslmode' => env('DB_SSL_MODE', 'prefer'),
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'pgsql_test' => [
|
'pgsql_test' => [
|
||||||
'driver' => 'pgsql',
|
'driver' => 'pgsql',
|
||||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
'url' => env('DB_URL'),
|
||||||
'host' => env('DB_TEST_HOST', '127.0.0.1'),
|
'host' => env('DB_TEST_HOST', '127.0.0.1'),
|
||||||
'port' => env('DB_TEST_PORT', '5432'),
|
'port' => env('DB_TEST_PORT', '5432'),
|
||||||
'database' => env('DB_TEST_DATABASE', 'forge'),
|
'database' => env('DB_TEST_DATABASE', 'forge'),
|
||||||
@@ -72,12 +72,12 @@ return [
|
|||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'search_path' => 'public',
|
'search_path' => 'public',
|
||||||
'sslmode' => env('DB_SSL_MODE', 'prefer'),
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'sqlsrv' => [
|
'sqlsrv' => [
|
||||||
'driver' => 'sqlsrv',
|
'driver' => 'sqlsrv',
|
||||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
'url' => env('DB_URL'),
|
||||||
'host' => env('DB_HOST', 'localhost'),
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
'port' => env('DB_PORT', '1433'),
|
'port' => env('DB_PORT', '1433'),
|
||||||
'database' => env('DB_DATABASE', 'laravel'),
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
|||||||
@@ -28,30 +28,7 @@ return [
|
|||||||
/*
|
/*
|
||||||
* Description rendered on the home page of the API documentation (`/docs/api`).
|
* Description rendered on the home page of the API documentation (`/docs/api`).
|
||||||
*/
|
*/
|
||||||
'description' => <<<'MD'
|
'description' => '',
|
||||||
## Getting started
|
|
||||||
|
|
||||||
All organization endpoints live under `/v1/organizations/{organization}`, where `{organization}` is the organization's ID. Authenticate with `Authorization: Bearer <token>` and send `Accept: application/json`.
|
|
||||||
|
|
||||||
**1. Find yourself.** Call `GET /v1/users/me/memberships`. Each membership contains the **organization ID** (use it as `{organization}` in paths) and your **member ID** in that organization (the membership `id`). Most endpoints filter by member ID, not by user ID.
|
|
||||||
|
|
||||||
**2. Scope to your own data.** For owners and admins, `GET /time-entries` and `GET /time-entries/aggregate` return the whole organization's time entries unless you pass `member_id`. When acting for "me", always pass your member ID, both when reading and before changing entries.
|
|
||||||
|
|
||||||
**3. Resolve names to IDs.** Look up projects, clients, tags, tasks and members by name with their list endpoints (`GET /projects`, `GET /clients`, `GET /tags`, `GET /tasks`, `GET /members`). Never guess IDs.
|
|
||||||
|
|
||||||
**4. Use UTC.** All timestamps are sent and returned in UTC as `Y-m-d\TH:i:s\Z` (example: `2026-10-02T07:30:00Z`). Convert the user's local times and day boundaries to UTC before sending them.
|
|
||||||
|
|
||||||
**5. Money is in cents.** Billable rates and costs are integers in cents of the organization's currency (`8000` means 80.00).
|
|
||||||
|
|
||||||
## Common tasks
|
|
||||||
|
|
||||||
- **Start a timer:** stop the running entry first (find it with `GET /v1/users/me/time-entries/active`, then `PUT` its `end`), then `POST /time-entries` with `start` and `end: null`. Only one entry can run per member.
|
|
||||||
- **Log past work:** `POST /time-entries` once per block with `member_id`, `start`, `end`, `project_id` and `billable` set to the project's `is_billable` (it is not derived automatically).
|
|
||||||
- **Fix or stop an entry:** `PUT /time-entries/{timeEntry}` with the new `start` or `end` in UTC.
|
|
||||||
- **Move entries to another project:** list them with `member_id` and filters, then `PATCH /time-entries` with their `ids` and `changes.project_id`, plus `changes.task_id` set to a task of the new project or `null`.
|
|
||||||
- **Totals and reports:** `GET /time-entries/aggregate` with `group` (for example `client` or `project`) and `start`/`end`; durations are in `seconds`, amounts in `cost` (cents).
|
|
||||||
- **Share a report:** `POST /reports` with `is_public: true` and use the returned `shareable_link`.
|
|
||||||
MD,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const resourcePages = [
|
|||||||
{ path: '/admin/project-members', heading: 'Project Members' },
|
{ path: '/admin/project-members', heading: 'Project Members' },
|
||||||
{ path: '/admin/tokens', heading: 'Tokens' },
|
{ path: '/admin/tokens', heading: 'Tokens' },
|
||||||
{ path: '/admin/failed-jobs', heading: 'Failed Jobs' },
|
{ path: '/admin/failed-jobs', heading: 'Failed Jobs' },
|
||||||
|
{ path: '/admin/audits', heading: 'Audits' },
|
||||||
];
|
];
|
||||||
|
|
||||||
test.describe('Admin Panel Access', () => {
|
test.describe('Admin Panel Access', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Billing": {
|
"Billing": {
|
||||||
"repository": "solidtime-io/extension-billing",
|
"repository": "solidtime-io/extension-billing",
|
||||||
"ref": "v0.0.8"
|
"ref": "v0.0.7"
|
||||||
},
|
},
|
||||||
"Services": {
|
"Services": {
|
||||||
"repository": "solidtime-io/extension-services",
|
"repository": "solidtime-io/extension-services",
|
||||||
@@ -10,9 +10,5 @@
|
|||||||
"Invoicing": {
|
"Invoicing": {
|
||||||
"repository": "solidtime-io/extension-invoicing",
|
"repository": "solidtime-io/extension-invoicing",
|
||||||
"ref": "v0.0.7"
|
"ref": "v0.0.7"
|
||||||
},
|
|
||||||
"Auditing": {
|
|
||||||
"repository": "solidtime-io/extension-auditing",
|
|
||||||
"ref": "v0.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,6 @@ export function useEventResize(params: {
|
|||||||
edge: 'start' | 'end',
|
edge: 'start' | 'end',
|
||||||
dayStr: string
|
dayStr: string
|
||||||
) {
|
) {
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
|||||||
@@ -168,10 +168,6 @@ defineExpose({ submit, focusAfterStart });
|
|||||||
data-testid="time_entry_description"
|
data-testid="time_entry_description"
|
||||||
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-base text-text-primary bg-transparent border-none placeholder-text-secondary focus:ring-0 transition"
|
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-base text-text-primary bg-transparent border-none placeholder-text-secondary focus:ring-0 transition"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
|
||||||
data-1p-ignore
|
|
||||||
data-lpignore="true"
|
|
||||||
data-form-type="other"
|
|
||||||
@keydown.enter="submit"
|
@keydown.enter="submit"
|
||||||
@keydown.esc="showDropdown = false"
|
@keydown.esc="showDropdown = false"
|
||||||
@blur="updateTimeEntryDescription" />
|
@blur="updateTimeEntryDescription" />
|
||||||
|
|||||||
@@ -170,10 +170,6 @@ function closeAndFocusInput() {
|
|||||||
: 'text-text-primary bg-card-background border-border-secondary border border-none'
|
: 'text-text-primary bg-card-background border-border-secondary border border-none'
|
||||||
"
|
"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
|
||||||
data-1p-ignore
|
|
||||||
data-lpignore="true"
|
|
||||||
data-form-type="other"
|
|
||||||
@focusin="openModalOnTab"
|
@focusin="openModalOnTab"
|
||||||
@click="openModalOnClick"
|
@click="openModalOnClick"
|
||||||
@keydown.exact.tab="focusNextElement"
|
@keydown.exact.tab="focusNextElement"
|
||||||
|
|||||||
59
tests/Unit/Filament/Resources/AuditResourceTest.php
Normal file
59
tests/Unit/Filament/Resources/AuditResourceTest.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use App\Models\Audit;
|
||||||
|
use App\Models\TimeEntry;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use PHPUnit\Framework\Attributes\UsesClass;
|
||||||
|
use Tests\Unit\Filament\FilamentTestCase;
|
||||||
|
|
||||||
|
#[UsesClass(AuditResource::class)]
|
||||||
|
class AuditResourceTest extends FilamentTestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Config::set('auth.super_admins', ['admin@example.com']);
|
||||||
|
$user = User::factory()->withPersonalOrganization()->create([
|
||||||
|
'email' => 'admin@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_list_audits(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$user = $this->createUserWithPermission();
|
||||||
|
$timeEntry = TimeEntry::factory()->forMember($user->member)->create();
|
||||||
|
DB::table((new Audit)->getTable())->delete();
|
||||||
|
$audits = Audit::factory()->auditFor($timeEntry)->auditUser($user->user)->createMany(5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = Livewire::test(AuditResource\Pages\ListAudits::class);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$response->assertCanSeeTableRecords($audits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_see_view_page_of_audit(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
DB::table((new Audit)->getTable())->delete();
|
||||||
|
$audit = Audit::factory()->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = Livewire::test(AuditResource\Pages\ViewAudit::class, ['record' => $audit->getKey()]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,6 @@ use App\Exceptions\Api\CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembe
|
|||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Passport\Client as PassportClient;
|
|
||||||
use App\Models\Passport\RefreshToken;
|
|
||||||
use App\Models\Passport\Token;
|
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\ProjectMember;
|
use App\Models\ProjectMember;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
@@ -26,7 +23,6 @@ use Illuminate\Support\Collection;
|
|||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use Tests\TestCaseWithDatabase;
|
use Tests\TestCaseWithDatabase;
|
||||||
use TiMacDonald\Log\LogEntry;
|
use TiMacDonald\Log\LogEntry;
|
||||||
@@ -428,45 +424,4 @@ class DeletionServiceTest extends TestCaseWithDatabase
|
|||||||
'role' => Role::Placeholder->value,
|
'role' => Role::Placeholder->value,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_delete_user_deletes_access_tokens_and_their_refresh_tokens_but_does_not_delete_tokens_of_other_users(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$otherUser = User::factory()->create();
|
|
||||||
$passportClient = PassportClient::factory()->create();
|
|
||||||
|
|
||||||
$userToken = Token::factory()->forUser($user)->forClient($passportClient)->create();
|
|
||||||
$userRefreshToken = RefreshToken::query()->create([
|
|
||||||
'id' => Str::random(100),
|
|
||||||
'access_token_id' => $userToken->getKey(),
|
|
||||||
'revoked' => false,
|
|
||||||
'expires_at' => now()->addDays(30),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$otherUserToken = Token::factory()->forUser($otherUser)->forClient($passportClient)->create();
|
|
||||||
$otherUserRefreshToken = RefreshToken::query()->create([
|
|
||||||
'id' => Str::random(100),
|
|
||||||
'access_token_id' => $otherUserToken->getKey(),
|
|
||||||
'revoked' => false,
|
|
||||||
'expires_at' => now()->addDays(30),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$this->deletionService->deleteUser($user);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertDatabaseMissing(Token::class, [
|
|
||||||
'id' => $userToken->getKey(),
|
|
||||||
]);
|
|
||||||
$this->assertDatabaseMissing(RefreshToken::class, [
|
|
||||||
'id' => $userRefreshToken->getKey(),
|
|
||||||
]);
|
|
||||||
$this->assertDatabaseHas(Token::class, [
|
|
||||||
'id' => $otherUserToken->getKey(),
|
|
||||||
]);
|
|
||||||
$this->assertDatabaseHas(RefreshToken::class, [
|
|
||||||
'id' => $otherUserRefreshToken->getKey(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user