diff --git a/electron/decoder/decoder.js b/electron/decoder/decoder.js
index 597eb57..3182cc3 100644
--- a/electron/decoder/decoder.js
+++ b/electron/decoder/decoder.js
@@ -23,13 +23,13 @@ async function ImportJob(path) {
NewNotification({
title: "Job Uploaded",
body: "A new job has been uploaded.",
- }).show();
+ });
} else {
log.info(`Ignored job. ${newJob.ERROR}`);
NewNotification({
title: "Job Ignored",
body: newJob.ERROR,
- }).show();
+ });
}
}
diff --git a/electron/electron-store.js b/electron/electron-store.js
index 2ffcb40..e8bd180 100644
--- a/electron/electron-store.js
+++ b/electron/electron-store.js
@@ -2,6 +2,7 @@ const Store = require("electron-store");
const store = new Store({
defaults: {
+ enableNotifications: true,
filePaths: [],
accepted_ins_co: [],
polling: {
diff --git a/electron/file-watcher/file-watcher.js b/electron/file-watcher/file-watcher.js
index ff7866d..ca2dd5c 100644
--- a/electron/file-watcher/file-watcher.js
+++ b/electron/file-watcher/file-watcher.js
@@ -17,7 +17,7 @@ async function StartWatcher() {
NewNotification({
title: "RPS Watcher cannot start",
body: "Please set the appropriate file paths and try again.",
- }).show();
+ });
log.warn("Cannot start watcher. No file paths set.");
return [];
}
@@ -87,7 +87,7 @@ function onWatcherReady() {
NewNotification({
title: "RPS Watcher Started",
body: "Newly exported estimates will be automatically uploaded.",
- }).show();
+ });
console.log("Confirmed watched paths:", watcher.getWatched());
}
@@ -99,7 +99,7 @@ async function StopWatcher() {
NewNotification({
title: "RPS Watcher Stopped",
body: "Estimates will not be automatically uploaded.",
- }).show();
+ });
}
exports.StartWatcher = StartWatcher;
diff --git a/electron/notification-wrapper/notification-wrapper.js b/electron/notification-wrapper/notification-wrapper.js
index b60cc9b..0192515 100644
--- a/electron/notification-wrapper/notification-wrapper.js
+++ b/electron/notification-wrapper/notification-wrapper.js
@@ -1,11 +1,14 @@
const { Notification } = require("electron");
const path = require("path");
+const { store } = require("../electron-store");
function NewNotification(config) {
- return Notification({
-
- icon: path.join(__dirname, "../../src/assets/logo512.png"),
- ...config,
- });
+ const enableNotifications = store.get("enableNotifications");
+ if (enableNotifications) {
+ Notification({
+ icon: path.join(__dirname, "../../src/assets/logo512.png"),
+ ...config,
+ }).show();
+ }
}
exports.NewNotification = NewNotification;
diff --git a/hasura/migrations/1603322483238_alter_table_public_bodyshops_add_column_groups/down.yaml b/hasura/migrations/1603322483238_alter_table_public_bodyshops_add_column_groups/down.yaml
new file mode 100644
index 0000000..e7be601
--- /dev/null
+++ b/hasura/migrations/1603322483238_alter_table_public_bodyshops_add_column_groups/down.yaml
@@ -0,0 +1,5 @@
+- args:
+ cascade: false
+ read_only: false
+ sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "groups";
+ type: run_sql
diff --git a/hasura/migrations/1603322483238_alter_table_public_bodyshops_add_column_groups/up.yaml b/hasura/migrations/1603322483238_alter_table_public_bodyshops_add_column_groups/up.yaml
new file mode 100644
index 0000000..267f579
--- /dev/null
+++ b/hasura/migrations/1603322483238_alter_table_public_bodyshops_add_column_groups/up.yaml
@@ -0,0 +1,6 @@
+- args:
+ cascade: false
+ read_only: false
+ sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "groups" jsonb NOT NULL DEFAULT
+ jsonb_build_array();
+ type: run_sql
diff --git a/hasura/migrations/1603322490951_update_permission_user_public_table_bodyshops/down.yaml b/hasura/migrations/1603322490951_update_permission_user_public_table_bodyshops/down.yaml
new file mode 100644
index 0000000..c82a190
--- /dev/null
+++ b/hasura/migrations/1603322490951_update_permission_user_public_table_bodyshops/down.yaml
@@ -0,0 +1,27 @@
+- args:
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: drop_select_permission
+- args:
+ permission:
+ allow_aggregations: false
+ columns:
+ - accepted_ins_co
+ - created_at
+ - id
+ - shopname
+ - targets
+ - updated_at
+ computed_fields: []
+ filter:
+ associations:
+ user:
+ authid:
+ _eq: X-Hasura-User-Id
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: create_select_permission
diff --git a/hasura/migrations/1603322490951_update_permission_user_public_table_bodyshops/up.yaml b/hasura/migrations/1603322490951_update_permission_user_public_table_bodyshops/up.yaml
new file mode 100644
index 0000000..da91d45
--- /dev/null
+++ b/hasura/migrations/1603322490951_update_permission_user_public_table_bodyshops/up.yaml
@@ -0,0 +1,28 @@
+- args:
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: drop_select_permission
+- args:
+ permission:
+ allow_aggregations: false
+ columns:
+ - accepted_ins_co
+ - created_at
+ - groups
+ - id
+ - shopname
+ - targets
+ - updated_at
+ computed_fields: []
+ filter:
+ associations:
+ user:
+ authid:
+ _eq: X-Hasura-User-Id
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: create_select_permission
diff --git a/hasura/migrations/1603322495969_update_permission_user_public_table_bodyshops/down.yaml b/hasura/migrations/1603322495969_update_permission_user_public_table_bodyshops/down.yaml
new file mode 100644
index 0000000..b8ad060
--- /dev/null
+++ b/hasura/migrations/1603322495969_update_permission_user_public_table_bodyshops/down.yaml
@@ -0,0 +1,23 @@
+- args:
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: drop_update_permission
+- args:
+ permission:
+ columns:
+ - accepted_ins_co
+ - shopname
+ - targets
+ filter:
+ associations:
+ user:
+ authid:
+ _eq: X-Hasura-User-Id
+ set: {}
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: create_update_permission
diff --git a/hasura/migrations/1603322495969_update_permission_user_public_table_bodyshops/up.yaml b/hasura/migrations/1603322495969_update_permission_user_public_table_bodyshops/up.yaml
new file mode 100644
index 0000000..1ff07f9
--- /dev/null
+++ b/hasura/migrations/1603322495969_update_permission_user_public_table_bodyshops/up.yaml
@@ -0,0 +1,24 @@
+- args:
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: drop_update_permission
+- args:
+ permission:
+ columns:
+ - accepted_ins_co
+ - groups
+ - shopname
+ - targets
+ filter:
+ associations:
+ user:
+ authid:
+ _eq: X-Hasura-User-Id
+ set: {}
+ role: user
+ table:
+ name: bodyshops
+ schema: public
+ type: create_update_permission
diff --git a/hasura/migrations/1603324841916_update_permission_user_public_table_jobs/down.yaml b/hasura/migrations/1603324841916_update_permission_user_public_table_jobs/down.yaml
new file mode 100644
index 0000000..e1abc5c
--- /dev/null
+++ b/hasura/migrations/1603324841916_update_permission_user_public_table_jobs/down.yaml
@@ -0,0 +1,6 @@
+- args:
+ role: user
+ table:
+ name: jobs
+ schema: public
+ type: drop_delete_permission
diff --git a/hasura/migrations/1603324841916_update_permission_user_public_table_jobs/up.yaml b/hasura/migrations/1603324841916_update_permission_user_public_table_jobs/up.yaml
new file mode 100644
index 0000000..e02a8fe
--- /dev/null
+++ b/hasura/migrations/1603324841916_update_permission_user_public_table_jobs/up.yaml
@@ -0,0 +1,14 @@
+- args:
+ permission:
+ backend_only: false
+ filter:
+ bodyshop:
+ associations:
+ user:
+ authid:
+ _eq: X-Hasura-User-Id
+ role: user
+ table:
+ name: jobs
+ schema: public
+ type: create_delete_permission
diff --git a/hasura/migrations/metadata.yaml b/hasura/migrations/metadata.yaml
index c3aca2f..f864e4b 100644
--- a/hasura/migrations/metadata.yaml
+++ b/hasura/migrations/metadata.yaml
@@ -44,6 +44,7 @@ tables:
columns:
- accepted_ins_co
- created_at
+ - groups
- id
- shopname
- targets
@@ -58,6 +59,7 @@ tables:
permission:
columns:
- accepted_ins_co
+ - groups
- shopname
- targets
filter:
@@ -277,6 +279,15 @@ tables:
authid:
_eq: X-Hasura-User-Id
check: null
+ delete_permissions:
+ - role: user
+ permission:
+ filter:
+ bodyshop:
+ associations:
+ user:
+ authid:
+ _eq: X-Hasura-User-Id
- table:
schema: public
name: users
diff --git a/src/components/atoms/notifications-toggle/notifications-toggle.atom.jsx b/src/components/atoms/notifications-toggle/notifications-toggle.atom.jsx
new file mode 100644
index 0000000..12ec5a3
--- /dev/null
+++ b/src/components/atoms/notifications-toggle/notifications-toggle.atom.jsx
@@ -0,0 +1,39 @@
+import { Switch } from "antd";
+import React from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import ipcTypes from "../../../ipc.types";
+import { selectSettings } from "../../../redux/application/application.selectors";
+import DataLabel from "../../atoms/data-label/data-label.atom";
+const { ipcRenderer } = window;
+
+const mapStateToProps = createStructuredSelector({
+ //currentUser: selectCurrentUser
+ appSettings: selectSettings,
+});
+const mapDispatchToProps = (dispatch) => ({
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
+});
+
+export function WatcherPollingMolecule({ appSettings }) {
+ const handleChange = (val) => {
+ ipcRenderer.send(ipcTypes.default.store.set, {
+ enableNotifications: val,
+ });
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(WatcherPollingMolecule);
diff --git a/src/components/atoms/part-type-converter/part-type-converter.atom.jsx b/src/components/atoms/part-type-converter/part-type-converter.atom.jsx
index c551e0a..90a1570 100644
--- a/src/components/atoms/part-type-converter/part-type-converter.atom.jsx
+++ b/src/components/atoms/part-type-converter/part-type-converter.atom.jsx
@@ -1,15 +1,20 @@
export default (part_type) => {
switch (part_type) {
case "PAA":
- case "PAL":
- case "PAC":
return "A/M";
case "PAE":
- return "Exist.";
+ return "Ex.";
case "PAN":
- case "PAP":
return "OEM";
+ case "PAP":
+ return "OEMP";
+ case "PAL":
+ return "LKQ";
+ case "PAC":
+ return "A/M (PAC)";
+ case "PAR":
+ return "A/M (PAR)";
default:
return part_type;
}
diff --git a/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx b/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx
index d7d5217..dd40e4d 100644
--- a/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx
+++ b/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx
@@ -12,7 +12,7 @@ export default function TimeAgoFormatter(props) {
}, [m]);
return props.children ? (
-
+
{timestampString}
) : null;
diff --git a/src/components/molecules/job-group/job-group.molecule.jsx b/src/components/molecules/job-group/job-group.molecule.jsx
new file mode 100644
index 0000000..09186e1
--- /dev/null
+++ b/src/components/molecules/job-group/job-group.molecule.jsx
@@ -0,0 +1,53 @@
+import { DownOutlined, LoadingOutlined } from "@ant-design/icons";
+import { useMutation } from "@apollo/client";
+import { Dropdown, Menu, message } from "antd";
+import React, { useState } from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { UPDATE_JOB } from "../../../graphql/jobs.queries";
+import { selectBodyshop } from "../../../redux/user/user.selectors";
+const mapStateToProps = createStructuredSelector({
+ //currentUser: selectCurrentUser
+ bodyshop: selectBodyshop,
+});
+const mapDispatchToProps = (dispatch) => ({
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
+});
+export default connect(mapStateToProps, mapDispatchToProps)(JobGroupMolecule);
+
+export function JobGroupMolecule({ bodyshop, jobId, group }) {
+ const [loading, setLoading] = useState(false);
+ const [updateJob] = useMutation(UPDATE_JOB);
+
+ const handleMenuClick = async (value) => {
+ setLoading(true);
+ const result = await updateJob({
+ variables: { jobId: jobId, job: { group: value.key } },
+ });
+
+ if (!result.errors) {
+ message.success("Close date updated.");
+ } else {
+ message.error("Error updating job.");
+ }
+ setLoading(false);
+ };
+
+ const menu = (
+
+ );
+
+ return (
+
+ e.preventDefault()}>
+ {group}
+
+ {loading && }
+
+
+ );
+}
diff --git a/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx b/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx
index fc9e4cb..1c832d1 100644
--- a/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx
+++ b/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx
@@ -2,8 +2,9 @@ import { Descriptions, PageHeader, Skeleton } from "antd";
import React from "react";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
-import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.atom";
+import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
+import JobGroupMolecule from "../job-group/job-group.molecule";
export default function JobsDetailDescriptionMolecule({ loading, job }) {
if (loading) return ;
@@ -15,11 +16,13 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
{`${job.ownr_fn} ${job.ownr_ln}`}
- {`${job.v_model_yr} ${job.v_makedesc} ${job.v_model}`}
+ {`${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type})`}
{job.clm_total}
- {job.group}
+
+
+
{job.v_age}
a.line_no - b.line_no,
},
{
title: "S#",
dataIndex: "line_ind",
key: "line_ind",
+ sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
},
{
title: "Line Description",
dataIndex: "line_desc",
key: "line_desc",
+ sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
},
{
title: "Part Type",
dataIndex: "part_type",
key: "part_type",
+ sorter: (a, b) => alphaSort(a.part_type, b.part_type),
render: (text, record) => partTypeConverterAtom(text),
},
{
title: "Part Number",
dataIndex: "oem_partno",
key: "oem_partno",
+ sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
},
{
title: "Database Price",
dataIndex: "db_price",
key: "db_price",
-
+ sorter: (a, b) => a.db_price - b.db_price,
render: (text, record) => (
{record.db_price}
),
@@ -49,7 +55,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Actual Price",
dataIndex: "act_price",
key: "act_price",
-
+ sorter: (a, b) => a.act_price - b.act_price,
render: (text, record) => (
{record.act_price}
),
@@ -58,7 +64,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Price Diff.",
dataIndex: "price_diff",
key: "price_diff",
-
+ sorter: (a, b) => a.price_diff - b.price_diff,
render: (text, record) => (
{record.price_diff}
),
@@ -67,7 +73,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Price Diff. %",
dataIndex: "price_diff_pc",
key: "price_diff_pc",
-
+ sorter: (a, b) => a.price_diff_pc - b.price_diff_pc,
render: (text, record) => (
;
if (!job) return ;
-
return (
currentRpsPc ? "tomato" : "seagreen",
+ color: selectedJobTargetPc > (jobRpsPc || 0) ? "tomato" : "seagreen",
}}
- value={(currentRpsPc * 100).toFixed(1)}
+ value={((jobRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
-
+
+
+
+
);
}
diff --git a/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx
index c17b519..98e09f2 100644
--- a/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx
+++ b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx
@@ -55,16 +55,16 @@ export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) {
title="Current RPS %"
valueStyle={{
color:
- scoreCard.currentRpsPc <= scoreCard.targetRpsPc
+ (scoreCard.currentRpsPc || 0) <= (scoreCard.targetRpsPc || 0)
? "tomato"
: "seagreen",
}}
- value={(scoreCard.currentRpsPc * 100).toFixed(1)}
+ value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
diff --git a/src/components/molecules/scan-estimate-list/scan-estimate-list.molecule.jsx b/src/components/molecules/scan-estimate-list/scan-estimate-list.molecule.jsx
index f86f68e..ceef2ad 100644
--- a/src/components/molecules/scan-estimate-list/scan-estimate-list.molecule.jsx
+++ b/src/components/molecules/scan-estimate-list/scan-estimate-list.molecule.jsx
@@ -7,6 +7,7 @@ import {
selectScanEstimates,
selectScanLoading,
} from "../../../redux/scan/scan.selectors";
+import { alphaSort } from "../../../util/sorters";
import LastScannedAtom from "../../atoms/last-scanned/last-scanned.atom";
import ScanRefreshAtom from "../../atoms/scan-refresh/scan-refresh.atom";
@@ -25,21 +26,25 @@ export function ScanEstimateListMolecule({ scanLoading, estimates }) {
title: "Claim No.",
dataIndex: "clm_no",
key: "clm_no",
+ sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
},
{
title: "Ins Co.",
dataIndex: "ins_co_nm",
key: "ins_co_nm",
+ sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
},
{
title: "First Name",
dataIndex: "ownr_fn",
key: "ownr_fn",
+ sorter: (a, b) => alphaSort(a.ownr_fn, b.ownr_fn),
},
{
title: "Last Name",
dataIndex: "ownr_ln",
key: "ownr_ln",
+ sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
},
{
title: "Vehicle",
@@ -48,6 +53,11 @@ export function ScanEstimateListMolecule({ scanLoading, estimates }) {
render: (text, record) =>
`${record.v_model_yr} ${record.v_makedesc} ${record.v_model} (${record.v_type})`,
},
+ {
+ title: "File Path",
+ dataIndex: "filepath",
+ key: "filepath",
+ },
{
title: "Import",
dataIndex: "import",
diff --git a/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx b/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx
index c06355e..0ea61d3 100644
--- a/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx
+++ b/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx
@@ -1,10 +1,18 @@
-import { Button, Input, Form, Select, InputNumber, Typography } from "antd";
-import FormListMoveArrows from "../../atoms/form-list-move-arrows/form-list-move-arrows.atom";
-import React from "react";
-import LayoutFormRow from "../../atoms/layout-form-row/layout-form-row.atom";
import { DeleteFilled } from "@ant-design/icons";
+import { Button, Form, Input, InputNumber, Select, Typography } from "antd";
+import React, { useState } from "react";
+import FormListMoveArrows from "../../atoms/form-list-move-arrows/form-list-move-arrows.atom";
+import LayoutFormRow from "../../atoms/layout-form-row/layout-form-row.atom";
export default function ShopSettingsFormMolecule({ form, saveLoading }) {
+ const [groupOptions, setGroupOptions] = useState(
+ form.getFieldValue("groups") || []
+ );
+ const handleBlur = () => {
+ console.log(form.getFieldValue("groups") || []);
+ setGroupOptions(form.getFieldValue("groups") || []);
+ };
+
return (
Shop Settings
@@ -40,6 +48,19 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) {
>
+
+
+
Group Definitions
@@ -59,7 +80,13 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) {
},
]}
>
-
+
+
diff --git a/src/graphql/bodyshop.queries.js b/src/graphql/bodyshop.queries.js
index d36e62d..8c170af 100644
--- a/src/graphql/bodyshop.queries.js
+++ b/src/graphql/bodyshop.queries.js
@@ -6,9 +6,11 @@ export const QUERY_BODYSHOP = gql`
shopname
targets
accepted_ins_co
+ groups
}
}
`;
+
export const UPDATE_SHOP = gql`
mutation UPDATE_SHOP($id: uuid, $shop: bodyshops_set_input!) {
update_bodyshops(where: { id: { _eq: $id } }, _set: $shop) {
@@ -17,6 +19,7 @@ export const UPDATE_SHOP = gql`
shopname
targets
accepted_ins_co
+ groups
}
}
}
diff --git a/src/graphql/jobs.queries.js b/src/graphql/jobs.queries.js
index fb87aa6..5175feb 100644
--- a/src/graphql/jobs.queries.js
+++ b/src/graphql/jobs.queries.js
@@ -87,6 +87,7 @@ export const QUERY_JOB_BY_PK = gql`
updated_at
group
v_age
+ v_type
loss_date
close_date
updated_at
diff --git a/src/ipc/ipc-estimate-utils.js b/src/ipc/ipc-estimate-utils.js
index 0de15fc..823ed6a 100644
--- a/src/ipc/ipc-estimate-utils.js
+++ b/src/ipc/ipc-estimate-utils.js
@@ -89,7 +89,10 @@ export const GetSupplementDelta = async (jobId, existingLinesO, newLines) => {
//Found a relevant matching line. Add it to lines to update.
linesToUpdate.push({
id: existingLines[matchingIndex].id,
- newData: newLine,
+ newData: {
+ ...newLine,
+ ignore: existingLines[matchingIndex].ignore,
+ },
});
//Splice out item we found for performance.
existingLines.splice(matchingIndex, 1);
diff --git a/src/redux/application/application.reducer.js b/src/redux/application/application.reducer.js
index 0814540..4cb31fe 100644
--- a/src/redux/application/application.reducer.js
+++ b/src/redux/application/application.reducer.js
@@ -4,7 +4,7 @@ const INITIAL_STATE = {
watchedPaths: [],
watcherError: null,
selectedJobId: null,
- selectedJobTargetPc: 100,
+ selectedJobTargetPc: 0,
settings: {},
};
diff --git a/src/util/sorters.js b/src/util/sorters.js
new file mode 100644
index 0000000..1139ee7
--- /dev/null
+++ b/src/util/sorters.js
@@ -0,0 +1,12 @@
+export function alphaSort(a, b) {
+ let A;
+ let B;
+
+ A = a ? a.toLowerCase() : "";
+ B = b ? b.toLowerCase() : "";
+ if (A < B)
+ //sort string ascending
+ return -1;
+ if (A > B) return 1;
+ return 0; //default return value (no sorting)
+}