- added page managebandi;

- improved styles of the pages and sections;
This commit is contained in:
Vitalii Kiiko
2024-08-14 08:28:54 +02:00
parent c09127a675
commit 76b5dd2ece
28 changed files with 945 additions and 56 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,10 @@
.appForm {
display: flex;
flex-direction: column;
gap: 24px;
}
.appForm__field {
display: flex;
flex-direction: column;
gap: 14px;
}

View File

@@ -1,22 +0,0 @@
.appPage {
display: flex;
flex-direction: column;
h1 {
color: var(--primary-color);
font-size: 28px;
font-style: normal;
font-weight: 600;
line-height: normal;
margin: 28px 0;
}
h2 {
color: var(--Black);
font-size: 21px;
font-style: normal;
font-weight: 600;
line-height: normal;
margin: 24px 0;
}
}

View File

@@ -0,0 +1,67 @@
.appPage {
display: flex;
flex-direction: column;
h1 {
color: var(--primary-color);
font-size: 28px;
font-style: normal;
font-weight: 600;
line-height: normal;
}
}
.appPageSection {
display: flex;
flex-direction: column;
h2 {
color: var(--Black);
font-size: 21px;
font-style: normal;
font-weight: 600;
line-height: normal;
margin: 0 0 24px;
}
}
.appTableHeader {
display: flex;
justify-content: space-between;
}
.appPage__pageHeader {
display: flex;
flex-direction: column;
gap: 14px;
padding: 28px;
h1, h2, h3 {
margin: 0;
}
p, span {
color: var(--text-color-secondary);
font-size: 17.5px;
font-style: normal;
font-weight: 400;
line-height: 120%;
margin: 0;
}
span {
margin-left: 10px;
text-transform: uppercase;
}
}
.appPage__spacer {
width: 100%;
padding: 24px 0;
}
.appPageSection__actions {
display: flex;
gap: 24px;
margin-bottom: 24px;
}

View File

@@ -0,0 +1,7 @@
.bandoStatusTag {
text-transform: uppercase;
font-size: 10.5px;
font-style: normal;
font-weight: 600;
line-height: 150%;
}

View File

