feat : first commit
This commit is contained in:
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{compose.yaml,compose.*.yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
49
.env
Normal file
49
.env
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# In all environments, the following files are loaded if they exist,
|
||||||
|
# the latter taking precedence over the former:
|
||||||
|
#
|
||||||
|
# * .env contains default values for the environment variables needed by the app
|
||||||
|
# * .env.local uncommitted file with local overrides
|
||||||
|
# * .env.$APP_ENV committed environment-specific defaults
|
||||||
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||||
|
#
|
||||||
|
# Real environment variables win over .env files.
|
||||||
|
#
|
||||||
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||||
|
# https://symfony.com/doc/current/configuration/secrets.html
|
||||||
|
#
|
||||||
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
APP_SHARE_DIR=var/share
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/routing ###
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
###< symfony/routing ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||||
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
|
#
|
||||||
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||||
|
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> nelmio/cors-bundle ###
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
|
###< nelmio/cors-bundle ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=9efb9a2ec48439c723621d0c6393d04da5516c8fa00ecdba1660717b4f996867
|
||||||
|
JWT_COOKIE_SECURE=0
|
||||||
|
JWT_COOKIE_SAMESITE=lax
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
3
.env.test
Normal file
3
.env.test
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# define your env variables for the test env here
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
APP_SECRET='$ecretf0rt3st'
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
8
.idea/SIRH.iml
generated
Normal file
8
.idea/SIRH.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="postgres@localhost" uuid="9cad43df-2147-4989-b7a4-443067034884">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://localhost:5434/postgres</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/data_source_mapping.xml
generated
Normal file
6
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourcePerFileMappings">
|
||||||
|
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/9cad43df-2147-4989-b7a4-443067034884/console.sql" value="9cad43df-2147-4989-b7a4-443067034884" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/material_theme_project_new.xml
generated
Normal file
10
.idea/material_theme_project_new.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MaterialThemeProjectNewConfig">
|
||||||
|
<option name="metadata">
|
||||||
|
<MTProjectMetadataState>
|
||||||
|
<option name="userId" value="-7cf7a629:19c1e9ce3f8:-7e79" />
|
||||||
|
</MTProjectMetadataState>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/SIRH.iml" filepath="$PROJECT_DIR$/.idea/SIRH.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
20
.idea/php.xml
generated
Normal file
20
.idea/php.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="highlightLevel" value="WARNING" />
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||||
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PsalmOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
56
.php-cs-fixer.dist.php
Normal file
56
.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PhpCsFixer\Config;
|
||||||
|
use PhpCsFixer\Finder;
|
||||||
|
|
||||||
|
$finder = Finder::create()
|
||||||
|
->in('src')
|
||||||
|
->notName('Kernel.php')
|
||||||
|
;
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'@Symfony' => true,
|
||||||
|
'@PSR12' => true,
|
||||||
|
'@PHP84Migration' => true,
|
||||||
|
'@PER-CS' => true,
|
||||||
|
'@PhpCsFixer' => true,
|
||||||
|
'strict_param' => true,
|
||||||
|
'strict_comparison' => true,
|
||||||
|
'no_useless_else' => true,
|
||||||
|
'no_useless_return' => true,
|
||||||
|
'binary_operator_spaces' => [
|
||||||
|
'operators' => [
|
||||||
|
'=' => 'align_single_space_minimal',
|
||||||
|
'||' => 'align_single_space_minimal',
|
||||||
|
'=>' => 'align_single_space_minimal',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'global_namespace_import' => [
|
||||||
|
'import_classes' => true,
|
||||||
|
'import_constants' => true,
|
||||||
|
'import_functions' => true,
|
||||||
|
],
|
||||||
|
'modernize_strpos' => true, // needs PHP 8+ or polyfill
|
||||||
|
'no_superfluous_phpdoc_tags' => true,
|
||||||
|
'echo_tag_syntax' => true,
|
||||||
|
'semicolon_after_instruction' => true,
|
||||||
|
'combine_consecutive_unsets' => true,
|
||||||
|
'ternary_to_null_coalescing' => true,
|
||||||
|
'declare_strict_types' => true,
|
||||||
|
'operator_linebreak' => [
|
||||||
|
'position' => 'beginning',
|
||||||
|
],
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'single_line_throw' => false,
|
||||||
|
'php_unit_test_class_requires_covers' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$config = new Config();
|
||||||
|
|
||||||
|
return $config
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules($rules)
|
||||||
|
->setFinder($finder)
|
||||||
|
;
|
||||||
21
bin/console
Executable file
21
bin/console
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
|
||||||
|
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||||
|
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||||
|
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
|
||||||
|
return new Application($kernel);
|
||||||
|
};
|
||||||
4
bin/phpunit
Executable file
4
bin/phpunit
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||||
31
commit-msg
Normal file
31
commit-msg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MSG_FILE="${1}"
|
||||||
|
FIRST_LINE="$(head -n 1 "$MSG_FILE" | tr -d '\r')"
|
||||||
|
|
||||||
|
# Autoriser commits auto-générés par git
|
||||||
|
if [[ "$FIRST_LINE" =~ ^Merge\ ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Types autorisés (MINUSCULES uniquement)
|
||||||
|
# Optionnel: scope => feat(auth) : ...
|
||||||
|
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+'
|
||||||
|
|
||||||
|
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
|
||||||
|
echo "❌ Message de commit invalide."
|
||||||
|
echo ""
|
||||||
|
echo "➡️ Format attendu : <type>(<scope optionnel>) : <message>"
|
||||||
|
echo "➡️ Types autorisés (minuscules uniquement) :"
|
||||||
|
echo " build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Exemples :"
|
||||||
|
echo " feat : add login page"
|
||||||
|
echo " fix(auth) : prevent null token crash"
|
||||||
|
echo " docs : update README"
|
||||||
|
echo ""
|
||||||
|
echo "❌ Exemple refusé :"
|
||||||
|
echo " Feat : add login page"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
90
composer.json
Normal file
90
composer.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"type": "project",
|
||||||
|
"license": "proprietary",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"api-platform/doctrine-orm": "^4.2",
|
||||||
|
"api-platform/symfony": "^4.2",
|
||||||
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
|
"doctrine/orm": "^3.6",
|
||||||
|
"lexik/jwt-authentication-bundle": "^3.2",
|
||||||
|
"nelmio/cors-bundle": "^2.6",
|
||||||
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
|
"symfony/asset": "8.0.*",
|
||||||
|
"symfony/console": "8.0.*",
|
||||||
|
"symfony/dotenv": "8.0.*",
|
||||||
|
"symfony/expression-language": "8.0.*",
|
||||||
|
"symfony/flex": "^2",
|
||||||
|
"symfony/framework-bundle": "8.0.*",
|
||||||
|
"symfony/property-access": "8.0.*",
|
||||||
|
"symfony/property-info": "8.0.*",
|
||||||
|
"symfony/runtime": "8.0.*",
|
||||||
|
"symfony/security-bundle": "8.0.*",
|
||||||
|
"symfony/serializer": "8.0.*",
|
||||||
|
"symfony/twig-bundle": "8.0.*",
|
||||||
|
"symfony/validator": "8.0.*",
|
||||||
|
"symfony/yaml": "8.0.*"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"php-http/discovery": true,
|
||||||
|
"symfony/flex": true,
|
||||||
|
"symfony/runtime": true
|
||||||
|
},
|
||||||
|
"bump-after-update": true,
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
|
"symfony/polyfill-iconv": "*",
|
||||||
|
"symfony/polyfill-php72": "*",
|
||||||
|
"symfony/polyfill-php73": "*",
|
||||||
|
"symfony/polyfill-php74": "*",
|
||||||
|
"symfony/polyfill-php80": "*",
|
||||||
|
"symfony/polyfill-php81": "*",
|
||||||
|
"symfony/polyfill-php82": "*",
|
||||||
|
"symfony/polyfill-php83": "*",
|
||||||
|
"symfony/polyfill-php84": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"auto-scripts": {
|
||||||
|
"cache:clear": "symfony-cmd",
|
||||||
|
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||||
|
},
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/symfony": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"symfony": {
|
||||||
|
"allow-contrib": false,
|
||||||
|
"require": "8.0.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.93",
|
||||||
|
"phpunit/phpunit": "^12.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
10212
composer.lock
generated
Normal file
10212
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
config/bundles.php
Normal file
23
config/bundles.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||||
|
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||||
|
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
|
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||||
|
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||||
|
|
||||||
|
return [
|
||||||
|
FrameworkBundle::class => ['all' => true],
|
||||||
|
TwigBundle::class => ['all' => true],
|
||||||
|
SecurityBundle::class => ['all' => true],
|
||||||
|
DoctrineBundle::class => ['all' => true],
|
||||||
|
DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
|
NelmioCorsBundle::class => ['all' => true],
|
||||||
|
ApiPlatformBundle::class => ['all' => true],
|
||||||
|
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
|
];
|
||||||
10
config/packages/api_platform.yaml
Normal file
10
config/packages/api_platform.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
api_platform:
|
||||||
|
title: Hello API Platform
|
||||||
|
version: 1.0.0
|
||||||
|
formats:
|
||||||
|
json: ['application/json']
|
||||||
|
jsonld: ['application/ld+json']
|
||||||
|
defaults:
|
||||||
|
stateless: true
|
||||||
|
cache_headers:
|
||||||
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
|
#prefix_seed: your_vendor_name/app_name
|
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default.
|
||||||
|
# The data in this cache should persist between deploys.
|
||||||
|
# Other options include:
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
#app: cache.adapter.redis
|
||||||
|
#default_redis_provider: redis://localhost
|
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||||
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default
|
||||||
|
#pools:
|
||||||
|
#my.dedicated.cache: null
|
||||||
48
config/packages/doctrine.yaml
Normal file
48
config/packages/doctrine.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
|
# IMPORTANT: You MUST configure your server version,
|
||||||
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
#server_version: '16'
|
||||||
|
|
||||||
|
profiling_collect_backtrace: '%kernel.debug%'
|
||||||
|
orm:
|
||||||
|
validate_xml_mapping: true
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
|
identity_generation_preferences:
|
||||||
|
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||||
|
auto_mapping: true
|
||||||
|
mappings:
|
||||||
|
App:
|
||||||
|
type: attribute
|
||||||
|
is_bundle: false
|
||||||
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
|
prefix: 'App\Entity'
|
||||||
|
alias: App
|
||||||
|
controller_resolver:
|
||||||
|
auto_mapping: false
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
# "TEST_TOKEN" is typically set by ParaTest
|
||||||
|
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
doctrine:
|
||||||
|
orm:
|
||||||
|
query_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
result_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.result_cache_pool
|
||||||
|
|
||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
pools:
|
||||||
|
doctrine.result_cache_pool:
|
||||||
|
adapter: cache.app
|
||||||
|
doctrine.system_cache_pool:
|
||||||
|
adapter: cache.system
|
||||||
6
config/packages/doctrine_migrations.yaml
Normal file
6
config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
doctrine_migrations:
|
||||||
|
migrations_paths:
|
||||||
|
# namespace is arbitrary but should be different from App\Migrations
|
||||||
|
# as migrations classes should NOT be autoloaded
|
||||||
|
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||||
|
enable_profiler: false
|
||||||
15
config/packages/framework.yaml
Normal file
15
config/packages/framework.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
|
framework:
|
||||||
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
|
session: true
|
||||||
|
|
||||||
|
#esi: true
|
||||||
|
#fragments: true
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
test: true
|
||||||
|
session:
|
||||||
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
24
config/packages/lexik_jwt_authentication.yaml
Normal file
24
config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
lexik_jwt_authentication:
|
||||||
|
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||||
|
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||||
|
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||||
|
remove_token_from_body_when_cookies_used: true
|
||||||
|
token_extractors:
|
||||||
|
authorization_header:
|
||||||
|
enabled: false
|
||||||
|
cookie:
|
||||||
|
enabled: true
|
||||||
|
name: BEARER
|
||||||
|
query_parameter:
|
||||||
|
enabled: false
|
||||||
|
set_cookies:
|
||||||
|
BEARER:
|
||||||
|
lifetime: 86400
|
||||||
|
samesite: lax
|
||||||
|
path: /
|
||||||
|
secure: '%env(bool:JWT_COOKIE_SECURE)%'
|
||||||
|
httpOnly: true
|
||||||
|
api_platform:
|
||||||
|
check_path: /api/login_check
|
||||||
|
username_path: username
|
||||||
|
password_path: password
|
||||||
11
config/packages/nelmio_cors.yaml
Normal file
11
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
nelmio_cors:
|
||||||
|
defaults:
|
||||||
|
origin_regex: true
|
||||||
|
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
|
allow_headers: ['Content-Type', 'Authorization']
|
||||||
|
allow_credentials: true
|
||||||
|
expose_headers: ['Link']
|
||||||
|
max_age: 3600
|
||||||
|
paths:
|
||||||
|
'^/': null
|
||||||
5
config/packages/prod/api_platform.yaml
Normal file
5
config/packages/prod/api_platform.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
api_platform:
|
||||||
|
enable_docs: false
|
||||||
|
enable_swagger: false
|
||||||
|
enable_swagger_ui: false
|
||||||
|
docs_formats: []
|
||||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
property_info:
|
||||||
|
with_constructor_extractor: true
|
||||||
10
config/packages/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
default_uri: '%env(DEFAULT_URI)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
||||||
62
config/packages/security.yaml
Normal file
62
config/packages/security.yaml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
security:
|
||||||
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
|
password_hashers:
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
|
providers:
|
||||||
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\User
|
||||||
|
property: username
|
||||||
|
|
||||||
|
firewalls:
|
||||||
|
dev:
|
||||||
|
# Ensure dev tools and static assets are always allowed
|
||||||
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
|
security: false
|
||||||
|
login:
|
||||||
|
pattern: ^/login_check
|
||||||
|
stateless: true
|
||||||
|
provider: app_user_provider
|
||||||
|
json_login:
|
||||||
|
check_path: /login_check
|
||||||
|
username_path: username
|
||||||
|
password_path: password
|
||||||
|
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||||
|
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||||
|
api:
|
||||||
|
pattern: ^/api
|
||||||
|
stateless: true
|
||||||
|
provider: app_user_provider
|
||||||
|
jwt: ~
|
||||||
|
logout:
|
||||||
|
path: /api/logout
|
||||||
|
target: /login
|
||||||
|
enable_csrf: false
|
||||||
|
delete_cookies:
|
||||||
|
BEARER:
|
||||||
|
path: /
|
||||||
|
|
||||||
|
# Activate different ways to authenticate:
|
||||||
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||||
|
# switch_user: true
|
||||||
|
|
||||||
|
# Note: Only the *first* matching rule is applied
|
||||||
|
access_control:
|
||||||
|
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
security:
|
||||||
|
password_hashers:
|
||||||
|
# Password hashers are resource-intensive by design to ensure security.
|
||||||
|
# In tests, it's safe to reduce their cost to improve performance.
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
|
algorithm: auto
|
||||||
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
|
time_cost: 3 # Lowest possible value for argon
|
||||||
|
memory_cost: 10 # Lowest possible value for argon
|
||||||
6
config/packages/twig.yaml
Normal file
6
config/packages/twig.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
twig:
|
||||||
|
file_name_pattern: '*.twig'
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
twig:
|
||||||
|
strict_variables: true
|
||||||
11
config/packages/validator.yaml
Normal file
11
config/packages/validator.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
# Enables validator auto-mapping support.
|
||||||
|
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||||
|
#auto_mapping:
|
||||||
|
# App\Entity\: []
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
not_compromised_password: false
|
||||||
7
config/preload.php
Normal file
7
config/preload.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||||
|
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||||
|
}
|
||||||
1758
config/reference.php
Normal file
1758
config/reference.php
Normal file
File diff suppressed because it is too large
Load Diff
11
config/routes.yaml
Normal file
11
config/routes.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
||||||
|
|
||||||
|
# This file is the entry point to configure the routes of your app.
|
||||||
|
# Methods with the #[Route] attribute are automatically imported.
|
||||||
|
# See also https://symfony.com/doc/current/routing.html
|
||||||
|
|
||||||
|
# To list all registered routes, run the following command:
|
||||||
|
# bin/console debug:router
|
||||||
|
|
||||||
|
controllers:
|
||||||
|
resource: routing.controllers
|
||||||
4
config/routes/api_platform.yaml
Normal file
4
config/routes/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
api_platform:
|
||||||
|
resource: .
|
||||||
|
type: api_platform
|
||||||
|
prefix: /api
|
||||||
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
when@dev:
|
||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||||
|
prefix: /_error
|
||||||
7
config/routes/security.yaml
Normal file
7
config/routes/security.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
_security_logout:
|
||||||
|
resource: security.route_loader.logout
|
||||||
|
type: service
|
||||||
|
|
||||||
|
api_login:
|
||||||
|
path: /login_check
|
||||||
|
methods: [POST]
|
||||||
23
config/services.yaml
Normal file
23
config/services.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
||||||
|
|
||||||
|
# This file is the entry point to configure your own services.
|
||||||
|
# Files in the packages/ subdirectory configure your dependencies.
|
||||||
|
# See also https://symfony.com/doc/current/service_container/import.html
|
||||||
|
|
||||||
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
|
parameters:
|
||||||
|
|
||||||
|
services:
|
||||||
|
# default configuration for services in *this* file
|
||||||
|
_defaults:
|
||||||
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
|
|
||||||
|
# makes classes in src/ available to be used as services
|
||||||
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
|
App\:
|
||||||
|
resource: '../src/'
|
||||||
|
|
||||||
|
# add more service definitions when explicit configuration is needed
|
||||||
|
# please note that last definitions always *replace* previous ones
|
||||||
57
docker-compose.yml
Normal file
57
docker-compose.yml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
services:
|
||||||
|
php:
|
||||||
|
container_name: php-${DOCKER_APP_NAME}-fpm
|
||||||
|
build:
|
||||||
|
context: ./docker/php
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
DOCKER_PHP_VERSION: ${DOCKER_PHP_VERSION}
|
||||||
|
DOCKER_NODE_VERSION: ${DOCKER_NODE_VERSION}
|
||||||
|
CURRENT_UID: ${CURRENT_UID}
|
||||||
|
CURRENT_GID: ${CURRENT_GID}
|
||||||
|
environment:
|
||||||
|
PHP_IDE_CONFIG: serverName=${DOCKER_APP_NAME}-docker
|
||||||
|
XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal}
|
||||||
|
XDEBUG_CONFIG: client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} client_port=9003
|
||||||
|
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
|
COMPOSER_HOME: /tmp/composer
|
||||||
|
COMPOSER_CACHE_DIR: /tmp/composer/cache
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www/html
|
||||||
|
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
||||||
|
- ~/.config:/var/www/.config # Pour la config de yarn
|
||||||
|
- ~/.composer:/var/www/.composer # Pour la config de composer
|
||||||
|
- ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
|
||||||
|
- ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
|
- ./LOG:/var/www/html/LOG
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
restart: unless-stopped
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: nginx-${DOCKER_APP_NAME}
|
||||||
|
depends_on:
|
||||||
|
- php
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www/html:ro
|
||||||
|
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
||||||
9
docker/.env.docker
Normal file
9
docker/.env.docker
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
DOCKER_APP_NAME=sirh
|
||||||
|
DOCKER_PHP_VERSION=8.4.6
|
||||||
|
DOCKER_NODE_VERSION=24.12.0
|
||||||
|
APP_USER=www-data
|
||||||
|
POSTGRES_DB=sirh
|
||||||
|
POSTGRES_USER=root
|
||||||
|
POSTGRES_PASSWORD=root
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||||
52
docker/nginx/conf.d/sirh.conf
Normal file
52
docker/nginx/conf.d/sirh.conf
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /var/www/html/frontend/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /_wdt/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /_profiler/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /bundles/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/login_check {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_param SCRIPT_NAME /index.php;
|
||||||
|
fastcgi_param PATH_INFO /login_check;
|
||||||
|
fastcgi_param REQUEST_URI /login_check;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
docker/php/Dockerfile
Normal file
129
docker/php/Dockerfile
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
ARG DOCKER_PHP_VERSION
|
||||||
|
|
||||||
|
FROM php:${DOCKER_PHP_VERSION}-fpm-bullseye
|
||||||
|
|
||||||
|
ARG DOCKER_NODE_VERSION
|
||||||
|
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
||||||
|
|
||||||
|
# Installer les dépendances et extensions PHP nécessaires
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libpng-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
libbz2-dev \
|
||||||
|
libgmp-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libonig-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
unixodbc-dev \
|
||||||
|
libsqlite3-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libc-client-dev \
|
||||||
|
libkrb5-dev \
|
||||||
|
freetds-dev \
|
||||||
|
vim \
|
||||||
|
tcpdump \
|
||||||
|
dnsutils \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
unzip \
|
||||||
|
&& docker-php-ext-install -j$(nproc) \
|
||||||
|
intl \
|
||||||
|
zip \
|
||||||
|
bcmath \
|
||||||
|
bz2 \
|
||||||
|
calendar \
|
||||||
|
exif \
|
||||||
|
gd \
|
||||||
|
gettext \
|
||||||
|
gmp \
|
||||||
|
ldap \
|
||||||
|
# mysqli \
|
||||||
|
pcntl \
|
||||||
|
pdo_pgsql \
|
||||||
|
# pdo_mysql \
|
||||||
|
# pdo_sqlite \
|
||||||
|
# pdo_sqlsrv \
|
||||||
|
soap \
|
||||||
|
sockets \
|
||||||
|
sysvsem \
|
||||||
|
xsl
|
||||||
|
|
||||||
|
|
||||||
|
# Installation de node
|
||||||
|
RUN wget -qO- "https://nodejs.org/dist/v${DOCKER_NODE_VERSION}/node-v${DOCKER_NODE_VERSION}-linux-x64.tar.xz" | tar xJC /tmp/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/bin /usr/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/include /usr/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/lib /usr/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/share /usr/ && \
|
||||||
|
npm install --global yarn
|
||||||
|
|
||||||
|
# installation/activation d'extensions php
|
||||||
|
RUN pecl install xdebug
|
||||||
|
RUN docker-php-ext-enable xdebug && \
|
||||||
|
docker-php-ext-install zip && \
|
||||||
|
docker-php-ext-install gd && \
|
||||||
|
docker-php-ext-install soap && \
|
||||||
|
docker-php-ext-configure intl && \
|
||||||
|
docker-php-ext-install intl
|
||||||
|
|
||||||
|
# Configuration spéciale pour quelques extensions
|
||||||
|
# RUN docker-php-ext-configure pdo_odbc --with-pdo-odbc=unixODBC,/usr && \
|
||||||
|
# docker-php-ext-install pdo_odbc \
|
||||||
|
RUN docker-php-ext-enable opcache
|
||||||
|
|
||||||
|
# Configurer Oracle OCI8 (nécessite le SDK Oracle, à installer manuellement ou à lier via les dépendances)
|
||||||
|
#RUN apt-get update && apt-get -y install wget unzip libaio1 && \
|
||||||
|
# wget https://download.oracle.com/otn_software/linux/instantclient/2340000/instantclient-basic-linux.x64-23.4.0.24.05.zip && \
|
||||||
|
# unzip -o instantclient-basic-linux.x64-23.4.0.24.05.zip -d /usr/local && \
|
||||||
|
# wget https://download.oracle.com/otn_software/linux/instantclient/2340000/instantclient-sdk-linux.x64-23.4.0.24.05.zip && \
|
||||||
|
# unzip -o instantclient-sdk-linux.x64-23.4.0.24.05.zip -d /usr/local
|
||||||
|
#
|
||||||
|
#RUN echo 'instantclient,/usr/local/instantclient_23_4' | pecl install oci8-3.4.0 \
|
||||||
|
# && docker-php-ext-enable oci8
|
||||||
|
#
|
||||||
|
#ENV ORACLE_BASE /usr/local/instantclient_23_4
|
||||||
|
#ENV LD_LIBRARY_PATH /usr/local/instantclient_23_4
|
||||||
|
#ENV TNS_ADMIN /usr/local/instantclient_23_4
|
||||||
|
#ENV ORACLE_HOME /usr/local/instantclient_23_4
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration pour utiliser Kerberos avec IMAP (si nécessaire)
|
||||||
|
# RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
|
||||||
|
# && docker-php-ext-install imap
|
||||||
|
|
||||||
|
# installation de composer
|
||||||
|
RUN rm -rf /var/cache/apk/* && rm -rf /tmp/* && \
|
||||||
|
curl --insecure https://getcomposer.org/composer.phar -o /usr/bin/composer && chmod +x /usr/bin/composer
|
||||||
|
|
||||||
|
# cache Composer pour www-data
|
||||||
|
RUN mkdir -p /var/www/.composer/cache/vcs \
|
||||||
|
&& chown -R www-data:www-data /var/www/.composer
|
||||||
|
ENV COMPOSER_HOME=/var/www/.composer
|
||||||
|
|
||||||
|
# Création de la structure du projet
|
||||||
|
RUN mkdir /var/www/html/LOG
|
||||||
|
|
||||||
|
###> User ###
|
||||||
|
ARG CURRENT_UID
|
||||||
|
ARG CURRENT_GID
|
||||||
|
# mapping du user host avec www-data
|
||||||
|
RUN usermod -o -u ${CURRENT_UID} www-data && groupmod -o -g ${CURRENT_GID} www-data
|
||||||
|
RUN chown www-data:www-data -R /var/www/*
|
||||||
|
RUN chown www-data:www-data -R /var/www/.*
|
||||||
|
###< User ###
|
||||||
|
|
||||||
|
RUN rm -rf \
|
||||||
|
/var/lib/apt/lists/* \
|
||||||
|
/tmp/* \
|
||||||
|
/var/tmp/*
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
9
docker/php/config/docker-php-ext-xdebug.ini
Normal file
9
docker/php/config/docker-php-ext-xdebug.ini
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
zend_extension = /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so
|
||||||
|
xdebug.mode=debug
|
||||||
|
xdebug.idekey=PHPSTORM
|
||||||
|
xdebug.start_with_request=yes
|
||||||
|
xdebug.discover_client_host=1
|
||||||
|
xdebug.client_port=9003
|
||||||
|
xdebug.log="/var/www/html/LOG/xdebug.log"
|
||||||
|
xdebug.log_level=0
|
||||||
|
xdebug.connect_timeout_ms=2
|
||||||
4
docker/php/config/php.ini
Normal file
4
docker/php/config/php.ini
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[Date]
|
||||||
|
; Defines the default timezone used by the date functions
|
||||||
|
; http://php.net/date.timezone
|
||||||
|
date.timezone = Europe/Paris
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
75
frontend/README.md
Normal file
75
frontend/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Nuxt Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
5
frontend/app.vue
Normal file
5
frontend/app.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
56
frontend/components/AppDrawer.vue
Normal file
56
frontend/components/AppDrawer.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="modelValue" class="fixed inset-0 z-50">
|
||||||
|
<Transition name="drawer-backdrop">
|
||||||
|
<div class="absolute inset-0 bg-black/40" @click="close" />
|
||||||
|
</Transition>
|
||||||
|
<Transition name="drawer-panel">
|
||||||
|
<div class="absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-xl">
|
||||||
|
<div class="flex items-center justify-between border-b border-neutral-200 bg-tertiary-500 px-6 py-4">
|
||||||
|
<h2 class="text-lg font-semibold text-neutral-900">
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md p-2 text-neutral-500 hover:bg-neutral-100"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ modelValue: boolean; title?: string }>()
|
||||||
|
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||||
|
|
||||||
|
const close = () => emit('update:modelValue', false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drawer-backdrop-enter-active,
|
||||||
|
.drawer-backdrop-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-backdrop-enter-from,
|
||||||
|
.drawer-backdrop-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-panel-enter-active,
|
||||||
|
.drawer-panel-leave-active {
|
||||||
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-panel-enter-from,
|
||||||
|
.drawer-panel-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
183
frontend/composables/useApi.ts
Normal file
183
frontend/composables/useApi.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import type { FetchOptions } from 'ofetch'
|
||||||
|
import { $fetch, FetchError } from 'ofetch'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
export type AnyObject = Record<string, unknown>
|
||||||
|
|
||||||
|
export type ApiClient = {
|
||||||
|
get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<Blob>
|
||||||
|
post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
delete<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
|
||||||
|
FetchOptions<ResponseType> & {
|
||||||
|
toast?: boolean
|
||||||
|
toastTitle?: string
|
||||||
|
toastErrorMessage?: string
|
||||||
|
toastSuccessMessage?: string
|
||||||
|
toastErrorKey?: string
|
||||||
|
toastSuccessKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useApi = (): ApiClient => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const baseURL = config.public.apiBase ?? '/api'
|
||||||
|
const toast = useToast()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
let isHandlingUnauthorized = false
|
||||||
|
const i18n = nuxtApp.$i18n as
|
||||||
|
| {
|
||||||
|
t: (key: string) => string
|
||||||
|
te?: (key: string) => boolean
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
|
||||||
|
const te = (key: string) => (i18n?.te ? i18n.te(key) : false)
|
||||||
|
|
||||||
|
const extractErrorMessage = (error: unknown, responseData?: unknown): string => {
|
||||||
|
const data = responseData ?? (error as FetchError)?.data
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
const record = data as Record<string, unknown>
|
||||||
|
return (
|
||||||
|
(record['hydra:description'] as string) ||
|
||||||
|
(record.detail as string) ||
|
||||||
|
(record.message as string) ||
|
||||||
|
(record.error as string) ||
|
||||||
|
(record.title as string) ||
|
||||||
|
(record['hydra:title'] as string) ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (error as FetchError)?.message ?? 'Erreur inconnue.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodErrorKeys: Record<string, string> = {
|
||||||
|
GET: 'errors.http.get',
|
||||||
|
POST: 'errors.http.post',
|
||||||
|
PUT: 'errors.http.put',
|
||||||
|
PATCH: 'errors.http.patch',
|
||||||
|
DELETE: 'errors.http.delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = $fetch.create({
|
||||||
|
baseURL,
|
||||||
|
retry: 0,
|
||||||
|
credentials: 'include',
|
||||||
|
onResponse({ options, response }) {
|
||||||
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
|
if (apiOptions?.toast === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response?.status && response.status >= 400) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const successKey = apiOptions?.toastSuccessKey
|
||||||
|
const successMessage =
|
||||||
|
apiOptions?.toastSuccessMessage ||
|
||||||
|
(successKey ? (te(successKey) ? t(successKey) : successKey) : '')
|
||||||
|
|
||||||
|
if (successMessage) {
|
||||||
|
toast.success({
|
||||||
|
title: 'Succès',
|
||||||
|
message: successMessage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onResponseError({ response, error, options }) {
|
||||||
|
if (response?.status === 401) {
|
||||||
|
const requestUrl = typeof options?.url === 'string' ? options.url : ''
|
||||||
|
if (!requestUrl.includes('/login_check') && !requestUrl.includes('/logout')) {
|
||||||
|
if (!isHandlingUnauthorized) {
|
||||||
|
isHandlingUnauthorized = true
|
||||||
|
auth.clearSession()
|
||||||
|
const route = useRoute()
|
||||||
|
if (route.path !== '/login') {
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
isHandlingUnauthorized = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
|
if (apiOptions?.toast === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const method =
|
||||||
|
typeof options?.method === 'string' ? options.method.toUpperCase() : 'GET'
|
||||||
|
const defaultKey = methodErrorKeys[method]
|
||||||
|
const defaultMessage =
|
||||||
|
defaultKey && te(defaultKey) ? t(defaultKey) : ''
|
||||||
|
const errorKey = apiOptions?.toastErrorKey
|
||||||
|
const errorMessage =
|
||||||
|
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
|
||||||
|
const extractedMessage = extractErrorMessage(error, response?._data)
|
||||||
|
const message =
|
||||||
|
apiOptions?.toastErrorMessage ||
|
||||||
|
errorMessage ||
|
||||||
|
defaultMessage ||
|
||||||
|
extractedMessage ||
|
||||||
|
'Une erreur est survenue.'
|
||||||
|
|
||||||
|
toast.error({
|
||||||
|
title: apiOptions?.toastTitle ?? 'Erreur',
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const request = <T>(
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
||||||
|
url: string,
|
||||||
|
options: ApiFetchOptions<'json'> = {}
|
||||||
|
) => {
|
||||||
|
const needsJsonBody = method === 'POST' || method === 'PUT'
|
||||||
|
const needsMergePatch = method === 'PATCH'
|
||||||
|
|
||||||
|
const headers = new Headers(options.headers as HeadersInit | undefined)
|
||||||
|
|
||||||
|
if (needsMergePatch && !headers.has('Content-Type')) {
|
||||||
|
headers.set('Content-Type', 'application/merge-patch+json')
|
||||||
|
} else if (needsJsonBody && !headers.has('Content-Type')) {
|
||||||
|
headers.set('Content-Type', 'application/json')
|
||||||
|
}
|
||||||
|
|
||||||
|
return client<T>(url, { ...options, method, headers })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('GET', url, { ...options, query })
|
||||||
|
},
|
||||||
|
getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
|
||||||
|
return client<Blob>(url, { ...options, method: 'GET', query, responseType: 'blob' })
|
||||||
|
},
|
||||||
|
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('POST', url, { ...options, body })
|
||||||
|
},
|
||||||
|
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('PUT', url, { ...options, body })
|
||||||
|
},
|
||||||
|
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('PATCH', url, { ...options, body })
|
||||||
|
},
|
||||||
|
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('DELETE', url, { ...options, query })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/i18n.config.ts
Normal file
7
frontend/i18n.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineI18nConfig } from '@nuxtjs/i18n'
|
||||||
|
|
||||||
|
export default defineI18nConfig(() => ({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'fr',
|
||||||
|
messages: {}
|
||||||
|
}))
|
||||||
@@ -1,64 +1,22 @@
|
|||||||
{
|
{
|
||||||
"errors": {
|
"errors": {
|
||||||
"http": {
|
"http": {
|
||||||
"get": "Impossible de récupérer les données.",
|
"get": "Impossible de récupérer les données.",
|
||||||
"post": "Impossible de créer la ressource.",
|
"post": "Impossible de créer la ressource.",
|
||||||
"put": "Impossible de mettre à jour la ressource.",
|
"put": "Impossible de mettre à jour la ressource.",
|
||||||
"patch": "Impossible de mettre à jour la ressource.",
|
"patch": "Impossible de mettre à jour la ressource.",
|
||||||
"delete": "Impossible de supprimer la ressource."
|
"delete": "Impossible de supprimer la ressource."
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Identifiants invalides.",
|
||||||
|
"logout": "Impossible de se déconnecter.",
|
||||||
|
"session": "Session expirée"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"reception": {
|
"success": {
|
||||||
"list": "Impossible de récupérer la liste des réceptions.",
|
"auth": {
|
||||||
"fetch": "Impossible de récupérer la réception.",
|
"login": "Connexion réussie.",
|
||||||
"create": "Impossible de créer la réception.",
|
"logout": "Déconnexion réussie."
|
||||||
"update": "Impossible de mettre à jour la réception.",
|
}
|
||||||
"weigh": "Impossible de récupérer la pesée."
|
|
||||||
},
|
|
||||||
"receptionType": {
|
|
||||||
"list": "Impossible de récupérer la liste des types de réception."
|
|
||||||
},
|
|
||||||
"merchandiseType": {
|
|
||||||
"list": "Impossible de récupérer la liste des types de marchandises."
|
|
||||||
},
|
|
||||||
"building": {
|
|
||||||
"list": "Impossible de récupérer la liste des bâtiments."
|
|
||||||
},
|
|
||||||
"pelletType": {
|
|
||||||
"list": "Impossible de récupérer la liste des types de granulés."
|
|
||||||
},
|
|
||||||
"receptionPelletBuilding": {
|
|
||||||
"list": "Impossible de récupérer la liste des dépôts de granulés.",
|
|
||||||
"create": "Impossible d'enregistrer le dépôt de granulés.",
|
|
||||||
"delete": "Impossible de supprimer le dépôt de granulés."
|
|
||||||
},
|
|
||||||
"supplier": {
|
|
||||||
"list": "Impossible de récupérer la liste des fournisseurs."
|
|
||||||
},
|
|
||||||
"truck": {
|
|
||||||
"list": "Impossible de récupérer la liste des camions."
|
|
||||||
},
|
|
||||||
"carrier": {
|
|
||||||
"list": "Impossible de récupérer la liste des transporteurs."
|
|
||||||
},
|
|
||||||
"driver": {
|
|
||||||
"list": "Impossible de récupérer la liste des chauffeurs."
|
|
||||||
},
|
|
||||||
"vehicle": {
|
|
||||||
"list": "Impossible de récupérer la liste des immatriculations."
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"login": "Identifiants invalides.",
|
|
||||||
"users": "Impossible de récupérer les utilisateurs.",
|
|
||||||
"logout": "Impossible de se déconnecter."
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"reception": {
|
|
||||||
"update": "Réception mise à jour avec succès."
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"login": "Connexion réussie.",
|
|
||||||
"logout": "Déconnexion réussie."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-primary-500 from-primary-50 via-white to-neutral-100 text-neutral-900">
|
<div class="min-h-screen bg-tertiary-500 from-tertiary-50 via-white to-neutral-100 text-neutral-900">
|
||||||
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
|
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,11 +1,64 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
$END$
|
<div class="min-h-screen">
|
||||||
|
<div class="flex min-h-screen">
|
||||||
|
<aside class="flex w-64 flex-col border-r border-neutral-200 bg-tertiary-500 no-print">
|
||||||
|
<div>
|
||||||
|
<img src="/malio.png" alt="Logo" class="w-auto"/>
|
||||||
|
</div>
|
||||||
|
<nav class="flex-1 px-4 pb-6">
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="flex items-center gap-3 px-4 pb-3 pt-6 text-md font-semibold text-neutral-700 hover:bg-primary-50 hover:text-primary-600 border-t border-secondary-500"
|
||||||
|
active-class="bg-primary-50 text-primary-600"
|
||||||
|
>
|
||||||
|
Tableau de bord
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/calendar"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-primary-50 hover:text-primary-600"
|
||||||
|
active-class="bg-primary-50 text-primary-600"
|
||||||
|
>
|
||||||
|
Calendrier
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/employees"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-primary-50 hover:text-primary-600"
|
||||||
|
active-class="bg-primary-50 text-primary-600"
|
||||||
|
>
|
||||||
|
Employés
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/absence-types"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-primary-50 hover:text-primary-600"
|
||||||
|
active-class="bg-primary-50 text-primary-600"
|
||||||
|
>
|
||||||
|
Types d'absence
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full rounded-lg px-4 py-2 text-md font-semibold text-white bg-primary-500"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="flex-1 px-8 py-8">
|
||||||
|
<slot/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<script setup lang="ts">
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
</style>
|
const handleLogout = async () => {
|
||||||
|
await auth.logout()
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
1
frontend/locales/fr.json
Normal file
1
frontend/locales/fr.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
16
frontend/middleware/auth.global.ts
Normal file
16
frontend/middleware/auth.global.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const isLogin = to.path === '/login'
|
||||||
|
|
||||||
|
if (!auth.checked) {
|
||||||
|
await auth.ensureSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLogin && !auth.isAuthenticated) {
|
||||||
|
return navigateTo('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLogin && auth.isAuthenticated) {
|
||||||
|
return navigateTo('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
39
frontend/nuxt.config.ts
Normal file
39
frontend/nuxt.config.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-07-15',
|
||||||
|
devtools: {enabled: false},
|
||||||
|
ssr: false,
|
||||||
|
app: {
|
||||||
|
baseURL: process.env.NUXT_PUBLIC_APP_BASE || '/'
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'nuxt-toast',
|
||||||
|
'@nuxtjs/i18n'
|
||||||
|
],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devServer: {port: 3001},
|
||||||
|
toast: {
|
||||||
|
settings: {
|
||||||
|
timeout: 10000,
|
||||||
|
closeOnClick: true,
|
||||||
|
progressBar: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
strategy: 'no_prefix',
|
||||||
|
defaultLocale: 'fr',
|
||||||
|
langDir: 'locales',
|
||||||
|
locales: [
|
||||||
|
{code: 'fr', file: 'fr.json', name: 'Français'}
|
||||||
|
],
|
||||||
|
vueI18n: './i18n.config.ts'
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
strict: true
|
||||||
|
}
|
||||||
|
})
|
||||||
14181
frontend/package-lock.json
generated
Normal file
14181
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
frontend/package.json
Normal file
25
frontend/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
"nuxt": "^4.3.0",
|
||||||
|
"nuxt-toast": "^1.4.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.27",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxtjs/tailwindcss": "^6.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
211
frontend/pages/absence-types.vue
Normal file
211
frontend/pages/absence-types.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between pb-12">
|
||||||
|
<h1 class="text-4xl font-bold text-primary-500">Types d'absence</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
|
@click="openCreate"
|
||||||
|
>
|
||||||
|
Ajouter un type
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isLoading && absenceTypes.length === 0"
|
||||||
|
class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600"
|
||||||
|
>
|
||||||
|
Aucun type pour le moment.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="overflow-hidden rounded-lg border border-neutral-200 bg-white">
|
||||||
|
<div class="grid grid-cols-[120px_120px_1fr_200px] gap-4 border-b border-neutral-200 bg-tertiary-500 px-6 py-3 text-md font-semibold text-neutral-700">
|
||||||
|
<span class="text-left">Code</span>
|
||||||
|
<span class="text-left">Libellé</span>
|
||||||
|
<span class="text-left">Couleur</span>
|
||||||
|
<span class="text-right">Actions</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="px-6 py-4 text-md text-neutral-500">
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="type in absenceTypes"
|
||||||
|
:key="type.id"
|
||||||
|
class="grid grid-cols-[120px_120px_1fr_200px] items-center gap-4 border-b border-neutral-100 px-6 py-3 text-md text-neutral-800 last:border-b-0"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-left">{{ type.code }}</span>
|
||||||
|
<span class="text-left">{{ type.label }}</span>
|
||||||
|
<div class="flex items-center gap-2 justify-start">
|
||||||
|
<span
|
||||||
|
class="inline-block h-3 w-3 rounded-full"
|
||||||
|
:style="{ backgroundColor: type.color }"
|
||||||
|
/>
|
||||||
|
<span class="text-md uppercase text-neutral-500">{{ type.color }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border border-neutral-200 px-2 py-1 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
@click="openEdit(type)"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border border-red-200 px-2 py-1 text-md font-semibold text-red-600 hover:bg-red-50"
|
||||||
|
@click="confirmDelete(type)"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
||||||
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="code">Code</label>
|
||||||
|
<input
|
||||||
|
id="code"
|
||||||
|
v-model="form.code"
|
||||||
|
type="text"
|
||||||
|
maxlength="10"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="label">Libellé</label>
|
||||||
|
<input
|
||||||
|
id="label"
|
||||||
|
v-model="form.label"
|
||||||
|
type="text"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="color">Couleur</label>
|
||||||
|
<div class="mt-2 flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
id="color"
|
||||||
|
v-model="form.color"
|
||||||
|
type="color"
|
||||||
|
class="h-10 w-16 cursor-pointer rounded-md border border-neutral-300 bg-white p-1"
|
||||||
|
/>
|
||||||
|
<span class="text-md font-semibold text-neutral-600">{{ form.color }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
@click="closeDrawer"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</AppDrawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AbsenceType } from '~/services/dto/absence-type'
|
||||||
|
import { createAbsenceType, deleteAbsenceType, listAbsenceTypes, updateAbsenceType } from '~/services/absence-types'
|
||||||
|
|
||||||
|
const isDrawerOpen = ref(false)
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const absenceTypes = ref<AbsenceType[]>([])
|
||||||
|
const editingType = ref<AbsenceType | null>(null)
|
||||||
|
|
||||||
|
const drawerTitle = computed(() =>
|
||||||
|
editingType.value ? "Modifier un type" : "Ajouter un type"
|
||||||
|
)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
code: '',
|
||||||
|
label: '',
|
||||||
|
color: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadAbsenceTypes = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
absenceTypes.value = await listAbsenceTypes()
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadAbsenceTypes)
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.code = ''
|
||||||
|
form.label = ''
|
||||||
|
form.color = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
editingType.value = null
|
||||||
|
resetForm()
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = (type: AbsenceType) => {
|
||||||
|
editingType.value = type
|
||||||
|
form.code = type.code
|
||||||
|
form.label = type.label
|
||||||
|
form.color = type.color
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
isDrawerOpen.value = false
|
||||||
|
editingType.value = null
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting.value) return
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
try {
|
||||||
|
if (editingType.value) {
|
||||||
|
await updateAbsenceType(editingType.value.id, {
|
||||||
|
code: form.code,
|
||||||
|
label: form.label,
|
||||||
|
color: form.color
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createAbsenceType({
|
||||||
|
code: form.code,
|
||||||
|
label: form.label,
|
||||||
|
color: form.color
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDrawer()
|
||||||
|
await loadAbsenceTypes()
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async (type: AbsenceType) => {
|
||||||
|
const ok = window.confirm(`Supprimer le type ${type.label} ?`)
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
|
await deleteAbsenceType(type.id)
|
||||||
|
await loadAbsenceTypes()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
510
frontend/pages/calendar.vue
Normal file
510
frontend/pages/calendar.vue
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4 pb-10 no-print">
|
||||||
|
<h1 class="text-4xl font-bold text-primary-500">Calendrier des absences</h1>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<select
|
||||||
|
v-model="selectedMonth"
|
||||||
|
class="rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||||
|
>
|
||||||
|
<option v-for="month in months" :key="month.value" :value="month.value">
|
||||||
|
{{ month.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-model="selectedYear"
|
||||||
|
class="rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||||
|
>
|
||||||
|
<option v-for="year in years" :key="year" :value="year">
|
||||||
|
{{ year }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
|
@click="openCreateFromToday"
|
||||||
|
>
|
||||||
|
Ajouter une absence
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
@click="printMonth('1')"
|
||||||
|
>
|
||||||
|
Imprimer 1 mois
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
@click="printMonth('3')"
|
||||||
|
>
|
||||||
|
Imprimer 3 mois
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-h-[80vh] overflow-auto rounded-lg border border-neutral-200 bg-white no-print">
|
||||||
|
<div class="min-w-[900px]">
|
||||||
|
<div class="grid" :style="gridStyle">
|
||||||
|
<div class="sticky left-0 z-10 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-md font-semibold text-neutral-700">
|
||||||
|
Employés
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="day in daysInMonth"
|
||||||
|
:key="day.date"
|
||||||
|
class="border-b border-neutral-200 bg-tertiary-500 px-2 py-3 text-center text-xs font-semibold text-neutral-700"
|
||||||
|
>
|
||||||
|
<div>{{ day.label }}</div>
|
||||||
|
<div class="text-[10px] text-neutral-500">{{ day.weekday }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-for="employee in employees" :key="employee.id">
|
||||||
|
<div class="sticky left-0 z-10 border-b border-neutral-100 bg-white px-4 py-3 text-md font-semibold text-neutral-800">
|
||||||
|
{{ employee.firstName }} {{ employee.lastName }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="day in daysInMonth"
|
||||||
|
:key="employee.id + '-' + day.date"
|
||||||
|
class="border-b border-neutral-100 px-2 py-2 text-center text-xs text-neutral-800"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex h-8 w-full items-center justify-center rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800 hover:border-primary-500/40"
|
||||||
|
:style="getCellStyle(employee.id, day.date)"
|
||||||
|
@click="openCreate(employee, day.date)"
|
||||||
|
>
|
||||||
|
<span v-if="getCellCode(employee.id, day.date)">
|
||||||
|
{{ getCellCode(employee.id, day.date) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="printMode" class="print-only space-y-8">
|
||||||
|
<div
|
||||||
|
v-for="month in printMonths"
|
||||||
|
:key="`${month.year}-${month.month}`"
|
||||||
|
class="overflow-hidden rounded-lg border border-neutral-200 bg-white"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between border-b border-neutral-200 bg-tertiary-500 px-6 py-3 text-lg font-bold text-neutral-700">
|
||||||
|
<span>Calendrier {{ month.label }} {{ month.year }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<div class="min-w-[900px]">
|
||||||
|
<div class="grid" :style="getGridStyle(month.days.length)">
|
||||||
|
<div class="sticky left-0 z-10 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-md font-semibold text-neutral-700">
|
||||||
|
Employé
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="day in month.days"
|
||||||
|
:key="day.date"
|
||||||
|
class="border-b border-neutral-200 bg-tertiary-500 px-2 py-3 text-center text-xs font-semibold text-neutral-700"
|
||||||
|
>
|
||||||
|
<div>{{ day.label }}</div>
|
||||||
|
<div class="text-[10px] text-neutral-500">{{ day.weekday }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-for="employee in employees" :key="employee.id">
|
||||||
|
<div class="sticky left-0 z-10 border-b border-neutral-100 bg-white px-4 py-3 text-md font-semibold text-neutral-800">
|
||||||
|
{{ employee.firstName }} {{ employee.lastName }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="day in month.days"
|
||||||
|
:key="employee.id + '-' + day.date"
|
||||||
|
class="border-b border-neutral-100 px-2 py-2 text-center text-xs text-neutral-800"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex h-8 w-full items-center justify-center rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800"
|
||||||
|
:style="getCellStyle(employee.id, day.date)"
|
||||||
|
>
|
||||||
|
<span v-if="getCellCode(employee.id, day.date)">
|
||||||
|
{{ getCellCode(employee.id, day.date) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppDrawer v-model="isDrawerOpen" title="Nouvelle absence">
|
||||||
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="employee">Employé</label>
|
||||||
|
<select
|
||||||
|
id="employee"
|
||||||
|
v-model="form.employeeId"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Choisir un employé</option>
|
||||||
|
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
|
||||||
|
{{ employee.firstName }} {{ employee.lastName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="type">Type d'absence</label>
|
||||||
|
<select
|
||||||
|
id="type"
|
||||||
|
v-model="form.typeId"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Choisir un type</option>
|
||||||
|
<option v-for="type in absenceTypes" :key="type.id" :value="type.id">
|
||||||
|
{{ type.label }} ({{ type.code }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="start-date">Date de début</label>
|
||||||
|
<input
|
||||||
|
id="start-date"
|
||||||
|
v-model="form.startDate"
|
||||||
|
type="date"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="end-date">Date de fin</label>
|
||||||
|
<input
|
||||||
|
id="end-date"
|
||||||
|
v-model="form.endDate"
|
||||||
|
type="date"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="comment">Commentaire</label>
|
||||||
|
<textarea
|
||||||
|
id="comment"
|
||||||
|
v-model="form.comment"
|
||||||
|
rows="3"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
v-if="editingAbsence"
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-red-200 px-4 py-2 text-md font-semibold text-red-600 hover:bg-red-50"
|
||||||
|
@click="handleDelete"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
@click="closeDrawer"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</AppDrawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Employee } from '~/services/dto/employee'
|
||||||
|
import type { AbsenceType } from '~/services/dto/absence-type'
|
||||||
|
import type { Absence } from '~/services/dto/absence'
|
||||||
|
import { listEmployees } from '~/services/employees'
|
||||||
|
import { listAbsenceTypes } from '~/services/absence-types'
|
||||||
|
import { createAbsence, deleteAbsence, listAbsences, updateAbsence } from '~/services/absences'
|
||||||
|
import { getDaysInMonth, normalizeDate, toYmd } from '~/utils/date'
|
||||||
|
|
||||||
|
const employees = ref<Employee[]>([])
|
||||||
|
const absenceTypes = ref<AbsenceType[]>([])
|
||||||
|
const absences = ref<Absence[]>([])
|
||||||
|
|
||||||
|
const isDrawerOpen = ref(false)
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
const editingAbsence = ref<Absence | null>(null)
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const selectedMonth = ref(now.getMonth())
|
||||||
|
const selectedYear = ref(now.getFullYear())
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
{ value: 0, label: 'Janvier' },
|
||||||
|
{ value: 1, label: 'Février' },
|
||||||
|
{ value: 2, label: 'Mars' },
|
||||||
|
{ value: 3, label: 'Avril' },
|
||||||
|
{ value: 4, label: 'Mai' },
|
||||||
|
{ value: 5, label: 'Juin' },
|
||||||
|
{ value: 6, label: 'Juillet' },
|
||||||
|
{ value: 7, label: 'Août' },
|
||||||
|
{ value: 8, label: 'Septembre' },
|
||||||
|
{ value: 9, label: 'Octobre' },
|
||||||
|
{ value: 10, label: 'Novembre' },
|
||||||
|
{ value: 11, label: 'Décembre' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const years = Array.from({ length: 5 }, (_, i) => now.getFullYear() - 2 + i)
|
||||||
|
|
||||||
|
|
||||||
|
const daysInMonth = computed(() => getDaysInMonth(selectedYear.value, selectedMonth.value))
|
||||||
|
|
||||||
|
const getGridStyle = (daysCount: number) => {
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: `220px repeat(${daysCount}, minmax(44px, 1fr))`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridStyle = computed(() => getGridStyle(daysInMonth.value.length))
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
employeeId: '' as number | '',
|
||||||
|
typeId: '' as number | '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
comment: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const printMode = ref<'1' | '3' | null>(null)
|
||||||
|
|
||||||
|
const monthLabel = (monthIndex: number) => months.find((m) => m.value === monthIndex)?.label ?? ''
|
||||||
|
|
||||||
|
const printMonths = computed(() => {
|
||||||
|
if (!printMode.value) return []
|
||||||
|
|
||||||
|
const startYear = selectedYear.value
|
||||||
|
const startMonth = selectedMonth.value
|
||||||
|
const count = printMode.value === '3' ? 3 : 1
|
||||||
|
|
||||||
|
return Array.from({ length: count }, (_, offset) => {
|
||||||
|
const date = new Date(startYear, startMonth + offset, 1)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = date.getMonth()
|
||||||
|
return {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
label: monthLabel(month),
|
||||||
|
days: getDaysInMonth(year, month)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.employeeId = ''
|
||||||
|
form.typeId = ''
|
||||||
|
form.startDate = ''
|
||||||
|
form.endDate = ''
|
||||||
|
form.comment = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
isDrawerOpen.value = false
|
||||||
|
editingAbsence.value = null
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadEmployees = async () => {
|
||||||
|
employees.value = await listEmployees()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAbsenceTypes = async () => {
|
||||||
|
absenceTypes.value = await listAbsenceTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAbsences = async () => {
|
||||||
|
absences.value = await listAbsences()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadEmployees(), loadAbsenceTypes(), loadAbsences()])
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([selectedMonth, selectedYear], async () => {
|
||||||
|
await loadAbsences()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCellAbsence = (employeeId: number, date: string) => {
|
||||||
|
const match = absences.value.find((absence) => {
|
||||||
|
const employee = absence.employee?.id
|
||||||
|
const start = normalizeDate(absence.startDate)
|
||||||
|
const end = normalizeDate(absence.endDate)
|
||||||
|
return Number(employee) === employeeId && date >= start && date <= end
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!match) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: match.id,
|
||||||
|
code: match.type?.code ?? '',
|
||||||
|
color: match.type?.color ?? '#222783'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCellStyle = (employeeId: number, date: string) => {
|
||||||
|
const absence = getCellAbsence(employeeId, date)
|
||||||
|
if (!absence) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: absence.color,
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCellCode = (employeeId: number, date: string) => {
|
||||||
|
return getCellAbsence(employeeId, date)?.code ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreate = (employee: Employee, date: string) => {
|
||||||
|
const existing = absences.value.find((absence) => {
|
||||||
|
const start = normalizeDate(absence.startDate)
|
||||||
|
const end = normalizeDate(absence.endDate)
|
||||||
|
return absence.employee?.id === employee.id && date >= start && date <= end
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
editingAbsence.value = existing
|
||||||
|
form.employeeId = existing.employee.id
|
||||||
|
form.typeId = existing.type.id
|
||||||
|
form.startDate = normalizeDate(existing.startDate)
|
||||||
|
form.endDate = normalizeDate(existing.endDate)
|
||||||
|
form.comment = existing.comment ?? ''
|
||||||
|
} else {
|
||||||
|
editingAbsence.value = null
|
||||||
|
form.employeeId = employee.id
|
||||||
|
form.startDate = date
|
||||||
|
form.endDate = date
|
||||||
|
form.typeId = ''
|
||||||
|
form.comment = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateFromToday = () => {
|
||||||
|
editingAbsence.value = null
|
||||||
|
form.employeeId = ''
|
||||||
|
form.typeId = ''
|
||||||
|
const now = new Date()
|
||||||
|
const today = toYmd(now.getFullYear(), now.getMonth(), now.getDate())
|
||||||
|
form.startDate = today
|
||||||
|
form.endDate = today
|
||||||
|
form.comment = ''
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting.value) return
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
try {
|
||||||
|
const start = normalizeDate(form.startDate)
|
||||||
|
const end = normalizeDate(form.endDate)
|
||||||
|
const overlaps = absences.value.filter((absence) => {
|
||||||
|
if (absence.employee?.id !== Number(form.employeeId)) return false
|
||||||
|
if (editingAbsence.value && absence.id === editingAbsence.value.id) return false
|
||||||
|
const aStart = normalizeDate(absence.startDate)
|
||||||
|
const aEnd = normalizeDate(absence.endDate)
|
||||||
|
return start <= aEnd && end >= aStart
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const overlap of overlaps) {
|
||||||
|
await deleteAbsence(overlap.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingAbsence.value) {
|
||||||
|
await updateAbsence({
|
||||||
|
id: editingAbsence.value.id,
|
||||||
|
employeeId: Number(form.employeeId),
|
||||||
|
typeId: Number(form.typeId),
|
||||||
|
startDate: form.startDate,
|
||||||
|
endDate: form.endDate,
|
||||||
|
comment: form.comment
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createAbsence({
|
||||||
|
employeeId: Number(form.employeeId),
|
||||||
|
typeId: Number(form.typeId),
|
||||||
|
startDate: form.startDate,
|
||||||
|
endDate: form.endDate,
|
||||||
|
comment: form.comment
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDrawer()
|
||||||
|
await loadAbsences()
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!editingAbsence.value) return
|
||||||
|
|
||||||
|
const ok = window.confirm('Supprimer cette absence ?')
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
|
await deleteAbsence(editingAbsence.value.id)
|
||||||
|
closeDrawer()
|
||||||
|
await loadAbsences()
|
||||||
|
}
|
||||||
|
|
||||||
|
const printMonth = async (mode: '1' | '3') => {
|
||||||
|
printMode.value = mode
|
||||||
|
await nextTick()
|
||||||
|
window.print()
|
||||||
|
printMode.value = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.print-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-only .border-neutral-200,
|
||||||
|
.print-only .border-neutral-100 {
|
||||||
|
border-color: #d1d5db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-only .bg-tertiary-500 {
|
||||||
|
background-color: #f3f4f8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-only .rounded-lg,
|
||||||
|
.print-only .rounded-md {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
170
frontend/pages/employees.vue
Normal file
170
frontend/pages/employees.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between pb-12">
|
||||||
|
<h1 class="text-4xl font-bold text-primary-500">Employés</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
|
@click="isDrawerOpen = true"
|
||||||
|
>
|
||||||
|
Ajouter un employé
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isLoading && employees.length === 0"
|
||||||
|
class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600"
|
||||||
|
>
|
||||||
|
Aucun employé pour le moment.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="max-h-[80vh] overflow-auto rounded-lg border border-neutral-200 bg-white">
|
||||||
|
<div class="grid grid-cols-[120px_1fr_200px] gap-4 border-b border-neutral-200 bg-tertiary-500 px-6 py-3 text-md font-semibold text-neutral-700">
|
||||||
|
<span class="text-left">Prénom</span>
|
||||||
|
<span class="text-left">Nom</span>
|
||||||
|
<span class="text-right">Actions</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="px-6 py-4 text-md text-neutral-500">
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="employee in employees"
|
||||||
|
:key="employee.id"
|
||||||
|
class="grid grid-cols-[120px_1fr_200px] items-center gap-4 border-b border-neutral-100 px-6 py-3 text-md text-neutral-800 last:border-b-0"
|
||||||
|
>
|
||||||
|
<span>{{ employee.firstName }}</span>
|
||||||
|
<span>{{ employee.lastName }}</span>
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border border-neutral-200 px-2 py-1 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
@click="openEdit(employee)"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border border-red-200 px-2 py-1 text-md font-semibold text-red-600 hover:bg-red-50"
|
||||||
|
@click="confirmDelete(employee)"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
||||||
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="first-name">Prénom</label>
|
||||||
|
<input
|
||||||
|
id="first-name"
|
||||||
|
v-model="form.firstName"
|
||||||
|
type="text"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="last-name">Nom</label>
|
||||||
|
<input
|
||||||
|
id="last-name"
|
||||||
|
v-model="form.lastName"
|
||||||
|
type="text"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
@click="isDrawerOpen = false"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</AppDrawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Employee } from '~/services/dto/employee'
|
||||||
|
import { createEmployee, deleteEmployee, listEmployees, updateEmployee } from '~/services/employees'
|
||||||
|
|
||||||
|
const isDrawerOpen = ref(false)
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const editingEmployee = ref<Employee | null>(null)
|
||||||
|
const drawerTitle = computed(() =>
|
||||||
|
editingEmployee.value ? 'Modifier un employé' : 'Ajouter un employé'
|
||||||
|
)
|
||||||
|
|
||||||
|
const employees = ref<Employee[]>([])
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
firstName: '',
|
||||||
|
lastName: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadEmployees = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
employees.value = await listEmployees()
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadEmployees)
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting.value) return
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
try {
|
||||||
|
if (editingEmployee.value) {
|
||||||
|
await updateEmployee(editingEmployee.value.id, {
|
||||||
|
firstName: form.firstName,
|
||||||
|
lastName: form.lastName
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createEmployee({
|
||||||
|
firstName: form.firstName,
|
||||||
|
lastName: form.lastName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
form.firstName = ''
|
||||||
|
form.lastName = ''
|
||||||
|
editingEmployee.value = null
|
||||||
|
isDrawerOpen.value = false
|
||||||
|
await loadEmployees()
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = (employee: Employee) => {
|
||||||
|
editingEmployee.value = employee
|
||||||
|
form.firstName = employee.firstName
|
||||||
|
form.lastName = employee.lastName
|
||||||
|
isDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async (employee: Employee) => {
|
||||||
|
const ok = window.confirm(`Supprimer ${employee.firstName} ${employee.lastName} ?`)
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
|
await deleteEmployee(employee.id)
|
||||||
|
await loadEmployees()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<h1 class="text-4xl font-bold text-primary-500">Tableau de bord</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
$END$
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
72
frontend/pages/login.vue
Normal file
72
frontend/pages/login.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto w-full max-w-lg">
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
||||||
|
>
|
||||||
|
LOGO
|
||||||
|
</span>
|
||||||
|
<form
|
||||||
|
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-semibold text-neutral-700" for="username">
|
||||||
|
Nom d'utilisateur
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-semibold text-neutral-700" for="password">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({ layout: 'auth' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting.value) return
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
try {
|
||||||
|
console.log(useRuntimeConfig().public.apiBase)
|
||||||
|
await auth.login(username.value, password.value)
|
||||||
|
|
||||||
|
await router.push('/')
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 7.9 KiB |
2
frontend/public/robots.txt
Normal file
2
frontend/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
38
frontend/services/absence-types.ts
Normal file
38
frontend/services/absence-types.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { AbsenceType } from './dto/absence-type'
|
||||||
|
import { extractItems } from '~/utils/api'
|
||||||
|
|
||||||
|
export const listAbsenceTypes = async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const data = await api.get<AbsenceType[] | { 'hydra:member'?: AbsenceType[] }>(
|
||||||
|
'/absence_types',
|
||||||
|
{},
|
||||||
|
{ toast: false }
|
||||||
|
)
|
||||||
|
return extractItems<AbsenceType>(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAbsenceType = async (
|
||||||
|
payload: Pick<AbsenceType, 'code' | 'label' | 'color'>
|
||||||
|
) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<AbsenceType>('/absence_types', payload, {
|
||||||
|
toastSuccessMessage: 'Type créé.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAbsenceType = async (
|
||||||
|
id: number,
|
||||||
|
payload: Pick<AbsenceType, 'code' | 'label' | 'color'>
|
||||||
|
) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<AbsenceType>(`/absence_types/${id}`, payload, {
|
||||||
|
toastSuccessMessage: 'Type mis à jour.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAbsenceType = async (id: number) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.delete(`/absence_types/${id}`, {}, {
|
||||||
|
toastSuccessMessage: 'Type supprimé.'
|
||||||
|
})
|
||||||
|
}
|
||||||
58
frontend/services/absences.ts
Normal file
58
frontend/services/absences.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { Absence } from './dto/absence'
|
||||||
|
import { extractItems } from '~/utils/api'
|
||||||
|
|
||||||
|
export const listAbsences = async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const data = await api.get<Absence[] | { 'hydra:member'?: Absence[] }>(
|
||||||
|
'/absences',
|
||||||
|
{},
|
||||||
|
{ toast: false }
|
||||||
|
)
|
||||||
|
return extractItems<Absence>(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAbsence = async (payload: {
|
||||||
|
employeeId: number
|
||||||
|
typeId: number
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
comment?: string
|
||||||
|
}) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<Absence>('/absences', {
|
||||||
|
employee: `/api/employees/${payload.employeeId}`,
|
||||||
|
type: `/api/absence_types/${payload.typeId}`,
|
||||||
|
startDate: payload.startDate,
|
||||||
|
endDate: payload.endDate,
|
||||||
|
comment: payload.comment
|
||||||
|
}, {
|
||||||
|
toastSuccessMessage: 'Absence créée.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAbsence = async (payload: {
|
||||||
|
id: number
|
||||||
|
employeeId: number
|
||||||
|
typeId: number
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
comment?: string
|
||||||
|
}) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<Absence>(`/absences/${payload.id}`, {
|
||||||
|
employee: `/api/employees/${payload.employeeId}`,
|
||||||
|
type: `/api/absence_types/${payload.typeId}`,
|
||||||
|
startDate: payload.startDate,
|
||||||
|
endDate: payload.endDate,
|
||||||
|
comment: payload.comment
|
||||||
|
}, {
|
||||||
|
toastSuccessMessage: 'Absence mise à jour.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAbsence = async (id: number) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.delete(`/absences/${id}`, {}, {
|
||||||
|
toastSuccessMessage: 'Absence supprimée.'
|
||||||
|
})
|
||||||
|
}
|
||||||
21
frontend/services/auth.ts
Normal file
21
frontend/services/auth.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { UserData } from './dto/user-data'
|
||||||
|
|
||||||
|
export const getCurrentUser = () => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<UserData>('/me', {}, { toastErrorKey: 'errors.auth.session' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const login = (username: string, password: string) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post('/login_check', { username, password }, {
|
||||||
|
toastErrorKey: 'errors.auth.login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post('/logout', {}, {
|
||||||
|
toastErrorKey: 'errors.auth.logout',
|
||||||
|
toastSuccessKey: 'success.auth.logout'
|
||||||
|
})
|
||||||
|
}
|
||||||
6
frontend/services/dto/absence-type.ts
Normal file
6
frontend/services/dto/absence-type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type AbsenceType = {
|
||||||
|
id: number
|
||||||
|
code: string
|
||||||
|
label: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
11
frontend/services/dto/absence.ts
Normal file
11
frontend/services/dto/absence.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Employee } from './employee'
|
||||||
|
import type { AbsenceType } from './absence-type'
|
||||||
|
|
||||||
|
export type Absence = {
|
||||||
|
id: number
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
comment?: string | null
|
||||||
|
employee: Employee
|
||||||
|
type: AbsenceType
|
||||||
|
}
|
||||||
5
frontend/services/dto/employee.ts
Normal file
5
frontend/services/dto/employee.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type Employee = {
|
||||||
|
id: number
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
4
frontend/services/dto/user-data.ts
Normal file
4
frontend/services/dto/user-data.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type UserData = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
}
|
||||||
36
frontend/services/employees.ts
Normal file
36
frontend/services/employees.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Employee } from './dto/employee'
|
||||||
|
import { extractItems } from '~/utils/api'
|
||||||
|
|
||||||
|
export const listEmployees = async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const data = await api.get<Employee[] | { 'hydra:member'?: Employee[] }>(
|
||||||
|
'/employees',
|
||||||
|
{},
|
||||||
|
{ toast: false }
|
||||||
|
)
|
||||||
|
return extractItems<Employee>(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createEmployee = async (payload: Pick<Employee, 'firstName' | 'lastName'>) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<Employee>('/employees', payload, {
|
||||||
|
toastSuccessMessage: 'Employé créé.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateEmployee = async (
|
||||||
|
id: number,
|
||||||
|
payload: Pick<Employee, 'firstName' | 'lastName'>
|
||||||
|
) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<Employee>(`/employees/${id}`, payload, {
|
||||||
|
toastSuccessMessage: 'Employé mis à jour.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteEmployee = async (id: number) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.delete(`/employees/${id}`, {}, {
|
||||||
|
toastSuccessMessage: 'Employé supprimé.'
|
||||||
|
})
|
||||||
|
}
|
||||||
63
frontend/stores/auth.ts
Normal file
63
frontend/stores/auth.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import { getCurrentUser, login, logout } from '~/services/auth'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
user: null as UserData | null,
|
||||||
|
isLoading: false,
|
||||||
|
checked: false
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
isAuthenticated: (state) => Boolean(state.user)
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
clearSession() {
|
||||||
|
this.user = null
|
||||||
|
this.checked = true
|
||||||
|
this.isLoading = false
|
||||||
|
},
|
||||||
|
async ensureSession() {
|
||||||
|
if (this.checked) {
|
||||||
|
return this.user
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checked = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const me = await getCurrentUser()
|
||||||
|
this.user = me
|
||||||
|
return me
|
||||||
|
} catch {
|
||||||
|
this.user = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async login(username: string, password: string) {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(username, password)
|
||||||
|
const me = await getCurrentUser()
|
||||||
|
this.user = me
|
||||||
|
this.checked = true
|
||||||
|
return me
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async logout() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await logout()
|
||||||
|
} catch {
|
||||||
|
// Ignore logout errors so we can still clear local auth state.
|
||||||
|
} finally {
|
||||||
|
this.user = null
|
||||||
|
this.checked = true
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
import type { Config } from 'tailwindcss'
|
import type {Config} from 'tailwindcss'
|
||||||
|
|
||||||
export default <Partial<Config>>{
|
export default <Partial<Config>>{
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['"Helvetica Neue"', 'Helvetica', 'Arial', 'sans-serif']
|
sans: ['"Helvetica Neue"', 'Helvetica', 'Arial', 'sans-serif']
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
50: '#f6f9ea',
|
500: '#222783',
|
||||||
100: '#eaf2cf',
|
},
|
||||||
200: '#d6e3a4',
|
secondary: {
|
||||||
300: '#c1d47a',
|
500: '#304998'
|
||||||
400: '#afc85a',
|
},
|
||||||
500: '#9ebb43',
|
tertiary: {
|
||||||
600: '#7e9735',
|
500: '#F3F4F8'
|
||||||
700: '#607228',
|
}
|
||||||
800: '#414d1a',
|
}
|
||||||
900: '#24290d'
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
18
frontend/tsconfig.json
Normal file
18
frontend/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
frontend/utils/api.ts
Normal file
7
frontend/utils/api.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type HydraCollection<T> = {
|
||||||
|
'hydra:member'?: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractItems = <T>(data: HydraCollection<T> | T[]): T[] => {
|
||||||
|
return Array.isArray(data) ? data : data['hydra:member'] ?? []
|
||||||
|
}
|
||||||
22
frontend/utils/date.ts
Normal file
22
frontend/utils/date.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const toYmd = (year: number, month: number, day: number) => {
|
||||||
|
const mm = String(month + 1).padStart(2, '0')
|
||||||
|
const dd = String(day).padStart(2, '0')
|
||||||
|
return `${year}-${mm}-${dd}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeDate = (value: string) => value.slice(0, 10)
|
||||||
|
|
||||||
|
export const getDaysInMonth = (year: number, month: number) => {
|
||||||
|
const total = new Date(year, month + 1, 0).getDate()
|
||||||
|
const weekdays = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']
|
||||||
|
|
||||||
|
return Array.from({ length: total }, (_, index) => {
|
||||||
|
const day = index + 1
|
||||||
|
const dateObj = new Date(year, month, day)
|
||||||
|
return {
|
||||||
|
date: toYmd(year, month, day),
|
||||||
|
label: String(day),
|
||||||
|
weekday: weekdays[dateObj.getDay()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
128
makefile
Normal file
128
makefile
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Permet d'utiliser un .env.docker.local pour override
|
||||||
|
ENV_DEFAULT = docker/.env.docker
|
||||||
|
ENV_LOCAL = docker/.env.docker.local
|
||||||
|
ENV_FILE := $(if $(wildcard $(ENV_LOCAL)),$(ENV_LOCAL),$(ENV_DEFAULT))
|
||||||
|
|
||||||
|
# Permet d'avoir les variables du fichier .env.docker.local
|
||||||
|
include $(ENV_DEFAULT)
|
||||||
|
-include $(ENV_LOCAL)
|
||||||
|
|
||||||
|
PHP_CONTAINER = php-$(DOCKER_APP_NAME)-fpm
|
||||||
|
SYMFONY_CONSOLE = $(EXEC_PHP) php bin/console
|
||||||
|
|
||||||
|
DOCKER_COMPOSE = docker compose --env-file $(ENV_FILE)
|
||||||
|
DOCKER = docker
|
||||||
|
|
||||||
|
EXEC_PHP = $(DOCKER) exec -t -u $(APP_USER) $(PHP_CONTAINER)
|
||||||
|
EXEC_PHP_CS_FIXER = $(EXEC_PHP) php vendor/bin/php-cs-fixer
|
||||||
|
EXEC_PHP_ROOT = $(DOCKER) exec -t -u root $(PHP_CONTAINER)
|
||||||
|
EXEC_PHP_INTERACTIVE = $(DOCKER) exec -it -u $(APP_USER) $(PHP_CONTAINER)
|
||||||
|
EXEC_PHP_INTERACTIVE_ROOT = $(DOCKER) exec -it -u root $(PHP_CONTAINER)
|
||||||
|
FILES =
|
||||||
|
|
||||||
|
#========================================================================================
|
||||||
|
|
||||||
|
env-init:
|
||||||
|
@mkdir -p docker
|
||||||
|
@cp --update=none $(ENV_DEFAULT) $(ENV_LOCAL)
|
||||||
|
|
||||||
|
# Lance le container
|
||||||
|
start: env-init
|
||||||
|
@echo "**** START CONTAINERS ****"
|
||||||
|
@cp --update=none docker/.env.docker docker/.env.docker.local
|
||||||
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
|
# Éteint le container
|
||||||
|
stop:
|
||||||
|
$(DOCKER_COMPOSE) stop
|
||||||
|
|
||||||
|
restart: env-init
|
||||||
|
$(DOCKER_COMPOSE) down
|
||||||
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
|
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS migration-migrate
|
||||||
|
|
||||||
|
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
||||||
|
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
||||||
|
|
||||||
|
composer-install:
|
||||||
|
$(EXEC_PHP) composer install
|
||||||
|
$(SYMFONY_CONSOLE) lexik:jwt:generate-keypair --skip-if-exists
|
||||||
|
|
||||||
|
build-nuxtJS:
|
||||||
|
# $(EXEC_PHP) cp -n frontend/.env.dist frontend/.env.local
|
||||||
|
$(EXEC_PHP) sh -lc "cd frontend && npm install && npm run build:dist"
|
||||||
|
|
||||||
|
dev-nuxt:
|
||||||
|
$(EXEC_PHP) sh -c "cd frontend && npm run dev"
|
||||||
|
|
||||||
|
delete_built_dir:
|
||||||
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
|
||||||
|
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf frontend/node_modules
|
||||||
|
|
||||||
|
remove_orphans:
|
||||||
|
$(DOCKER_COMPOSE) kill
|
||||||
|
$(DOCKER_COMPOSE) down --volumes --remove-orphans
|
||||||
|
|
||||||
|
build-without-cache:
|
||||||
|
$(DOCKER_COMPOSE) build \
|
||||||
|
--build-arg="DOCKER_PHP_VERSION=$(DOCKER_PHP_VERSION)" \
|
||||||
|
--build-arg="DOCKER_NODE_VERSION=$(DOCKER_NODE_VERSION)" \
|
||||||
|
--build-arg="CURRENT_UID=$(shell id -u)" \
|
||||||
|
--build-arg="CURRENT_GID=$(shell id -g)" \
|
||||||
|
--no-cache
|
||||||
|
|
||||||
|
migration-migrate:
|
||||||
|
$(SYMFONY_CONSOLE) doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
fixtures:
|
||||||
|
$(SYMFONY_CONSOLE) doctrine:fixtures:load
|
||||||
|
|
||||||
|
# Attention, supprime votre bdd local
|
||||||
|
db-reset:
|
||||||
|
$(DOCKER_COMPOSE) down -v
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
$(MAKE) wait
|
||||||
|
$(SYMFONY_CONSOLE) doctrine:database:create --if-not-exists
|
||||||
|
$(MAKE) migration-migrate
|
||||||
|
$(MAKE) fixtures
|
||||||
|
|
||||||
|
# Restart la bdd
|
||||||
|
db-restart:
|
||||||
|
$(DOCKER_COMPOSE) down
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
|
cache-clear:
|
||||||
|
$(SYMFONY_CONSOLE) cache:clear
|
||||||
|
|
||||||
|
copy-git-hook:
|
||||||
|
$(EXEC_PHP) cp pre-commit .git/hooks/
|
||||||
|
$(EXEC_PHP) cp commit-msg .git/hooks/
|
||||||
|
$(EXEC_PHP) chmod a+x .git/hooks/pre-commit
|
||||||
|
$(EXEC_PHP) chmod a+x .git/hooks/commit-msg
|
||||||
|
|
||||||
|
shell:
|
||||||
|
$(EXEC_PHP_INTERACTIVE) bash
|
||||||
|
|
||||||
|
shell-root:
|
||||||
|
$(EXEC_PHP_INTERACTIVE_ROOT) bash
|
||||||
|
|
||||||
|
# Suivi temps réel des logs dev
|
||||||
|
logs-dev:
|
||||||
|
$(EXEC_PHP_INTERACTIVE) sh -lc "tail -f var/log/dev.log"
|
||||||
|
|
||||||
|
# Force la version node
|
||||||
|
node-use:
|
||||||
|
bash -lc 'source "$$HOME/.nvm/nvm.sh" && nvm install && nvm use'
|
||||||
|
|
||||||
|
# Utilisé par le pre-commit pour fix les fichiers modifiés
|
||||||
|
php-cs-fixer-allow-risky:
|
||||||
|
@echo "Fixing files: $(FILES)"
|
||||||
|
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes $(FILES)
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(EXEC_PHP) php -d memory_limit="512M" vendor/bin/phpunit $(FILES)
|
||||||
|
|
||||||
|
wait:
|
||||||
|
sleep 10
|
||||||
0
migrations/.gitignore
vendored
Normal file
0
migrations/.gitignore
vendored
Normal file
27
migrations/Version20260202195900.php
Normal file
27
migrations/Version20260202195900.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260202195900 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create users table for authentication';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE users (id SERIAL NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_users_username ON users (username)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE users');
|
||||||
|
}
|
||||||
|
}
|
||||||
26
migrations/Version20260202213000.php
Normal file
26
migrations/Version20260202213000.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260202213000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create employees table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE employees (id SERIAL NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE employees');
|
||||||
|
}
|
||||||
|
}
|
||||||
26
migrations/Version20260202220500.php
Normal file
26
migrations/Version20260202220500.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260202220500 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create absence types table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE absence_types (id SERIAL NOT NULL, code VARCHAR(10) NOT NULL, label VARCHAR(100) NOT NULL, color VARCHAR(20) NOT NULL, PRIMARY KEY(id))');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE absence_types');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
migrations/Version20260202224500.php
Normal file
32
migrations/Version20260202224500.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260202224500 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create absences table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE absences (id SERIAL NOT NULL, employee_id INT NOT NULL, type_id INT NOT NULL, date DATE NOT NULL, comment TEXT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_ABSENCES_EMPLOYEE ON absences (employee_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_ABSENCES_TYPE ON absences (type_id)');
|
||||||
|
$this->addSql('ALTER TABLE absences ADD CONSTRAINT FK_ABSENCES_EMPLOYEE FOREIGN KEY (employee_id) REFERENCES employees (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE absences ADD CONSTRAINT FK_ABSENCES_TYPE FOREIGN KEY (type_id) REFERENCES absence_types (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE absences DROP CONSTRAINT FK_ABSENCES_EMPLOYEE');
|
||||||
|
$this->addSql('ALTER TABLE absences DROP CONSTRAINT FK_ABSENCES_TYPE');
|
||||||
|
$this->addSql('DROP TABLE absences');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
migrations/Version20260203120000.php
Normal file
34
migrations/Version20260203120000.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260203120000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add start and end dates to absences';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE absences ADD start_date DATE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE absences ADD end_date DATE DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE absences SET start_date = date, end_date = date');
|
||||||
|
$this->addSql('ALTER TABLE absences ALTER COLUMN start_date SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE absences ALTER COLUMN end_date SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE absences DROP COLUMN date');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE absences ADD date DATE NOT NULL');
|
||||||
|
$this->addSql('UPDATE absences SET date = start_date');
|
||||||
|
$this->addSql('ALTER TABLE absences DROP COLUMN start_date');
|
||||||
|
$this->addSql('ALTER TABLE absences DROP COLUMN end_date');
|
||||||
|
}
|
||||||
|
}
|
||||||
44
phpunit.dist.xml
Normal file
44
phpunit.dist.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
colors="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="display_errors" value="1" />
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
<server name="APP_ENV" value="test" force="true" />
|
||||||
|
<server name="SHELL_VERBOSITY" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Project Test Suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source ignoreSuppressionOfDeprecations="true"
|
||||||
|
ignoreIndirectDeprecations="true"
|
||||||
|
restrictNotices="true"
|
||||||
|
restrictWarnings="true"
|
||||||
|
>
|
||||||
|
<include>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
|
||||||
|
<deprecationTrigger>
|
||||||
|
<method>Doctrine\Deprecations\Deprecation::trigger</method>
|
||||||
|
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
|
||||||
|
<function>trigger_deprecation</function>
|
||||||
|
</deprecationTrigger>
|
||||||
|
</source>
|
||||||
|
|
||||||
|
<extensions>
|
||||||
|
</extensions>
|
||||||
|
</phpunit>
|
||||||
38
pre-commit
Normal file
38
pre-commit
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "######### Pre-commit hook start #############"
|
||||||
|
echo "--- php-cs-fixer pre commit hook start ---"
|
||||||
|
|
||||||
|
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.php$')
|
||||||
|
# Vérifier s'il y a des fichiers PHP modifiés
|
||||||
|
if [ -n "$FILES" ]; then
|
||||||
|
echo "Running PHP CS Fixer on staged PHP files..."
|
||||||
|
|
||||||
|
# Convertir la liste des fichiers en une chaîne séparée par des espaces
|
||||||
|
FILES_LIST=""
|
||||||
|
for FILE in $FILES; do
|
||||||
|
FILES_LIST="$FILES_LIST $FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Exécuter la cible make pour PHP CS Fixer
|
||||||
|
make php-cs-fixer-allow-risky FILES="$FILES_LIST"
|
||||||
|
|
||||||
|
# Ajouter les fichiers corrigés au commit
|
||||||
|
git add $FILES
|
||||||
|
else
|
||||||
|
echo "No PHP files to fix."
|
||||||
|
fi
|
||||||
|
echo "--- php-cs-fixer pre commit hook finish---"
|
||||||
|
|
||||||
|
echo "--- phpunit pre commit hook start ---"
|
||||||
|
make test
|
||||||
|
PHPUNIT_RESULT=$?
|
||||||
|
|
||||||
|
if [ $PHPUNIT_RESULT -ne 0 ]; then
|
||||||
|
echo "PHPUnit tests failed. Aborting commit."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "--- phpunit pre commit hook finished ---"
|
||||||
|
|
||||||
|
echo "All checks passed. Proceeding with commit."
|
||||||
|
exit 0
|
||||||
11
public/index.php
Normal file
11
public/index.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
};
|
||||||
0
src/ApiResource/.gitignore
vendored
Normal file
0
src/ApiResource/.gitignore
vendored
Normal file
103
src/Command/SeedEmployeesCommand.php
Normal file
103
src/Command/SeedEmployeesCommand.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\Employee;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:seed:employees',
|
||||||
|
description: 'Seed employees (idempotent).'
|
||||||
|
)]
|
||||||
|
class SeedEmployeesCommand extends Command
|
||||||
|
{
|
||||||
|
private int $created = 0;
|
||||||
|
private int $updated = 0;
|
||||||
|
|
||||||
|
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$this->created = 0;
|
||||||
|
$this->updated = 0;
|
||||||
|
|
||||||
|
$employees = [
|
||||||
|
['lastName' => 'LIOT', 'firstName' => 'Geoffrey'],
|
||||||
|
['lastName' => 'ARCHAMBAULT', 'firstName' => 'Emilie'],
|
||||||
|
['lastName' => 'SARAZI', 'firstName' => 'Olivier'],
|
||||||
|
['lastName' => 'SOURIAU', 'firstName' => 'Elodie'],
|
||||||
|
['lastName' => 'BRUYERE', 'firstName' => 'Aurore'],
|
||||||
|
['lastName' => 'PANGALLO', 'firstName' => 'Julie'],
|
||||||
|
['lastName' => 'BACHELIER', 'firstName' => 'Delphine'],
|
||||||
|
['lastName' => 'SAILLY', 'firstName' => 'Marine'],
|
||||||
|
['lastName' => 'KUPCZYK', 'firstName' => 'Corinne'],
|
||||||
|
['lastName' => 'ODUNCU', 'firstName' => 'Kemal'],
|
||||||
|
['lastName' => 'PROUTEAU', 'firstName' => 'Damien'],
|
||||||
|
['lastName' => 'COURLIVANT', 'firstName' => 'Franck'],
|
||||||
|
['lastName' => 'GUILLOT', 'firstName' => 'Damien'],
|
||||||
|
['lastName' => 'GELINET', 'firstName' => 'Alexandre'],
|
||||||
|
['lastName' => 'ASSANI', 'firstName' => 'Chafainna'],
|
||||||
|
['lastName' => 'BRIQUET', 'firstName' => 'Alain'],
|
||||||
|
['lastName' => 'BOULOIZEAU', 'firstName' => 'Gwenael'],
|
||||||
|
['lastName' => 'MENANTEAU', 'firstName' => 'Dimitri'],
|
||||||
|
['lastName' => 'ROBERT', 'firstName' => 'Remi'],
|
||||||
|
['lastName' => 'COUTEAU', 'firstName' => 'Johnny'],
|
||||||
|
['lastName' => 'MAISSANT', 'firstName' => 'Guillaume'],
|
||||||
|
['lastName' => 'GARRAUD', 'firstName' => 'Nadia'],
|
||||||
|
['lastName' => 'CRUBILE', 'firstName' => 'Franck'],
|
||||||
|
['lastName' => 'ALMAGRO', 'firstName' => 'Antonio'],
|
||||||
|
['lastName' => 'MARCHAL SANCHEZ', 'firstName' => 'Jose'],
|
||||||
|
['lastName' => 'LOISEAU', 'firstName' => 'Nicolas'],
|
||||||
|
['lastName' => 'BRAAK', 'firstName' => 'Estelle'],
|
||||||
|
['lastName' => 'RODRIGUES MENDES', 'firstName' => 'Hugo'],
|
||||||
|
['lastName' => 'RICAUD', 'firstName' => 'Jason'],
|
||||||
|
['lastName' => 'LECOINT', 'firstName' => 'Jerome'],
|
||||||
|
['lastName' => 'DALEMBA', 'firstName' => 'Ewa'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($employees as $data) {
|
||||||
|
$this->upsertEmployee($data['firstName'], $data['lastName']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$io->success(sprintf('Seed completed: %d created, %d updated.', $this->created, $this->updated));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function upsertEmployee(string $firstName, string $lastName): void
|
||||||
|
{
|
||||||
|
$repo = $this->entityManager->getRepository(Employee::class);
|
||||||
|
$employee = $repo->findOneBy([
|
||||||
|
'firstName' => $firstName,
|
||||||
|
'lastName' => $lastName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$employee) {
|
||||||
|
$employee = new Employee();
|
||||||
|
++$this->created;
|
||||||
|
} else {
|
||||||
|
++$this->updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
$employee
|
||||||
|
->setFirstName($firstName)
|
||||||
|
->setLastName($lastName)
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->entityManager->persist($employee);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
120
src/Entity/Absence.php
Normal file
120
src/Entity/Absence.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
normalizationContext: [
|
||||||
|
'groups' => ['absence:read', 'employee:read', 'absence_type:read'],
|
||||||
|
'datetime_format' => 'Y-m-d',
|
||||||
|
],
|
||||||
|
denormalizationContext: [
|
||||||
|
'datetime_format' => 'Y-m-d',
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'absences')]
|
||||||
|
class Absence
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Groups(['absence:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
#[ORM\ManyToOne(targetEntity: Employee::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Groups(['absence:read'])]
|
||||||
|
private ?Employee $employee = null;
|
||||||
|
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
#[ORM\ManyToOne(targetEntity: AbsenceType::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Groups(['absence:read'])]
|
||||||
|
private ?AbsenceType $type = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'date')]
|
||||||
|
#[Groups(['absence:read'])]
|
||||||
|
private DateTimeInterface $startDate;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'date')]
|
||||||
|
#[Groups(['absence:read'])]
|
||||||
|
private DateTimeInterface $endDate;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
#[Groups(['absence:read'])]
|
||||||
|
private ?string $comment = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmployee(): ?Employee
|
||||||
|
{
|
||||||
|
return $this->employee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmployee(?Employee $employee): self
|
||||||
|
{
|
||||||
|
$this->employee = $employee;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): ?AbsenceType
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(?AbsenceType $type): self
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStartDate(): DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStartDate(DateTimeInterface $startDate): self
|
||||||
|
{
|
||||||
|
$this->startDate = $startDate;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEndDate(): DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEndDate(DateTimeInterface $endDate): self
|
||||||
|
{
|
||||||
|
$this->endDate = $endDate;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComment(): ?string
|
||||||
|
{
|
||||||
|
return $this->comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComment(?string $comment): self
|
||||||
|
{
|
||||||
|
$this->comment = $comment;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/Entity/AbsenceType.php
Normal file
74
src/Entity/AbsenceType.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(normalizationContext: ['groups' => ['absence_type:read']])]
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'absence_types')]
|
||||||
|
class AbsenceType
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Groups(['absence:read', 'absence_type:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 10)]
|
||||||
|
#[Groups(['absence:read', 'absence_type:read'])]
|
||||||
|
private string $code = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100)]
|
||||||
|
#[Groups(['absence:read', 'absence_type:read'])]
|
||||||
|
private string $label = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 20)]
|
||||||
|
#[Groups(['absence:read', 'absence_type:read'])]
|
||||||
|
private string $color = '';
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCode(): string
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCode(string $code): self
|
||||||
|
{
|
||||||
|
$this->code = $code;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return $this->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLabel(string $label): self
|
||||||
|
{
|
||||||
|
$this->label = $label;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColor(): string
|
||||||
|
{
|
||||||
|
return $this->color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setColor(string $color): self
|
||||||
|
{
|
||||||
|
$this->color = $color;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/Entity/Employee.php
Normal file
72
src/Entity/Employee.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(normalizationContext: ['groups' => ['employee:read']])]
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'employees')]
|
||||||
|
class Employee
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Groups(['absence:read', 'employee:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100)]
|
||||||
|
#[Groups(['absence:read', 'employee:read'])]
|
||||||
|
private string $firstName = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100)]
|
||||||
|
#[Groups(['absence:read', 'employee:read'])]
|
||||||
|
private string $lastName = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable')]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFirstName(): string
|
||||||
|
{
|
||||||
|
return $this->firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFirstName(string $firstName): self
|
||||||
|
{
|
||||||
|
$this->firstName = $firstName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastName(): string
|
||||||
|
{
|
||||||
|
return $this->lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLastName(string $lastName): self
|
||||||
|
{
|
||||||
|
$this->lastName = $lastName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/Entity/User.php
Normal file
99
src/Entity/User.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\State\CurrentUserProvider;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/me',
|
||||||
|
normalizationContext: ['groups' => ['user:read']],
|
||||||
|
security: "is_granted('ROLE_USER')",
|
||||||
|
provider: CurrentUserProvider::class
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'users')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'uniq_users_username', fields: ['username'])]
|
||||||
|
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 180)]
|
||||||
|
private string $username = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'json')]
|
||||||
|
private array $roles = [];
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $password = '';
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsername(): string
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUsername(string $username): self
|
||||||
|
{
|
||||||
|
$this->username = $username;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
$roles = $this->roles;
|
||||||
|
$roles[] = 'ROLE_USER';
|
||||||
|
|
||||||
|
return array_values(array_unique($roles));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $roles
|
||||||
|
*/
|
||||||
|
public function setRoles(array $roles): self
|
||||||
|
{
|
||||||
|
$this->roles = $roles;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPassword(): string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPassword(string $password): self
|
||||||
|
{
|
||||||
|
$this->password = $password;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function eraseCredentials(): void {}
|
||||||
|
}
|
||||||
13
src/Kernel.php
Normal file
13
src/Kernel.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
|
|
||||||
|
class Kernel extends BaseKernel
|
||||||
|
{
|
||||||
|
use MicroKernelTrait;
|
||||||
|
}
|
||||||
22
src/State/CurrentUserProvider.php
Normal file
22
src/State/CurrentUserProvider.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
|
||||||
|
final class CurrentUserProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(private readonly Security $security) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?User
|
||||||
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
return $user instanceof User ? $user : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
220
symfony.lock
Normal file
220
symfony.lock
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
{
|
||||||
|
"api-platform/symfony": {
|
||||||
|
"version": "4.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "4.0",
|
||||||
|
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/api_platform.yaml",
|
||||||
|
"config/routes/api_platform.yaml",
|
||||||
|
"src/ApiResource/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/deprecations": {
|
||||||
|
"version": "1.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "18ee08e513ba0303fd09a01fc1c934870af06ffa"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine.yaml",
|
||||||
|
"src/Entity/.gitignore",
|
||||||
|
"src/Repository/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-migrations-bundle": {
|
||||||
|
"version": "4.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.1",
|
||||||
|
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine_migrations.yaml",
|
||||||
|
"migrations/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"friendsofphp/php-cs-fixer": {
|
||||||
|
"version": "3.93",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "be2103eb4a20942e28a6dd87736669b757132435"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".php-cs-fixer.dist.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lexik/jwt-authentication-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.5",
|
||||||
|
"ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/lexik_jwt_authentication.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nelmio/cors-bundle": {
|
||||||
|
"version": "2.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.5",
|
||||||
|
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/nelmio_cors.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"phpunit/phpunit": {
|
||||||
|
"version": "12.5",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "11.1",
|
||||||
|
"ref": "1117deb12541f35793eec9fff7494d7aa12283fc"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env.test",
|
||||||
|
"phpunit.dist.xml",
|
||||||
|
"tests/bootstrap.php",
|
||||||
|
"bin/phpunit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/console": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "5.3",
|
||||||
|
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/console"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/flex": {
|
||||||
|
"version": "2.10",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.4",
|
||||||
|
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env",
|
||||||
|
".env.dev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/framework-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "09f6e081c763a206802674ce0cb34a022f0ffc6d"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/cache.yaml",
|
||||||
|
"config/packages/framework.yaml",
|
||||||
|
"config/preload.php",
|
||||||
|
"config/routes/framework.yaml",
|
||||||
|
"config/services.yaml",
|
||||||
|
"public/index.php",
|
||||||
|
"src/Controller/.gitignore",
|
||||||
|
"src/Kernel.php",
|
||||||
|
".editorconfig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/property-info": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/property_info.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/routing": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/routing.yaml",
|
||||||
|
"config/routes.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/security-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "c42fee7802181cdd50f61b8622715829f5d2335c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/security.yaml",
|
||||||
|
"config/routes/security.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/twig-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.4",
|
||||||
|
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/twig.yaml",
|
||||||
|
"templates/base.html.twig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/uid": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symfony/validator": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/validator.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
templates/base.html.twig
Normal file
16
templates/base.html.twig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||||
|
{% block stylesheets %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user