Loading module/Oscar/src/Oscar/Controller/ProjectController.php +11 −26 Original line number Diff line number Diff line Loading @@ -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, Loading module/Oscar/src/Oscar/Service/ProjectService.php +112 −17 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 = [ Loading @@ -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; Loading Loading @@ -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 = []; Loading @@ -789,7 +883,8 @@ class ProjectService implements UseServiceContainer $output[] = $projectGrantApiService->getActivityJson( $activity->getId(), $urlHelper, $oscarUserContext $oscarUserContext, 'core,budget' ); } return $output; Loading ui/src/views/Project.vue +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:{ Loading @@ -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; }); } }, Loading @@ -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> Loading
module/Oscar/src/Oscar/Controller/ProjectController.php +11 −26 Original line number Diff line number Diff line Loading @@ -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, Loading
module/Oscar/src/Oscar/Service/ProjectService.php +112 −17 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 = [ Loading @@ -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; Loading Loading @@ -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 = []; Loading @@ -789,7 +883,8 @@ class ProjectService implements UseServiceContainer $output[] = $projectGrantApiService->getActivityJson( $activity->getId(), $urlHelper, $oscarUserContext $oscarUserContext, 'core,budget' ); } return $output; Loading
ui/src/views/Project.vue +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:{ Loading @@ -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; }); } }, Loading @@ -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>