Compare commits
12
Commits
cf1788ebcd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
026d0cfd56 | ||
|
|
1a14485c57 | ||
|
|
50730a2b09 | ||
|
|
9fc14cffd0 | ||
|
|
e534506894 | ||
|
|
89558cdd1a | ||
|
|
d6b319e6a5 | ||
|
|
f350018eb2 | ||
|
|
65e8899426 | ||
|
|
ec91f6df42 | ||
|
|
f2dd382cf3 | ||
|
|
058466294b |
@@ -25,9 +25,35 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and Push Docker image -- finn-board
|
- name: Build and Push Docker image
|
||||||
uses: docker/build-push-action@675965c0e16f1a0f94ecafff969d8c966f92c17b
|
uses: docker/build-push-action@675965c0e16f1a0f94ecafff969d8c966f92c17b
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: git.joshuacoles.me/joshuacoles/revision-ui:latest
|
tags: git.joshuacoles.me/joshuacoles/revision-ui:latest
|
||||||
|
|
||||||
|
build-arm:
|
||||||
|
runs-on: macos-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to Docker
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: git.joshuacoles.me
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image
|
||||||
|
uses: docker/build-push-action@675965c0e16f1a0f94ecafff969d8c966f92c17b
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: git.joshuacoles.me/joshuacoles/revision-ui:arm
|
||||||
|
|||||||
@@ -40,3 +40,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
# Private env files
|
# Private env files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Caddyfile for local development when testing weird deploys
|
||||||
|
/Caddyfile
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ export type DonutChartData = {
|
|||||||
value: number;
|
value: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export function Donut({data, centerLabel, formatter, hoverSuffix}: {
|
export function Donut({
|
||||||
|
data,
|
||||||
|
centerLabel,
|
||||||
|
formatter,
|
||||||
|
hoverSuffix
|
||||||
|
}: {
|
||||||
data: DonutChartData,
|
data: DonutChartData,
|
||||||
formatter: (value: number) => string,
|
formatter: (value: number) => string,
|
||||||
centerLabel: string,
|
centerLabel: string,
|
||||||
@@ -71,12 +76,13 @@ export function Donut({data, centerLabel, formatter, hoverSuffix}: {
|
|||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
strokeDasharray={`${value * normalisingFactor} ${100 - value * normalisingFactor}`}
|
strokeDasharray={`${value * normalisingFactor} ${100 - value * normalisingFactor}`}
|
||||||
strokeDashoffset={100 - (offset * normalisingFactor) + 25}
|
strokeDashoffset={100 - (offset * normalisingFactor) + 25}
|
||||||
|
style={{outline: 'none'}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
<g className="chart-text">
|
<g className="chart-text" style={{ userSelect: 'none'}}>
|
||||||
<text x="50%" y="50%" className="chart-number">
|
<text x="50%" y="50%" className="chart-number">
|
||||||
{formatter(total)}
|
{formatter(total)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as dFns from "date-fns";
|
||||||
|
import * as R from 'ramda';
|
||||||
|
|
||||||
|
interface DateValue {
|
||||||
|
date: Date;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
data: DateValue[];
|
||||||
|
className?: string;
|
||||||
|
goal: number;
|
||||||
|
penaliseOvertime?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GitHubCalendarHeatmap({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
data,
|
||||||
|
className,
|
||||||
|
goal,
|
||||||
|
penaliseOvertime = false,
|
||||||
|
}: Props) {
|
||||||
|
const numDays = dFns.differenceInDays(endDate, startDate);
|
||||||
|
|
||||||
|
// Calculate the number of weeks in the range
|
||||||
|
const numWeeks = Math.ceil(numDays / 7);
|
||||||
|
|
||||||
|
// Calculate the size of each square in the heatmap and the padding between each square
|
||||||
|
const squareSize = 15;
|
||||||
|
const padding = 2;
|
||||||
|
const weekTotalPadding = 10; // Extra padding for the week total row
|
||||||
|
const totalWidth = numWeeks * (squareSize + padding);
|
||||||
|
const totalHeight = 8 * (squareSize + padding) + weekTotalPadding; // Added an extra row for week total
|
||||||
|
|
||||||
|
// Goal times for the daily and weekly squares
|
||||||
|
const dayGoalTime = goal;
|
||||||
|
const weekGoalTime = 5 * dayGoalTime;
|
||||||
|
|
||||||
|
const getColorForValue = (value: number, goalTime: number, zeroColor: string): string => {
|
||||||
|
const buckets = [
|
||||||
|
{
|
||||||
|
min: -Infinity,
|
||||||
|
max: 0,
|
||||||
|
color: zeroColor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 0,
|
||||||
|
max: 0.40,
|
||||||
|
color: '#d6e685',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 0.40,
|
||||||
|
max: 0.50,
|
||||||
|
color: '#8cc665',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 0.50,
|
||||||
|
max: 0.90,
|
||||||
|
color: '#44a340',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 0.90,
|
||||||
|
// If penalising overtime, the max is 1.125, otherwise it's infinity (ie this is the last bucket)
|
||||||
|
max: penaliseOvertime ? 1.125 : Infinity,
|
||||||
|
color: '#1e6823',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 1.125,
|
||||||
|
max: 1.25,
|
||||||
|
color: '#f59e0b'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 1.25,
|
||||||
|
max: Infinity,
|
||||||
|
color: '#ef4444'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const linearValue = value / goalTime;
|
||||||
|
return R.find(minMax => minMax.min < linearValue && linearValue <= minMax.max, buckets)!.color;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeekTotal = (weekIndex: number): number => {
|
||||||
|
const weekStart = dFns.addWeeks(dFns.startOfWeek(startDate, {weekStartsOn: 1}), weekIndex);
|
||||||
|
|
||||||
|
return data.filter(dv => dFns.isSameWeek(dv.date, weekStart, {weekStartsOn: 1}))
|
||||||
|
.reduce((total, {count}) => total + count, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateValues: DateValue[] = dFns.eachDayOfInterval({
|
||||||
|
start: startDate,
|
||||||
|
end: endDate,
|
||||||
|
}).map((date) => {
|
||||||
|
const foundData = data.find(
|
||||||
|
({date: dataDate}) => dFns.isEqual(dFns.startOfDay(date), dFns.startOfDay(dataDate))
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
count: foundData ? foundData.count : 0,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width="100%" height="100%" viewBox={`0 0 ${totalWidth} ${totalHeight}`} className={className}>
|
||||||
|
{dateValues.map(({
|
||||||
|
date,
|
||||||
|
count
|
||||||
|
}) => {
|
||||||
|
const weekIndex = dFns.differenceInCalendarWeeks(date, startDate, {weekStartsOn: 1});
|
||||||
|
|
||||||
|
const dayOfWeek = (date.getDay() + 6) % 7;
|
||||||
|
const x = weekIndex * (squareSize + padding);
|
||||||
|
const y = dayOfWeek * (squareSize + padding);
|
||||||
|
const color = getColorForValue(count, dayGoalTime, dFns.isWeekend(date) ? '#d4d4d4' : '#eeeeee');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={date.toISOString()}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={squareSize}
|
||||||
|
height={squareSize}
|
||||||
|
fill={color}
|
||||||
|
data-tooltip-id={`calendar-tooltip`}
|
||||||
|
data-tooltip-content={count ? `${dFns.format(date, 'EEE do')}: ${count.toFixed(2)} hours` : `${dFns.format(date, 'EEE do')}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{Array(numWeeks).fill(0)
|
||||||
|
.map((_, weekIndex) => {
|
||||||
|
const x = weekIndex * (squareSize + padding);
|
||||||
|
const y = 7 * (squareSize + padding) + weekTotalPadding; // Position for the week total
|
||||||
|
const count = getWeekTotal(weekIndex);
|
||||||
|
const color = getColorForValue(count, weekGoalTime, '#eeeeee');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={`week-total-${weekIndex}`}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={squareSize}
|
||||||
|
height={squareSize}
|
||||||
|
fill={color}
|
||||||
|
data-tooltip-id={`calendar-tooltip`}
|
||||||
|
data-tooltip-content={count ? `Week total ${count.toFixed(2)} hours` : ``}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GitHubCalendarHeatmap;
|
||||||
@@ -47,6 +47,7 @@ export default function OverviewPage({
|
|||||||
<CalendarOverviewCard
|
<CalendarOverviewCard
|
||||||
data={data}
|
data={data}
|
||||||
goal={config.goalHours}
|
goal={config.goalHours}
|
||||||
|
penaliseOvertime={config.penaliseOvertime}
|
||||||
startTime={config.timePeriod.start}
|
startTime={config.timePeriod.start}
|
||||||
endTime={config.timePeriod.end}
|
endTime={config.timePeriod.end}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
"use client";
|
|
||||||
import {Card, Title} from "@tremor/react";
|
import {Card, Title} from "@tremor/react";
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import * as dFns from 'date-fns';
|
import * as dFns from 'date-fns';
|
||||||
import CalendarHeatmap from 'react-calendar-heatmap';
|
|
||||||
import 'react-calendar-heatmap/dist/styles.css';
|
|
||||||
import '../../app/calendar-styles.css'
|
|
||||||
import {Tooltip} from 'react-tooltip';
|
import {Tooltip} from 'react-tooltip';
|
||||||
import {Data} from "@/data/fetchData";
|
import {Data} from "@/data/fetchData";
|
||||||
|
import HeatMap from "@/components/HeatMap";
|
||||||
|
|
||||||
const granularity = 4;
|
function useCalendarData(data: Data) {
|
||||||
|
|
||||||
function computeCompletionShade(value: number, dailyGoal: number) {
|
|
||||||
const linearValue = Math.round((value / dailyGoal) * granularity);
|
|
||||||
|
|
||||||
// If we did something, but not enough to reach the first level, return 1
|
|
||||||
if (linearValue == 0 && value > 0) return 1;
|
|
||||||
|
|
||||||
// Clamp to the granularity
|
|
||||||
if (linearValue > granularity) return granularity;
|
|
||||||
return linearValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
|
|
||||||
const timeEntries = data.timeEntries;
|
const timeEntries = data.timeEntries;
|
||||||
|
|
||||||
// Group by day, sum up seconds
|
// Group by day, sum up seconds
|
||||||
@@ -33,17 +17,6 @@ function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
|
|||||||
return R.sum((entries ?? []).map((entry) => entry.duration))
|
return R.sum((entries ?? []).map((entry) => entry.duration))
|
||||||
}, grouped);
|
}, grouped);
|
||||||
|
|
||||||
// Fill in missing days, hacky
|
|
||||||
dFns.eachDayOfInterval({
|
|
||||||
start: initialDate,
|
|
||||||
end: endDate,
|
|
||||||
}).forEach((date) => {
|
|
||||||
const key = dFns.formatISO(date);
|
|
||||||
if (summed[key] == undefined) {
|
|
||||||
summed[key] = 0;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Object.entries(summed)
|
return Object.entries(summed)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
date: dFns.parseISO(key),
|
date: dFns.parseISO(key),
|
||||||
@@ -54,33 +27,25 @@ function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
|
|||||||
export function CalendarOverviewCard({
|
export function CalendarOverviewCard({
|
||||||
data,
|
data,
|
||||||
goal,
|
goal,
|
||||||
|
penaliseOvertime,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
}: {
|
}: {
|
||||||
data: Data,
|
data: Data,
|
||||||
goal: number,
|
goal: number,
|
||||||
|
penaliseOvertime?: boolean,
|
||||||
startTime: string,
|
startTime: string,
|
||||||
endTime: string,
|
endTime: string,
|
||||||
}) {
|
}) {
|
||||||
const initialDate = dFns.parseISO(startTime);
|
const initialDate = dFns.parseISO(startTime);
|
||||||
const endDate = dFns.parseISO(endTime);
|
const endDate = dFns.parseISO(endTime);
|
||||||
const calendarData = useCalendarData(data, initialDate, endDate);
|
const calendarData = useCalendarData(data);
|
||||||
|
|
||||||
return <Card className="col-span-1">
|
return <Card className="col-span-1">
|
||||||
<Tooltip id="calendar-tooltip"/>
|
<Tooltip id="calendar-tooltip"/>
|
||||||
<Title>Overview</Title>
|
<Title>Semester Overview</Title>
|
||||||
<CalendarHeatmap
|
<div className="m-2">
|
||||||
showWeekdayLabels={true}
|
<HeatMap startDate={initialDate} endDate={endDate} data={calendarData} goal={goal} penaliseOvertime={penaliseOvertime}/>
|
||||||
startDate={initialDate}
|
</div>
|
||||||
endDate={endDate}
|
|
||||||
values={calendarData}
|
|
||||||
classForValue={value => `color-github-${computeCompletionShade(value?.count ?? 0, goal)}`}
|
|
||||||
tooltipDataAttrs={(value: any) => {
|
|
||||||
return value.date ? {
|
|
||||||
'data-tooltip-id': `calendar-tooltip`,
|
|
||||||
'data-tooltip-content': value.count ? `${dFns.format(value.date, 'EEE do')}: ${value.count.toFixed(2)} hours` : `${dFns.format(value.date, 'EEE do')}`
|
|
||||||
} : undefined
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+37
-38
@@ -1,8 +1,8 @@
|
|||||||
import type {ColumnType, JSONColumnType} from "kysely";
|
import type {ColumnType} from "kysely";
|
||||||
|
|
||||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||||
? ColumnType<S, I | undefined, U>
|
? ColumnType<S, I | undefined, U>
|
||||||
: ColumnType<T, T | undefined, T>;
|
: ColumnType<T, T | undefined, T>;
|
||||||
|
|
||||||
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
|
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export type Json = ColumnType<JsonValue, string, string>;
|
|||||||
export type JsonArray = JsonValue[];
|
export type JsonArray = JsonValue[];
|
||||||
|
|
||||||
export type JsonObject = {
|
export type JsonObject = {
|
||||||
[K in string]?: JsonValue;
|
[K in string]?: JsonValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JsonPrimitive = boolean | number | string | null;
|
export type JsonPrimitive = boolean | number | string | null;
|
||||||
@@ -21,50 +21,49 @@ export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
|||||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||||
|
|
||||||
export interface Client {
|
export interface Client {
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
at: Timestamp;
|
at: Timestamp;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
server_deleted_at: Timestamp | null;
|
server_deleted_at: Timestamp | null;
|
||||||
workspace_id: number;
|
workspace_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
client_id: number | null;
|
client_id: number | null;
|
||||||
id: Generated<number>;
|
color: string;
|
||||||
name: string;
|
id: Generated<number>;
|
||||||
raw_json: JSONColumnType<{
|
name: string;
|
||||||
color: string;
|
raw_json: Json;
|
||||||
id: number;
|
server_created_at: Timestamp;
|
||||||
name: string;
|
server_deleted_at: Timestamp | null;
|
||||||
}>;
|
server_updated_at: Timestamp;
|
||||||
toggl_id: Int8;
|
toggl_id: Int8;
|
||||||
workspace_id: Int8;
|
workspace_id: Int8;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeEntry {
|
export interface TimeEntry {
|
||||||
description: string;
|
description: string;
|
||||||
id: Generated<number>;
|
id: Generated<number>;
|
||||||
project_id: Int8 | null;
|
project_id: Int8 | null;
|
||||||
raw_json: JSONColumnType<{
|
raw_json: Json;
|
||||||
start: string;
|
server_deleted_at: Timestamp | null;
|
||||||
end: string;
|
server_updated_at: Timestamp;
|
||||||
seconds: number;
|
start: Timestamp;
|
||||||
}>;
|
stop: Timestamp;
|
||||||
start: Timestamp;
|
tags: Generated<Json>;
|
||||||
stop: Timestamp;
|
toggl_id: Int8;
|
||||||
toggl_id: Int8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TogglPortalSeaqlMigrations {
|
export interface TogglPortalSeaqlMigrations {
|
||||||
applied_at: Int8;
|
applied_at: Int8;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DB {
|
export interface DB {
|
||||||
client: Client;
|
client: Client;
|
||||||
project: Project;
|
project: Project;
|
||||||
time_entry: TimeEntry;
|
time_entry: TimeEntry;
|
||||||
toggl_portal_seaql_migrations: TogglPortalSeaqlMigrations;
|
toggl_portal_seaql_migrations: TogglPortalSeaqlMigrations;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -30,6 +30,7 @@ export interface OverviewConfig {
|
|||||||
}[],
|
}[],
|
||||||
|
|
||||||
goalHours: number,
|
goalHours: number,
|
||||||
|
penaliseOvertime?: boolean,
|
||||||
|
|
||||||
timePeriod: {
|
timePeriod: {
|
||||||
start: string,
|
start: string,
|
||||||
@@ -51,12 +52,12 @@ export async function getData(config: OverviewConfig): Promise<Data> {
|
|||||||
let projectIds = config.subjects.map((subject) => subject.projectId.toString());
|
let projectIds = config.subjects.map((subject) => subject.projectId.toString());
|
||||||
|
|
||||||
const projects = await db.selectFrom('project')
|
const projects = await db.selectFrom('project')
|
||||||
.select('raw_json')
|
.select(['toggl_id', 'name', 'color'])
|
||||||
.where('project.toggl_id', 'in', projectIds)
|
.where('toggl_id', 'in', projectIds)
|
||||||
.execute();
|
.execute()
|
||||||
|
|
||||||
const timeEntries = await db.selectFrom('time_entry')
|
const timeEntries = await db.selectFrom('time_entry')
|
||||||
.select(['project_id', 'raw_json'])
|
.select(['project_id', 'start', 'stop'])
|
||||||
.where('project_id', 'in', projectIds)
|
.where('project_id', 'in', projectIds)
|
||||||
.where('start', '>', dFns.parseISO(config.timePeriod.start))
|
.where('start', '>', dFns.parseISO(config.timePeriod.start))
|
||||||
.where('start', '<', dFns.parseISO(config.timePeriod.end))
|
.where('start', '<', dFns.parseISO(config.timePeriod.end))
|
||||||
@@ -64,16 +65,16 @@ export async function getData(config: OverviewConfig): Promise<Data> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
projects: projects.map((project) => ({
|
projects: projects.map((project) => ({
|
||||||
projectId: project.raw_json.id,
|
projectId: parseInt(project.toggl_id),
|
||||||
name: project.raw_json.name,
|
name: project.name,
|
||||||
color: project.raw_json.color,
|
color: project.color,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
timeEntries: timeEntries.map((timeEntry) => ({
|
timeEntries: timeEntries.map((timeEntry) => ({
|
||||||
projectId: parseInt(timeEntry.project_id!),
|
projectId: parseInt(timeEntry.project_id!),
|
||||||
start: timeEntry.raw_json.start,
|
start: dFns.formatISO(timeEntry.start),
|
||||||
end: timeEntry.raw_json.end,
|
end: dFns.formatISO(timeEntry.stop),
|
||||||
duration: timeEntry.raw_json.seconds,
|
duration: dFns.differenceInSeconds(timeEntry.stop, timeEntry.start)
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -29,7 +29,8 @@ export const semester1Revision: OverviewConfig = {
|
|||||||
export const semester2: OverviewConfig = {
|
export const semester2: OverviewConfig = {
|
||||||
title: 'Semester 2',
|
title: 'Semester 2',
|
||||||
periodKey: 'y5-s2',
|
periodKey: 'y5-s2',
|
||||||
goalHours: 7.5,
|
goalHours: 6,
|
||||||
|
penaliseOvertime: true,
|
||||||
subjects: [
|
subjects: [
|
||||||
{
|
{
|
||||||
projectId: 195754611,
|
projectId: 195754611,
|
||||||
|
|||||||
Reference in New Issue
Block a user