Browse Source

Ajout Crud

François Drouhard 1 month ago
parent
commit
a1d4424d00

+ 1 - 1
assets/app.js

@@ -7,4 +7,4 @@ import './bootstrap.js';
  */
 import './styles/app.css';
 
-import './js/main.js';
+import './styles/counter.css'

+ 33 - 18
assets/styles/counter.css

@@ -13,23 +13,10 @@
     -moz-osx-font-smoothing: grayscale;
   }
   
-  .counter {
-  
-  }
-  
   input {
     font-size: 1.7rem;
   }
   
-  a {
-    font-weight: 500;
-    color: #646cff;
-    text-decoration: inherit;
-  }
-  a:hover {
-    color: #535bf2;
-  }
-  
   .hidden {
     display: none;
   }
@@ -79,12 +66,28 @@
   .card {
     padding: 2em;
   }
+
+  input {
+    height: 1.5rem;
+    padding: 0.5rem 1rem;
+    margin: 0.5rem 1rem;
+  }
   
   .read-the-docs {
     color: #888;
   }
   
-  button {
+  a {
+    font-weight: 500;
+  }
+  a:hover {
+    color: #535bf2;
+  }
+  
+  .btn {
+    color: #9ba0f8;
+    text-decoration: none;
+    display: inline-block;
     border-radius: 8px;
     border: 1px solid transparent;
     padding: 0.6em 1.2em;
@@ -94,12 +97,24 @@
     background-color: #1a1a1a;
     cursor: pointer;
     transition: border-color 0.25s;
+    margin: 0.3rem 0.2rem;;
+  }
+
+  .btn-del {
+    background-color: #e41c1c !important;
+    color: white;
+  }
+
+  .btn-del:hover {
+    border-color: #ffffff !important;
+    background-color: #d10000 !important;
   }
-  button:hover {
+
+  .btn:hover {
     border-color: #646cff;
   }
-  button:focus,
-  button:focus-visible {
+  .btn:focus,
+  .btn:focus-visible {
     outline: 4px auto -webkit-focus-ring-color;
   }
   
@@ -111,7 +126,7 @@
     a:hover {
       color: #747bff;
     }
-    button {
+    .btn {
       background-color: #f9f9f9;
     }
   }

+ 2 - 2
config/packages/translation.yaml

@@ -1,7 +1,7 @@
 framework:
-    default_locale: en
+    default_locale: fr
     translator:
         default_path: '%kernel.project_dir%/translations'
         fallbacks:
-            - en
+            - fr
         providers:

+ 4 - 0
importmap.php

@@ -16,6 +16,10 @@ return [
         'path' => './assets/app.js',
         'entrypoint' => true,
     ],
+    'main' => [
+        'path' => './assets/js/main.js',
+        'entrypoint' => true,
+    ],
     '@hotwired/stimulus' => [
         'version' => '3.2.2',
     ],

+ 101 - 0
src/Controller/AdminController.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace App\Controller;
+
+use App\Entity\Counter;
+use App\Form\CounterType;
+use App\Repository\CounterRepository;
+use App\Service\CounterManager;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Attribute\Route;
+use Symfony\Component\Workflow\Exception\LogicException;
+use Symfony\Component\Workflow\WorkflowInterface;
+
+#[Route('/admin')]
+class AdminController extends AbstractController
+{
+    public function __construct(protected WorkflowInterface $countdownStateMachine)
+    {
+        
+    }
+    #[Route('/', name: 'app_admin_index', methods: ['GET'])]
+    public function index(
+        CounterRepository $counterRepository,
+    ): Response
+    {
+        return $this->render('admin/index.html.twig', [
+            'counters' => $counterRepository->findAll()
+        ]);
+    }
+
+    #[Route('/new', name: 'app_admin_new', methods: ['GET', 'POST'])]
+    public function new(Request $request, CounterManager $counterManager): Response
+    {
+        $counter = new Counter();
+        $form = $this->createForm(CounterType::class, $counter);
+        $form->handleRequest($request);
+
+        if ($form->isSubmitted() && $form->isValid()) {
+            $this->countdownStateMachine->getMarking($counter);
+
+            return $this->redirectToRoute('app_admin_index', [], Response::HTTP_SEE_OTHER);
+        }
+
+        return $this->render('admin/new.html.twig', [
+            'counter' => $counter,
+            'form' => $form,
+        ]);
+    }
+
+    #[Route('/{id}/start', name: 'app_admin_start', methods: ['GET'])]
+    public function start(Counter $counter): RedirectResponse
+    {
+        try {
+            $this->countdownStateMachine->apply($counter, Counter::TRANSITION_TO_STARTED);
+        } catch(LogicException $e) {
+
+        }
+        return $this->redirectToRoute('app_admin_index');
+    }
+
+    #[Route('/{id}', name: 'app_admin_show', methods: ['GET'])]
+    public function show(Counter $counter): Response
+    {
+        return $this->render('admin/show.html.twig', [
+            'counter' => $counter,
+        ]);
+    }
+
+    #[Route('/{id}/edit', name: 'app_admin_edit', methods: ['GET', 'POST'])]
+    public function edit(Request $request, Counter $counter, EntityManagerInterface $entityManager): Response
+    {
+        $form = $this->createForm(CounterType::class, $counter);
+        $form->handleRequest($request);
+
+        if ($form->isSubmitted() && $form->isValid()) {
+            $entityManager->flush();
+
+            return $this->redirectToRoute('app_admin_index', [], Response::HTTP_SEE_OTHER);
+        }
+
+        return $this->render('admin/edit.html.twig', [
+            'counter' => $counter,
+            'form' => $form,
+        ]);
+    }
+
+    #[Route('/{id}', name: 'app_admin_delete', methods: ['POST'])]
+    public function delete(Request $request, Counter $counter, EntityManagerInterface $entityManager): Response
+    {
+        if ($this->isCsrfTokenValid('delete'.$counter->getId(), $request->request->get('_token'))) {
+            $entityManager->remove($counter);
+            $entityManager->flush();
+        }
+
+        return $this->redirectToRoute('app_admin_index', [], Response::HTTP_SEE_OTHER);
+    }
+}

+ 20 - 8
src/Controller/CounterController.php

@@ -3,16 +3,20 @@
 namespace App\Controller;
 
 use App\Entity\Counter;
+use App\Form\DataTransformer\MinutesToPeriodTransformer;
 use App\Repository\CounterRepository;
 use App\Service\CounterManager;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Attribute\Route;
+use Symfony\Component\Workflow\Exception\LogicException;
 use Symfony\Component\Workflow\WorkflowInterface;
 
+#[Route('/counter')]
 class CounterController extends AbstractController
 {
+
     public function __construct(
         protected CounterManager $counterManager,
         protected WorkflowInterface $countdownStateMachine,
@@ -22,42 +26,50 @@ class CounterController extends AbstractController
         
     }
 
-    #[Route('/counter', name: 'app_counter', methods: ['GET'])]
+    #[Route('/', name: 'app_counter', methods: ['GET'])]
     public function index(CounterRepository $counterRepository): JsonResponse
     {
         $counters = $counterRepository->findAll();
         return $this->json($counters, 200);
     }
 
-    #[Route('/counter', name: 'app_counter_post', methods: ['POST'])]
+    #[Route('/', name: 'app_counter_post', methods: ['POST'])]
     public function postCounter(
         Request $request,
-        CounterManager $counterManager,
+        MinutesToPeriodTransformer $minutesToPeriodTransformer
     ): JsonResponse
     {
         $ttl = $request->toArray()['ttl'] ?? null;
         $name = $request->toArray()['name'] ?? null;
 
-        $counter = $counterManager->init($ttl, $name);
+        $counter = new Counter();
+        $counter
+            ->setTimeTolive($minutesToPeriodTransformer->reverseTransform($ttl))
+            ->setName($name);
+
+        $this->countdownStateMachine->getMarking($counter);
 
         return $this->json($counter, 200);
     }
 
-    #[Route('/counter/start/{id}', name: 'app_counter_start')]
+    #[Route('/start/{id}', name: 'app_counter_start')]
     public function startCounter(Counter $counter): JsonResponse
     {
-        $this->countdownStateMachine->apply($counter, Counter::TRANSITION_TO_STARTED);
+        try {
+            $this->countdownStateMachine->apply($counter, Counter::TRANSITION_TO_STARTED);
+        } catch(LogicException $e) {
 
+        }
         return $this->json($counter, 200);
     }
 
-    #[Route('counter/{id}', name: 'app_counter_id', methods: ['GET'])]
+    #[Route('/{id}', name: 'app_counter_id', methods: ['GET'])]
     public function getCounter(Counter $counter): JsonResponse
     {
         return $this->json($counter, 200);
     }
 
-    #[Route('counter/clear/{id}', name: 'app_counter_clear')]
+    #[Route('/clear/{id}', name: 'app_counter_clear')]
     public function clear(Counter $counter): JsonResponse
     {
         $this->countdownStateMachine->apply($counter, Counter::TRANSITION_TO_READY);

+ 35 - 0
src/Form/CounterType.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Counter;
+use App\Form\DataTransformer\MinutesToPeriodTransformer;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\IntegerType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class CounterType extends AbstractType
+{
+    public function __construct(protected MinutesToPeriodTransformer $minutesToPeriodTransormer)
+    {
+        
+    }
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('name')
+            ->add('timeToLive', IntegerType::class)
+        ;
+        $builder->get('timeToLive')
+            ->addModelTransformer($this->minutesToPeriodTransormer)
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Counter::class,
+        ]);
+    }
+}

+ 39 - 0
src/Form/DataTransformer/MinutesToPeriodTransformer.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Form\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+class MinutesToPeriodTransformer implements DataTransformerInterface
+{
+    /**
+     * Transforms a string (period) to an int
+     * 
+     * @param string|null $period
+     */
+    public function transform($period): int
+    {
+        if (null === $period) {
+            return 0;
+        }
+
+        return (int) filter_var($period, FILTER_SANITIZE_NUMBER_INT) ?? 0;
+    }
+
+    /**
+     * Transforms a string (number) to an object (issue).
+     *
+     * @param  int $minutes
+     * @throws TransformationFailedException if object (issue) is not found.
+     */
+    public function reverseTransform($minutes): ?string
+    {
+        // no issue number? It's optional, so that's ok
+        if (!$minutes) {
+            $minutes = 0;
+        }
+
+        return 'PT' . $minutes . 'M';
+    }
+}

+ 6 - 12
src/Service/CounterManager.php

@@ -9,7 +9,7 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
 use Symfony\Component\HttpKernel\Event\ControllerEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\Workflow\Attribute\AsCompletedListener;
-use Symfony\Component\Workflow\Attribute\AsTransitionListener;
+use Symfony\Component\Workflow\Attribute\AsEnteredListener;
 use Symfony\Component\Workflow\Event\Event;
 use Symfony\Component\Workflow\WorkflowInterface;
 
@@ -24,19 +24,13 @@ class CounterManager
 
     }
 
-    public function init(string $ttl, ?string $name = null): Counter
+    #[AsEnteredListener('countdown', Counter::STATE_READY)]
+    public function init(Event $event): void
     {
-        $counter = new Counter();
-        $counter
-            ->setName($name)
-            ->setTimeTolive($ttl)
-        ;
-
-        $this->countdownStateMachine->getMarking($counter);
+        /** @var Counter $counter */
+        $counter = $event->getSubject();
         $this->em->persist($counter);
         $this->em->flush();
-
-        return $counter;
     }
 
     #[AsCompletedListener('countdown', Counter::TRANSITION_TO_STARTED)]
@@ -66,7 +60,7 @@ class CounterManager
             ->setStartTime(null)
             ->setEndTime(null)
         ;
-        $this->em->flush();
+        //$this->em->flush();
     }
 
     #[AsEventListener(KernelEvents::CONTROLLER)]

