svg
This commit is contained in:
@ -3,19 +3,13 @@
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/afornerot/bNine-MdEditorBundle"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"apereo/phpcas": "^1.6",
|
||||
"bnine/filesbundle": "^1.0",
|
||||
"bnine/mdeditorbundle": "*",
|
||||
"bnine/mdeditorbundle": "^1.1",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/doctrine-bundle": "^2.13",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
|
17
composer.lock
generated
17
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6f7395c5fc09ba0bc1a2325b862a69c7",
|
||||
"content-hash": "2d59dc750783494ae1dbf58d9a3025c3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "apereo/phpcas",
|
||||
@ -140,16 +140,16 @@
|
||||
},
|
||||
{
|
||||
"name": "bnine/mdeditorbundle",
|
||||
"version": "v0.1.5",
|
||||
"version": "v1.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/afornerot/bNine-MdEditorBundle.git",
|
||||
"reference": "b5897339b8aed8f32578b3ebdf1e9ffa4ef83fa0"
|
||||
"reference": "ad2c2829177a6788452fde1d70170ba5c5c6d0d6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/afornerot/bNine-MdEditorBundle/zipball/b5897339b8aed8f32578b3ebdf1e9ffa4ef83fa0",
|
||||
"reference": "b5897339b8aed8f32578b3ebdf1e9ffa4ef83fa0",
|
||||
"url": "https://api.github.com/repos/afornerot/bNine-MdEditorBundle/zipball/ad2c2829177a6788452fde1d70170ba5c5c6d0d6",
|
||||
"reference": "ad2c2829177a6788452fde1d70170ba5c5c6d0d6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -173,6 +173,7 @@
|
||||
"Bnine\\MdEditorBundle\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
@ -191,10 +192,10 @@
|
||||
"symfony"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/afornerot/bNine-MdEditorBundle/tree/v0.1.5",
|
||||
"issues": "https://github.com/afornerot/bNine-MdEditorBundle/issues"
|
||||
"issues": "https://github.com/afornerot/bNine-MdEditorBundle/issues",
|
||||
"source": "https://github.com/afornerot/bNine-MdEditorBundle/tree/v1.1.3"
|
||||
},
|
||||
"time": "2025-08-02T20:06:19+00:00"
|
||||
"time": "2025-08-02T20:38:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
@ -29,4 +29,3 @@ services:
|
||||
App\Security\DynamicAuthenticator:
|
||||
arguments:
|
||||
$modeAuth: '%env(MODE_AUTH)%'
|
||||
|
||||
|
@ -78,7 +78,7 @@ class ProjectController extends AbstractController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_admin_project');
|
||||
// return $this->redirectToRoute('app_admin_project');
|
||||
}
|
||||
|
||||
return $this->render('project/edit.html.twig', [
|
||||
|
@ -8,8 +8,18 @@ use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class Project
|
||||
{
|
||||
public const DRAFT = 'Brouillon';
|
||||
public const TOVOTE = 'A Voter';
|
||||
public const VOTED = 'Voté';
|
||||
public const ARCHIVED = 'Archivé';
|
||||
|
||||
public const NATURE_COLLECTIVE = 'Collective';
|
||||
public const NATURE_STRATEGIC = 'Stratégique';
|
||||
public const NATURE_TACTICAL = 'Tactique';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
@ -18,11 +28,20 @@ class Project
|
||||
#[ORM\Column(length: 255, unique: true)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column()]
|
||||
private int $status;
|
||||
#[ORM\Column(type: 'text')]
|
||||
private string $summary;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 20)]
|
||||
private string $nature;
|
||||
|
||||
#[ORM\Column]
|
||||
private string $status;
|
||||
|
||||
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||
private ?\DateTimeInterface $updateAt = null;
|
||||
private ?\DateTimeInterface $dueDate = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>
|
||||
@ -30,9 +49,17 @@ class Project
|
||||
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'projects')]
|
||||
private Collection $users;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ProjectTimeline>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectTimeline::class, cascade: ['remove'])]
|
||||
#[ORM\OrderBy(['createdAt' => 'DESC'])]
|
||||
private Collection $timelines;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->users = new ArrayCollection();
|
||||
$this->timelines = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@ -52,30 +79,66 @@ class Project
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdateAt(): ?\DateTimeInterface
|
||||
public function getSummary(): string
|
||||
{
|
||||
return $this->updateAt;
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function setUpdateAt(?\DateTimeInterface $updateAt): static
|
||||
public function setSummary(string $summary): static
|
||||
{
|
||||
$this->updateAt = $updateAt;
|
||||
$this->summary = $summary;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?int
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNature(): string
|
||||
{
|
||||
return $this->nature;
|
||||
}
|
||||
|
||||
public function setNature(string $nature): static
|
||||
{
|
||||
$this->nature = $nature;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(int $status): static
|
||||
public function setStatus(string $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDueDate(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dueDate;
|
||||
}
|
||||
|
||||
public function setDueDate(?\DateTimeInterface $dueDate): static
|
||||
{
|
||||
$this->dueDate = $dueDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, User>
|
||||
*/
|
||||
@ -102,4 +165,9 @@ class Project
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimelines(): Collection
|
||||
{
|
||||
return $this->timelines;
|
||||
}
|
||||
}
|
||||
|
86
src/Entity/ProjectTimeline.php
Normal file
86
src/Entity/ProjectTimeline.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ProjectTimelineRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ProjectTimelineRepository::class)]
|
||||
class ProjectTimeline
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'timelines')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private Project $project;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private User $user;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private \DateTimeInterface $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'array')]
|
||||
private array $description;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProject(): Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(Project $project): static
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeInterface $createAt): static
|
||||
{
|
||||
$this->createdAt = $createAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): array
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(array $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
155
src/EventListener/ProjectListener.php
Normal file
155
src/EventListener/ProjectListener.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectTimeline;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
#[AsDoctrineListener(event: 'onFlush')]
|
||||
class ProjectListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
$uow = $em->getUnitOfWork();
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!$entity instanceof Project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$timeline = new ProjectTimeline();
|
||||
$timeline->setProject($entity);
|
||||
$timeline->setUser($user);
|
||||
$timeline->setCreatedAt(new \DateTime());
|
||||
$timeline->setDescription(['created' => true]);
|
||||
|
||||
$em->persist($timeline);
|
||||
$meta = $em->getClassMetadata(ProjectTimeline::class);
|
||||
$uow->computeChangeSet($meta, $timeline);
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$changeSet = $uow->getEntityChangeSet($entity);
|
||||
$changes = [];
|
||||
|
||||
foreach ($changeSet as $field => [$old, $new]) {
|
||||
$oldStr = $this->stringify($old);
|
||||
$newStr = $this->stringify($new);
|
||||
|
||||
if ($oldStr !== $newStr) {
|
||||
$changes[$field] = [
|
||||
'old' => $oldStr,
|
||||
'new' => $newStr,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
$timeline = new ProjectTimeline();
|
||||
$timeline->setProject($entity);
|
||||
$timeline->setUser($user);
|
||||
$timeline->setCreatedAt(new \DateTime());
|
||||
$timeline->setDescription($changes);
|
||||
|
||||
$em->persist($timeline);
|
||||
$meta = $em->getClassMetadata(ProjectTimeline::class);
|
||||
$uow->computeChangeSet($meta, $timeline);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledCollectionUpdates() as $col) {
|
||||
$owner = $col->getOwner();
|
||||
|
||||
if (!$owner instanceof Project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapping = $col->getMapping();
|
||||
$fieldName = $mapping['fieldName'];
|
||||
|
||||
$added = $col->getInsertDiff();
|
||||
$removed = $col->getDeleteDiff();
|
||||
|
||||
$changes = [];
|
||||
|
||||
foreach ($added as $addedEntity) {
|
||||
$changes[$fieldName.'_added'][] = $this->stringify($addedEntity);
|
||||
}
|
||||
|
||||
foreach ($removed as $removedEntity) {
|
||||
$changes[$fieldName.'_removed'][] = $this->stringify($removedEntity);
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
$timeline = new ProjectTimeline();
|
||||
$timeline->setProject($owner);
|
||||
$timeline->setUser($user);
|
||||
$timeline->setCreatedAt(new \DateTime());
|
||||
$timeline->setDescription($changes);
|
||||
|
||||
$em->persist($timeline);
|
||||
$meta = $em->getClassMetadata(ProjectTimeline::class);
|
||||
$uow->computeChangeSet($meta, $timeline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function stringify(mixed $value): string
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if ($value instanceof Collection || is_array($value)) {
|
||||
$elements = [];
|
||||
foreach ($value as $item) {
|
||||
$elements[] = $this->stringify($item);
|
||||
}
|
||||
|
||||
return '['.implode(', ', $elements).']';
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
if (method_exists($value, '__toString')) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
foreach (['getUsername', 'getName', 'getTitle', 'getEmail', 'getId'] as $method) {
|
||||
if (method_exists($value, $method)) {
|
||||
return (string) $value->{$method}();
|
||||
}
|
||||
}
|
||||
|
||||
return get_class($value);
|
||||
}
|
||||
|
||||
return json_encode($value);
|
||||
}
|
||||
}
|
@ -4,10 +4,12 @@ namespace App\Form;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use Bnine\MdEditorBundle\Form\Type\MarkdownType;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@ -26,8 +28,32 @@ class ProjectType extends AbstractType
|
||||
'label' => 'Titre',
|
||||
])
|
||||
|
||||
->add('summary', TextareaType::class, [
|
||||
'label' => 'Résumé',
|
||||
])
|
||||
|
||||
->add('status', ChoiceType::class, [
|
||||
'choices' => ['Brouillon' => 0],
|
||||
'label' => 'Statut',
|
||||
'choices' => [
|
||||
Project::DRAFT => Project::DRAFT,
|
||||
Project::TOVOTE => Project::TOVOTE,
|
||||
Project::VOTED => Project::VOTED,
|
||||
Project::ARCHIVED => Project::ARCHIVED,
|
||||
],
|
||||
])
|
||||
|
||||
->add('nature', ChoiceType::class, [
|
||||
'label' => 'Nature',
|
||||
'choices' => [
|
||||
Project::NATURE_COLLECTIVE => Project::NATURE_COLLECTIVE,
|
||||
Project::NATURE_STRATEGIC => Project::NATURE_STRATEGIC,
|
||||
Project::NATURE_TACTICAL => Project::NATURE_TACTICAL,
|
||||
],
|
||||
])
|
||||
|
||||
->add('description', MarkdownType::class, [
|
||||
'label' => 'Description du Projet',
|
||||
'markdown_height' => 900,
|
||||
])
|
||||
|
||||
->add('users', EntityType::class, [
|
||||
|
18
src/Repository/ProjectTimelineRepository.php
Normal file
18
src/Repository/ProjectTimelineRepository.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ProjectTimeline;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ProjectTimeline>
|
||||
*/
|
||||
class ProjectTimelineRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ProjectTimeline::class);
|
||||
}
|
||||
}
|
60
src/Security/ProjectVoter.php
Normal file
60
src/Security/ProjectVoter.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class ProjectVoter extends Voter
|
||||
{
|
||||
// Les actions que ce voter supporte
|
||||
private const EDIT = 'EDIT';
|
||||
private const VIEW = 'VIEW';
|
||||
private const DELETE = 'DELETE';
|
||||
private const MOVEDRAFT = 'MOVEDRAFT';
|
||||
|
||||
protected function supports(string $attribute, $subject): bool
|
||||
{
|
||||
$attributes = [self::EDIT, self::VIEW, self::DELETE, self::MOVEDRAFT];
|
||||
|
||||
return in_array($attribute, $attributes) && $subject instanceof Project;
|
||||
}
|
||||
|
||||
private function canView(Project $project, User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function canEdit(Project $project, User $user): bool
|
||||
{
|
||||
return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user));
|
||||
}
|
||||
|
||||
private function canDelete(Project $project, User $user): bool
|
||||
{
|
||||
return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user));
|
||||
}
|
||||
|
||||
private function canMoveDraft(Project $project, User $user): bool
|
||||
{
|
||||
return $user->hasRole('ROLE_ADMIN') || (Project::TOVOTE === $project->getStatus() && $project->getUsers()->contains($user));
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($attribute) {
|
||||
self::VIEW => $this->canView($project, $user),
|
||||
self::EDIT => $this->canEdit($project, $user),
|
||||
self::DELETE => $this->canDelete($project, $user),
|
||||
self::MOVEDRAFT => $this->canMoveDraft($project, $user),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
"version": "v1.0.4"
|
||||
},
|
||||
"bnine/mdeditorbundle": {
|
||||
"version": "v0.1.1"
|
||||
"version": "v1.1.1"
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "1.1",
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
<script src="{{ asset('lib/jquery/jquery.min.js') }}"></script>
|
||||
<script src="{{ asset('lib/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
||||
<script src="{{ asset('lib/fontawesome/fontawesome-free.index.js') }}"></script>
|
||||
<!-- <script src="{{ asset('lib/fontawesome/fontawesome-free.index.js') }}"></script> -->
|
||||
<script src="{{ asset('lib/datatables/datatables.min.js') }}"></script>
|
||||
<script src="{{ asset('lib/datatables/datatables.init.js') }}"></script>
|
||||
<script src="{{ asset('lib/select2/select2.min.js') }}"></script>
|
||||
@ -30,6 +30,7 @@
|
||||
<script src="{{ asset('lib/jqueryui/jquery-ui.min.js') }}"></script>
|
||||
<script src="{{ asset('lib/chart/chart.js') }}"></script>
|
||||
|
||||
<script src="{{ asset('bundles/bninemdeditor/bninemdeditor.js') }}"></script>
|
||||
<script src="{{ asset('lib/app/app.js') }}"></script>
|
||||
|
||||
{% block javascripts %}
|
||||
|
@ -1,12 +1,25 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
|
||||
|
||||
{%block body%}
|
||||
<h2>Projets</h2>
|
||||
|
||||
<div class='d-flex' style='justify-content: center'>
|
||||
<div class='d-flex' style='justify-content: left'>
|
||||
{% for project in projects %}
|
||||
<div class='card'>
|
||||
<h5>{{project.title}}</h5>
|
||||
{{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:0})) }}
|
||||
<div class='card' style='width:300px'>
|
||||
<div class='card-header d-flex justify-content-between align-items-center'>
|
||||
{{project.title}}
|
||||
|
||||
<div>
|
||||
{% if is_granted('EDIT', project) %}
|
||||
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-pencil"></i></button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-secondary btn-sm"><i class="fas fa-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='card-body'>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
@ -14,31 +14,40 @@
|
||||
{% include('include/error.html.twig') %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mx-auto">
|
||||
<div class="col-md-4 mx-auto">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Information</div>
|
||||
<div class="card-body">
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.nature) }}
|
||||
{{ form_row(form.status) }}
|
||||
{{ form_row(form.users) }}
|
||||
{{ form_row(form.summary) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:1})) }}
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Timeline</div>
|
||||
<div class="card-body">
|
||||
{% include('project/timeline.html.twig') %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mx-auto">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Permissions</div>
|
||||
<div class="card-header">Description du Projet</div>
|
||||
<div class="card-body">
|
||||
{{ form_row(form.users) }}
|
||||
{{ form_widget(form.description) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if mode=="update" %}
|
||||
{{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:1})) }}
|
||||
{% endif %}
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block localscript %}
|
||||
|
@ -22,7 +22,7 @@
|
||||
<a href="{{ path(routeupdate,{id:project.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
|
||||
</td>
|
||||
<td>{{project.title}}</td>
|
||||
<td>{{project.updateAt}}</td>
|
||||
<td>{{project.timelines.first ? project.timelines.first.createdAt|date('d/m/Y H:i') : '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
57
templates/project/timeline.html.twig
Normal file
57
templates/project/timeline.html.twig
Normal file
@ -0,0 +1,57 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<div class="timeline">
|
||||
{% for event in project.timelines %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker"></div>
|
||||
<div class="timeline-content">
|
||||
<p class="text-muted small mb-1">
|
||||
{{ event.createdAt|date('d/m/Y H:i') }} par <strong>{{ event.user.username }}</strong><br>
|
||||
{% for field, change in event.description %}
|
||||
<strong>{{ field }}</strong>
|
||||
{% if field=="status" %}
|
||||
<span class="text-danger">{{ change.old ?? '' }}</span>
|
||||
→
|
||||
<span class="text-success">{{ change.new ?? '' }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if not change.old is defined %}
|
||||
= <span>{{ change[0] }}</span>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timeline {
|
||||
border-left: 3px solid #dee2e6;
|
||||
padding-left: 2rem;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
}
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
margin-left: -23px;
|
||||
}
|
||||
.timeline-content {
|
||||
line-height:14px;
|
||||
}
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: -1.1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
background: #0d6efd;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 3px #0d6efd44;
|
||||
}
|
||||
.timeline-content {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user