mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-09 22:33:18 +01:00
Compare commits
1 Commits
main
...
feature/vo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fbf7b87bb |
4
.env.ci
4
.env.ci
@@ -6,7 +6,6 @@ APP_DEBUG=true
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
APP_FORCE_HTTPS=false
|
APP_FORCE_HTTPS=false
|
||||||
APP_ENABLE_REGISTRATION=true
|
APP_ENABLE_REGISTRATION=true
|
||||||
SUPER_ADMINS=admin@example.com
|
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
@@ -65,6 +64,3 @@ GOTENBERG_URL=http://localhost:3000
|
|||||||
|
|
||||||
# Octane
|
# Octane
|
||||||
OCTANE_SERVER=frankenphp
|
OCTANE_SERVER=frankenphp
|
||||||
|
|
||||||
# Inertia
|
|
||||||
INERTIA_SSR_ENABLED=false
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
|
|||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=https://solidtime.test
|
APP_URL=https://solidtime.test
|
||||||
APP_FORCE_HTTPS=false
|
APP_FORCE_HTTPS=false
|
||||||
# Supported values: on, off, invite-only (true/false are supported for backwards compatibility)
|
APP_ENABLE_REGISTRATION=true
|
||||||
APP_ENABLE_REGISTRATION=on
|
|
||||||
SUPER_ADMINS=admin@example.com
|
SUPER_ADMINS=admin@example.com
|
||||||
PAGINATION_PER_PAGE_DEFAULT=500
|
PAGINATION_PER_PAGE_DEFAULT=500
|
||||||
|
|
||||||
@@ -88,6 +87,3 @@ FORWARD_DB_PORT=54329
|
|||||||
VITE_HOST_NAME=vite.solidtime.test
|
VITE_HOST_NAME=vite.solidtime.test
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
#SAIL_XDEBUG_MODE=develop,debug,coverage
|
#SAIL_XDEBUG_MODE=develop,debug,coverage
|
||||||
|
|
||||||
# This app does not ship a Node SSR bundle, so Inertia SSR must stay disabled.
|
|
||||||
INERTIA_SSR_ENABLED=false
|
|
||||||
|
|||||||
@@ -13,5 +13,3 @@ LOG_DEPRECATIONS_CHANNEL=null
|
|||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=pgsql
|
DB_CONNECTION=pgsql
|
||||||
|
|
||||||
INERTIA_SSR_ENABLED=false
|
|
||||||
|
|||||||
12
.github/VOUCHED.td
vendored
12
.github/VOUCHED.td
vendored
@@ -14,14 +14,10 @@
|
|||||||
# Seeded 2026-07-25 from the authors of every merged pull request.
|
# Seeded 2026-07-25 from the authors of every merged pull request.
|
||||||
|
|
||||||
agross
|
agross
|
||||||
akolenda
|
|
||||||
bufferhead-code
|
|
||||||
candideu
|
candideu
|
||||||
kasparrosin
|
KasparRosin
|
||||||
korridor
|
korridor
|
||||||
nikbucher tasks table sorting
|
Onatcer
|
||||||
onatcer
|
ShrootBuck
|
||||||
shrootbuck
|
smileBeda
|
||||||
smilebeda
|
|
||||||
thespyder
|
|
||||||
utlark
|
utlark
|
||||||
|
|||||||
23
.github/workflows/build-onpremise.yml
vendored
23
.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
|
||||||
|
|
||||||
@@ -160,7 +141,7 @@ jobs:
|
|||||||
${{ env.DOCKER_REPO }}
|
${{ env.DOCKER_REPO }}
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
@@ -214,7 +195,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
|
|||||||
28
.github/workflows/build-private.yml
vendored
28
.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"
|
||||||
@@ -99,7 +97,7 @@ jobs:
|
|||||||
- name: "Install dependencies in billing extension"
|
- name: "Install dependencies in billing extension"
|
||||||
uses: php-actions/composer@v6
|
uses: php-actions/composer@v6
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "${{ secrets.LARAVEL_SPARK_USERNAME }}", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "gregor@vostrak.at", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
||||||
with:
|
with:
|
||||||
working_dir: "extensions/Billing"
|
working_dir: "extensions/Billing"
|
||||||
command: install
|
command: install
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -201,7 +177,7 @@ jobs:
|
|||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: rg.fr-par.scw.cloud/solidtime
|
registry: rg.fr-par.scw.cloud/solidtime
|
||||||
username: nologin
|
username: nologin
|
||||||
|
|||||||
8
.github/workflows/build-public.yml
vendored
8
.github/workflows/build-public.yml
vendored
@@ -117,13 +117,13 @@ jobs:
|
|||||||
${{ env.GHCR_REPO }}
|
${{ env.GHCR_REPO }}
|
||||||
|
|
||||||
- name: "Login to Docker Hub Container Registry"
|
- name: "Login to Docker Hub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -177,13 +177,13 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to Docker Hub"
|
- name: "Login to Docker Hub"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GHCR"
|
- name: "Login to GHCR"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
31
.github/workflows/extension-manifest-check.yml
vendored
31
.github/workflows/extension-manifest-check.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: Extension Manifest Check
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- extensions/manifest.json
|
|
||||||
- .github/workflows/extension-manifest-check.yml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
check-refs:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- name: "Checkout code"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
# Extension refs must be pinned to a release tag (vX.Y.Z) or a full commit
|
|
||||||
# SHA. Branch names (main, feature/foo, ...) are only acceptable while
|
|
||||||
# developing and must be replaced before the pull request is merged.
|
|
||||||
- name: "Check extension refs are pinned"
|
|
||||||
run: |
|
|
||||||
failed=0
|
|
||||||
while IFS=$'\t' read -r name ref; do
|
|
||||||
if [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.]+)?$ ]] || [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then
|
|
||||||
echo "✅ $name: $ref"
|
|
||||||
else
|
|
||||||
echo "::error file=extensions/manifest.json::Extension \"$name\" uses ref \"$ref\", which is not a release tag or commit SHA. Pin it before merging."
|
|
||||||
failed=1
|
|
||||||
fi
|
|
||||||
done < <(jq -r 'to_entries[] | [.key, .value.ref] | @tsv' extensions/manifest.json)
|
|
||||||
exit $failed
|
|
||||||
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:
|
||||||
|
|||||||
139
.github/workflows/phpunit-extensions.yml
vendored
139
.github/workflows/phpunit-extensions.yml
vendored
@@ -1,139 +0,0 @@
|
|||||||
name: PHPUnit Tests - Extensions
|
|
||||||
on: push
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
phpunit-extensions:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 25
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
postgres_version: [ 15, 16, 17 ]
|
|
||||||
|
|
||||||
services:
|
|
||||||
pgsql_test:
|
|
||||||
image: postgres:${{ matrix.postgres_version }}
|
|
||||||
env:
|
|
||||||
PGPASSWORD: 'root'
|
|
||||||
POSTGRES_DB: 'laravel'
|
|
||||||
POSTGRES_USER: 'root'
|
|
||||||
POSTGRES_PASSWORD: 'root'
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--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@v7
|
|
||||||
|
|
||||||
- name: "Setup PHP"
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
|
||||||
coverage: pcov
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '20.x'
|
|
||||||
|
|
||||||
- name: "Read extension manifest"
|
|
||||||
id: extension-manifest
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "billing_repository=$(jq -r '.Billing.repository' extensions/manifest.json)"
|
|
||||||
echo "billing_ref=$(jq -r '.Billing.ref' extensions/manifest.json)"
|
|
||||||
echo "services_repository=$(jq -r '.Services.repository' 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_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: "Checkout billing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.billing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
|
|
||||||
path: extensions/Billing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in billing extension"
|
|
||||||
working-directory: extensions/Billing
|
|
||||||
env:
|
|
||||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "contact@solidtime.io", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in billing extension"
|
|
||||||
run: cd extensions/Billing && npm ci
|
|
||||||
|
|
||||||
- name: "Checkout services extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.services_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.services_ref }}
|
|
||||||
path: extensions/Services
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in services extension"
|
|
||||||
working-directory: extensions/Services
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in services extension"
|
|
||||||
run: cd extensions/Services && npm ci
|
|
||||||
|
|
||||||
- name: "Checkout invoicing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
|
|
||||||
path: extensions/Invoicing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in invoicing extension"
|
|
||||||
working-directory: extensions/Invoicing
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in invoicing extension"
|
|
||||||
run: cd extensions/Invoicing && npm ci
|
|
||||||
|
|
||||||
- name: "Run composer install"
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Activate billing extension"
|
|
||||||
run: php artisan module:enable Billing
|
|
||||||
|
|
||||||
- name: "Activate services extension"
|
|
||||||
run: php artisan module:enable Services
|
|
||||||
|
|
||||||
- name: "Activate invoicing extension"
|
|
||||||
run: php artisan module:enable Invoicing
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: "Build Frontend"
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: "Prepare Laravel Application"
|
|
||||||
run: |
|
|
||||||
cp .env.ci .env
|
|
||||||
php artisan key:generate
|
|
||||||
php artisan passport:keys
|
|
||||||
|
|
||||||
- name: "Run PHPUnit (extensions)"
|
|
||||||
run: php artisan test extensions/Billing/tests extensions/Services/tests extensions/Invoicing/tests --stop-on-failure
|
|
||||||
|
|
||||||
- name: "Run PHPUnit (core)"
|
|
||||||
run: php artisan test --testsuite=Unit,Feature --stop-on-failure
|
|
||||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
||||||
|
|
||||||
- name: "Upload coverage reports to Codecov"
|
- name: "Upload coverage reports to Codecov"
|
||||||
uses: codecov/codecov-action@v7.1.1
|
uses: codecov/codecov-action@v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: solidtime-io/solidtime
|
slug: solidtime-io/solidtime
|
||||||
|
|||||||
185
.github/workflows/playwright-extensions.yml
vendored
185
.github/workflows/playwright-extensions.yml
vendored
@@ -1,185 +0,0 @@
|
|||||||
name: Playwright Tests - Extensions
|
|
||||||
on: push
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: test (${{ matrix.variant }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
# Same extension sets as build-private.yml and build-onpremise.yml, so the extension specs run
|
|
||||||
# with and without Billing.
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- variant: private
|
|
||||||
billing: true
|
|
||||||
extensions: Billing Services Invoicing
|
|
||||||
- variant: onpremise
|
|
||||||
billing: false
|
|
||||||
extensions: Invoicing
|
|
||||||
|
|
||||||
services:
|
|
||||||
mailpit:
|
|
||||||
image: 'axllent/mailpit:latest'
|
|
||||||
ports:
|
|
||||||
- 1025:1025
|
|
||||||
- 8025:8025
|
|
||||||
pgsql_test:
|
|
||||||
image: postgres:15
|
|
||||||
env:
|
|
||||||
PGPASSWORD: 'root'
|
|
||||||
POSTGRES_DB: 'laravel'
|
|
||||||
POSTGRES_USER: 'root'
|
|
||||||
POSTGRES_PASSWORD: 'root'
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--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@v7
|
|
||||||
|
|
||||||
- name: "Setup node"
|
|
||||||
uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '20.x'
|
|
||||||
|
|
||||||
- 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: "Read extension manifest"
|
|
||||||
id: extension-manifest
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "billing_repository=$(jq -r '.Billing.repository' extensions/manifest.json)"
|
|
||||||
echo "billing_ref=$(jq -r '.Billing.ref' extensions/manifest.json)"
|
|
||||||
echo "services_repository=$(jq -r '.Services.repository' 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_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: "Checkout billing extension"
|
|
||||||
if: matrix.billing
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.billing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
|
|
||||||
path: extensions/Billing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in billing extension"
|
|
||||||
if: matrix.billing
|
|
||||||
working-directory: extensions/Billing
|
|
||||||
env:
|
|
||||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "contact@solidtime.io", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in billing extension"
|
|
||||||
if: matrix.billing
|
|
||||||
run: cd extensions/Billing && npm ci
|
|
||||||
|
|
||||||
- name: "Checkout services extension"
|
|
||||||
if: matrix.billing
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.services_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.services_ref }}
|
|
||||||
path: extensions/Services
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in services extension"
|
|
||||||
if: matrix.billing
|
|
||||||
working-directory: extensions/Services
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in services extension"
|
|
||||||
if: matrix.billing
|
|
||||||
run: cd extensions/Services && npm ci
|
|
||||||
|
|
||||||
- name: "Checkout invoicing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
|
|
||||||
path: extensions/Invoicing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in invoicing extension"
|
|
||||||
working-directory: extensions/Invoicing
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in invoicing extension"
|
|
||||||
run: cd extensions/Invoicing && npm ci
|
|
||||||
|
|
||||||
- name: "Run composer install"
|
|
||||||
run: composer install -n --prefer-dist
|
|
||||||
|
|
||||||
# Must run before `migrate` so the extensions' migrations are applied, and before the
|
|
||||||
# frontend build so vite collects the extensions' assets. It also writes
|
|
||||||
# modules_statuses.json, which playwright.config.ts reads to collect the extension specs.
|
|
||||||
- name: "Activate extensions"
|
|
||||||
run: |
|
|
||||||
for extension in ${{ matrix.extensions }}; do
|
|
||||||
php artisan module:enable "$extension"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: "Prepare Laravel Application"
|
|
||||||
run: |
|
|
||||||
cp .env.ci .env
|
|
||||||
php artisan key:generate
|
|
||||||
php artisan passport:keys
|
|
||||||
php artisan migrate --seed
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: "Build Frontend"
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- 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: "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
|
|
||||||
|
|
||||||
# Only the extension specs, the core specs run without extensions in playwright.yml
|
|
||||||
- name: "Run Playwright tests"
|
|
||||||
run: npx playwright test extensions/
|
|
||||||
env:
|
|
||||||
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
|
|
||||||
MAILPIT_BASE_URL: 'http://localhost:8025'
|
|
||||||
|
|
||||||
# No artifacts are uploaded on purpose. This repository is public, so every artifact is
|
|
||||||
# downloadable by anyone, and Playwright's reports carry the source of the specs that
|
|
||||||
# produced them (traces embed whole source files, error-context.md embeds a window of the
|
|
||||||
# spec). Those specs live in the private extension repositories. Failures are diagnosed from
|
|
||||||
# the job log; to inspect a trace, reproduce the failure locally with the extension checked out.
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,6 @@ dist
|
|||||||
/public/storage
|
/public/storage
|
||||||
/public/css
|
/public/css
|
||||||
/public/js
|
/public/js
|
||||||
/public/fonts/filament
|
|
||||||
/public/vendor
|
/public/vendor
|
||||||
/lang/vendor
|
/lang/vendor
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
@@ -44,5 +43,4 @@ yarn-error.log
|
|||||||
/data
|
/data
|
||||||
/config/caddy
|
/config/caddy
|
||||||
/config/composer
|
/config/composer
|
||||||
/config/psysh
|
|
||||||
/AGENTS.md
|
/AGENTS.md
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# Build environment for Anthropic's OSS Scanner (https://github.com/anthropics/oss-scanner).
|
|
||||||
# The scanner builds this image with the repository root as the build context and then audits it without network
|
|
||||||
# access, so everything needed to run the app and its test suite (PHP + Composer deps, Node deps + built frontend,
|
|
||||||
# a local PostgreSQL and a local Gotenberg for PDF rendering) is installed here.
|
|
||||||
#
|
|
||||||
# The base image is the FrankenPHP image the production image (docker/prod/Dockerfile) is built on: in production the
|
|
||||||
# app runs as long-lived Laravel Octane workers in FrankenPHP worker mode, not as one PHP process per request.
|
|
||||||
#
|
|
||||||
# Inside the finished image:
|
|
||||||
# .oss-scanner/start-postgres.sh start the local PostgreSQL server (required for tests and the app)
|
|
||||||
# .oss-scanner/start-gotenberg.sh start the local Gotenberg server (required for PDF exports)
|
|
||||||
# php artisan test run the PHPUnit suite (needs PostgreSQL and Gotenberg running)
|
|
||||||
# .oss-scanner/start-octane.sh run the app like production on http://127.0.0.1:8000 (starts everything above)
|
|
||||||
|
|
||||||
ARG FRANKENPHP_VERSION=1.11
|
|
||||||
ARG PHP_VERSION=8.3
|
|
||||||
|
|
||||||
FROM node:20-trixie-slim AS node
|
|
||||||
|
|
||||||
FROM gotenberg/gotenberg:8 AS gotenberg
|
|
||||||
|
|
||||||
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
|
||||||
COMPOSER_ALLOW_SUPERUSER=1 \
|
|
||||||
COMPOSER_NO_INTERACTION=1 \
|
|
||||||
OCTANE_SERVER=frankenphp \
|
|
||||||
TZ=UTC
|
|
||||||
|
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
|
||||||
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
|
||||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
|
||||||
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|
||||||
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
|
||||||
|
|
||||||
# PostgreSQL 17 (Debian trixie default) is one of the versions solidtime is tested against in CI.
|
|
||||||
# chromium, qpdf and exiftool are what Gotenberg needs for HTML to PDF rendering.
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends git unzip curl ca-certificates postgresql postgresql-client \
|
|
||||||
chromium fonts-liberation qpdf libimage-exiftool-perl \
|
|
||||||
&& install-php-extensions pdo_pgsql pgsql intl gd zip bcmath exif pcntl sockets soap apcu \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& echo "memory_limit=2G" > "$PHP_INI_DIR/conf.d/99-oss-scanner.ini"
|
|
||||||
|
|
||||||
# Gotenberg 8 (the PDF renderer, same image as in CI). solidtime only uses its Chromium HTML to PDF route, so LibreOffice
|
|
||||||
# and pdftk are not installed: Gotenberg only checks at startup that their binaries exist, start-gotenberg.sh disables
|
|
||||||
# the LibreOffice routes and the Chromium route does not use pdftk.
|
|
||||||
COPY --from=gotenberg /usr/bin/gotenberg /usr/local/bin/gotenberg
|
|
||||||
COPY --from=gotenberg /usr/bin/pdfcpu /usr/local/bin/pdfcpu
|
|
||||||
COPY --from=gotenberg /opt/gotenberg /opt/gotenberg
|
|
||||||
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium \
|
|
||||||
CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/chromium-hyphen-data \
|
|
||||||
GOTENBERG_VERSIONS_DIR_PATH=/opt/gotenberg/versions \
|
|
||||||
QPDF_BIN_PATH=/usr/bin/qpdf \
|
|
||||||
EXIFTOOL_BIN_PATH=/usr/bin/exiftool \
|
|
||||||
PDFCPU_BIN_PATH=/usr/local/bin/pdfcpu \
|
|
||||||
LIBREOFFICE_BIN_PATH=/bin/false \
|
|
||||||
UNOCONVERTER_BIN_PATH=/bin/false \
|
|
||||||
PDFTK_BIN_PATH=/bin/false
|
|
||||||
|
|
||||||
# Database matching .env.ci: user root / password root, database laravel.
|
|
||||||
RUN pg_ctlcluster 17 main start \
|
|
||||||
&& runuser -u postgres -- psql -c "CREATE ROLE root WITH LOGIN SUPERUSER PASSWORD 'root';" \
|
|
||||||
&& runuser -u postgres -- createdb -O root laravel \
|
|
||||||
&& pg_ctlcluster 17 main stop
|
|
||||||
|
|
||||||
# scanner contract: the checkout lives inside the image, at /src
|
|
||||||
COPY . /src
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
RUN composer install --prefer-dist \
|
|
||||||
&& npm ci \
|
|
||||||
&& npm run build
|
|
||||||
|
|
||||||
RUN cp .env.ci .env \
|
|
||||||
&& php artisan key:generate \
|
|
||||||
&& php artisan passport:keys --force \
|
|
||||||
&& php artisan octane:install --server=frankenphp --no-interaction \
|
|
||||||
&& chmod +x .oss-scanner/*.sh
|
|
||||||
|
|
||||||
# Run the test suite so the image is known to work, but do not fail the build on test failures.
|
|
||||||
RUN .oss-scanner/start-postgres.sh \
|
|
||||||
&& .oss-scanner/start-gotenberg.sh \
|
|
||||||
&& (php artisan test || echo "WARNING: PHPUnit reported failures") \
|
|
||||||
&& kill "$(cat /tmp/gotenberg.pid)" \
|
|
||||||
&& pg_ctlcluster 17 main stop
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Used instead of the root .dockerignore when building .oss-scanner/Dockerfile. The root one is tailored to the
|
|
||||||
# production image and excludes tests, phpunit.xml etc., which the scanner needs.
|
|
||||||
node_modules
|
|
||||||
extensions/*/node_modules
|
|
||||||
vendor
|
|
||||||
.env
|
|
||||||
public/build
|
|
||||||
public/hot
|
|
||||||
storage/*.key
|
|
||||||
storage/logs/*
|
|
||||||
coverage
|
|
||||||
test-results
|
|
||||||
playwright-report
|
|
||||||
.phpunit.cache
|
|
||||||
.phpunit.result.cache
|
|
||||||
auth.json
|
|
||||||
.DS_Store
|
|
||||||
.idea
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Start the local Gotenberg server (PDF rendering) on 127.0.0.1:3000, matching GOTENBERG_URL in .env.ci.
|
|
||||||
# Only the Chromium routes are needed by solidtime, see .oss-scanner/Dockerfile. Stop: kill "$(cat /tmp/gotenberg.pid)"
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if ! curl -fs http://127.0.0.1:3000/health >/dev/null; then
|
|
||||||
nohup gotenberg \
|
|
||||||
--api-port=3000 \
|
|
||||||
--libreoffice-disable-routes \
|
|
||||||
--libreoffice-auto-start=false \
|
|
||||||
--log-level=warn \
|
|
||||||
>/tmp/gotenberg.log 2>&1 &
|
|
||||||
echo $! >/tmp/gotenberg.pid
|
|
||||||
until curl -fs http://127.0.0.1:3000/health >/dev/null; do sleep 0.5; done
|
|
||||||
fi
|
|
||||||
echo "Gotenberg is running on http://127.0.0.1:3000 (log: /tmp/gotenberg.log)"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Run the app the way production does: Laravel Octane on FrankenPHP in worker mode, with the production Caddyfile.
|
|
||||||
# A worker boots the app once and then serves many requests, so state kept in memory (static properties, singletons,
|
|
||||||
# changed config, ...) survives from one request to the next. With a single worker, consecutive requests always hit the
|
|
||||||
# same worker, so such leaks between requests (e.g. between users of different organizations) reproduce reliably.
|
|
||||||
#
|
|
||||||
# Usage: .oss-scanner/start-octane.sh (OCTANE_WORKERS and OCTANE_MAX_REQUESTS can be overridden)
|
|
||||||
# Stop: php artisan octane:stop
|
|
||||||
# After changing PHP code, run `php artisan octane:reload`: workers keep the code they booted with.
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
.oss-scanner/start-postgres.sh
|
|
||||||
.oss-scanner/start-gotenberg.sh
|
|
||||||
|
|
||||||
php artisan migrate --force
|
|
||||||
if [ "$(PGPASSWORD=root psql -h 127.0.0.1 -U root -d laravel -tAc 'SELECT count(*) FROM users')" = "0" ]; then
|
|
||||||
php artisan db:seed --force
|
|
||||||
fi
|
|
||||||
php artisan optimize:clear >/dev/null
|
|
||||||
|
|
||||||
nohup php artisan octane:frankenphp \
|
|
||||||
--host=127.0.0.1 \
|
|
||||||
--port=8000 \
|
|
||||||
--workers="${OCTANE_WORKERS:-1}" \
|
|
||||||
--max-requests="${OCTANE_MAX_REQUESTS:-10000}" \
|
|
||||||
--caddyfile=docker/prod/deployment/octane/FrankenPHP/Caddyfile \
|
|
||||||
>/tmp/octane.log 2>&1 &
|
|
||||||
|
|
||||||
until curl -fs -o /dev/null http://127.0.0.1:8000/login; do
|
|
||||||
if ! kill -0 $! 2>/dev/null; then
|
|
||||||
cat /tmp/octane.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 0.5
|
|
||||||
done
|
|
||||||
echo "solidtime is running on http://127.0.0.1:8000 with Octane/FrankenPHP (log: /tmp/octane.log)"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Start the local PostgreSQL server used by the test suite and the app (see .oss-scanner/Dockerfile).
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
pg_ctlcluster 17 main start 2>/dev/null || true
|
|
||||||
until pg_isready -h 127.0.0.1 -p 5432 -q; do sleep 0.5; done
|
|
||||||
echo "PostgreSQL is running on 127.0.0.1:5432 (user root, password root, database laravel)"
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# Threat model
|
|
||||||
|
|
||||||
## What this project does
|
|
||||||
|
|
||||||
solidtime is an open-source, multi-tenant time tracking web application (Laravel backend, Vue 3 + Inertia frontend,
|
|
||||||
PostgreSQL). It runs as a hosted SaaS (solidtime.io) and is self-hosted by many organisations. Users belong to one or
|
|
||||||
more **organizations**; inside an organization each member has a role: `owner`, `admin`, `manager`, `employee` or
|
|
||||||
`placeholder` (an imported, non-login member). What each role may do is defined in `app/Service/PermissionStore.php`.
|
|
||||||
|
|
||||||
The most important security property is **isolation**: a user must never read or modify data of an organization they
|
|
||||||
are not a member of, and within an organization a member must not exceed the permissions of their role (e.g. an
|
|
||||||
employee must not see other members' time entries, billable rates, or manage members, unless the organization settings
|
|
||||||
explicitly allow it).
|
|
||||||
|
|
||||||
## Trust boundaries
|
|
||||||
|
|
||||||
- **Super admins are fully trusted.** They are the instance operators, configured via the `SUPER_ADMINS` env
|
|
||||||
variable, and have access to the Filament admin panel (`app/Filament`), which can view and change data of every
|
|
||||||
organization and impersonate users. Anything a super admin can do through the panel (including XSS, SQL injection,
|
|
||||||
SSRF or file access that is only reachable from the panel) is not a vulnerability.
|
|
||||||
- What **is** in scope: a user who is not a super admin reaching the admin panel, or any of its actions, at all.
|
|
||||||
- Operators of a self-hosted instance (shell, database, environment, filesystem access) are trusted.
|
|
||||||
- Everyone else, including organization owners and admins when acting outside their own organization, is untrusted.
|
|
||||||
|
|
||||||
## Runtime: long-lived Octane workers
|
|
||||||
|
|
||||||
In production (the hosted SaaS and the official Docker image, `docker/prod/`) the app does **not** run as one PHP
|
|
||||||
process per request. It runs on Laravel Octane with FrankenPHP in worker mode: each worker boots the application once
|
|
||||||
and then serves many requests from different users and organizations. Anything kept in memory survives from one request
|
|
||||||
to the next unless Octane resets it (`config/octane.php` lists what is reset). This includes static properties and
|
|
||||||
static caches, container bindings registered with `singleton()` instead of `scoped()` (see
|
|
||||||
`app/Providers/AppServiceProvider.php`), objects captured by those singletons, runtime `config()` / locale / timezone
|
|
||||||
changes, macros and event listeners registered during a request, and state in third-party packages.
|
|
||||||
|
|
||||||
Request A leaving state behind that request B (another user, possibly of another organization) then sees or is
|
|
||||||
affected by is in scope, rated by its impact like any other issue (see severity below). The PHPUnit suite cannot
|
|
||||||
show this class of bug, because it boots a fresh application for every test. It has to be reproduced over HTTP against
|
|
||||||
the Octane server, see "How to exercise it".
|
|
||||||
|
|
||||||
## Where untrusted input enters
|
|
||||||
|
|
||||||
All authenticated users, including employees of any organization and anyone who self-registers (registration is open
|
|
||||||
by default), are untrusted.
|
|
||||||
|
|
||||||
- **JSON API** `routes/api.php` (`/api/v1/...`), authenticated via Passport (session cookie or personal access token).
|
|
||||||
Most routes are scoped by `{organization}` and authorised in the controllers / form requests.
|
|
||||||
- **Public, unauthenticated** endpoints: `GET /api/v1/public/reports` (shared reports, accessed by a secret), login,
|
|
||||||
registration, password reset, email verification, organization invitation acceptance (`routes/web.php`).
|
|
||||||
- **Web / Inertia routes** `routes/web.php` and Fortify/Jetstream actions in `app/Actions`.
|
|
||||||
- **Imports** (`app/Service/Import/Importers`): user-uploaded CSV and ZIP files from Toggl, Clockify, Harvest,
|
|
||||||
generic CSV and solidtime's own export format. ZIP handling is in `ZipImportHelper.php`.
|
|
||||||
- **Exports / reports** (`app/Service/Export`, `app/Service/ReportExport`): CSV/XLSX/ODS and PDF. PDFs are rendered by
|
|
||||||
sending HTML to a Gotenberg (headless Chromium) service, so user-controlled content in that HTML matters.
|
|
||||||
- **OAuth** (Passport) authorization and token endpoints.
|
|
||||||
- **Filament admin panel** (`app/Filament`): only its access control is in scope (see Trust boundaries).
|
|
||||||
|
|
||||||
## Components that matter most / least
|
|
||||||
|
|
||||||
Most important: organization scoping and role checks in the API controllers, form requests (`app/Http/Requests`),
|
|
||||||
`PermissionStore`, public report sharing, invitations and member management (role changes, ownership transfer, member
|
|
||||||
merge), authentication flows (Fortify, 2FA, email change, API tokens), import parsing.
|
|
||||||
|
|
||||||
Less important / out of scope:
|
|
||||||
- `extensions/` is empty in this repository (proprietary modules are not part of the open-source code).
|
|
||||||
- `docker/`, `k8s/`, `e2e/`, `playwright/`, `docs/` and developer tooling.
|
|
||||||
- Third-party dependencies in `vendor/` and `node_modules/`, unless solidtime uses them in an unsafe way.
|
|
||||||
|
|
||||||
## How to exercise it
|
|
||||||
|
|
||||||
- `.oss-scanner/start-postgres.sh` starts the local PostgreSQL server (user `root`, password `root`, db `laravel`).
|
|
||||||
- `.oss-scanner/start-gotenberg.sh` starts the local Gotenberg server (PDF rendering via headless Chromium) on
|
|
||||||
http://127.0.0.1:3000, so the PDF export code path, including what Chromium does with the rendered HTML, can be
|
|
||||||
exercised.
|
|
||||||
- `php artisan test` runs the PHPUnit suite (start PostgreSQL and Gotenberg first); all tests are expected to pass. Endpoint tests in `tests/Unit/Endpoint/Api/V1/` show how to create users,
|
|
||||||
organizations and members with factories and call the API with a given role; they are the quickest way to write a
|
|
||||||
reproducer. Example: `php artisan test --filter=TimeEntryEndpointTest`.
|
|
||||||
- To run the app like production: `.oss-scanner/start-octane.sh` (http://127.0.0.1:8000). It starts PostgreSQL and
|
|
||||||
Gotenberg, migrates (and seeds an empty database, see `database/seeders/DatabaseSeeder.php` for the users) and runs
|
|
||||||
Octane/FrankenPHP with the production Caddyfile and a **single worker**, so consecutive requests always hit the same
|
|
||||||
worker and leaks between requests reproduce reliably. Workers keep the code they booted with: after changing PHP
|
|
||||||
code run `php artisan octane:reload`. Stop it with `php artisan octane:stop`. Note that the test suite and the app
|
|
||||||
share the same database, so `php artisan test` wipes the app's data.
|
|
||||||
- A reproducer for a leak between requests is a script that sends request A (e.g. as a member of organization X) and
|
|
||||||
then request B (as a user of organization Y) to the running Octane server and shows that B observes A's state. API
|
|
||||||
requests can be authenticated with a personal access token, e.g. created with
|
|
||||||
`php artisan tinker --execute="echo App\Models\User::where('email', '...')->first()->createToken('t')->accessToken;"`.
|
|
||||||
- There is no network: mail delivery is not available (mail uses the `array` driver in tests), and remote resources
|
|
||||||
referenced by PDF templates (e.g. fonts from fonts.bunny.net) fail to load, so PDFs fall back to local fonts.
|
|
||||||
|
|
||||||
## How we rate severity
|
|
||||||
|
|
||||||
- **Critical**: unauthenticated access to other users' data or accounts; authentication bypass; remote code execution;
|
|
||||||
SQL injection reachable by any registered user; reading or writing data of an organization the attacker is not a
|
|
||||||
member of, including through state leaking between requests in an Octane worker.
|
|
||||||
- **High**: privilege escalation within an organization (e.g. employee to admin/owner, or performing admin-only
|
|
||||||
actions); access to data the role must not see (other members' time entries, billable rates, member emails) when
|
|
||||||
the organization settings do not allow it; stored XSS that executes in another user's session; SSRF via PDF
|
|
||||||
rendering or imports; account takeover requiring user interaction.
|
|
||||||
- **Medium**: information disclosure with limited impact, CSRF on state-changing endpoints, issues requiring an
|
|
||||||
unusual but realistic configuration, denial of service by a single authenticated request (e.g. pathological
|
|
||||||
import file).
|
|
||||||
- **Low**: everything else with real security impact.
|
|
||||||
|
|
||||||
## Anything to leave alone
|
|
||||||
|
|
||||||
Please do not report (see also `SECURITY.md`):
|
|
||||||
- Theoretical findings without a working reproducer.
|
|
||||||
- Missing or weak security headers in isolation; TLS / mail DNS configuration.
|
|
||||||
- Self-XSS; CSRF on non-state-changing endpoints (logout, theme).
|
|
||||||
- CSV / spreadsheet formula injection in exports.
|
|
||||||
- Owners or admins acting destructively within their own organization.
|
|
||||||
- Anything requiring direct DB, shell or filesystem access on a self-hosted instance.
|
|
||||||
- Anything that requires being a super admin, including issues inside the Filament admin panel.
|
|
||||||
- Missing OAuth scope enforcement (not implemented yet).
|
|
||||||
- Rate-limit tuning and generic DoS through volume of requests.
|
|
||||||
|
|
||||||
## Reports and patches
|
|
||||||
|
|
||||||
Please include the affected endpoint or code path, the attacker's role and the victim, a PHPUnit test (in the style of
|
|
||||||
`tests/Unit/Endpoint/Api/V1/`) that reproduces the issue (for leaks between requests: a script against
|
|
||||||
`.oss-scanner/start-octane.sh` instead), and a minimal patch that follows the existing patterns
|
|
||||||
(authorisation in form requests/controllers via `PermissionStore`).
|
|
||||||
@@ -4,11 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Actions\Fortify;
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
use App\Enums\RegistrationMode;
|
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Events\NewsletterRegistered;
|
use App\Events\NewsletterRegistered;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\InvitationService;
|
|
||||||
use App\Service\IpLookup\IpLookupServiceContract;
|
use App\Service\IpLookup\IpLookupServiceContract;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
@@ -33,14 +31,13 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
*/
|
*/
|
||||||
public function create(array $input): User
|
public function create(array $input): User
|
||||||
{
|
{
|
||||||
$registrationMode = RegistrationMode::fromConfig(config('app.enable_registration'));
|
if (! config('app.enable_registration')) {
|
||||||
if ($registrationMode === RegistrationMode::Off) {
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'email' => [__('Registration is disabled.')],
|
'email' => [__('Registration is disabled.')],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = Validator::make($input, [
|
Validator::make($input, [
|
||||||
'name' => [
|
'name' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
@@ -63,20 +60,6 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
],
|
],
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
if ($registrationMode === RegistrationMode::InviteOnly) {
|
|
||||||
$invitationService = app(InvitationService::class);
|
|
||||||
$email = (string) $validated['email'];
|
|
||||||
if (! $invitationService->hasAcceptedInvitationForEmail($email)) {
|
|
||||||
$message = $invitationService->hasPendingInvitationForEmail($email)
|
|
||||||
? __('Please accept the organization invitation sent to your email address before registering.')
|
|
||||||
: __('Registration is only available to invited users.');
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'email' => [$message],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$timezone = null;
|
$timezone = null;
|
||||||
if (array_key_exists('timezone', $input) && is_string($input['timezone'])) {
|
if (array_key_exists('timezone', $input) && is_string($input['timezone'])) {
|
||||||
if (app(TimezoneService::class)->isValid($input['timezone'])) {
|
if (app(TimezoneService::class)->isValid($input['timezone'])) {
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Auth;
|
|
||||||
|
|
||||||
use Illuminate\Auth\EloquentUserProvider;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User provider that only resolves non-placeholder users.
|
|
||||||
*
|
|
||||||
* Placeholder users are created by imports and when members are removed from an
|
|
||||||
* organization. They can share an email address with a real user, so resolving a user by
|
|
||||||
* email can return a placeholder instead of the real account. The login flow filters them
|
|
||||||
* out explicitly, but the password broker and the guard credential checks (for example the
|
|
||||||
* password confirmation) resolve users through the configured user provider.
|
|
||||||
*
|
|
||||||
* Registered as the "eloquent" provider driver in the AuthServiceProvider, so it replaces the
|
|
||||||
* built-in one for every provider in config/auth.php.
|
|
||||||
*/
|
|
||||||
class ActiveUserProvider extends EloquentUserProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param Model|null $model
|
|
||||||
* @return Builder<Model>
|
|
||||||
*/
|
|
||||||
#[Override]
|
|
||||||
protected function newModelQuery($model = null): Builder
|
|
||||||
{
|
|
||||||
$query = parent::newModelQuery($model);
|
|
||||||
$query->getQuery()->where('is_placeholder', '=', false);
|
|
||||||
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ use Illuminate\Database\Query\JoinClause;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class SelfHostDatabaseConsistency extends Command
|
class SelfHostDatabaseConsistency extends Command
|
||||||
{
|
{
|
||||||
@@ -102,7 +101,7 @@ class SelfHostDatabaseConsistency extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection<int, stdClass> $problems
|
* @param Collection<int, \stdClass> $problems
|
||||||
*/
|
*/
|
||||||
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
|
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace App\Console\Commands\SelfHost;
|
|||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Encryption\Encrypter;
|
use Illuminate\Encryption\Encrypter;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use phpseclib4\Crypt\RSA;
|
use phpseclib3\Crypt\RSA;
|
||||||
|
|
||||||
class SelfHostGenerateKeysCommand extends Command
|
class SelfHostGenerateKeysCommand extends Command
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ class TimeEntrySendStillRunningMailsCommand extends Command
|
|||||||
])
|
])
|
||||||
->whereHas('user', function (Builder $query): void {
|
->whereHas('user', function (Builder $query): void {
|
||||||
/** @var Builder<User> $query */
|
/** @var Builder<User> $query */
|
||||||
$query->where('is_placeholder', '=', false)
|
$query->where('is_placeholder', '=', false);
|
||||||
->where('send_time_entry_still_running_email', '=', true);
|
|
||||||
})
|
})
|
||||||
->orderBy('created_at', 'asc')
|
->orderBy('created_at', 'asc')
|
||||||
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {
|
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalComparison: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The goal is reached when at least the target amount of time was tracked in the period.
|
|
||||||
*/
|
|
||||||
case AtLeast = 'at_least';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The goal is reached when less than the target amount of time was tracked in the period.
|
|
||||||
*/
|
|
||||||
case LessThan = 'less_than';
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalPeriod: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Day = 'day';
|
|
||||||
case Week = 'week';
|
|
||||||
case Month = 'month';
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalStatus: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "At least" goal, the target has not been reached yet in the current period.
|
|
||||||
*/
|
|
||||||
case InProgress = 'in_progress';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "At least" goal, the target has been reached in the current period.
|
|
||||||
*/
|
|
||||||
case Achieved = 'achieved';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Less than" goal, the tracked time is still below the limit.
|
|
||||||
*/
|
|
||||||
case OnTrack = 'on_track';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Less than" goal, the tracked time reached or exceeded the limit.
|
|
||||||
*/
|
|
||||||
case Exceeded = 'exceeded';
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalType: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A goal a member set for themselves. Only that member can see and manage it, no permission reaches it.
|
|
||||||
*/
|
|
||||||
case Personal = 'personal';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A goal the organization set for a member or for the whole organization.
|
|
||||||
* Managed by the members that may manage goals, visible to them and to the member the goal is for.
|
|
||||||
* Requires the team goals extension.
|
|
||||||
*/
|
|
||||||
case Organization = 'organization';
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
enum RegistrationMode: string
|
|
||||||
{
|
|
||||||
case On = 'on';
|
|
||||||
case InviteOnly = 'invite-only';
|
|
||||||
case Off = 'off';
|
|
||||||
|
|
||||||
public static function fromConfig(mixed $value): self
|
|
||||||
{
|
|
||||||
if ($value === true) {
|
|
||||||
return self::On;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value === false || $value === null) {
|
|
||||||
return self::Off;
|
|
||||||
}
|
|
||||||
|
|
||||||
return match (strtolower(trim((string) $value))) {
|
|
||||||
'1', 'on', 'true' => self::On,
|
|
||||||
'invite-only' => self::InviteOnly,
|
|
||||||
default => self::Off,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ enum TimeEntryAggregationType: string
|
|||||||
case Billable = 'billable';
|
case Billable = 'billable';
|
||||||
case Description = 'description';
|
case Description = 'description';
|
||||||
case Tag = 'tag';
|
case Tag = 'tag';
|
||||||
case Type = 'type';
|
|
||||||
|
|
||||||
public static function fromInterval(TimeEntryAggregationTypeInterval $timeEntryAggregationTypeInterval): TimeEntryAggregationType
|
public static function fromInterval(TimeEntryAggregationTypeInterval $timeEntryAggregationTypeInterval): TimeEntryAggregationType
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum TimeEntryType: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Work = 'work';
|
|
||||||
case Break = 'break';
|
|
||||||
}
|
|
||||||
@@ -60,6 +60,7 @@ abstract class ApiException extends Exception
|
|||||||
*/
|
*/
|
||||||
public function report(): bool
|
public function report(): bool
|
||||||
{
|
{
|
||||||
|
// TODO: temporary activated
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException
|
class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members';
|
public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class CanNotRemoveOwnerFromOrganization extends ApiException
|
class CanNotRemoveOwnerFromOrganization extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'can_not_remove_owner_from_organization';
|
public const string KEY = 'can_not_remove_owner_from_organization';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
|
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
|
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class ChangingRoleToPlaceholderIsNotAllowed extends ApiException
|
class ChangingRoleToPlaceholderIsNotAllowed extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'changing_role_to_placeholder_is_not_allowed';
|
public const string KEY = 'changing_role_to_placeholder_is_not_allowed';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Exceptions\Api;
|
namespace App\Exceptions\Api;
|
||||||
|
|
||||||
use Override;
|
|
||||||
|
|
||||||
class EntityStillInUseApiException extends ApiException
|
class EntityStillInUseApiException extends ApiException
|
||||||
{
|
{
|
||||||
private string $modelToDelete;
|
private string $modelToDelete;
|
||||||
@@ -21,20 +19,10 @@ class EntityStillInUseApiException extends ApiException
|
|||||||
|
|
||||||
public const string KEY = 'entity_still_in_use';
|
public const string KEY = 'entity_still_in_use';
|
||||||
|
|
||||||
/**
|
|
||||||
* Report the exception.
|
|
||||||
*
|
|
||||||
* @return bool true means the exception handler will not report it again
|
|
||||||
*/
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the translated message for the exception.
|
* Get the translated message for the exception.
|
||||||
*/
|
*/
|
||||||
#[Override]
|
#[\Override]
|
||||||
public function getTranslatedMessage(): string
|
public function getTranslatedMessage(): string
|
||||||
{
|
{
|
||||||
return __('exceptions.api.'.$this->getKey(), [
|
return __('exceptions.api.'.$this->getKey(), [
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
|
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'feature_is_not_available_in_free_plan';
|
public const string KEY = 'feature_is_not_available_in_free_plan';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class InactiveUserCanNotBeUsedApiException extends ApiException
|
class InactiveUserCanNotBeUsedApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'inactive_user_can_not_be_used';
|
public const string KEY = 'inactive_user_can_not_be_used';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
|
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'invitation_for_the_email_already_exists';
|
public const string KEY = 'invitation_for_the_email_already_exists';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OnlyOwnerCanChangeOwnership extends ApiException
|
class OnlyOwnerCanChangeOwnership extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'only_owner_can_change_ownership';
|
public const string KEY = 'only_owner_can_change_ownership';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
|
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
|
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OrganizationHasNoSubscriptionButMultipleMembersException extends ApiException
|
class OrganizationHasNoSubscriptionButMultipleMembersException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'organization_has_no_subscription_but_multiple_members';
|
public const string KEY = 'organization_has_no_subscription_but_multiple_members';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OrganizationNeedsAtLeastOneOwner extends ApiException
|
class OrganizationNeedsAtLeastOneOwner extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'organization_needs_at_least_one_owner';
|
public const string KEY = 'organization_needs_at_least_one_owner';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OverlappingTimeEntryApiException extends ApiException
|
class OverlappingTimeEntryApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'overlapping_time_entry';
|
public const string KEY = 'overlapping_time_entry';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
|
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
|
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class TimeEntryCanNotBeRestartedApiException extends ApiException
|
class TimeEntryCanNotBeRestartedApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'time_entry_can_not_be_restarted';
|
public const string KEY = 'time_entry_can_not_be_restarted';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class TimeEntryStillRunningApiException extends ApiException
|
class TimeEntryStillRunningApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'time_entry_still_running';
|
public const string KEY = 'time_entry_still_running';
|
||||||
|
|
||||||
/**
|
|
||||||
* Report the exception.
|
|
||||||
*
|
|
||||||
* @return bool true means the exception handler will not report it again
|
|
||||||
*/
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class UserIsAlreadyMemberOfOrganizationApiException extends ApiException
|
class UserIsAlreadyMemberOfOrganizationApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'user_is_already_member_of_organization';
|
public const string KEY = 'user_is_already_member_of_organization';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class UserIsAlreadyMemberOfProjectApiException extends ApiException
|
class UserIsAlreadyMemberOfProjectApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'user_is_already_member_of_project';
|
public const string KEY = 'user_is_already_member_of_project';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class UserNotPlaceholderApiException extends ApiException
|
class UserNotPlaceholderApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'user_not_placeholder';
|
public const string KEY = 'user_not_placeholder';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class UserResendEmailVerificationNoPendingEmailApiException extends ApiException
|
class UserResendEmailVerificationNoPendingEmailApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'user_resend_email_verification_no_pending_email';
|
public const string KEY = 'user_resend_email_verification_no_pending_email';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ namespace App\Exceptions;
|
|||||||
|
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
|
||||||
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
@@ -34,33 +30,6 @@ class Handler extends ExceptionHandler
|
|||||||
$this->reportable(function (Throwable $e): void {
|
$this->reportable(function (Throwable $e): void {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->dontReportWhen(fn (Throwable $e): bool => $e instanceof OAuthServerException
|
|
||||||
&& $e->getErrorType() === 'access_denied'
|
|
||||||
&& $e->getHttpStatusCode() === 401);
|
|
||||||
|
|
||||||
// A request on an untrusted host (see App\Http\Middleware\TrustHosts)
|
|
||||||
// otherwise renders as a bare "Bad request." 400. Show a message that
|
|
||||||
// says how to fix it instead. The framework has already converted the
|
|
||||||
// SuspiciousOperationException into a BadRequestHttpException by the time
|
|
||||||
// renderables run, so we match that and inspect the original.
|
|
||||||
$this->renderable(function (BadRequestHttpException $e, Request $request): ?Response {
|
|
||||||
$previous = $e->getPrevious();
|
|
||||||
|
|
||||||
if (! $previous instanceof SuspiciousOperationException
|
|
||||||
|| ! str_starts_with($previous->getMessage(), 'Untrusted Host')) {
|
|
||||||
return null; // any other bad request keeps the default response
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = 'This hostname is not configured for this instance. '
|
|
||||||
.'Set APP_URL, or add the host to TRUSTED_HOSTS.';
|
|
||||||
|
|
||||||
if ($request->expectsJson()) {
|
|
||||||
return response()->json(['message' => $message], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->view('errors.untrusted-host', ['message' => $message], 400);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render($request, Throwable $e): Response|RedirectResponse
|
public function render($request, Throwable $e): Response|RedirectResponse
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Extensions\Scramble;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
|
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
||||||
|
use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Response;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Schema;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\ArrayType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\BooleanType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\IntegerType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\StringType;
|
||||||
|
use Dedoc\Scramble\Support\Type\Generic;
|
||||||
|
use Dedoc\Scramble\Support\Type\ObjectType;
|
||||||
|
use Dedoc\Scramble\Support\Type\Type;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
|
||||||
|
{
|
||||||
|
public function shouldHandle(Type $type): bool
|
||||||
|
{
|
||||||
|
return $type instanceof ObjectType
|
||||||
|
&& $type->isInstanceOf(PaginatedResourceCollection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toSchema(Type $type): ?OpenApiObjectType
|
||||||
|
{
|
||||||
|
/** @var Type|null $collectingClassType */
|
||||||
|
$collectingClassType = $type->templateTypes[0] ?? null;
|
||||||
|
|
||||||
|
if (! $collectingClassType instanceof ObjectType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $collectingClassType->isInstanceOf(JsonResource::class) && ! $collectingClassType->isInstanceOf(Model::class)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$collectingType = $this->openApiTransformer->transform($collectingClassType);
|
||||||
|
|
||||||
|
$newType = new OpenApiObjectType;
|
||||||
|
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
||||||
|
if ($type instanceof ObjectType && $type->isInstanceOf(TimeEntryCollection::class)) {
|
||||||
|
$newType->addProperty(
|
||||||
|
'meta',
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
||||||
|
->setRequired(['total'])
|
||||||
|
);
|
||||||
|
$newType->setRequired(['data', 'meta']);
|
||||||
|
} else {
|
||||||
|
$newType->addProperty(
|
||||||
|
'links',
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('first', (new StringType)->nullable(true))
|
||||||
|
->addProperty('last', (new StringType)->nullable(true))
|
||||||
|
->addProperty('prev', (new StringType)->nullable(true))
|
||||||
|
->addProperty('next', (new StringType)->nullable(true))
|
||||||
|
->setRequired(['first', 'last', 'prev', 'next'])
|
||||||
|
);
|
||||||
|
$newType->addProperty(
|
||||||
|
'meta',
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('current_page', new IntegerType)
|
||||||
|
->addProperty('from', (new IntegerType)->nullable(true))
|
||||||
|
->addProperty('last_page', new IntegerType)
|
||||||
|
->addProperty('links', (new ArrayType)->setItems(
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('url', (new StringType)->nullable(true))
|
||||||
|
->addProperty('label', new StringType)
|
||||||
|
->addProperty('active', new BooleanType)
|
||||||
|
->setRequired(['url', 'label', 'active'])
|
||||||
|
)->setDescription('Generated paginator links.'))
|
||||||
|
->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.'))
|
||||||
|
->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.'))
|
||||||
|
->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.'))
|
||||||
|
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
||||||
|
->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total'])
|
||||||
|
);
|
||||||
|
$newType->setRequired(['data', 'links', 'meta']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Generic $type
|
||||||
|
*/
|
||||||
|
public function toResponse(Type $type): ?Response
|
||||||
|
{
|
||||||
|
/** @var ObjectType|null $collectingClassType */
|
||||||
|
$collectingClassType = $type->templateTypes[0] ?? null;
|
||||||
|
if (! $collectingClassType instanceof ObjectType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$type = $this->toSchema($type);
|
||||||
|
|
||||||
|
return Response::make(200)
|
||||||
|
->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`')
|
||||||
|
->setContent('application/json', Schema::fromType($type));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource\Pages;
|
||||||
|
use App\Models\Audit;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
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 $navigationIcon = 'heroicon-o-archive-box';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'System';
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('user_type')
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\TextInput::make('user_id'),
|
||||||
|
Forms\Components\TextInput::make('event')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\TextInput::make('auditable_type')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\TextInput::make('auditable_id')
|
||||||
|
->required(),
|
||||||
|
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')
|
||||||
|
->maxLength(1023),
|
||||||
|
Forms\Components\TextInput::make('tags')
|
||||||
|
->maxLength(255),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('user.name'),
|
||||||
|
Tables\Columns\TextColumn::make('event'),
|
||||||
|
Tables\Columns\TextColumn::make('auditable_type'),
|
||||||
|
Tables\Columns\TextColumn::make('auditable_id'),
|
||||||
|
IconColumn::make('was_command')
|
||||||
|
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
|
||||||
|
->boolean(),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\ViewAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListAudits::route('/'),
|
||||||
|
'create' => Pages\CreateAudit::route('/create'),
|
||||||
|
'view' => Pages\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;
|
||||||
|
}
|
||||||
@@ -4,18 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\ClientResource\Pages\CreateClient;
|
use App\Filament\Resources\ClientResource\Pages;
|
||||||
use App\Filament\Resources\ClientResource\Pages\EditClient;
|
|
||||||
use App\Filament\Resources\ClientResource\Pages\ListClients;
|
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
@@ -23,16 +18,16 @@ class ClientResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Client::class;
|
protected static ?string $model = Client::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-briefcase';
|
protected static ?string $navigationIcon = 'heroicon-o-briefcase';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
protected static ?string $navigationGroup = 'Timetracking';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 4;
|
protected static ?int $navigationSort = 4;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('name')
|
TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required(),
|
->required(),
|
||||||
@@ -48,17 +43,17 @@ class ClientResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('organization.name')
|
Tables\Columns\TextColumn::make('organization.name')
|
||||||
->sortable()
|
->sortable()
|
||||||
->label('Organization'),
|
->label('Organization'),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->label('Created at')
|
->label('Created at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->label('Updated at')
|
->label('Updated at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
@@ -73,12 +68,12 @@ class ClientResource extends Resource
|
|||||||
->relationship('organization', 'id')
|
->relationship('organization', 'id')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -93,9 +88,9 @@ class ClientResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListClients::route('/'),
|
'index' => Pages\ListClients::route('/'),
|
||||||
'create' => CreateClient::route('/create'),
|
'create' => Pages\CreateClient::route('/create'),
|
||||||
'edit' => EditClient::route('/{record}/edit'),
|
'edit' => Pages\EditClient::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ClientResource\Pages;
|
namespace App\Filament\Resources\ClientResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ClientResource;
|
use App\Filament\Resources\ClientResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditClient extends EditRecord
|
class EditClient extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditClient extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ClientResource\Pages;
|
namespace App\Filament\Resources\ClientResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ClientResource;
|
use App\Filament\Resources\ClientResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListClients extends ListRecords
|
class ListClients extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListClients extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ namespace App\Filament\Resources;
|
|||||||
use App\Filament\Resources\FailedJobResource\Pages\ListFailedJobs;
|
use App\Filament\Resources\FailedJobResource\Pages\ListFailedJobs;
|
||||||
use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs;
|
use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs;
|
||||||
use App\Models\FailedJob;
|
use App\Models\FailedJob;
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Actions\BulkAction;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\ViewAction;
|
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
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\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -30,19 +30,19 @@ class FailedJobResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = FailedJob::class;
|
protected static ?string $model = FailedJob::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-circle';
|
protected static ?string $navigationIcon = 'heroicon-o-exclamation-circle';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'System';
|
protected static ?string $navigationGroup = 'System';
|
||||||
|
|
||||||
public static function getNavigationBadge(): ?string
|
public static function getNavigationBadge(): ?string
|
||||||
{
|
{
|
||||||
return (string) FailedJob::query()->count();
|
return (string) FailedJob::query()->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('uuid')->disabled()->columnSpan(4),
|
TextInput::make('uuid')->disabled()->columnSpan(4),
|
||||||
TextInput::make('failed_at')->disabled(),
|
TextInput::make('failed_at')->disabled(),
|
||||||
TextInput::make('id')->disabled(),
|
TextInput::make('id')->disabled(),
|
||||||
@@ -74,7 +74,7 @@ class FailedJobResource extends Resource
|
|||||||
TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true),
|
TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true),
|
||||||
])
|
])
|
||||||
->filters([])
|
->filters([])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkAction::make('retry')
|
BulkAction::make('retry')
|
||||||
->icon('heroicon-o-arrow-path')
|
->icon('heroicon-o-arrow-path')
|
||||||
->label('Retry selected')
|
->label('Retry selected')
|
||||||
@@ -91,7 +91,7 @@ class FailedJobResource extends Resource
|
|||||||
}),
|
}),
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
DeleteAction::make(),
|
DeleteAction::make(),
|
||||||
ViewAction::make(),
|
ViewAction::make(),
|
||||||
Action::make('retry')
|
Action::make('retry')
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
|
||||||
|
|
||||||
use App\Enums\GoalComparison;
|
|
||||||
use App\Enums\GoalPeriod;
|
|
||||||
use App\Enums\GoalType;
|
|
||||||
use App\Enums\Weekday;
|
|
||||||
use App\Filament\Resources\GoalResource\Pages;
|
|
||||||
use App\Models\Goal;
|
|
||||||
use App\Service\Dto\GoalFiltersDto;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Actions\ViewAction;
|
|
||||||
use Filament\Forms;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
|
||||||
|
|
||||||
class GoalResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = Goal::class;
|
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-viewfinder-circle';
|
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
|
||||||
|
|
||||||
protected static ?int $navigationSort = 8;
|
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->columns(1)
|
|
||||||
->components([
|
|
||||||
Forms\Components\TextInput::make('name')
|
|
||||||
->label('Name')
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
Forms\Components\Select::make('type')
|
|
||||||
->label('Type')
|
|
||||||
->options(collect(GoalType::cases())->mapWithKeys(fn (GoalType $type): array => [$type->value => $type->name])->all())
|
|
||||||
->disabled()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('comparison')
|
|
||||||
->label('Comparison')
|
|
||||||
->options(collect(GoalComparison::cases())->mapWithKeys(fn (GoalComparison $comparison): array => [$comparison->value => $comparison->name])->all())
|
|
||||||
->required(),
|
|
||||||
Forms\Components\TextInput::make('target_seconds')
|
|
||||||
->label('Target (seconds)')
|
|
||||||
->numeric()
|
|
||||||
->minValue(1)
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('period')
|
|
||||||
->label('Period')
|
|
||||||
->options(collect(GoalPeriod::cases())->mapWithKeys(fn (GoalPeriod $period): array => [$period->value => $period->name])->all())
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('timezone')
|
|
||||||
->label('Timezone')
|
|
||||||
->options(fn (): array => collect(\DateTimeZone::listIdentifiers())->mapWithKeys(fn (string $timezone): array => [$timezone => $timezone])->all())
|
|
||||||
->searchable()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('week_start')
|
|
||||||
->label('Week start')
|
|
||||||
->options(collect(Weekday::cases())->mapWithKeys(fn (Weekday $weekday): array => [$weekday->value => $weekday->name])->all())
|
|
||||||
->required(),
|
|
||||||
DateTimePicker::make('archived_at')
|
|
||||||
->label('Archived At'),
|
|
||||||
Forms\Components\Select::make('organization_id')
|
|
||||||
->label('Organization')
|
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
|
||||||
->searchable(['name'])
|
|
||||||
->disabled()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('member_id')
|
|
||||||
->label('Member (empty = every member)')
|
|
||||||
->relationship(name: 'member', titleAttribute: 'id')
|
|
||||||
->disabled(),
|
|
||||||
PrettyJsonField::make('filters')
|
|
||||||
->formatStateUsing(function (GoalFiltersDto $state, Goal $record): string {
|
|
||||||
return $record->getRawOriginal('filters');
|
|
||||||
})
|
|
||||||
->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('comparison')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('target_seconds')
|
|
||||||
->label('Target')
|
|
||||||
->formatStateUsing(fn (int $state): string => sprintf('%dh %02dm', intdiv($state, 3600), intdiv($state % 3600, 60)))
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('period')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('type')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('member.user.email')
|
|
||||||
->label('Member')
|
|
||||||
->placeholder('Every member')
|
|
||||||
->searchable(),
|
|
||||||
TextColumn::make('archived_at')
|
|
||||||
->dateTime()
|
|
||||||
->placeholder('Not archived')
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
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('type')
|
|
||||||
->label('Type')
|
|
||||||
->options(collect(GoalType::cases())->mapWithKeys(fn (GoalType $type): array => [$type->value => $type->name])->all()),
|
|
||||||
SelectFilter::make('organization')
|
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
|
||||||
->searchable(),
|
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
|
||||||
->recordActions([
|
|
||||||
ViewAction::make(),
|
|
||||||
EditAction::make(),
|
|
||||||
DeleteAction::make(),
|
|
||||||
])
|
|
||||||
->toolbarActions([
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => Pages\ListGoals::route('/'),
|
|
||||||
'edit' => Pages\EditGoal::route('/{record}/edit'),
|
|
||||||
'view' => Pages\ViewGoal::route('/{record}'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\GoalResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\GoalResource;
|
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditGoal extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = GoalResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
Actions\DeleteAction::make()
|
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\GoalResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\GoalResource;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListGoals extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = GoalResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\GoalResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\GoalResource;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
|
||||||
|
|
||||||
class ViewGoal extends ViewRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = GoalResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
EditAction::make('edit')
|
|
||||||
->icon('heroicon-s-pencil'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,21 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\EditOrganizationInvitation;
|
use App\Filament\Resources\OrganizationInvitationResource\Pages;
|
||||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\ListOrganizationInvitations;
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\ViewOrganizationInvitation;
|
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
use App\Service\OrganizationInvitationService;
|
use App\Service\OrganizationInvitationService;
|
||||||
use Filament\Actions\BulkAction;
|
use Filament\Forms;
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
@@ -29,18 +22,18 @@ class OrganizationInvitationResource extends Resource
|
|||||||
|
|
||||||
protected static ?string $label = 'Invitations';
|
protected static ?string $label = 'Invitations';
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user-plus';
|
protected static ?string $navigationIcon = 'heroicon-o-user-plus';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Users';
|
protected static ?string $navigationGroup = 'Users';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 9;
|
protected static ?int $navigationSort = 9;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('email')
|
Forms\Components\TextInput::make('email')
|
||||||
->label('Email')
|
->label('Email')
|
||||||
->disabledOn(['edit'])
|
->disabledOn(['edit'])
|
||||||
->required(),
|
->required(),
|
||||||
@@ -52,11 +45,11 @@ class OrganizationInvitationResource extends Resource
|
|||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
->disabledOn(['edit'])
|
->disabledOn(['edit'])
|
||||||
->required(),
|
->required(),
|
||||||
DateTimePicker::make('created_at')
|
Forms\Components\DateTimePicker::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
DateTimePicker::make('updated_at')
|
Forms\Components\DateTimePicker::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
@@ -67,17 +60,17 @@ class OrganizationInvitationResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('organization.name')
|
Tables\Columns\TextColumn::make('organization.name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('email')
|
Tables\Columns\TextColumn::make('email')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('role'),
|
Tables\Columns\TextColumn::make('role'),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable()
|
->sortable()
|
||||||
@@ -87,13 +80,13 @@ class OrganizationInvitationResource extends Resource
|
|||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
DeleteAction::make(),
|
Tables\Actions\DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
BulkAction::make('resend')
|
Tables\Actions\BulkAction::make('resend')
|
||||||
->label('Resend')
|
->label('Resend')
|
||||||
->action(function (Collection $records): void {
|
->action(function (Collection $records): void {
|
||||||
foreach ($records as $organizationInvite) {
|
foreach ($records as $organizationInvite) {
|
||||||
@@ -113,9 +106,9 @@ class OrganizationInvitationResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListOrganizationInvitations::route('/'),
|
'index' => Pages\ListOrganizationInvitations::route('/'),
|
||||||
'edit' => EditOrganizationInvitation::route('/{record}/edit'),
|
'edit' => Pages\EditOrganizationInvitation::route('/{record}/edit'),
|
||||||
'view' => ViewOrganizationInvitation::route('/{record}'),
|
'view' => Pages\ViewOrganizationInvitation::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
|
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource;
|
use App\Filament\Resources\OrganizationInvitationResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditOrganizationInvitation extends EditRecord
|
class EditOrganizationInvitation extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditOrganizationInvitation extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ use App\Enums\DateFormat;
|
|||||||
use App\Enums\IntervalFormat;
|
use App\Enums\IntervalFormat;
|
||||||
use App\Enums\NumberFormat;
|
use App\Enums\NumberFormat;
|
||||||
use App\Enums\TimeFormat;
|
use App\Enums\TimeFormat;
|
||||||
use App\Filament\Resources\OrganizationResource\Pages\CreateOrganization;
|
use App\Filament\Resources\OrganizationResource\Pages;
|
||||||
use App\Filament\Resources\OrganizationResource\Pages\EditOrganization;
|
|
||||||
use App\Filament\Resources\OrganizationResource\Pages\ListOrganizations;
|
|
||||||
use App\Filament\Resources\OrganizationResource\Pages\ViewOrganization;
|
|
||||||
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
|
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
|
||||||
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
|
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
@@ -23,45 +20,38 @@ use App\Service\Import\Importers\ImportException;
|
|||||||
use App\Service\Import\Importers\ReportDto;
|
use App\Service\Import\Importers\ReportDto;
|
||||||
use App\Service\Import\ImportService;
|
use App\Service\Import\ImportService;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Exception;
|
use Filament\Forms;
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Forms\Components\FileUpload;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Form;
|
||||||
use Filament\Forms\Components\Toggle;
|
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\IconColumn;
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use League\Flysystem\UnableToReadFile;
|
|
||||||
|
|
||||||
class OrganizationResource extends Resource
|
class OrganizationResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = Organization::class;
|
protected static ?string $model = Organization::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2';
|
protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Users';
|
protected static ?string $navigationGroup = 'Users';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 7;
|
protected static ?int $navigationSort = 7;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Toggle::make('personal_team')
|
Forms\Components\Toggle::make('personal_team')
|
||||||
->label('Is personal?')
|
->label('Is personal?')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->required(),
|
->required(),
|
||||||
@@ -89,7 +79,7 @@ class OrganizationResource extends Resource
|
|||||||
Select::make('currency')
|
Select::make('currency')
|
||||||
->label('Currency')
|
->label('Currency')
|
||||||
->options(function (): array {
|
->options(function (): array {
|
||||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||||
$select = [];
|
$select = [];
|
||||||
foreach ($currencies as $currency) {
|
foreach ($currencies as $currency) {
|
||||||
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
||||||
@@ -99,7 +89,7 @@ class OrganizationResource extends Resource
|
|||||||
})
|
})
|
||||||
->required()
|
->required()
|
||||||
->searchable(),
|
->searchable(),
|
||||||
TextInput::make('billable_rate')
|
Forms\Components\TextInput::make('billable_rate')
|
||||||
->label('Billable rate (in Cents)')
|
->label('Billable rate (in Cents)')
|
||||||
->nullable()
|
->nullable()
|
||||||
->rules([
|
->rules([
|
||||||
@@ -109,11 +99,11 @@ class OrganizationResource extends Resource
|
|||||||
'max:2147483647',
|
'max:2147483647',
|
||||||
])
|
])
|
||||||
->numeric(),
|
->numeric(),
|
||||||
DateTimePicker::make('created_at')
|
Forms\Components\DateTimePicker::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
DateTimePicker::make('updated_at')
|
Forms\Components\DateTimePicker::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
@@ -127,7 +117,7 @@ class OrganizationResource extends Resource
|
|||||||
TextColumn::make('name')
|
TextColumn::make('name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
IconColumn::make('personal_team')
|
Tables\Columns\IconColumn::make('personal_team')
|
||||||
->boolean()
|
->boolean()
|
||||||
->label('Is personal?')
|
->label('Is personal?')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
@@ -148,9 +138,9 @@ class OrganizationResource extends Resource
|
|||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
DeleteAction::make()
|
Tables\Actions\DeleteAction::make()
|
||||||
->using(function (Organization $record): void {
|
->using(function (Organization $record): void {
|
||||||
app(DeletionService::class)->deleteOrganization($record);
|
app(DeletionService::class)->deleteOrganization($record);
|
||||||
}),
|
}),
|
||||||
@@ -168,7 +158,7 @@ class OrganizationResource extends Resource
|
|||||||
return response()->streamDownload(function () use ($file): void {
|
return response()->streamDownload(function () use ($file): void {
|
||||||
echo Storage::disk(config('filesystems.private'))->get($file);
|
echo Storage::disk(config('filesystems.private'))->get($file);
|
||||||
}, 'export.zip');
|
}, 'export.zip');
|
||||||
} catch (Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
report($exception);
|
report($exception);
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Export failed')
|
->title('Export failed')
|
||||||
@@ -182,13 +172,9 @@ class OrganizationResource extends Resource
|
|||||||
->icon('heroicon-o-inbox-arrow-down')
|
->icon('heroicon-o-inbox-arrow-down')
|
||||||
->action(function (Organization $record, array $data): void {
|
->action(function (Organization $record, array $data): void {
|
||||||
try {
|
try {
|
||||||
try {
|
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
||||||
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
|
||||||
} catch (UnableToReadFile) {
|
|
||||||
$file = null;
|
|
||||||
}
|
|
||||||
if ($file === null) {
|
if ($file === null) {
|
||||||
throw new ImportException('File not found');
|
throw new \Exception('File not found');
|
||||||
}
|
}
|
||||||
/** @var string $timezone */
|
/** @var string $timezone */
|
||||||
$timezone = $data['timezone'];
|
$timezone = $data['timezone'];
|
||||||
@@ -223,8 +209,8 @@ class OrganizationResource extends Resource
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
->tooltip(fn (Organization $record): string => 'Import into '.$record->name)
|
->tooltip(fn (Organization $record): string => 'Import into '.$record->name)
|
||||||
->schema([
|
->form([
|
||||||
FileUpload::make('file')
|
Forms\Components\FileUpload::make('file')
|
||||||
->label('File')
|
->label('File')
|
||||||
->required(),
|
->required(),
|
||||||
Select::make('type')
|
Select::make('type')
|
||||||
@@ -244,7 +230,7 @@ class OrganizationResource extends Resource
|
|||||||
->required(),
|
->required(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,10 +245,10 @@ class OrganizationResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListOrganizations::route('/'),
|
'index' => Pages\ListOrganizations::route('/'),
|
||||||
'create' => CreateOrganization::route('/create'),
|
'create' => Pages\CreateOrganization::route('/create'),
|
||||||
'edit' => EditOrganization::route('/{record}/edit'),
|
'edit' => Pages\EditOrganization::route('/{record}/edit'),
|
||||||
'view' => ViewOrganization::route('/{record}'),
|
'view' => Pages\ViewOrganization::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\OrganizationResource\Pages;
|
namespace App\Filament\Resources\OrganizationResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\OrganizationResource;
|
use App\Filament\Resources\OrganizationResource;
|
||||||
use App\Filament\Resources\OrganizationResource\Actions\DeleteOrganization;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditOrganization extends EditRecord
|
class EditOrganization extends EditRecord
|
||||||
@@ -15,7 +14,7 @@ class EditOrganization extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteOrganization::make(),
|
OrganizationResource\Actions\DeleteOrganization::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\OrganizationResource\Pages;
|
namespace App\Filament\Resources\OrganizationResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\OrganizationResource;
|
use App\Filament\Resources\OrganizationResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListOrganizations extends ListRecords
|
class ListOrganizations extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListOrganizations extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,12 @@ use App\Filament\Resources\OrganizationInvitationResource;
|
|||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
use App\Service\InvitationService;
|
use App\Service\InvitationService;
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\CreateAction;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\DetachBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@@ -30,10 +25,10 @@ class InvitationsRelationManager extends RelationManager
|
|||||||
|
|
||||||
protected static ?string $title = 'Invitations';
|
protected static ?string $title = 'Invitations';
|
||||||
|
|
||||||
public function form(Schema $schema): Schema
|
public function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('email')
|
TextInput::make('email')
|
||||||
->label('Email')
|
->label('Email')
|
||||||
->disabledOn(['edit'])
|
->disabledOn(['edit'])
|
||||||
@@ -58,33 +53,33 @@ class InvitationsRelationManager extends RelationManager
|
|||||||
->modelLabel('Invitation')
|
->modelLabel('Invitation')
|
||||||
->pluralModelLabel('Invitations')
|
->pluralModelLabel('Invitations')
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('email'),
|
Tables\Columns\TextColumn::make('email'),
|
||||||
TextColumn::make('role'),
|
Tables\Columns\TextColumn::make('role'),
|
||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
CreateAction::make()
|
Tables\Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus')
|
->icon('heroicon-s-plus')
|
||||||
->using(function (array $data, string $model): Model {
|
->using(function (array $data, string $model): Model {
|
||||||
/** @var Organization $ownerRecord */
|
/** @var Organization $ownerRecord */
|
||||||
$ownerRecord = $this->getOwnerRecord();
|
$ownerRecord = $this->getOwnerRecord();
|
||||||
|
|
||||||
return app(InvitationService::class)
|
return app(InvitationService::class)
|
||||||
->inviteUser($ownerRecord, $data['email'], ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), auth()->user());
|
->inviteUser($ownerRecord, $data['email'], Role::from($data['role']), auth()->user());
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [
|
->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [
|
||||||
'record' => $record->getKey(),
|
'record' => $record->getKey(),
|
||||||
])),
|
])),
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
DeleteAction::make(),
|
Tables\Actions\DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DetachBulkAction::make(),
|
Tables\Actions\DetachBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,14 @@ use App\Models\Organization;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\BillableRateService;
|
use App\Service\BillableRateService;
|
||||||
use App\Service\MemberService;
|
use App\Service\MemberService;
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Actions\AttachAction;
|
|
||||||
use Filament\Actions\DetachAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Actions\Action;
|
||||||
|
use Filament\Tables\Actions\AttachAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@@ -29,10 +28,10 @@ class UsersRelationManager extends RelationManager
|
|||||||
{
|
{
|
||||||
protected static string $relationship = 'users';
|
protected static string $relationship = 'users';
|
||||||
|
|
||||||
public function form(Schema $schema): Schema
|
public function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
Select::make('role')
|
Select::make('role')
|
||||||
->options(Role::class),
|
->options(Role::class),
|
||||||
TextInput::make('billable_rate')
|
TextInput::make('billable_rate')
|
||||||
@@ -76,17 +75,17 @@ class UsersRelationManager extends RelationManager
|
|||||||
->using(function (User $record, array $data): void {
|
->using(function (User $record, array $data): void {
|
||||||
/** @var Organization $organization */
|
/** @var Organization $organization */
|
||||||
$organization = $this->getOwnerRecord();
|
$organization = $this->getOwnerRecord();
|
||||||
app(MemberService::class)->addMember($record, $organization, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
|
app(MemberService::class)->addMember($record, $organization, Role::from($data['role']), true);
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->url(fn (User $record): string => UserResource::getUrl('view', [
|
->url(fn (User $record): string => UserResource::getUrl('view', [
|
||||||
'record' => $record->getKey(),
|
'record' => $record->getKey(),
|
||||||
])),
|
])),
|
||||||
EditAction::make()
|
Tables\Actions\EditAction::make()
|
||||||
->using(function (User $record, array $data): User {
|
->using(function (User $record, array $data): User {
|
||||||
/** @var Organization $organization */
|
/** @var Organization $organization */
|
||||||
$organization = $this->getOwnerRecord();
|
$organization = $this->getOwnerRecord();
|
||||||
@@ -98,10 +97,9 @@ class UsersRelationManager extends RelationManager
|
|||||||
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
|
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
|
||||||
}
|
}
|
||||||
|
|
||||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
if ($data['role'] !== $member->role) {
|
||||||
if ($newRole->value !== $member->role) {
|
|
||||||
try {
|
try {
|
||||||
app(MemberService::class)->changeRole($member, $organization, $newRole, true);
|
app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true);
|
||||||
} catch (ApiException $exception) {
|
} catch (ApiException $exception) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
@@ -115,7 +113,7 @@ class UsersRelationManager extends RelationManager
|
|||||||
|
|
||||||
return $record;
|
return $record;
|
||||||
}),
|
}),
|
||||||
DetachAction::make()
|
Tables\Actions\DetachAction::make()
|
||||||
->using(function (User $record): void {
|
->using(function (User $record): void {
|
||||||
/** @var Organization $organization */
|
/** @var Organization $organization */
|
||||||
$organization = $this->getOwnerRecord();
|
$organization = $this->getOwnerRecord();
|
||||||
@@ -135,7 +133,7 @@ class UsersRelationManager extends RelationManager
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\ProjectMemberResource\Pages\CreateProjectMember;
|
use App\Filament\Resources\ProjectMemberResource\Pages;
|
||||||
use App\Filament\Resources\ProjectMemberResource\Pages\EditProjectMember;
|
|
||||||
use App\Filament\Resources\ProjectMemberResource\Pages\ListProjectMembers;
|
|
||||||
use App\Filament\Resources\ProjectMemberResource\Pages\ViewProjectMembers;
|
|
||||||
use App\Models\ProjectMember;
|
use App\Models\ProjectMember;
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Forms;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
use Filament\Forms\Form;
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
class ProjectMemberResource extends Resource
|
class ProjectMemberResource extends Resource
|
||||||
@@ -25,11 +18,11 @@ class ProjectMemberResource extends Resource
|
|||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('billable_rate')
|
Forms\Components\TextInput::make('billable_rate')
|
||||||
->label('Billable rate (in Cents)')
|
->label('Billable rate (in Cents)')
|
||||||
->nullable()
|
->nullable()
|
||||||
->rules([
|
->rules([
|
||||||
@@ -39,10 +32,10 @@ class ProjectMemberResource extends Resource
|
|||||||
'max:2147483647',
|
'max:2147483647',
|
||||||
])
|
])
|
||||||
->numeric(),
|
->numeric(),
|
||||||
Select::make('user_id')
|
Forms\Components\Select::make('user_id')
|
||||||
->relationship('user', 'name')
|
->relationship('user', 'name')
|
||||||
->required(),
|
->required(),
|
||||||
Select::make('member_id')
|
Forms\Components\Select::make('member_id')
|
||||||
->relationship('member', 'id')
|
->relationship('member', 'id')
|
||||||
->required(),
|
->required(),
|
||||||
]);
|
]);
|
||||||
@@ -52,17 +45,17 @@ class ProjectMemberResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('id')
|
Tables\Columns\TextColumn::make('id')
|
||||||
->label('ID'),
|
->label('ID'),
|
||||||
TextColumn::make('billable_rate')
|
Tables\Columns\TextColumn::make('billable_rate')
|
||||||
->numeric()
|
->numeric()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('project.name'),
|
Tables\Columns\TextColumn::make('project.name'),
|
||||||
TextColumn::make('user.name'),
|
Tables\Columns\TextColumn::make('user.name'),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable()
|
->sortable()
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
@@ -70,12 +63,12 @@ class ProjectMemberResource extends Resource
|
|||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -90,10 +83,10 @@ class ProjectMemberResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListProjectMembers::route('/'),
|
'index' => Pages\ListProjectMembers::route('/'),
|
||||||
'create' => CreateProjectMember::route('/create'),
|
'create' => Pages\CreateProjectMember::route('/create'),
|
||||||
'edit' => EditProjectMember::route('/{record}/edit'),
|
'edit' => Pages\EditProjectMember::route('/{record}/edit'),
|
||||||
'view' => ViewProjectMembers::route('/{record}'),
|
'view' => Pages\ViewProjectMembers::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ProjectMemberResource\Pages;
|
namespace App\Filament\Resources\ProjectMemberResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ProjectMemberResource;
|
use App\Filament\Resources\ProjectMemberResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditProjectMember extends EditRecord
|
class EditProjectMember extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditProjectMember extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ProjectMemberResource\Pages;
|
namespace App\Filament\Resources\ProjectMemberResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ProjectMemberResource;
|
use App\Filament\Resources\ProjectMemberResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListProjectMembers extends ListRecords
|
class ListProjectMembers extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListProjectMembers extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\ProjectResource\Pages\CreateProject;
|
use App\Filament\Resources\ProjectResource\Pages;
|
||||||
use App\Filament\Resources\ProjectResource\Pages\EditProject;
|
|
||||||
use App\Filament\Resources\ProjectResource\Pages\ListProjects;
|
|
||||||
use App\Filament\Resources\ProjectResource\RelationManagers\ProjectMembersRelationManager;
|
use App\Filament\Resources\ProjectResource\RelationManagers\ProjectMembersRelationManager;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Forms;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\ColorPicker;
|
use Filament\Forms\Components\ColorPicker;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Form;
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\ColorColumn;
|
use Filament\Tables\Columns\ColorColumn;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
@@ -26,24 +21,24 @@ class ProjectResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Project::class;
|
protected static ?string $model = Project::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-folder';
|
protected static ?string $navigationIcon = 'heroicon-o-folder';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
protected static ?string $navigationGroup = 'Timetracking';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 2;
|
protected static ?int $navigationSort = 2;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
ColorPicker::make('color')
|
ColorPicker::make('color')
|
||||||
->label('Color')
|
->label('Color')
|
||||||
->required(),
|
->required(),
|
||||||
TextInput::make('billable_rate')
|
Forms\Components\TextInput::make('billable_rate')
|
||||||
->label('Billable rate (in Cents)')
|
->label('Billable rate (in Cents)')
|
||||||
->nullable()
|
->nullable()
|
||||||
->rules([
|
->rules([
|
||||||
@@ -53,7 +48,7 @@ class ProjectResource extends Resource
|
|||||||
'max:2147483647',
|
'max:2147483647',
|
||||||
])
|
])
|
||||||
->numeric(),
|
->numeric(),
|
||||||
Select::make('organization_id')
|
Forms\Components\Select::make('organization_id')
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
->relationship(name: 'organization', titleAttribute: 'name')
|
||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
->required(),
|
->required(),
|
||||||
@@ -86,12 +81,12 @@ class ProjectResource extends Resource
|
|||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -106,9 +101,9 @@ class ProjectResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListProjects::route('/'),
|
'index' => Pages\ListProjects::route('/'),
|
||||||
'create' => CreateProject::route('/create'),
|
'create' => Pages\CreateProject::route('/create'),
|
||||||
'edit' => EditProject::route('/{record}/edit'),
|
'edit' => Pages\EditProject::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ProjectResource\Pages;
|
namespace App\Filament\Resources\ProjectResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ProjectResource;
|
use App\Filament\Resources\ProjectResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditProject extends EditRecord
|
class EditProject extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditProject extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ProjectResource\Pages;
|
namespace App\Filament\Resources\ProjectResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ProjectResource;
|
use App\Filament\Resources\ProjectResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListProjects extends ListRecords
|
class ListProjects extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListProjects extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ namespace App\Filament\Resources\ProjectResource\RelationManagers;
|
|||||||
|
|
||||||
use App\Filament\Resources\ProjectMemberResource;
|
use App\Filament\Resources\ProjectMemberResource;
|
||||||
use App\Models\ProjectMember;
|
use App\Models\ProjectMember;
|
||||||
use Filament\Actions\Action;
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
class ProjectMembersRelationManager extends RelationManager
|
class ProjectMembersRelationManager extends RelationManager
|
||||||
@@ -18,10 +18,10 @@ class ProjectMembersRelationManager extends RelationManager
|
|||||||
|
|
||||||
protected static string $relationship = 'members';
|
protected static string $relationship = 'members';
|
||||||
|
|
||||||
public function form(Schema $schema): Schema
|
public function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ class ProjectMembersRelationManager extends RelationManager
|
|||||||
return $table
|
return $table
|
||||||
->recordTitleAttribute('name')
|
->recordTitleAttribute('name')
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('user.name'),
|
Tables\Columns\TextColumn::make('user.name'),
|
||||||
TextColumn::make('billable_rate')
|
Tables\Columns\TextColumn::make('billable_rate')
|
||||||
->numeric()
|
->numeric()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
@@ -40,7 +40,7 @@ class ProjectMembersRelationManager extends RelationManager
|
|||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
@@ -54,7 +54,7 @@ class ProjectMembersRelationManager extends RelationManager
|
|||||||
]))
|
]))
|
||||||
->openUrlInNewTab(),
|
->openUrlInNewTab(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\ReportResource\Pages\EditReport;
|
use App\Filament\Resources\ReportResource\Pages;
|
||||||
use App\Filament\Resources\ReportResource\Pages\ListReports;
|
|
||||||
use App\Filament\Resources\ReportResource\Pages\ViewReport;
|
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
use App\Service\Dto\ReportPropertiesDto;
|
use App\Service\Dto\ReportPropertiesDto;
|
||||||
use Filament\Actions\Action;
|
use Filament\Forms;
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Actions\ViewAction;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Columns\ToggleColumn;
|
use Filament\Tables\Columns\ToggleColumn;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
@@ -29,22 +24,22 @@ class ReportResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Report::class;
|
protected static ?string $model = Report::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-chart-bar';
|
protected static ?string $navigationIcon = 'heroicon-o-document-chart-bar';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
protected static ?string $navigationGroup = 'Timetracking';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 7;
|
protected static ?int $navigationSort = 7;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('description')
|
Forms\Components\TextInput::make('description')
|
||||||
->label('Description')
|
->label('Description')
|
||||||
->nullable()
|
->nullable()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
@@ -54,13 +49,13 @@ class ReportResource extends Resource
|
|||||||
DateTimePicker::make('public_until')
|
DateTimePicker::make('public_until')
|
||||||
->label('Public until')
|
->label('Public until')
|
||||||
->nullable(),
|
->nullable(),
|
||||||
Select::make('organization_id')
|
Forms\Components\Select::make('organization_id')
|
||||||
->label('Organization')
|
->label('Organization')
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
->relationship(name: 'organization', titleAttribute: 'name')
|
||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
->disabled()
|
->disabled()
|
||||||
->required(),
|
->required(),
|
||||||
TextInput::make('share_secret')
|
Forms\Components\TextInput::make('share_secret')
|
||||||
->label('Share Secret')
|
->label('Share Secret')
|
||||||
->nullable(),
|
->nullable(),
|
||||||
PrettyJsonField::make('properties')
|
PrettyJsonField::make('properties')
|
||||||
@@ -114,18 +109,18 @@ class ReportResource extends Resource
|
|||||||
->relationship('organization', 'id')
|
->relationship('organization', 'id')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('public-view')
|
Action::make('public-view')
|
||||||
->label('Public')
|
->label('Public')
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->hidden(fn (Report $record): bool => $record->getShareableLink() === null)
|
->hidden(fn (Report $record): bool => $record->getShareableLink() === null)
|
||||||
->url(fn (Report $record): string => $record->getShareableLink(), true),
|
->url(fn (Report $record): string => $record->getShareableLink(), true),
|
||||||
ViewAction::make(),
|
Tables\Actions\ViewAction::make(),
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
DeleteAction::make(),
|
Tables\Actions\DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +133,9 @@ class ReportResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListReports::route('/'),
|
'index' => Pages\ListReports::route('/'),
|
||||||
'edit' => EditReport::route('/{record}/edit'),
|
'edit' => Pages\EditReport::route('/{record}/edit'),
|
||||||
'view' => ViewReport::route('/{record}'),
|
'view' => Pages\ViewReport::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ReportResource\Pages;
|
namespace App\Filament\Resources\ReportResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ReportResource;
|
use App\Filament\Resources\ReportResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditReport extends EditRecord
|
class EditReport extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditReport extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\TagResource\Pages\CreateTag;
|
use App\Filament\Resources\TagResource\Pages;
|
||||||
use App\Filament\Resources\TagResource\Pages\EditTag;
|
|
||||||
use App\Filament\Resources\TagResource\Pages\ListTags;
|
|
||||||
use App\Models\Tag;
|
use App\Models\Tag;
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Forms;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
@@ -23,20 +18,20 @@ class TagResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Tag::class;
|
protected static ?string $model = Tag::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-tag';
|
protected static ?string $navigationIcon = 'heroicon-o-tag';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
protected static ?string $navigationGroup = 'Timetracking';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 5;
|
protected static ?int $navigationSort = 5;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('name')
|
TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required(),
|
->required(),
|
||||||
Select::make('organization_id')
|
Forms\Components\Select::make('organization_id')
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
->relationship(name: 'organization', titleAttribute: 'name')
|
||||||
->label('Organization')
|
->label('Organization')
|
||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
@@ -48,17 +43,17 @@ class TagResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('organization.name')
|
Tables\Columns\TextColumn::make('organization.name')
|
||||||
->sortable()
|
->sortable()
|
||||||
->label('Organization'),
|
->label('Organization'),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->label('Created at')
|
->label('Created at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->label('Updated at')
|
->label('Updated at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
@@ -73,12 +68,12 @@ class TagResource extends Resource
|
|||||||
->relationship('organization', 'id')
|
->relationship('organization', 'id')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -93,9 +88,9 @@ class TagResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListTags::route('/'),
|
'index' => Pages\ListTags::route('/'),
|
||||||
'create' => CreateTag::route('/create'),
|
'create' => Pages\CreateTag::route('/create'),
|
||||||
'edit' => EditTag::route('/{record}/edit'),
|
'edit' => Pages\EditTag::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\TagResource\Pages;
|
namespace App\Filament\Resources\TagResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\TagResource;
|
use App\Filament\Resources\TagResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditTag extends EditRecord
|
class EditTag extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditTag extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\TagResource\Pages;
|
namespace App\Filament\Resources\TagResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\TagResource;
|
use App\Filament\Resources\TagResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListTags extends ListRecords
|
class ListTags extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListTags extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\TaskResource\Pages\CreateTask;
|
use App\Filament\Resources\TaskResource\Pages;
|
||||||
use App\Filament\Resources\TaskResource\Pages\EditTask;
|
|
||||||
use App\Filament\Resources\TaskResource\Pages\ListTasks;
|
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Forms;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
@@ -23,17 +18,17 @@ class TaskResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Task::class;
|
protected static ?string $model = Task::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-list-bullet';
|
protected static ?string $navigationIcon = 'heroicon-o-list-bullet';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
protected static ?string $navigationGroup = 'Timetracking';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 3;
|
protected static ?int $navigationSort = 3;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
@@ -52,16 +47,16 @@ class TaskResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('project.name')
|
Tables\Columns\TextColumn::make('project.name')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('organization.name')
|
Tables\Columns\TextColumn::make('organization.name')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
@@ -75,12 +70,12 @@ class TaskResource extends Resource
|
|||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -95,9 +90,9 @@ class TaskResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListTasks::route('/'),
|
'index' => Pages\ListTasks::route('/'),
|
||||||
'create' => CreateTask::route('/create'),
|
'create' => Pages\CreateTask::route('/create'),
|
||||||
'edit' => EditTask::route('/{record}/edit'),
|
'edit' => Pages\EditTask::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\TaskResource\Pages;
|
namespace App\Filament\Resources\TaskResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\TaskResource;
|
use App\Filament\Resources\TaskResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditTask extends EditRecord
|
class EditTask extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditTask extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\TaskResource\Pages;
|
namespace App\Filament\Resources\TaskResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\TaskResource;
|
use App\Filament\Resources\TaskResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListTasks extends ListRecords
|
class ListTasks extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListTasks extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\TimeEntryResource\Pages\CreateTimeEntry;
|
use App\Filament\Resources\TimeEntryResource\Pages;
|
||||||
use App\Filament\Resources\TimeEntryResource\Pages\EditTimeEntry;
|
|
||||||
use App\Filament\Resources\TimeEntryResource\Pages\ListTimeEntries;
|
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@@ -27,16 +23,16 @@ class TimeEntryResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = TimeEntry::class;
|
protected static ?string $model = TimeEntry::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clock';
|
protected static ?string $navigationIcon = 'heroicon-o-clock';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
protected static ?string $navigationGroup = 'Timetracking';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 1;
|
protected static ?int $navigationSort = 1;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('id')
|
TextInput::make('id')
|
||||||
->label('ID')
|
->label('ID')
|
||||||
->readOnly()
|
->readOnly()
|
||||||
@@ -115,12 +111,12 @@ class TimeEntryResource extends Resource
|
|||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -135,9 +131,9 @@ class TimeEntryResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListTimeEntries::route('/'),
|
'index' => Pages\ListTimeEntries::route('/'),
|
||||||
'create' => CreateTimeEntry::route('/create'),
|
'create' => Pages\CreateTimeEntry::route('/create'),
|
||||||
'edit' => EditTimeEntry::route('/{record}/edit'),
|
'edit' => Pages\EditTimeEntry::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace App\Filament\Resources\TimeEntryResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Resources\TimeEntryResource;
|
use App\Filament\Resources\TimeEntryResource;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditTimeEntry extends EditRecord
|
class EditTimeEntry extends EditRecord
|
||||||
@@ -16,7 +16,7 @@ class EditTimeEntry extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\TimeEntryResource\Pages;
|
namespace App\Filament\Resources\TimeEntryResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\TimeEntryResource;
|
use App\Filament\Resources\TimeEntryResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListTimeEntries extends ListRecords
|
class ListTimeEntries extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListTimeEntries extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\TokenResource\Pages\ListTokens;
|
use App\Filament\Resources\TokenResource\Pages;
|
||||||
use App\Filament\Resources\TokenResource\Pages\ViewToken;
|
|
||||||
use App\Models\Passport\Token;
|
use App\Models\Passport\Token;
|
||||||
use Filament\Actions\ViewAction;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Form;
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Forms\Components\Toggle;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\IconColumn;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\TernaryFilter;
|
use Filament\Tables\Filters\TernaryFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
@@ -24,48 +18,48 @@ class TokenResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Token::class;
|
protected static ?string $model = Token::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-key';
|
protected static ?string $navigationIcon = 'heroicon-o-key';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Auth';
|
protected static ?string $navigationGroup = 'Auth';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 6;
|
protected static ?int $navigationSort = 6;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('id')
|
Forms\Components\TextInput::make('id')
|
||||||
->label('ID')
|
->label('ID')
|
||||||
->disabled()
|
->disabled()
|
||||||
->visibleOn(['update', 'show'])
|
->visibleOn(['update', 'show'])
|
||||||
->readOnly()
|
->readOnly()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Select::make('owner_id')
|
Forms\Components\Select::make('owner_id')
|
||||||
->label('User')
|
->label('User')
|
||||||
->relationship(name: 'user', titleAttribute: 'name')
|
->relationship(name: 'user', titleAttribute: 'name')
|
||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
->disabled()
|
->disabled()
|
||||||
->required(),
|
->required(),
|
||||||
Select::make('client_id')
|
Forms\Components\Select::make('client_id')
|
||||||
->label('Client')
|
->label('Client')
|
||||||
->relationship(name: 'client', titleAttribute: 'name')
|
->relationship(name: 'client', titleAttribute: 'name')
|
||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
->required(),
|
->required(),
|
||||||
Toggle::make('revoked')
|
Forms\Components\Toggle::make('revoked')
|
||||||
->label('Revoked')
|
->label('Revoked')
|
||||||
->required(),
|
->required(),
|
||||||
DateTimePicker::make('expires_at')
|
Forms\Components\DateTimePicker::make('expires_at')
|
||||||
->label('Expires At')
|
->label('Expires At')
|
||||||
->disabled(),
|
->disabled(),
|
||||||
DateTimePicker::make('created_at')
|
Forms\Components\DateTimePicker::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->disabled(),
|
->disabled(),
|
||||||
DateTimePicker::make('updated_at')
|
Forms\Components\DateTimePicker::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->disabled(),
|
->disabled(),
|
||||||
]);
|
]);
|
||||||
@@ -75,32 +69,32 @@ class TokenResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('user.name')
|
Tables\Columns\TextColumn::make('user.name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('client.name')
|
Tables\Columns\TextColumn::make('client.name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
IconColumn::make('personal_access_client')
|
Tables\Columns\IconColumn::make('personal_access_client')
|
||||||
->state(function (Token $token): bool {
|
->state(function (Token $token): bool {
|
||||||
return in_array('personal_access', $token->client->grant_types ?? [], true);
|
return in_array('personal_access', $token->client->grant_types ?? [], true);
|
||||||
})
|
})
|
||||||
->boolean()
|
->boolean()
|
||||||
->label('API token?'),
|
->label('API token?'),
|
||||||
IconColumn::make('revoked')
|
Tables\Columns\IconColumn::make('revoked')
|
||||||
->boolean()
|
->boolean()
|
||||||
->label('Revoked?')
|
->label('Revoked?')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('expires_at')
|
Tables\Columns\TextColumn::make('expires_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable()
|
->sortable()
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
@@ -126,10 +120,10 @@ class TokenResource extends Resource
|
|||||||
TernaryFilter::make('revoked')
|
TernaryFilter::make('revoked')
|
||||||
->label('Revoked?'),
|
->label('Revoked?'),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
ViewAction::make(),
|
Tables\Actions\ViewAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,8 +136,8 @@ class TokenResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListTokens::route('/'),
|
'index' => Pages\ListTokens::route('/'),
|
||||||
'view' => ViewToken::route('/{record}'),
|
'view' => Pages\ViewToken::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,30 +6,21 @@ namespace App\Filament\Resources;
|
|||||||
|
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Exceptions\Api\ApiException;
|
use App\Exceptions\Api\ApiException;
|
||||||
use App\Filament\Resources\UserResource\Pages\CreateUser;
|
use App\Filament\Resources\UserResource\Pages;
|
||||||
use App\Filament\Resources\UserResource\Pages\EditUser;
|
|
||||||
use App\Filament\Resources\UserResource\Pages\ListUsers;
|
|
||||||
use App\Filament\Resources\UserResource\Pages\ViewUser;
|
|
||||||
use App\Filament\Resources\UserResource\RelationManagers\OrganizationsRelationManager;
|
use App\Filament\Resources\UserResource\RelationManagers\OrganizationsRelationManager;
|
||||||
use App\Filament\Resources\UserResource\RelationManagers\OwnedOrganizationsRelationManager;
|
use App\Filament\Resources\UserResource\RelationManagers\OwnedOrganizationsRelationManager;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\DeletionService;
|
use App\Service\DeletionService;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\BulkAction;
|
use Filament\Forms;
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\IconColumn;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\TernaryFilter;
|
use Filament\Tables\Filters\TernaryFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
@@ -37,26 +28,26 @@ use Illuminate\Support\Collection;
|
|||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
|
||||||
|
|
||||||
class UserResource extends Resource
|
class UserResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = User::class;
|
protected static ?string $model = User::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user';
|
protected static ?string $navigationIcon = 'heroicon-o-user';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Users';
|
protected static ?string $navigationGroup = 'Users';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 6;
|
protected static ?int $navigationSort = 6;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
/** @var User|null $record */
|
/** @var User|null $record */
|
||||||
$record = $schema->getRecord();
|
$record = $form->getRecord();
|
||||||
|
|
||||||
return $schema
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('id')
|
TextInput::make('id')
|
||||||
->label('ID')
|
->label('ID')
|
||||||
->disabled()
|
->disabled()
|
||||||
@@ -78,23 +69,23 @@ class UserResource extends Resource
|
|||||||
'email:rfc,strict',
|
'email:rfc,strict',
|
||||||
])
|
])
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Toggle::make('is_placeholder')
|
Forms\Components\Toggle::make('is_placeholder')
|
||||||
->label('Is Placeholder?')
|
->label('Is Placeholder?')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabledOn(['edit']),
|
->disabledOn(['edit']),
|
||||||
DateTimePicker::make('email_verified_at')
|
Forms\Components\DateTimePicker::make('email_verified_at')
|
||||||
->label('Email Verified At')
|
->label('Email Verified At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->nullable(),
|
->nullable(),
|
||||||
Toggle::make('is_email_verified')
|
Forms\Components\Toggle::make('is_email_verified')
|
||||||
->label('Email Verified?')
|
->label('Email Verified?')
|
||||||
->visibleOn(['create']),
|
->visibleOn(['create']),
|
||||||
Select::make('timezone')
|
Forms\Components\Select::make('timezone')
|
||||||
->label('Timezone')
|
->label('Timezone')
|
||||||
->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
|
->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
|
||||||
->searchable()
|
->searchable()
|
||||||
->required(),
|
->required(),
|
||||||
Select::make('week_start')
|
Forms\Components\Select::make('week_start')
|
||||||
->label('Week Start')
|
->label('Week Start')
|
||||||
->options(Weekday::class)
|
->options(Weekday::class)
|
||||||
->required(),
|
->required(),
|
||||||
@@ -112,10 +103,10 @@ class UserResource extends Resource
|
|||||||
->visibleOn(['create'])
|
->visibleOn(['create'])
|
||||||
->required(fn (string $context): bool => $context === 'create')
|
->required(fn (string $context): bool => $context === 'create')
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Select::make('currency')
|
Forms\Components\Select::make('currency')
|
||||||
->label('Currency (Personal Organization)')
|
->label('Currency (Personal Organization)')
|
||||||
->options(function (): array {
|
->options(function (): array {
|
||||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||||
$select = [];
|
$select = [];
|
||||||
foreach ($currencies as $currency) {
|
foreach ($currencies as $currency) {
|
||||||
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
||||||
@@ -126,11 +117,11 @@ class UserResource extends Resource
|
|||||||
->required()
|
->required()
|
||||||
->visibleOn(['create'])
|
->visibleOn(['create'])
|
||||||
->searchable(),
|
->searchable(),
|
||||||
DateTimePicker::make('created_at')
|
Forms\Components\DateTimePicker::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
DateTimePicker::make('updated_at')
|
Forms\Components\DateTimePicker::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
@@ -141,25 +132,25 @@ class UserResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('email')
|
Tables\Columns\TextColumn::make('email')
|
||||||
->icon('heroicon-m-envelope')
|
->icon('heroicon-m-envelope')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
IconColumn::make('is_real_user')
|
Tables\Columns\IconColumn::make('is_real_user')
|
||||||
->getStateUsing(fn (User $record): bool => ! $record->is_placeholder)
|
->getStateUsing(fn (User $record): bool => ! $record->is_placeholder)
|
||||||
->label('Real user?')
|
->label('Real user?')
|
||||||
->boolean(),
|
->boolean(),
|
||||||
IconColumn::make('email_verified')
|
Tables\Columns\IconColumn::make('email_verified')
|
||||||
->getStateUsing(fn (User $record): bool => $record->email_verified_at !== null)
|
->getStateUsing(fn (User $record): bool => $record->email_verified_at !== null)
|
||||||
->label('Email verified?')
|
->label('Email verified?')
|
||||||
->boolean(),
|
->boolean(),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable()
|
->sortable()
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
@@ -187,7 +178,7 @@ class UserResource extends Resource
|
|||||||
->attribute('email_verified_at')
|
->attribute('email_verified_at')
|
||||||
->nullable(),
|
->nullable(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Impersonate::make()->before(function (User $record): void {
|
Impersonate::make()->before(function (User $record): void {
|
||||||
if ($record->currentOrganization === null) {
|
if ($record->currentOrganization === null) {
|
||||||
$organization = $record->organizations()->where('personal_team', '=', true)->first();
|
$organization = $record->organizations()->where('personal_team', '=', true)->first();
|
||||||
@@ -200,8 +191,8 @@ class UserResource extends Resource
|
|||||||
app(UserService::class)->switchCurrentOrganization($record, $organization);
|
app(UserService::class)->switchCurrentOrganization($record, $organization);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
DeleteAction::make()
|
Tables\Actions\DeleteAction::make()
|
||||||
->hidden(fn (User $record) => $record->is(Auth::user()))
|
->hidden(fn (User $record) => $record->is(Auth::user()))
|
||||||
->using(function (User $record): void {
|
->using(function (User $record): void {
|
||||||
try {
|
try {
|
||||||
@@ -216,8 +207,8 @@ class UserResource extends Resource
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkAction::make('Resend verification email')
|
Tables\Actions\BulkAction::make('Resend verification email')
|
||||||
->icon('heroicon-o-paper-airplane')
|
->icon('heroicon-o-paper-airplane')
|
||||||
->action(function (Collection $records): void {
|
->action(function (Collection $records): void {
|
||||||
foreach ($records as $user) {
|
foreach ($records as $user) {
|
||||||
@@ -239,10 +230,10 @@ class UserResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListUsers::route('/'),
|
'index' => Pages\ListUsers::route('/'),
|
||||||
'create' => CreateUser::route('/create'),
|
'create' => Pages\CreateUser::route('/create'),
|
||||||
'edit' => EditUser::route('/{record}/edit'),
|
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||||
'view' => ViewUser::route('/{record}'),
|
'view' => Pages\ViewUser::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class CreateUser extends CreateRecord
|
|||||||
$data['email'],
|
$data['email'],
|
||||||
$data['password_create'],
|
$data['password_create'],
|
||||||
$data['timezone'],
|
$data['timezone'],
|
||||||
$data['week_start'] instanceof Weekday ? $data['week_start'] : Weekday::from($data['week_start']),
|
Weekday::from($data['week_start']),
|
||||||
$data['currency'],
|
$data['currency'],
|
||||||
verifyEmail: (bool) $data['is_email_verified']
|
verifyEmail: (bool) $data['is_email_verified']
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
use App\Filament\Resources\UserResource\Actions\DeleteUser;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
use STS\FilamentImpersonate\Pages\Actions\Impersonate;
|
||||||
|
|
||||||
class EditUser extends EditRecord
|
class EditUser extends EditRecord
|
||||||
{
|
{
|
||||||
@@ -17,7 +16,7 @@ class EditUser extends EditRecord
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Impersonate::make()->record($this->getRecord()),
|
Impersonate::make()->record($this->getRecord()),
|
||||||
DeleteUser::make(),
|
UserResource\Actions\DeleteUser::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListUsers extends ListRecords
|
class ListUsers extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListUsers extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace App\Filament\Resources\UserResource\Pages;
|
|||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
use STS\FilamentImpersonate\Pages\Actions\Impersonate;
|
||||||
|
|
||||||
class ViewUser extends ViewRecord
|
class ViewUser extends ViewRecord
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ use App\Models\Member;
|
|||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\MemberService;
|
use App\Service\MemberService;
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Actions\DetachAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
@@ -25,10 +24,10 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
{
|
{
|
||||||
protected static string $relationship = 'organizations';
|
protected static string $relationship = 'organizations';
|
||||||
|
|
||||||
public function form(Schema $schema): Schema
|
public function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
Select::make('role')
|
Select::make('role')
|
||||||
->options(Role::class),
|
->options(Role::class),
|
||||||
]);
|
]);
|
||||||
@@ -47,22 +46,21 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [
|
->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [
|
||||||
'record' => $record->getKey(),
|
'record' => $record->getKey(),
|
||||||
])),
|
])),
|
||||||
EditAction::make()
|
Tables\Actions\EditAction::make()
|
||||||
->using(function (Organization $record, array $data): Organization {
|
->using(function (Organization $record, array $data): Organization {
|
||||||
/** @var Member $member */
|
/** @var Member $member */
|
||||||
$member = $record->getRelation('membership');
|
$member = $record->getRelation('membership');
|
||||||
|
|
||||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
if ($data['role'] !== $member->role) {
|
||||||
if ($newRole->value !== $member->role) {
|
|
||||||
try {
|
try {
|
||||||
app(MemberService::class)->changeRole($member, $record, $newRole, true);
|
app(MemberService::class)->changeRole($member, $record, Role::from($data['role']), true);
|
||||||
} catch (ApiException $exception) {
|
} catch (ApiException $exception) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
@@ -76,7 +74,7 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
|
|
||||||
return $record;
|
return $record;
|
||||||
}),
|
}),
|
||||||
DetachAction::make()
|
Tables\Actions\DetachAction::make()
|
||||||
->using(function (Organization $record): void {
|
->using(function (Organization $record): void {
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = $this->getOwnerRecord();
|
$user = $this->getOwnerRecord();
|
||||||
@@ -96,7 +94,7 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ namespace App\Filament\Resources\UserResource\RelationManagers;
|
|||||||
|
|
||||||
use App\Filament\Resources\OrganizationResource;
|
use App\Filament\Resources\OrganizationResource;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Filament\Actions\Action;
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
class OwnedOrganizationsRelationManager extends RelationManager
|
class OwnedOrganizationsRelationManager extends RelationManager
|
||||||
@@ -18,10 +18,10 @@ class OwnedOrganizationsRelationManager extends RelationManager
|
|||||||
|
|
||||||
protected static string $relationship = 'ownedOrganizations';
|
protected static string $relationship = 'ownedOrganizations';
|
||||||
|
|
||||||
public function form(Schema $schema): Schema
|
public function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,14 +30,14 @@ class OwnedOrganizationsRelationManager extends RelationManager
|
|||||||
return $table
|
return $table
|
||||||
->recordTitleAttribute('name')
|
->recordTitleAttribute('name')
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name'),
|
Tables\Columns\TextColumn::make('name'),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
@@ -51,7 +51,7 @@ class OwnedOrganizationsRelationManager extends RelationManager
|
|||||||
]))
|
]))
|
||||||
->openUrlInNewTab(),
|
->openUrlInNewTab(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Cache;
|
|||||||
|
|
||||||
class ServerOverview extends Widget
|
class ServerOverview extends Widget
|
||||||
{
|
{
|
||||||
protected string $view = 'filament.widgets.server-overview';
|
protected static string $view = 'filament.widgets.server-overview';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user