Commit b0a974cf authored by Oleksii Savran's avatar Oleksii Savran

Audit logs for Accession and Cooperator

parent 359d0805
......@@ -5,13 +5,14 @@ import { AxiosInstance, AxiosRequestConfig } from 'axios';
import Accession from '@gringlobal/client/model/gringlobal/Accession';
import AccessionFilter from '@gringlobal/client/model/gringlobal/AccessionFilter';
import { IPageRequest, FilteredPage } from '@gringlobal/client/model/page';
import { IPageRequest, FilteredPage, Page } from '@gringlobal/client/model/page';
import { dereferenceReferences3 } from '@gringlobal/client/utilities';
import AccessionSource from '@gringlobal/client/model/gringlobal/AccessionSource';
import AccessionInvGroup from '@gringlobal/client/model/gringlobal/AccessionInvGroup';
import AccessionDetails from '@gringlobal/client/model/gringlobal/AccessionDetails';
import AccessionAction from '@gringlobal/client/model/gringlobal/AccessionAction';
import AccessionActionFilter from '@gringlobal/client/model/gringlobal/AccessionActionFilter';
import AuditLog from '@gringlobal/client/model/gringlobal/AuditLog';
const URL_UPDATE = '/api/v1/a';
const URL_CREATE = '/api/v1/a';
......@@ -22,6 +23,7 @@ const URL_ADD_MATERIAL = '/api/v1/a/acquire';
const URL_DETAILS = UrlTemplate.parse('/api/v1/a/details/{id}');
const URL_OVERVIEW = UrlTemplate.parse('/api/v1/a/overview/{groupBy}');
const URL_LIST_ACTIONS = '/api/v1/a/action/list';
const URL_ACCESSION_AUDIT_LOGS = UrlTemplate.parse('/api/v1/a/auditlog/{id}');
/**
* Accession service
......@@ -246,6 +248,33 @@ class AccessionService {
});
return data as FilteredPage<AccessionAction>;
});
};
/**
* accessionAuditLogs at /api/v1/a/auditlog/{id}
*
* @param id undefined
* @param page the page
* @param xhrConfig additional xhr config
*/
public accessionAuditLogs = (id: number, page: IPageRequest = {}, xhrConfig?: AxiosRequestConfig): Promise<Page<AuditLog>> => {
const qs = QueryString.stringify({
p: page.page || undefined,
l: page.size || undefined,
d: page.direction ? page.direction : undefined,
s: page.properties || undefined,
}, {});
const apiUrl = URL_ACCESSION_AUDIT_LOGS.expand({ id }) + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as Page<AuditLog>);
}
}
......
......@@ -7,12 +7,14 @@ import { dereferenceReferences3 } from '@gringlobal/client/utilities';
import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator';
import { FilteredPage, Page, IPageRequest } from '@gringlobal/client/model/page';
import CooperatorFilter from '@gringlobal/client/model/gringlobal/CooperatorFilter';
import AuditLog from '@gringlobal/client/model/gringlobal/AuditLog';
const URL_UPDATE_COOPERATOR = '/api/v1/cooperator';
const URL_CREATE_COOPERATOR = '/api/v1/cooperator';
const URL_FILTER = '/api/v1/cooperator/filter';
const URL_LIST = '/api/v1/cooperator/list';
const URL_GET_COOPERATOR = UrlTemplate.parse('/api/v1/cooperator/{id}');
const URL_COOPERATOR_AUDIT_LOGS = UrlTemplate.parse('/api/v1/cooperator/auditlog/{id}');
/**
* Cooperator service
......@@ -29,7 +31,7 @@ class CooperatorService {
/**
* filter at /api/v1/cooperator/filter
*
* @param f undefined
* @param filter undefined
* @param page undefined
* @param xhrConfig additional xhr config
*/
......@@ -65,7 +67,7 @@ class CooperatorService {
/**
* list at /api/v1/cooperator/list
*
* @param f undefined
* @param filter undefined
* @param page undefined
* @param xhrConfig additional xhr config
*/
......@@ -155,6 +157,33 @@ class CooperatorService {
...content,
}).then(({ data }) => data as Cooperator);
};
/**
* cooperatorAuditLogs at /api/v1/cooperator/auditlog/{id}
*
* @param id undefined
* @param page the page
* @param xhrConfig additional xhr config
*/
public cooperatorAuditLogs = (id: number, page: IPageRequest = {}, xhrConfig?: AxiosRequestConfig): Promise<Page<AuditLog>> => {
const qs = QueryString.stringify({
p: page.page || undefined,
l: page.size || undefined,
d: page.direction ? page.direction : undefined,
s: page.properties || undefined,
}, {});
const apiUrl = URL_COOPERATOR_AUDIT_LOGS.expand({ id }) + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as Page<AuditLog>);
}
}
export default CooperatorService;
......@@ -141,6 +141,8 @@
}
},
"details": {
"auditLogs": "Audit Logs",
"actions": "Actions",
"title": "Accession",
"attachments": "Attachments",
"quarantine": "Accession quarantine",
......@@ -184,6 +186,7 @@
"title": "Cooperators"
},
"details": {
"auditLogs": "Audit Logs",
"title": "Cooperator",
"auditingInformation": "Auditing information",
"organization": "Organization",
......
......@@ -13,6 +13,8 @@
}
},
"details": {
"auditLogs": "Audit Logs",
"actions": "Actions",
"title": "Accession",
"attachments": "Attachments",
"quarantine": "Accession quarantine",
......
......@@ -5,10 +5,8 @@ import { WithTranslation, withTranslation } from 'react-i18next';
// Action
import { ApiCall } from '@gringlobal/client/model/common';
import { getAccessionAction, getAccessionInventories, uploadAccessionAttachment } from 'accession/action/public';
import Tab from '@material-ui/core/Tab';
import HeaderTabs from '@gringlobal/client/ui/common/tabs/HeaderTabs';
import TabPanel from '@gringlobal/client/ui/common/tabs/TabPanel';
// Service
import { AccessionService } from '@gringlobal/client/service';
// Models
import AccessionDetails from '@gringlobal/client/model/gringlobal/AccessionDetails';
import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
......@@ -17,7 +15,7 @@ import AccessionSource from '@gringlobal/client/model/gringlobal/AccessionSource
import AccessionIpr from '@gringlobal/client/model/gringlobal/AccessionIpr';
import AccessionAction from '@gringlobal/client/model/gringlobal/AccessionAction';
import Accession from '@gringlobal/client/model/gringlobal/Accession';
import { FilteredPage } from '@gringlobal/client/model/page';
import { FilteredPage, SortDirection, Page } from '@gringlobal/client/model/page';
// UI
import Loading from '@gringlobal/client/ui/common/Loading';
import { Card, CardContent, CardHeader, CardActions, Button } from '@material-ui/core';
......@@ -33,6 +31,10 @@ import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import FileUploader from '@gringlobal/client/ui/common/file-uploader';
import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay';
import AuditDataDisplay from 'common/AuditDataDisplay';
import { AuditLogsTable } from 'common/AuditLogsTable';
import Tab from '@material-ui/core/Tab';
import HeaderTabs from '@gringlobal/client/ui/common/tabs/HeaderTabs';
import TabPanel from '@gringlobal/client/ui/common/tabs/TabPanel';
const InventoryTableConfig = new TableConfiguration(TableConfiguration.merge(
......@@ -66,6 +68,14 @@ interface IDetailsPageProps extends React.ClassAttributes<any>, WithTranslation
uploadAccessionAttachment: (id: number, file: File) => void;
}
enum AccessionDetailsTabs {
INFO = 'info',
ACTIONS = 'actions',
INVENTORIES = 'inventories',
ATTACHMENTS = 'attachments',
AUDITLOGS = 'auditlogs',
}
class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
protected static needs = [
......@@ -74,7 +84,8 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
];
public state = {
selectedTab: 'info',
selectedTab: AccessionDetailsTabs.INFO,
auditLogs: null,
};
public constructor(props) {
......@@ -101,6 +112,13 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
this.setState({
selectedTab: newValue,
});
if (newValue === AccessionDetailsTabs.AUDITLOGS) {
if (!this.state.auditLogs) {
AccessionService.accessionAuditLogs(+this.props.id)
.then((auditLogs) => this.setState({ auditLogs }));
}
}
}
private handleUploading = (files: File[]) => {
......@@ -111,9 +129,20 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
}
};
private onSortChange = (sortBy: string, dir: SortDirection): void => {
AccessionService.accessionAuditLogs(+this.props.id, { properties: [sortBy], direction: [dir] })
.then((auditLogs) => this.setState({ auditLogs }));
};
private loadMore = (): void => {
const { auditLogs } = this.state;
AccessionService.accessionAuditLogs(+this.props.id, Page.nextPage(auditLogs))
.then((newAuditLogs) => this.setState({ auditLogs: Page.merge(auditLogs, newAuditLogs) }));
};
public render(): React.ReactNode {
const { inventories, accessionCall, t } = this.props;
const { selectedTab } = this.state;
const { selectedTab, auditLogs } = this.state;
const columns = InventoryTableConfig.getColumns(inventories && inventories.data && inventories.data.content ? inventories.data.content[0] : null);
if (!accessionCall) {
return null;
......@@ -132,16 +161,16 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
scrollButtons="auto"
aria-label="Inventory tabs"
>
<Tab value="info" label={ accession ? <>{ accession.accessionNumber }</> : t('accession.public.p.details.title') } />
<Tab value="inventories" label="Inventories" />
<Tab value="attachments" label="Attachments" />
<Tab value="actions" label="Actions" />
{/* <Tab value="other" label="Other" /> */}
<Tab value={ AccessionDetailsTabs.INFO } label={ accession ? <>{ accession.accessionNumber }</> : t('accession.public.p.details.title') } />
<Tab value={ AccessionDetailsTabs.INVENTORIES } label={ t('client:model.name.Inventory_plural') } />
<Tab value={ AccessionDetailsTabs.ATTACHMENTS } label={ t('accession.public.p.details.attachments') } />
<Tab value={ AccessionDetailsTabs.ACTIONS } label={ t('accession.public.p.details.actions') } />
<Tab value={ AccessionDetailsTabs.AUDITLOGS } label={ t('accession.public.p.details.auditLogs') } />
</HeaderTabs>
{ loading && <Loading/> }
{ accession &&
<>
<TabPanel value={ selectedTab } index="info">
<TabPanel value={ selectedTab } index={ AccessionDetailsTabs.INFO }>
<>
<Card>
<CardHeader
......@@ -336,7 +365,7 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
</>
</TabPanel>
<TabPanel value={ selectedTab } index="inventories">
<TabPanel value={ selectedTab } index={ AccessionDetailsTabs.INVENTORIES }>
<Table
noWrap
tableKey="accession-inventories-list"
......@@ -350,7 +379,7 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
/>
</TabPanel>
<TabPanel value={ selectedTab } index="attachments">
<TabPanel value={ selectedTab } index={ AccessionDetailsTabs.ATTACHMENTS }>
<Card>
<CardHeader title={ t('accession.public.p.details.attachments') } />
{ accession.attachments && <AttachmentsDisplay attachments={ accession.attachments }/> }
......@@ -361,7 +390,7 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
</TabPanel>
<TabPanel value={ selectedTab } index="actions">
<TabPanel value={ selectedTab } index={ AccessionDetailsTabs.ACTIONS }>
<>
<Card>
{ accession.actions && accession.actions.sort(AccessionAction.completedDateSort).map((action) => (
......@@ -385,8 +414,18 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps> {
</>
</TabPanel>
<TabPanel value={ selectedTab } index="other">
<TabPanel value={ selectedTab } index={ AccessionDetailsTabs.AUDITLOGS }>
<>
{ !auditLogs && <Loading/> }
{ auditLogs && auditLogs.content.length > 0 &&
<AuditLogsTable
auditLogs={ auditLogs.content }
sort={ auditLogs.sort }
total={ auditLogs.totalElements }
onSortChange={ this.onSortChange }
loadMore={ this.loadMore }
/>
}
</>
</TabPanel>
</>
......
......@@ -20,6 +20,7 @@ const InventoryAuditLogsTableConfigProps = {
previousState: { sort: null },
newState: { sort: null },
createdBy: { sort: null },
property: { sort: null },
},
columnsRenderers: {
logDate: Renderers.DATE_RENDERER,
......@@ -29,14 +30,14 @@ const InventoryAuditLogsTableConfigProps = {
const className = row.classPk.classname.split('.').pop(); // split using dots and get the last element of array
const key = 'propertyName';
const propertyName = row[key];
return propertyName ? t(`client:model.${className}.${propertyName}`) : null
return propertyName ? t([`client:model.${className}.${propertyName}`, `client:model._.${propertyName}`]) : null
},
},
};
const InventoryAuditLogsTableConfig = new TableConfiguration(InventoryAuditLogsTableConfigProps as any);
const AuditLogsTableConfig = new TableConfiguration(InventoryAuditLogsTableConfigProps as any);
interface IInventoryAuditLogsTableProps {
interface IAuditLogsTableProps {
auditLogs: AuditLog[];
total: number;
sort: ISort[];
......@@ -44,17 +45,17 @@ interface IInventoryAuditLogsTableProps {
loadMore: () => void;
}
export const InventoryAuditLogsTable = (props: IInventoryAuditLogsTableProps) => {
export const AuditLogsTable = (props: IAuditLogsTableProps) => {
const { auditLogs, total, sort, onSortChange, loadMore } = props;
if (auditLogs && auditLogs.length > 0) {
return (
<Table
noWrap
tableKey="inventory-auditlogs-table"
tableKey="auditlogs-table"
type={ 'AuditLog' }
columns={ InventoryAuditLogsTableConfig.defaultColumns }
columns={ AuditLogsTableConfig.defaultColumns }
data={ auditLogs }
tableConfig={ InventoryAuditLogsTableConfig }
tableConfig={ AuditLogsTableConfig }
sort={ sort }
total={ total }
onSortChange={ onSortChange }
......
......@@ -5,6 +5,7 @@
"title": "Cooperators"
},
"details": {
"auditLogs": "Audit Logs",
"title": "Cooperator",
"auditingInformation": "Auditing information",
"organization": "Organization",
......
......@@ -2,15 +2,17 @@ import * as React from 'react';
import { bindActionCreators, compose } from 'redux';
import { connect } from 'react-redux';
import { WithTranslation, withTranslation } from 'react-i18next';
// Service
import { CooperatorService } from '@gringlobal/client/service';
// Action
import { getCooperatorAction } from 'cooperator/action/public';
import navigateTo from '@gringlobal/client/action/navigation';
// Model
import ApiCall from '@gringlobal/client/model/common/ApiCall';
import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator';
import { SortDirection, Page } from '@gringlobal/client/model/page';
// Ui
import { Link } from 'react-router-dom';
import ContentHeader from '@gringlobal/client/ui/common/heading/ContentHeader';
import { Card, CardContent, CardHeader, CardActions, Button } from '@material-ui/core';
import ButtonBar from '@gringlobal/client/ui/common/button/ButtonBar';
import { Properties, PropertiesItem } from '@gringlobal/client/ui/common/Properties';
......@@ -19,6 +21,10 @@ import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import Email from '@gringlobal/client/ui/common/Email';
import { CodeValueDisplay } from 'common/CodeValue';
import AuditDataDisplay from 'common/AuditDataDisplay';
import Tab from '@material-ui/core/Tab/Tab';
import HeaderTabs from '@gringlobal/client/ui/common/tabs/HeaderTabs';
import TabPanel from '@gringlobal/client/ui/common/tabs/TabPanel';
import { AuditLogsTable } from 'common/AuditLogsTable';
interface ICooperatorDetailsPage extends React.ClassAttributes<any>, WithTranslation {
......@@ -28,12 +34,22 @@ interface ICooperatorDetailsPage extends React.ClassAttributes<any>, WithTransla
navigateTo: (path: string, query?: object) => void;
}
enum CooperatorDetailsTabs {
INFO = 'info',
AUDITLOGS = 'auditlogs',
}
class CooperatorDetailsPage extends React.Component<ICooperatorDetailsPage> {
protected static needs = [
({ params: { id } }) => getCooperatorAction(id),
];
public state = {
selectedTab: CooperatorDetailsTabs.INFO,
auditLogs: null,
};
public componentDidMount(): void {
const { id, cooperatorCall, getCooperatorAction } = this.props;
......@@ -54,135 +70,189 @@ class CooperatorDetailsPage extends React.Component<ICooperatorDetailsPage> {
navigateTo(`/cooperator/edit/${id}`);
};
private selectTab = (event, newValue) => {
this.setState({
selectedTab: newValue,
});
if (newValue === CooperatorDetailsTabs.AUDITLOGS) {
if (!this.state.auditLogs) {
CooperatorService.cooperatorAuditLogs(+this.props.id)
.then((auditLogs) => this.setState({ auditLogs }));
}
}
};
private onSortChange = (sortBy: string, dir: SortDirection): void => {
CooperatorService.cooperatorAuditLogs(+this.props.id, { properties: [sortBy], direction: [dir] })
.then((auditLogs) => this.setState({ auditLogs }));
};
private loadMore = (): void => {
const { auditLogs } = this.state;
CooperatorService.cooperatorAuditLogs(+this.props.id, Page.nextPage(auditLogs))
.then((newAuditLogs) => this.setState({ auditLogs: Page.merge(auditLogs, newAuditLogs) }));
};
public render() {
const { cooperatorCall, t } = this.props;
if (! cooperatorCall) {
return null;
}
const { loading, data: cooperator } = cooperatorCall;
const { selectedTab, auditLogs } = this.state;
return (
<>
<PageTitle title={ t('cooperator.public.p.details.title') }/>
<ContentHeader title={ t('cooperator.public.p.details.title') }/>
<HeaderTabs
value={ selectedTab }
textColor="primary"
onChange={ this.selectTab }
variant="scrollable"
scrollButtons="auto"
aria-label="Cooperator tabs"
>
<Tab value={ CooperatorDetailsTabs.INFO } label={ t('cooperator.public.p.details.title') } />
<Tab value={ CooperatorDetailsTabs.AUDITLOGS } label={ t('cooperator.public.p.details.auditLogs') } />
</HeaderTabs>
{ loading && <Loading /> }
{ cooperator && (
<div key={ cooperator.id }>
<Card>
<CardActions>
<ButtonBar>
<Button variant="contained" color="secondary" onClick={ this.handleEdit }>Edit</Button>
</ButtonBar>
</CardActions>
<CardContent>
<Properties>
{ (cooperator.currentCooperator && cooperator.currentCooperator !== cooperator.id) &&
<PropertiesItem title={ t('client:model.Cooperator.currentCooperator') }>
<Link to={ `/cooperator/${cooperator.currentCooperator}` }>See</Link>
</PropertiesItem>
}
{ [ 'statusCode', 'categoryCode', 'disciplineCode', 'title' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] && <CodeValueDisplay codeGroup={ Cooperator.CodeValues[property] } value={ cooperator[property] } /> }
</PropertiesItem>
)) }
{ [ 'firstName', 'lastName', 'job'].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }
</PropertiesItem>
)) }
{ [ 'email', 'secondaryEmail' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] && <Email value={ cooperator[property] }/> }
</PropertiesItem>
)) }
{ [ 'primaryPhone', 'secondaryPhone', 'note' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }
</PropertiesItem>
)) }
</Properties>
</CardContent>
</Card>
<Card>
<CardHeader title={ t('cooperator.public.p.details.organization') } />
<CardContent>
<Properties>
{ [ 'organization', 'organizationAbbrev' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }
</PropertiesItem>
)) }
<PropertiesItem key={ 'organizationRegionCode' } title={ t(['client:model.Cooperator.organizationRegionCode', 'client:model._.organizationRegionCode']) }>
{ cooperator.organizationRegionCode && <CodeValueDisplay codeGroup={ Cooperator.CodeValues.organizationRegionCode } value={ cooperator.organizationRegionCode } /> }
</PropertiesItem>
</Properties>
</CardContent>
</Card>
<Card>
<CardHeader title={ t('cooperator.public.p.details.address') } />
<CardContent>
<Properties>
{ [ 'addressLine1', 'addressLine2', 'addressLine3', 'city' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }
</PropertiesItem>
)) }
{ cooperator.geography &&
<PropertiesItem title={ t(['client:model.Cooperator.geography','client:model._.geography']) }>
{ cooperator.geography.adm1 }{ cooperator.geography.adm1 && ', ' }{ cooperator.geography.countryCode }
</PropertiesItem>
}
</Properties>
</CardContent>
</Card>
{ (cooperator.secondaryOrganization || cooperator.secondaryOrganizationAbbrev) &&
<Card>
<CardHeader title={ t('cooperator.public.p.details.secondaryOrganization') } />
<CardContent>
<Properties>
{ [ 'secondaryOrganization', 'secondaryOrganizationAbbrev' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }
<>
<TabPanel value={ selectedTab } index={ CooperatorDetailsTabs.INFO }>
<div key={ cooperator.id }>
<Card>
<CardActions>
<ButtonBar>
<Button variant="contained" color="secondary" onClick={ this.handleEdit }>Edit</Button>
</ButtonBar>
</CardActions>
<CardContent>
<Properties>
{ (cooperator.currentCooperator && cooperator.currentCooperator !== cooperator.id) &&
<PropertiesItem title={ t('client:model.Cooperator.currentCooperator') }>
<Link to={ `/cooperator/${cooperator.currentCooperator}` }>See</Link>
</PropertiesItem>
}
{ [ 'statusCode', 'categoryCode', 'disciplineCode', 'title' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] && <CodeValueDisplay codeGroup={ Cooperator.CodeValues[property] } value={ cooperator[property] } /> }
</PropertiesItem>
)) }
{ [ 'firstName', 'lastName', 'job'].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }
</PropertiesItem>
)) }
{ [ 'email', 'secondaryEmail' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] && <Email value={ cooperator[property] }/> }
</PropertiesItem>
)) }
{ [ 'primaryPhone', 'secondaryPhone', 'note' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }
</PropertiesItem>
)) }
</Properties>
</CardContent>
</Card>
<Card>
<CardHeader title={ t('cooperator.public.p.details.organization') } />
<CardContent>
<Properties>
{ [ 'organization', 'organizationAbbrev' ].map((property) => (
<PropertiesItem key={ property } title={ t([`client:model.Cooperator.${property}`, `client:model._.${property}`]) }>
{ cooperator[property] }