+ 26 - 0
src/Twig/ConvertPeriod.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Twig;
+
+use App\Form\DataTransformer\MinutesToPeriodTransformer;
+use Twig\Extension\AbstractExtension;
+use Twig\TwigFilter;
+
+class ConvertPeriod extends AbstractExtension
+{
+    public function __construct(protected MinutesToPeriodTransformer $minutesToPeriodTransformer)
+    {
+        
+    }
+    public function getFilters(): array
+    {
+        return [
+            new TwigFilter('minutes', [$this, 'showMinutes'])
+        ];
+    }
+
+    public function showMinutes(?string $period): int
+    {
+        return $this->minutesToPeriodTransformer->transform($period);
+    }
+}

+ 4 - 0
templates/admin/_delete_form.html.twig

@@ -0,0 +1,4 @@
+<form method="post" action="{{ path('app_admin_delete', {'id': counter.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');">
+    <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ counter.id) }}">
+    <button class="btn btn-del">{{ "Delete" | trans }}</button>
+</form>

+ 4 - 0
templates/admin/_form.html.twig

@@ -0,0 +1,4 @@
+{{ form_start(form) }}
+    {{ form_widget(form) }}
+    <button class="btn">{{ button_label|default('Save') | trans }}</button>
+{{ form_end(form) }}

