mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 16:22:16 +01:00
Compare commits
1 Commits
feature/gi
...
feature/ta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b84ba67cd |
61
.env.ci
61
.env.ci
@@ -1,58 +1,57 @@
|
|||||||
# Application
|
|
||||||
APP_NAME=solidtime
|
APP_NAME=solidtime
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
APP_FORCE_HTTPS=false
|
APP_FORCE_HTTPS=false
|
||||||
APP_ENABLE_REGISTRATION=true
|
SESSION_SECURE_COOKIE=false
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
# Database
|
|
||||||
DB_CONNECTION=pgsql_test
|
DB_CONNECTION=pgsql_test
|
||||||
|
|
||||||
DB_TEST_HOST=127.0.0.1
|
DB_TEST_HOST=127.0.0.1
|
||||||
DB_TEST_PORT=5432
|
DB_TEST_PORT=5432
|
||||||
DB_TEST_DATABASE=laravel
|
DB_TEST_DATABASE=laravel
|
||||||
DB_TEST_USERNAME=root
|
DB_TEST_USERNAME=root
|
||||||
DB_TEST_PASSWORD=root
|
DB_TEST_PASSWORD=root
|
||||||
|
|
||||||
# Broadcasting
|
BROADCAST_DRIVER=log
|
||||||
BROADCAST_DRIVER=null
|
|
||||||
|
|
||||||
# Cache
|
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
# Queue
|
|
||||||
QUEUE_CONNECTION=sync
|
QUEUE_CONNECTION=sync
|
||||||
|
|
||||||
# Session
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
|
||||||
# Mail
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_MAILER=log
|
MAIL_MAILER=log
|
||||||
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
MAIL_FROM_NAME="solidtime"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
|
|
||||||
MAIL_REPLY_TO_NAME="solidtime"
|
|
||||||
|
|
||||||
# Filesystems
|
S3_ACCESS_KEY_ID=
|
||||||
FILESYSTEM_DISK=local
|
S3_SECRET_ACCESS_KEY=
|
||||||
PUBLIC_FILESYSTEM_DISK=public
|
S3_REGION=us-east-1
|
||||||
|
S3_BUCKET=
|
||||||
|
S3_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
# Passport
|
PUSHER_APP_ID=
|
||||||
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="9e27f54d-5dfb-4dde-99d7-834518236c92"
|
PUSHER_APP_KEY=
|
||||||
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="EL5mXp3aF8ITjcwoOXRpbSK7zGrWhW4zTDpQXTkf"
|
PUSHER_APP_SECRET=
|
||||||
|
PUSHER_HOST=
|
||||||
|
PUSHER_PORT=443
|
||||||
|
PUSHER_SCHEME=https
|
||||||
|
PUSHER_APP_CLUSTER=mt1
|
||||||
|
|
||||||
# Auditing
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
AUDITING_ENABLED=true
|
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
|
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||||
# Telescope
|
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||||
TELESCOPE_ENABLED=false
|
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||||
|
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
# Services
|
|
||||||
GOTENBERG_URL=http://0.0.0.0:3000
|
|
||||||
|
|||||||
73
.env.example
73
.env.example
@@ -1,21 +1,18 @@
|
|||||||
# Application
|
|
||||||
APP_NAME=solidtime
|
APP_NAME=solidtime
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
|
APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=https://solidtime.test
|
APP_URL=https://solidtime.test
|
||||||
APP_FORCE_HTTPS=false
|
AUDITING_ENABLED=true
|
||||||
APP_ENABLE_REGISTRATION=true
|
|
||||||
SUPER_ADMINS=admin@example.com
|
SUPER_ADMINS=admin@example.com
|
||||||
PAGINATION_PER_PAGE_DEFAULT=500
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_CHANNEL=single
|
LOG_CHANNEL=single
|
||||||
LOG_DEPRECATIONS_CHANNEL=deprecation
|
LOG_DEPRECATIONS_CHANNEL=deprecation
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
# Database
|
|
||||||
DB_CONNECTION=pgsql
|
DB_CONNECTION=pgsql
|
||||||
|
|
||||||
DB_HOST=pgsql
|
DB_HOST=pgsql
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_DATABASE=laravel
|
DB_DATABASE=laravel
|
||||||
@@ -28,20 +25,18 @@ DB_TEST_DATABASE=laravel
|
|||||||
DB_TEST_USERNAME=root
|
DB_TEST_USERNAME=root
|
||||||
DB_TEST_PASSWORD=root
|
DB_TEST_PASSWORD=root
|
||||||
|
|
||||||
# Broadcasting
|
BROADCAST_DRIVER=log
|
||||||
BROADCAST_DRIVER=null
|
|
||||||
|
|
||||||
# Cache
|
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
|
|
||||||
# Queue
|
|
||||||
QUEUE_CONNECTION=sync
|
QUEUE_CONNECTION=sync
|
||||||
|
|
||||||
# Session
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
|
||||||
# Mail
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
MAIL_MAILER=smtp
|
||||||
MAIL_HOST=mailpit
|
MAIL_HOST=mailpit
|
||||||
MAIL_PORT=1025
|
MAIL_PORT=1025
|
||||||
@@ -49,11 +44,17 @@ MAIL_USERNAME=null
|
|||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
|
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
|
||||||
MAIL_FROM_NAME="solidtime"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
|
|
||||||
MAIL_REPLY_TO_NAME="solidtime"
|
|
||||||
|
|
||||||
# Filesystems
|
PUSHER_APP_ID=
|
||||||
|
PUSHER_APP_KEY=
|
||||||
|
PUSHER_APP_SECRET=
|
||||||
|
PUSHER_HOST=
|
||||||
|
PUSHER_PORT=443
|
||||||
|
PUSHER_SCHEME=https
|
||||||
|
PUSHER_APP_CLUSTER=mt1
|
||||||
|
|
||||||
|
# Storage
|
||||||
FILESYSTEM_DISK=s3
|
FILESYSTEM_DISK=s3
|
||||||
PUBLIC_FILESYSTEM_DISK=s3
|
PUBLIC_FILESYSTEM_DISK=s3
|
||||||
S3_ACCESS_KEY_ID=sail
|
S3_ACCESS_KEY_ID=sail
|
||||||
@@ -64,24 +65,18 @@ S3_URL=http://storage.solidtime.test/local
|
|||||||
S3_ENDPOINT=http://storage.solidtime.test
|
S3_ENDPOINT=http://storage.solidtime.test
|
||||||
S3_USE_PATH_STYLE_ENDPOINT=true
|
S3_USE_PATH_STYLE_ENDPOINT=true
|
||||||
|
|
||||||
# Passport
|
|
||||||
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="9e27f54d-5dfb-4dde-99d7-834518236c92"
|
|
||||||
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="EL5mXp3aF8ITjcwoOXRpbSK7zGrWhW4zTDpQXTkf"
|
|
||||||
|
|
||||||
# Auditing
|
|
||||||
AUDITING_ENABLED=true
|
|
||||||
|
|
||||||
# Telescope
|
|
||||||
TELESCOPE_ENABLED=false
|
|
||||||
|
|
||||||
# Services
|
|
||||||
GOTENBERG_URL=http://gotenberg:3000
|
|
||||||
|
|
||||||
# Local setup
|
|
||||||
NGINX_HOST_NAME=solidtime.test
|
|
||||||
NETWORK_NAME=reverse-proxy-docker-traefik_routing
|
|
||||||
FORWARD_DB_PORT=5432
|
|
||||||
FORWARD_WEB_PORT=8083
|
|
||||||
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
|
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
|
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||||
|
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||||
|
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||||
|
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
|
|
||||||
|
NGINX_HOST_NAME=solidtime.test
|
||||||
|
NETWORK_NAME=reverse-proxy-docker-traefik_routing
|
||||||
|
|
||||||
|
FORWARD_DB_PORT=5432
|
||||||
|
FORWARD_WEB_PORT=8083
|
||||||
|
|
||||||
|
PAGINATION_PER_PAGE_DEFAULT=500
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
APP_NAME=solidtime
|
APP_NAME=solidtime
|
||||||
APP_VERSION=0.0.0
|
|
||||||
APP_BUILD=0
|
|
||||||
VITE_APP_NAME=solidtime
|
VITE_APP_NAME=solidtime
|
||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
APP_DEBUG=false
|
APP_DEBUG=false
|
||||||
APP_FORCE_HTTPS=true
|
APP_FORCE_HTTPS=true
|
||||||
|
SESSION_SECURE_COOKIE=true
|
||||||
OCTANE_SERVER=frankenphp
|
OCTANE_SERVER=frankenphp
|
||||||
PAGINATION_PER_PAGE_DEFAULT=500
|
PAGINATION_PER_PAGE_DEFAULT=500
|
||||||
|
|
||||||
|
|||||||
13
.eslintrc.cjs
Normal file
13
.eslintrc.cjs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require("@rushstack/eslint-patch/modern-module-resolution")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extends: ['plugin:vue/vue3-essential', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier'],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
"unused-imports/no-unused-vars": "error",
|
||||||
|
},
|
||||||
|
plugins: ['unused-imports'],
|
||||||
|
}
|
||||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
github: solidtime-io
|
|
||||||
47
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
47
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Bug Report
|
|
||||||
description: "Report a bug"
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Before creating a new bug report, please check that there isn't already a similar issue.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: A clear and concise description of what the bug is.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: "Steps To Reproduce"
|
|
||||||
description: How do you trigger this bug? Please walk us through it step by step.
|
|
||||||
value: |
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
...
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: "Self-hosted or Cloud?"
|
|
||||||
options:
|
|
||||||
- Self-Hosted
|
|
||||||
- solidtime Cloud
|
|
||||||
- Both
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: "Version of solidtime: (for self-hosted)"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: "solidtime self-hosting guide: (for self-hosted)"
|
|
||||||
description: "Did you use the official guide to self-host solidtime? If yes, which one?"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: 🚀 Feature Request
|
|
||||||
url: https://github.com/solidtime-io/solidtime/discussions/new?category=feature-requests
|
|
||||||
about: Share ideas for new features
|
|
||||||
- name: ❓ Ask a Question
|
|
||||||
url: https://github.com/solidtime-io/solidtime/discussions/new?category=general
|
|
||||||
about: Ask the community for help
|
|
||||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,8 +0,0 @@
|
|||||||
<!--
|
|
||||||
This project is early stage. The structure and APIs are still subject to change and not stable.
|
|
||||||
Therefore, we do not currently accept any contributions, unless you are a member of the team.
|
|
||||||
|
|
||||||
As soon as we feel comfortable enough that the application structure is stable enough, we will open up the project for contributions.
|
|
||||||
|
|
||||||
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.
|
|
||||||
-->
|
|
||||||
70
.github/workflows/build-private.yml
vendored
70
.github/workflows/build-private.yml
vendored
@@ -10,8 +10,6 @@ on:
|
|||||||
- '.github/workflows/build-private.yml'
|
- '.github/workflows/build-private.yml'
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
name: Build - Private
|
name: Build - Private
|
||||||
jobs:
|
jobs:
|
||||||
@@ -19,59 +17,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check out code"
|
- name: "Check out code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
|
||||||
|
|
||||||
- name: "Get build"
|
|
||||||
id: build
|
|
||||||
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: "Get Previous tag (normal push)"
|
|
||||||
id: previoustag
|
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
|
||||||
with:
|
|
||||||
prefix: "v"
|
|
||||||
|
|
||||||
- name: "Get version"
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
|
|
||||||
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
|
|
||||||
version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-)
|
|
||||||
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "ERROR: No previous tag found";
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
version=$(echo "${{ github.ref }}" | cut -c 12-)
|
|
||||||
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Copy .env template for production"
|
|
||||||
run: |
|
|
||||||
cp .env.production .env
|
|
||||||
rm .env.production .env.ci .env.example
|
|
||||||
|
|
||||||
- name: "Add version to .env"
|
|
||||||
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env
|
|
||||||
|
|
||||||
- name: "Add build to .env"
|
|
||||||
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env
|
|
||||||
|
|
||||||
- name: "Output .env"
|
|
||||||
run: cat .env
|
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
|
- name: "Copy .env template for production"
|
||||||
|
run: cp .env.production .env && cat .env
|
||||||
|
|
||||||
- name: "Checkout billing extension"
|
- name: "Checkout billing extension"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -110,24 +67,6 @@ jobs:
|
|||||||
- name: "Install npm dependencies in services extension"
|
- name: "Install npm dependencies in services extension"
|
||||||
run: cd extensions/Services && npm ci
|
run: cd extensions/Services && npm ci
|
||||||
|
|
||||||
- name: "Checkout invoicing extension"
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: solidtime-io/extension-invoicing
|
|
||||||
path: extensions/Invoicing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in invoicing extension"
|
|
||||||
uses: php-actions/composer@v6
|
|
||||||
with:
|
|
||||||
working_dir: "extensions/Invoicing"
|
|
||||||
command: install
|
|
||||||
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
|
||||||
php_version: 8.3
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in invoicing extension"
|
|
||||||
run: cd extensions/Invoicing && npm ci
|
|
||||||
|
|
||||||
- name: "Setup PHP with PECL extension"
|
- name: "Setup PHP with PECL extension"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -148,9 +87,6 @@ jobs:
|
|||||||
- name: "Activate services extension"
|
- name: "Activate services extension"
|
||||||
run: php artisan module:enable Services
|
run: php artisan module:enable Services
|
||||||
|
|
||||||
- name: "Activate invoicing extension"
|
|
||||||
run: php artisan module:enable Invoicing
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
|||||||
90
.github/workflows/build-public-release.yml
vendored
Normal file
90
.github/workflows/build-public-release.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/build-public.yml'
|
||||||
|
- 'docker/prod/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
name: Build - Public (Release)
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
timeout-minutes: 90
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: "Check out code"
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: "Copy .env template for production"
|
||||||
|
run: cp .env.production .env
|
||||||
|
|
||||||
|
- name: "Install dependencies"
|
||||||
|
uses: php-actions/composer@v6
|
||||||
|
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||||
|
php_version: 8.3
|
||||||
|
|
||||||
|
- name: "Use Node.js"
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
|
||||||
|
- name: "Install npm dependencies"
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: "Build"
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: "Login to GitHub Container Registry"
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: "Login to GitHub Container Registry"
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: "Docker meta"
|
||||||
|
id: "meta"
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
solidtime/solidtime
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
|
||||||
|
- name: "Set up QEMU"
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: "Set up Docker Buildx"
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: "Build and push"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/prod/Dockerfile
|
||||||
|
build-args: |
|
||||||
|
DOCKER_FILES_BASE_PATH=docker/prod/
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
185
.github/workflows/build-public.yml
vendored
185
.github/workflows/build-public.yml
vendored
@@ -3,93 +3,37 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- develop
|
- develop
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-public.yml'
|
- '.github/workflows/build-public.yml'
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKERHUB_REPO: solidtime/solidtime
|
|
||||||
GHCR_REPO: ghcr.io/solidtime-io/solidtime
|
|
||||||
|
|
||||||
name: Build - Public
|
name: Build - Public
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
matrix:
|
permissions:
|
||||||
include:
|
packages: write
|
||||||
- runs-on: "ubuntu-24.04-arm"
|
contents: read
|
||||||
platform: "linux/arm64"
|
attestations: write
|
||||||
- runs-on: "ubuntu-24.04"
|
id-token: write
|
||||||
platform: "linux/amd64"
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check out code"
|
- name: "Check out code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
|
||||||
|
|
||||||
- name: "Get build"
|
|
||||||
id: release-build
|
|
||||||
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: "Get Previous tag (normal push)"
|
|
||||||
id: previoustag
|
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
|
||||||
with:
|
|
||||||
prefix: "v"
|
|
||||||
|
|
||||||
- name: "Get version"
|
|
||||||
id: release-version
|
|
||||||
run: |
|
|
||||||
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
|
|
||||||
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
|
|
||||||
version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-)
|
|
||||||
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "ERROR: No previous tag found";
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
version=$(echo "${{ github.ref }}" | cut -c 12-)
|
|
||||||
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Copy .env template for production"
|
- name: "Copy .env template for production"
|
||||||
run: |
|
run: cp .env.production .env
|
||||||
cp .env.production .env
|
|
||||||
rm .env.production .env.ci .env.example
|
|
||||||
|
|
||||||
- name: "Add version to .env"
|
|
||||||
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.release-version.outputs.app_version }}/g' .env
|
|
||||||
|
|
||||||
- name: "Add build to .env"
|
|
||||||
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.release-build.outputs.build }}/g' .env
|
|
||||||
|
|
||||||
- name: "Output .env"
|
|
||||||
run: cat .env
|
|
||||||
|
|
||||||
- name: "Setup PHP with PECL extension"
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
extensions: mbstring, dom, fileinfo, pgsql
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
uses: php-actions/composer@v6
|
||||||
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||||
|
php_version: 8.3
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -102,31 +46,29 @@ jobs:
|
|||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: "Prepare"
|
- name: "Login to GitHub Container Registry"
|
||||||
run: |
|
|
||||||
platform=${{ matrix.platform }}
|
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: "Docker meta"
|
|
||||||
id: "meta"
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ env.DOCKERHUB_REPO }}
|
|
||||||
${{ env.GHCR_REPO }}
|
|
||||||
|
|
||||||
- name: "Login to Docker Hub Container Registry"
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
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@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: "Docker meta"
|
||||||
|
id: "meta"
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
solidtime/solidtime
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
|
||||||
- name: "Set up QEMU"
|
- name: "Set up QEMU"
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -134,85 +76,16 @@ jobs:
|
|||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: "Build and push by digest"
|
- name: "Build and push"
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/prod/Dockerfile
|
file: docker/prod/Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
DOCKER_FILES_BASE_PATH=docker/prod/
|
DOCKER_FILES_BASE_PATH=docker/prod/
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
outputs: type=image,"name=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
- name: "Export digest"
|
|
||||||
run: |
|
|
||||||
mkdir -p ${{ runner.temp }}/digests
|
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
|
||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
|
||||||
|
|
||||||
- name: "Upload digest"
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
|
||||||
path: ${{ runner.temp }}/digests/*
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
merge:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 90
|
|
||||||
needs:
|
|
||||||
- build
|
|
||||||
steps:
|
|
||||||
- name: "Download digests"
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ${{ runner.temp }}/digests
|
|
||||||
pattern: digests-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: "Login to Docker Hub"
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: "Login to GHCR"
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: "Docker meta"
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ env.DOCKERHUB_REPO }}
|
|
||||||
${{ env.GHCR_REPO }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
|
|
||||||
- name: "Create manifest list and push"
|
|
||||||
working-directory: ${{ runner.temp }}/digests
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
|
||||||
|
|
||||||
- name: "Inspect image"
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools inspect ${{ env.DOCKERHUB_REPO }}:${{ steps.meta.outputs.version }}
|
|
||||||
docker buildx imagetools inspect ${{ env.GHCR_REPO }}:${{ steps.meta.outputs.version }}
|
|
||||||
|
|||||||
3
.github/workflows/generate-api-docs.yml
vendored
3
.github/workflows/generate-api-docs.yml
vendored
@@ -3,9 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
api_docs:
|
api_docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.github/workflows/npm-build.yml
vendored
2
.github/workflows/npm-build.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: NPM Build
|
name: NPM Build
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
2
.github/workflows/npm-lint.yml
vendored
2
.github/workflows/npm-lint.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: NPM Lint
|
name: NPM Lint
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
5
.github/workflows/npm-publish-api.yml
vendored
5
.github/workflows/npm-publish-api.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: Publish API package to NPM
|
name: Publish API package to NPM
|
||||||
on:
|
on:
|
||||||
workflow_dispatch
|
workflow_dispatch
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -10,8 +8,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- name: Install root project dependencies
|
- name: Install root project dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
5
.github/workflows/npm-publish-ui.yml
vendored
5
.github/workflows/npm-publish-ui.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: Publish UI package to NPM
|
name: Publish UI package to NPM
|
||||||
on:
|
on:
|
||||||
workflow_dispatch
|
workflow_dispatch
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -10,8 +8,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.github/workflows/npm-typecheck.yml
vendored
3
.github/workflows/npm-typecheck.yml
vendored
@@ -1,8 +1,7 @@
|
|||||||
name: NPM Typecheck
|
name: NPM Typecheck
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.github/workflows/phpstan.yml
vendored
2
.github/workflows/phpstan.yml
vendored
@@ -1,7 +1,5 @@
|
|||||||
name: Static code analysis (PHPStan)
|
name: Static code analysis (PHPStan)
|
||||||
on: push
|
on: push
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
phpstan:
|
phpstan:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
14
.github/workflows/phpunit.yml
vendored
14
.github/workflows/phpunit.yml
vendored
@@ -1,7 +1,5 @@
|
|||||||
name: PHPUnit Tests
|
name: PHPUnit Tests
|
||||||
on: push
|
on: push
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
phpunit:
|
phpunit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -22,15 +20,7 @@ jobs:
|
|||||||
--health-interval 10s
|
--health-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--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:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -65,7 +55,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@v5.4.2
|
uses: codecov/codecov-action@v4.5.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: solidtime-io/solidtime
|
slug: solidtime-io/solidtime
|
||||||
|
|||||||
4
.github/workflows/pint.yml
vendored
4
.github/workflows/pint.yml
vendored
@@ -1,7 +1,5 @@
|
|||||||
name: PHP Linting
|
name: PHP Linting
|
||||||
on: push
|
on: push
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
pint:
|
pint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -12,6 +10,6 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: "Check code style"
|
- name: "Check code style"
|
||||||
uses: aglipanci/laravel-pint-action@2.5
|
uses: aglipanci/laravel-pint-action@2.4
|
||||||
with:
|
with:
|
||||||
configPath: "pint.json"
|
configPath: "pint.json"
|
||||||
|
|||||||
26
.github/workflows/playwright.yml
vendored
26
.github/workflows/playwright.yml
vendored
@@ -1,7 +1,5 @@
|
|||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
on: [push]
|
on: [push]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -29,47 +27,45 @@ jobs:
|
|||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: "Setup node"
|
- uses: actions/setup-node@v4
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
- name: "Setup PHP"
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: '8.3'
|
php-version: '8.3'
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- name: "Run composer install"
|
- name: Run composer install
|
||||||
run: composer install -n --prefer-dist
|
run: composer install -n --prefer-dist
|
||||||
|
|
||||||
- name: "Prepare Laravel Application"
|
- name: Prepare Laravel Application
|
||||||
run: |
|
run: |
|
||||||
cp .env.ci .env
|
cp .env.ci .env
|
||||||
php artisan key:generate
|
php artisan key:generate
|
||||||
php artisan passport:keys
|
|
||||||
php artisan migrate --seed
|
php artisan migrate --seed
|
||||||
|
php artisan passport:keys
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: "Build Frontend"
|
- name: Build Frontend
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: "Run Laravel Server"
|
- name: Run Laravel Server
|
||||||
run: php artisan serve > /dev/null 2>&1 &
|
run: php artisan serve > /dev/null 2>&1 &
|
||||||
|
|
||||||
- name: "Install Playwright Browsers"
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
- name: "Run Playwright tests"
|
- name: Run Playwright tests
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
|
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
|
||||||
|
|
||||||
- name: "Upload test results"
|
- uses: actions/upload-artifact@v4
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ solidtime is a modern open-source time tracking application for Freelancers and
|
|||||||
|
|
||||||
- Time tracking: Track your time with a modern and easy-to-use interface
|
- Time tracking: Track your time with a modern and easy-to-use interface
|
||||||
- Projects: Create and manage projects and assign project members
|
- Projects: Create and manage projects and assign project members
|
||||||
- Tasks: Create and manage tasks and assign tasks to projects
|
- Tasks: Create and manage tasks and assign tasks to project members
|
||||||
- Clients: Create and manage clients and assign clients to projects
|
- Clients: Create and manage clients and assign clients to projects
|
||||||
- Billable rates: Set billable rates for projects, project members, organization members and organizations
|
- Billable rates: Set billable rates for projects, project members, organization members and organizations
|
||||||
- Multiple organizations: Create and manage multiple organizations with one account
|
- Multiple organizations: Create and manage multiple organizations with one account
|
||||||
@@ -28,11 +28,6 @@ We also have an examples repository [here](https://github.com/solidtime-io/self-
|
|||||||
|
|
||||||
If you do not want to self-host solidtime or try it out you can sign up for [solidtime cloud](https://www.solidtime.io/)
|
If you do not want to self-host solidtime or try it out you can sign up for [solidtime cloud](https://www.solidtime.io/)
|
||||||
|
|
||||||
## Issues & Feature Requests
|
|
||||||
|
|
||||||
If you find any **bugs in solidtime**, please feel free to [**open an issue**](https://github.com/solidtime-io/solidtime/issues/new) in this repository, with instructions on how to reproduce the bug.
|
|
||||||
If you have a **feature request**, please [**create a discussion**](https://github.com/solidtime-io/solidtime/discussions/new?category=feature-requests) in this repository.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
This project is in a very early stage. The structure and APIs are still subject to change and not stable.
|
This project is in a very early stage. The structure and APIs are still subject to change and not stable.
|
||||||
@@ -40,8 +35,6 @@ Therefore, we do not currently accept any contributions, unless you are a member
|
|||||||
|
|
||||||
As soon as we feel comfortable enough that the application structure is stable enough, we will open up the project for contributions.
|
As soon as we feel comfortable enough that the application structure is stable enough, we will open up the project for contributions.
|
||||||
|
|
||||||
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Actions\Fortify;
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Events\NewsletterRegistered;
|
use App\Events\NewsletterRegistered;
|
||||||
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\IpLookup\IpLookupServiceContract;
|
use App\Service\IpLookup\IpLookupServiceContract;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use App\Service\UserService;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
@@ -32,12 +34,6 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
*/
|
*/
|
||||||
public function create(array $input): User
|
public function create(array $input): User
|
||||||
{
|
{
|
||||||
if (! config('app.enable_registration')) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'email' => [__('Registration is disabled.')],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Validator::make($input, [
|
Validator::make($input, [
|
||||||
'name' => [
|
'name' => [
|
||||||
'required',
|
'required',
|
||||||
@@ -47,9 +43,9 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
'email' => [
|
'email' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
'email:rfc,strict',
|
'email',
|
||||||
'max:255',
|
'max:255',
|
||||||
UniqueEloquent::make(User::class, 'email', function (Builder $builder): Builder {
|
new UniqueEloquent(User::class, 'email', function (Builder $builder): Builder {
|
||||||
/** @var Builder<User> $builder */
|
/** @var Builder<User> $builder */
|
||||||
return $builder->where('is_placeholder', '=', false);
|
return $builder->where('is_placeholder', '=', false);
|
||||||
}),
|
}),
|
||||||
@@ -76,11 +72,6 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
||||||
|
|
||||||
$startOfWeek = Weekday::Monday;
|
$startOfWeek = Weekday::Monday;
|
||||||
$numberFormat = null;
|
|
||||||
$currencyFormat = null;
|
|
||||||
$dateFormat = null;
|
|
||||||
$intervalFormat = null;
|
|
||||||
$timeFormat = null;
|
|
||||||
$currency = null;
|
$currency = null;
|
||||||
if ($ipLookupResponse !== null) {
|
if ($ipLookupResponse !== null) {
|
||||||
$startOfWeek = $ipLookupResponse->startOfWeek ?? Weekday::Monday;
|
$startOfWeek = $ipLookupResponse->startOfWeek ?? Weekday::Monday;
|
||||||
@@ -90,21 +81,30 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
$currency = $ipLookupResponse->currency;
|
$currency = $ipLookupResponse->currency;
|
||||||
}
|
}
|
||||||
$user = null;
|
$user = null;
|
||||||
DB::transaction(function () use (&$user, $input, $timezone, $startOfWeek, $currency, $numberFormat, $currencyFormat, $dateFormat, $intervalFormat, $timeFormat): void {
|
$organization = null;
|
||||||
$userService = app(UserService::class);
|
DB::transaction(function () use (&$user, &$organization, $input, $timezone, $startOfWeek, $currency) {
|
||||||
$user = $userService->createUser(
|
$user = User::create([
|
||||||
$input['name'],
|
'name' => $input['name'],
|
||||||
$input['email'],
|
'email' => $input['email'],
|
||||||
$input['password'],
|
'password' => Hash::make($input['password']),
|
||||||
$timezone ?? 'UTC',
|
'timezone' => $timezone ?? 'UTC',
|
||||||
$startOfWeek,
|
'week_start' => $startOfWeek,
|
||||||
$currency,
|
]);
|
||||||
$numberFormat,
|
|
||||||
$currencyFormat,
|
$organization = new Organization;
|
||||||
$dateFormat,
|
$organization->name = explode(' ', $user->name, 2)[0]."'s Organization";
|
||||||
$intervalFormat,
|
$organization->personal_team = true;
|
||||||
$timeFormat
|
$organization->currency = $currency ?? 'EUR';
|
||||||
|
$organization->owner()->associate($user);
|
||||||
|
$organization->save();
|
||||||
|
|
||||||
|
$organization->users()->attach(
|
||||||
|
$user, [
|
||||||
|
'role' => Role::Owner->value,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$user->ownedTeams()->save($organization);
|
||||||
});
|
});
|
||||||
|
|
||||||
$newsletterConsent = isset($input['newsletter_consent']) && (bool) $input['newsletter_consent'];
|
$newsletterConsent = isset($input['newsletter_consent']) && (bool) $input['newsletter_consent'];
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
|||||||
'required',
|
'required',
|
||||||
'email',
|
'email',
|
||||||
'max:255',
|
'max:255',
|
||||||
UniqueEloquent::make(User::class, 'email')->ignore($user->id)->query(function (Builder $query) {
|
(new UniqueEloquent(User::class, 'email'))->ignore($user->id)->query(function (Builder $query) {
|
||||||
/** @var Builder<User> $query */
|
/** @var Builder<User> $query */
|
||||||
return $query->where('is_placeholder', '=', false);
|
return $query->where('is_placeholder', '=', false);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ namespace App\Actions\Jetstream;
|
|||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\MemberService;
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\Rules\In;
|
use Illuminate\Validation\Rules\In;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
use Laravel\Jetstream\Contracts\AddsTeamMembers;
|
use Laravel\Jetstream\Contracts\AddsTeamMembers;
|
||||||
|
use Laravel\Jetstream\Events\AddingTeamMember;
|
||||||
|
use Laravel\Jetstream\Events\TeamMemberAdded;
|
||||||
|
|
||||||
class AddOrganizationMember implements AddsTeamMembers
|
class AddOrganizationMember implements AddsTeamMembers
|
||||||
{
|
{
|
||||||
@@ -34,7 +36,15 @@ class AddOrganizationMember implements AddsTeamMembers
|
|||||||
->where('is_placeholder', '=', false)
|
->where('is_placeholder', '=', false)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
app(MemberService::class)->addMember($newOrganizationMember, $organization, Role::from($role));
|
AddingTeamMember::dispatch($organization, $newOrganizationMember);
|
||||||
|
|
||||||
|
DB::transaction(function () use ($organization, $newOrganizationMember, $role) {
|
||||||
|
$organization->users()->attach(
|
||||||
|
$newOrganizationMember, ['role' => $role]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TeamMemberAdded::dispatch($organization, $newOrganizationMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,10 +71,10 @@ class AddOrganizationMember implements AddsTeamMembers
|
|||||||
'email' => [
|
'email' => [
|
||||||
'required',
|
'required',
|
||||||
'email',
|
'email',
|
||||||
ExistsEloquent::make(User::class, 'email', function (Builder $builder) {
|
(new ExistsEloquent(User::class, 'email', function (Builder $builder) {
|
||||||
/** @var Builder<User> $builder */
|
/** @var Builder<User> $builder */
|
||||||
return $builder->where('is_placeholder', '=', false);
|
return $builder->where('is_placeholder', '=', false);
|
||||||
})->withMessage(__('We were unable to find a registered user with this email address.')),
|
}))->withMessage(__('We were unable to find a registered user with this email address.')),
|
||||||
],
|
],
|
||||||
'role' => [
|
'role' => [
|
||||||
'required',
|
'required',
|
||||||
@@ -83,7 +93,7 @@ class AddOrganizationMember implements AddsTeamMembers
|
|||||||
*/
|
*/
|
||||||
protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure
|
protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure
|
||||||
{
|
{
|
||||||
return function ($validator) use ($team, $email): void {
|
return function ($validator) use ($team, $email) {
|
||||||
$validator->errors()->addIf(
|
$validator->errors()->addIf(
|
||||||
$team->hasRealUserWithEmail($email),
|
$team->hasRealUserWithEmail($email),
|
||||||
'email',
|
'email',
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Actions\Jetstream;
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Events\AfterCreateOrganization;
|
use App\Events\AfterCreateOrganization;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\IpLookup\IpLookupServiceContract;
|
|
||||||
use App\Service\OrganizationService;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
@@ -34,18 +33,16 @@ class CreateOrganization implements CreatesTeams
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
])->validateWithBag('createTeam');
|
])->validateWithBag('createTeam');
|
||||||
|
|
||||||
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
$organization = new Organization;
|
||||||
|
$organization->name = $input['name'];
|
||||||
|
$organization->personal_team = false;
|
||||||
|
$organization->owner()->associate($user);
|
||||||
|
$organization->save();
|
||||||
|
|
||||||
$currency = null;
|
$organization->users()->attach(
|
||||||
if ($ipLookupResponse !== null) {
|
$user, [
|
||||||
$currency = $ipLookupResponse->currency;
|
'role' => Role::Owner->value,
|
||||||
}
|
]
|
||||||
|
|
||||||
$organization = app(OrganizationService::class)->createOrganization(
|
|
||||||
$input['name'],
|
|
||||||
$user,
|
|
||||||
false,
|
|
||||||
$currency
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$user->switchTeam($organization);
|
$user->switchTeam($organization);
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands\Admin;
|
|
||||||
|
|
||||||
use App\Enums\Weekday;
|
|
||||||
use App\Models\Organization;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Service\UserService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use LogicException;
|
|
||||||
|
|
||||||
class UserCreateCommand extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'admin:user:create
|
|
||||||
{ name : The name of the user }
|
|
||||||
{ email : The email of the user }
|
|
||||||
{ --ask-for-password : Ask for the password, otherwise the command will generate a random one }
|
|
||||||
{ --verify-email : Verify the email address of the user }';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Create a new user';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$name = $this->argument('name');
|
|
||||||
$email = $this->argument('email');
|
|
||||||
$askForPassword = (bool) $this->option('ask-for-password');
|
|
||||||
$verifyEmail = (bool) $this->option('verify-email');
|
|
||||||
|
|
||||||
if (User::query()->where('email', $email)->where('is_placeholder', '=', false)->exists()) {
|
|
||||||
$this->error('User with email "'.$email.'" already exists.');
|
|
||||||
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($askForPassword) {
|
|
||||||
$outputPassword = false;
|
|
||||||
$password = $this->secret('Enter the password');
|
|
||||||
} else {
|
|
||||||
$outputPassword = true;
|
|
||||||
$password = bin2hex(random_bytes(16));
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = null;
|
|
||||||
DB::transaction(function () use (&$user, $name, $email, $password, $verifyEmail): void {
|
|
||||||
$user = app(UserService::class)->createUser(
|
|
||||||
$name,
|
|
||||||
$email,
|
|
||||||
$password,
|
|
||||||
'UTC',
|
|
||||||
Weekday::Monday,
|
|
||||||
null,
|
|
||||||
verifyEmail: $verifyEmail
|
|
||||||
);
|
|
||||||
});
|
|
||||||
/** @var Organization|null $organization */
|
|
||||||
$organization = $user->ownedTeams->first();
|
|
||||||
if ($organization === null) {
|
|
||||||
throw new LogicException('User does not have an organization');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Created user "'.$name.'" ("'.$email.'")');
|
|
||||||
$this->line('ID: '.$user->getKey());
|
|
||||||
$this->line('Name: '.$name);
|
|
||||||
$this->line('Email: '.$email);
|
|
||||||
if ($outputPassword) {
|
|
||||||
$this->line('Password: '.$password);
|
|
||||||
}
|
|
||||||
$this->line('Timezone: '.$user->timezone);
|
|
||||||
$this->line('Week start: '.$user->week_start->value);
|
|
||||||
|
|
||||||
// Organization
|
|
||||||
$this->line('Currency: '.$organization->currency);
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,9 +35,7 @@ class UserVerifyCommand extends Command
|
|||||||
$this->info('Start verifying user with email "'.$email.'"');
|
$this->info('Start verifying user with email "'.$email.'"');
|
||||||
|
|
||||||
/** @var User|null $user */
|
/** @var User|null $user */
|
||||||
$user = User::query()->where('email', $email)
|
$user = User::where('email', $email)->first();
|
||||||
->where('is_placeholder', '=', false)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($user === null) {
|
if ($user === null) {
|
||||||
$this->error('User with email "'.$email.'" not found.');
|
$this->error('User with email "'.$email.'" not found.');
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands\Correction;
|
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Models\Member;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
|
|
||||||
class CorrectionPlaceholderMembersCommand extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'correction:placeholder-members '.
|
|
||||||
' { --dry-run : Do not actually save anything to the database, just output what would happen }';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Sets all members who belong to a placeholder user to role placeholder';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$this->comment('Sets all members who belong to a placeholder user to role placeholder...');
|
|
||||||
$dryRun = (bool) $this->option('dry-run');
|
|
||||||
if ($dryRun) {
|
|
||||||
$this->comment('Running in dry-run mode. Nothing will be saved to the database.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$members = Member::query()
|
|
||||||
->where('role', '!=', Role::Placeholder->value)
|
|
||||||
->whereHas('user', function (Builder $builder): void {
|
|
||||||
/** @var Builder<User> $builder */
|
|
||||||
$builder->where('is_placeholder', '=', true);
|
|
||||||
})
|
|
||||||
->get();
|
|
||||||
foreach ($members as $member) {
|
|
||||||
/** @var Member $member */
|
|
||||||
$member->role = Role::Placeholder->value;
|
|
||||||
if (! $dryRun) {
|
|
||||||
$member->save();
|
|
||||||
}
|
|
||||||
$this->line('Set role of member (id='.$member->getKey().') to placeholder');
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands\Report;
|
|
||||||
|
|
||||||
use App\Models\Report;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use LogicException;
|
|
||||||
|
|
||||||
class ReportSetExpiredToPrivateCommand extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'report:set-expired-to-private '.
|
|
||||||
' { --dry-run : Do not actually save anything to the database, just output what would happen }';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Makes public reports private if the public_until date has passed.';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$this->comment('Makes public reports private if the public_until date has passed...');
|
|
||||||
$dryRun = (bool) $this->option('dry-run');
|
|
||||||
if ($dryRun) {
|
|
||||||
$this->comment('Running in dry-run mode. Nothing will be saved to the database.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$resetReports = 0;
|
|
||||||
Report::query()
|
|
||||||
->where('public_until', '<', Carbon::now())
|
|
||||||
->orderBy('created_at', 'asc')
|
|
||||||
->chunk(500, function (Collection $reports) use ($dryRun, &$resetReports): void {
|
|
||||||
/** @var Collection<int, Report> $reports */
|
|
||||||
foreach ($reports as $report) {
|
|
||||||
$publicUntil = $report->public_until;
|
|
||||||
if ($publicUntil === null) {
|
|
||||||
throw new LogicException('public_until should not be null');
|
|
||||||
}
|
|
||||||
$this->info('Make report "'.$report->name.'" ('.$report->getKey().') private, expired: '.
|
|
||||||
$publicUntil->toIso8601ZuluString().' ('.$publicUntil->diffForHumans().')');
|
|
||||||
$resetReports++;
|
|
||||||
if (! $dryRun) {
|
|
||||||
$report->is_public = false;
|
|
||||||
$report->share_secret = null;
|
|
||||||
$report->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->comment('Finished setting '.$resetReports.' expired reports to private...');
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands\SelfHost;
|
|
||||||
|
|
||||||
use App\Service\ApiService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
class SelfHostCheckForUpdateCommand extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'self-host:check-for-update';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$apiService = app(ApiService::class);
|
|
||||||
|
|
||||||
$latestVersion = $apiService->checkForUpdate();
|
|
||||||
if ($latestVersion === null) {
|
|
||||||
$this->error('Failed to check for update, check the logs for more information.');
|
|
||||||
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Cache for 13 hours, because the command runs twice daily (every 12 hours).
|
|
||||||
Cache::put('latest_version', $latestVersion, 60 * 60 * 12);
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands\SelfHost;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Database\Query\Builder;
|
|
||||||
use Illuminate\Database\Query\JoinClause;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class SelfHostDatabaseConsistency extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'self-host:database-consistency';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$hadAProblem = false;
|
|
||||||
|
|
||||||
// Task need to be part of project in time entries
|
|
||||||
$problems = DB::table('time_entries')
|
|
||||||
->select(['time_entries.id as id'])
|
|
||||||
->join('tasks', 'time_entries.task_id', '=', 'tasks.id')
|
|
||||||
->where('tasks.project_id', '!=', DB::raw('time_entries.project_id'))
|
|
||||||
->get();
|
|
||||||
$this->logProblems($problems, 'Time entries have a task that does not belong to the project of the time entry', $hadAProblem);
|
|
||||||
|
|
||||||
// Client id is the client id of the project
|
|
||||||
$problems = DB::table('time_entries')
|
|
||||||
->select(['time_entries.id as id'])
|
|
||||||
->join('projects', 'time_entries.project_id', '=', 'projects.id')
|
|
||||||
->where(DB::raw('coalesce(projects.client_id::varchar, \'\')'), '!=', DB::raw('coalesce(time_entries.client_id::varchar, \'\')'))
|
|
||||||
->get();
|
|
||||||
$this->logProblems($problems, 'Time entries have a client that does not match the client of the project', $hadAProblem);
|
|
||||||
|
|
||||||
// Client id can only be not null if the project id is not null
|
|
||||||
$problems = DB::table('time_entries')
|
|
||||||
->select(['time_entries.id as id'])
|
|
||||||
->whereNotNull('client_id')
|
|
||||||
->whereNull('project_id')
|
|
||||||
->get();
|
|
||||||
$this->logProblems($problems, 'Time entries have a client but no project', $hadAProblem);
|
|
||||||
|
|
||||||
// Every user needs to be a member of at least one organization
|
|
||||||
$problems = DB::table('users')
|
|
||||||
->select(['users.id as id'])
|
|
||||||
->leftJoin('members', 'users.id', '=', 'members.user_id')
|
|
||||||
->whereNull('members.id')
|
|
||||||
->get();
|
|
||||||
$this->logProblems($problems, 'Users are not member of any organization', $hadAProblem);
|
|
||||||
|
|
||||||
// Every organization needs at least an owner
|
|
||||||
$problems = DB::table('organizations')
|
|
||||||
->select(['organizations.id as id'])
|
|
||||||
->leftJoin('members', function (JoinClause $join): void {
|
|
||||||
$join->on('organizations.id', '=', 'members.organization_id')
|
|
||||||
->where('members.role', '=', 'owner');
|
|
||||||
})
|
|
||||||
->whereNull('members.id')
|
|
||||||
->get();
|
|
||||||
$this->logProblems($problems, 'Organizations without an owner', $hadAProblem);
|
|
||||||
|
|
||||||
// Every member can only have one running time entry
|
|
||||||
$problems = DB::table('time_entries')
|
|
||||||
->select(['user_id as id'])
|
|
||||||
->whereNull('end')
|
|
||||||
->groupBy('user_id')
|
|
||||||
->havingRaw('count(*) > 1')
|
|
||||||
->get(['user_id', DB::raw('count(*) as count')]);
|
|
||||||
$this->logProblems($problems, 'Users with more than one running time entry', $hadAProblem);
|
|
||||||
|
|
||||||
// Users have a current organization that they are not a member of
|
|
||||||
$problems = DB::table('users')
|
|
||||||
->select(['users.id as id'])
|
|
||||||
->whereNotNull('current_team_id')
|
|
||||||
->whereNotIn('current_team_id', function (Builder $query): void {
|
|
||||||
$query->select('organization_id')
|
|
||||||
->from('members')
|
|
||||||
->whereColumn('members.user_id', 'users.id');
|
|
||||||
})->get();
|
|
||||||
$this->logProblems($problems, 'Users have a current organization that they are not a member of', $hadAProblem);
|
|
||||||
|
|
||||||
return $hadAProblem ? self::FAILURE : self::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Collection<int, \stdClass> $problems
|
|
||||||
*/
|
|
||||||
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
|
|
||||||
{
|
|
||||||
$message = 'Consistency problem: '.$message;
|
|
||||||
if ($problems->isNotEmpty()) {
|
|
||||||
$ids = $problems->pluck('id');
|
|
||||||
$hadAProblem = true;
|
|
||||||
Log::error($message, [
|
|
||||||
'ids' => $ids,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$error = $message;
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
$error .= "\n - ".$id;
|
|
||||||
}
|
|
||||||
$this->error($error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@ class SelfHostGenerateKeysCommand extends Command
|
|||||||
*/
|
*/
|
||||||
protected $signature = 'self-host:generate-keys
|
protected $signature = 'self-host:generate-keys
|
||||||
{ --length=4096 : The length of the passport private key }
|
{ --length=4096 : The length of the passport private key }
|
||||||
{ --multi-line : Whether to output the keys in multiple lines }
|
|
||||||
{ --format=env : The format of the output (env, yaml) }';
|
{ --format=env : The format of the output (env, yaml) }';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,7 +34,6 @@ class SelfHostGenerateKeysCommand extends Command
|
|||||||
{
|
{
|
||||||
$format = $this->option('format');
|
$format = $this->option('format');
|
||||||
$key = RSA::createKey((int) $this->option('length'));
|
$key = RSA::createKey((int) $this->option('length'));
|
||||||
$multiLine = (bool) $this->option('multi-line');
|
|
||||||
|
|
||||||
$publicKey = (string) $key->getPublicKey();
|
$publicKey = (string) $key->getPublicKey();
|
||||||
$privateKey = (string) $key;
|
$privateKey = (string) $key;
|
||||||
@@ -43,17 +41,12 @@ class SelfHostGenerateKeysCommand extends Command
|
|||||||
|
|
||||||
if ($format === 'env') {
|
if ($format === 'env') {
|
||||||
$this->line('APP_KEY="'.$appKey.'"');
|
$this->line('APP_KEY="'.$appKey.'"');
|
||||||
if ($multiLine) {
|
$this->line('PASSPORT_PRIVATE_KEY="'.$privateKey.'"');
|
||||||
$this->line('PASSPORT_PRIVATE_KEY="'.Str::replace("\r\n", "\n", $privateKey).'"');
|
$this->line('PASSPORT_PUBLIC_KEY="'.$publicKey.'"');
|
||||||
$this->line('PASSPORT_PUBLIC_KEY="'.Str::replace("\r\n", "\n", $publicKey).'"');
|
|
||||||
} else {
|
|
||||||
$this->line('PASSPORT_PRIVATE_KEY="'.Str::replace("\r\n", '\n', $privateKey).'"');
|
|
||||||
$this->line('PASSPORT_PUBLIC_KEY="'.Str::replace("\r\n", '\n', $publicKey).'"');
|
|
||||||
}
|
|
||||||
} elseif ($format === 'yaml') {
|
} elseif ($format === 'yaml') {
|
||||||
$this->line('APP_KEY: "'.$appKey.'"');
|
$this->line('APP_KEY: "'.$appKey.'"');
|
||||||
$this->line("PASSPORT_PRIVATE_KEY: |\n ".Str::replace("\r\n", "\n ", $privateKey));
|
$this->line("PASSPORT_PRIVATE_KEY: |\n ".Str::replace("\n", "\n ", $privateKey));
|
||||||
$this->line("PASSPORT_PUBLIC_KEY: |\n ".Str::replace("\r\n", "\n ", $publicKey));
|
$this->line("PASSPORT_PUBLIC_KEY: |\n ".Str::replace("\n", "\n ", $publicKey));
|
||||||
} else {
|
} else {
|
||||||
$this->error('Invalid format');
|
$this->error('Invalid format');
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands\SelfHost;
|
|
||||||
|
|
||||||
use App\Service\ApiService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class SelfHostTelemetryCommand extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'self-host:telemetry';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$apiService = app(ApiService::class);
|
|
||||||
|
|
||||||
$success = $apiService->telemetry();
|
|
||||||
|
|
||||||
if (! $success) {
|
|
||||||
$this->error('Failed to send telemetry data, check the logs for more information.');
|
|
||||||
|
|
||||||
return self::FAILURE;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,7 +54,7 @@ class TimeEntrySendStillRunningMailsCommand extends Command
|
|||||||
$query->where('is_placeholder', '=', false);
|
$query->where('is_placeholder', '=', false);
|
||||||
})
|
})
|
||||||
->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) {
|
||||||
/** @var Collection<int, TimeEntry> $timeEntries */
|
/** @var Collection<int, TimeEntry> $timeEntries */
|
||||||
foreach ($timeEntries as $timeEntry) {
|
foreach ($timeEntries as $timeEntry) {
|
||||||
$user = $timeEntry->user;
|
$user = $timeEntry->user;
|
||||||
|
|||||||
@@ -17,18 +17,6 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->command('time-entry:send-still-running-mails')
|
$schedule->command('time-entry:send-still-running-mails')
|
||||||
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
|
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
|
||||||
->everyTenMinutes();
|
->everyTenMinutes();
|
||||||
|
|
||||||
$schedule->command('self-host:check-for-update')
|
|
||||||
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
|
|
||||||
->twiceDaily();
|
|
||||||
|
|
||||||
$schedule->command('self-host:telemetry')
|
|
||||||
->when(fn (): bool => config('scheduling.tasks.self_hosting_telemetry'))
|
|
||||||
->twiceDaily();
|
|
||||||
|
|
||||||
$schedule->command('self-host:database-consistency')
|
|
||||||
->when(fn (): bool => config('scheduling.tasks.self_hosting_database_consistency'))
|
|
||||||
->twiceDaily();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum CurrencyFormat: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case ISOCodeBeforeWithSpace = 'iso-code-before-with-space';
|
|
||||||
case ISOCodeAfterWithSpace = 'iso-code-after-with-space';
|
|
||||||
|
|
||||||
case SymbolBefore = 'symbol-before';
|
|
||||||
|
|
||||||
case SymbolAfter = 'symbol-after';
|
|
||||||
|
|
||||||
case SymbolBeforeWithSpace = 'symbol-before-with-space';
|
|
||||||
|
|
||||||
case SymbolAfterWithSpace = 'symbol-after-with-space';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public static function toSelectArray(): array
|
|
||||||
{
|
|
||||||
$selectArray = [];
|
|
||||||
foreach (self::values() as $value) {
|
|
||||||
$selectArray[(string) $value] = (string) __('enum.currency_format.'.$value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $selectArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum DateFormat: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case PointSeparatedDMYYYY = 'point-separated-d-m-yyyy';
|
|
||||||
case SlashSeparatedMMDDYYYY = 'slash-separated-mm-dd-yyyy';
|
|
||||||
|
|
||||||
case SlashSeparatedDDMMYYYY = 'slash-separated-dd-mm-yyyy';
|
|
||||||
|
|
||||||
case HyphenSeparatedDDMMYYY = 'hyphen-separated-dd-mm-yyyy';
|
|
||||||
|
|
||||||
case HyphenSeparatedMMDDDYYYY = 'hyphen-separated-mm-dd-yyyy';
|
|
||||||
|
|
||||||
case HyphenSeparatedYYYYMMDD = 'hyphen-separated-yyyy-mm-dd';
|
|
||||||
|
|
||||||
public function toCarbonFormat(): string
|
|
||||||
{
|
|
||||||
return match ($this->value) {
|
|
||||||
self::PointSeparatedDMYYYY->value => 'j.n.Y',
|
|
||||||
self::SlashSeparatedMMDDYYYY->value => 'm/d/Y',
|
|
||||||
self::SlashSeparatedDDMMYYYY->value => 'd/m/Y',
|
|
||||||
self::HyphenSeparatedDDMMYYY->value => 'd-m-Y',
|
|
||||||
self::HyphenSeparatedMMDDDYYYY->value => 'm-d-Y',
|
|
||||||
self::HyphenSeparatedYYYYMMDD->value => 'Y-m-d',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public static function toSelectArray(): array
|
|
||||||
{
|
|
||||||
$selectArray = [];
|
|
||||||
foreach (self::values() as $value) {
|
|
||||||
$selectArray[(string) $value] = (string) __('enum.date_format.'.$value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $selectArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Maatwebsite\Excel\Excel;
|
|
||||||
|
|
||||||
enum ExportFormat: string
|
|
||||||
{
|
|
||||||
case CSV = 'csv';
|
|
||||||
case PDF = 'pdf';
|
|
||||||
case XLSX = 'xlsx';
|
|
||||||
case ODS = 'ods';
|
|
||||||
|
|
||||||
public function getFileExtension(): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::CSV => 'csv',
|
|
||||||
self::PDF => 'pdf',
|
|
||||||
self::XLSX => 'xlsx',
|
|
||||||
self::ODS => 'ods',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getExportPackageType(): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::CSV => Excel::CSV,
|
|
||||||
self::PDF => Excel::MPDF,
|
|
||||||
self::XLSX => Excel::XLSX,
|
|
||||||
self::ODS => Excel::ODS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum IntervalFormat: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Decimal = 'decimal';
|
|
||||||
case HoursMinutes = 'hours-minutes';
|
|
||||||
|
|
||||||
case HoursMinutesColonSeparated = 'hours-minutes-colon-separated';
|
|
||||||
|
|
||||||
case HoursMinutesSecondsColonSeparated = 'hours-minutes-seconds-colon-separated';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public static function toSelectArray(): array
|
|
||||||
{
|
|
||||||
$selectArray = [];
|
|
||||||
foreach (self::values() as $value) {
|
|
||||||
$selectArray[(string) $value] = (string) __('enum.interval_format.'.$value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $selectArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @info https://en.wikipedia.org/wiki/Decimal_separator
|
|
||||||
*/
|
|
||||||
enum NumberFormat: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case ThousandsPointDecimalComma = 'point-comma';
|
|
||||||
|
|
||||||
case ThousandsCommaDecimalPoint = 'comma-point';
|
|
||||||
case ThousandsSpaceDecimalComma = 'space-comma';
|
|
||||||
|
|
||||||
case ThousandsSpaceDecimalPoint = 'space-point';
|
|
||||||
|
|
||||||
case ThousandsApostropheDecimalPoint = 'apostrophe-point';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public static function toSelectArray(): array
|
|
||||||
{
|
|
||||||
$selectArray = [];
|
|
||||||
foreach (self::values() as $value) {
|
|
||||||
$selectArray[(string) $value] = (string) __('enum.number_format.'.$value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $selectArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Enums;
|
namespace App\Enums;
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum TimeEntryAggregationType: string
|
enum TimeEntryAggregationType: string
|
||||||
{
|
{
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Day = 'day';
|
case Day = 'day';
|
||||||
case Week = 'week';
|
case Week = 'week';
|
||||||
case Month = 'month';
|
case Month = 'month';
|
||||||
@@ -21,16 +17,6 @@ enum TimeEntryAggregationType: string
|
|||||||
case Billable = 'billable';
|
case Billable = 'billable';
|
||||||
case Description = 'description';
|
case Description = 'description';
|
||||||
|
|
||||||
public static function fromInterval(TimeEntryAggregationTypeInterval $timeEntryAggregationTypeInterval): TimeEntryAggregationType
|
|
||||||
{
|
|
||||||
return match ($timeEntryAggregationTypeInterval) {
|
|
||||||
TimeEntryAggregationTypeInterval::Day => TimeEntryAggregationType::Day,
|
|
||||||
TimeEntryAggregationTypeInterval::Week => TimeEntryAggregationType::Week,
|
|
||||||
TimeEntryAggregationTypeInterval::Month => TimeEntryAggregationType::Month,
|
|
||||||
TimeEntryAggregationTypeInterval::Year => TimeEntryAggregationType::Year,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toInterval(): ?TimeEntryAggregationTypeInterval
|
public function toInterval(): ?TimeEntryAggregationTypeInterval
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum TimeFormat: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case TwelveHours = '12-hours';
|
|
||||||
case TwentyFourHours = '24-hours';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public static function toSelectArray(): array
|
|
||||||
{
|
|
||||||
$selectArray = [];
|
|
||||||
foreach (self::values() as $value) {
|
|
||||||
$selectArray[(string) $value] = (string) __('enum.time_format.'.$value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $selectArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Enums;
|
namespace App\Enums;
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
enum Weekday: string
|
enum Weekday: string
|
||||||
{
|
{
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Monday = 'monday';
|
case Monday = 'monday';
|
||||||
case Tuesday = 'tuesday';
|
case Tuesday = 'tuesday';
|
||||||
case Wednesday = 'wednesday';
|
case Wednesday = 'wednesday';
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
|
|
||||||
class DatabaseSeederAfterSeed
|
|
||||||
{
|
|
||||||
use Dispatchable;
|
|
||||||
|
|
||||||
public function __construct() {}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
|
|
||||||
class DatabaseSeederBeforeDelete
|
|
||||||
{
|
|
||||||
use Dispatchable;
|
|
||||||
|
|
||||||
public function __construct() {}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Api;
|
|
||||||
|
|
||||||
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
|
|
||||||
{
|
|
||||||
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Api;
|
|
||||||
|
|
||||||
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
|
|
||||||
{
|
|
||||||
public const string KEY = 'feature_is_not_available_in_free_plan';
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Api;
|
|
||||||
|
|
||||||
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
|
|
||||||
{
|
|
||||||
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Api;
|
|
||||||
|
|
||||||
class PdfRendererIsNotConfiguredException extends ApiException
|
|
||||||
{
|
|
||||||
public const string KEY = 'pdf_renderer_is_not_configured';
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Api;
|
|
||||||
|
|
||||||
class PersonalAccessClientIsNotConfiguredException extends ApiException
|
|
||||||
{
|
|
||||||
public const string KEY = 'personal_access_client_is_not_configured';
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Api;
|
|
||||||
|
|
||||||
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
|
|
||||||
{
|
|
||||||
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ class Handler extends ExceptionHandler
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
$this->reportable(function (Throwable $e): void {
|
$this->reportable(function (Throwable $e) {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Extensions\Scramble;
|
namespace App\Extensions\Scramble;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
|
||||||
use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
|
use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
|
||||||
use Dedoc\Scramble\Support\Generator\Response;
|
use Dedoc\Scramble\Support\Generator\Response;
|
||||||
use Dedoc\Scramble\Support\Generator\Schema;
|
use Dedoc\Scramble\Support\Generator\Schema;
|
||||||
@@ -45,49 +44,39 @@ class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$newType = new OpenApiObjectType;
|
$type = new OpenApiObjectType;
|
||||||
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
$type->addProperty('data', (new ArrayType)->setItems($collectingType));
|
||||||
if ($type instanceof ObjectType && $type->isInstanceOf(TimeEntryCollection::class)) {
|
$type->addProperty(
|
||||||
$newType->addProperty(
|
'links',
|
||||||
'meta',
|
(new OpenApiObjectType)
|
||||||
(new OpenApiObjectType)
|
->addProperty('first', (new StringType)->nullable(true))
|
||||||
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
->addProperty('last', (new StringType)->nullable(true))
|
||||||
->setRequired(['total'])
|
->addProperty('prev', (new StringType)->nullable(true))
|
||||||
);
|
->addProperty('next', (new StringType)->nullable(true))
|
||||||
$newType->setRequired(['data', 'meta']);
|
->setRequired(['first', 'last', 'prev', 'next'])
|
||||||
} else {
|
);
|
||||||
$newType->addProperty(
|
$type->addProperty(
|
||||||
'links',
|
'meta',
|
||||||
(new OpenApiObjectType)
|
(new OpenApiObjectType)
|
||||||
->addProperty('first', (new StringType)->nullable(true))
|
->addProperty('current_page', new IntegerType)
|
||||||
->addProperty('last', (new StringType)->nullable(true))
|
->addProperty('from', (new IntegerType)->nullable(true))
|
||||||
->addProperty('prev', (new StringType)->nullable(true))
|
->addProperty('last_page', new IntegerType)
|
||||||
->addProperty('next', (new StringType)->nullable(true))
|
->addProperty('links', (new ArrayType)->setItems(
|
||||||
->setRequired(['first', 'last', 'prev', 'next'])
|
(new OpenApiObjectType)
|
||||||
);
|
->addProperty('url', (new StringType)->nullable(true))
|
||||||
$newType->addProperty(
|
->addProperty('label', new StringType)
|
||||||
'meta',
|
->addProperty('active', new BooleanType)
|
||||||
(new OpenApiObjectType)
|
->setRequired(['url', 'label', 'active'])
|
||||||
->addProperty('current_page', new IntegerType)
|
)->setDescription('Generated paginator links.'))
|
||||||
->addProperty('from', (new IntegerType)->nullable(true))
|
->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.'))
|
||||||
->addProperty('last_page', new IntegerType)
|
->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.'))
|
||||||
->addProperty('links', (new ArrayType)->setItems(
|
->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.'))
|
||||||
(new OpenApiObjectType)
|
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
||||||
->addProperty('url', (new StringType)->nullable(true))
|
->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total'])
|
||||||
->addProperty('label', new StringType)
|
);
|
||||||
->addProperty('active', new BooleanType)
|
$type->setRequired(['data', 'links', 'meta']);
|
||||||
->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;
|
return $type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use Filament\Tables;
|
|||||||
use Filament\Tables\Columns\IconColumn;
|
use Filament\Tables\Columns\IconColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
||||||
|
|
||||||
class AuditResource extends Resource
|
class AuditResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -38,8 +38,8 @@ class AuditResource extends Resource
|
|||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\TextInput::make('auditable_id')
|
Forms\Components\TextInput::make('auditable_id')
|
||||||
->required(),
|
->required(),
|
||||||
PrettyJsonField::make('old_values'),
|
PrettyJson::make('old_values'),
|
||||||
PrettyJsonField::make('new_values'),
|
PrettyJson::make('new_values'),
|
||||||
Forms\Components\Textarea::make('url'),
|
Forms\Components\Textarea::make('url'),
|
||||||
Forms\Components\TextInput::make('ip_address'),
|
Forms\Components\TextInput::make('ip_address'),
|
||||||
Forms\Components\TextInput::make('user_agent')
|
Forms\Components\TextInput::make('user_agent')
|
||||||
|
|||||||
@@ -60,13 +60,8 @@ class ClientResource extends Resource
|
|||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('organization')
|
SelectFilter::make('organization')
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
->relationship('organization', 'name')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class EditClient extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
Actions\DeleteAction::make(),
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListClients extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use Filament\Tables\Columns\TextColumn;
|
|||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @source https://gitlab.com/amvisor/filament-failed-jobs
|
* @source https://gitlab.com/amvisor/filament-failed-jobs
|
||||||
@@ -50,7 +50,7 @@ class FailedJobResource extends Resource
|
|||||||
|
|
||||||
// make text a little bit smaller because often a complete Stack Trace is shown:
|
// make text a little bit smaller because often a complete Stack Trace is shown:
|
||||||
TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
|
TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
|
||||||
PrettyJsonField::make('payload')->disabled()->columnSpan(4),
|
PrettyJson::make('payload')->disabled()->columnSpan(4),
|
||||||
])->columns(4);
|
])->columns(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource\Pages;
|
|
||||||
use App\Models\OrganizationInvitation;
|
|
||||||
use App\Service\OrganizationInvitationService;
|
|
||||||
use Filament\Forms;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class OrganizationInvitationResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = OrganizationInvitation::class;
|
|
||||||
|
|
||||||
protected static ?string $label = 'Invitations';
|
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-user-plus';
|
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Users';
|
|
||||||
|
|
||||||
protected static ?int $navigationSort = 9;
|
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
|
||||||
{
|
|
||||||
return $form
|
|
||||||
->columns(1)
|
|
||||||
->schema([
|
|
||||||
Forms\Components\TextInput::make('email')
|
|
||||||
->label('Email')
|
|
||||||
->disabledOn(['edit'])
|
|
||||||
->required(),
|
|
||||||
Select::make('role')
|
|
||||||
->options(Role::class),
|
|
||||||
Forms\Components\Select::make('organization_id')
|
|
||||||
->label('Organization')
|
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
|
||||||
->searchable(['name'])
|
|
||||||
->disabledOn(['edit'])
|
|
||||||
->required(),
|
|
||||||
Forms\Components\DateTimePicker::make('created_at')
|
|
||||||
->label('Created At')
|
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
|
||||||
Forms\Components\DateTimePicker::make('updated_at')
|
|
||||||
->label('Updated At')
|
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
Tables\Columns\TextColumn::make('organization.name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('email')
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('role'),
|
|
||||||
Tables\Columns\TextColumn::make('created_at')
|
|
||||||
->label('Created At')
|
|
||||||
->dateTime()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('updated_at')
|
|
||||||
->label('Updated At')
|
|
||||||
->dateTime()
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
|
||||||
->defaultSort('created_at', 'desc')
|
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Tables\Actions\EditAction::make(),
|
|
||||||
Tables\Actions\DeleteAction::make(),
|
|
||||||
])
|
|
||||||
->bulkActions([
|
|
||||||
Tables\Actions\BulkActionGroup::make([
|
|
||||||
Tables\Actions\BulkAction::make('resend')
|
|
||||||
->label('Resend')
|
|
||||||
->action(function (Collection $records): void {
|
|
||||||
foreach ($records as $organizationInvite) {
|
|
||||||
app(OrganizationInvitationService::class)->resend($organizationInvite);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => Pages\ListOrganizationInvitations::route('/'),
|
|
||||||
'edit' => Pages\EditOrganizationInvitation::route('/{record}/edit'),
|
|
||||||
'view' => Pages\ViewOrganizationInvitation::route('/{record}'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource;
|
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditOrganizationInvitation extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = OrganizationInvitationResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
Actions\DeleteAction::make()
|
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListOrganizationInvitations extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = OrganizationInvitationResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
|
||||||
|
|
||||||
class ViewOrganizationInvitation extends ViewRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = OrganizationInvitationResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
EditAction::make('edit')
|
|
||||||
->icon('heroicon-s-pencil'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,16 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Enums\CurrencyFormat;
|
|
||||||
use App\Enums\DateFormat;
|
|
||||||
use App\Enums\IntervalFormat;
|
|
||||||
use App\Enums\NumberFormat;
|
|
||||||
use App\Enums\TimeFormat;
|
|
||||||
use App\Filament\Resources\OrganizationResource\Pages;
|
use App\Filament\Resources\OrganizationResource\Pages;
|
||||||
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;
|
||||||
use App\Service\DeletionService;
|
|
||||||
use App\Service\Export\ExportService;
|
use App\Service\Export\ExportService;
|
||||||
use App\Service\Import\Importers\ImporterProvider;
|
use App\Service\Import\Importers\ImporterProvider;
|
||||||
use App\Service\Import\Importers\ImportException;
|
use App\Service\Import\Importers\ImportException;
|
||||||
@@ -53,28 +46,10 @@ class OrganizationResource extends Resource
|
|||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\Toggle::make('personal_team')
|
Forms\Components\Toggle::make('personal_team')
|
||||||
->label('Is personal?')
|
->label('Is personal?')
|
||||||
->hiddenOn(['create'])
|
|
||||||
->required(),
|
->required(),
|
||||||
Forms\Components\Select::make('user_id')
|
Forms\Components\Select::make('user_id')
|
||||||
->label('Owner')
|
|
||||||
->relationship(name: 'owner', titleAttribute: 'email')
|
->relationship(name: 'owner', titleAttribute: 'email')
|
||||||
->searchable(['name', 'email'])
|
->searchable(['name', 'email'])
|
||||||
->disabledOn(['edit'])
|
|
||||||
->required(),
|
|
||||||
Select::make('date_format')
|
|
||||||
->options(DateFormat::toSelectArray())
|
|
||||||
->required(),
|
|
||||||
Select::make('currency_format')
|
|
||||||
->options(CurrencyFormat::toSelectArray())
|
|
||||||
->required(),
|
|
||||||
Select::make('interval_format')
|
|
||||||
->options(IntervalFormat::toSelectArray())
|
|
||||||
->required(),
|
|
||||||
Select::make('number_format')
|
|
||||||
->options(NumberFormat::toSelectArray())
|
|
||||||
->required(),
|
|
||||||
Select::make('time_format')
|
|
||||||
->options(TimeFormat::toSelectArray())
|
|
||||||
->required(),
|
->required(),
|
||||||
Forms\Components\Select::make('currency')
|
Forms\Components\Select::make('currency')
|
||||||
->label('Currency')
|
->label('Currency')
|
||||||
@@ -87,7 +62,6 @@ class OrganizationResource extends Resource
|
|||||||
|
|
||||||
return $select;
|
return $select;
|
||||||
})
|
})
|
||||||
->required()
|
|
||||||
->searchable(),
|
->searchable(),
|
||||||
Forms\Components\TextInput::make('billable_rate')
|
Forms\Components\TextInput::make('billable_rate')
|
||||||
->label('Billable rate (in Cents)')
|
->label('Billable rate (in Cents)')
|
||||||
@@ -96,16 +70,13 @@ class OrganizationResource extends Resource
|
|||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
'integer',
|
||||||
'gt:0',
|
'gt:0',
|
||||||
'max:2147483647',
|
|
||||||
])
|
])
|
||||||
->numeric(),
|
->numeric(),
|
||||||
Forms\Components\DateTimePicker::make('created_at')
|
Forms\Components\DateTimePicker::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
->disabled(),
|
||||||
Forms\Components\DateTimePicker::make('updated_at')
|
Forms\Components\DateTimePicker::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
->disabled(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -125,7 +96,7 @@ class OrganizationResource extends Resource
|
|||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\TextColumn::make('currency'),
|
Tables\Columns\TextColumn::make('currency'),
|
||||||
TextColumn::make('billable_rate')
|
TextColumn::make('billable_rate')
|
||||||
->money(fn (Organization $resource) => $resource->currency, divideBy: 100),
|
->money(fn (Organization $resource) => $resource->currency ?? 'EUR', divideBy: 100),
|
||||||
Tables\Columns\TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
@@ -140,10 +111,6 @@ class OrganizationResource extends Resource
|
|||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
Tables\Actions\DeleteAction::make()
|
|
||||||
->using(function (Organization $record): void {
|
|
||||||
app(DeletionService::class)->deleteOrganization($record);
|
|
||||||
}),
|
|
||||||
Action::make('Export')
|
Action::make('Export')
|
||||||
->icon('heroicon-o-arrow-down-tray')
|
->icon('heroicon-o-arrow-down-tray')
|
||||||
->action(function (Organization $record) {
|
->action(function (Organization $record) {
|
||||||
@@ -155,7 +122,7 @@ class OrganizationResource extends Resource
|
|||||||
->persistent()
|
->persistent()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return response()->streamDownload(function () use ($file): void {
|
return response()->streamDownload(function () use ($file) {
|
||||||
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) {
|
||||||
@@ -170,7 +137,7 @@ class OrganizationResource extends Resource
|
|||||||
}),
|
}),
|
||||||
Action::make('Import')
|
Action::make('Import')
|
||||||
->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) {
|
||||||
try {
|
try {
|
||||||
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
||||||
if ($file === null) {
|
if ($file === null) {
|
||||||
@@ -231,6 +198,8 @@ class OrganizationResource extends Resource
|
|||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +207,6 @@ class OrganizationResource extends Resource
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
UsersRelationManager::class,
|
UsersRelationManager::class,
|
||||||
InvitationsRelationManager::class,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class DeleteOrganization extends DeleteAction
|
|||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
// TODO: check why setting the icon is necessary
|
||||||
$this->icon('heroicon-m-trash');
|
$this->icon('heroicon-m-trash');
|
||||||
$this->action(function (): void {
|
$this->action(function (): void {
|
||||||
$result = $this->process(function (Organization $record): bool {
|
$result = $this->process(function (Organization $record): bool {
|
||||||
|
|||||||
@@ -4,33 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources\OrganizationResource\Pages;
|
namespace App\Filament\Resources\OrganizationResource\Pages;
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Filament\Resources\OrganizationResource;
|
use App\Filament\Resources\OrganizationResource;
|
||||||
use App\Models\Organization;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
class CreateOrganization extends CreateRecord
|
class CreateOrganization extends CreateRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = OrganizationResource::class;
|
protected static string $resource = OrganizationResource::class;
|
||||||
|
|
||||||
protected function mutateFormDataBeforeCreate(array $data): array
|
|
||||||
{
|
|
||||||
$data['personal_team'] = false;
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function afterCreate(): void
|
|
||||||
{
|
|
||||||
/** @var Organization $organization */
|
|
||||||
$organization = $this->record;
|
|
||||||
|
|
||||||
$user = $organization->owner;
|
|
||||||
|
|
||||||
$organization->users()->attach(
|
|
||||||
$user, [
|
|
||||||
'role' => Role::Owner->value,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListOrganizations extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\OrganizationResource\RelationManagers;
|
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource;
|
|
||||||
use App\Models\Organization;
|
|
||||||
use App\Models\OrganizationInvitation;
|
|
||||||
use App\Service\InvitationService;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Tables\Actions\Action;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class InvitationsRelationManager extends RelationManager
|
|
||||||
{
|
|
||||||
protected static string $relationship = 'teamInvitations';
|
|
||||||
|
|
||||||
protected static ?string $title = 'Invitations';
|
|
||||||
|
|
||||||
public function form(Form $form): Form
|
|
||||||
{
|
|
||||||
return $form
|
|
||||||
->schema([
|
|
||||||
TextInput::make('email')
|
|
||||||
->label('Email')
|
|
||||||
->disabledOn(['edit'])
|
|
||||||
->required(),
|
|
||||||
Select::make('role')
|
|
||||||
->options(Role::class)
|
|
||||||
->label('Role')
|
|
||||||
->rules([
|
|
||||||
'required',
|
|
||||||
'string',
|
|
||||||
Rule::enum(Role::class)
|
|
||||||
->except([Role::Owner, Role::Placeholder]),
|
|
||||||
])
|
|
||||||
->required(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->recordTitleAttribute('email')
|
|
||||||
->modelLabel('Invitation')
|
|
||||||
->pluralModelLabel('Invitations')
|
|
||||||
->columns([
|
|
||||||
Tables\Columns\TextColumn::make('email'),
|
|
||||||
Tables\Columns\TextColumn::make('role'),
|
|
||||||
])
|
|
||||||
->headerActions([
|
|
||||||
Tables\Actions\CreateAction::make()
|
|
||||||
->icon('heroicon-s-plus')
|
|
||||||
->using(function (array $data, string $model): Model {
|
|
||||||
/** @var Organization $ownerRecord */
|
|
||||||
$ownerRecord = $this->getOwnerRecord();
|
|
||||||
|
|
||||||
return app(InvitationService::class)
|
|
||||||
->inviteUser($ownerRecord, $data['email'], Role::from($data['role']));
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Action::make('view')
|
|
||||||
->icon('heroicon-o-eye')
|
|
||||||
->color('gray')
|
|
||||||
->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [
|
|
||||||
'record' => $record->getKey(),
|
|
||||||
])),
|
|
||||||
Tables\Actions\EditAction::make(),
|
|
||||||
Tables\Actions\DeleteAction::make(),
|
|
||||||
])
|
|
||||||
->bulkActions([
|
|
||||||
Tables\Actions\BulkActionGroup::make([
|
|
||||||
Tables\Actions\DetachBulkAction::make(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,24 +5,17 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\OrganizationResource\RelationManagers;
|
namespace App\Filament\Resources\OrganizationResource\RelationManagers;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Exceptions\Api\ApiException;
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
use App\Models\Member;
|
|
||||||
use App\Models\Organization;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\BillableRateService;
|
|
||||||
use App\Service\MemberService;
|
|
||||||
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\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Actions\Action;
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Actions\AttachAction;
|
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;
|
|
||||||
|
|
||||||
class UsersRelationManager extends RelationManager
|
class UsersRelationManager extends RelationManager
|
||||||
{
|
{
|
||||||
@@ -43,40 +36,20 @@ class UsersRelationManager extends RelationManager
|
|||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
/** @var Organization $organization */
|
|
||||||
$organization = $this->getOwnerRecord();
|
|
||||||
|
|
||||||
return $table
|
return $table
|
||||||
->recordTitleAttribute('name')
|
->recordTitleAttribute('name')
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('name'),
|
Tables\Columns\TextColumn::make('name'),
|
||||||
Tables\Columns\TextColumn::make('role'),
|
Tables\Columns\TextColumn::make('role'),
|
||||||
TextColumn::make('billable_rate')
|
TextColumn::make('billable_rate')
|
||||||
->money($organization->currency, divideBy: 100),
|
->money($this->getOwnerRecord()->currency ?? 'EUR', divideBy: 100),
|
||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
Tables\Actions\AttachAction::make()
|
Tables\Actions\AttachAction::make()->form(fn (AttachAction $action): array => [
|
||||||
->recordTitle(fn (User $record): string => "{$record->name} ({$record->email})")
|
$action->getRecordSelect(),
|
||||||
->form(fn (AttachAction $action): array => [
|
Select::make('role')
|
||||||
$action->getRecordSelect(),
|
->options(Role::class),
|
||||||
Select::make('role')
|
]),
|
||||||
->required()
|
|
||||||
->options(Role::class)
|
|
||||||
->rule([
|
|
||||||
'required',
|
|
||||||
'string',
|
|
||||||
Rule::enum(Role::class)
|
|
||||||
->except([Role::Owner, Role::Placeholder]),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
->label('Add user')
|
|
||||||
->modalHeading('Add user')
|
|
||||||
->icon('heroicon-s-plus')
|
|
||||||
->using(function (User $record, array $data): void {
|
|
||||||
/** @var Organization $organization */
|
|
||||||
$organization = $this->getOwnerRecord();
|
|
||||||
app(MemberService::class)->addMember($record, $organization, Role::from($data['role']), true);
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
@@ -85,55 +58,13 @@ class UsersRelationManager extends RelationManager
|
|||||||
->url(fn (User $record): string => UserResource::getUrl('view', [
|
->url(fn (User $record): string => UserResource::getUrl('view', [
|
||||||
'record' => $record->getKey(),
|
'record' => $record->getKey(),
|
||||||
])),
|
])),
|
||||||
Tables\Actions\EditAction::make()
|
Tables\Actions\EditAction::make(),
|
||||||
->using(function (User $record, array $data): User {
|
Tables\Actions\DetachAction::make(),
|
||||||
/** @var Organization $organization */
|
|
||||||
$organization = $this->getOwnerRecord();
|
|
||||||
/** @var Member $member */
|
|
||||||
$member = $record->getRelation('membership');
|
|
||||||
|
|
||||||
if ($data['billable_rate'] !== $member->billable_rate) {
|
|
||||||
$member->billable_rate = $data['billable_rate'];
|
|
||||||
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($data['role'] !== $member->role) {
|
|
||||||
try {
|
|
||||||
app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true);
|
|
||||||
} catch (ApiException $exception) {
|
|
||||||
Notification::make()
|
|
||||||
->danger()
|
|
||||||
->title('Update failed')
|
|
||||||
->body($exception->getTranslatedMessage())
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$member->save();
|
|
||||||
|
|
||||||
return $record;
|
|
||||||
}),
|
|
||||||
Tables\Actions\DetachAction::make()
|
|
||||||
->using(function (User $record): void {
|
|
||||||
/** @var Organization $organization */
|
|
||||||
$organization = $this->getOwnerRecord();
|
|
||||||
$member = Member::query()
|
|
||||||
->whereBelongsTo($record, 'user')
|
|
||||||
->whereBelongsTo($organization, 'organization')
|
|
||||||
->firstOrFail();
|
|
||||||
try {
|
|
||||||
app(MemberService::class)->removeMember($member, $organization);
|
|
||||||
} catch (ApiException $exception) {
|
|
||||||
Notification::make()
|
|
||||||
->danger()
|
|
||||||
->title('Delete failed')
|
|
||||||
->body($exception->getTranslatedMessage())
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DetachBulkAction::make(),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class ProjectMemberResource extends Resource
|
|||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
'integer',
|
||||||
'gt:0',
|
'gt:0',
|
||||||
'max:2147483647',
|
|
||||||
])
|
])
|
||||||
->numeric(),
|
->numeric(),
|
||||||
Forms\Components\Select::make('user_id')
|
Forms\Components\Select::make('user_id')
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class EditProjectMember extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
Actions\DeleteAction::make(),
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListProjectMembers extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ class ProjectResource extends Resource
|
|||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
'integer',
|
||||||
'gt:0',
|
'gt:0',
|
||||||
'max:2147483647',
|
|
||||||
])
|
])
|
||||||
->numeric(),
|
->numeric(),
|
||||||
Forms\Components\Select::make('organization_id')
|
Forms\Components\Select::make('organization_id')
|
||||||
@@ -72,13 +71,8 @@ class ProjectResource extends Resource
|
|||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('organization')
|
SelectFilter::make('organization')
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
->relationship('organization', 'name')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
])
|
||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->actions([
|
->actions([
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class EditProject extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
Actions\DeleteAction::make(),
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListProjects extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
|
||||||
|
|
||||||
use App\Filament\Resources\ReportResource\Pages;
|
|
||||||
use App\Models\Report;
|
|
||||||
use App\Service\Dto\ReportPropertiesDto;
|
|
||||||
use Filament\Forms;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Forms\Components\Toggle;
|
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Tables\Actions\Action;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Columns\ToggleColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
|
||||||
|
|
||||||
class ReportResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = Report::class;
|
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-document-chart-bar';
|
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Timetracking';
|
|
||||||
|
|
||||||
protected static ?int $navigationSort = 7;
|
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
|
||||||
{
|
|
||||||
return $form
|
|
||||||
->columns(1)
|
|
||||||
->schema([
|
|
||||||
Forms\Components\TextInput::make('name')
|
|
||||||
->label('Name')
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
Forms\Components\TextInput::make('description')
|
|
||||||
->label('Description')
|
|
||||||
->nullable()
|
|
||||||
->maxLength(255),
|
|
||||||
Toggle::make('is_public')
|
|
||||||
->label('Is public?')
|
|
||||||
->required(),
|
|
||||||
DateTimePicker::make('public_until')
|
|
||||||
->label('Public until')
|
|
||||||
->nullable(),
|
|
||||||
Forms\Components\Select::make('organization_id')
|
|
||||||
->label('Organization')
|
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
|
||||||
->searchable(['name'])
|
|
||||||
->disabled()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\TextInput::make('share_secret')
|
|
||||||
->label('Share Secret')
|
|
||||||
->nullable(),
|
|
||||||
PrettyJsonField::make('properties')
|
|
||||||
->formatStateUsing(function (ReportPropertiesDto $state, Report $record): string {
|
|
||||||
return $record->getRawOriginal('properties');
|
|
||||||
})
|
|
||||||
->disabled(),
|
|
||||||
Forms\Components\DateTimePicker::make('created_at')
|
|
||||||
->label('Created At')
|
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
|
||||||
Forms\Components\DateTimePicker::make('updated_at')
|
|
||||||
->label('Updated At')
|
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
Tables\Columns\TextColumn::make('name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('description')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
ToggleColumn::make('is_public')
|
|
||||||
->label('Is public?')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('organization.name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('created_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('updated_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
|
||||||
->defaultSort('created_at', 'desc')
|
|
||||||
->filters([
|
|
||||||
SelectFilter::make('organization')
|
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
|
||||||
->searchable(),
|
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Action::make('public-view')
|
|
||||||
->label('Public')
|
|
||||||
->icon('heroicon-o-eye')
|
|
||||||
->color('gray')
|
|
||||||
->hidden(fn (Report $record): bool => $record->getShareableLink() === null)
|
|
||||||
->url(fn (Report $record): string => $record->getShareableLink(), true),
|
|
||||||
Tables\Actions\ViewAction::make(),
|
|
||||||
Tables\Actions\EditAction::make(),
|
|
||||||
Tables\Actions\DeleteAction::make(),
|
|
||||||
])
|
|
||||||
->bulkActions([
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => Pages\ListReports::route('/'),
|
|
||||||
'edit' => Pages\EditReport::route('/{record}/edit'),
|
|
||||||
'view' => Pages\ViewReport::route('/{record}'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\ReportResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\ReportResource;
|
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditReport extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = ReportResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
Actions\DeleteAction::make()
|
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\ReportResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\ReportResource;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListReports extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = ReportResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\ReportResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\ReportResource;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
|
||||||
|
|
||||||
class ViewReport extends ViewRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = ReportResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
EditAction::make('edit')
|
|
||||||
->icon('heroicon-s-pencil'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,13 +60,8 @@ class TagResource extends Resource
|
|||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('organization')
|
SelectFilter::make('organization')
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
->relationship('organization', 'name')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class EditTag extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
Actions\DeleteAction::make(),
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListTags extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,8 @@ class TaskResource extends Resource
|
|||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('organization')
|
SelectFilter::make('organization')
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
->relationship('organization', 'name')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
])
|
||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->actions([
|
->actions([
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class EditTask extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
Actions\DeleteAction::make(),
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListTasks extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,13 +92,8 @@ class TimeEntryResource extends Resource
|
|||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('organization')
|
SelectFilter::make('organization')
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
->relationship('organization', 'name')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
])
|
||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->actions([
|
->actions([
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class EditTimeEntry extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
Actions\DeleteAction::make(),
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListTimeEntries extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
|
||||||
|
|
||||||
use App\Filament\Resources\TokenResource\Pages;
|
|
||||||
use App\Models\Passport\Client;
|
|
||||||
use App\Models\Passport\Token;
|
|
||||||
use Filament\Forms;
|
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Tables\Filters\TernaryFilter;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
|
|
||||||
class TokenResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = Token::class;
|
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-key';
|
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Auth';
|
|
||||||
|
|
||||||
protected static ?int $navigationSort = 6;
|
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
|
||||||
{
|
|
||||||
return $form
|
|
||||||
->columns(1)
|
|
||||||
->schema([
|
|
||||||
Forms\Components\TextInput::make('id')
|
|
||||||
->label('ID')
|
|
||||||
->disabled()
|
|
||||||
->visibleOn(['update', 'show'])
|
|
||||||
->readOnly()
|
|
||||||
->maxLength(255),
|
|
||||||
Forms\Components\TextInput::make('name')
|
|
||||||
->label('Name')
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
Forms\Components\Select::make('user_id')
|
|
||||||
->label('User')
|
|
||||||
->relationship(name: 'user', titleAttribute: 'name')
|
|
||||||
->searchable(['name'])
|
|
||||||
->disabled()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('client_id')
|
|
||||||
->label('Client')
|
|
||||||
->relationship(name: 'client', titleAttribute: 'name')
|
|
||||||
->searchable(['name'])
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Toggle::make('revoked')
|
|
||||||
->label('Revoked')
|
|
||||||
->required(),
|
|
||||||
Forms\Components\DateTimePicker::make('expires_at')
|
|
||||||
->label('Expires At')
|
|
||||||
->disabled(),
|
|
||||||
Forms\Components\DateTimePicker::make('created_at')
|
|
||||||
->label('Created At')
|
|
||||||
->disabled(),
|
|
||||||
Forms\Components\DateTimePicker::make('updated_at')
|
|
||||||
->label('Updated At')
|
|
||||||
->disabled(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
Tables\Columns\TextColumn::make('name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('user.name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('client.name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\IconColumn::make('client.personal_access_client')
|
|
||||||
->boolean()
|
|
||||||
->label('API token?')
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\IconColumn::make('revoked')
|
|
||||||
->boolean()
|
|
||||||
->label('Revoked?')
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('expires_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('created_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\TextColumn::make('updated_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
|
||||||
->defaultSort('created_at', 'desc')
|
|
||||||
->filters([
|
|
||||||
TernaryFilter::make('is_personal_access_client')
|
|
||||||
->queries(
|
|
||||||
true: function (Builder $query) {
|
|
||||||
/** @var Builder<Token> $query */
|
|
||||||
return $query->whereHas('client', function (Builder $query) {
|
|
||||||
/** @var Builder<Client> $query */
|
|
||||||
return $query->where('personal_access_client', true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
false: function (Builder $query) {
|
|
||||||
/** @var Builder<Token> $query */
|
|
||||||
return $query->whereHas('client', function (Builder $query) {
|
|
||||||
/** @var Builder<Client> $query */
|
|
||||||
return $query->where('personal_access_client', false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
blank: function (Builder $query) {
|
|
||||||
/** @var Builder<Token> $query */
|
|
||||||
return $query;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
->label('API token?'),
|
|
||||||
TernaryFilter::make('revoked')
|
|
||||||
->label('Revoked?'),
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Tables\Actions\ViewAction::make(),
|
|
||||||
])
|
|
||||||
->bulkActions([
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => Pages\ListTokens::route('/'),
|
|
||||||
'view' => Pages\ViewToken::route('/{record}'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TokenResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\TokenResource;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListTokens extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = TokenResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TokenResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\TokenResource;
|
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
|
||||||
|
|
||||||
class ViewToken extends ViewRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = TokenResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,28 +5,21 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Exceptions\Api\ApiException;
|
|
||||||
use App\Filament\Resources\UserResource\Pages;
|
use App\Filament\Resources\UserResource\Pages;
|
||||||
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\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use Brick\Money\ISOCurrencyProvider;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
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;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
|
||||||
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
|
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
|
||||||
|
|
||||||
class UserResource extends Resource
|
class UserResource extends Resource
|
||||||
@@ -41,9 +34,6 @@ class UserResource extends Resource
|
|||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
/** @var User|null $record */
|
|
||||||
$record = $form->getRecord();
|
|
||||||
|
|
||||||
return $form
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->schema([
|
->schema([
|
||||||
@@ -60,25 +50,12 @@ class UserResource extends Resource
|
|||||||
Forms\Components\TextInput::make('email')
|
Forms\Components\TextInput::make('email')
|
||||||
->label('Email')
|
->label('Email')
|
||||||
->required()
|
->required()
|
||||||
->rules($record?->is_placeholder ? [] : [
|
|
||||||
UniqueEloquent::make(User::class, 'email')
|
|
||||||
->ignore($record?->getKey()),
|
|
||||||
])
|
|
||||||
->rule([
|
|
||||||
'email',
|
|
||||||
])
|
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\Toggle::make('is_placeholder')
|
Forms\Components\Toggle::make('is_placeholder')
|
||||||
->label('Is Placeholder?')
|
->label('Is Placeholder'),
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabledOn(['edit']),
|
|
||||||
Forms\Components\DateTimePicker::make('email_verified_at')
|
Forms\Components\DateTimePicker::make('email_verified_at')
|
||||||
->label('Email Verified At')
|
->label('Email Verified At')
|
||||||
->hiddenOn(['create'])
|
|
||||||
->nullable(),
|
->nullable(),
|
||||||
Forms\Components\Toggle::make('is_email_verified')
|
|
||||||
->label('Email Verified?')
|
|
||||||
->visibleOn(['create']),
|
|
||||||
Forms\Components\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())
|
||||||
@@ -90,39 +67,15 @@ class UserResource extends Resource
|
|||||||
->required(),
|
->required(),
|
||||||
TextInput::make('password')
|
TextInput::make('password')
|
||||||
->password()
|
->password()
|
||||||
->label('Password')
|
|
||||||
->dehydrateStateUsing(fn ($state) => Hash::make($state))
|
->dehydrateStateUsing(fn ($state) => Hash::make($state))
|
||||||
->dehydrated(fn ($state) => filled($state))
|
->dehydrated(fn ($state) => filled($state))
|
||||||
->hiddenOn(['create'])
|
|
||||||
->required(fn (string $context): bool => $context === 'create')
|
->required(fn (string $context): bool => $context === 'create')
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('password_create')
|
|
||||||
->password()
|
|
||||||
->label('Password')
|
|
||||||
->visibleOn(['create'])
|
|
||||||
->required(fn (string $context): bool => $context === 'create')
|
|
||||||
->maxLength(255),
|
|
||||||
Forms\Components\Select::make('currency')
|
|
||||||
->label('Currency (Personal Organization)')
|
|
||||||
->options(function (): array {
|
|
||||||
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
|
||||||
$select = [];
|
|
||||||
foreach ($currencies as $currency) {
|
|
||||||
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $select;
|
|
||||||
})
|
|
||||||
->required()
|
|
||||||
->visibleOn(['create'])
|
|
||||||
->searchable(),
|
|
||||||
Forms\Components\DateTimePicker::make('created_at')
|
Forms\Components\DateTimePicker::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
->disabled(),
|
||||||
Forms\Components\DateTimePicker::make('updated_at')
|
Forms\Components\DateTimePicker::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
->disabled(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -192,30 +145,11 @@ class UserResource extends Resource
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Tables\Actions\EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
Tables\Actions\DeleteAction::make()
|
|
||||||
->hidden(fn (User $record) => $record->is(Auth::user()))
|
|
||||||
->using(function (User $record): void {
|
|
||||||
try {
|
|
||||||
app(DeletionService::class)->deleteUser($record);
|
|
||||||
} catch (ApiException $exception) {
|
|
||||||
Notification::make()
|
|
||||||
->danger()
|
|
||||||
->title('Delete failed')
|
|
||||||
->body($exception->getTranslatedMessage())
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkAction::make('Resend verification email')
|
Tables\Actions\BulkActionGroup::make([
|
||||||
->icon('heroicon-o-paper-airplane')
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
->action(function (Collection $records): void {
|
]),
|
||||||
foreach ($records as $user) {
|
|
||||||
/** @var User $user */
|
|
||||||
$user->sendEmailVerificationNotification();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,29 +4,24 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Enums\Weekday;
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\UserService;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
class CreateUser extends CreateRecord
|
class CreateUser extends CreateRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = UserResource::class;
|
protected static string $resource = UserResource::class;
|
||||||
|
|
||||||
protected function handleRecordCreation(array $data): User
|
protected function afterCreate(): void
|
||||||
{
|
{
|
||||||
$userService = app(UserService::class);
|
/** @var User $user */
|
||||||
$user = $userService->createUser(
|
$user = $this->record;
|
||||||
$data['name'],
|
|
||||||
$data['email'],
|
|
||||||
$data['password_create'],
|
|
||||||
$data['timezone'],
|
|
||||||
Weekday::from($data['week_start']),
|
|
||||||
$data['currency'],
|
|
||||||
verifyEmail: (bool) $data['is_email_verified']
|
|
||||||
);
|
|
||||||
|
|
||||||
return $user;
|
$user->ownedTeams()->save(Organization::forceCreate([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => explode(' ', $user->name, 2)[0]."'s Organization",
|
||||||
|
'personal_team' => true,
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class ListUsers extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\CreateAction::make(),
|
||||||
->icon('heroicon-s-plus'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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\Pages\Actions\Impersonate;
|
|
||||||
|
|
||||||
class ViewUser extends ViewRecord
|
class ViewUser extends ViewRecord
|
||||||
{
|
{
|
||||||
@@ -16,7 +15,6 @@ class ViewUser extends ViewRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Impersonate::make()->record($this->getRecord()),
|
|
||||||
EditAction::make('edit')
|
EditAction::make('edit')
|
||||||
->icon('heroicon-s-pencil'),
|
->icon('heroicon-s-pencil'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,18 +5,15 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\UserResource\RelationManagers;
|
namespace App\Filament\Resources\UserResource\RelationManagers;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Exceptions\Api\ApiException;
|
|
||||||
use App\Filament\Resources\OrganizationResource;
|
use App\Filament\Resources\OrganizationResource;
|
||||||
use App\Models\Member;
|
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
|
||||||
use App\Service\MemberService;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Actions\Action;
|
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;
|
||||||
|
|
||||||
@@ -30,6 +27,10 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
->schema([
|
->schema([
|
||||||
Select::make('role')
|
Select::make('role')
|
||||||
->options(Role::class),
|
->options(Role::class),
|
||||||
|
TextInput::make('billable_rate')
|
||||||
|
->label('Billable rate (in Cents)')
|
||||||
|
->nullable()
|
||||||
|
->numeric(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,11 +41,15 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name'),
|
TextColumn::make('name'),
|
||||||
TextColumn::make('role'),
|
TextColumn::make('role'),
|
||||||
TextColumn::make('membership.billable_rate')
|
TextColumn::make('billable_rate')
|
||||||
->label('Billable rate')
|
->money(fn (Organization $resource) => $resource->currency ?? 'EUR', divideBy: 100),
|
||||||
->money(fn (Organization $resource) => $resource->currency, divideBy: 100),
|
|
||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
|
Tables\Actions\AttachAction::make()->form(fn (AttachAction $action): array => [
|
||||||
|
$action->getRecordSelect(),
|
||||||
|
Select::make('role')
|
||||||
|
->options(Role::class),
|
||||||
|
]),
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
@@ -53,48 +58,13 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [
|
->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [
|
||||||
'record' => $record->getKey(),
|
'record' => $record->getKey(),
|
||||||
])),
|
])),
|
||||||
Tables\Actions\EditAction::make()
|
Tables\Actions\EditAction::make(),
|
||||||
->using(function (Organization $record, array $data): Organization {
|
Tables\Actions\DetachAction::make(),
|
||||||
/** @var Member $member */
|
|
||||||
$member = $record->getRelation('membership');
|
|
||||||
|
|
||||||
if ($data['role'] !== $member->role) {
|
|
||||||
try {
|
|
||||||
app(MemberService::class)->changeRole($member, $record, Role::from($data['role']), true);
|
|
||||||
} catch (ApiException $exception) {
|
|
||||||
Notification::make()
|
|
||||||
->danger()
|
|
||||||
->title('Update failed')
|
|
||||||
->body($exception->getTranslatedMessage())
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$member->save();
|
|
||||||
|
|
||||||
return $record;
|
|
||||||
}),
|
|
||||||
Tables\Actions\DetachAction::make()
|
|
||||||
->using(function (Organization $record): void {
|
|
||||||
/** @var User $user */
|
|
||||||
$user = $this->getOwnerRecord();
|
|
||||||
$member = Member::query()
|
|
||||||
->whereBelongsTo($user, 'user')
|
|
||||||
->whereBelongsTo($record, 'organization')
|
|
||||||
->firstOrFail();
|
|
||||||
try {
|
|
||||||
app(MemberService::class)->removeMember($member, $record);
|
|
||||||
} catch (ApiException $exception) {
|
|
||||||
Notification::make()
|
|
||||||
->danger()
|
|
||||||
->title('Delete failed')
|
|
||||||
->body($exception->getTranslatedMessage())
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DetachBulkAction::make(),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class ActiveUserOverview extends BaseWidget
|
|||||||
{
|
{
|
||||||
protected static ?int $sort = 1;
|
protected static ?int $sort = 1;
|
||||||
|
|
||||||
protected ?string $heading = 'A Registrations';
|
protected static ?string $heading = 'A Registrations';
|
||||||
|
|
||||||
protected function getCards(): array
|
protected function getCards(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Widgets;
|
|
||||||
|
|
||||||
use Filament\Widgets\Widget;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
class ServerOverview extends Widget
|
|
||||||
{
|
|
||||||
protected static string $view = 'filament.widgets.server-overview';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
protected function getViewData(): array
|
|
||||||
{
|
|
||||||
/** @var string|null $currentVersion */
|
|
||||||
$currentVersion = config('app.version');
|
|
||||||
/** @var string|null $build */
|
|
||||||
$build = config('app.build');
|
|
||||||
$latestVersion = Cache::get('latest_version', null);
|
|
||||||
|
|
||||||
$needsUpdate = false;
|
|
||||||
if ($latestVersion !== null && $currentVersion !== null && version_compare($latestVersion, $currentVersion) > 0) {
|
|
||||||
$needsUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'version' => $currentVersion,
|
|
||||||
'build' => $build,
|
|
||||||
'environment' => config('app.env'),
|
|
||||||
'currentVersion' => $latestVersion,
|
|
||||||
'needsUpdate' => $needsUpdate,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
|
||||||
|
|
||||||
use App\Exceptions\Api\PersonalAccessClientIsNotConfiguredException;
|
|
||||||
use App\Http\Requests\V1\ApiToken\ApiTokenStoreRequest;
|
|
||||||
use App\Http\Resources\V1\ApiToken\ApiTokenCollection;
|
|
||||||
use App\Http\Resources\V1\ApiToken\ApiTokenWithAccessTokenResource;
|
|
||||||
use App\Models\Passport\Token;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
|
|
||||||
class ApiTokenController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* List all api token of the currently authenticated user
|
|
||||||
*
|
|
||||||
* This endpoint is independent of organization.
|
|
||||||
*
|
|
||||||
* @operationId getApiTokens
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*/
|
|
||||||
public function index(): ApiTokenCollection
|
|
||||||
{
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$tokens = $user->tokens()
|
|
||||||
->where('client_id', '=', config('passport.personal_access_client.id'))
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return new ApiTokenCollection($tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new api token for the currently authenticated user
|
|
||||||
*
|
|
||||||
* The response will contain the access token that can be used to send authenticated API requests.
|
|
||||||
* Please note that the access token is only shown in this response and cannot be retrieved later.
|
|
||||||
*
|
|
||||||
* @operationId createApiToken
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException|PersonalAccessClientIsNotConfiguredException
|
|
||||||
*/
|
|
||||||
public function store(ApiTokenStoreRequest $request): ApiTokenWithAccessTokenResource
|
|
||||||
{
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
|
||||||
throw new PersonalAccessClientIsNotConfiguredException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $user->createToken($request->getName(), ['*']);
|
|
||||||
/** @var Token $tokenModel */
|
|
||||||
$tokenModel = $token->token;
|
|
||||||
|
|
||||||
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Revoke an api token
|
|
||||||
*
|
|
||||||
* @operationId revokeApiToken
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
|
||||||
* @throws PersonalAccessClientIsNotConfiguredException
|
|
||||||
*/
|
|
||||||
public function revoke(Token $apiToken): JsonResponse
|
|
||||||
{
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
|
||||||
throw new PersonalAccessClientIsNotConfiguredException;
|
|
||||||
}
|
|
||||||
if ($apiToken->user_id !== $user->getKey()) {
|
|
||||||
throw new AuthorizationException('API token does not belong to user');
|
|
||||||
}
|
|
||||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
|
||||||
throw new AuthorizationException('API token is not a personal access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
$apiToken->revoke();
|
|
||||||
|
|
||||||
return response()->json(null, 204);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an api token
|
|
||||||
*
|
|
||||||
* @operationId deleteApiToken
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException|PersonalAccessClientIsNotConfiguredException
|
|
||||||
*/
|
|
||||||
public function destroy(Token $apiToken): JsonResponse
|
|
||||||
{
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
|
||||||
throw new PersonalAccessClientIsNotConfiguredException;
|
|
||||||
}
|
|
||||||
if ($apiToken->user_id !== $user->getKey()) {
|
|
||||||
throw new AuthorizationException('API token does not belong to user');
|
|
||||||
}
|
|
||||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
|
||||||
throw new AuthorizationException('API token is not a personal access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
$apiToken->delete();
|
|
||||||
|
|
||||||
return response()->json(null, 204);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Models\Organization;
|
|
||||||
use App\Service\DashboardService;
|
|
||||||
use App\Service\PermissionStore;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
|
|
||||||
class ChartController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId weeklyProjectOverview
|
|
||||||
*
|
|
||||||
* @response array<int, array{value: int, name: string, color: string}>
|
|
||||||
*/
|
|
||||||
public function weeklyProjectOverview(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$weeklyProjectOverview = $dashboardService->weeklyProjectOverview($user, $organization);
|
|
||||||
|
|
||||||
return response()->json($weeklyProjectOverview);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId latestTasks
|
|
||||||
*
|
|
||||||
* @response array<int, array{task_id: string, name: string, description: string|null, status: bool, time_entry_id: string|null}>
|
|
||||||
*/
|
|
||||||
public function latestTasks(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$latestTasks = $dashboardService->latestTasks($user, $organization);
|
|
||||||
|
|
||||||
return response()->json($latestTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId lastSevenDays
|
|
||||||
*
|
|
||||||
* @response array<int, array{ date: string, duration: int, history: array<int> }>
|
|
||||||
*/
|
|
||||||
public function lastSevenDays(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$lastSevenDays = $dashboardService->lastSevenDays($user, $organization);
|
|
||||||
|
|
||||||
return response()->json($lastSevenDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId latestTeamActivity
|
|
||||||
*
|
|
||||||
* @response array<int, array{member_id: string, name: string, description: string|null, time_entry_id: string, task_id: string|null, status: bool }>
|
|
||||||
*/
|
|
||||||
public function latestTeamActivity(Organization $organization, DashboardService $dashboardService, PermissionStore $permissionStore): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:all');
|
|
||||||
|
|
||||||
$latestTeamActivity = $dashboardService->latestTeamActivity($organization);
|
|
||||||
|
|
||||||
return response()->json($latestTeamActivity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId dailyTrackedHours
|
|
||||||
*
|
|
||||||
* @response array<int, array{date: string, duration: int}>
|
|
||||||
*/
|
|
||||||
public function dailyTrackedHours(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 60);
|
|
||||||
|
|
||||||
return response()->json($dailyTrackedHours);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId totalWeeklyTime
|
|
||||||
*
|
|
||||||
* @response int
|
|
||||||
*/
|
|
||||||
public function totalWeeklyTime(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$totalWeeklyTime = $dashboardService->totalWeeklyTime($user, $organization);
|
|
||||||
|
|
||||||
return response()->json($totalWeeklyTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId totalWeeklyBillableTime
|
|
||||||
*
|
|
||||||
* @response int
|
|
||||||
*/
|
|
||||||
public function totalWeeklyBillableTime(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$totalWeeklyBillableTime = $dashboardService->totalWeeklyBillableTime($user, $organization);
|
|
||||||
|
|
||||||
return response()->json($totalWeeklyBillableTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId totalWeeklyBillableAmount
|
|
||||||
*
|
|
||||||
* @response array{value: int, currency: string}
|
|
||||||
*/
|
|
||||||
public function totalWeeklyBillableAmount(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
|
||||||
if (! $showBillableRate) {
|
|
||||||
throw new AuthorizationException('You do not have permission to view billable rates.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalWeeklyBillableAmount = $dashboardService->totalWeeklyBillableAmount($user, $organization);
|
|
||||||
|
|
||||||
return response()->json($totalWeeklyBillableAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*
|
|
||||||
* @operationId weeklyHistory
|
|
||||||
*
|
|
||||||
* @response array<int, array{date: string, duration: int}>
|
|
||||||
*/
|
|
||||||
public function weeklyHistory(Organization $organization, DashboardService $dashboardService): JsonResponse
|
|
||||||
{
|
|
||||||
$this->checkPermission($organization, 'charts:view:own');
|
|
||||||
$user = $this->user();
|
|
||||||
|
|
||||||
$weeklyHistory = $dashboardService->getWeeklyHistory($user, $organization);
|
|
||||||
|
|
||||||
return response()->json($weeklyHistory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user