Commit 3db0cfb7 authored by Stephane Bouvry's avatar Stephane Bouvry
Browse files

Fiche projet (base)

parent 95da7bc7
Loading
Loading
Loading
Loading
Loading
+11 −26
Original line number Diff line number Diff line
@@ -281,33 +281,18 @@ class ProjectController extends AbstractOscarController


            if ($this->getRequest()->isXmlHttpRequest() || $format === 'json') {
                $perimeter = $this->params()->fromQuery('p', 'all');
                if ($perimeter) {
                    if ($perimeter == 'logs') {
                        $this->getOscarUserContextService()->check(Privileges::MAINTENANCE_MENU_ADMIN);
                try {
                    $perimeter = $this->params()->fromQuery('p', null);
                    $datas = $this->getProjectService()->api(
                        $entity->getId(),
                        $this->url(),
                        $this->getOscarUserContextService(),
                                'logs'
                        $perimeter
                    );
                    return $this->jsonOutput($datas);
                } catch (\Exception $e) {
                    return $this->jsonError($e->getMessage());
                }
                    } elseif ($perimeter == 'all') {
                        $datas = $this->getProjectService()->api(
                            $entity->getId(),
                            $this->url(),
                            $this->getOscarUserContextService(),
                            $perimeter
                        );
                        return $this->jsonOutput($datas);
                    } else {
                        throw new OscarException("Périmètre inconnu");
                    }
                }
            }
            return [
                'project' => $entity,
+112 −17
Original line number Diff line number Diff line
@@ -43,6 +43,11 @@ use Laminas\Mvc\Controller\Plugin\Url;
 */
class ProjectService implements UseServiceContainer
{
    public const PERIMETER_CORE = 'core';
    public const PERIMETER_ACTIVITIES = 'activities';
    public const PERIMETER_PERSONS = 'persons';
    public const PERIMETER_ORGANIZATIONS = 'organizations';
    public const PERIMETER_LOGS = 'logs';

    ///////////////////////////////////////////////////////////////////////////////////////////////////// ACCES SERVICES

@@ -114,24 +119,46 @@ class ProjectService implements UseServiceContainer
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public function getPerimetersKeys(): array
    {
        return [
            self::PERIMETER_CORE,
            self::PERIMETER_ACTIVITIES,
            self::PERIMETER_PERSONS,
            self::PERIMETER_ORGANIZATIONS,
            self::PERIMETER_LOGS,
        ];
    }

    public function api(
        int $projectId,
        ?Url $urlPlugin = null,
        ?OscarUserContext $oscarUserContext = null,
        ?string $perimeters = null
    ){
    ): array {

        $output = [];
        $logs = [];
        try {
            $project = $this->getProjectRepository()->find($projectId);

            if (!$project) {
                throw new OscarException("Project not found");
            }
        } catch (\Exception $exception) {
            throw new OscarException($exception->getMessage());
        }

        if ($perimeters === 'all') {
            $perimeters = null;
        }

        if( $perimeters === 'logs' ){
            $output = $this->getEntityManager()->getRepository(LogActivity::class)->getLogsProject($project->getId());
        if ($urlPlugin === null) {
            throw new OscarException("URL helper is required");
        }

        if ($perimeters !== null) {
            $perimeters = array_intersect(explode(',', $perimeters), $this->getPerimetersKeys());
        } else {
            $perimeters = $this->getPerimetersKeys();
        }

        $out = [
@@ -140,18 +167,74 @@ class ProjectService implements UseServiceContainer
            'warnings'    => null,
            'perimeter'   => $perimeters,
            'credentials' => null,
            'traces'       => $output
            'datas'       => [],
            'traces'      => [],
        ];

        if( $perimeters === 'all' ){
            $out['datas'] = [
                'core' => $this->getProjectDataCore($project),
                'activities' => $this->getProjectDataActivities(
        if (!$oscarUserContext) {
            $credentials = [
                'forced' => true,
                'read' => true,
            ];
        } else {
            $read = $oscarUserContext->hasPrivileges(Privileges::PROJECT_SHOW, $project);
            $credentials = [
                'read' => $read,
                self::PERIMETER_CORE => ['read' => $read],
                self::PERIMETER_ACTIVITIES => ['read' => $read],
                self::PERIMETER_PERSONS => [
                    'read' => $oscarUserContext->hasPrivileges(Privileges::PROJECT_PERSON_SHOW, $project),
                    'edit' => $oscarUserContext->hasPrivileges(Privileges::PROJECT_PERSON_MANAGE, $project),
                ],
                self::PERIMETER_ORGANIZATIONS => [
                    'read' => $oscarUserContext->hasPrivileges(Privileges::PROJECT_ORGANIZATION_SHOW, $project),
                    'edit' => $oscarUserContext->hasPrivileges(Privileges::PROJECT_ORGANIZATION_MANAGE, $project),
                ],
                self::PERIMETER_LOGS => [
                    'read' => $oscarUserContext->hasPrivileges(Privileges::MAINTENANCE_MENU_ADMIN),
                ],
            ];
        }

        if ($credentials['read'] !== true) {
            $out['error'] = 'access denied';
            return $out;
        }

        $out['credentials'] = $credentials;
        $forced = (($credentials['forced'] ?? false) === true);
        foreach ($perimeters as $perimeter) {
            if (!$forced) {
                if (isset($credentials[$perimeter]['read']) && $credentials[$perimeter]['read'] !== true) {
                    continue;
                }
            }

            switch ($perimeter) {
                case self::PERIMETER_CORE:
                    $out['datas'][self::PERIMETER_CORE] = $this->getProjectDataCore($project, $urlPlugin);
                    break;
                case self::PERIMETER_ACTIVITIES:
                    $out['datas'][self::PERIMETER_ACTIVITIES] = $this->getProjectDataActivities(
                        $project,
                        $urlPlugin,
                        $oscarUserContext
                ),
            ];
                    );
                    break;
                case self::PERIMETER_PERSONS:
                    $persons = [];
                    $out['datas'][self::PERIMETER_PERSONS] = $this->getPersonsProjectsAPI($project, $persons, $urlPlugin);
                    break;
                case self::PERIMETER_ORGANIZATIONS:
                    $organizations = [];
                    $out['datas'][self::PERIMETER_ORGANIZATIONS] = $this->getOrganizationsProjectsAPI($project, $organizations, $urlPlugin);
                    break;
                case self::PERIMETER_LOGS:
                    $logs = $this->getEntityManager()->getRepository(LogActivity::class)->getLogsProject($project->getId());
                    $out['datas'][self::PERIMETER_LOGS] = $logs;
                    $out['traces'] = $logs;
                    break;
            }
        }

        return $out;
@@ -770,16 +853,27 @@ class ProjectService implements UseServiceContainer


    ///
    private function getProjectDataCore(Project $project) :array
    private function getProjectDataCore(Project $project, ?Url $urlHelper = null): array
    {
        return [
        $out = [
            'id' => $project->getId(),
            'label' => $project->getLabel(),
            'acronym' => $project->getAcronym(),
            'description' => $project->getDescription(),
            'dateStart' => $project->getDateStart()?->format('Y-m-d H:i:s'),
            'dateEnd' => $project->getDateEnd()?->format('Y-m-d H:i:s'),
        ];

        if ($urlHelper) {
            $out['urls'] = [
                'show' => $urlHelper->fromRoute('project/show', ['id' => $project->getId()]),
                'show2' => $urlHelper->fromRoute('project/show2', ['id' => $project->getId()]),
            ];
        }

        return $out;
    }

    private function getProjectDataActivities(Project $project, Url $urlHelper, OscarUserContext $oscarUserContext): array
    {
        $output = [];
@@ -789,7 +883,8 @@ class ProjectService implements UseServiceContainer
            $output[] = $projectGrantApiService->getActivityJson(
                $activity->getId(),
                $urlHelper,
                $oscarUserContext
                $oscarUserContext,
                'core,budget'
            );
        }
        return $output;
+208 −23
Original line number Diff line number Diff line
<script>
import AxiosOscar from "../utils/AxiosOscar.js";

const PROJECT_TAB_ACTIVE_STOREKEY = 'project_tab_active';

export default {
  name: "Project",
  props:{
@@ -9,16 +12,33 @@ export default {
  data(){
    return {
      project: null,
      activities: []
      activities: [],
      persons: [],
      organizations: [],
      credentials: null,
      loading: false,
      uiActiveTab: 'general',
    }
  },

  watch: {
    uiActiveTab(value){
      window.localStorage.setItem(PROJECT_TAB_ACTIVE_STOREKEY, value);
    }
  },

  methods:{
    fetch(){
      AxiosOscar.get(this.url).then(ok => {
        this.project = ok.data.datas.core;
        this.activities = ok.data.datas.activities;
      })
      this.loading = true;
      AxiosOscar.get(this.url + '?f=json&p=core,activities,persons,organizations').then(ok => {
        this.credentials = ok.data.credentials || {};
        this.project = ok.data.datas?.core || null;
        this.activities = ok.data.datas?.activities || [];
        this.persons = ok.data.datas?.persons?.persons || [];
        this.organizations = ok.data.datas?.organizations?.organizations || [];
      }).finally(() => {
        this.loading = false;
      });
    }
  },

@@ -26,43 +46,208 @@ export default {
    montant(){
      let total = 0;
      this.activities.forEach(a => {
        if( a.credentials.budget.read ){
        if( a.credentials?.budget?.read && a.datas?.budget?.amount ){
          total += a.datas.budget.amount;
        }
      });
      return total;
    },
    activitiesRows(){
      const rows = this.activities.map((activity) => {
        const core = activity?.datas?.core || {};
        return {
          id: core.id || null,
          label: core.label || 'Activité',
          numOscar: core.numOscar || null,
          statusCode: core.status || null,
          statusText: core.status_label || null,
          dateStart: core.dateStart || null,
          dateEnd: core.dateEnd || null,
          pfi: core.pfi || null,
        };
      });
      return rows.sort((a, b) => {
        const aDate = a.dateStart ? new Date(a.dateStart).getTime() : Number.POSITIVE_INFINITY;
        const bDate = b.dateStart ? new Date(b.dateStart).getTime() : Number.POSITIVE_INFINITY;
        if (aDate === bDate) {
          return (a.label || '').localeCompare(b.label || '');
        }
        return aDate - bDate;
      });
    },
    personsSorted(){
      return [...this.persons].sort((a, b) => (a.enrolledLabel || '').localeCompare(b.enrolledLabel || ''));
    },
    organizationsSorted(){
      return [...this.organizations].sort((a, b) => (a.enrolledLabel || '').localeCompare(b.enrolledLabel || ''));
    }
  },

  mounted() {
    this.uiActiveTab = window.localStorage.getItem(PROJECT_TAB_ACTIVE_STOREKEY) || 'general';
    this.fetch();
  }
}
</script>

<template>
  <section class="fiche" v-if="project">
    <div class="fiche-header">
      <h1>
  <section class="fiche" v-if="project && !loading">
    <header>
      <div class="title">
        <i class="icon-cubes"></i>
        <abbr>{{ project.acronym }}</abbr>
        <span class="label">{{ project.label }}</span>
      </h1>
      <div class="description">
        {{ project.description }}
        <code class="cartouche primary">{{ project.acronym || '-' }}</code>
        <strong>{{ project.label }}</strong>
      </div>
      <nav>
        <div class="nav nav-tabs" role="tablist">
          <button class="nav-link"
                  :class="{'active': uiActiveTab === 'general'}"
                  @click="uiActiveTab = 'general'"
                  data-bs-toggle="tab" data-bs-target="#project-general"
                  type="button" role="tab">
            <i class="icon-bank"></i> Général
          </button>
          <button class="nav-link"
                  :class="{'active': uiActiveTab === 'activities'}"
                  @click="uiActiveTab = 'activities'"
                  data-bs-toggle="tab" data-bs-target="#project-activities"
                  type="button" role="tab">
            <i class="icon-cube"></i> Activités
          </button>
          <button class="nav-link"
                  :class="{'active': uiActiveTab === 'persons'}"
                  @click="uiActiveTab = 'persons'"
                  data-bs-toggle="tab" data-bs-target="#project-persons"
                  type="button" role="tab">
            <i class="icon-group"></i> Membres
          </button>
          <button class="nav-link"
                  :class="{'active': uiActiveTab === 'organizations'}"
                  @click="uiActiveTab = 'organizations'"
                  data-bs-toggle="tab" data-bs-target="#project-organizations"
                  type="button" role="tab">
            <i class="icon-building-filled"></i> Partenaires
          </button>
          <button class="nav-link"
                  :class="{'active': uiActiveTab === 'finances'}"
                  @click="uiActiveTab = 'finances'"
                  data-bs-toggle="tab" data-bs-target="#project-finances"
                  type="button" role="tab">
            <i class="icon-bank"></i> Finances
          </button>
        </div>
      </nav>
    </header>

    <div class="tab-content">
      <div class="tab-pane fade show"
           id="project-general"
           :class="{'active show': uiActiveTab === 'general'}"
           role="tabpanel">
        <div class="row row-bordered">
          <div class="col-md-6">
            <div class="labeled-value labeled-value-horizontal">
              <div class="label">Intitulé</div>
              <div class="value">{{ project.label }}</div>
            </div>
      <div class="montant">
        Montant :
        <strong>{{ montant }}</strong>
            <div class="labeled-value labeled-value-horizontal">
              <div class="label">Acronyme</div>
              <div class="value">{{ project.acronym || '-' }}</div>
            </div>
            <div class="labeled-value labeled-value-horizontal">
              <div class="label">Date de début</div>
              <div class="value">{{ project.dateStart ? $filters.date(project.dateStart) : '-' }}</div>
            </div>
            <div class="labeled-value labeled-value-horizontal">
              <div class="label">Date de fin</div>
              <div class="value">{{ project.dateEnd ? $filters.date(project.dateEnd) : '-' }}</div>
            </div>
          </div>
          <div class="col-md-6">
            <h3>Description</h3>
            <div class="description-limited">{{ project.description || '-' }}</div>
          </div>
        </div>
      </div>

      <div class="tab-pane fade"
           id="project-activities"
           :class="{'active show': uiActiveTab === 'activities'}"
           role="tabpanel">
        <table class="table table-condensed" v-if="activitiesRows.length > 0">
          <thead>
          <tr>
            <th>Activité</th>
            <th>NumOscar</th>
            <th>Statut (code)</th>
            <th>Statut</th>
            <th>CodeEOTP</th>
            <th>Date début</th>
            <th>Date fin</th>
          </tr>
          </thead>
          <tbody>
          <tr v-for="activity in activitiesRows" :key="activity.id || activity.label">
            <td>
              <a :href="activity.id ? ('/activites-de-recherche/fiche-detaillee/' + activity.id) : '#'">
                {{ activity.label }}
              </a>
            </td>
            <td>{{ activity.numOscar || '-' }}</td>
            <td>{{ activity.statusCode || '-' }}</td>
            <td>{{ activity.statusText || '-' }}</td>
            <td>{{ activity.pfi || '-' }}</td>
            <td>{{ activity.dateStart ? $filters.date(activity.dateStart) : '-' }}</td>
            <td>{{ activity.dateEnd ? $filters.date(activity.dateEnd) : '-' }}</td>
          </tr>
          </tbody>
        </table>
        <p v-else>Aucune activité</p>
      </div>

      <div class="tab-pane fade"
           id="project-persons"
           :class="{'active show': uiActiveTab === 'persons'}"
           role="tabpanel">
        <ul v-if="personsSorted.length > 0">
          <li v-for="person in personsSorted" :key="person.id + '-' + person.roleId">
            {{ person.enrolledLabel }} ({{ person.roleLabel }})
          </li>
        </ul>
        <p v-else>Aucun membre</p>
      </div>

      <div class="tab-pane fade"
           id="project-organizations"
           :class="{'active show': uiActiveTab === 'organizations'}"
           role="tabpanel">
        <ul v-if="organizationsSorted.length > 0">
          <li v-for="organization in organizationsSorted" :key="organization.id + '-' + organization.roleId">
            {{ organization.enrolledLabel }} ({{ organization.roleLabel }})
          </li>
        </ul>
        <p v-else>Aucun partenaire</p>
      </div>

      <div class="tab-pane fade"
           id="project-finances"
           :class="{'active show': uiActiveTab === 'finances'}"
           role="tabpanel">
        <div class="labeled-value labeled-value-horizontal">
          <div class="label">Montant total (activités visibles)</div>
          <div class="value text-private"><strong>{{ $filters.money(montant) }}</strong></div>
        </div>
      </div>
    </div>
  </section>
  <pre>
    {{ activities }}
  </pre>
  <p v-else-if="loading">Chargement...</p>
</template>

<style scoped>

.description-limited {
  line-height: 1.4;
  max-height: 14em;
  overflow-y: auto;
  white-space: pre-wrap;
}
</style>