From a640703f2da8815dc26ad1600a6f206be1624379 Mon Sep 17 00:00:00 2001
From: ivarlovlie <git@ivarlovlie.no>
Date: Wed, 1 Jun 2022 22:10:32 +0200
Subject: feat: Initial after clean slate

---
 apps/projects-web/src/app/pages/data.svelte | 392 ++++++++++++++++++++++++++++
 1 file changed, 392 insertions(+)
 create mode 100644 apps/projects-web/src/app/pages/data.svelte

(limited to 'apps/projects-web/src/app/pages/data.svelte')

diff --git a/apps/projects-web/src/app/pages/data.svelte b/apps/projects-web/src/app/pages/data.svelte
new file mode 100644
index 0000000..070b98b
--- /dev/null
+++ b/apps/projects-web/src/app/pages/data.svelte
@@ -0,0 +1,392 @@
+<script>
+	import {IconNames} from "$shared/lib/configuration";
+	import {onMount} from "svelte";
+	import {Temporal} from "@js-temporal/polyfill";
+	import Layout from "./_layout.svelte";
+	import Modal from "$shared/components/modal.svelte";
+	import Tile from "$shared/components/tile.svelte";
+	import Icon from "$shared/components/icon.svelte";
+	import EntryForm from "$app/pages/views/entry-form/index.svelte";
+	import {Table, THead, TBody, TCell, TRow, TablePaginator} from "$shared/components/table";
+	import {TimeEntryQueryDuration} from "$shared/lib/models/TimeEntryQuery";
+	import {delete_time_entry, get_time_entries, get_time_entry} from "$shared/lib/api/time-entry";
+	import {seconds_to_hour_minute_string, is_guid, move_focus, unwrap_date_time_from_entry} from "$shared/lib/helpers";
+	import Button from "$shared/components/button.svelte";
+
+	let pageCount = 1;
+	let page = 1;
+
+	const defaultQuery = {
+		duration: TimeEntryQueryDuration.THIS_YEAR,
+		categories: [],
+		labels: [],
+		page: page,
+		pageSize: 50,
+	};
+
+	let isLoading;
+	let categories = [];
+	let labels = [];
+	let entries = [];
+	let durationSummary = false;
+	let EditEntryModal;
+	let EditEntryForm;
+	let currentTimespanFilter = TimeEntryQueryDuration.THIS_YEAR;
+	let currentSpecificDateFilter = Temporal.Now.plainDateTimeISO().subtract({days: 1}).toString().substring(0, 10);
+	let currentDateRangeFilter = {};
+	let currentCategoryFilter = "all";
+	let currentLabelFilter = "all";
+	let showDateFilterOptions = false;
+	let secondsLogged = 0;
+
+	function set_duration_summary_string() {
+		if (entries.length > 0) {
+			durationSummary = `Showing ${entries.length} ${entries.length === 1 ? "entry" : "entries"}, totalling in ${seconds_to_hour_minute_string(secondsLogged)}`;
+		} else {
+			durationSummary = "";
+		}
+	}
+
+	async function load_entries(query = defaultQuery) {
+		isLoading = true;
+		const response = await get_time_entries(query);
+		if (response.status === 200) {
+			const responseEntries = [];
+			secondsLogged = 0;
+			for (const entry of response.data.results) {
+				const date_time = unwrap_date_time_from_entry(entry);
+				const seconds = (date_time.duration.hours * 60 * 60) + (date_time.duration.minutes * 60);
+				responseEntries.push({
+					id: entry.id,
+					date: date_time.start_date,
+					start: date_time.start_time,
+					stop: date_time.stop_time,
+					durationString: date_time.duration.hours + "h" + date_time.duration.minutes + "m",
+					seconds: seconds,
+					category: entry.category,
+					labels: entry.labels,
+					description: entry.description,
+				});
+				secondsLogged += seconds;
+			}
+			entries = responseEntries;
+			page = response.data.page;
+			pageCount = response.data.totalPageCount;
+		} else {
+			entries = [];
+			page = 0;
+			pageCount = 0;
+		}
+		isLoading = false;
+		set_duration_summary_string();
+	}
+
+	function load_entries_with_filter(page = 1) {
+		let query = defaultQuery;
+		query.duration = currentTimespanFilter;
+		query.labels = [];
+		query.categories = [];
+		query.page = page;
+
+		if (currentTimespanFilter === TimeEntryQueryDuration.SPECIFIC_DATE) {
+			query.specificDate = currentSpecificDateFilter;
+		} else {
+			delete query.specificDate;
+		}
+
+		if (currentTimespanFilter === TimeEntryQueryDuration.DATE_RANGE) {
+			query.dateRange = currentDateRangeFilter;
+		} else {
+			delete query.dateRange;
+		}
+
+		if ((currentCategoryFilter !== "all" && currentCategoryFilter?.length > 0) ?? false) {
+			for (const chosenCategoryId of currentCategoryFilter) {
+				if (chosenCategoryId === "all") {
+					continue;
+				}
+				query.categories.push({
+					id: chosenCategoryId,
+				});
+			}
+		}
+
+		if ((currentLabelFilter !== "all" && currentLabelFilter?.length > 0) ?? false) {
+			for (const chosenLabelId of currentLabelFilter) {
+				if (chosenLabelId === "all") {
+					continue;
+				}
+				query.labels.push({
+					id: chosenLabelId,
+				});
+			}
+		}
+
+		load_entries(query);
+	}
+
+	async function handle_delete_entry_button_click(e, entryId) {
+		if (confirm("Are you sure you want to delete this entry?")) {
+			const response = await delete_time_entry(entryId);
+			if (response.ok) {
+				const indexOfEntry = entries.findIndex((c) => c.id === entryId);
+				if (indexOfEntry !== -1) {
+					secondsLogged -= entries[indexOfEntry].seconds;
+					entries.splice(indexOfEntry, 1);
+					entries = entries;
+					set_duration_summary_string();
+				}
+			}
+		}
+	}
+
+	function handle_edit_entry_form_updated() {
+		load_entries_with_filter(page);
+		EditEntryModal.close();
+	}
+
+	async function handle_edit_entry_button_click(event, entryId) {
+		const response = await get_time_entry(entryId);
+		if (response.status === 200) {
+			if (is_guid(response.data.id)) {
+				EditEntryForm.set_values(response.data);
+				EditEntryModal.open();
+				move_focus(document.querySelector("input[id='date']"));
+			}
+		}
+	}
+
+	function close_date_filter_box(event) {
+		if (!event.target.closest(".date_filter_box_el")) {
+			showDateFilterOptions = false;
+			window.removeEventListener("click", close_date_filter_box);
+		}
+	}
+
+	function toggle_date_filter_box(event) {
+		const box = document.getElementById("date_filter_box");
+		const rect = event.target.getBoundingClientRect();
+		box.style.top = rect.y + "px";
+		box.style.left = rect.x - 50 + "px";
+		showDateFilterOptions = true;
+		window.addEventListener("click", close_date_filter_box);
+	}
+
+	onMount(() => {
+		isLoading = true;
+		Promise.all([load_entries()]).then(() => {
+			isLoading = false;
+		});
+	});
+</script>
+
+<Modal title="Edit entry"
+       bind:functions={EditEntryModal}
+       on:closed={() => EditEntryForm.reset()}>
+    <EntryForm bind:functions={EditEntryForm}
+               on:updated={handle_edit_entry_form_updated}/>
+</Modal>
+
+<div id="date_filter_box"
+     style="margin-top:25px"
+     class="padding-xs z-index-overlay bg shadow-sm position-absolute date_filter_box_el border {showDateFilterOptions ? '' : 'hide'}">
+    <div class="flex items-baseline margin-bottom-xxxxs">
+        <label class="text-sm color-contrast-medium margin-right-xs"
+               for="durationSelect">Timespan:</label>
+        <div class="select inline-block js-select">
+            <select name="durationSelect"
+                    bind:value={currentTimespanFilter}
+                    id="durationSelect">
+                <option value={TimeEntryQueryDuration.TODAY}
+                        selected> Today
+                </option>
+                <option value={TimeEntryQueryDuration.THIS_WEEK}>This week</option>
+                <option value={TimeEntryQueryDuration.THIS_MONTH}>This month</option>
+                <option value={TimeEntryQueryDuration.THIS_YEAR}>This year</option>
+                <option value={TimeEntryQueryDuration.SPECIFIC_DATE}>Spesific date</option>
+                <option value={TimeEntryQueryDuration.DATE_RANGE}>Date range</option>
+            </select>
+
+            <svg class="icon icon--xxxs margin-left-xxs"
+                 viewBox="0 0 8 8">
+                <path d="M7.934,1.251A.5.5,0,0,0,7.5,1H.5a.5.5,0,0,0-.432.752l3.5,6a.5.5,0,0,0,.864,0l3.5-6A.5.5,0,0,0,7.934,1.251Z"/>
+            </svg>
+        </div>
+    </div>
+
+    {#if currentTimespanFilter === TimeEntryQueryDuration.SPECIFIC_DATE}
+        <div class="flex items-baseline margin-bottom-xxxxs justify-between">
+            <span class="text-sm color-contrast-medium margin-right-xs">Date:</span>
+            <span class="text-sm">
+				<input type="date"
+                       class="border-none padding-0 color-inherit bg-transparent"
+                       bind:value={currentSpecificDateFilter}/>
+			</span>
+        </div>
+    {/if}
+
+    {#if currentTimespanFilter === TimeEntryQueryDuration.DATE_RANGE}
+        <div class="flex items-baseline margin-bottom-xxxxs justify-between">
+            <span class="text-sm color-contrast-medium margin-right-xs">From:</span>
+            <span class="text-sm">
+				<input type="date"
+                       class="border-none padding-0 color-inherit bg-transparent"
+                       on:change={(e) => (currentDateRangeFilter.from = e.target.value)}/>
+			</span>
+        </div>
+
+        <div class="flex items-baseline margin-bottom-xxxxs justify-between">
+            <span class="text-sm color-contrast-medium margin-right-xs">To:</span>
+            <span class="text-sm">
+				<input type="date"
+                       class="border-none padding-0 color-inherit bg-transparent"
+                       on:change={(e) => (currentDateRangeFilter.to = e.target.value)}/>
+			</span>
+        </div>
+    {/if}
+
+    <div class="flex items-baseline justify-end">
+        <Button variant="subtle"
+                on:click={() => load_entries_with_filter(page)}
+                class="text-sm"
+                text="Save"/>
+    </div>
+</div>
+
+<Layout>
+    <Tile class="{isLoading ? 'c-disabled loading' : ''}">
+        <nav class="s-tabs text-sm">
+            <ul class="s-tabs__list">
+                <li><span class="s-tabs__link s-tabs__link--current">All (21)</span></li>
+                <li><span class="s-tabs__link">Published (19)</span></li>
+                <li><span class="s-tabs__link">Draft (2)</span></li>
+            </ul>
+        </nav>
+        <div class="max-width-100% overflow-auto"
+             style="max-height: 82.5vh">
+            <Table class="text-sm width-100% int-table--sticky-header">
+                <THead>
+                <TCell type="th"
+                       style="width: 30px;">
+                    <div class="custom-checkbox int-table__checkbox">
+                        <input class="custom-checkbox__input"
+                               type="checkbox"
+                               aria-label="Select all rows"/>
+                        <div class="custom-checkbox__control"
+                             aria-hidden="true"></div>
+                    </div>
+                </TCell>
+
+                <TCell type="th"
+                       style="width: 100px">
+                    <div class="flex items-center justify-between">
+                        <span>Date</span>
+                        <div class="date_filter_box_el cursor-pointer"
+                             on:click={toggle_date_filter_box}>
+                            <Icon name="{IconNames.funnel}"/>
+                        </div>
+                    </div>
+                </TCell>
+
+                <TCell type="th"
+                       style="width: 100px">
+                    <div class="flex items-center">
+                        <span>Duration</span>
+                    </div>
+                </TCell>
+
+                <TCell type="th"
+                       style="width: 100px;">
+                    <div class="flex items-center">
+                        <span>Category</span>
+                    </div>
+                </TCell>
+
+                <TCell type="th"
+                       style="width: 300px;">
+                    <div class="flex items-center">
+                        <span>Description</span>
+                    </div>
+                </TCell>
+                <TCell type="th"
+                       style="width: 50px"></TCell>
+                </THead>
+                <TBody>
+                {#if entries.length > 0}
+                    {#each entries as entry}
+                        <TRow class="text-nowrap"
+                              data-id={entry.id}>
+                            <TCell type="th"
+                                   thScope="row">
+                                <div class="custom-checkbox int-table__checkbox">
+                                    <input class="custom-checkbox__input"
+                                           type="checkbox"
+                                           aria-label="Select this row"/>
+                                    <div class="custom-checkbox__control"
+                                         aria-hidden="true"></div>
+                                </div>
+                            </TCell>
+                            <TCell>
+                                <pre>{entry.date.toLocaleString()}</pre>
+                            </TCell>
+                            <TCell>
+                                <pre class="flex justify-between">
+                                    <div class="flex justify-between">
+                                        <span>{entry.start.toLocaleString(undefined, {timeStyle: "short"})}</span>
+                                        <span> - </span>
+                                        <span>{entry.stop.toLocaleString(undefined, {timeStyle: "short"})}</span>
+                                    </div>
+                                </pre>
+                            </TCell>
+                            <TCell>
+                                <span data-id={entry.category.id}>{entry.category.name}</span>
+                            </TCell>
+                            <TCell class="text-truncate max-width-xxxxs"
+                                   title="{entry.description}">
+                                {entry.description ?? ""}
+                            </TCell>
+                            <TCell class="flex flex-row justify-end items-center">
+                                <Button icon="{IconNames.pencilSquare}"
+                                        variant="reset"
+                                        icon_width="1.2rem"
+                                        icon_height="1.2rem"
+                                        on:click={(e) => handle_edit_entry_button_click(e, entry.id)}
+                                        title="Edit entry"/>
+                                <Button icon="{IconNames.trash}"
+                                        variant="reset"
+                                        icon_width="1.2rem"
+                                        icon_height="1.2rem"
+                                        on:click={(e) => handle_delete_entry_button_click(e, entry.id)}
+                                        title="Delete entry"/>
+                            </TCell>
+                        </TRow>
+                    {/each}
+                {:else}
+                    <TRow class="text-nowrap">
+                        <TCell type="th"
+                               thScope="row"
+                               colspan="7">
+                            {isLoading ? "Loading..." : "No entries"}
+                        </TCell>
+                    </TRow>
+                {/if}
+                </TBody>
+            </Table>
+        </div>
+        <div class="flex items-center justify-between">
+            <p class="text-sm">
+                {#if durationSummary}
+                    <small class={isLoading ? "c-disabled loading" : ""}>{durationSummary}</small>
+                {:else}
+                    <small class={isLoading ? "c-disabled loading" : ""}>No entries</small>
+                {/if}
+            </p>
+
+            <nav class="grid padding-y-sm {isLoading ? 'c-disabled loading' : ''}">
+                <TablePaginator {page}
+                                on:value_change={(e) => load_entries_with_filter(e.detail.newValue)}
+                                {pageCount}/>
+            </nav>
+        </div>
+    </Tile>
+</Layout>
-- 
cgit v1.3

