Skip to content

Commit

Permalink
adds currency support with overriding of currency type per part. (sti…
Browse files Browse the repository at this point in the history
…ll needs user settings ability)
  • Loading branch information
replaysMike committed Apr 8, 2023
1 parent ad8b419 commit 9e64a23
Show file tree
Hide file tree
Showing 26 changed files with 2,590 additions and 148 deletions.
2 changes: 1 addition & 1 deletion Binner/Binner.Web/Binner.Web.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<LibraryVersion Condition="'$(LibraryVersion)'==''">1.0.36</LibraryVersion>
<LibraryVersion Condition="'$(LibraryVersion)'==''">1.0.37</LibraryVersion>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
<RuntimeIdentifiers>win10-x64;linux-arm;linux-arm64;linux-x64;osx.10.12-x64;ubuntu.14.04-x64</RuntimeIdentifiers>
Expand Down
520 changes: 501 additions & 19 deletions Binner/Binner.Web/ClientApp/public/locales/de/translation.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@
"displayAll": "Displays all parts including parts not associated with a PCB.",
"unassociatedPartName": "Edit the name of your unassociated <1 /> part",
"bomQuantity": "Edit the <1 />BOM quantity required",
"bomCost": "Edit the part cost",
"quantityAvailable": "Edit the quantity available in your <1 />inventory",
"editReferenceId": "Edit your custom <1 />reference Id(s) you can use for identifying this part.",
"editSchematicReferenceId": "Edit your custom <1 />schematic reference Id(s) that identify the part on the PCB silkscreen. <3 />Examples: <5>D1</5>, <7>C2</7>, <9>Q1</9>",
Expand Down
595 changes: 594 additions & 1 deletion Binner/Binner.Web/ClientApp/public/locales/es/translation.json

Large diffs are not rendered by default.

595 changes: 594 additions & 1 deletion Binner/Binner.Web/ClientApp/public/locales/fr/translation.json

Large diffs are not rendered by default.

595 changes: 594 additions & 1 deletion Binner/Binner.Web/ClientApp/public/locales/zh/translation.json

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions Binner/Binner.Web/ClientApp/src/common/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,22 @@ export const decodeResistance = (str) => {
* @returns formatted number string
*/
export const formatCurrency = (number, currency = 'USD', maxDecimals = 5) => {
const lang = getLocaleLanguage();
return number.toLocaleString(lang, { style: "currency", currency: currency, maximumFractionDigits: maxDecimals });
if (!number || typeof number !== "number") number = 0;
const locale = getLocaleLanguage();
return number.toLocaleString(locale, { style: "currency", currency: currency, maximumFractionDigits: maxDecimals });
};

/**
* Get the currency symbol
* @param {string} currency Currency to use, default: 'USD'
* @returns formatted number string
*/
export const getCurrencySymbol = (currency = 'USD') => {
const locale = getLocaleLanguage();
return (0).toLocaleString(locale, { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: 0 }).replace(/\d/g, '').trim();
};


/**
* Get the locale language of the browser
* @returns language string
Expand All @@ -68,7 +80,6 @@ export const getLocaleLanguage = () => {
* @returns formatted number string
*/
export const formatNumber = (number) => {
// return number.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",");
return number.toLocaleString();
};

Expand Down
13 changes: 13 additions & 0 deletions Binner/Binner.Web/ClientApp/src/common/currency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getCurrencySymbol } from "./Utils";

export const Currencies = [
{ key: 0, text: getCurrencySymbol("USD"), value: "USD", description: "US Dollar" },
{ key: 1, text: getCurrencySymbol("EUR"), value: "EUR", description: "Euro" },
{ key: 2, text: getCurrencySymbol("CAD"), value: "CAD", description: "Canadian dollar" },
{ key: 3, text: getCurrencySymbol("JPY"), value: "JPY", description: "Japanese yen" },
{ key: 4, text: getCurrencySymbol("GBP"), value: "GBP", description: "Pound sterling" },
{ key: 5, text: getCurrencySymbol("AUD"), value: "AUD", description: "Australian dollar" },
{ key: 6, text: getCurrencySymbol("CAD"), value: "CNY", description: "Renminbi" },
{ key: 7, text: getCurrencySymbol("KRW"), value: "KRW", description: "South Korean won" },
{ key: 8, text: getCurrencySymbol("MXN"), value: "MXN", description: "Mexican peso" }
];
3 changes: 2 additions & 1 deletion Binner/Binner.Web/ClientApp/src/components/PartsGrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { fetchApi } from '../common/fetchApi';
import { AppEvents, Events } from "../common/events";
import { formatCurrency } from "../common/Utils";
import "./PartsGrid.css";

