mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-13 16:23:18 +01:00
Compare commits
1 Commits
feature/po
...
47327aa52b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47327aa52b |
1
.env.ci
1
.env.ci
@@ -6,7 +6,6 @@ APP_DEBUG=true
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
APP_FORCE_HTTPS=false
|
APP_FORCE_HTTPS=false
|
||||||
APP_ENABLE_REGISTRATION=true
|
APP_ENABLE_REGISTRATION=true
|
||||||
SUPER_ADMINS=admin@example.com
|
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
|
|||||||
23
.github/workflows/build-onpremise.yml
vendored
23
.github/workflows/build-onpremise.yml
vendored
@@ -103,8 +103,6 @@ jobs:
|
|||||||
{
|
{
|
||||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||||
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
|
|
||||||
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: "Checkout invoicing extension"
|
- name: "Checkout invoicing extension"
|
||||||
@@ -121,26 +119,9 @@ jobs:
|
|||||||
- name: "Install npm dependencies in invoicing extension"
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
run: cd extensions/Invoicing && npm ci
|
run: cd extensions/Invoicing && npm ci
|
||||||
|
|
||||||
- name: "Checkout auditing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
|
|
||||||
path: extensions/Auditing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in auditing extension"
|
|
||||||
run: cd extensions/Auditing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in auditing extension"
|
|
||||||
run: cd extensions/Auditing && npm ci
|
|
||||||
|
|
||||||
- name: "Activate invoicing extension"
|
- name: "Activate invoicing extension"
|
||||||
run: php artisan module:enable Invoicing
|
run: php artisan module:enable Invoicing
|
||||||
|
|
||||||
- name: "Activate auditing extension"
|
|
||||||
run: php artisan module:enable Auditing
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -160,7 +141,7 @@ jobs:
|
|||||||
${{ env.DOCKER_REPO }}
|
${{ env.DOCKER_REPO }}
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
@@ -214,7 +195,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
|
|||||||
28
.github/workflows/build-private.yml
vendored
28
.github/workflows/build-private.yml
vendored
@@ -84,8 +84,6 @@ jobs:
|
|||||||
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
||||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||||
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
|
|
||||||
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: "Checkout billing extension"
|
- name: "Checkout billing extension"
|
||||||
@@ -99,7 +97,7 @@ jobs:
|
|||||||
- name: "Install dependencies in billing extension"
|
- name: "Install dependencies in billing extension"
|
||||||
uses: php-actions/composer@v6
|
uses: php-actions/composer@v6
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "${{ secrets.LARAVEL_SPARK_USERNAME }}", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "gregor@vostrak.at", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
||||||
with:
|
with:
|
||||||
working_dir: "extensions/Billing"
|
working_dir: "extensions/Billing"
|
||||||
command: install
|
command: install
|
||||||
@@ -147,25 +145,6 @@ jobs:
|
|||||||
- name: "Install npm dependencies in invoicing extension"
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
run: cd extensions/Invoicing && npm ci
|
run: cd extensions/Invoicing && npm ci
|
||||||
|
|
||||||
- name: "Checkout auditing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
|
|
||||||
path: extensions/Auditing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in auditing extension"
|
|
||||||
uses: php-actions/composer@v6
|
|
||||||
with:
|
|
||||||
working_dir: "extensions/Auditing"
|
|
||||||
command: install
|
|
||||||
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
|
||||||
php_version: 8.3
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in auditing extension"
|
|
||||||
run: cd extensions/Auditing && npm ci
|
|
||||||
|
|
||||||
- name: "Setup PHP with PECL extension"
|
- name: "Setup PHP with PECL extension"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -189,9 +168,6 @@ jobs:
|
|||||||
- name: "Activate invoicing extension"
|
- name: "Activate invoicing extension"
|
||||||
run: php artisan module:enable Invoicing
|
run: php artisan module:enable Invoicing
|
||||||
|
|
||||||
- name: "Activate auditing extension"
|
|
||||||
run: php artisan module:enable Auditing
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -201,7 +177,7 @@ jobs:
|
|||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: rg.fr-par.scw.cloud/solidtime
|
registry: rg.fr-par.scw.cloud/solidtime
|
||||||
username: nologin
|
username: nologin
|
||||||
|
|||||||
8
.github/workflows/build-public.yml
vendored
8
.github/workflows/build-public.yml
vendored
@@ -117,13 +117,13 @@ jobs:
|
|||||||
${{ env.GHCR_REPO }}
|
${{ env.GHCR_REPO }}
|
||||||
|
|
||||||
- name: "Login to Docker Hub Container Registry"
|
- name: "Login to Docker Hub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -177,13 +177,13 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to Docker Hub"
|
- name: "Login to Docker Hub"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GHCR"
|
- name: "Login to GHCR"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
31
.github/workflows/extension-manifest-check.yml
vendored
31
.github/workflows/extension-manifest-check.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: Extension Manifest Check
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- extensions/manifest.json
|
|
||||||
- .github/workflows/extension-manifest-check.yml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
check-refs:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- name: "Checkout code"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
# Extension refs must be pinned to a release tag (vX.Y.Z) or a full commit
|
|
||||||
# SHA. Branch names (main, feature/foo, ...) are only acceptable while
|
|
||||||
# developing and must be replaced before the pull request is merged.
|
|
||||||
- name: "Check extension refs are pinned"
|
|
||||||
run: |
|
|
||||||
failed=0
|
|
||||||
while IFS=$'\t' read -r name ref; do
|
|
||||||
if [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.]+)?$ ]] || [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then
|
|
||||||
echo "✅ $name: $ref"
|
|
||||||
else
|
|
||||||
echo "::error file=extensions/manifest.json::Extension \"$name\" uses ref \"$ref\", which is not a release tag or commit SHA. Pin it before merging."
|
|
||||||
failed=1
|
|
||||||
fi
|
|
||||||
done < <(jq -r 'to_entries[] | [.key, .value.ref] | @tsv' extensions/manifest.json)
|
|
||||||
exit $failed
|
|
||||||
2
.github/workflows/npm-format-check.yml
vendored
2
.github/workflows/npm-format-check.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: NPM Format Check
|
name: NPM Format Check
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format-check:
|
format-check:
|
||||||
|
|||||||
136
.github/workflows/phpunit-extensions.yml
vendored
136
.github/workflows/phpunit-extensions.yml
vendored
@@ -1,136 +0,0 @@
|
|||||||
name: PHPUnit Tests - Extensions
|
|
||||||
on: push
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
phpunit-extensions:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
postgres_version: [ 15, 16, 17, 18 ]
|
|
||||||
|
|
||||||
services:
|
|
||||||
pgsql_test:
|
|
||||||
image: postgres:${{ matrix.postgres_version }}
|
|
||||||
env:
|
|
||||||
PGPASSWORD: 'root'
|
|
||||||
POSTGRES_DB: 'laravel'
|
|
||||||
POSTGRES_USER: 'root'
|
|
||||||
POSTGRES_PASSWORD: 'root'
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
gotenberg:
|
|
||||||
image: gotenberg/gotenberg:8
|
|
||||||
ports:
|
|
||||||
- 3000:3000
|
|
||||||
options: >-
|
|
||||||
--health-cmd "curl --silent --fail http://localhost:3000/health"
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
steps:
|
|
||||||
- name: "Checkout code"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: "Setup PHP"
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
|
||||||
coverage: pcov
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '20.x'
|
|
||||||
|
|
||||||
- name: "Read extension manifest"
|
|
||||||
id: extension-manifest
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "billing_repository=$(jq -r '.Billing.repository' extensions/manifest.json)"
|
|
||||||
echo "billing_ref=$(jq -r '.Billing.ref' extensions/manifest.json)"
|
|
||||||
echo "services_repository=$(jq -r '.Services.repository' extensions/manifest.json)"
|
|
||||||
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
|
||||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
|
||||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: "Checkout billing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.billing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
|
|
||||||
path: extensions/Billing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in billing extension"
|
|
||||||
working-directory: extensions/Billing
|
|
||||||
env:
|
|
||||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "contact@solidtime.io", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in billing extension"
|
|
||||||
run: cd extensions/Billing && npm ci
|
|
||||||
|
|
||||||
- name: "Checkout services extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.services_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.services_ref }}
|
|
||||||
path: extensions/Services
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in services extension"
|
|
||||||
working-directory: extensions/Services
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in services extension"
|
|
||||||
run: cd extensions/Services && npm ci
|
|
||||||
|
|
||||||
- name: "Checkout invoicing extension"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
|
|
||||||
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
|
|
||||||
path: extensions/Invoicing
|
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
|
||||||
|
|
||||||
- name: "Install composer dependencies in invoicing extension"
|
|
||||||
working-directory: extensions/Invoicing
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in invoicing extension"
|
|
||||||
run: cd extensions/Invoicing && npm ci
|
|
||||||
|
|
||||||
- name: "Run composer install"
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Activate billing extension"
|
|
||||||
run: php artisan module:enable Billing
|
|
||||||
|
|
||||||
- name: "Activate services extension"
|
|
||||||
run: php artisan module:enable Services
|
|
||||||
|
|
||||||
- name: "Activate invoicing extension"
|
|
||||||
run: php artisan module:enable Invoicing
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: "Build Frontend"
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: "Prepare Laravel Application"
|
|
||||||
run: |
|
|
||||||
cp .env.ci .env
|
|
||||||
php artisan key:generate
|
|
||||||
php artisan passport:keys
|
|
||||||
|
|
||||||
- name: "Run PHPUnit"
|
|
||||||
run: php artisan test extensions/Billing/tests extensions/Services/tests extensions/Invoicing/tests --stop-on-failure
|
|
||||||
4
.github/workflows/phpunit.yml
vendored
4
.github/workflows/phpunit.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
postgres_version: [ 15, 16, 17, 18 ]
|
postgres_version: [ 15, 16, 17 ]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
pgsql_test:
|
pgsql_test:
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
||||||
|
|
||||||
- name: "Upload coverage reports to Codecov"
|
- name: "Upload coverage reports to Codecov"
|
||||||
uses: codecov/codecov-action@v7.1.1
|
uses: codecov/codecov-action@v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: solidtime-io/solidtime
|
slug: solidtime-io/solidtime
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,6 @@ dist
|
|||||||
/public/storage
|
/public/storage
|
||||||
/public/css
|
/public/css
|
||||||
/public/js
|
/public/js
|
||||||
/public/fonts/filament
|
|
||||||
/public/vendor
|
/public/vendor
|
||||||
/lang/vendor
|
/lang/vendor
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
@@ -44,5 +43,4 @@ yarn-error.log
|
|||||||
/data
|
/data
|
||||||
/config/caddy
|
/config/caddy
|
||||||
/config/composer
|
/config/composer
|
||||||
/config/psysh
|
|
||||||
/AGENTS.md
|
/AGENTS.md
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource\Pages\CreateAudit;
|
||||||
|
use App\Filament\Resources\AuditResource\Pages\ListAudits;
|
||||||
|
use App\Filament\Resources\AuditResource\Pages\ViewAudit;
|
||||||
|
use App\Models\Audit;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||||
|
|
||||||
|
class AuditResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Audit::class;
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-archive-box';
|
||||||
|
|
||||||
|
protected static string|\UnitEnum|null $navigationGroup = 'System';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('user_type')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('user_id'),
|
||||||
|
TextInput::make('event')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('auditable_type')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('auditable_id')
|
||||||
|
->required(),
|
||||||
|
PrettyJsonField::make('old_values'),
|
||||||
|
PrettyJsonField::make('new_values'),
|
||||||
|
Textarea::make('url'),
|
||||||
|
TextInput::make('ip_address'),
|
||||||
|
TextInput::make('user_agent')
|
||||||
|
->maxLength(1023),
|
||||||
|
TextInput::make('tags')
|
||||||
|
->maxLength(255),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('user.name'),
|
||||||
|
TextColumn::make('event'),
|
||||||
|
TextColumn::make('auditable_type'),
|
||||||
|
TextColumn::make('auditable_id'),
|
||||||
|
IconColumn::make('was_command')
|
||||||
|
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
|
||||||
|
->boolean(),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
TextColumn::make('updated_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
ViewAction::make(),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListAudits::route('/'),
|
||||||
|
'create' => CreateAudit::route('/create'),
|
||||||
|
'view' => ViewAudit::route('/{record}'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateAudit extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
}
|
||||||
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal file
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListAudits extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
|
|
||||||
|
class ViewAudit extends ViewRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ use App\Service\Import\Importers\ImportException;
|
|||||||
use App\Service\Import\Importers\ReportDto;
|
use App\Service\Import\Importers\ReportDto;
|
||||||
use App\Service\Import\ImportService;
|
use App\Service\Import\ImportService;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
@@ -40,7 +40,6 @@ use Filament\Tables\Columns\IconColumn;
|
|||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use League\Flysystem\UnableToReadFile;
|
|
||||||
|
|
||||||
class OrganizationResource extends Resource
|
class OrganizationResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -89,7 +88,7 @@ class OrganizationResource extends Resource
|
|||||||
Select::make('currency')
|
Select::make('currency')
|
||||||
->label('Currency')
|
->label('Currency')
|
||||||
->options(function (): array {
|
->options(function (): array {
|
||||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||||
$select = [];
|
$select = [];
|
||||||
foreach ($currencies as $currency) {
|
foreach ($currencies as $currency) {
|
||||||
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
||||||
@@ -182,13 +181,9 @@ class OrganizationResource extends Resource
|
|||||||
->icon('heroicon-o-inbox-arrow-down')
|
->icon('heroicon-o-inbox-arrow-down')
|
||||||
->action(function (Organization $record, array $data): void {
|
->action(function (Organization $record, array $data): void {
|
||||||
try {
|
try {
|
||||||
try {
|
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
||||||
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
|
||||||
} catch (UnableToReadFile) {
|
|
||||||
$file = null;
|
|
||||||
}
|
|
||||||
if ($file === null) {
|
if ($file === null) {
|
||||||
throw new ImportException('File not found');
|
throw new Exception('File not found');
|
||||||
}
|
}
|
||||||
/** @var string $timezone */
|
/** @var string $timezone */
|
||||||
$timezone = $data['timezone'];
|
$timezone = $data['timezone'];
|
||||||
|
|||||||
@@ -98,10 +98,9 @@ class UsersRelationManager extends RelationManager
|
|||||||
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
|
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
|
||||||
}
|
}
|
||||||
|
|
||||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
if ($data['role'] !== $member->role) {
|
||||||
if ($newRole->value !== $member->role) {
|
|
||||||
try {
|
try {
|
||||||
app(MemberService::class)->changeRole($member, $organization, $newRole, true);
|
app(MemberService::class)->changeRole($member, $organization, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
|
||||||
} catch (ApiException $exception) {
|
} catch (ApiException $exception) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use App\Models\User;
|
|||||||
use App\Service\DeletionService;
|
use App\Service\DeletionService;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\BulkAction;
|
use Filament\Actions\BulkAction;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
@@ -115,7 +115,7 @@ class UserResource extends Resource
|
|||||||
Select::make('currency')
|
Select::make('currency')
|
||||||
->label('Currency (Personal Organization)')
|
->label('Currency (Personal Organization)')
|
||||||
->options(function (): array {
|
->options(function (): array {
|
||||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||||
$select = [];
|
$select = [];
|
||||||
foreach ($currencies as $currency) {
|
foreach ($currencies as $currency) {
|
||||||
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
||||||
|
|||||||
@@ -59,10 +59,9 @@ class OrganizationsRelationManager extends RelationManager
|
|||||||
/** @var Member $member */
|
/** @var Member $member */
|
||||||
$member = $record->getRelation('membership');
|
$member = $record->getRelation('membership');
|
||||||
|
|
||||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
if ($data['role'] !== $member->role) {
|
||||||
if ($newRole->value !== $member->role) {
|
|
||||||
try {
|
try {
|
||||||
app(MemberService::class)->changeRole($member, $record, $newRole, true);
|
app(MemberService::class)->changeRole($member, $record, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
|
||||||
} catch (ApiException $exception) {
|
} catch (ApiException $exception) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace App\Http\Controllers\Api\V1;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Service\CurrencyService;
|
use App\Service\CurrencyService;
|
||||||
use Brick\Money\Currency;
|
use Brick\Money\Currency;
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
class CurrencyController extends Controller
|
class CurrencyController extends Controller
|
||||||
@@ -29,7 +29,7 @@ class CurrencyController extends Controller
|
|||||||
'name' => $currency->getName(),
|
'name' => $currency->getName(),
|
||||||
'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()),
|
'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()),
|
||||||
],
|
],
|
||||||
IsoCurrencyProvider::getInstance()->getAvailableCurrencies()
|
ISOCurrencyProvider::getInstance()->getAvailableCurrencies()
|
||||||
));
|
));
|
||||||
|
|
||||||
return response()->json($currencies);
|
return response()->json($currencies);
|
||||||
|
|||||||
@@ -191,9 +191,8 @@ class TimeEntryController extends Controller
|
|||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
if ($roundingType !== null && $roundingMinutes !== null) {
|
if ($roundingType !== null && $roundingMinutes !== null) {
|
||||||
$select = array_diff($select, ['start', 'end']);
|
$select = array_diff($select, ['start', 'end']);
|
||||||
// These SQL fragments are generated from a rounding enum and validated minute count.
|
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start');
|
||||||
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start'); // @phpstan-ignore argument.type
|
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end');
|
||||||
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); // @phpstan-ignore argument.type
|
|
||||||
}
|
}
|
||||||
$timeEntriesQuery = TimeEntry::query()
|
$timeEntriesQuery = TimeEntry::query()
|
||||||
->whereBelongsTo($organization, 'organization')
|
->whereBelongsTo($organization, 'organization')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Web;
|
|||||||
|
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Brick\Money\Currency;
|
use Brick\Money\Currency;
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -53,7 +53,7 @@ class OrganizationController extends Controller
|
|||||||
],
|
],
|
||||||
'currencies' => array_map(function (Currency $currency): string {
|
'currencies' => array_map(function (Currency $currency): string {
|
||||||
return $currency->getName();
|
return $currency->getName();
|
||||||
}, IsoCurrencyProvider::getInstance()->getAvailableCurrencies()),
|
}, ISOCurrencyProvider::getInstance()->getAvailableCurrencies()),
|
||||||
'permissions' => [
|
'permissions' => [
|
||||||
'canDeleteTeam' => $this->hasPermission($organization, 'organizations:delete'),
|
'canDeleteTeam' => $this->hasPermission($organization, 'organizations:delete'),
|
||||||
'canUpdateTeam' => $this->hasPermission($organization, 'organizations:update'),
|
'canUpdateTeam' => $this->hasPermission($organization, 'organizations:update'),
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use App\Http\Middleware\EnsureEmailIsVerified;
|
|||||||
use App\Http\Middleware\ForceHttps;
|
use App\Http\Middleware\ForceHttps;
|
||||||
use App\Http\Middleware\ForceJsonResponse;
|
use App\Http\Middleware\ForceJsonResponse;
|
||||||
use App\Http\Middleware\HandleInertiaRequests;
|
use App\Http\Middleware\HandleInertiaRequests;
|
||||||
use App\Http\Middleware\PreventRequestForgery;
|
|
||||||
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||||
use App\Http\Middleware\ShareInertiaData;
|
use App\Http\Middleware\ShareInertiaData;
|
||||||
@@ -19,6 +18,7 @@ use App\Http\Middleware\TrimStrings;
|
|||||||
use App\Http\Middleware\TrustHosts;
|
use App\Http\Middleware\TrustHosts;
|
||||||
use App\Http\Middleware\TrustProxies;
|
use App\Http\Middleware\TrustProxies;
|
||||||
use App\Http\Middleware\ValidateSignature;
|
use App\Http\Middleware\ValidateSignature;
|
||||||
|
use App\Http\Middleware\VerifyCsrfToken;
|
||||||
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||||
use Illuminate\Auth\Middleware\Authorize;
|
use Illuminate\Auth\Middleware\Authorize;
|
||||||
use Illuminate\Auth\Middleware\RequirePassword;
|
use Illuminate\Auth\Middleware\RequirePassword;
|
||||||
@@ -68,7 +68,7 @@ class Kernel extends HttpKernel
|
|||||||
AddQueuedCookiesToResponse::class,
|
AddQueuedCookiesToResponse::class,
|
||||||
StartSession::class,
|
StartSession::class,
|
||||||
ShareErrorsFromSession::class,
|
ShareErrorsFromSession::class,
|
||||||
PreventRequestForgery::class,
|
VerifyCsrfToken::class,
|
||||||
SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
ShareInertiaData::class,
|
ShareInertiaData::class,
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Filament\Http\Middleware\Authenticate as Middleware;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class AuthenticateFilamentPanel extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The admin panel has no login page of its own (it shares the app's
|
|
||||||
* session-based login), so Filament::getLoginUrl() always returns null.
|
|
||||||
* Fall back to the app's login route so guests are redirected instead
|
|
||||||
* of receiving an empty 401 response.
|
|
||||||
*/
|
|
||||||
protected function redirectTo($request): ?string
|
|
||||||
{
|
|
||||||
return parent::redirectTo($request) ?? ($request->expectsJson() ? null : route('login'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as Middleware;
|
|
||||||
|
|
||||||
class PreventRequestForgery extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The URIs that should be excluded.
|
|
||||||
*
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
protected $except = [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
19
app/Http/Middleware/VerifyCsrfToken.php
Normal file
19
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||||
|
|
||||||
|
class VerifyCsrfToken extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The URIs that should be excluded from CSRF verification.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $except = [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||||
@@ -99,7 +100,7 @@ class Organization extends Model implements AuditableContract
|
|||||||
/**
|
/**
|
||||||
* Get all the users that belong to the team.
|
* Get all the users that belong to the team.
|
||||||
*
|
*
|
||||||
* @return BelongsToMany<User, $this, Member, 'membership'>
|
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||||
*/
|
*/
|
||||||
public function users(): BelongsToMany
|
public function users(): BelongsToMany
|
||||||
{
|
{
|
||||||
@@ -132,7 +133,7 @@ class Organization extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<User, $this, Member, 'membership'>
|
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||||
*/
|
*/
|
||||||
public function realUsers(): BelongsToMany
|
public function realUsers(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -174,7 +175,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<Organization, $this, Member, 'membership'>
|
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||||
*/
|
*/
|
||||||
public function organizations(): BelongsToMany
|
public function organizations(): BelongsToMany
|
||||||
{
|
{
|
||||||
@@ -189,7 +190,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<Organization, $this, Member, 'membership'>
|
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||||
*/
|
*/
|
||||||
public function ownedOrganizations(): BelongsToMany
|
public function ownedOrganizations(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use App\Filament\Widgets\ServerOverview;
|
|||||||
use App\Filament\Widgets\TimeEntriesCreated;
|
use App\Filament\Widgets\TimeEntriesCreated;
|
||||||
use App\Filament\Widgets\TimeEntriesImported;
|
use App\Filament\Widgets\TimeEntriesImported;
|
||||||
use App\Filament\Widgets\UserRegistrations;
|
use App\Filament\Widgets\UserRegistrations;
|
||||||
use App\Http\Middleware\AuthenticateFilamentPanel;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||||
use Filament\Navigation\NavigationGroup;
|
use Filament\Navigation\NavigationGroup;
|
||||||
@@ -19,7 +19,7 @@ use Filament\PanelProvider;
|
|||||||
use Filament\Support\Colors\Color;
|
use Filament\Support\Colors\Color;
|
||||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
@@ -71,9 +71,6 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
NavigationGroup::make()
|
NavigationGroup::make()
|
||||||
->label('Auth')
|
->label('Auth')
|
||||||
->collapsed(),
|
->collapsed(),
|
||||||
NavigationGroup::make()
|
|
||||||
->label('Self Hosting')
|
|
||||||
->collapsed(),
|
|
||||||
])
|
])
|
||||||
->middleware([
|
->middleware([
|
||||||
EncryptCookies::class,
|
EncryptCookies::class,
|
||||||
@@ -81,13 +78,13 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
StartSession::class,
|
StartSession::class,
|
||||||
AuthenticateSession::class,
|
AuthenticateSession::class,
|
||||||
ShareErrorsFromSession::class,
|
ShareErrorsFromSession::class,
|
||||||
PreventRequestForgery::class,
|
VerifyCsrfToken::class,
|
||||||
SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
DisableBladeIconComponents::class,
|
DisableBladeIconComponents::class,
|
||||||
DispatchServingFilamentEvent::class,
|
DispatchServingFilamentEvent::class,
|
||||||
])
|
])
|
||||||
->authMiddleware([
|
->authMiddleware([
|
||||||
AuthenticateFilamentPanel::class,
|
Authenticate::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$modules = Module::allEnabled();
|
$modules = Module::allEnabled();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Rules;
|
namespace App\Rules;
|
||||||
|
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Translation\PotentiallyTranslatedString;
|
use Illuminate\Translation\PotentiallyTranslatedString;
|
||||||
@@ -24,7 +24,7 @@ class CurrencyRule implements ValidationRule
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||||
if (array_key_exists($value, $currencies)) {
|
if (array_key_exists($value, $currencies)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,12 @@ use App\Models\User;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Log;
|
use Log;
|
||||||
use Nwidart\Modules\Facades\Module;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class ApiService
|
class ApiService
|
||||||
{
|
{
|
||||||
private const string API_URL = 'https://app.solidtime.io/api/v1';
|
private const string API_URL = 'https://app.solidtime.io/api/v1';
|
||||||
|
|
||||||
private function isPremium(): bool
|
|
||||||
{
|
|
||||||
return Module::has('Invoicing');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkForUpdate(): ?string
|
public function checkForUpdate(): ?string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -37,7 +31,6 @@ class ApiService
|
|||||||
'version' => config('app.version'),
|
'version' => config('app.version'),
|
||||||
'build' => config('app.build'),
|
'build' => config('app.build'),
|
||||||
'url' => config('app.url'),
|
'url' => config('app.url'),
|
||||||
'is_premium' => $this->isPremium(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) {
|
if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) {
|
||||||
@@ -69,7 +62,6 @@ class ApiService
|
|||||||
'version' => config('app.version'),
|
'version' => config('app.version'),
|
||||||
'build' => config('app.build'),
|
'build' => config('app.build'),
|
||||||
'url' => config('app.url'),
|
'url' => config('app.url'),
|
||||||
'is_premium' => $this->isPremium(),
|
|
||||||
// telemetry data
|
// telemetry data
|
||||||
'user_count' => User::count(),
|
'user_count' => User::count(),
|
||||||
'organization_count' => Organization::count(),
|
'organization_count' => Organization::count(),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
use Brick\Money\IsoCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Brick\Money\Money;
|
use Brick\Money\Money;
|
||||||
|
|
||||||
class CurrencyService
|
class CurrencyService
|
||||||
@@ -378,7 +378,7 @@ class CurrencyService
|
|||||||
|
|
||||||
public function getRandomCurrencyCode(): string
|
public function getRandomCurrencyCode(): string
|
||||||
{
|
{
|
||||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||||
$currencyCodes = array_keys($currencies);
|
$currencyCodes = array_keys($currencies);
|
||||||
|
|
||||||
return $currencyCodes[array_rand($currencyCodes)];
|
return $currencyCodes[array_rand($currencyCodes)];
|
||||||
|
|||||||
@@ -152,13 +152,12 @@ class DashboardService
|
|||||||
|
|
||||||
$possibleDays = $this->lastDays($days, $timezone);
|
$possibleDays = $this->lastDays($days, $timezone);
|
||||||
|
|
||||||
// The SQL fragment contains only a timezone offset calculated as an integer.
|
|
||||||
$query = TimeEntry::query()
|
$query = TimeEntry::query()
|
||||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
|
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
|
||||||
->where('user_id', '=', $user->getKey())
|
->where('user_id', '=', $user->getKey())
|
||||||
->where('organization_id', '=', $organization->getKey())
|
->where('organization_id', '=', $organization->getKey())
|
||||||
->workTime()
|
->workTime()
|
||||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
|
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
|
||||||
->orderBy('date');
|
->orderBy('date');
|
||||||
|
|
||||||
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
||||||
@@ -195,13 +194,12 @@ class DashboardService
|
|||||||
}
|
}
|
||||||
$possibleDays = $this->daysOfThisWeek($timezone, $user->week_start);
|
$possibleDays = $this->daysOfThisWeek($timezone, $user->week_start);
|
||||||
|
|
||||||
// The SQL fragment contains only a timezone offset calculated as an integer.
|
|
||||||
$query = TimeEntry::query()
|
$query = TimeEntry::query()
|
||||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
|
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
|
||||||
->where('user_id', '=', $user->getKey())
|
->where('user_id', '=', $user->getKey())
|
||||||
->where('organization_id', '=', $organization->getKey())
|
->where('organization_id', '=', $organization->getKey())
|
||||||
->workTime()
|
->workTime()
|
||||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
|
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
|
||||||
->orderBy('date');
|
->orderBy('date');
|
||||||
|
|
||||||
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ClockifyProjectsImporter extends DefaultImporter
|
|||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
'is_billable' => ($record['Billability'] ?? '') === 'Yes',
|
'is_billable' => $record['Billability'] === 'Yes',
|
||||||
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
||||||
'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null,
|
'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null,
|
||||||
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
|
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
|
||||||
@@ -55,7 +55,6 @@ class ClockifyProjectsImporter extends DefaultImporter
|
|||||||
if ($tasksKey !== null && $record[$tasksKey] !== '') {
|
if ($tasksKey !== null && $record[$tasksKey] !== '') {
|
||||||
$tasks = explode(', ', $record[$tasksKey]);
|
$tasks = explode(', ', $record[$tasksKey]);
|
||||||
foreach ($tasks as $task) {
|
foreach ($tasks as $task) {
|
||||||
$this->checkTaskNameLength($task);
|
|
||||||
$this->taskImportHelper->getKey([
|
$this->taskImportHelper->getKey([
|
||||||
'name' => $task,
|
'name' => $task,
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
@@ -85,6 +84,7 @@ class ClockifyProjectsImporter extends DefaultImporter
|
|||||||
'Project',
|
'Project',
|
||||||
'Status',
|
'Status',
|
||||||
'Visibility',
|
'Visibility',
|
||||||
|
'Billability',
|
||||||
];
|
];
|
||||||
foreach ($requiredFields as $requiredField) {
|
foreach ($requiredFields as $requiredField) {
|
||||||
if (! in_array($requiredField, $header, true)) {
|
if (! in_array($requiredField, $header, true)) {
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
}
|
}
|
||||||
$taskId = null;
|
$taskId = null;
|
||||||
if (! $isBreak && $taskKey !== null && $record[$taskKey] !== '') {
|
if (! $isBreak && $taskKey !== null && $record[$taskKey] !== '') {
|
||||||
$this->checkTaskNameLength($record[$taskKey]);
|
|
||||||
$taskId = $this->taskImportHelper->getKey([
|
$taskId = $this->taskImportHelper->getKey([
|
||||||
'name' => $record[$taskKey],
|
'name' => $record[$taskKey],
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
@@ -229,6 +228,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
'Project',
|
'Project',
|
||||||
'Description',
|
'Description',
|
||||||
'User',
|
'User',
|
||||||
|
'Group',
|
||||||
'Email',
|
'Email',
|
||||||
'Tags',
|
'Tags',
|
||||||
'Start Date',
|
'Start Date',
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ use Override;
|
|||||||
|
|
||||||
abstract class DefaultImporter implements ImporterContract
|
abstract class DefaultImporter implements ImporterContract
|
||||||
{
|
{
|
||||||
protected const TASK_NAME_MAX_LENGTH = 500;
|
|
||||||
|
|
||||||
protected Organization $organization;
|
protected Organization $organization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,16 +182,6 @@ abstract class DefaultImporter implements ImporterContract
|
|||||||
$this->billableRateService = app(BillableRateService::class);
|
$this->billableRateService = app(BillableRateService::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws ImportException
|
|
||||||
*/
|
|
||||||
protected function checkTaskNameLength(string $taskName): void
|
|
||||||
{
|
|
||||||
if (strlen($taskName) > self::TASK_NAME_MAX_LENGTH) {
|
|
||||||
throw new ImportException('Task name ("'.$taskName.'") is too long, maximum length is '.self::TASK_NAME_MAX_LENGTH.' characters');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function getReport(): ReportDto
|
public function getReport(): ReportDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ class GenericTimeEntriesImporter extends DefaultImporter
|
|||||||
}
|
}
|
||||||
$taskId = null;
|
$taskId = null;
|
||||||
if ($record['task'] !== '') {
|
if ($record['task'] !== '') {
|
||||||
$this->checkTaskNameLength($record['task']);
|
|
||||||
$taskId = $this->taskImportHelper->getKey([
|
$taskId = $this->taskImportHelper->getKey([
|
||||||
'name' => $record['task'],
|
'name' => $record['task'],
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ class HarvestTimeEntriesImporter extends DefaultImporter
|
|||||||
}
|
}
|
||||||
$taskId = null;
|
$taskId = null;
|
||||||
if ($record['Task'] !== '') {
|
if ($record['Task'] !== '') {
|
||||||
$this->checkTaskNameLength($record['Task']);
|
|
||||||
$taskId = $this->taskImportHelper->getKey([
|
$taskId = $this->taskImportHelper->getKey([
|
||||||
'name' => $record['Task'],
|
'name' => $record['Task'],
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
|
|||||||
@@ -199,7 +199,6 @@ class SolidtimeImporter extends DefaultImporter
|
|||||||
if ($projectId === null) {
|
if ($projectId === null) {
|
||||||
throw new Exception('Project does not exist');
|
throw new Exception('Project does not exist');
|
||||||
}
|
}
|
||||||
$this->checkTaskNameLength($task['name']);
|
|
||||||
$this->taskImportHelper->getKey([
|
$this->taskImportHelper->getKey([
|
||||||
'name' => $task['name'],
|
'name' => $task['name'],
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
|
|||||||
@@ -153,16 +153,9 @@ class TogglDataImporter extends DefaultImporter
|
|||||||
}
|
}
|
||||||
foreach ($projectMembers as $projectMember) {
|
foreach ($projectMembers as $projectMember) {
|
||||||
$userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id);
|
$userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id);
|
||||||
if ($userId === null) {
|
|
||||||
throw new Exception('User does not exist');
|
|
||||||
}
|
|
||||||
$memberId = $this->memberImportHelper->getKeyByExternalIdentifier($userId);
|
|
||||||
if ($memberId === null) {
|
|
||||||
throw new Exception('Member does not exist');
|
|
||||||
}
|
|
||||||
$this->projectMemberImportHelper->getKey([
|
$this->projectMemberImportHelper->getKey([
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
'member_id' => $memberId,
|
'member_id' => $this->memberImportHelper->getKeyByExternalIdentifier($userId),
|
||||||
], [
|
], [
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null,
|
'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null,
|
||||||
@@ -189,7 +182,6 @@ class TogglDataImporter extends DefaultImporter
|
|||||||
if ($projectId === null) {
|
if ($projectId === null) {
|
||||||
throw new Exception('Project does not exist');
|
throw new Exception('Project does not exist');
|
||||||
}
|
}
|
||||||
$this->checkTaskNameLength($task->name);
|
|
||||||
$this->taskImportHelper->getKey([
|
$this->taskImportHelper->getKey([
|
||||||
'name' => $task->name,
|
'name' => $task->name,
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
|||||||
}
|
}
|
||||||
$taskId = null;
|
$taskId = null;
|
||||||
if ($record['Task'] !== '') {
|
if ($record['Task'] !== '') {
|
||||||
$this->checkTaskNameLength($record['Task']);
|
|
||||||
$taskId = $this->taskImportHelper->getKey([
|
$taskId = $this->taskImportHelper->getKey([
|
||||||
'name' => $record['Task'],
|
'name' => $record['Task'],
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
|
|||||||
@@ -75,9 +75,8 @@ class TimeEntryAggregationService
|
|||||||
$startRawSelect = app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes);
|
$startRawSelect = app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes);
|
||||||
$endRawSelect = app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes);
|
$endRawSelect = app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes);
|
||||||
|
|
||||||
// SQL fragments come from enum cases, integer offsets, and formatted dates.
|
|
||||||
$timeEntriesQuery->selectRaw(
|
$timeEntriesQuery->selectRaw(
|
||||||
($group1Select !== null ? $group1Select.' as group_1,' : ''). // @phpstan-ignore argument.type
|
($group1Select !== null ? $group1Select.' as group_1,' : '').
|
||||||
($group2Select !== null ? $group2Select.' as group_2,' : '').
|
($group2Select !== null ? $group2Select.' as group_2,' : '').
|
||||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||||
@@ -106,7 +105,7 @@ class TimeEntryAggregationService
|
|||||||
$baseTotalsPerGroup1Query = $baseTotalsQuery->clone();
|
$baseTotalsPerGroup1Query = $baseTotalsQuery->clone();
|
||||||
$baseTotalsPerGroup1 = $baseTotalsPerGroup1Query
|
$baseTotalsPerGroup1 = $baseTotalsPerGroup1Query
|
||||||
->selectRaw(
|
->selectRaw(
|
||||||
$group1Select.' as group_1,'. // @phpstan-ignore argument.type
|
$group1Select.' as group_1,'.
|
||||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||||
)
|
)
|
||||||
@@ -171,7 +170,7 @@ class TimeEntryAggregationService
|
|||||||
// Reset selects and ordering on the cloned base query
|
// Reset selects and ordering on the cloned base query
|
||||||
$baseTotals = $baseTotalsQuery
|
$baseTotals = $baseTotalsQuery
|
||||||
->selectRaw(
|
->selectRaw(
|
||||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'. // @phpstan-ignore argument.type
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||||
)
|
)
|
||||||
->first();
|
->first();
|
||||||
|
|||||||
@@ -7,22 +7,22 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "8.3.*",
|
"php": "8.3.*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"brick/money": "^0.15.1",
|
"brick/money": "^0.11.2",
|
||||||
"datomatic/laravel-enum-helper": "^2.0.0",
|
"datomatic/laravel-enum-helper": "^2.0.0",
|
||||||
"dedoc/scramble": "^0.13.43",
|
"dedoc/scramble": "^0.13.43",
|
||||||
"filament/filament": "^5.0",
|
"filament/filament": "^5.0",
|
||||||
"flowframe/laravel-trend": "^0.5.0",
|
"flowframe/laravel-trend": "^0.5.0",
|
||||||
"gotenberg/gotenberg-php": "^2.8",
|
"gotenberg/gotenberg-php": "^2.8",
|
||||||
"guzzlehttp/guzzle": "^8.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"inertiajs/inertia-laravel": "^3.3.4",
|
"inertiajs/inertia-laravel": "^3.3.4",
|
||||||
"korridor/laravel-computed-attributes": "^3.1",
|
"korridor/laravel-computed-attributes": "^3.1",
|
||||||
"korridor/laravel-has-many-sync": "^3.1",
|
"korridor/laravel-has-many-sync": "^3.1",
|
||||||
"korridor/laravel-model-validation-rules": "^3.0",
|
"korridor/laravel-model-validation-rules": "^3.0",
|
||||||
"laravel/fortify": "^1.37",
|
"laravel/fortify": "^1.37",
|
||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^12.19.3",
|
||||||
"laravel/octane": "^2.3",
|
"laravel/octane": "^2.3",
|
||||||
"laravel/passport": "^13.0.5",
|
"laravel/passport": "^13.0.5",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^2.8",
|
||||||
"league/csv": "^9.16.0",
|
"league/csv": "^9.16.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"league/iso3166": "^4.3",
|
"league/iso3166": "^4.3",
|
||||||
|
|||||||
694
composer.lock
generated
694
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,9 @@ return [
|
|||||||
| Application Name
|
| Application Name
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This value is the name of your application, which will be used when the
|
| This value is the name of your application. This value is used when the
|
||||||
| framework needs to place the application's name in a notification or
|
| framework needs to place the application's name in a notification or
|
||||||
| other UI elements where an application name needs to be displayed.
|
| any other location as required by the application or its packages.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -110,8 +110,8 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you may specify the default timezone for your application, which
|
| Here you may specify the default timezone for your application, which
|
||||||
| will be used by the PHP date and date-time functions. The timezone
|
| will be used by the PHP date and date-time functions. We have gone
|
||||||
| is set to "UTC" by default as it is suitable for most use cases.
|
| ahead and set this to a sensible default for you out of the box.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| The application locale determines the default locale that will be used
|
| The application locale determines the default locale that will be used
|
||||||
| by Laravel's translation / localization methods. This option can be
|
| by the translation service provider. You are free to set this value
|
||||||
| set to any locale for which you plan to have translation strings.
|
| to any of the locales which will be supported by the application.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -178,21 +178,15 @@ return [
|
|||||||
| Encryption Key
|
| Encryption Key
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This key is utilized by Laravel's encryption services and should be set
|
| This key is used by the Illuminate encrypter service and should be set
|
||||||
| to a random, 32 character string to ensure that all encrypted values
|
| to a random, 32 character string, otherwise these encrypted strings
|
||||||
| are secure. You should do this prior to deploying the application.
|
| will not be safe. Please do this before deploying an application!
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'cipher' => 'AES-256-CBC',
|
|
||||||
|
|
||||||
'key' => env('APP_KEY'),
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
'previous_keys' => [
|
'cipher' => 'AES-256-CBC',
|
||||||
...array_filter(
|
|
||||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
'localization' => [
|
'localization' => [
|
||||||
'default_currency' => env('LOCALIZATION_DEFAULT_CURRENCY', 'EUR'),
|
'default_currency' => env('LOCALIZATION_DEFAULT_CURRENCY', 'EUR'),
|
||||||
@@ -212,13 +206,13 @@ return [
|
|||||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
| allow maintenance mode to be controlled across multiple machines.
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
|
||||||
| Supported drivers: "file", "cache", "array"
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'maintenance' => [
|
'maintenance' => [
|
||||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
'driver' => 'file',
|
||||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
// 'store' => 'redis',
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ return [
|
|||||||
| Authentication Defaults
|
| Authentication Defaults
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This option defines the default authentication "guard" and password
|
| This option controls the default authentication "guard" and password
|
||||||
| reset "broker" for your application. You may change these values
|
| reset options for your application. You may change these defaults
|
||||||
| as required, but they're a perfect start for most applications.
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
@@ -28,11 +28,11 @@ return [
|
|||||||
|
|
|
|
||||||
| Next, you may define every authentication guard for your application.
|
| Next, you may define every authentication guard for your application.
|
||||||
| Of course, a great default configuration has been defined for you
|
| Of course, a great default configuration has been defined for you
|
||||||
| which utilizes session storage plus the Eloquent user provider.
|
| here which uses session storage and the Eloquent user provider.
|
||||||
|
|
|
|
||||||
| All authentication guards have a user provider, which defines how the
|
| All authentication drivers have a user provider. This defines how the
|
||||||
| users are actually retrieved out of your database or other storage
|
| users are actually retrieved out of your database or other storage
|
||||||
| system used by the application. Typically, Eloquent is utilized.
|
| mechanisms used by this application to persist your user's data.
|
||||||
|
|
|
|
||||||
| Supported: "session"
|
| Supported: "session"
|
||||||
|
|
|
|
||||||
@@ -55,12 +55,12 @@ return [
|
|||||||
| User Providers
|
| User Providers
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| All authentication guards have a user provider, which defines how the
|
| All authentication drivers have a user provider. This defines how the
|
||||||
| users are actually retrieved out of your database or other storage
|
| users are actually retrieved out of your database or other storage
|
||||||
| system used by the application. Typically, Eloquent is utilized.
|
| mechanisms used by this application to persist your user's data.
|
||||||
|
|
|
|
||||||
| If you have multiple user tables or models you may configure multiple
|
| If you have multiple user tables or models you may configure multiple
|
||||||
| providers to represent the model / table. These providers may then
|
| sources which represent each model / table. These sources may then
|
||||||
| be assigned to any extra authentication guards you have defined.
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
|
||||||
| Supported: "database", "eloquent"
|
| Supported: "database", "eloquent"
|
||||||
@@ -80,9 +80,9 @@ return [
|
|||||||
| Resetting Passwords
|
| Resetting Passwords
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| These configuration options specify the behavior of Laravel's password
|
| You may specify multiple password reset configurations if you have more
|
||||||
| reset functionality, including the table utilized for token storage
|
| than one user table or model in the application and you want to have
|
||||||
| and the user provider that is invoked to actually retrieve users.
|
| separate password reset settings based on the specific user types.
|
||||||
|
|
|
|
||||||
| The expiry time is the number of minutes that each reset token will be
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
| considered valid. This security feature keeps tokens short-lived so
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
@@ -108,13 +108,13 @@ return [
|
|||||||
| Password Confirmation Timeout
|
| Password Confirmation Timeout
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you may define the number of seconds before a password confirmation
|
| Here you may define the amount of seconds before a password confirmation
|
||||||
| window expires and users are asked to re-enter their password via the
|
| times out and the user is prompted to re-enter their password via the
|
||||||
| confirmation screen. By default, the timeout lasts for three hours.
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
'password_timeout' => 10800,
|
||||||
|
|
||||||
'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')),
|
'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')),
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ return [
|
|||||||
| Default Cache Store
|
| Default Cache Store
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This option controls the default cache store that will be used by the
|
| This option controls the default cache connection that gets used while
|
||||||
| framework. This connection is utilized if another isn't explicitly
|
| using this caching library. This connection is used when another is
|
||||||
| specified when running a cache operation inside the application.
|
| not explicitly specified when executing a given caching function.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -28,9 +28,8 @@ return [
|
|||||||
| well as their drivers. You may even define multiple stores for the
|
| well as their drivers. You may even define multiple stores for the
|
||||||
| same cache driver to group types of items stored in your caches.
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
|
||||||
| Supported drivers: "array", "database", "file", "memcached",
|
| Supported drivers: "apc", "array", "database", "file",
|
||||||
| "redis", "dynamodb", "storage", "octane",
|
| "memcached", "redis", "dynamodb", "octane", "null"
|
||||||
| "session", "failover", "null"
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -47,10 +46,9 @@ return [
|
|||||||
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'driver' => 'database',
|
'driver' => 'database',
|
||||||
'connection' => env('DB_CACHE_CONNECTION'),
|
'table' => 'cache',
|
||||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
'connection' => null,
|
||||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
'lock_connection' => null,
|
||||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'file' => [
|
'file' => [
|
||||||
@@ -59,12 +57,6 @@ return [
|
|||||||
'lock_path' => storage_path('framework/cache/data'),
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'storage' => [
|
|
||||||
'driver' => 'storage',
|
|
||||||
'disk' => env('CACHE_STORAGE_DISK'),
|
|
||||||
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
|
|
||||||
],
|
|
||||||
|
|
||||||
'memcached' => [
|
'memcached' => [
|
||||||
'driver' => 'memcached',
|
'driver' => 'memcached',
|
||||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
@@ -86,8 +78,8 @@ return [
|
|||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'driver' => 'redis',
|
'driver' => 'redis',
|
||||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
'connection' => 'cache',
|
||||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
'lock_connection' => 'default',
|
||||||
],
|
],
|
||||||
|
|
||||||
'dynamodb' => [
|
'dynamodb' => [
|
||||||
@@ -103,14 +95,6 @@ return [
|
|||||||
'driver' => 'octane',
|
'driver' => 'octane',
|
||||||
],
|
],
|
||||||
|
|
||||||
'failover' => [
|
|
||||||
'driver' => 'failover',
|
|
||||||
'stores' => [
|
|
||||||
'database',
|
|
||||||
'array',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -118,24 +102,12 @@ return [
|
|||||||
| Cache Key Prefix
|
| Cache Key Prefix
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
| When utilizing the APC, database, memcached, Redis, or DynamoDB cache
|
||||||
| stores, there might be other applications using the same cache. For
|
| stores there might be other applications using the same cache. For
|
||||||
| that reason, you may prefix every cache key to avoid collisions.
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Serializable Classes
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This value determines the classes that can be unserialized from cache
|
|
||||||
| storage. By default, no PHP classes will be unserialized from your
|
|
||||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'serializable_classes' => false,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,22 +12,26 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you may specify which of the database connections below you wish
|
| Here you may specify which of the database connections below you wish
|
||||||
| to use as your default connection for database operations. This is
|
| to use as your default connection for all database work. Of course
|
||||||
| the connection which will be utilized unless another connection
|
| you may use many connections at once using the Database library.
|
||||||
| is explicitly specified when you execute a query / statement.
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'default' => env('DB_CONNECTION', 'pgsql'),
|
'default' => env('DB_CONNECTION', 'mysql'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Database Connections
|
| Database Connections
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Below are all of the database connections defined for your application.
|
| Here are each of the database connections setup for your application.
|
||||||
| An example configuration is provided for each database system which
|
| Of course, examples of configuring each database platform that is
|
||||||
| is supported by Laravel. You're free to add / remove connections.
|
| supported by Laravel is shown below to make development simple.
|
||||||
|
|
|
||||||
|
|
|
||||||
|
| All database work in Laravel is done through the PHP PDO facilities
|
||||||
|
| so make sure you have the driver for your particular database of
|
||||||
|
| choice installed on your machine before you begin development.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -35,34 +39,50 @@ return [
|
|||||||
|
|
||||||
'sqlite' => [
|
'sqlite' => [
|
||||||
'driver' => 'sqlite',
|
'driver' => 'sqlite',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DATABASE_URL'),
|
||||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
'busy_timeout' => null,
|
],
|
||||||
'journal_mode' => null,
|
|
||||||
'synchronous' => null,
|
'mysql' => [
|
||||||
'transaction_mode' => 'DEFERRED',
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DATABASE_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'forge'),
|
||||||
|
'username' => env('DB_USERNAME', 'forge'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
'pgsql' => [
|
'pgsql' => [
|
||||||
'driver' => 'pgsql',
|
'driver' => 'pgsql',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DATABASE_URL'),
|
||||||
'host' => env('DB_HOST', '127.0.0.1'),
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
'port' => env('DB_PORT', '5432'),
|
'port' => env('DB_PORT', '5432'),
|
||||||
'database' => env('DB_DATABASE', 'forge'),
|
'database' => env('DB_DATABASE', 'forge'),
|
||||||
'username' => env('DB_USERNAME', 'forge'),
|
'username' => env('DB_USERNAME', 'forge'),
|
||||||
'password' => env('DB_PASSWORD', ''),
|
'password' => env('DB_PASSWORD', ''),
|
||||||
'charset' => env('DB_CHARSET', 'utf8'),
|
'charset' => 'utf8',
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'search_path' => 'public',
|
'search_path' => 'public',
|
||||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
'sslmode' => 'prefer',
|
||||||
],
|
],
|
||||||
|
|
||||||
'pgsql_test' => [
|
'pgsql_test' => [
|
||||||
'driver' => 'pgsql',
|
'driver' => 'pgsql',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DATABASE_URL'),
|
||||||
'host' => env('DB_TEST_HOST', '127.0.0.1'),
|
'host' => env('DB_TEST_HOST', '127.0.0.1'),
|
||||||
'port' => env('DB_TEST_PORT', '5432'),
|
'port' => env('DB_TEST_PORT', '5432'),
|
||||||
'database' => env('DB_TEST_DATABASE', 'forge'),
|
'database' => env('DB_TEST_DATABASE', 'forge'),
|
||||||
@@ -72,18 +92,18 @@ return [
|
|||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'search_path' => 'public',
|
'search_path' => 'public',
|
||||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
'sslmode' => 'prefer',
|
||||||
],
|
],
|
||||||
|
|
||||||
'sqlsrv' => [
|
'sqlsrv' => [
|
||||||
'driver' => 'sqlsrv',
|
'driver' => 'sqlsrv',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DATABASE_URL'),
|
||||||
'host' => env('DB_HOST', 'localhost'),
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
'port' => env('DB_PORT', '1433'),
|
'port' => env('DB_PORT', '1433'),
|
||||||
'database' => env('DB_DATABASE', 'laravel'),
|
'database' => env('DB_DATABASE', 'forge'),
|
||||||
'username' => env('DB_USERNAME', 'root'),
|
'username' => env('DB_USERNAME', 'forge'),
|
||||||
'password' => env('DB_PASSWORD', ''),
|
'password' => env('DB_PASSWORD', ''),
|
||||||
'charset' => env('DB_CHARSET', 'utf8'),
|
'charset' => 'utf8',
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
@@ -99,14 +119,11 @@ return [
|
|||||||
|
|
|
|
||||||
| This table keeps track of all the migrations that have already run for
|
| This table keeps track of all the migrations that have already run for
|
||||||
| your application. Using this information, we can determine which of
|
| your application. Using this information, we can determine which of
|
||||||
| the migrations on disk haven't actually been run on the database.
|
| the migrations on disk haven't actually been run in the database.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'migrations' => [
|
'migrations' => 'migrations',
|
||||||
'table' => 'migrations',
|
|
||||||
'update_date_on_publish' => false,
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -115,7 +132,7 @@ return [
|
|||||||
|
|
|
|
||||||
| Redis is an open source, fast, and advanced key-value store that also
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
| provides a richer body of commands than a typical key-value system
|
| provides a richer body of commands than a typical key-value system
|
||||||
| such as Memcached. You may define your connection settings here.
|
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -126,7 +143,6 @@ return [
|
|||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||||
'persistent' => env('REDIS_PERSISTENT', false),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'default' => [
|
'default' => [
|
||||||
@@ -136,10 +152,6 @@ return [
|
|||||||
'password' => env('REDIS_PASSWORD'),
|
'password' => env('REDIS_PASSWORD'),
|
||||||
'port' => env('REDIS_PORT', '6379'),
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
'database' => env('REDIS_DB', '0'),
|
'database' => env('REDIS_DB', '0'),
|
||||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
|
||||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
|
||||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
|
||||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'cache' => [
|
'cache' => [
|
||||||
@@ -149,10 +161,6 @@ return [
|
|||||||
'password' => env('REDIS_PASSWORD'),
|
'password' => env('REDIS_PASSWORD'),
|
||||||
'port' => env('REDIS_PORT', '6379'),
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
'database' => env('REDIS_CACHE_DB', '1'),
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
|
||||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
|
||||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
|
||||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ return [
|
|||||||
|
|
|
|
||||||
| Here you may specify the default filesystem disk that should be used
|
| Here you may specify the default filesystem disk that should be used
|
||||||
| by the framework. The "local" disk, as well as a variety of cloud
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
| based disks are available to your application for file storage.
|
| based disks are available to your application. Just store away!
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -26,11 +26,11 @@ return [
|
|||||||
| Filesystem Disks
|
| Filesystem Disks
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Below you may configure as many filesystem disks as necessary, and you
|
| Here you may configure as many filesystem "disks" as you wish, and you
|
||||||
| may even configure multiple disks for the same driver. Examples for
|
| may even configure multiple disks of the same driver. Defaults have
|
||||||
| most supported storage drivers are configured here for reference.
|
| been set up for each driver as an example of the required values.
|
||||||
|
|
|
|
||||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -41,16 +41,14 @@ return [
|
|||||||
'root' => storage_path('app'),
|
'root' => storage_path('app'),
|
||||||
'serve' => true,
|
'serve' => true,
|
||||||
'throw' => true,
|
'throw' => true,
|
||||||
'report' => false,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'public' => [
|
'public' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/public'),
|
'root' => storage_path('app/public'),
|
||||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
'url' => env('APP_URL').'/storage',
|
||||||
'visibility' => 'public',
|
'visibility' => 'public',
|
||||||
'throw' => true,
|
'throw' => true,
|
||||||
'report' => false,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
's3' => [
|
's3' => [
|
||||||
@@ -64,7 +62,6 @@ return [
|
|||||||
'endpoint' => env('S3_ENDPOINT'),
|
'endpoint' => env('S3_ENDPOINT'),
|
||||||
'use_path_style_endpoint' => env('S3_USE_PATH_STYLE_ENDPOINT', false),
|
'use_path_style_endpoint' => env('S3_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
'throw' => true,
|
'throw' => true,
|
||||||
'report' => false,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'testfiles' => [
|
'testfiles' => [
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ return [
|
|||||||
|
|
||||||
'ssr' => [
|
'ssr' => [
|
||||||
|
|
||||||
'enabled' => (bool) env('INERTIA_SSR_ENABLED', false),
|
'enabled' => (bool) env('INERTIA_SSR_ENABLED', true),
|
||||||
|
|
||||||
'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
|
'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ return [
|
|||||||
| Default Log Channel
|
| Default Log Channel
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This option defines the default log channel that is utilized to write
|
| This option defines the default log channel that gets used when writing
|
||||||
| messages to your logs. The value provided here should match one of
|
| messages to the logs. The name specified in this option should match
|
||||||
| the channels present in the list of "channels" configured below.
|
| one of the channels defined in the "channels" configuration array.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -43,20 +43,20 @@ return [
|
|||||||
| Log Channels
|
| Log Channels
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you may configure the log channels for your application. Laravel
|
| Here you may configure the log channels for your application. Out of
|
||||||
| utilizes the Monolog PHP logging library, which includes a variety
|
| the box, Laravel uses the Monolog PHP logging library. This gives
|
||||||
| of powerful log handlers and formatters that you're free to use.
|
| you a variety of powerful log handlers / formatters to utilize.
|
||||||
|
|
|
|
||||||
| Available drivers: "single", "daily", "monthly", "slack", "syslog",
|
| Available Drivers: "single", "daily", "slack", "syslog",
|
||||||
| "errorlog", "monolog", "custom", "stack"
|
| "errorlog", "monolog",
|
||||||
|
| "custom", "stack"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'channels' => [
|
'channels' => [
|
||||||
|
|
||||||
'stack' => [
|
'stack' => [
|
||||||
'driver' => 'stack',
|
'driver' => 'stack',
|
||||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
'channels' => ['single'],
|
||||||
'ignore_exceptions' => false,
|
'ignore_exceptions' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -81,23 +81,15 @@ return [
|
|||||||
'driver' => 'daily',
|
'driver' => 'daily',
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'max_files' => env('LOG_DAILY_DAYS', 14),
|
'days' => 14,
|
||||||
'replace_placeholders' => true,
|
|
||||||
],
|
|
||||||
|
|
||||||
'monthly' => [
|
|
||||||
'driver' => 'monthly',
|
|
||||||
'path' => storage_path('logs/laravel.log'),
|
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
|
||||||
'max_files' => 3,
|
|
||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
'slack' => [
|
'slack' => [
|
||||||
'driver' => 'slack',
|
'driver' => 'slack',
|
||||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
'username' => 'Laravel Log',
|
||||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
'emoji' => ':boom:',
|
||||||
'level' => env('LOG_LEVEL', 'critical'),
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
],
|
],
|
||||||
@@ -124,17 +116,17 @@ return [
|
|||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'handler' => StreamHandler::class,
|
'handler' => StreamHandler::class,
|
||||||
'handler_with' => [
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'with' => [
|
||||||
'stream' => 'php://stderr',
|
'stream' => 'php://stderr',
|
||||||
],
|
],
|
||||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
|
||||||
'processors' => [PsrLogMessageProcessor::class],
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
],
|
],
|
||||||
|
|
||||||
'syslog' => [
|
'syslog' => [
|
||||||
'driver' => 'syslog',
|
'driver' => 'syslog',
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
'facility' => LOG_USER,
|
||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ return [
|
|||||||
| Default Mailer
|
| Default Mailer
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This option controls the default mailer that is used to send all email
|
| This option controls the default mailer that is used to send any email
|
||||||
| messages unless another mailer is explicitly specified when sending
|
| messages sent by your application. Alternative mailers may be setup
|
||||||
| the message. All additional mailers can be configured within the
|
| and used as needed; however, this mailer will be used by default.
|
||||||
| "mailers" array. Examples of each type of mailer are provided.
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -27,28 +26,26 @@ return [
|
|||||||
| their respective settings. Several examples have been configured for
|
| their respective settings. Several examples have been configured for
|
||||||
| you and you are free to add your own as your application requires.
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
|
||||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
| Laravel supports a variety of mail "transport" drivers to be used while
|
||||||
| when delivering an email. You may specify which one you're using for
|
| sending an e-mail. You will specify which one you are using for your
|
||||||
| your mailers below. You may also add additional mailers if needed.
|
| mailers below. You are free to add additional mailers as required.
|
||||||
|
|
|
|
||||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
| "postmark", "resend", "log", "array",
|
| "postmark", "log", "array", "failover", "roundrobin"
|
||||||
| "failover", "roundrobin"
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'mailers' => [
|
'mailers' => [
|
||||||
'smtp' => [
|
'smtp' => [
|
||||||
'transport' => 'smtp',
|
'transport' => 'smtp',
|
||||||
'scheme' => env('MAIL_SCHEME'),
|
|
||||||
'url' => env('MAIL_URL'),
|
'url' => env('MAIL_URL'),
|
||||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
|
||||||
'port' => env('MAIL_PORT', 2525),
|
'port' => env('MAIL_PORT', 587),
|
||||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||||
'username' => env('MAIL_USERNAME'),
|
'username' => env('MAIL_USERNAME'),
|
||||||
'password' => env('MAIL_PASSWORD'),
|
'password' => env('MAIL_PASSWORD'),
|
||||||
'timeout' => null,
|
'timeout' => null,
|
||||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
'local_domain' => env('MAIL_EHLO_DOMAIN'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'ses' => [
|
'ses' => [
|
||||||
@@ -57,14 +54,17 @@ return [
|
|||||||
|
|
||||||
'postmark' => [
|
'postmark' => [
|
||||||
'transport' => 'postmark',
|
'transport' => 'postmark',
|
||||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
// 'message_stream_id' => null,
|
||||||
// 'client' => [
|
// 'client' => [
|
||||||
// 'timeout' => 5,
|
// 'timeout' => 5,
|
||||||
// ],
|
// ],
|
||||||
],
|
],
|
||||||
|
|
||||||
'resend' => [
|
'mailgun' => [
|
||||||
'transport' => 'resend',
|
'transport' => 'mailgun',
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
],
|
],
|
||||||
|
|
||||||
'sendmail' => [
|
'sendmail' => [
|
||||||
@@ -87,7 +87,6 @@ return [
|
|||||||
'smtp',
|
'smtp',
|
||||||
'log',
|
'log',
|
||||||
],
|
],
|
||||||
'retry_after' => 60,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'roundrobin' => [
|
'roundrobin' => [
|
||||||
@@ -96,9 +95,7 @@ return [
|
|||||||
'ses',
|
'ses',
|
||||||
'postmark',
|
'postmark',
|
||||||
],
|
],
|
||||||
'retry_after' => 60,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -106,15 +103,15 @@ return [
|
|||||||
| Global "From" Address
|
| Global "From" Address
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| You may wish for all emails sent by your application to be sent from
|
| You may wish for all e-mails sent by your application to be sent from
|
||||||
| the same address. Here you may specify a name and address that is
|
| the same address. Here, you may specify a name and address that is
|
||||||
| used globally for all emails that are sent by your application.
|
| used globally for all e-mails that are sent by your application.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'from' => [
|
'from' => [
|
||||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'reply_to' => [
|
'reply_to' => [
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ return [
|
|||||||
| Default Queue Connection Name
|
| Default Queue Connection Name
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Laravel's queue supports a variety of backends via a single, unified
|
| Laravel's queue API supports an assortment of back-ends via a single
|
||||||
| API, giving you convenient access to each backend using identical
|
| API, giving you convenient access to each back-end using the same
|
||||||
| syntax for each. The default queue connection is defined below.
|
| syntax for every one. Here you may define a default connection.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -22,12 +22,11 @@ return [
|
|||||||
| Queue Connections
|
| Queue Connections
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you may configure the connection options for every queue backend
|
| Here you may configure the connection information for each server that
|
||||||
| used by your application. An example configuration is provided for
|
| is used by your application. A default configuration has been added
|
||||||
| each backend supported by Laravel. You're also free to add more.
|
| for each back-end shipped with Laravel. You are free to add more.
|
||||||
|
|
|
|
||||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
||||||
| "deferred", "background", "failover", "null"
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -39,18 +38,17 @@ return [
|
|||||||
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'driver' => 'database',
|
'driver' => 'database',
|
||||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
'table' => 'jobs',
|
||||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
'queue' => 'default',
|
||||||
'queue' => env('DB_QUEUE', 'default'),
|
'retry_after' => 90,
|
||||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
|
||||||
'after_commit' => false,
|
'after_commit' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
'beanstalkd' => [
|
'beanstalkd' => [
|
||||||
'driver' => 'beanstalkd',
|
'driver' => 'beanstalkd',
|
||||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
'host' => 'localhost',
|
||||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
'queue' => 'default',
|
||||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
'retry_after' => 90,
|
||||||
'block_for' => 0,
|
'block_for' => 0,
|
||||||
'after_commit' => false,
|
'after_commit' => false,
|
||||||
],
|
],
|
||||||
@@ -68,29 +66,13 @@ return [
|
|||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'driver' => 'redis',
|
'driver' => 'redis',
|
||||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
'connection' => 'default',
|
||||||
'queue' => env('REDIS_QUEUE', 'default'),
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
'retry_after' => 90,
|
||||||
'block_for' => null,
|
'block_for' => null,
|
||||||
'after_commit' => false,
|
'after_commit' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
'deferred' => [
|
|
||||||
'driver' => 'deferred',
|
|
||||||
],
|
|
||||||
|
|
||||||
'background' => [
|
|
||||||
'driver' => 'background',
|
|
||||||
],
|
|
||||||
|
|
||||||
'failover' => [
|
|
||||||
'driver' => 'failover',
|
|
||||||
'connections' => [
|
|
||||||
'database',
|
|
||||||
'deferred',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -105,7 +87,7 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'batching' => [
|
'batching' => [
|
||||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
'database' => env('DB_CONNECTION', 'mysql'),
|
||||||
'table' => 'job_batches',
|
'table' => 'job_batches',
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -115,16 +97,14 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| These options configure the behavior of failed queue job logging so you
|
| These options configure the behavior of failed queue job logging so you
|
||||||
| can control how and where failed jobs are stored. Laravel ships with
|
| can control which database and table are used to store the jobs that
|
||||||
| support for storing failed jobs in a simple file or in a database.
|
| have failed. You may change them to any database / table you wish.
|
||||||
|
|
|
||||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'failed' => [
|
'failed' => [
|
||||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
'database' => env('DB_CONNECTION', 'mysql'),
|
||||||
'table' => 'failed_jobs',
|
'table' => 'failed_jobs',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ return [
|
|||||||
| Default Session Driver
|
| Default Session Driver
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This option determines the default session driver that is utilized for
|
| This option controls the default session "driver" that will be used on
|
||||||
| incoming requests. Laravel supports a variety of storage options to
|
| requests. By default, we will use the lightweight native driver but
|
||||||
| persist session data. Database storage is a great default choice.
|
| you may specify any of the other wonderful drivers provided here.
|
||||||
|
|
|
|
||||||
| Supported: "file", "cookie", "database", "memcached",
|
| Supported: "file", "cookie", "database", "apc",
|
||||||
| "redis", "dynamodb", "array"
|
| "memcached", "redis", "dynamodb", "array"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -27,14 +27,13 @@ return [
|
|||||||
|
|
|
|
||||||
| Here you may specify the number of minutes that you wish the session
|
| Here you may specify the number of minutes that you wish the session
|
||||||
| to be allowed to remain idle before it expires. If you want them
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
| to expire immediately when the browser is closed then you may
|
| to immediately expire on the browser closing, set that option.
|
||||||
| indicate that via the expire_on_close configuration option.
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
'lifetime' => env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
'expire_on_close' => false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -80,24 +79,24 @@ return [
|
|||||||
| Session Database Table
|
| Session Database Table
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| When using the "database" session driver, you may specify the table to
|
| When using the "database" session driver, you may specify the table we
|
||||||
| be used to store sessions. Of course, a sensible default is defined
|
| should use to manage the sessions. Of course, a sensible default is
|
||||||
| for you; however, you're welcome to change this to another table.
|
| provided for you; however, you are free to change this as needed.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'table' => env('SESSION_TABLE', 'sessions'),
|
'table' => 'sessions',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Session Cache Store
|
| Session Cache Store
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| When using one of the framework's cache driven session backends, you may
|
| While using one of the framework's cache driven session backends you may
|
||||||
| define the cache store which should be used to store the session data
|
| list a cache store that should be used for these sessions. This value
|
||||||
| between requests. This must match one of your defined cache stores.
|
| must match with one of the application's configured cache "stores".
|
||||||
|
|
|
|
||||||
| Affects: "dynamodb", "memcached", "redis"
|
| Affects: "apc", "dynamodb", "memcached", "redis"
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -121,9 +120,9 @@ return [
|
|||||||
| Session Cookie Name
|
| Session Cookie Name
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Here you may change the name of the session cookie that is created by
|
| Here you may change the name of the cookie used to identify a session
|
||||||
| the framework. Typically, you should not need to change this value
|
| instance by ID. The name specified here will get used every time a
|
||||||
| since doing so does not grant a meaningful security improvement.
|
| new session cookie is created by the framework for every driver.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -139,20 +138,20 @@ return [
|
|||||||
|
|
|
|
||||||
| The session cookie path determines the path for which the cookie will
|
| The session cookie path determines the path for which the cookie will
|
||||||
| be regarded as available. Typically, this will be the root path of
|
| be regarded as available. Typically, this will be the root path of
|
||||||
| your application, but you're free to change this when necessary.
|
| your application but you are free to change this when necessary.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'path' => env('SESSION_PATH', '/'),
|
'path' => '/',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Session Cookie Domain
|
| Session Cookie Domain
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This value determines the domain and subdomains the session cookie is
|
| Here you may change the domain of the cookie used to identify a session
|
||||||
| available to. By default, the cookie will be available to the root
|
| in your application. This will determine which domains the cookie is
|
||||||
| domain without subdomains. Typically, this shouldn't be changed.
|
| available to in your application. A sensible default has been set.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -178,11 +177,11 @@ return [
|
|||||||
|
|
|
|
||||||
| Setting this value to true will prevent JavaScript from accessing the
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
| value of the cookie and the cookie will only be accessible through
|
| value of the cookie and the cookie will only be accessible through
|
||||||
| the HTTP protocol. It's unlikely you should disable this option.
|
| the HTTP protocol. You are free to modify this option if needed.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
'http_only' => true,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -191,15 +190,13 @@ return [
|
|||||||
|
|
|
|
||||||
| This option determines how your cookies behave when cross-site requests
|
| This option determines how your cookies behave when cross-site requests
|
||||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
| will set this value to "lax" to permit secure cross-site requests.
|
| will set this value to "lax" since this is a secure default value.
|
||||||
|
|
|
||||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
|
||||||
|
|
|
|
||||||
| Supported: "lax", "strict", "none", null
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
'same_site' => 'lax',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -212,22 +209,6 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
'partitioned' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Serialization
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This value controls the serialization strategy for session data, which
|
|
||||||
| is JSON by default. Setting this to "php" allows the storage of PHP
|
|
||||||
| objects in the session but can make an application vulnerable to
|
|
||||||
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
|
||||||
|
|
|
||||||
| Supported: "json", "php"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'serialization' => 'php',
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
ARG PHP_VERSION=8.3
|
ARG PHP_VERSION=8.3
|
||||||
ARG FRANKENPHP_VERSION=1.11
|
ARG FRANKENPHP_VERSION=1.8
|
||||||
ARG COMPOSER_VERSION=2.8
|
ARG COMPOSER_VERSION=2.8
|
||||||
|
ARG BUN_VERSION="latest"
|
||||||
ARG APP_ENV
|
ARG APP_ENV
|
||||||
ARG DOCKER_FILES_BASE_PATH="docker/prod/"
|
ARG DOCKER_FILES_BASE_PATH="docker/prod/"
|
||||||
|
|
||||||
@@ -15,13 +16,13 @@ RUN CGO_ENABLED=1 \
|
|||||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||||
CGO_CFLAGS=$(php-config --includes) \
|
CGO_CFLAGS=$(php-config --includes) \
|
||||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||||
xcaddy build \
|
xcaddy build v2.10.0 \
|
||||||
--output /usr/local/bin/frankenphp \
|
--output /usr/local/bin/frankenphp \
|
||||||
--with github.com/dunglas/frankenphp=./ \
|
--with github.com/dunglas/frankenphp=./ \
|
||||||
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
|
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
|
||||||
--with github.com/dunglas/caddy-cbrotli
|
--with github.com/dunglas/caddy-cbrotli
|
||||||
|
|
||||||
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}
|
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} AS base
|
||||||
|
|
||||||
COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||||
|
|
||||||
@@ -31,28 +32,26 @@ LABEL org.opencontainers.image.description="solidtime is a modern open source ti
|
|||||||
LABEL org.opencontainers.image.source="https://github.com/solidtime-io/solidtime"
|
LABEL org.opencontainers.image.source="https://github.com/solidtime-io/solidtime"
|
||||||
LABEL org.opencontainers.image.licenses="AGPL"
|
LABEL org.opencontainers.image.licenses="AGPL"
|
||||||
|
|
||||||
ARG USER_ID=1000
|
ARG WWWUSER=1000
|
||||||
ARG GROUP_ID=1000
|
ARG WWWGROUP=1000
|
||||||
ARG TZ=UTC
|
ARG TZ=UTC
|
||||||
|
ARG APP_DIR=/var/www/html
|
||||||
|
ARG APP_ENV
|
||||||
|
ARG APP_HOST
|
||||||
ARG DOCKER_FILES_BASE_PATH
|
ARG DOCKER_FILES_BASE_PATH
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
TERM=xterm-color \
|
TERM=xterm-color \
|
||||||
OCTANE_SERVER=frankenphp \
|
OCTANE_SERVER=frankenphp \
|
||||||
TZ=${TZ} \
|
TZ=${TZ} \
|
||||||
LANG=C.UTF-8 \
|
USER=octane \
|
||||||
USER=laravel \
|
ROOT=${APP_DIR} \
|
||||||
ROOT=/var/www/html \
|
APP_ENV=${APP_ENV} \
|
||||||
APP_ENV=production \
|
|
||||||
COMPOSER_ALLOW_SUPERUSER=1 \
|
|
||||||
COMPOSER_FUND=0 \
|
COMPOSER_FUND=0 \
|
||||||
COMPOSER_MAX_PARALLEL_HTTP=48 \
|
COMPOSER_MAX_PARALLEL_HTTP=24 \
|
||||||
WITH_HORIZON=false \
|
XDG_CONFIG_HOME=${APP_DIR}/.config \
|
||||||
WITH_SCHEDULER=false \
|
XDG_DATA_HOME=${APP_DIR}/.data \
|
||||||
WITH_REVERB=false \
|
SERVER_NAME=${APP_HOST}
|
||||||
WITH_SSR=false
|
|
||||||
|
|
||||||
ENV XDG_CONFIG_HOME=${ROOT}/.config XDG_DATA_HOME=${ROOT}/.data
|
|
||||||
|
|
||||||
WORKDIR ${ROOT}
|
WORKDIR ${ROOT}
|
||||||
|
|
||||||
@@ -61,9 +60,6 @@ SHELL ["/bin/bash", "-eou", "pipefail", "-c"]
|
|||||||
RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \
|
RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \
|
||||||
&& echo ${TZ} > /etc/timezone
|
&& echo ${TZ} > /etc/timezone
|
||||||
|
|
||||||
RUN echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
|
||||||
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
|
||||||
|
|
||||||
RUN apt-get update; \
|
RUN apt-get update; \
|
||||||
apt-get upgrade -yqq; \
|
apt-get upgrade -yqq; \
|
||||||
apt-get install -yqq --no-install-recommends --show-progress \
|
apt-get install -yqq --no-install-recommends --show-progress \
|
||||||
@@ -72,36 +68,40 @@ RUN apt-get update; \
|
|||||||
wget \
|
wget \
|
||||||
vim \
|
vim \
|
||||||
git \
|
git \
|
||||||
unzip \
|
|
||||||
ncdu \
|
ncdu \
|
||||||
procps \
|
procps \
|
||||||
|
unzip \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
supervisor \
|
supervisor \
|
||||||
libsodium-dev \
|
libsodium-dev \
|
||||||
# && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \
|
libbrotli-dev \
|
||||||
|
# Install PHP extensions (included with dunglas/frankenphp)
|
||||||
&& install-php-extensions \
|
&& install-php-extensions \
|
||||||
apcu \
|
|
||||||
bz2 \
|
bz2 \
|
||||||
pcntl \
|
pcntl \
|
||||||
mbstring \
|
mbstring \
|
||||||
bcmath \
|
bcmath \
|
||||||
sockets \
|
sockets \
|
||||||
|
pgsql \
|
||||||
pdo_pgsql \
|
pdo_pgsql \
|
||||||
opcache \
|
opcache \
|
||||||
exif \
|
exif \
|
||||||
pdo_mysql \
|
pdo_mysql \
|
||||||
zip \
|
zip \
|
||||||
uv \
|
uv \
|
||||||
|
vips \
|
||||||
intl \
|
intl \
|
||||||
gd \
|
gd \
|
||||||
redis \
|
redis \
|
||||||
rdkafka \
|
rdkafka \
|
||||||
ffi \
|
memcached \
|
||||||
|
igbinary \
|
||||||
ldap \
|
ldap \
|
||||||
&& apt-get -y autoremove \
|
&& apt-get -y autoremove \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& docker-php-source delete \
|
&& docker-php-source delete \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/lastlog /var/log/faillog
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||||
|
&& rm /var/log/lastlog /var/log/faillog
|
||||||
|
|
||||||
RUN arch="$(uname -m)" \
|
RUN arch="$(uname -m)" \
|
||||||
&& case "$arch" in \
|
&& case "$arch" in \
|
||||||
@@ -111,64 +111,106 @@ RUN arch="$(uname -m)" \
|
|||||||
x86) _cronic_fname='supercronic-linux-386' ;; \
|
x86) _cronic_fname='supercronic-linux-386' ;; \
|
||||||
*) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \
|
*) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \
|
||||||
esac \
|
esac \
|
||||||
&& wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \
|
&& wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.29/${_cronic_fname}" \
|
||||||
-O /usr/bin/supercronic \
|
-O /usr/bin/supercronic \
|
||||||
&& chmod +x /usr/bin/supercronic \
|
&& chmod +x /usr/bin/supercronic \
|
||||||
&& mkdir -p /etc/supercronic \
|
&& mkdir -p /etc/supercronic \
|
||||||
&& echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel
|
&& echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel
|
||||||
|
|
||||||
RUN userdel --remove --force www-data \
|
RUN userdel --remove --force www-data \
|
||||||
&& groupadd --force -g ${GROUP_ID} ${USER} \
|
&& groupadd --force -g ${WWWGROUP} ${USER} \
|
||||||
&& useradd -ms /bin/bash --no-log-init --no-user-group -g ${GROUP_ID} -u ${USER_ID} ${USER}
|
&& useradd -ms /bin/bash --no-log-init --no-user-group -g ${WWWGROUP} -u ${WWWUSER} ${USER} \
|
||||||
|
&& setcap -r /usr/local/bin/frankenphp
|
||||||
|
|
||||||
|
RUN chown -R ${USER}:${USER} ${ROOT} /var/{log,run} \
|
||||||
|
&& chmod -R a+rw ${ROOT} /var/{log,run}
|
||||||
|
|
||||||
RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini
|
RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini
|
||||||
|
|
||||||
COPY --link --from=vendor /usr/bin/composer /usr/bin/composer
|
USER ${USER}
|
||||||
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/supervisord.conf /etc/
|
|
||||||
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/
|
|
||||||
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/supervisord.*.conf /etc/supervisor/conf.d/
|
|
||||||
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/start-container /usr/local/bin/start-container
|
|
||||||
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/healthcheck /usr/local/bin/healthcheck
|
|
||||||
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini
|
|
||||||
#COPY --link composer.* ./
|
|
||||||
|
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} --from=vendor /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.conf /etc/
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.*.conf /etc/supervisor/conf.d/
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/start-container /usr/local/bin/start-container
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/healthcheck /usr/local/bin/healthcheck
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini ${PHP_INI_DIR}/conf.d/99-octane.ini
|
||||||
|
|
||||||
|
RUN chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck
|
||||||
|
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
#FROM base AS common
|
||||||
|
#
|
||||||
|
#USER ${USER}
|
||||||
|
#
|
||||||
|
#COPY --link --chown=${WWWUSER}:${WWWUSER} . .
|
||||||
|
#
|
||||||
#RUN composer install \
|
#RUN composer install \
|
||||||
# --no-dev \
|
# --no-dev \
|
||||||
# --no-interaction \
|
# --no-interaction \
|
||||||
# --no-autoloader \
|
# --no-autoloader \
|
||||||
# --no-ansi \
|
# --no-ansi \
|
||||||
# --no-scripts \
|
# --no-scripts \
|
||||||
# --no-progress \
|
|
||||||
# --audit
|
# --audit
|
||||||
|
|
||||||
|
###########################################
|
||||||
|
# Build frontend assets with Bun
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
#FROM oven/bun:${BUN_VERSION} AS build
|
||||||
|
#
|
||||||
|
#ARG APP_ENV
|
||||||
|
#
|
||||||
|
#ENV ROOT=/var/www/html \
|
||||||
|
# APP_ENV=${APP_ENV} \
|
||||||
|
# NODE_ENV=${APP_ENV:-production}
|
||||||
|
#
|
||||||
|
#WORKDIR ${ROOT}
|
||||||
|
#
|
||||||
#COPY --link package.json bun.lock* ./
|
#COPY --link package.json bun.lock* ./
|
||||||
|
#
|
||||||
#RUN bun install --frozen-lockfile
|
#RUN bun install --frozen-lockfile
|
||||||
|
#
|
||||||
|
#COPY --link . .
|
||||||
|
#COPY --link --from=common ${ROOT}/vendor vendor
|
||||||
|
#
|
||||||
|
#RUN bun run build
|
||||||
|
|
||||||
COPY --link . .
|
###########################################
|
||||||
|
|
||||||
|
#FROM common AS runner
|
||||||
|
|
||||||
|
USER ${USER}
|
||||||
|
|
||||||
|
ENV WITH_HORIZON=false \
|
||||||
|
WITH_SCHEDULER=false \
|
||||||
|
WITH_REVERB=false
|
||||||
|
|
||||||
|
COPY --link --chown=${WWWUSER}:${WWWUSER} . ./
|
||||||
|
|
||||||
|
RUN test -z "$(find . -name .git -print -quit)"
|
||||||
|
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
|
||||||
|
|
||||||
RUN mkdir -p \
|
RUN mkdir -p \
|
||||||
storage/framework/{sessions,views,cache,testing} \
|
storage/framework/{sessions,views,cache,testing} \
|
||||||
storage/logs \
|
storage/logs \
|
||||||
bootstrap/cache \
|
bootstrap/cache && chmod -R a+rw storage
|
||||||
&& chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck
|
|
||||||
|
|
||||||
RUN composer dump-autoload \
|
#RUN composer install \
|
||||||
--optimize \
|
# --classmap-authoritative \
|
||||||
--apcu \
|
# --no-interaction \
|
||||||
--no-dev
|
# --no-ansi \
|
||||||
|
# --no-dev \
|
||||||
|
# && composer clear-cache
|
||||||
|
|
||||||
#RUN bun run build
|
RUN cat .env
|
||||||
|
#RUN php artisan env
|
||||||
RUN chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \
|
|
||||||
&& find / -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
|
|
||||||
|
|
||||||
USER ${USER}
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
#EXPOSE 2019
|
|
||||||
#EXPOSE 8080
|
|
||||||
|
|
||||||
ENTRYPOINT ["start-container"]
|
ENTRYPOINT ["start-container"]
|
||||||
|
|
||||||
#HEALTHCHECK --start-period=30s --interval=10s --timeout=3s --retries=3 CMD healthcheck || exit 1
|
#HEALTHCHECK --start-period=5s --interval=2s --timeout=5s --retries=8 CMD healthcheck || exit 1
|
||||||
|
|||||||
@@ -22,13 +22,6 @@ elif [ "${container_mode}" = "reverb" ]; then
|
|||||||
echo "Healthcheck failed."
|
echo "Healthcheck failed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
elif [ "${container_mode}" = "ssr" ]; then
|
|
||||||
if [ "$(supervisorctl status inertia-ssr-server:inertia-ssr-server_0 | awk '{print tolower($2)}')" = "running" ]; then
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Healthcheck failed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [ "${container_mode}" = "worker" ]; then
|
elif [ "${container_mode}" = "worker" ]; then
|
||||||
if [ "$(supervisorctl status worker:worker_0 | awk '{print tolower($2)}')" = "running" ]; then
|
if [ "$(supervisorctl status worker:worker_0 | awk '{print tolower($2)}')" = "running" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -3,14 +3,6 @@
|
|||||||
|
|
||||||
admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
|
admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
|
||||||
|
|
||||||
log {
|
|
||||||
level {$CADDY_SERVER_LOG_LEVEL:WARN}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_https off
|
|
||||||
|
|
||||||
skip_install_trust
|
|
||||||
|
|
||||||
frankenphp {
|
frankenphp {
|
||||||
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
|
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
|
||||||
}
|
}
|
||||||
@@ -28,7 +20,7 @@
|
|||||||
|
|
||||||
{$CADDY_SERVER_SERVER_NAME} {
|
{$CADDY_SERVER_SERVER_NAME} {
|
||||||
log {
|
log {
|
||||||
level {$CADDY_SERVER_LOG_LEVEL:WARN}
|
level WARN
|
||||||
|
|
||||||
format filter {
|
format filter {
|
||||||
wrap {$CADDY_SERVER_LOGGER}
|
wrap {$CADDY_SERVER_LOGGER}
|
||||||
@@ -68,6 +60,7 @@
|
|||||||
error @rejected 401
|
error @rejected 401
|
||||||
|
|
||||||
php_server {
|
php_server {
|
||||||
|
index frankenphp-worker.php
|
||||||
try_files {path} frankenphp-worker.php
|
try_files {path} frankenphp-worker.php
|
||||||
resolve_root_symlink
|
resolve_root_symlink
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,65 @@
|
|||||||
[program:octane]
|
[program:octane]
|
||||||
process_name = %(program_name)s_%(process_num)s
|
process_name = %(program_name)s_%(process_num)s
|
||||||
command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-host=0.0.0.0 --admin-port=2019 --log-level=WARN --caddyfile=%(ENV_ROOT)s/docker/prod/deployment/octane/FrankenPHP/Caddyfile
|
command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-port=2019 --caddyfile=%(ENV_ROOT)s/docker/prod/deployment/octane/FrankenPHP/Caddyfile
|
||||||
user = %(ENV_USER)s
|
user = %(ENV_USER)s
|
||||||
priority = 1
|
priority = 1
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stopwaitsecs = 30
|
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
environment = LARAVEL_OCTANE = "1"
|
environment = LARAVEL_OCTANE = "1"
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile_maxbytes = 0
|
||||||
stderr_logfile = /dev/stderr
|
stderr_logfile = /dev/stderr
|
||||||
stderr_logfile_maxbytes = 0
|
stderr_logfile_maxbytes = 0
|
||||||
|
|
||||||
|
[program:horizon]
|
||||||
|
process_name = %(program_name)s_%(process_num)s
|
||||||
|
command = php %(ENV_ROOT)s/artisan horizon
|
||||||
|
user = %(ENV_USER)s
|
||||||
|
priority = 3
|
||||||
|
autostart = %(ENV_WITH_HORIZON)s
|
||||||
|
autorestart = true
|
||||||
|
stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
|
||||||
|
stdout_logfile_maxbytes = 200MB
|
||||||
|
stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
|
||||||
|
stderr_logfile_maxbytes = 200MB
|
||||||
|
stopwaitsecs = 3600
|
||||||
|
|
||||||
|
[program:scheduler]
|
||||||
|
process_name = %(program_name)s_%(process_num)s
|
||||||
|
command = supercronic -overlapping /etc/supercronic/laravel
|
||||||
|
user = %(ENV_USER)s
|
||||||
|
autostart = %(ENV_WITH_SCHEDULER)s
|
||||||
|
autorestart = true
|
||||||
|
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||||
|
stdout_logfile_maxbytes = 200MB
|
||||||
|
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||||
|
stderr_logfile_maxbytes = 200MB
|
||||||
|
|
||||||
|
[program:clear-scheduler-cache]
|
||||||
|
process_name = %(program_name)s_%(process_num)s
|
||||||
|
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||||
|
user = %(ENV_USER)s
|
||||||
|
autostart = %(ENV_WITH_SCHEDULER)s
|
||||||
|
autorestart = false
|
||||||
|
startsecs = 0
|
||||||
|
startretries = 1
|
||||||
|
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||||
|
stdout_logfile_maxbytes = 200MB
|
||||||
|
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||||
|
stderr_logfile_maxbytes = 200MB
|
||||||
|
|
||||||
|
[program:reverb]
|
||||||
|
process_name = %(program_name)s_%(process_num)s
|
||||||
|
command = php %(ENV_ROOT)s/artisan reverb:start
|
||||||
|
user = %(ENV_USER)s
|
||||||
|
priority = 2
|
||||||
|
autostart = %(ENV_WITH_REVERB)s
|
||||||
|
autorestart = true
|
||||||
|
stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
|
||||||
|
stdout_logfile_maxbytes = 200MB
|
||||||
|
stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
|
||||||
|
stderr_logfile_maxbytes = 200MB
|
||||||
|
minfds = 10000
|
||||||
|
|
||||||
[include]
|
[include]
|
||||||
files = /etc/supervisord.conf /etc/supervisor/conf.d/supervisord.services.conf
|
files = /etc/supervisord.conf
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
post_max_size = 100M
|
post_max_size = 100M
|
||||||
upload_max_filesize = 100M
|
upload_max_filesize = 100M
|
||||||
expose_php = 0
|
expose_php = 0
|
||||||
realpath_cache_size = 32M
|
realpath_cache_size = 16M
|
||||||
realpath_cache_ttl = 720
|
realpath_cache_ttl = 360
|
||||||
memory_limit = 256M
|
|
||||||
max_input_time = 5
|
max_input_time = 5
|
||||||
register_argc_argv = 0
|
register_argc_argv = 0
|
||||||
date.timezone = ${TZ:-UTC}
|
date.timezone = ${TZ:-UTC}
|
||||||
@@ -12,24 +11,18 @@ date.timezone = ${TZ:-UTC}
|
|||||||
[Opcache]
|
[Opcache]
|
||||||
opcache.enable = 1
|
opcache.enable = 1
|
||||||
opcache.enable_cli = 1
|
opcache.enable_cli = 1
|
||||||
opcache.memory_consumption = 256
|
opcache.memory_consumption = 256M
|
||||||
opcache.use_cwd = 0
|
opcache.use_cwd = 0
|
||||||
opcache.save_comments = 1
|
|
||||||
opcache.max_file_size = 0
|
opcache.max_file_size = 0
|
||||||
opcache.max_accelerated_files = 32531
|
opcache.max_accelerated_files = 32531
|
||||||
opcache.validate_timestamps = 0
|
opcache.validate_timestamps = 0
|
||||||
opcache.file_update_protection = 0
|
opcache.file_update_protection = 0
|
||||||
opcache.interned_strings_buffer = 16
|
opcache.interned_strings_buffer = 16
|
||||||
opcache.enable_file_override = 1
|
|
||||||
opcache.file_cache_consistency_checks = 0
|
|
||||||
opcache.file_cache = /tmp/opcache-file-cache
|
|
||||||
|
|
||||||
[JIT]
|
[JIT]
|
||||||
opcache.jit_buffer_size = 128M
|
opcache.jit_buffer_size = 128M
|
||||||
opcache.jit = tracing
|
opcache.jit = function
|
||||||
opcache.jit_hot_loop = 16
|
opcache.jit_prof_threshold = 0.001
|
||||||
opcache.jit_hot_func = 32
|
|
||||||
opcache.jit_hot_return = 4
|
|
||||||
opcache.jit_max_root_traces = 2048
|
opcache.jit_max_root_traces = 2048
|
||||||
opcache.jit_max_side_traces = 256
|
opcache.jit_max_side_traces = 256
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
[supervisord]
|
[supervisord]
|
||||||
nodaemon = true
|
nodaemon = true
|
||||||
user = %(ENV_USER)s
|
user = %(ENV_USER)s
|
||||||
|
logfile = /var/log/supervisor/supervisord.log
|
||||||
|
pidfile = /var/run/supervisord.pid
|
||||||
|
|
||||||
[supervisorctl]
|
[supervisorctl]
|
||||||
|
|
||||||
@@ -8,4 +10,4 @@ user = %(ENV_USER)s
|
|||||||
port = 127.0.0.1:9001
|
port = 127.0.0.1:9001
|
||||||
|
|
||||||
[rpcinterface:supervisor]
|
[rpcinterface:supervisor]
|
||||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ command = php %(ENV_ROOT)s/artisan horizon
|
|||||||
user = %(ENV_USER)s
|
user = %(ENV_USER)s
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile_maxbytes = 0
|
||||||
stderr_logfile = /dev/stderr
|
stderr_logfile = /dev/stderr
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
[program:inertia-ssr-server]
|
|
||||||
process_name = %(program_name)s_%(process_num)s
|
|
||||||
command = php %(ENV_ROOT)s/artisan inertia:start-ssr --runtime=bun --quiet
|
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = true
|
|
||||||
autorestart = true
|
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = /dev/stdout
|
|
||||||
stdout_logfile_maxbytes = 0
|
|
||||||
stderr_logfile = /dev/stderr
|
|
||||||
stderr_logfile_maxbytes = 0
|
|
||||||
|
|
||||||
[include]
|
|
||||||
files = /etc/supervisord.conf
|
|
||||||
@@ -4,8 +4,6 @@ command = php %(ENV_ROOT)s/artisan reverb:start
|
|||||||
user = %(ENV_USER)s
|
user = %(ENV_USER)s
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile_maxbytes = 0
|
||||||
stderr_logfile = /dev/stderr
|
stderr_logfile = /dev/stderr
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ command = supercronic -overlapping /etc/supercronic/laravel
|
|||||||
user = %(ENV_USER)s
|
user = %(ENV_USER)s
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile_maxbytes = 0
|
||||||
stderr_logfile = /dev/stderr
|
stderr_logfile = /dev/stderr
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
[program:horizon]
|
|
||||||
process_name = %(program_name)s_%(process_num)s
|
|
||||||
command = php %(ENV_ROOT)s/artisan horizon
|
|
||||||
user = %(ENV_USER)s
|
|
||||||
priority = 4
|
|
||||||
autostart = %(ENV_WITH_HORIZON)s
|
|
||||||
autorestart = true
|
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
|
|
||||||
stdout_logfile_maxbytes = 200MB
|
|
||||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
|
|
||||||
stderr_logfile_maxbytes = 200MB
|
|
||||||
stopwaitsecs = 3600
|
|
||||||
|
|
||||||
[program:scheduler]
|
|
||||||
process_name = %(program_name)s_%(process_num)s
|
|
||||||
command = supercronic -overlapping /etc/supercronic/laravel
|
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = %(ENV_WITH_SCHEDULER)s
|
|
||||||
autorestart = true
|
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
|
||||||
stdout_logfile_maxbytes = 200MB
|
|
||||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
|
||||||
stderr_logfile_maxbytes = 200MB
|
|
||||||
|
|
||||||
[program:clear-scheduler-cache]
|
|
||||||
process_name = %(program_name)s_%(process_num)s
|
|
||||||
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
|
||||||
user = %(ENV_USER)s
|
|
||||||
autostart = %(ENV_WITH_SCHEDULER)s
|
|
||||||
autorestart = false
|
|
||||||
startsecs = 0
|
|
||||||
startretries = 1
|
|
||||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
|
||||||
stdout_logfile_maxbytes = 200MB
|
|
||||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
|
||||||
stderr_logfile_maxbytes = 200MB
|
|
||||||
|
|
||||||
[program:reverb]
|
|
||||||
process_name = %(program_name)s_%(process_num)s
|
|
||||||
command = php %(ENV_ROOT)s/artisan reverb:start
|
|
||||||
user = %(ENV_USER)s
|
|
||||||
priority = 3
|
|
||||||
autostart = %(ENV_WITH_REVERB)s
|
|
||||||
autorestart = true
|
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
|
|
||||||
stdout_logfile_maxbytes = 200MB
|
|
||||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
|
|
||||||
stderr_logfile_maxbytes = 200MB
|
|
||||||
minfds = 10000
|
|
||||||
|
|
||||||
[program:inertia-ssr-server]
|
|
||||||
process_name = %(program_name)s_%(process_num)s
|
|
||||||
command = php %(ENV_ROOT)s/artisan inertia:start-ssr --runtime=bun --quiet
|
|
||||||
user = %(ENV_USER)s
|
|
||||||
priority = 2
|
|
||||||
autostart = %(ENV_WITH_SSR)s
|
|
||||||
autorestart = true
|
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/inertia-ssr-server.log
|
|
||||||
stdout_logfile_maxbytes = 200MB
|
|
||||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/inertia-ssr-server.log
|
|
||||||
stderr_logfile_maxbytes = 200MB
|
|
||||||
@@ -4,8 +4,6 @@ command = %(ENV_WORKER_COMMAND)s
|
|||||||
user = %(ENV_USER)s
|
user = %(ENV_USER)s
|
||||||
autostart = true
|
autostart = true
|
||||||
autorestart = true
|
autorestart = true
|
||||||
stopasgroup = true
|
|
||||||
killasgroup = true
|
|
||||||
stdout_logfile = /dev/stdout
|
stdout_logfile = /dev/stdout
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile_maxbytes = 0
|
||||||
stderr_logfile = /dev/stderr
|
stderr_logfile = /dev/stderr
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { adminTest as test, expect } from '../playwright/fixtures';
|
|
||||||
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from '../playwright/config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Index pages of all Filament resources that ship with the core application.
|
|
||||||
* Resources provided by extensions are intentionally left out, because
|
|
||||||
* extensions are not installed in CI.
|
|
||||||
*/
|
|
||||||
const resourcePages = [
|
|
||||||
{ path: '/admin/time-entries', heading: 'Time Entries' },
|
|
||||||
{ path: '/admin/projects', heading: 'Projects' },
|
|
||||||
{ path: '/admin/tasks', heading: 'Tasks' },
|
|
||||||
{ path: '/admin/clients', heading: 'Clients' },
|
|
||||||
{ path: '/admin/tags', heading: 'Tags' },
|
|
||||||
{ path: '/admin/reports', heading: 'Reports' },
|
|
||||||
{ path: '/admin/users', heading: 'Users' },
|
|
||||||
{ path: '/admin/organizations', heading: 'Organizations' },
|
|
||||||
{ path: '/admin/organization-invitations', heading: 'Invitations' },
|
|
||||||
{ path: '/admin/project-members', heading: 'Project Members' },
|
|
||||||
{ path: '/admin/tokens', heading: 'Tokens' },
|
|
||||||
{ path: '/admin/failed-jobs', heading: 'Failed Jobs' },
|
|
||||||
];
|
|
||||||
|
|
||||||
test.describe('Admin Panel Access', () => {
|
|
||||||
test.use({ storageState: undefined });
|
|
||||||
|
|
||||||
test('test that the admin panel redirects guests to the login page', async ({ page }) => {
|
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/admin');
|
|
||||||
await expect(page).toHaveURL(PLAYWRIGHT_BASE_URL + '/login');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('test that the admin panel is forbidden for users that are no super admin', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const email = `john+${Date.now()}_${Math.floor(Math.random() * 10000)}@doe.com`;
|
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/register');
|
|
||||||
await page.getByLabel('Name').fill('John Doe');
|
|
||||||
await page.getByLabel('Email').fill(email);
|
|
||||||
await page.getByLabel('Password', { exact: true }).fill(TEST_USER_PASSWORD);
|
|
||||||
await page.getByLabel('Confirm Password').fill(TEST_USER_PASSWORD);
|
|
||||||
await page.getByLabel('I agree to the Terms of').click();
|
|
||||||
await page.getByRole('button', { name: 'Register' }).click();
|
|
||||||
await expect(page.getByTestId('dashboard_view')).toBeVisible();
|
|
||||||
|
|
||||||
const response = await page.goto(PLAYWRIGHT_BASE_URL + '/admin');
|
|
||||||
expect(response?.status()).toBe(403);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Admin Panel', () => {
|
|
||||||
test('test that the admin dashboard loads with all widgets', async ({ page }) => {
|
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/admin');
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Dashboard', level: 1 })).toBeVisible();
|
|
||||||
|
|
||||||
// The widgets are loaded lazily via Livewire, so they only show up once
|
|
||||||
// their content has been rendered.
|
|
||||||
await expect(page.getByText('Total real users')).toBeVisible();
|
|
||||||
await expect(page.getByText('Placeholder users')).toBeVisible();
|
|
||||||
await expect(page.getByText('Active users in the last seven days')).toBeVisible();
|
|
||||||
await expect(page.getByText('User Registrations', { exact: true })).toBeVisible();
|
|
||||||
await expect(page.getByText('Time Entries Created', { exact: true })).toBeVisible();
|
|
||||||
await expect(page.getByText('Time Entries Imported', { exact: true })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const { path, heading } of resourcePages) {
|
|
||||||
test(`test that the admin page ${path} loads`, async ({ page }) => {
|
|
||||||
const response = await page.goto(PLAYWRIGHT_BASE_URL + path);
|
|
||||||
|
|
||||||
expect(response?.status()).toBe(200);
|
|
||||||
await expect(page.getByRole('heading', { name: heading, level: 1 })).toBeVisible();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -252,19 +252,19 @@ test('test that calendar page loads and displays time entries', async ({ page, c
|
|||||||
test('test that calendar navigation buttons work', async ({ page }) => {
|
test('test that calendar navigation buttons work', async ({ page }) => {
|
||||||
await goToCalendar(page);
|
await goToCalendar(page);
|
||||||
await expect(page.locator('.fc')).toBeVisible();
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
await expect(page.getByTestId('calendar-title')).toContainText('This Week');
|
|
||||||
|
|
||||||
// Navigate between named periods.
|
// Click the "next" button to navigate forward
|
||||||
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
|
// Click the "prev" button to navigate back
|
||||||
await page.getByRole('button', { name: 'Previous' }).click();
|
await page.getByRole('button', { name: 'Previous' }).click();
|
||||||
await expect(page.getByTestId('calendar-title')).toContainText('Last Week');
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Next' }).click();
|
|
||||||
await expect(page.getByTestId('calendar-title')).toContainText('This Week');
|
|
||||||
|
|
||||||
// Navigate forward first, then use the range button to return to today.
|
// Navigate forward first, then click today
|
||||||
await page.getByRole('button', { name: 'Next' }).click();
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
await expect(page.getByTestId('calendar-title')).toContainText('Next Week');
|
|
||||||
await page.getByRole('button', { name: 'today' }).click();
|
await page.getByRole('button', { name: 'today' }).click();
|
||||||
await expect(page.getByTestId('calendar-title')).toContainText('This Week');
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test that editing time entry description via calendar modal works', async ({ page, ctx }) => {
|
test('test that editing time entry description via calendar modal works', async ({ page, ctx }) => {
|
||||||
|
|||||||
@@ -18,25 +18,6 @@ import {
|
|||||||
// Each test registers a new user and creates test data via API
|
// Each test registers a new user and creates test data via API
|
||||||
test.describe.configure({ timeout: 30000 });
|
test.describe.configure({ timeout: 30000 });
|
||||||
|
|
||||||
test('date range picker labels and navigates reporting periods', async ({ page }) => {
|
|
||||||
await goToReporting(page);
|
|
||||||
|
|
||||||
const range = page.getByTestId('date_range_picker_display');
|
|
||||||
const previous = page.getByTestId('date_range_picker_previous');
|
|
||||||
const next = page.getByTestId('date_range_picker_next');
|
|
||||||
|
|
||||||
await expect(range).toContainText('Last 14 Days');
|
|
||||||
await expect(next).toBeDisabled();
|
|
||||||
|
|
||||||
await Promise.all([waitForReportingUpdate(page), previous.click()]);
|
|
||||||
await expect(range).not.toContainText('Last 14 Days');
|
|
||||||
|
|
||||||
// The initial reporting range is still fresh in Vue Query's cache, so returning to it
|
|
||||||
// does not necessarily trigger another aggregate request.
|
|
||||||
await next.click();
|
|
||||||
await expect(range).toContainText('Last 14 Days');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// Project Multiselect Dropdown Tests
|
// Project Multiselect Dropdown Tests
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1579,57 +1579,6 @@ test('test that project selection works in create modal', async ({ page, ctx })
|
|||||||
expect(createBody.data.project_id).not.toBeNull();
|
expect(createBody.data.project_id).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test that ctrl+enter submits the create modal after selecting a project via keyboard', async ({
|
|
||||||
page,
|
|
||||||
ctx,
|
|
||||||
}) => {
|
|
||||||
// Regression test for https://github.com/solidtime-io/solidtime/issues/1238
|
|
||||||
const projectName = 'Keyboard Submit Project ' + Math.floor(1 + Math.random() * 10000);
|
|
||||||
await createProjectViaApi(ctx, { name: projectName });
|
|
||||||
|
|
||||||
await goToTimeOverview(page);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Time entry actions' }).click();
|
|
||||||
await page.getByRole('menuitem', { name: 'Manual time entry' }).click();
|
|
||||||
await expect(page.getByRole('dialog')).toBeVisible();
|
|
||||||
// The menu that opened the modal animates out and only then hands focus back to its
|
|
||||||
// own trigger.
|
|
||||||
await expect(page.locator('[role="menu"]')).toHaveCount(0);
|
|
||||||
|
|
||||||
// Fill the description, then move to the project dropdown and select a project purely via keyboard
|
|
||||||
const description = page.getByRole('dialog').getByRole('textbox', { name: 'Description' });
|
|
||||||
await description.fill('Keyboard submit test');
|
|
||||||
await description.press('Tab');
|
|
||||||
await expect(
|
|
||||||
page.getByRole('dialog').getByRole('button', { name: 'No Project' })
|
|
||||||
).toBeFocused();
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await page.getByTestId('client_dropdown_search').fill(projectName);
|
|
||||||
await expect(page.getByRole('option', { name: projectName })).toBeVisible();
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
const projectTrigger = page.getByRole('dialog').getByRole('button', { name: projectName });
|
|
||||||
await expect(projectTrigger).toBeVisible();
|
|
||||||
// The trigger label updates on the next tick, but the dropdown keeps focus until its
|
|
||||||
// exit animation has finished and reka-ui hands focus back to the trigger.
|
|
||||||
await expect(projectTrigger).toBeFocused();
|
|
||||||
|
|
||||||
// Ctrl+Enter must submit even though focus is no longer on the description input
|
|
||||||
const [createResponse] = await Promise.all([
|
|
||||||
page.waitForResponse(
|
|
||||||
(response) => response.url().includes('/time-entries') && response.status() === 201
|
|
||||||
),
|
|
||||||
page.keyboard.press('Control+Enter'),
|
|
||||||
]);
|
|
||||||
const createBody = await createResponse.json();
|
|
||||||
expect(createBody.data.description).toBe('Keyboard submit test');
|
|
||||||
expect(createBody.data.project_id).not.toBeNull();
|
|
||||||
await expect(page.getByRole('dialog')).toBeHidden();
|
|
||||||
const newTimeEntry = page.locator('[data-testid="time_entry_row"]').first();
|
|
||||||
await expect(newTimeEntry.getByTestId('time_entry_description').first()).toHaveValue(
|
|
||||||
'Keyboard submit test'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('test that tag selection works in create modal', async ({ page }) => {
|
test('test that tag selection works in create modal', async ({ page }) => {
|
||||||
await goToTimeOverview(page);
|
await goToTimeOverview(page);
|
||||||
|
|
||||||
|
|||||||
@@ -409,20 +409,20 @@ test('navigating to previous week shows entries from that week', async ({ page,
|
|||||||
test('can navigate forward and return to current week', async ({ page }) => {
|
test('can navigate forward and return to current week', async ({ page }) => {
|
||||||
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
||||||
|
|
||||||
// Should show "This Week"
|
// Should show "This week"
|
||||||
await expect(page.getByTestId('timesheet_week_display')).toContainText('This Week');
|
await expect(page.getByTestId('timesheet_week_display')).toContainText('This week');
|
||||||
|
|
||||||
// Go to next week — the text assertions below auto-retry until the
|
// Go to next week — the text assertions below auto-retry until the
|
||||||
// header label flips.
|
// header label flips.
|
||||||
await page.getByTestId('timesheet_next_week').click();
|
await page.getByTestId('timesheet_next_week').click();
|
||||||
|
|
||||||
// Should no longer show "This Week"
|
// Should no longer show "This week"
|
||||||
await expect(page.getByTestId('timesheet_week_display')).not.toContainText('This Week');
|
await expect(page.getByTestId('timesheet_week_display')).not.toContainText('This week');
|
||||||
|
|
||||||
// Go back to this week
|
// Go back to this week
|
||||||
await page.getByTestId('timesheet_week_display').click();
|
await page.getByTestId('timesheet_week_display').click();
|
||||||
|
|
||||||
await expect(page.getByTestId('timesheet_week_display')).toContainText('This Week');
|
await expect(page.getByTestId('timesheet_week_display')).toContainText('This week');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../../playwright/config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The seeded user that is allowed to access the Filament admin panel.
|
|
||||||
*
|
|
||||||
* Panel access is granted by email address via the `SUPER_ADMINS` config, so
|
|
||||||
* the admin panel tests cannot use the throwaway users created by the default
|
|
||||||
* fixture and rely on the seeded user instead.
|
|
||||||
*/
|
|
||||||
export const SUPER_ADMIN_EMAIL = 'admin@example.com';
|
|
||||||
export const SEEDED_USER_PASSWORD = 'password';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log in as the seeded super admin user via the regular application login.
|
|
||||||
*/
|
|
||||||
export async function loginAsSuperAdmin(page: Page): Promise<void> {
|
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/login');
|
|
||||||
await page.getByLabel('Email').fill(SUPER_ADMIN_EMAIL);
|
|
||||||
await page.getByLabel('Password').fill(SEEDED_USER_PASSWORD);
|
|
||||||
await page.getByRole('button', { name: 'Log in' }).click();
|
|
||||||
await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 15000 });
|
|
||||||
}
|
|
||||||
@@ -68,7 +68,7 @@ export async function getPasswordResetUrl(
|
|||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
searchResult = await searchEmails(
|
searchResult = await searchEmails(
|
||||||
request,
|
request,
|
||||||
`to:${encodeURIComponent(recipientEmail)} subject:"Reset your password"`
|
`to:${encodeURIComponent(recipientEmail)} subject:"Reset Password"`
|
||||||
);
|
);
|
||||||
if (searchResult.messages.length > 0) break;
|
if (searchResult.messages.length > 0) break;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
{
|
{
|
||||||
"Billing": {
|
"Billing": {
|
||||||
"repository": "solidtime-io/extension-billing",
|
"repository": "solidtime-io/extension-billing",
|
||||||
"ref": "v0.0.7"
|
"ref": "v0.0.6"
|
||||||
},
|
},
|
||||||
"Services": {
|
"Services": {
|
||||||
"repository": "solidtime-io/extension-services",
|
"repository": "solidtime-io/extension-services",
|
||||||
"ref": "v0.0.3"
|
"ref": "v0.0.2"
|
||||||
},
|
},
|
||||||
"Invoicing": {
|
"Invoicing": {
|
||||||
"repository": "solidtime-io/extension-invoicing",
|
"repository": "solidtime-io/extension-invoicing",
|
||||||
"ref": "v0.0.7"
|
"ref": "v0.0.6"
|
||||||
},
|
|
||||||
"Auditing": {
|
|
||||||
"repository": "solidtime-io/extension-auditing",
|
|
||||||
"ref": "v0.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="array"/>
|
<env name="CACHE_DRIVER" value="array"/>
|
||||||
<env name="DB_CONNECTION" value="pgsql_test"/>
|
<env name="DB_CONNECTION" value="pgsql_test"/>
|
||||||
<env name="FILESYSTEM_DISK" value="local"/>
|
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="PULSE_ENABLED" value="false"/>
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
@@ -44,7 +43,5 @@
|
|||||||
<env name="NEWSLETTER_URL" value="null"/>
|
<env name="NEWSLETTER_URL" value="null"/>
|
||||||
<env name="PASSPORT_PERSONAL_ACCESS_CLIENT_ID" value="null"/>
|
<env name="PASSPORT_PERSONAL_ACCESS_CLIENT_ID" value="null"/>
|
||||||
<env name="PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET" value="null"/>
|
<env name="PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET" value="null"/>
|
||||||
<env name="PADDLE_API_KEY" value="test_phpunit_paddle_api_key"/>
|
|
||||||
<env name="PADDLE_SANDBOX" value="true"/>
|
|
||||||
</php>
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { test as baseTest } from '@playwright/test';
|
import { test as baseTest } from '@playwright/test';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from './config';
|
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from './config';
|
||||||
import { type TestContext, setupTestContext } from '../e2e/utils/api';
|
import { type TestContext, setupTestContext } from '../e2e/utils/api';
|
||||||
import { setupAdminUser, setupEmployeeUser } from '../e2e/utils/members';
|
import { setupAdminUser, setupEmployeeUser } from '../e2e/utils/members';
|
||||||
import { loginAsSuperAdmin } from '../e2e/utils/admin';
|
|
||||||
|
|
||||||
export * from '@playwright/test';
|
export * from '@playwright/test';
|
||||||
export type { TestContext };
|
export type { TestContext };
|
||||||
@@ -119,33 +116,3 @@ export const test = baseTest.extend<
|
|||||||
await closeAdmin();
|
await closeAdmin();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentication fixture for the Filament admin panel.
|
|
||||||
*
|
|
||||||
* The admin panel can only be accessed by the seeded super admin user, so the
|
|
||||||
* throwaway users of the default fixture cannot be used. Logging in is rate
|
|
||||||
* limited (5 attempts per minute and email address), therefore the session is
|
|
||||||
* created once per worker and reused by all tests of that worker.
|
|
||||||
*/
|
|
||||||
export const adminTest = baseTest.extend<object, { adminStorageState: string }>({
|
|
||||||
adminStorageState: [
|
|
||||||
async ({ browser }, use, workerInfo) => {
|
|
||||||
const fileName = path.resolve(
|
|
||||||
workerInfo.project.outputDir,
|
|
||||||
`.auth/admin-${workerInfo.workerIndex}.json`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(fileName)) {
|
|
||||||
const context = await browser.newContext({ storageState: undefined });
|
|
||||||
await loginAsSuperAdmin(await context.newPage());
|
|
||||||
await context.storageState({ path: fileName });
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
await use(fileName);
|
|
||||||
},
|
|
||||||
{ scope: 'worker' },
|
|
||||||
],
|
|
||||||
storageState: ({ adminStorageState }, use) => use(adminStorageState),
|
|
||||||
});
|
|
||||||
|
|||||||
1
public/fonts/filament/filament/inter/index.css
Normal file
1
public/fonts/filament/filament/inter/index.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-ext-wght-normal-SP7Z6XGK.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-wght-normal-DR6K5BQD.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-ext-wght-normal-A2J6H3EX.woff2") format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-wght-normal-ZABHMKQG.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-vietnamese-wght-normal-VWEHJHBA.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-ext-wght-normal-ZIT2UBIY.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-wght-normal-DIHYUR35.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -28,7 +28,7 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Create Client </span>
|
<span> Create Client </span>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Update Client </span>
|
<span> Update Client </span>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const roleDescription = computed(() => {
|
|||||||
v-model:show="showOwnershipTransferConfirmModal"
|
v-model:show="showOwnershipTransferConfirmModal"
|
||||||
:member-name="member.name"
|
:member-name="member.name"
|
||||||
@submit="submit"></MemberOwnershipTransferConfirmModal>
|
@submit="submit"></MemberOwnershipTransferConfirmModal>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="saveWithChecks()">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Update Member </span>
|
<span> Update Member </span>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Invite Member </span>
|
<span> Invite Member </span>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ async function submitBillableRate() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Edit Project {{ props.originalProject.name }} </span>
|
<span> Edit Project {{ props.originalProject.name }} </span>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span>Add Project Member</span>
|
<span>Add Project Member</span>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span>Edit Project Member</span>
|
<span>Edit Project Member</span>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ async function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Create Report </span>
|
<span> Create Report </span>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ async function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Create Report </span>
|
<span> Create Report </span>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const { handleApiRequestNotifications } = useNotificationsStore();
|
|||||||
|
|
||||||
const startDate = useSessionStorage<string>(
|
const startDate = useSessionStorage<string>(
|
||||||
'reporting-start-date',
|
'reporting-start-date',
|
||||||
getLocalizedDayJs(getDayJsInstance()().format()).subtract(13, 'd').format()
|
getLocalizedDayJs(getDayJsInstance()().format()).subtract(14, 'd').format()
|
||||||
);
|
);
|
||||||
const endDate = useSessionStorage<string>(
|
const endDate = useSessionStorage<string>(
|
||||||
'reporting-end-date',
|
'reporting-end-date',
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ useFocus(tagNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Update Tag </span>
|
<span> Update Tag </span>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ useFocus(taskNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Create Task </span>
|
<span> Create Task </span>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ useFocus(taskNameInput, { initialValue: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="show = false" @submit="submit">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span> Update Task </span>
|
<span> Update Task </span>
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ async function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogModal closeable :show="show" @close="emit('cancel')" @submit="submit">
|
<DialogModal closeable :show="show" @close="emit('cancel')">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Coffee class="w-5 h-5 text-text-secondary" />
|
<Coffee class="w-5 h-5 text-text-secondary" />
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DateRangeNavigator from '@/packages/ui/src/Input/DateRangeNavigator.vue';
|
import { Button } from '@/packages/ui/src/Buttons';
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon, CalendarIcon } from '@heroicons/vue/20/solid';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
isCurrentWeek: boolean;
|
||||||
weekNumber: number;
|
weekNumber: number;
|
||||||
rangeLabel: string;
|
weekRangeDisplay: string;
|
||||||
weekTotalFormatted: string;
|
weekTotalFormatted: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -17,16 +19,33 @@ defineEmits<{
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4 px-2 sm:px-4 lg:px-6">
|
<div class="flex flex-wrap items-center justify-between gap-4 mb-4 px-2 sm:px-4 lg:px-6">
|
||||||
<!-- Left: Week navigation -->
|
<!-- Left: Week navigation -->
|
||||||
<DateRangeNavigator
|
<div class="flex items-center gap-2">
|
||||||
:label="rangeLabel"
|
<Button
|
||||||
:secondary-label="`W${weekNumber}`"
|
variant="outline"
|
||||||
previous-test-id="timesheet_prev_week"
|
size="icon"
|
||||||
trigger-test-id="timesheet_week_display"
|
class="h-8 w-8"
|
||||||
next-test-id="timesheet_next_week"
|
data-testid="timesheet_prev_week"
|
||||||
trigger-aria-label="Return to current week"
|
@click="$emit('previous')">
|
||||||
@previous="$emit('previous')"
|
<ChevronLeftIcon class="h-4 w-4" />
|
||||||
@next="$emit('next')"
|
</Button>
|
||||||
@select="$emit('current')" />
|
<button
|
||||||
|
data-testid="timesheet_week_display"
|
||||||
|
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-text-primary hover:bg-card-background rounded-md transition"
|
||||||
|
@click="$emit('current')">
|
||||||
|
<CalendarIcon class="h-4 w-4 text-icon-default" />
|
||||||
|
<span v-if="isCurrentWeek">This week</span>
|
||||||
|
<span v-else>{{ weekRangeDisplay }}</span>
|
||||||
|
<span class="text-text-tertiary">· W{{ weekNumber }}</span>
|
||||||
|
</button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
data-testid="timesheet_next_week"
|
||||||
|
@click="$emit('next')">
|
||||||
|
<ChevronRightIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Right: Week total -->
|
<!-- Right: Week total -->
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
|||||||
|
|
||||||
const startDate = useSessionStorage<string>(
|
const startDate = useSessionStorage<string>(
|
||||||
'reporting-start-date',
|
'reporting-start-date',
|
||||||
getLocalizedDayJs(getDayJsInstance()().format()).subtract(13, 'd').format()
|
getLocalizedDayJs(getDayJsInstance()().format()).subtract(14, 'd').format()
|
||||||
);
|
);
|
||||||
const endDate = useSessionStorage<string>(
|
const endDate = useSessionStorage<string>(
|
||||||
'reporting-end-date',
|
'reporting-end-date',
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
getLocalizedDayJs,
|
getLocalizedDayJs,
|
||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
import { getBreakPlacementHint } from '@/packages/ui/src/utils/breakPlacement';
|
import { getBreakPlacementHint } from '@/packages/ui/src/utils/breakPlacement';
|
||||||
import { getDateRangeLabel } from '@/packages/ui/src/utils/dateRange';
|
|
||||||
import { useTimesheetWeek } from '@/utils/timesheet/useTimesheetWeek';
|
import { useTimesheetWeek } from '@/utils/timesheet/useTimesheetWeek';
|
||||||
import { useTimesheetCellMutations } from '@/utils/timesheet/useTimesheetCellMutations';
|
import { useTimesheetCellMutations } from '@/utils/timesheet/useTimesheetCellMutations';
|
||||||
import { useTimesheetRowMutations } from '@/utils/timesheet/useTimesheetRowMutations';
|
import { useTimesheetRowMutations } from '@/utils/timesheet/useTimesheetRowMutations';
|
||||||
@@ -45,6 +44,7 @@ const {
|
|||||||
weekEnd,
|
weekEnd,
|
||||||
weekDays,
|
weekDays,
|
||||||
weekNumber,
|
weekNumber,
|
||||||
|
isCurrentWeek,
|
||||||
todayDate,
|
todayDate,
|
||||||
goToPreviousWeek,
|
goToPreviousWeek,
|
||||||
goToNextWeek,
|
goToNextWeek,
|
||||||
@@ -109,9 +109,13 @@ const weekTotalFormatted = computed(() =>
|
|||||||
formatHumanReadableDuration(grandTotal.value, intervalFormat.value, numberFormat.value)
|
formatHumanReadableDuration(grandTotal.value, intervalFormat.value, numberFormat.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
const weekRangeLabel = computed(() =>
|
const weekRangeDisplay = computed(() => {
|
||||||
getDateRangeLabel(weekStart.value, weekStart.value.add(6, 'day'))
|
const start = weekStart.value;
|
||||||
);
|
const end = start.add(6, 'day');
|
||||||
|
return start.month() === end.month()
|
||||||
|
? `${start.format('MMM D')} - ${end.format('D')}`
|
||||||
|
: `${start.format('MMM D')} - ${end.format('MMM D')}`;
|
||||||
|
});
|
||||||
|
|
||||||
// ── Cell / row mutation handlers ──────────────────────────────────
|
// ── Cell / row mutation handlers ──────────────────────────────────
|
||||||
const {
|
const {
|
||||||
@@ -204,8 +208,9 @@ async function createTag(name: string): Promise<Tag | undefined> {
|
|||||||
<AppLayout title="Timesheet" data-testid="timesheet_view">
|
<AppLayout title="Timesheet" data-testid="timesheet_view">
|
||||||
<div class="pt-5 lg:pt-8 pb-4 lg:pb-6">
|
<div class="pt-5 lg:pt-8 pb-4 lg:pb-6">
|
||||||
<TimesheetHeader
|
<TimesheetHeader
|
||||||
|
:is-current-week="isCurrentWeek"
|
||||||
:week-number="weekNumber"
|
:week-number="weekNumber"
|
||||||
:range-label="weekRangeLabel"
|
:week-range-display="weekRangeDisplay"
|
||||||
:week-total-formatted="weekTotalFormatted"
|
:week-total-formatted="weekTotalFormatted"
|
||||||
@previous="goToPreviousWeek"
|
@previous="goToPreviousWeek"
|
||||||
@next="goToNextWeek"
|
@next="goToNextWeek"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'submit']);
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
show: {
|
show: {
|
||||||
@@ -24,12 +24,7 @@ const close = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal :show="show" :max-width="maxWidth" :closeable="closeable" @close="close">
|
||||||
:show="show"
|
|
||||||
:max-width="maxWidth"
|
|
||||||
:closeable="closeable"
|
|
||||||
@close="close"
|
|
||||||
@submit="emit('submit')">
|
|
||||||
<div class="px-4 lg:px-6 py-4">
|
<div class="px-4 lg:px-6 py-4">
|
||||||
<div class="text-lg font-medium text-text-primary" role="heading">
|
<div class="text-lg font-medium text-text-primary" role="heading">
|
||||||
<slot name="title" />
|
<slot name="title" />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user