Skip to content

Commit

Permalink
Merge pull request #3150 from carlobeltrame/dashboard
Browse files Browse the repository at this point in the history
New dashboard
  • Loading branch information
carlobeltrame committed Nov 11, 2022
2 parents e79e835 + 8cb5b01 commit a83df19
Show file tree
Hide file tree
Showing 15 changed files with 732 additions and 170 deletions.
2 changes: 1 addition & 1 deletion api/src/Entity/Camp.php
Expand Up @@ -92,7 +92,7 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy
* All the programme that will be carried out during the camp. An activity may be carried out
* multiple times in the same camp.
*/
#[ApiProperty(writable: false, example: '["/activities/1a2b3c4d"]')]
#[ApiProperty(writable: false, example: '/activities?camp=%2Fcamps%2F1a2b3c4d')]
#[Groups(['read'])]
#[ORM\OneToMany(targetEntity: Activity::class, mappedBy: 'camp', orphanRemoval: true)]
public Collection $activities;
Expand Down
23 changes: 22 additions & 1 deletion common/helpers/dateHelperUTCFormatted.js
Expand Up @@ -13,6 +13,27 @@ function hourShort(dateTimeString) {
return dayjs.utc(dateTimeString).format(i18n.tc('global.datetime.hourShort'))
}

function timeDurationShort(start, end) {
const startTime = dayjs.utc(start)
const endTime = dayjs.utc(end)
const duration = dayjs(endTime.diff(startTime))
const durationInMinutes = duration.valueOf() / 1000 / 60
if (durationInMinutes < 60) {
return i18n.tc('global.datetime.duration.minutesOnly', 0, {
minutes: durationInMinutes,
})
}
if (durationInMinutes % 60 === 0) {
return i18n.tc('global.datetime.duration.hoursOnly', 0, {
hours: durationInMinutes / 60,
})
}
return i18n.tc('global.datetime.duration.hoursAndMinutes', 0, {
hours: Math.floor(durationInMinutes / 60.0),
minutes: durationInMinutes % 60,
})
}

// short format of dateTime range
// doesn't repeat end date if on the same day
function rangeShort(start, end) {
Expand Down Expand Up @@ -42,4 +63,4 @@ function dateRange(start, end) {
return `${dateShort(start)} - ${dateLong(end)}`
}

export { dateShort, dateLong, hourShort, dateRange, rangeShort }
export { dateShort, dateLong, timeDurationShort, hourShort, dateRange, rangeShort }
5 changes: 5 additions & 0 deletions common/locales/de.json
Expand Up @@ -165,6 +165,11 @@
"dateLong": "dd L",
"dateShort": "dd D.M.",
"dateTimeLong": "dd L HH:mm",
"duration": {
"hoursAndMinutes": "{hours}h {minutes}min",
"hoursOnly": "{hours}h",
"minutesOnly": "{minutes}min"
},
"hourLong": "HH:mm",
"hourShort": "H:mm"
}
Expand Down
5 changes: 5 additions & 0 deletions common/locales/en.json
Expand Up @@ -172,6 +172,11 @@
"dateLong": "dd L",
"dateShort": "dd M/D",
"dateTimeLong": "dd L HH:mm",
"duration": {
"hoursAndMinutes": "{hours}h {minutes}m",
"hoursOnly": "{hours}h",
"minutesOnly": "{minutes}m"
},
"hourLong": "HH:mm",
"hourShort": "h:mm A"
}
Expand Down
128 changes: 128 additions & 0 deletions frontend/src/components/dashboard/ActivityRow.vue
@@ -0,0 +1,128 @@
<template>
<tr class="row">
<th style="text-align: left" class="tabular-nums" scope="row">
<TextAlignBaseline
><span class="smaller">{{ scheduleEntry.number }}</span></TextAlignBaseline
>
<br />
<CategoryChip small dense :category="category" class="d-sm-none" />
</th>
<td class="d-none d-sm-table-cell">
<CategoryChip small dense :category="category" />
</td>
<td class="nowrap">
{{ start }}<br />
<span class="e-subtitle">{{ duration }}</span>
</td>
<td style="width: 100%" class="contentrow">
<router-link
:to="routerLink"
class="text-decoration-none text-decoration-hover-underline black--text"
>
{{ title }}<br />
</router-link>
<span class="e-subtitle">{{ location }}</span>
</td>
<td class="contentrow avatarrow overflow-visible">
<AvatarRow :camp-collaborations="collaborators" size="28" class="ml-auto" />
</td>
</tr>
</template>