const AppMedia = createMedia({
Expand Down Expand Up @@ -309,7 +310,7 @@ export default function PartsGrid(props) {
{visitable ? (renderChildren ? <Link to={`/inventory?by=binNumber2&value=${p.binNumber2}`} onClick={handleSelfLink}>{p.binNumber2}</Link> : null) : <span>{p.binNumber2}</span>}
</Table.Cell>)}}</Media> }
{col.cost && <Media greaterThan="computer">{(className, renderChildren) => { return (<Table.Cell className={className}>
{renderChildren ? "$" + p.cost.toFixed(2) : null}
{renderChildren ? formatCurrency(p.cost, p.currency || "USD") : null}
</Table.Cell>)}}</Media> }
{col.digikeypartnumber && <Media greaterThan="computer">{(className, renderChildren) => { return (<Table.Cell className={className}>
{renderChildren ? <span className='truncate'>{p.digiKeyPartNumber}</span> : null}
Expand Down
21 changes: 19 additions & 2 deletions Binner/Binner.Web/ClientApp/src/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,21 @@ code {
font-size: 0.8em;
float: right;
position: absolute;
top: 2px;
right: 4px;
z-index: 1000;

}

.header .language-selection .ui.selection.dropdown {
padding: 0.5em 0.5em;
min-width: 100px;
width: 100px;
min-height: 2em;
padding: 0.5em 2.1em 0.5em 1em;
}

.header .language-selection .ui.selection.dropdown .icon {
margin: -1em;
}

.header .language-selection .ui.selection.dropdown .menu {
Expand Down Expand Up @@ -305,8 +312,18 @@ section.formHeader {
cursor: pointer;
}

.ui.input.labeled.inline-editable .ui.label{
background-color: #fff;
padding: 2px 1px;
margin-right: 1px;
}

.ui.input.inline-editable>input {
padding: 4px !important;
padding: 0 2px !important;
}

.ui.input.labeled.inline-editable>input {
padding: 0 0 0 1px !important;
}

.ui.input.inline-editable>input:hover {
Expand Down
10 changes: 9 additions & 1 deletion Binner/Binner.Web/ClientApp/src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

const detectionOptions = {
order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag', 'path', 'subdomain'],
lookupQuerystring: 'lng',
caches: ['localStorage'],
};

i18n
// i18next-http-backend
// loads translations from your server
Expand All @@ -25,7 +31,9 @@ i18n
},
backend: {
loadPath: '/locales/{{lng}}/translation.json',
}
},
detection: detectionOptions,
saveMissing: true
});

export default i18n;
25 changes: 14 additions & 11 deletions Binner/Binner.Web/ClientApp/src/layouts/Header.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { NavMenu } from "./NavMenu";
import { Dropdown, Icon } from "semantic-ui-react";
import { Dropdown, Icon, Flag } from "semantic-ui-react";

// import i18n
import '../i18n';

const lngs = {
en: { nativeName: 'English' }, // english
de: { nativeName: 'Deutsch' }, // german
fr: { nativeName: 'Français ' }, // french
es: { nativeName: 'Español' }, // spanish
zh: { nativeName: '中文' }, // chinese
en: { nativeName: 'English', flag: 'gb' }, // english
// temporary: enable languages as the translations are finished.
//de: { nativeName: 'Deutsch', flag: 'de' }, // german
//fr: { nativeName: 'Français ', flag: 'fr' }, // french
//es: { nativeName: 'Español', flag: 'mx' }, // spanish
//zh: { nativeName: '中文', flag: 'cn' }, // chinese
};

export function Header() {
const { t, i18n } = useTranslation();
const [language, setLanguage] = useState(localStorage.getItem('language') || i18n.resolvedLanguage || 'en');
const { i18n } = useTranslation();
// console.log('resolved langauge', i18n.resolvedLanguage);
const [language, setLanguage] = useState(i18n.resolvedLanguage || 'en');

useEffect(() => {
// console.log('init', localStorage.getItem('language'), i18n.resolvedLanguage);
setLanguage(localStorage.getItem('language') || i18n.resolvedLanguage || 'en');
setLanguage(i18n.resolvedLanguage || 'en');
}, []);

const langOptions = Object.keys(lngs).map((lng, key) => ({
key,
text: lngs[lng].nativeName,
value: lng,
content: <span><Flag name={lngs[lng].flag} />{lngs[lng].nativeName}</span>
}));

const handleChange = (e, control) => {
e.preventDefault();
setLanguage(control.value);
localStorage.setItem('language', control.value);
// localStorage.setItem('i18nextLng', control.value);
i18n.changeLanguage(control.value);
};

Expand All @@ -42,6 +44,7 @@ export function Header() {
<Icon name="world" />
<Dropdown
selection
inline
value={language || 'en'}
options={langOptions}
onChange={handleChange}
Expand Down
53 changes: 42 additions & 11 deletions Binner/Binner.Web/ClientApp/src/pages/Bom.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import _ from "underscore";
import { fetchApi } from "../common/fetchApi";
import { ProjectColors } from "../common/Types";
import { toast } from "react-toastify";
import { formatCurrency } from "../common/Utils";
import { formatCurrency, formatNumber, getCurrencySymbol } from "../common/Utils";
import {format, parseJSON} from "date-fns";
import { AddBomPartModal } from "../components/AddBomPartModal";
import { AddPcbModal } from "../components/AddPcbModal";
Expand Down Expand Up @@ -231,14 +231,16 @@ export function Bom(props) {
const savePartInlineChange = async (bomPart) => {
if (!isDirty) return;
setLoading(true);
const request = { ...bomPart,
const request = { ...bomPart,
cost: parseFloat(bomPart.cost) || 0,
quantity: parseInt(bomPart.quantity) || 0,
quantityAvailable: bomPart.part ? 0 : (parseInt(bomPart.quantityAvailable) || 0),
// conditionally add the part if it's available
...(bomPart.part &&
{part: {
...bomPart.part,
quantity: parseInt(bomPart.part.quantity) || 0
cost: parseFloat(bomPart.part.cost) || 0,
quantity: parseInt(bomPart.part.quantity) || 0,
}
})
};
Expand Down Expand Up @@ -266,21 +268,28 @@ export function Bom(props) {
case "quantity":
parsed = parseInt(control.value);
if (!isNaN(parsed)) {
part[control.name] = parsed;
part.quantity = parsed;
}
break;
case "quantityAvailable":
parsed = parseInt(control.value);
if (!isNaN(parsed)) {
// special case, if editing a part change its quantity.
/// if no part is associated, set the quantityAvailable
/// if no part is associated, set the custom quantityAvailable
if (part.part) {
part.part['quantity'] = parsed;
part.part.quantity = parsed;
} else {
part[control.name] = parsed;
part.quantityAvailable = parsed;
}
}
break;
case "cost":
if (part.part) {
part.part.cost = control.value;
} else {
part.cost = control.value;
}
break;
default:
part[control.name] = control.value;
break;
Expand Down Expand Up @@ -543,7 +552,7 @@ export function Bom(props) {
</p>}
trigger={<span>{t('label.pcb', "PCB")}</span>} />
</Table.HeaderCell>}
<Table.HeaderCell width={3} sorted={column === "partNumber" ? direction : null} onClick={handleSort("partNumber")}>
<Table.HeaderCell width={2} sorted={column === "partNumber" ? direction : null} onClick={handleSort("partNumber")}>
{t('label.partNumber', "Part Number")}
</Table.HeaderCell>
<Table.HeaderCell width={2} sorted={column === "manufacturerPartNumber" ? direction : null} onClick={handleSort("manufacturerPartNumber")}>
Expand All @@ -552,13 +561,13 @@ export function Bom(props) {
<Table.HeaderCell style={{ width: "100px" }} sorted={column === "partType" ? direction : null} onClick={handleSort("partType")}>
{t('label.partType', "Part Type")}
</Table.HeaderCell>
<Table.HeaderCell sorted={column === "cost" ? direction : null} onClick={handleSort("cost")}>
<Table.HeaderCell style={{width: '102px'}} sorted={column === "cost" ? direction : null} onClick={handleSort("cost")}>
{t('label.cost', "Cost")}
</Table.HeaderCell>
<Table.HeaderCell sorted={column === "quantity" ? direction : null} onClick={handleSort("quantity")}>
<Popup mouseEnterDelay={PopupDelayMs} content={<p>{t('popup.bom.quantity', "The quantity of parts required for a single PCB")}</p>} trigger={<span>{t('label.quantity', "Quantity")}</span>} />
</Table.HeaderCell>
<Table.HeaderCell sorted={column === "stock" ? direction : null} onClick={handleSort("stock")}>
<Table.HeaderCell style={{width: '90px'}} sorted={column === "stock" ? direction : null} onClick={handleSort("stock")}>
<Popup mouseEnterDelay={PopupDelayMs} content={<p>{t('popup.bom.inventoryQuantity', "The quantity of parts currently in inventory")}</p>} trigger={<span>{t('label.inStock', "In Stock")}</span>} />
</Table.HeaderCell>
<Table.HeaderCell sorted={column === "leadTime" ? direction : null} onClick={handleSort("leadTime")}>
Expand Down Expand Up @@ -632,7 +641,29 @@ export function Bom(props) {
</Table.Cell>
<Table.Cell>{bomPart.part?.manufacturerPartNumber && <><Clipboard text={bomPart.part?.manufacturerPartNumber} /> {bomPart.part?.manufacturerPartNumber}</>}</Table.Cell>
<Table.Cell>{bomPart.part?.partType}</Table.Cell>
<Table.Cell>{formatCurrency(bomPart.part?.cost || 0)}</Table.Cell>
<Table.Cell>
<Popup
wide
mouseEnterDelay={PopupDelayMs}
content={
<p>
<Trans i18nKey='popup.bom.bomCost'>
Edit the part cost
</Trans>
</p>}
trigger={<Input
label={getCurrencySymbol(bomPart.part?.currency || bomPart.currency || "USD")}
type="text"
transparent
name="cost"
onBlur={(e) => saveColumn(e, bomPart)}
onChange={(e, control) => handlePartsInlineChange(e, control, bomPart)}
value={bomPart.part?.cost || bomPart.cost || 0}
fluid
className="inline-editable"
/>}
/>
</Table.Cell>
<Table.Cell>
<Popup
wide
Expand Down
25 changes: 17 additions & 8 deletions Binner/Binner.Web/ClientApp/src/pages/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Statistic, Segment, Icon } from "semantic-ui-react";
import { fetchApi } from "../common/fetchApi";
import { formatCurrency } from "../common/Utils";
import { VersionBanner } from "../components/VersionBanner";
import semver from "semver";
import customEvents from '../common/customEvents';
Expand All @@ -16,25 +17,29 @@ export function Home(props) {
const [subscription, setSubscription] = useState(null);
const [version, setVersion] = useState(null);

const getLatestVersion = useCallback(async (installedVersionData) => {
/**
* Fetch the latest Binner version from GitHub
*/
const getLatestVersion = useCallback(async (installedVersion) => {
const response = await fetch("system/version");
if (response.ok) {
const latestVersionData = await response.json();
setVersionData(latestVersionData);
const skipVersion = localStorage.getItem("skipVersion") || "1.0.0";
if (semver.gt(latestVersionData.version, installedVersionData.version) && semver.gt(latestVersionData.version, skipVersion)) {
if (semver.gt(latestVersionData.version, installedVersion) && semver.gt(latestVersionData.version, skipVersion)) {
// new version is available, and hasn't been skipped
setNewVersionBannerIsOpen(true);
}
}
}, []);

/**
* When we receive a version header from an api request, update it.
* This callback will be called on all updates to version
*/
const updateVersion = useCallback((installedVersionData, subscriptionId) => {
setVersion(installedVersionData.version);

getLatestVersion(installedVersionData);

}, [getLatestVersion]);
}, []);

useEffect(() => {
setSubscription(customEvents.subscribe("version", (data, subscriptionId) => updateVersion(data, subscriptionId)));
Expand Down Expand Up @@ -66,6 +71,11 @@ export function Home(props) {
load();
}, [setLoading, setSummary]);

useEffect(() => {
// if we know the current version, fetch the latest version
if (version) getLatestVersion(version);
}, [version, getLatestVersion]);

const route = (e, url) => {
e.preventDefault();
e.stopPropagation();
Expand Down Expand Up @@ -169,8 +179,7 @@ export function Home(props) {
</Statistic>
<Statistic color="green" inverted>
<Statistic.Value>
<Icon name="dollar" />
{(summary.partsCost || 0).toFixed(2)}
{formatCurrency(summary.partsCost, summary.currency, 2)}
</Statistic.Value>
<Statistic.Label>{t('page.home.value', "Value")}</Statistic.Label>
</Statistic>
Expand Down
Loading

0 comments on commit 9e64a23

Please sign in to comment.