+ 1 - 0
templates/admin/_start_form.html.twig

@@ -0,0 +1 @@
+<a class="btn" href="{{ path('app_admin_start', {'id': counter.id})}}">{{ "start" | trans }}</a>

+ 13 - 0
templates/admin/edit.html.twig

@@ -0,0 +1,13 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Edit Counter{% endblock %}
+
+{% block body %}
+    <h1>{{ "Edit Counter" | trans }}</h1>
+
+    {{ include('admin/_form.html.twig', {'button_label': 'Update'}) }}
+
+    <a class="btn" href="{{ path('app_admin_index') }}">{{ "back to list" | trans }}</a>
+
+    {{ include('admin/_delete_form.html.twig') }}
+{% endblock %}

+ 44 - 0
templates/admin/index.html.twig

@@ -0,0 +1,44 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Counter index{% endblock %}
+
+{% block body %}
+    <h1>Counter index</h1>
+
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Id</th>
+                <th>StartTime</th>
+                <th>EndTime</th>
+                <th>Name</th>
+                <th>State</th>
+                <th>TimeToLive</th>
+                <th>actions</th>
+            </tr>
+        </thead>
+        <tbody>
+        {% for counter in counters %}
+            <tr>
+                <td>{{ counter.id }}</td>
+                <td>{{ counter.startTime ? counter.startTime|date('H:i:s') : '' }}</td>
+                <td>{{ counter.endTime ? counter.endTime|date('H:i:s') : '' }}</td>
+                <td>{{ counter.name }}</td>
+                <td>{{ counter.state }}</td>
+                <td>{{ counter.timeToLive | minutes }}</td>
+                <td>
+                    <a class="btn" href="{{ path('app_admin_show', {'id': counter.id}) }}">{{ "show" | trans }}</a>
+                    <a class="btn" href="{{ path('app_admin_edit', {'id': counter.id}) }}">{{ "edit" | trans }}</a>
+                    {{ include('admin/_start_form.html.twig', {'counter': counter}) }}
+                </td>
+            </tr>
+        {% else %}
+            <tr>
+                <td colspan="7">{{ "no records found" | trans }}</td>
+            </tr>
+        {% endfor %}
+        </tbody>
+    </table>
+
+    <a class="btn" href="{{ path('app_admin_new') }}">{{ "Create new" | trans }}</a>
+{% endblock %}