<script>
import AvatarRow from './AvatarRow.vue'
import CategoryChip from '@/components/generic/CategoryChip.vue'
import {
hourShort,
timeDurationShort,
} from '../../common/helpers/dateHelperUTCFormatted.js'
import TextAlignBaseline from '@/components/layout/TextAlignBaseline.vue'
export default {
name: 'ActivityRow',
components: { CategoryChip, AvatarRow, TextAlignBaseline },
props: {
scheduleEntry: { type: Object, required: true },
},
computed: {
collaborators() {
return this.scheduleEntry
.activity()
.activityResponsibles()
.items.map((responsible) => responsible.campCollaboration())
},
category() {
return this.scheduleEntry.activity().category()
},
title() {
return this.scheduleEntry.activity().title
},
location() {
return this.scheduleEntry.activity().location
},
start() {
return hourShort(this.scheduleEntry.start)
},
duration() {
return timeDurationShort(this.scheduleEntry.start, this.scheduleEntry.end)
},
routerLink() {
return {
name: 'activity',
params: {
campId: this.scheduleEntry.period().camp().id,
scheduleEntryId: this.scheduleEntry.id,
},
}
},
},
}
</script>

<style scoped>
.row {
display: table-row;
vertical-align: baseline;
}
tr + tr :is(td, th) {
border-top: 1px solid #ddd;
}
:is(td, th) {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
:is(td, th) + :is(td, th) {
padding-left: 0.5rem;
}
.contentrow {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.avatarrow {
vertical-align: middle;
}
.e-subtitle {
font-size: 0.9em;
color: #666;
}
.nowrap {
white-space: nowrap;
}
.smaller {
font-size: 0.75em;
}
.text-decoration-hover-underline:hover {
text-decoration: underline !important;
}
</style>
66 changes: 66 additions & 0 deletions frontend/src/components/dashboard/AvatarRow.vue
@@ -0,0 +1,66 @@
<template>
<div class="avatarrow" :style="avatarrow">
<div
v-for="campCollaboration in campCollaborations"
:key="campCollaboration && campCollaboration._meta.self"
class="avataritem"
>
<UserAvatar :size="Number(size)" :camp-collaboration="campCollaboration" />
</div>
</div>
</template>

<script>
import UserAvatar from '@/components/user/UserAvatar.vue'
export default {
name: 'AvatarRow',
components: { UserAvatar },
props: {
campCollaborations: { type: Array, default: () => [] },
size: { type: [Number, String], default: 20 },
},
computed: {
maxWidth() {
return (
(this.campCollaborations?.length - 1) * (Number(this.size) * 0.25) +
Number(this.size)
)
},
avatarrow() {
return {
'max-width': `${this.maxWidth}px`,
'font-size': `${this.size}px`,
}
},
},
}
</script>
<style scoped lang="scss">
.avatarrow {
display: flex;
flex-direction: row-reverse;
gap: 0.75em;
padding-left: 0.5em;
padding-right: 0.5em;
transition: gap 0.25s ease;
}
@media #{map-get($display-breakpoints, 'md-and-up')} {
.avatarrow {
gap: 1.1em;
}
}
.avatarrow:hover {
gap: 1.1em;
}
.avataritem {
display: grid;
width: 0;
place-content: center;
text-decoration: none;
}
</style>
21 changes: 21 additions & 0 deletions frontend/src/components/dashboard/BooleanFilter.vue
@@ -0,0 +1,21 @@
<template>
<v-chip
label
outlined
:color="value ? 'primary' : null"
@click="$emit('input', !value)"
>{{ label }}</v-chip
>
</template>

<script>
export default {
name: 'BooleanFilter',
props: {
value: Boolean,
label: { type: String, required: true },
},
}
</script>

<style scoped></style>
16 changes: 16 additions & 0 deletions frontend/src/components/dashboard/FilterDivider.vue
@@ -0,0 +1,16 @@
<template>
<span>|</span>
</template>

<script>
export default {
name: 'FilterDivider',
}
</script>

<style scoped>
span {
color: rgba(0, 0, 0, 0.12);
align-self: center;
}
</style>

0 comments on commit a83df19

Please sign in to comment.