@@ -1,7 +1,4 @@
.statsBigBadges {
display: flex;
flex-direction: column;
padding: 24px 0;
}
.statsBigBadges__grid {
display: grid;

View File

@@ -0,0 +1,44 @@
/* montserrat-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: url('../../fonts/montserrat-v26-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* montserrat-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Montserrat';
font-style: italic;
font-weight: 400;
src: url('../../fonts/montserrat-v26-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* montserrat-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
src: url('../../fonts/montserrat-v26-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* montserrat-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
src: url('../../fonts/montserrat-v26-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* montserrat-800 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Montserrat';
font-style: normal;
font-weight: 800;
src: url('../../fonts/montserrat-v26-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

View File

@@ -1,6 +1,6 @@
$primaryColor: #4A644E;
$primaryDarkColor: #4A644E;
$primaryDarkerColor: #4A644E;
$primaryColor: #3B7C43;
$primaryDarkColor: #3B7C43;
$primaryDarkerColor: #3B7C43;
$primaryTextColor: #ffffff;
$chipBg: $primaryColor;
@@ -10,6 +10,7 @@ $chipFocusBg: $primaryColor;
$chipFocusTextColor: $primaryTextColor;
$colors: (
"grey": #6B7280,
"blue": #2196F3,
"green": #4caf50,
"yellow": #FBC02D,

View File

@@ -147,13 +147,16 @@ $successButtonActiveBorderColor: #388E3C;
$successButtonFocusShadow: 0 0 0 0.2rem lighten($successButtonBg, 35%);
$warningButtonBg: #FFC107;
$warningButtonBg: #F97316;
$closedButtonBg: #6B7280;
$warningButtonTextColor: $textColor;
$warningButtonBorder: 1px solid #FFC107;
$warningButtonBorder: 1px solid #F97316;
$warningButtonHoverBg: #FFB300;

View File

@@ -67,7 +67,7 @@
&.p-tag-warning {
background-color: $warningButtonBg;
color: $warningButtonTextColor;
color: $dangerButtonTextColor;
}
&.p-tag-danger {

View File

@@ -20,7 +20,7 @@
&.p-tag-warning {
background-color: $warningButtonBg;
color: $warningButtonTextColor;
color: $dangerButtonTextColor;
}
&.p-tag-danger {
@@ -28,6 +28,11 @@
color: $dangerButtonTextColor;
}
&.p-tag-closed {
background-color: $closedButtonBg;
color: $dangerButtonTextColor;
}
.p-tag-icon {
margin-right: math.div($inlineSpacing, 2);
font-size: $badgeFontSize;

View File

@@ -16,22 +16,38 @@
--card-full-background-color-1: #3B7C43;
}
body {
margin: 0;
}
.wrapper {
html {
min-height: 100%;
display: flex;
flex-direction: column;
height: 100vh;
min-height: 0;
}
body {
min-height: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
margin: 0;
font-family: 'Montserrat';
p, span, input, label, textarea, a, li, h1, h2, h3, h4, h5, h6, div, th, td, button {
font-family: 'Montserrat';
}
}
#root, .wrapper {
min-height: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.inner {
display: flex;
height: 100%;
min-height: 0;
flex-grow: 1;
> sidebar {
> aside {
flex: 0 0 320px;
border-right: 1px solid var(--menu-borderColor);
@@ -42,11 +58,13 @@ body {
margin: 0;
padding: 0;
li a {
li a, li button {
display: flex;
padding: 10.5px 17.5px;
align-items: center;
gap: 7px;
width: 100%;
border: 0;
border-bottom: 1px solid var(--menu-borderColor);
color: var(--menuitem-color);
background-color: transparent;
@@ -56,6 +74,10 @@ body {
line-height: 100%;
text-decoration: none;
&:hover {
cursor: pointer;
}
i {
color: var(--text-color-secondary);
}
@@ -75,10 +97,12 @@ body {
> main {
flex: 1 1 auto;
box-sizing: border-box;
padding: 0 24px;
padding: 0 24px 50px;
}
}
@import "./components/topBar.css";
@import "./components/appPage.css";
@import "./components/statsBigBadges.css";
@import "./components/topBar.scss";
@import "./components/appPage.scss";
@import "./components/statsBigBadges.scss";
@import "./components/bandoStatusTag.scss";
@import "./components/appForm.scss";

View File

@@ -0,0 +1,16 @@
import React from 'react';
// tools
import getBandoLabel from '../../helpers/getBandoLabel';
import getBandoSeverity from '../../helpers/getBandoSeverity';
// components
import { Tag } from 'primereact/tag';
const ProperBandoLabel = ({ status }) => {
return(
<Tag className="bandoStatusTag" value={getBandoLabel(status)} severity={getBandoSeverity(status)} />
)
}
export default ProperBandoLabel;

View File

@@ -0,0 +1,19 @@
import { __ } from '@wordpress/i18n';
const getBandoLabel = (status) => {
switch (status) {
case 'publish':
return __('Pubblicato', 'gepafin');
case 'draft':
return __('Bozza', 'gepafin');
case 'closed':
return __('Chiuso', 'gepafin');
default:
return '';
}
};
export default getBandoLabel;

View File

@@ -0,0 +1,17 @@
const getBandoSeverity = (status) => {
switch (status) {
case 'publish':
return 'success';
case 'draft':
return 'warning';
case 'closed':
return 'closed';
default:
return 'info';
}
};
export default getBandoSeverity;

View File

@@ -2,37 +2,38 @@ import React from 'react';
import { __ } from '@wordpress/i18n';
// components
import { NavLink } from 'react-router-dom';
const AppSidebar = () => {
const items = [
{
label: __('Riepilogo', 'gepafin'),
icon: 'pi pi-objects-column',
clickFn: () => {},
href: '/',
id: 1
},
{
label: __('Gestione Bandi', 'gepafin'),
icon: 'pi pi-file',
clickFn: () => {},
href: '/bandi',
id: 2
},
{
label: __('Gestione Utenti', 'gepafin'),
icon: 'pi pi-users',
clickFn: () => {},
href: '/utenti',
id: 3
},
{
label: __('Configurazione', 'gepafin'),
icon: 'pi pi-cog',
clickFn: () => {},
href: '/configurazione',
id: 4
},
{
label: __('Report e Analisi', 'gepafin'),
icon: 'pi pi-chart-bar',
clickFn: () => {},
href: '/stats',
id: 5
},
{
@@ -43,16 +44,21 @@ const AppSidebar = () => {
}
]
return <sidebar>
return <aside>
<ul>
{items.map(o => <li key={o.id}>
<a href="#" onClick={o.clickFn} className={o.id === 1 ? 'active' : ''}>
{o.href
? <NavLink to={o.href}>
<i className={o.icon}></i>
<span>{o.label}</span>
</a>
</NavLink>
: <button onClick={() => {}}>
<i className={o.icon}></i>
<span>{o.label}</span>
</button>}
</li>)}
</ul>
</sidebar>
</aside>
}
export default AppSidebar;

View File

@@ -0,0 +1,175 @@
import React, { useState, useEffect} from 'react';
import { __ } from '@wordpress/i18n';
import { uniq } from 'ramda';
// tools
import getBandoSeverity from '../../../../helpers/getBandoSeverity';
import getBandoLabel from '../../../../helpers/getBandoLabel';
// components
import { FilterMatchMode, FilterOperator } from 'primereact/api';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
import { Dropdown } from 'primereact/dropdown';
import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar';
import { Tag } from 'primereact/tag';
import ProperBandoLabel from '../../../../components/ProperBandoLabel';
import { Link } from 'react-router-dom';
const AllBandiTable = () => {
const [items, setItems] = useState(null);
const [filters, setFilters] = useState(null);
const [loading, setLoading] = useState(false);
const [globalFilterValue, setGlobalFilterValue] = useState('');
const [statuses, setStatuses] = useState([]);
useEffect(() => {
// TODO
const items = [
{
name: 'Bando Innovazione 2024',
start_date: '2024-08-08T00:00:00+00:00',
end_date: '2024-08-30T00:00:00+00:00',
submissions: 24,
status: 'publish',
id: 11
},
{
name: 'Bando Sostenibilità 2024',
start_date: '2024-07-28T00:00:00+00:00',
end_date: '2024-08-15T00:00:00+00:00',
submissions: 35,
status: 'draft',
id: 9
},
{
name: 'Bando A',
start_date: '2024-06-28T00:00:00+00:00',
end_date: '2024-06-15T00:00:00+00:00',
submissions: 2,
status: 'closed',
id: 2
}
]
setItems(getFormattedBandiData(items));
setStatuses(uniq(items.map(o => o.status)))
setLoading(false);
initFilters();
}, []);
const getFormattedBandiData = (data) => {
return [...(data || [])].map((d) => {
d.start_date = new Date(d.start_date);
d.end_date = new Date(d.end_date);
return d;
});
};
const formatDate = (value) => {
return value.toLocaleDateString('it-IT', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const clearFilter = () => {
initFilters();
};
const onGlobalFilterChange = (e) => {
const value = e.target.value;
let _filters = { ...filters };
_filters['global'].value = value;
setFilters(_filters);
setGlobalFilterValue(value);
};
const initFilters = () => {
setFilters({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
start_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
end_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
status: { operator: FilterOperator.OR, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
});
setGlobalFilterValue('');
};
const renderHeader = () => {
return (
<div className="appTableHeader">
<Button type="button" icon="pi pi-filter-slash" label={__('Pulisci', 'gepafin')} outlined onClick={clearFilter} />
<IconField iconPosition="left">
<InputIcon className="pi pi-search" />
<InputText value={globalFilterValue} onChange={onGlobalFilterChange} placeholder={__('Cerca', 'gepafin')} />
</IconField>
</div>
);
};
const dateStartBodyTemplate = (rowData) => {
return formatDate(rowData.start_date);
};
const dateEndBodyTemplate = (rowData) => {
return formatDate(rowData.end_date);
};
const dateFilterTemplate = (options) => {
return <Calendar value={options.value} onChange={(e) => options.filterCallback(e.value, options.index)} dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" mask="99/99/9999" />;
};
const statusBodyTemplate = (rowData) => {
return <ProperBandoLabel status={rowData.status}/>;
};
const statusFilterTemplate = (options) => {
return <Dropdown value={options.value} options={statuses} onChange={(e) => options.filterCallback(e.value, options.index)} itemTemplate={statusItemTemplate} placeholder="Select One" className="p-column-filter" showClear />;
};
const statusItemTemplate = (option) => {
return <Tag value={getBandoLabel(option)} severity={getBandoSeverity(option)} />;
};
const actionsBodyTemplate = (rowData) => {
return <Link to={`/bandi/${rowData.id}`}>
<Button severity="info" label={__('Modifica', 'gepafin')} icon="pi pi-pencil" size="small" iconPos="right" />
</Link>
}
const header = renderHeader();
return(
<div className="latestBandiTable">
<DataTable value={items} paginator showGridlines rows={10} loading={loading} dataKey="id"
filters={filters}
globalFilterFields={['name', 'status']}
header={header}
emptyMessage="Nothing found." onFilter={(e) => setFilters(e.filters)}>
<Column field="name" header={__('Nome Bando', 'gepafin')} filter filterPlaceholder="Search by name"
style={{ minWidth: '12rem' }}/>
<Column header={__('Data Pubblicazione', 'gepafin')} filterField="start_date" dataType="date"
style={{ minWidth: '10rem' }}
body={dateStartBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column header={__('Data Scadenza', 'gepafin')} filterField="end_date" dataType="date"
style={{ minWidth: '10rem' }}
body={dateEndBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column field="status" header={__('Stato', 'gepafin')} filterMenuStyle={{ width: '14rem' }}
style={{ minWidth: '12rem' }} body={statusBodyTemplate} filter
filterElement={statusFilterTemplate}/>
<Column header={__('Azioni', 'gepafin')}
body={actionsBodyTemplate}/>
</DataTable>
</div>
)
}
export default AllBandiTable;

37
src/pages/Bandi/index.js Normal file
View File

@@ -0,0 +1,37 @@
import React from 'react';
import { __ } from '@wordpress/i18n';
import { useNavigate } from 'react-router-dom';
// components
import AllBandiTable from './components/AllBandiTable';
import { Button } from 'primereact/button';
const Bandi = () => {
const navigate = useNavigate();
const onGoToCreateNewBando = () => {
navigate('/bandi/new');
}
return(
<div className="appPage">
<div className="appPage__pageHeader">
<h1>{__('Gestione Bandi', 'gepafin')}</h1>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection">
<div className="appPageSection__actions">
<Button
onClick={onGoToCreateNewBando}
label={__('Crea nuovo bando')} icon="pi pi-plus" iconPos="right"/>
</div>
<AllBandiTable/>
</div>
</div>
)
}
export default Bandi;

115
src/pages/Bando/index.js Normal file
View File

@@ -0,0 +1,115 @@
import React, { useState, useEffect } from 'react';
import { __ } from '@wordpress/i18n';
import { useParams } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { classNames } from 'primereact/utils';
// components
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import getBandoLabel from '../../helpers/getBandoLabel';
import { Button } from 'primereact/button';
const Bando = () => {
const { id } = useParams();
const [data, setData] = useState({});
const {
control,
reset,
handleSubmit,
formState: { errors },
getValues
} = useForm(data);
const onSubmit = data => console.log(data);
console.log(errors, getValues());
const onPublish = () => {
console.log('click onPublish');
}
useEffect(() => {
const parsed = parseInt(id)
const bandoId = !isNaN(parsed) ? parsed : 0;
const data = 0 === bandoId
? {
status: 'draft',
name: null,
description: null
}
: {
name: 'Bando Innovazione 2024',
description: '',
start_date: '2024-08-08T00:00:00+00:00',
end_date: '2024-08-30T00:00:00+00:00',
submissions: 24,
status: 'publish',
id: 11
}
setData(data);
reset();
}, [id]);
return (
<div className="appPage">
<div className="appPage__pageHeader">
<h1>{__('Creazione/Modifica Bando', 'gepafin')}</h1>
<p>
{__('Stato:', 'gepafin')}
<span>{getBandoLabel(data.status)}</span>
</p>
</div>
<div className="appPage__spacer"></div>
{data
? <form className="appForm" onSubmit={handleSubmit(onSubmit)}>
<div className="appForm__field">
<label htmlFor="name" className={classNames({ 'p-error': errors.name })}>
{__('Titolo del Bando', 'gepafin')}*
</label>
<Controller
name="name"
control={control}
defaultValue={data['name']}
rules={{ required: __('È obbligatorio', 'gepafin') }}
render={({ field, fieldState }) => (
<InputText id={field.name}
{...field}
autoFocus
className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/>
</div>
<div className="appForm__field">
<label htmlFor="description" className={classNames({ 'p-error': errors.description })}>
{__('Descrizione', 'gepafin')}*
</label>
<Controller
name="description"
control={control}
defaultValue={data['description']}
rules={{ required: __('È obbligatorio', 'gepafin') }}
render={({ field, fieldState }) => (
<InputTextarea id={field.name}
{...field}
className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/>
</div>
<div className="appPageSection">
<div className="appPageSection__actions">
<Button
type="submit"
label={__('Salva Bozza', 'gepafin')} icon="pi pi-save" iconPos="right"/>
<Button
onClick={onPublish}
label={__('Pubblica', 'gepafin')} icon="pi pi-upload" iconPos="right"/>
</div>
</div>
</form> : null}
</div>
)
}
export default Bando;

View File

@@ -0,0 +1,176 @@
import React, { useState, useEffect} from 'react';
import { __ } from '@wordpress/i18n';
import { uniq } from 'ramda';
// tools
import getBandoLabel from '../../../../helpers/getBandoLabel';
import getBandoSeverity from '../../../../helpers/getBandoSeverity';
// components
import { FilterMatchMode, FilterOperator } from 'primereact/api';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
import { Dropdown } from 'primereact/dropdown';
import { InputNumber } from 'primereact/inputnumber';
import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar';
import { Tag } from 'primereact/tag';
import ProperBandoLabel from '../../../../components/ProperBandoLabel';
const LatestBandiTable = () => {
const [items, setItems] = useState(null);
const [filters, setFilters] = useState(null);
const [loading, setLoading] = useState(false);
const [globalFilterValue, setGlobalFilterValue] = useState('');
const [statuses, setStatuses] = useState([]);
useEffect(() => {
// TODO
const items = [
{
name: 'Bando Innovazione 2024',
start_date: '2024-08-08T00:00:00+00:00',
end_date: '2024-08-30T00:00:00+00:00',
submissions: 24,
status: 'publish',
id: 11
},
{
name: 'Bando Sostenibilità 2024',
start_date: '2024-07-28T00:00:00+00:00',
end_date: '2024-08-15T00:00:00+00:00',
submissions: 35,
status: 'publish',
id: 9
},
{
name: 'Bando A',
start_date: '2024-06-28T00:00:00+00:00',
end_date: '2024-06-15T00:00:00+00:00',
submissions: 2,
status: 'closed',
id: 2
}
]
setItems(getFormattedBandiData(items));
setStatuses(uniq(items.map(o => o.status)))
setLoading(false);
initFilters();
}, []);
const getFormattedBandiData = (data) => {
return [...(data || [])].map((d) => {
d.start_date = new Date(d.start_date);
d.end_date = new Date(d.end_date);
return d;
});
};
const formatDate = (value) => {
return value.toLocaleDateString('it-IT', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const clearFilter = () => {
initFilters();
};
const onGlobalFilterChange = (e) => {
const value = e.target.value;
let _filters = { ...filters };
_filters['global'].value = value;
setFilters(_filters);
setGlobalFilterValue(value);
};
const initFilters = () => {
setFilters({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
start_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
end_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
submissions: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
status: { operator: FilterOperator.OR, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
});
setGlobalFilterValue('');
};
const renderHeader = () => {
return (
<div className="appTableHeader">
<Button type="button" icon="pi pi-filter-slash" label={__('Pulisci', 'gepafin')} outlined onClick={clearFilter} />
<IconField iconPosition="left">
<InputIcon className="pi pi-search" />
<InputText value={globalFilterValue} onChange={onGlobalFilterChange} placeholder={__('Cerca', 'gepafin')} />
</IconField>
</div>
);
};
const dateStartBodyTemplate = (rowData) => {
return formatDate(rowData.start_date);
};
const dateEndBodyTemplate = (rowData) => {
return formatDate(rowData.end_date);
};
const dateFilterTemplate = (options) => {
return <Calendar value={options.value} onChange={(e) => options.filterCallback(e.value, options.index)} dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" mask="99/99/9999" />;
};
const balanceFilterTemplate = (options) => {
return <InputNumber value={options.value} onChange={(e) => options.filterCallback(e.value, options.index)} />;
};
const statusBodyTemplate = (rowData) => {
return <ProperBandoLabel status={rowData.status}/>;
};
const statusFilterTemplate = (options) => {
return <Dropdown value={options.value} options={statuses} onChange={(e) => options.filterCallback(e.value, options.index)} itemTemplate={statusItemTemplate} placeholder="Select One" className="p-column-filter" showClear />;
};
const statusItemTemplate = (option) => {
return <Tag value={getBandoLabel(option)} severity={getBandoSeverity(option)} />;
};
const header = renderHeader();
return(
<div className="latestBandiTable">
<DataTable value={items} paginator showGridlines rows={10} loading={loading} dataKey="id"
filters={filters}
globalFilterFields={['name', 'status']}
header={header}
emptyMessage="Nothing found." onFilter={(e) => setFilters(e.filters)}>
<Column field="name" header={__('Nome Bando', 'gepafin')} filter filterPlaceholder="Search by name"
style={{ minWidth: '12rem' }}/>
<Column header={__('Data Pubblicazione', 'gepafin')} filterField="start_date" dataType="date"
style={{ minWidth: '10rem' }}
body={dateStartBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column header={__('Data Scadenza', 'gepafin')} filterField="end_date" dataType="date"
style={{ minWidth: '10rem' }}
body={dateEndBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column header={__('Domande ricevute', 'gepafin')} filterField="submissions" dataType="numeric"
style={{ minWidth: '10rem' }} field="submissions"
filter filterElement={balanceFilterTemplate}/>
<Column field="status" header={__('Stato', 'gepafin')} filterMenuStyle={{ width: '14rem' }}
style={{ minWidth: '12rem' }} body={statusBodyTemplate} filter
filterElement={statusFilterTemplate}/>
</DataTable>
</div>
)
}
export default LatestBandiTable;

View File

@@ -0,0 +1,123 @@
import React, { useState, useEffect} from 'react';
import { __ } from '@wordpress/i18n';
// components
import { FilterMatchMode, FilterOperator } from 'primereact/api';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar';
const LatestUsersActivityTable = () => {
const [items, setItems] = useState(null);
const [filters, setFilters] = useState(null);
const [loading, setLoading] = useState(false);
const [globalFilterValue, setGlobalFilterValue] = useState('');
useEffect(() => {
// TODO
const bandi = [
{
date: '2024-08-02T00:00:00+14:32',
email: 'mario.rossi@gepafin.it',
action: 'Valutazione Domanda',
details: 'Bando Innovazione 2024 - Domanda #123',
id: 11
},
{
date: '2024-08-01T00:00:00+08:23',
email: 'laura.bianchi@gepafin.it',
action: 'Creazione Bando',
details: 'Nuovo bando "Formazione 2025" in bozza',
id: 9
}
]
setItems(getFormattedBandiData(bandi));
setLoading(false);
initFilters();
}, []);
const getFormattedBandiData = (data) => {
return [...(data || [])].map((d) => {
d.date = new Date(d.date);
return d;
});
};
const formatDate = (value) => {
return value.toLocaleDateString('it-IT', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const clearFilter = () => {
initFilters();
};
const onGlobalFilterChange = (e) => {
const value = e.target.value;
let _filters = { ...filters };
_filters['global'].value = value;
setFilters(_filters);
setGlobalFilterValue(value);
};
const initFilters = () => {
setFilters({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
email: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
});
setGlobalFilterValue('');
};
const renderHeader = () => {
return (
<div className="appTableHeader">
<Button type="button" icon="pi pi-filter-slash" label={__('Pulisci', 'gepafin')} outlined onClick={clearFilter} />
<IconField iconPosition="left">
<InputIcon className="pi pi-search" />
<InputText value={globalFilterValue} onChange={onGlobalFilterChange} placeholder={__('Cerca', 'gepafin')} />
</IconField>
</div>
);
};
const dateBodyTemplate = (rowData) => {
return formatDate(rowData.date);
};
const dateFilterTemplate = (options) => {
return <Calendar value={options.value} onChange={(e) => options.filterCallback(e.value, options.index)} dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" mask="99/99/9999" />;
};
const header = renderHeader();
return(
<div className="latestBandiTable">
<DataTable value={items} paginator showGridlines rows={10} loading={loading} dataKey="id"
filters={filters}
globalFilterFields={['name', 'status']}
header={header}
emptyMessage="Nothing found." onFilter={(e) => setFilters(e.filters)}>
<Column header={__('Timestamp', 'gepafin')} filterField="date" dataType="date"
style={{ minWidth: '10rem' }}
body={dateBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column field="email" header={__('Utente', 'gepafin')} filter filterPlaceholder="Search by email"
style={{ minWidth: '12rem' }}/>
<Column field="action" header={__('Azione', 'gepafin')}/>
<Column field="dettails" header={__('Dettagli', 'gepafin')}/>
</DataTable>
</div>
)
}
export default LatestUsersActivityTable;

View File

@@ -1,12 +1,40 @@
import React from 'react';
import { __ } from '@wordpress/i18n';
import { useNavigate } from 'react-router-dom';
// components
import LatestBandiTable from './components/LatestBandiTable';
import LatestUsersActivityTable from './components/LatestUsersActivityTable';
import { Button } from 'primereact/button';
const Dashboard = () => {
const navigate = useNavigate();
const onGoToCreateNewBando = () => {
navigate('/bandi/new');
}
const onGoToUsers = () => {
console.log('onGoToUsers')
}
const onGoToStats = () => {
console.log('onGoToStats')
}
const onGoToSettings = () => {
console.log('onGoToSettings')
}
return(
<div className="appPage">
<div className="appPage__pageHeader">
<h1>{__('Dashboard', 'gepafin')}</h1>
</div>
<div className="statsBigBadges">
<div className="appPage__spacer"></div>
<div className="appPageSection statsBigBadges">
<h2>{__('Panoramica di Sistema', 'gepafin')}</h2>
<div className="statsBigBadges__grid">
<div className="statsBigBadges__gridItem">
@@ -27,6 +55,43 @@ const Dashboard = () => {
</div>
</div>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection">
<h2>{__('Ultimi Bandi Pubblicati', 'gepafin')}</h2>
<LatestBandiTable/>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection">
<h2>{__('Attività Recenti Utenti', 'gepafin')}</h2>
<LatestUsersActivityTable/>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection">
<h2>{__('Collegamenti rapidi', 'gepafin')}</h2>
<div className="appPageSection__actions">
<Button
onClick={onGoToCreateNewBando}
label={__('Crea nuovo bando', 'gepafin')} icon="pi pi-plus" iconPos="right"/>
<Button
disabled={true}
onClick={onGoToUsers}
label={__('Gestione utenti', 'gepafin')} icon="pi pi-users" iconPos="right"/>
<Button
disabled={true}
onClick={onGoToStats}
label={__('Report mensile', 'gepafin')} icon="pi pi-chart-bar" iconPos="right"/>
<Button
disabled={true}
onClick={onGoToSettings}
label={__('Configurazione', 'gepafin')} icon="pi pi-cog" iconPos="right"/>
</div>
</div>
</div>
)
}

View File

@@ -6,11 +6,15 @@ import Login from './pages/Login';
import ProtectedRoute from './components/ProtectedRoute';
import Dashboard from './pages/Dashboard';
import DefaultLayout from './layouts/DefaultLayout';
import Bandi from './pages/Bandi';
import Bando from './pages/Bando';
const routes = () => (
<Routes>
<Route element={<ProtectedRoute/>}>
<Route path="/" element={<DefaultLayout><Dashboard/></DefaultLayout>}/>
<Route path="/bandi" element={<DefaultLayout><Bandi/></DefaultLayout>}/>
<Route path="/bandi/:id" element={<DefaultLayout><Bando/></DefaultLayout>}/>
</Route>
<Route exact path="/login" element={<Login/>}/>
{/*<Route exact path="/forgot-password" element={<ForgotPassword/>}/>*/}