+ 11 - 0
templates/admin/new.html.twig

@@ -0,0 +1,11 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ "New Counter" | trans }}{% endblock %}
+
+{% block body %}
+    <h1>{{ "Create new Counter" | trans }}</h1>
+
+    {{ include('admin/_form.html.twig') }}
+
+    <a class="btn" href="{{ path('app_admin_index') }}">{{ "back to list" | trans }}</a>
+{% endblock %}

+ 42 - 0
templates/admin/show.html.twig

@@ -0,0 +1,42 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Counter{% endblock %}
+
+{% block body %}
+    <h1>{{ "Counter" | trans }}</h1>
+
+    <table class="table">
+        <tbody>
+            <tr>
+                <th>{{ "id" | trans }}</th>
+                <td>{{ counter.id }}</td>
+            </tr>
+            <tr>
+                <th>{{ "StartTime" | trans }}</th>
+                <td>{{ counter.startTime ? counter.startTime|date('H:i:s') : '' }}</td>
+            </tr>
+            <tr>
+                <th>{{ "EndTime" | trans }}</th>
+                <td>{{ counter.endTime ? counter.endTime|date('H:i:s') : '' }}</td>
+            </tr>
+            <tr>
+                <th>{{ "Name" | trans }}</th>
+                <td>{{ counter.name }}</td>
+            </tr>
+            <tr>
+                <th>{{ "State" | trans }}</th>
+                <td>{{ counter.state }}</td>
+            </tr>
+            <tr>
+                <th>{{ "TimeToLive" | trans }}</th>
+                <td>{{ counter.timeToLive | minutes}}</td>
+            </tr>
+        </tbody>
+    </table>
+
+    <a class="btn" href="{{ path('app_admin_index') }}">{{ "back to list" | trans }}</a>
+
+    <a class="btn" href="{{ path('app_admin_edit', {'id': counter.id}) }}">{{ "edit" | trans }}</a>
+
+    {{ include('admin/_delete_form.html.twig') }}
+{% endblock %}

