Commit 1b9f0eb4 authored by Marius Shekow's avatar Marius Shekow
Browse files

Merge branch '5-persist-config' into 'main'

Store (and restore) user-supplied parser config values to and from localStorage

Closes #17 and #5

See merge request shekow/sap-timesheet!6
parents 308ecd2c 2105a1e5
......@@ -2,6 +2,13 @@
This document describes both visible and internal changes of the SAP timesheet tool.
## 0.0.6 (2022-03-17)
- Changes for end-users:
- Fix parsing of CSVs generated on Windows that have `\r\n` delimiters, by stripping `\r` characters
- The user-supplied values for the CSV separation character, date format, and column indices are now permanently
stored in (and restored from) the browser's `localStorage`
## 0.0.5 (2022-02-22)
- Changes for end-users:
......
{
"name": "sap-timesheet",
"version": "0.0.5",
"version": "0.0.6",
"description": "Tool that creates entries in SAP's Meine Zeitbuchungen, from a CSV file",
"main": "sap-timesheet.js",
"scripts": {
......
......@@ -28,4 +28,27 @@ export class ColumnIndices {
const filteredColumnIndices = columnIndices.filter((index) => index >= 0);
return !hasDuplicates(filteredColumnIndices);
}
/**
* Attempts to construct and return a ColumnIndices from a JSON representation.
*
* @param {string} json
* @returns {ColumnIndices | null}
*/
static fromJson(json) {
try {
const obj = JSON.parse(json);
const columnIndices = new ColumnIndices(
obj.date, obj.startTime, obj.endTime, obj.startTime2, obj.endTime2, obj.pauseDuration, obj.comment
);
// Ensure that all values are numbers
const dataTypes = Object.entries(columnIndices).map((key, value) => typeof value);
const allDataTypesAreNumbers = Object.entries(dataTypes).map((value) => typeof value[1]);
if (allDataTypesAreNumbers) return columnIndices;
} catch (e) {
}
return null;
}
}
import {ColumnIndices} from "./column-indices.js";
const COLUMN_INDICES_KEY = "sap-timesheet-column-indices";
const DATE_FORMAT_STRING_KEY = "sap-timesheet-date-format-string";
const CSV_SEP_CHAR_KEY = "sap-timesheet-csv-separation-char";
/**
* Helper class that stores and retrieves user-supplied preferences for the SAP time sheet parsing from/to localStorage.
*/
export class PersistentPreferences {
/**
* @returns {ColumnIndices}
*/
static getColumnIndices() {
const jsonData = localStorage.getItem(COLUMN_INDICES_KEY);
if (jsonData) {
const storedColumnIndices = ColumnIndices.fromJson(jsonData);
if (storedColumnIndices) {
return storedColumnIndices;
}
}
return new ColumnIndices(0, 1, 2, -1, -1, 3, 4);
}
/**
* @param {ColumnIndices} columnIndices
*/
static storeColumnIndices(columnIndices) {
localStorage.setItem(COLUMN_INDICES_KEY, JSON.stringify(columnIndices));
}
/**
* @returns {string}
*/
static getCsvDateFormatString() {
const storedData = localStorage.getItem(DATE_FORMAT_STRING_KEY);
if (storedData) {
return storedData;
}
return "YYYY-MM-DD";
}
/**
* @param {string} csvDateFormatString
*/
static storeCsvDateFormatString(csvDateFormatString) {
localStorage.setItem(DATE_FORMAT_STRING_KEY, csvDateFormatString);
}
/**
* @returns {string}
*/
static getCsvSeparationChar() {
const storedData = localStorage.getItem(CSV_SEP_CHAR_KEY);
if (storedData) {
return storedData;
}
return ",";
}
/**
* @param {string} csvSeparationChar
*/
static storeCsvSeparationChar(csvSeparationChar) {
localStorage.setItem(CSV_SEP_CHAR_KEY, csvSeparationChar);
}
}
......@@ -6,6 +6,7 @@ import {
} from "./table-generation.js";
import {MAX_CSV_RAW_ROWS, PROJECT_URL} from "./constants.js";
import {ColumnIndices} from "./column-indices.js";
import {PersistentPreferences} from "./persistent-preferences.js";
import {getEntries, getSapEntries} from "./entries-processing.js";
import {SapSdkWrapper} from "./sap-sdk-wrapper.js";
import {showGenericModal} from "./bootstrap-modal.js";
......@@ -42,7 +43,7 @@ const wizardTemplate = `
"Meine Zeitbuchungen" module, from a CSV file.</p>
<p><strong>Caution: this tool is provided "as is", without any warranty of any kind.</strong></p>
<p>Please specify the character used as <em>column separator</em>:
<input type="text" id="csvSeparationChar" maxlength="1" class="form-control" value=";"
<input type="text" id="csvSeparationChar" maxlength="1" class="form-control"
style="width: 2em; display: inline-block">
</p>
......@@ -53,7 +54,7 @@ const wizardTemplate = `
</p>
<p>
Date format:
<input type="text" id="csvDateFormatString" class="form-control" value="YYYY-MM-DD"
<input type="text" id="csvDateFormatString" class="form-control"
style="width: 12em; display: inline-block">
</p>
<p>
......@@ -73,15 +74,15 @@ const wizardTemplate = `
</style>
<p>Please configure which column index contain the corresponding information:</p>
<p>
Date: <input type="number" value="0" min="0" max="50" class="input-spinner" id="csvDateColumnIndex" /> <strong>|</strong>
Start time: <input type="number" value="1" min="0" max="50" class="input-spinner" id="csvStartTimeColumnIndex" /> <strong>|</strong>
End time: <input type="number" value="2" min="0" max="50" class="input-spinner" id="csvEndTimeColumnIndex" /> <strong>|</strong>
Pause duration: <input type="number" value="3" min="0" max="50" class="input-spinner" id="csvPauseColumnIndex" /> <strong>|</strong>
Comments: <input type="number" value="4" min="0" max="50" class="input-spinner" id="csvCommentsColumnIndex" />
Date: <input type="number" min="0" max="50" class="input-spinner" id="csvDateColumnIndex" /> <strong>|</strong>
Start time: <input type="number" min="0" max="50" class="input-spinner" id="csvStartTimeColumnIndex" /> <strong>|</strong>
End time: <input type="number" min="0" max="50" class="input-spinner" id="csvEndTimeColumnIndex" /> <strong>|</strong>
Pause duration: <input type="number" min="0" max="50" class="input-spinner" id="csvPauseColumnIndex" /> <strong>|</strong>
Comments: <input type="number" min="0" max="50" class="input-spinner" id="csvCommentsColumnIndex" />
</p>
<div id="csvSecondTimepairContainer" style="display: none; margin-bottom: 15px">
Start time #2: <input type="number" value="1" min="0" max="50" class="input-spinner" id="csvStartTime2ColumnIndex" /> <strong>|</strong>
End time #2: <input type="number" value="2" min="0" max="50" class="input-spinner" id="csvEndTime2ColumnIndex" />
Start time #2: <input type="number" min="0" max="50" class="input-spinner" id="csvStartTime2ColumnIndex" /> <strong>|</strong>
End time #2: <input type="number" min="0" max="50" class="input-spinner" id="csvEndTime2ColumnIndex" />
</div>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="csvSecondTimepair">
......@@ -104,7 +105,7 @@ const wizardTemplate = `
<div id="step-4" class="tab-pane" role="tabpanel">
<p>The following entries will be created in SAP, once you click <strong>Next</strong>.</p>
<div id="sapEntriesContainerNode"></div>
<p><strong>Note:</strong> if your CSV file contains a <em>single</em> entry for a say with a non-zero break duration, the Timesheet tool splits it into <em>two</em> SAP entries.</p>
<p><strong>Note:</strong> if your CSV file contains a <em>single</em> entry for a day with a non-zero break duration, the Timesheet tool splits it into <em>two</em> SAP entries.</p>
<p>Once you press <strong>Next</strong>, this dialog will close, and the Timesheet tool starts creating entries. This may take up to a minute.</p>
<p>Once all entries were created, another dialog shows you the result. The SAP user interface
might also display popup error dialogs, in case a problem happened, or show
......@@ -135,17 +136,56 @@ export async function injectModal() {
}
});
let csvSeparationChar = "";
let csvDateFormatString = "";
/** @type {ColumnIndices | null} */
let columnIndices = null;
let csvSeparationChar = PersistentPreferences.getCsvSeparationChar();
let csvDateFormatString = PersistentPreferences.getCsvDateFormatString();
let columnIndices = PersistentPreferences.getColumnIndices();
/** @type {Entries | null} */
let sapEntries = null;
document.getElementById("csvProcessComments").addEventListener("change", (e) => {
$("#csvCommentsColumnIndex").prop("disabled", !e.target.checked);
});
document.getElementById("csvSecondTimepair").addEventListener("change", (e) => {
const timepair2Container = $("#csvSecondTimepairContainer");
if (e.target.checked) {
timepair2Container.show();
} else {
timepair2Container.hide();
}
});
// Preset the form values
$("#csvSeparationChar").val(csvSeparationChar);
$("#csvDateFormatString").val(csvDateFormatString);
$("#csvDateColumnIndex").val(columnIndices.date);
$("#csvStartTimeColumnIndex").val(columnIndices.startTime);
$("#csvEndTimeColumnIndex").val(columnIndices.endTime);
$("#csvPauseColumnIndex").val(columnIndices.pauseDuration);
if (columnIndices.comment >= 0) {
$('#csvProcessComments').prop('checked', true);
$("#csvCommentsColumnIndex").val(columnIndices.comment);
} else {
$('#csvProcessComments').prop('checked', false);
$("#csvCommentsColumnIndex").val(0);
}
if (columnIndices.startTime2 >= 0 && columnIndices.endTime2 >= 0) {
$('#csvSecondTimepair').prop('checked', true);
$("#csvSecondTimepairContainer").show();
$("#csvStartTime2ColumnIndex").val(columnIndices.startTime2);
$("#csvEndTime2ColumnIndex").val(columnIndices.endTime2);
} else {
$('#csvSecondTimepair').prop('checked', false);
$("#csvStartTime2ColumnIndex").val(0);
$("#csvEndTime2ColumnIndex").val(0);
}
wizardDomNode.on("leaveStep", async function (e, anchorObject, currentStepIndex, nextStepIndex, stepDirection) {
if (currentStepIndex === 0 && nextStepIndex === 1) {
csvSeparationChar = $("#csvSeparationChar").val();
csvDateFormatString = $("#csvDateFormatString").val();
PersistentPreferences.storeCsvSeparationChar(csvSeparationChar);
PersistentPreferences.storeCsvDateFormatString(csvDateFormatString);
const rawCsvTable = generateRawCsvTable(getFileContents(), csvSeparationChar, MAX_CSV_RAW_ROWS);
$("#rawCsvContainerNode").replaceWith(rawCsvTable);
} else if (currentStepIndex === 1 && nextStepIndex === 2) {
......@@ -164,6 +204,7 @@ export async function injectModal() {
commentsColumnIndex
);
if (columnIndices.isValid()) {
PersistentPreferences.storeColumnIndices(columnIndices);
const rawEntries = getEntries(getFileContents(), columnIndices, csvDateFormatString, csvSeparationChar);
const rawEntriesTable = generateParsedRawEntriesTable(rawEntries, commentsColumnIndex >= 0);
$("#rawEntriesContainerNode").replaceWith(rawEntriesTable);
......@@ -205,19 +246,6 @@ export async function injectModal() {
}
return true; // allow the step to proceed
});
document.getElementById("csvProcessComments").addEventListener("change", (e) => {
$("#csvCommentsColumnIndex").prop("disabled", !e.target.checked);
});
document.getElementById("csvSecondTimepair").addEventListener("change", (e) => {
const timepair2Container = $("#csvSecondTimepairContainer");
if (e.target.checked) {
timepair2Container.show();
} else {
timepair2Container.hide();
}
});
}
/**
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment