Browse Source

fix date range behavior to be more strongly typed, last 7 days is not an alias for a static date range.

main
Gisle Aune 3 years ago
parent
commit
ba4384b41d
  1. 24896
      svelte-ui/package-lock.json
  2. 2
      svelte-ui/package.json
  3. 2
      svelte-ui/src/components/Composition.svelte
  4. 198
      svelte-ui/src/components/DateRangeSelect.svelte
  5. 2
      svelte-ui/src/components/GoalEntry.svelte
  6. 4
      svelte-ui/src/components/ProgressNumbers.svelte
  7. 25
      svelte-ui/src/forms/GoalForm.svelte
  8. 17
      svelte-ui/src/pages/FrontPage.svelte
  9. 16
      svelte-ui/src/pages/GoalPage.svelte
  10. 24
      svelte-ui/src/pages/LogsPage.svelte
  11. 102
      svelte-ui/src/utils/date-range.ts
  12. 12
      svelte-ui/src/utils/time.ts

24896
svelte-ui/package-lock.json
File diff suppressed because it is too large
View File

2
svelte-ui/package.json

@ -19,12 +19,14 @@
"@tsconfig/svelte": "^1.0.0",
"@types/marked": "^1.2.1",
"@types/node": "^14.14.17",
"@types/pluralize": "0.0.29",
"amazon-cognito-identity-js": "^4.5.6",
"aws-amplify": "^3.3.14",
"fa-svelte": "^3.1.0",
"insane": "^2.6.2",
"lodash-es": "^4.17.20",
"marked": "^2.0.1",
"pluralize": "^8.0.0",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-dev": "^1.1.3",

2
svelte-ui/src/components/Composition.svelte