+ 2 - 2
templates/base.html.twig

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html>
+<html lang="fr">
     <head>
         <meta charset="UTF-8">
         <title>{% block title %}Welcome!{% endblock %}</title>
@@ -7,8 +7,8 @@
         {% block stylesheets %}
         {% endblock %}
 
+        {% block importmap %}{{ importmap('app') }}{% endblock %}
         {% block javascripts %}
-            {% block importmap %}{{ importmap('app') }}{% endblock %}
         {% endblock %}
     </head>
     <body>

+ 4 - 1
templates/index/index.html.twig

@@ -2,7 +2,10 @@
 
 {% block title %}Countdown{% endblock %}
 
+{% block javascripts %}
+    {{ importmap('main') }}
+{% endblock %}
 {% block body %}
-    <div class="countdown-timer"></div>
+    <div class="countdown-timer" data-id="{{ id }}"></div>
     <div class="message"></div>
 {% endblock %}

+ 136 - 0
tests/Controller/CounterControllerTest.php

@@ -0,0 +1,136 @@
+<?php
+
+namespace App\Test\Controller;
+
+use App\Entity\Counter;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\EntityRepository;
+use Symfony\Bundle\FrameworkBundle\KernelBrowser;
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+class CounterControllerTest extends WebTestCase
+{
+    private KernelBrowser $client;
+    private EntityManagerInterface $manager;
+    private EntityRepository $repository;
+    private string $path = '/admin/';
+
+    protected function setUp(): void
+    {
+        $this->client = static::createClient();
+        $this->manager = static::getContainer()->get('doctrine')->getManager();
+        $this->repository = $this->manager->getRepository(Counter::class);
+
+        foreach ($this->repository->findAll() as $object) {
+            $this->manager->remove($object);
+        }
+
+        $this->manager->flush();
+    }
+
+    public function testIndex(): void
+    {
+        $crawler = $this->client->request('GET', $this->path);
+
+        self::assertResponseStatusCodeSame(200);
+        self::assertPageTitleContains('Counter index');
+
+        // Use the $crawler to perform additional assertions e.g.
+        // self::assertSame('Some text on the page', $crawler->filter('.p')->first());
+    }
+
+    public function testNew(): void
+    {
+        $this->markTestIncomplete();
+        $this->client->request('GET', sprintf('%snew', $this->path));
+
+        self::assertResponseStatusCodeSame(200);
+
+        $this->client->submitForm('Save', [
+            'counter[startTime]' => 'Testing',
+            'counter[endTime]' => 'Testing',
+            'counter[name]' => 'Testing',
+            'counter[state]' => 'Testing',
+            'counter[timeToLive]' => 'Testing',
+        ]);
+
+        self::assertResponseRedirects($this->path);
+
+        self::assertSame(1, $this->repository->count([]));
+    }
+
+    public function testShow(): void
+    {
+        $this->markTestIncomplete();
+        $fixture = new Counter();
+        $fixture->setStartTime('My Title');
+        $fixture->setEndTime('My Title');
+        $fixture->setName('My Title');
+        $fixture->setState('My Title');
+        $fixture->setTimeToLive('My Title');
+
+        $this->manager->persist($fixture);
+        $this->manager->flush();
+
+        $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId()));
+
+        self::assertResponseStatusCodeSame(200);
+        self::assertPageTitleContains('Counter');
+
+        // Use assertions to check that the properties are properly displayed.
+    }
+
+    public function testEdit(): void
+    {
+        $this->markTestIncomplete();
+        $fixture = new Counter();
+        $fixture->setStartTime('Value');
+        $fixture->setEndTime('Value');
+        $fixture->setName('Value');
+        $fixture->setState('Value');
+        $fixture->setTimeToLive('Value');
+
+        $this->manager->persist($fixture);
+        $this->manager->flush();
+
+        $this->client->request('GET', sprintf('%s%s/edit', $this->path, $fixture->getId()));
+
+        $this->client->submitForm('Update', [
+            'counter[startTime]' => 'Something New',
+            'counter[endTime]' => 'Something New',
+            'counter[name]' => 'Something New',
+            'counter[state]' => 'Something New',
+            'counter[timeToLive]' => 'Something New',
+        ]);
+
+        self::assertResponseRedirects('/admin/');
+
+        $fixture = $this->repository->findAll();
+
+        self::assertSame('Something New', $fixture[0]->getStartTime());
+        self::assertSame('Something New', $fixture[0]->getEndTime());
+        self::assertSame('Something New', $fixture[0]->getName());
+        self::assertSame('Something New', $fixture[0]->getState());
+        self::assertSame('Something New', $fixture[0]->getTimeToLive());
+    }
+
+    public function testRemove(): void
+    {
+        $this->markTestIncomplete();
+        $fixture = new Counter();
+        $fixture->setStartTime('Value');
+        $fixture->setEndTime('Value');
+        $fixture->setName('Value');
+        $fixture->setState('Value');
+        $fixture->setTimeToLive('Value');
+
+        $this->manager->persist($fixture);
+        $this->manager->flush();
+
+        $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId()));
+        $this->client->submitForm('Delete');
+
+        self::assertResponseRedirects('/admin/');
+        self::assertSame(0, $this->repository->count([]));
+    }
+}

+ 15 - 0
translations/messages.fr.yaml

@@ -0,0 +1,15 @@
+Name: "Nom du compteur"
+Time to live: "Temps (minutes)"
+Update: Enregistrer
+Delete: Supprimer
+edit: Modifier
+show: Voir
+Create new: Créer nouveau compteur
+start: Démarrer
+back to list: Retour à la liste des compteurs
+Counter: Compteur
+id: id
+StartTime: Heure de départ
+EndTime: Heure de fin
+State: Statut
+TimeToLive: Temps de décompte