@ -84,7 +84,7 @@
</script>
<div class="composition">
{#each list as item (item.name)}
{#each list as item}
<div class="item">
<span class="amount">{item.amount}x&nbsp;</span>
<a href={item.link}>{item.name}</a>

198
svelte-ui/src/components/DateRangeSelect.svelte

@ -1,194 +1,54 @@
<script lang="ts">
import { onMount } from "svelte";
import { endOfDay, endOfMonth, endOfWeek, endOfYear, formatFormTime, lastMonth, lastYear, monthName, nextMonth, nextYear, startOfDay, startOfMonth, startOfWeek, startOfYear } from "../utils/time";
import EveryMinute from "./EveryMinute.svelte";
interface DateOption {
id: string
label: string
from: Date
to: Date
}
export let fromValue: string;
export let toValue: string;
import { allOffsets, pastOffsets, customDateRange } from "../utils/date-range";
import type { DateRange } from "../utils/date-range";
import { formatFormTime } from "../utils/time";
export let noFuture: boolean = false;
export let value: DateRange;
export let styled: boolean = false;
export let label: string = "Time";
export let disabled: boolean = false;
export let styled: boolean;
export let noFuture: boolean;
export let label: string = "Time Period";
let selected: string = "custom";
let options: DateOption[] = [];
let now: Date = new Date();
onMount(() => {
for (const option of options) {
console.log(formatFormTime(option.from), fromValue, formatFormTime(option.to), toValue);
if (formatFormTime(option.from) === fromValue && formatFormTime(option.to) === toValue) {
selected = option.id;
console.log("-- SELECTED")
break;
}
}
});
$: {
if (options.length === 0) {
let current = startOfMonth(now);
const nextWeek = new Date(now.getTime() + (86400000 * 7));
const lastWeek = new Date(now.getTime() - (86400000 * 7));
options.push({
id: "this_week",
label: "This Week",
from: startOfWeek(now),
to: endOfWeek(now),
});
if (!noFuture) {
options.push({
id: "next_week",
label: "Next Week",
from: startOfWeek(nextWeek),
to: endOfWeek(nextWeek),
});
options.push({
id: "next_7days",
label: "Next 7 Days",
from: startOfDay(now),
to: endOfDay(new Date(now.getTime() + 86400000 * 7)),
});
}
options.push({
id: "last_week",
label: "Last Week",
from: startOfWeek(lastWeek),
to: endOfWeek(lastWeek),
});
options.push({
id: "last_7days",
label: "Last 7 Days",
from: startOfDay(new Date(now.getTime() - 86400000 * 7)),
to: endOfDay(now),
});
options.push({
id: "this_month",
label: "This Month",
from: current,
to: endOfMonth(current),
});
if (!noFuture) {
options.push({
id: "next_month",
label: "Next Month",
from: nextMonth(current),
to: endOfMonth(nextMonth(current)),
});
options.push({
id: "next_30days",
label: "Next 30 Days",
from: startOfDay(now),
to: endOfDay(new Date(now.getTime() + 86400000 * 30)),
});
options.push({
id: "next_90days",
label: "Next 90 Days",
from: startOfDay(now),
to: endOfDay(new Date(now.getTime() + 86400000 * 90)),
});
}
options.push({
id: "last_month",
label: "Last Month",
from: lastMonth(current),
to: endOfMonth(lastMonth(current)),
});
options.push({
id: "last_30days",
label: "Last 30 Days",
from: startOfDay(new Date(now.getTime() - 86400000 * 30)),
to: endOfDay(now),
});
options.push({
id: "last_90days",
label: "Last 90 Days",
from: startOfDay(new Date(now.getTime() - 86400000 * 90)),
to: endOfDay(now),
});
options.push({
id: "this_year",
label: "This Year",
from: startOfYear(now),
to: endOfYear(now),
});
if (!noFuture) {
options.push({
id: "next_year",
label: "Next Year",
from: startOfYear(nextYear(now)),
to: endOfYear(nextYear(now, 2)),
});
options.push({
id: "next_1year",
label: "Next 1 Year",
from: startOfDay(now),
to: endOfDay(nextYear(now)),
});
}
options.push({
id: "last_year",
label: "Last Year",
from: startOfYear(lastYear(now)),
to: endOfYear(lastYear(now)),
});
options.push({
id: "last_1year",
label: "Last 1 Year",
from: startOfDay(lastYear(now)),
to: endOfDay(now),
});
options.push({
id: "custom",
label: "Specific Dates",
from: null,
to: null,
});
console.log(options);
}
}
let rangeOptions: DateRange[];
let selected = value.name;
let [customMin, customMax] = pastOffsets[0].calculate(new Date()).map(d => formatFormTime(d));
$: rangeOptions = noFuture ? pastOffsets : allOffsets;
$: {
if (selected !== "custom") {
const option = options.find(o => o.id === selected);
if (option != null) {
fromValue = formatFormTime(option.from);
toValue = formatFormTime(option.to);
if (selected === "Specific Dates") {
value = customDateRange(new Date(customMin), new Date(customMax))
} else {
value = rangeOptions.find(v => v.name === selected);
if (value != null) {
let [from, to] = value.calculate(new Date()).map(d => formatFormTime(d));
customMin = from;
customMax = to;
} else {
selected = rangeOptions[0].name;
value = rangeOptions[0];
}
}
}
</script>
<EveryMinute bind:now={now} />
<div class:styled>
<div>
<label for="timeRange">{label}</label>
<select name="timeRange" disabled={disabled} bind:value={selected}>
{#each options as option (option.id)}
<option value={option.id}>{option.label}</option>
{#each rangeOptions as option (option.name)}
<option value={option.name}>{option.name}</option>
{/each}
<option value="Specific Dates">Specific Dates</option>
</select>
</div>
{#if selected === "custom"}
{#if value.name === "Specific Dates"}
<div>
<label for="startTime">Start Time</label>
<input disabled={disabled} name="startTime" type="datetime-local" bind:value={fromValue} />
<input disabled={disabled} name="startTime" type="datetime-local" bind:value={customMin} />
</div>
<div>
<label for="endTime">End Time</label>
<input disabled={disabled} name="endTime" type="datetime-local" bind:value={toValue} />
<input disabled={disabled} name="endTime" type="datetime-local" bind:value={customMax} />
</div>
{/if}
</div>

2
svelte-ui/src/components/GoalEntry.svelte

@ -7,7 +7,7 @@
import OptionRow from "./OptionRow.svelte";
import ParentEntry from "./ParentEntry.svelte";
import Progress from "./Progress.svelte";
import ProgressNumbers from "./ProgressNumbers.svelte";
import ProgressNumbers from "./ProgressNumbers.svelte";
import TimeProgress from "./TimeProgress.svelte";
export let goal: GoalResult = null;

4
svelte-ui/src/components/ProgressNumbers.svelte

@ -2,8 +2,8 @@
import type { ProjectResult } from "../models/project";
import type { GoalResult } from "../models/goal";
export let project: ProjectResult | undefined | null;
export let goal: GoalResult | undefined | null;
export let project: ProjectResult | undefined | null = void(0);
export let goal: GoalResult | undefined | null = void(0);
let progressAmount: number = 0;
let progressTarget: number = 0;

25
svelte-ui/src/forms/GoalForm.svelte

@ -3,14 +3,15 @@
import Modal from "../components/Modal.svelte";
import modalStore from "../stores/modal";
import type { GoalResult } from "../models/goal";
import { formatFormTime, nextMonth } from "../utils/time";
import { nextMonth } from "../utils/time";
import GroupSelect from "../components/GroupSelect.svelte";
import markStale from "../stores/markStale";
import Checkbox from "../components/Checkbox.svelte";
import GroupItemSelect from "../components/GroupItemSelect.svelte";
import groupStore from "../stores/group";
import type { GroupResult } from "../models/group";
import DateRangeSelect from "../components/DateRangeSelect.svelte";
import Checkbox from "../components/Checkbox.svelte";
import GroupItemSelect from "../components/GroupItemSelect.svelte";
import groupStore from "../stores/group";
import type { GroupResult } from "../models/group";
import DateRangeSelect from "../components/DateRangeSelect.svelte";
import { allOffsets, rangeFromDates } from "../utils/date-range";
export let deletion = false;
export let creation = false;
@ -48,8 +49,7 @@ import DateRangeSelect from "../components/DateRangeSelect.svelte";
let unweighted = goal.unweighted;
let itemId = goal.itemId || "";
let compositionMode = goal.compositionMode;
let startTime = formatFormTime(goal.startTime);
let endTime = formatFormTime(goal.endTime);
let range = rangeFromDates(new Date(goal.startTime), new Date(goal.endTime), allOffsets);
let taskFilter = goal.taskFilter || "";
let itemFilter = goal.itemFilter || "";
@ -59,11 +59,11 @@ import DateRangeSelect from "../components/DateRangeSelect.svelte";
function onSubmit() {
loading = true;
const [startTime, endTime] = range.calculate(new Date());
if (creation) {
stuffLogClient.createGoal({
startTime: new Date(startTime),
endTime: new Date(endTime),
startTime, endTime,
itemId: itemId || null,
taskFilter: taskFilter.toLowerCase() || null,
itemFilter: itemFilter.toLowerCase() || null,
@ -87,8 +87,7 @@ import DateRangeSelect from "../components/DateRangeSelect.svelte";
})
} else {
stuffLogClient.updateGoal(goal.id, {
startTime: new Date(startTime),
endTime: new Date(endTime),
startTime, endTime,
itemId: itemId || null,
clearItemId: itemId === "",
taskFilter: taskFilter.toLowerCase() || null,
@ -134,7 +133,7 @@ import DateRangeSelect from "../components/DateRangeSelect.svelte";
<option value="task" selected={"task" === compositionMode}>Task</option>
<option value="project" selected={"project" === compositionMode}>Project</option>
</select>
<DateRangeSelect disabled={deletion} bind:fromValue={startTime} bind:toValue={endTime} />
<DateRangeSelect disabled={deletion} bind:value={range} />
<label for="taskFilter">Task Filter (Optional)</label>
<input disabled={deletion} name="taskFilter" type="text" bind:value={taskFilter} />
<label for="itemFilter">Item Filter (Optional)</label>

17
svelte-ui/src/pages/FrontPage.svelte

@ -1,6 +1,9 @@
<script lang="ts">
import DateRangeSelect from "../components/DateRangeSelect.svelte";
import EmptyList from "../components/EmptyList.svelte";
import EmptyParentEntry from "../components/EmptyParentEntry.svelte";
import EveryMinute from "../components/EveryMinute.svelte";
import GoalEntry from "../components/GoalEntry.svelte";
import LogEntry from "../components/LogEntry.svelte";
import ParentEntry from "../components/ParentEntry.svelte";
@ -11,18 +14,19 @@ import ParentEntry from "../components/ParentEntry.svelte";
import { fpLogStore } from "../stores/logs";
import { fpProjectStore, fpProjectStore2 } from "../stores/project";
import { fpTaskStore } from "../stores/tasks";
import { RelativeDateRange } from "../utils/date-range";
import { endOfDay, startOfDay } from "../utils/time";
let fakeMap: {[projectId: string]: boolean} = {}
let fakeProjects: ProjectResult[]
let sortedProjects: ProjectResult[]
let now = new Date();
$: {
if ($fpGoalStore.stale && !$fpGoalStore.loading) {
fpGoalStore.load({
maxTime: new Date(Date.now() + (86400000 * 3)),
minTime: new Date(),
});
const [minTime, maxTime] = (new RelativeDateRange(3, "day")).calculate(now);
fpGoalStore.load({minTime, maxTime});
}
}
@ -57,8 +61,8 @@ import ParentEntry from "../components/ParentEntry.svelte";
$: {
if ($fpLogStore.stale && !$fpLogStore.loading) {
fpLogStore.load({
maxTime: endOfDay(new Date()),
minTime: startOfDay(new Date()),
maxTime: endOfDay(now),
minTime: startOfDay(now),
})
}
}
@ -100,6 +104,7 @@ import ParentEntry from "../components/ParentEntry.svelte";
</script>
<div class="page">
<EveryMinute bind:now={now} />
<div class="left">
{#if !$fpGoalStore.loading || $fpGoalStore.goals.length > 0}
<h1>Active Goals</h1>

16
svelte-ui/src/pages/GoalPage.svelte

@ -4,17 +4,20 @@
import type { ModalData } from "../stores/modal";
import goalStore from "../stores/goal";
import RefreshSelection from "../components/RefreshSelection.svelte";
import { endOfWeek, formatFormTime, startOfWeek } from "../utils/time";
import DateRangeSelect from "../components/DateRangeSelect.svelte";
import { allOffsets, pastOffsets } from "../utils/date-range";
import EveryMinute from "../components/EveryMinute.svelte";
const lsValue = localStorage.getItem("sl2.goalspage.range")
const mdGoalAdd: ModalData = {name: "goal.add"};
let range = allOffsets.find(o => o.name === lsValue) || pastOffsets[0];
let now = new Date();
let minTime = formatFormTime($goalStore.filter.minTime || startOfWeek(new Date()));
let maxTime = formatFormTime($goalStore.filter.maxTime || endOfWeek(new Date()));
$: localStorage.setItem("sl2.goalspage.range", range.name);
$: {
const min = new Date(minTime);
const max = new Date(maxTime);
const [min, max] = range.calculate(now);
if (!$goalStore.loading) {
if ($goalStore.stale || $goalStore.filter.minTime?.getTime() != min.getTime() || $goalStore.filter.maxTime?.getTime() != max.getTime()) {
@ -28,7 +31,7 @@
</script>
<div class="page">
<DateRangeSelect label="Time Filter" styled bind:fromValue={minTime} bind:toValue={maxTime} />
<DateRangeSelect label="Time Filter" styled bind:value={range} />
<div class="goal-list" class:loading={$goalStore.loading}>
{#each $goalStore.goals as goal (goal.id)}
<GoalEntry showAllOptions goal={goal} />
@ -37,6 +40,7 @@
</div>
</div>
<RefreshSelection />
<EveryMinute now={now} />
<style>
div.page {

24
svelte-ui/src/pages/LogsPage.svelte

@ -7,18 +7,24 @@
import RefreshSelection from "../components/RefreshSelection.svelte";
import DateRangeSelect from "../components/DateRangeSelect.svelte";
import EveryMinute from "../components/EveryMinute.svelte";
import { allOffsets, RelativeDateRange } from "../utils/date-range";
const lsValue = localStorage.getItem("sl2.logspage.range")
let groupedLogs: {day: Date, text: string, logs: LogResult[]}[] = [];
let minTime = formatFormTime($logStore.filter.minTime || startOfDay(new Date(Date.now() - 86400000 * 7)));
let maxTime = formatFormTime($logStore.filter.maxTime || endOfDay(new Date()));
let emptyMessage = `No logs since ${formatWeekdayDate(minTime)}.`;
let error = "";
let range = allOffsets.find(o => o.name === lsValue) || new RelativeDateRange(-7, "day");
let now = new Date();
let error = "";
let emptyMessage = `No logs since ${formatWeekdayDate(range.calculate(now)[0])}.`;
$: {
const min = new Date(minTime);
const max = new Date(maxTime);
let min: Date;
let max: Date;
$: [min, max] = range.calculate(now);
$: localStorage.setItem("sl2.logspage.range", range.name);
$: {
error = "";
if (!Number.isNaN(min.getTime()) && !Number.isNaN(max.getTime())) {
@ -41,7 +47,7 @@
$: {
if (!$logStore.loading && !$logStore.stale) {
emptyMessage = `No logs between ${formatWeekdayDate(minTime)} and ${formatWeekdayDate(maxTime)}.`;
emptyMessage = `No logs between ${formatWeekdayDate(min)} and ${formatWeekdayDate(max)}.`;
}
}
@ -94,7 +100,7 @@
</script>
<div class="page">
<DateRangeSelect label="Time Filter" noFuture styled bind:fromValue={minTime} bind:toValue={maxTime} />
<DateRangeSelect label="Time Filter" noFuture styled bind:value={range} />
<div class="error">{error}</div>
<div class="log-list" class:loading={$logStore.loading}>
{#each groupedLogs as logGroup (logGroup.day.getTime())}

102
svelte-ui/src/utils/date-range.ts

@ -0,0 +1,102 @@
import pluralize from "pluralize";
import {
addDays, addMonths, addYears,
endOfDay, endOfMonth, endOfWeek, endOfYear,
startOfDay, startOfMonth, startOfWeek, startOfYear
} from "./time";
export interface DateRange {
name: string
calculate(date: Date): [Date, Date]
}
function dateRangeCallback(name: string, calculate: (date: Date) => [Date, Date]): DateRange {
return {name, calculate};
}
export class RelativeDateRange {
private duration: number
private unit: "day" | "month" | "year"
public name: string
constructor(duration: number, unit: "day" | "month" | "year") {
this.duration = duration;
this.unit = unit;
if (this.duration < 0) {
this.name = `Last ${-this.duration} ${pluralize(this.unit, -this.duration)}`;
} else {
this.name = `Next ${this.duration} ${pluralize(this.unit, this.duration)}`;
}
}
calculate(date: Date): [Date, Date] {
switch (this.unit) {
case "day": {
return this.duration < 0
? [ startOfDay(addDays(date, this.duration)), endOfDay(date) ]
: [ startOfDay(date), endOfDay(addDays(date, this.duration)) ];
}
case "month": {
return this.duration < 0
? [ startOfDay(addMonths(date, this.duration)), endOfDay(date) ]
: [ startOfDay(date), endOfDay(addMonths(date, this.duration)) ];
}
case "year": {
return this.duration < 0
? [ startOfDay(addYears(date, this.duration)), endOfDay(date) ]
: [ startOfDay(date), endOfDay(addYears(date, this.duration)) ];
}
}
}
}
export function customDateRange(from: Date, to: Date): DateRange {
return {name: "Specific Dates", calculate: () => [from, to]}
}
const thisWeek = dateRangeCallback("This week", date => [startOfWeek(date), endOfWeek(date)]);
const nextWeek = dateRangeCallback("Next week", date => [startOfWeek(addDays(date, 7)), endOfWeek(addDays(date, 7))]);
const lastWeek = dateRangeCallback("Last week", date => [startOfWeek(addDays(date, -7)), endOfWeek(addDays(date, -7))]);
const thisMonth = dateRangeCallback("This month", date => [startOfMonth(date), endOfMonth(date)]);
const nextMonth = dateRangeCallback("Next month", date => [startOfMonth(addMonths(date, 1)), endOfMonth(addMonths(date, 1))]);
const lastMonth = dateRangeCallback("Last month", date => [startOfMonth(addMonths(date, -1)), endOfMonth(addMonths(date, -1))]);
const thisYear = dateRangeCallback("This year", date => [startOfYear(date), endOfYear(date)]);
const nextYear = dateRangeCallback("Next year", date => [startOfYear(addYears(date, 1)), endOfYear(addYears(date, 1))]);
const lastYear = dateRangeCallback("Last year", date => [startOfYear(addYears(date, -1)), endOfYear(addYears(date, -1))]);
export const allOffsets: DateRange[] = [
thisWeek,
nextWeek,
new RelativeDateRange(7, "day"),
lastWeek,
new RelativeDateRange(-7, "day"),
thisMonth,
nextMonth,
new RelativeDateRange(30, "day"),
new RelativeDateRange(90, "day"),
lastMonth,
new RelativeDateRange(-30, "day"),
new RelativeDateRange(-90, "day"),
thisYear,
nextYear,
new RelativeDateRange(1, "year"),
lastYear,
new RelativeDateRange(-1, "year"),
];
export function rangeFromDates(from: Date, to: Date, options: DateRange[]): DateRange {
for (const option of options) {
const [from2, to2] = option.calculate(new Date());
if (from2.getTime() == from.getTime() && to2.getTime() === to.getTime()) {
return option;
}
}
return customDateRange(from, to);
}
export const pastOffsets: DateRange[] = allOffsets.filter(r => !r.name.startsWith("Next"));

12
svelte-ui/src/utils/time.ts

@ -110,4 +110,16 @@ export function addDays(date: Date, days: number): Date {
const newDate = new Date(date.getTime());
newDate.setDate(newDate.getDate() + days);
return newDate;
}
export function addMonths(date: Date, months: number): Date {
const newDate = new Date(date.getTime());
newDate.setMonth(newDate.getMonth() + months);
return newDate;
}
export function addYears(date: Date, years: number): Date {
const newDate = new Date(date.getTime());
newDate.setFullYear(newDate.getFullYear() + years);
return newDate;
}
Loading…
Cancel
Save