diff options
author | Nic Eigel <nic@eigel.ch> | 2024-01-14 15:18:12 +0100 |
---|---|---|
committer | Nic Eigel <nic@eigel.ch> | 2024-01-14 15:18:12 +0100 |
commit | 7a201c3b885c5d23bf0fd0f3da32379a49b30c38 (patch) | |
tree | 13f35c4761087b0e6adce39153be5ca03c5c846b /packages/auditor-backoffice-ui | |
parent | 2be9142ac5f944fbc03186b22ca67e6020187c92 (diff) |
adding auditor-backoffice-ui
Diffstat (limited to 'packages/auditor-backoffice-ui')
282 files changed, 79531 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/.gitignore b/packages/auditor-backoffice-ui/.gitignore new file mode 100644 index 000000000..df149101c --- /dev/null +++ b/packages/auditor-backoffice-ui/.gitignore @@ -0,0 +1,6 @@ +/build +/size-plugin.json +/storybook-static +/docs +/single +/coverage diff --git a/packages/auditor-backoffice-ui/DESIGN.md b/packages/auditor-backoffice-ui/DESIGN.md new file mode 100644 index 000000000..d6252ccdc --- /dev/null +++ b/packages/auditor-backoffice-ui/DESIGN.md @@ -0,0 +1,195 @@ +# Page internal routing + +* The SPA is loaded from the BACKOFFICE_URL + +* The view to be rendered is decided by the URL fragment + +* Query parameters that may affect routing + + - instance: use from the default instance to mimic another instance management + +* The user must provide BACKEND_URL or BACKOFFICE_URL will use as default + +* Token for querying the backend will be saved in localStorage under + backend-token-${name} + +# HTTP queries to the backend + +HTTP queries will have 4 states: + +* loading: request did not end yet. data and error are undefined + +* ok: data has information, http response status == 200 + +* clientError: http response status is between 400 and 499 + + - notfound: http status 404 + + - unauthorized: http status 401 + +* serverError: http response status is grater than 500 + +There are categories of queries: + + * sync: getting information for the page rendering + + * async: performing an CRUD operation + +## Loading the page information (sync) + +In this scenario, a failed request will make the app flow to break. + +When receiving an not found error a generic not found page will be shown. If the +BACKEND_URL points to a default instance it should send the user to create the +instance. + +When receiving an unauthorized error, the user should be prompted with a login form. + +When receiving an another error (400 < http status < 600), the login form should +be shown with an error message using the hint from the backend. + +On other unexpected error (like network error), the login form should be shown +with an error message. + +## CRUD operation (async) + +In this scenario, a failed request does not break the flow but a message will be +prompted. + +# Forms + +All the input components should be placed in the folder `src/components/from`. + +The core concepts are: + + * <FormProvider<T> /> places instead of <form /> it should be mapped to an + object of type T + + * <Input /> an others: defines UI, create <input /> DOM controls and access the + form with useField() + +To use it you will need a state somewhere with the object holding all the form +information. + +``` +const [state, setState] = useState({ name: '', age: 11 }) +``` + +Optionally an error object an be built with the error messages + +``` +const errors = { + field1: undefined, + field2: 'should be greater than 18', +} +``` + +These 3 elements are used to setup the FormProvider + +``` +<FormProvider errors={errors} object={state} valueHandler={setState}> +...inputs +</FormProvider> +``` + +Inputs should handle UI rendering and use `useField(name)` to get: + + * error: the field has been modified and the value is not correct + * required: the field need to be corrected + * value: the current value of the object + * initial: original value before change + * onChange: function to update the current field + +Also, every input must be ready to receive these properties + + * name: property of the form object being manipulated + * label: how the name of the property will be shown in the UI + * placeholder: optional, inplace text when there is no value yet + * readonly: default to false, will prevent change the value + * help: optional, example text below the input text to help the user + * tooltip: optional, will add a (i) with a popup to describe the field + + +# Custom Hooks + +All the general purpose hooks should be placed in folder `src/hooks` and tests +under `tests/hooks`. Starts with the `use` word. + +# Contexts + +All the contexts should be placed in the folder `src/context` as a function. +Should expose provider as a component `<XxxContextProvider />` and consumer as a +hook function `useXxxContext()` (where XXX is the name) + +# Components + +Type of components: + + * main entry point: src/index.tsx, mostly initialization + + * routing: in the `src` folder, deciding who is going to take the work. That's + when the page is loading but also create navigation handlers + + * pages: in the `paths` folder, setup page information (like querying the + backend for the list of things), handlers for CRUD events, delegated routing + to parent and UI to children. + +Some other guidelines: + + * Hooks over classes are preferred + + * Components that are ready to be reused on any place should be in + `src/components` folder + + * Since one of the build targets is a single bundle with all the pages, we are + avoiding route based code splitting + https://github.com/preactjs/preact-cli#route-based-code-splitting + + +# Testing + +Every components should have examples using storybook (xxx.stories.tsx). There +is an automated test that check that every example can be rendered so we make +sure that we do not add a regression. + +Every hook should have examples under `tests/hooks` with common usage trying to +follow this structure: + + * (Given) set some context of the initial condition + + * (When) some action to be tested. May be the initialization of a hook or an + action associated with it + + * (Then) a particular set of observable consequences should be expected + +# Accessibility + +Pages and components should be built with accessibility in mind. + +https://github.com/nickcolley/jest-axe +https://orkhanhuseyn.medium.com/accessibility-testing-in-react-with-jest-axe-e08c2a3f3289 +http://accesibilidadweb.dlsi.ua.es/?menu=jaws +https://webaim.org/projects/screenreadersurvey8/#intro +https://www.gov.uk/service-manual/technology/testing-with-assistive-technologies#how-to-test +https://es.reactjs.org/docs/accessibility.html + +# Internationalization + +Every non translated message should be written in English and wrapped into: + + * i18n function from useTranslator() hook + * <Translate /> component + +Makefile has a i18n that will parse source files and update the po template. +When *.po are updated, running the i18n target will create the strings.ts that +the application will use in runtime. + +# Documentation Conventions + +* labels + * begin w/ a capital letter + * acronyms (e.g., "URL") are upper case +* tooltips + * begin w/ a lower case letter + * do not end w/ punctuation (period) + * avoid leading article ("a", "an", "the") diff --git a/packages/auditor-backoffice-ui/Makefile b/packages/auditor-backoffice-ui/Makefile new file mode 100644 index 000000000..57b3e0cb5 --- /dev/null +++ b/packages/auditor-backoffice-ui/Makefile @@ -0,0 +1,35 @@ +# This Makefile has been placed in the public domain + +ifeq ($(TOPLEVEL), yes) + $(info top-level build) + -include ../../.config.mk + override DESTDIR := $(TOP_DESTDIR) +else + $(info package-level build) + -include ../../.config.mk + -include .config.mk +endif + +$(info prefix is $(prefix)) + +.PHONY: all +all: + @echo run \'make install\' to install + +spa_dir=$(DESTDIR)$(prefix)/share/taler/auditor-backoffice + +.PHONY: deps +deps: + pnpm install --frozen-lockfile --filter @gnu-taler/auditor-backoffice... + pnpm run build + +.PHONY: install-nodeps +install-nodeps: + (cd dist/prod && find . -type f -exec install -D "{}" "$(spa_dir)/{}" \;) + + +.PHONY: install +install: + $(MAKE) deps + $(MAKE) install-nodeps + diff --git a/packages/auditor-backoffice-ui/README.md b/packages/auditor-backoffice-ui/README.md new file mode 100644 index 000000000..b10fa6a94 --- /dev/null +++ b/packages/auditor-backoffice-ui/README.md @@ -0,0 +1,64 @@ +## AUditor Admin Frontend + +Auditor Admin Frontend is a Single Page Application (SPA) that connects with a running Auditor Backend and lets you audit the exchange. + +## System requirements + +- Node: v16.15.0 +- pnpm: 7.14.2 +- make + +## Compiling from source + +Run `pnpm install --frozen-lockfile --filter @gnu-taler/auditor-backoffice...` to install all the nodejs dependencies. + +Then the command `pnpm build` create the distribution in the `dist` folder. + +By default the installation prefix will be `/usr/local/share/taler/auditor-backoffice/` but it can be overridden by `--prefix` in the configuration process: + +```shell +./configure --prefix=/another/directory +``` + +To install run `make install` + +## Running develop + +To run a development server run: + +```shell +./dev.mjs +``` + +This should start a watch process that will reload the server every time that a file is saved. + +The application need to connect to a auditor-backend properly configured to run. + +## Building for deploy + +To build and deploy the SPA in your local server run the install script: + +```shell +make install +``` + +## Runtime dependencies + +* preact: Fast 3kB alternative to React with the same modern API + +* preact-router: URL component router for Preact + +* SWR: React Hooks library for data fetching (stale-while-revalidate) + +* Yup: schema builder for value parsing and validation (to be deprecated) + +* Date-fns: library for manipulating javascript date + +* qrcode-generator: simplest qr implementation based on JIS X 0510:1999 + +* @gnu-taler/taler-util: types and tooling + +* history: manage the history stack, navigate, and persist state between sessions + +* jed: gettext like library for internationalization + diff --git a/packages/auditor-backoffice-ui/build.mjs b/packages/auditor-backoffice-ui/build.mjs new file mode 100755 index 000000000..b6d6e5127 --- /dev/null +++ b/packages/auditor-backoffice-ui/build.mjs @@ -0,0 +1,28 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { build } from "@gnu-taler/web-util/build"; + +await build({ + type: "production", + source: { + js: ["src/index.tsx"], + assets: [{base:"src",files:["src/index.html"]}], + }, + destination: "./dist/prod", + css: "sass", +}); diff --git a/packages/auditor-backoffice-ui/contrib/po2ts b/packages/auditor-backoffice-ui/contrib/po2ts new file mode 100755 index 000000000..d32e922ba --- /dev/null +++ b/packages/auditor-backoffice-ui/contrib/po2ts @@ -0,0 +1,42 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Convert a <lang>.po file into a JavaScript / TypeScript expression. + */ + +const po2json = require("po2json"); + +const filename = process.argv[2]; + +if (!filename) { + console.error("error: missing filename"); + process.exit(1); +} + +const m = filename.match(/([a-zA-Z0-9-_]+).po/); + +if (!m) { + console.error("error: unexpected filename (expected <lang>.po)"); + process.exit(1); +} + +const lang = m[1]; +const pojson = po2json.parseFileSync(filename, { format: "jed1.x", fuzzy: true }); +const s = + "strings['" + lang + "'] = " + JSON.stringify(pojson, null, " ") + ";\n"; +console.log(s); diff --git a/packages/auditor-backoffice-ui/copyleft-header.js b/packages/auditor-backoffice-ui/copyleft-header.js new file mode 100644 index 000000000..2589fdc92 --- /dev/null +++ b/packages/auditor-backoffice-ui/copyleft-header.js @@ -0,0 +1,15 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ diff --git a/packages/auditor-backoffice-ui/dev.mjs b/packages/auditor-backoffice-ui/dev.mjs new file mode 100755 index 000000000..14d5737de --- /dev/null +++ b/packages/auditor-backoffice-ui/dev.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { serve } from "@gnu-taler/web-util/node"; +import { initializeDev } from "@gnu-taler/web-util/build"; + +const devEntryPoints = ["src/stories.tsx", "src/index.tsx"]; + +const build = initializeDev({ + type: "development", + source: { + js: devEntryPoints, + assets: [{base:"src",files:["src/index.html"]}], + }, + css: "sass", + destination: "./dist/dev", +}); + +await build(); + +serve({ + folder: "./dist/dev", + port: 8080, + source: "./src", + onSourceUpdate: build, +}); diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json new file mode 100644 index 000000000..1fe7332c0 --- /dev/null +++ b/packages/auditor-backoffice-ui/package.json @@ -0,0 +1,83 @@ +{ + "private": true, + "name": "@gnu-taler/auditor-backoffice-ui", + "version": "0.9.3-dev.27", + "license": "AGPL-3.0-or-later", + "type": "module", + "scripts": { + "build": "./build.mjs", + "check": "tsc", + "compile": "tsc && ./build.mjs", + "dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm", + "test": "./test.mjs && mocha --require source-map-support/register 'dist/**/*.test.js' 'dist/**/test.js'", + "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", + "i18n:extract": "pogen extract", + "i18n:merge": "pogen merge", + "i18n:emit": "pogen emit", + "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit", + "typedoc": "typedoc --out dist/typedoc ./src/", + "pretty": "prettier --write src" + }, + "eslintConfig": { + "plugins": [ + "header" + ], + "rules": { + "header/header": [ + 2, + "copyleft-header.js" + ] + }, + "extends": [ + "prettier" + ] + }, + "dependencies": { + "@gnu-taler/taler-util": "workspace:*", + "@gnu-taler/web-util": "workspace:*", + "date-fns": "2.29.3", + "history": "4.10.1", + "jed": "1.1.1", + "preact": "10.11.3", + "preact-router": "3.2.1", + "qrcode-generator": "1.4.4", + "swr": "2.2.2", + "yup": "^0.32.9" + }, + "devDependencies": { + "@creativebulma/bulma-tooltip": "^1.2.0", + "@gnu-taler/pogen": "^0.0.5", + "@types/chai": "^4.3.0", + "@types/history": "^4.7.8", + "@types/mocha": "^8.2.3", + "@types/node": "^18.11.17", + "@typescript-eslint/eslint-plugin": "^4.22.0", + "@typescript-eslint/parser": "^4.22.0", + "base64-inline-loader": "^1.1.1", + "bulma": "^0.9.2", + "bulma-checkbox": "^1.1.1", + "bulma-radio": "^1.1.1", + "bulma-responsive-tables": "^1.2.3", + "bulma-switch-control": "^1.1.1", + "bulma-timeline": "^3.0.4", + "bulma-upload-control": "^1.2.0", + "chai": "^4.3.6", + "dotenv": "^8.2.0", + "eslint": "^7.25.0", + "eslint-config-preact": "^1.1.4", + "eslint-plugin-header": "^3.1.1", + "html-webpack-inline-chunk-plugin": "^1.1.1", + "html-webpack-inline-source-plugin": "0.0.10", + "html-webpack-skip-assets-plugin": "^1.0.1", + "inline-chunk-html-plugin": "^1.1.1", + "mocha": "^9.2.0", + "preact-render-to-string": "^5.2.6", + "sass": "1.56.1", + "source-map-support": "^0.5.21", + "typedoc": "^0.25.4", + "typescript": "5.3.3" + }, + "pogen": { + "domain": "taler-auditor-backoffice" + } +} diff --git a/packages/auditor-backoffice-ui/preact.config.js b/packages/auditor-backoffice-ui/preact.config.js new file mode 100644 index 000000000..9b65d3ec7 --- /dev/null +++ b/packages/auditor-backoffice-ui/preact.config.js @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { DefinePlugin } from 'webpack'; + +import pack from './package.json'; +import * as cp from 'child_process'; + +const commitHash = cp.execSync('git rev-parse --short HEAD').toString(); + +export default { + webpack(config, env, helpers) { + // ensure that process.env will not be undefined on runtime + config.node.process = 'mock' + + // add __VERSION__ to be use in the html + config.plugins.push( + new DefinePlugin({ + 'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`) , + }), + ); + + // suddenly getting out of memory error from build process, error below [1] + // FIXME: remove preact-cli, use rollup + let { index } = helpers.getPluginsByName(config, 'WebpackFixStyleOnlyEntriesPlugin')[0] + config.plugins.splice(index, 1) + } +} + + + +/* [1] from this error decided to remove plugin 'webpack-fix-style-only-entries + leaving this error for future reference + + +<--- Last few GCs ---> + +[32479:0x2e01870] 19969 ms: Mark-sweep 1869.4 (1950.2) -> 1443.1 (1504.1) MB, 497.5 / 0.0 ms (average mu = 0.631, current mu = 0.455) allocation failure scavenge might not succeed +[32479:0x2e01870] 21907 ms: Mark-sweep 2016.9 (2077.9) -> 1628.6 (1681.4) MB, 1596.0 / 0.0 ms (average mu = 0.354, current mu = 0.176) allocation failure scavenge might not succeed + +<--- JS stacktrace ---> + +==== JS stack trace ========================================= + + 0: ExitFrame [pc: 0x13cf099] +Security context: 0x2f4ca66c08d1 <JSObject> + 1: /* anonymous * / [0x35d05555b4b9] [...path/merchant-backoffice/node_modules/.pnpm/webpack-fix-style-only-entries@0.5.2/node_modules/webpack-fix-style-only-entries/index.js:~80] [pc=0x2145e699d1a4](this=0x1149465410e9 <GlobalObject Object map = 0xff481b5b5f9>,0x047e52e36a49 <Dependency map = 0x1ed1fe41cd19>) + 2: arguments adaptor frame: 3... + +FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory + +*/
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/preact.single-config.js b/packages/auditor-backoffice-ui/preact.single-config.js new file mode 100644 index 000000000..849269d6e --- /dev/null +++ b/packages/auditor-backoffice-ui/preact.single-config.js @@ -0,0 +1,62 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import defaultConfig from './preact.config' + +export default { + webpack(config, env, helpers, options) { + defaultConfig.webpack(config, env, helpers, options) + + //1. check no file is under /routers or /component/{routers,async} to prevent async components + // https://github.com/preactjs/preact-cli#route-based-code-splitting + + //2. remove devtools to prevent sourcemaps + config.devtool = false + + //3. change assetLoader to load assets inline + const loaders = helpers.getLoaders(config) + const assetsLoader = loaders.find(lo => lo.rule.test.test('something.woff')) + if (assetsLoader) { + assetsLoader.rule.use = 'base64-inline-loader' + assetsLoader.rule.loader = undefined + } + + //4. remove critters + //critters remove the css bundle from htmlWebpackPlugin.files.css + //for now, pushing all the content into the html is enough + const crittersWrapper = helpers.getPluginsByName(config, 'Critters') + if (crittersWrapper && crittersWrapper.length > 0) { + const [{ index }] = crittersWrapper + config.plugins.splice(index, 1) + } + + //5. remove favicon from src/assets + + //6. remove performance hints since we now that this is going to be big + if (config.performance) { + config.performance.hints = false + } + + //7. template.html should have a favicon and add js/css content + + //last, after building remove the mysterious link to stylesheet with remove-link-stylesheet.sh + } +} diff --git a/packages/auditor-backoffice-ui/remove-link-stylesheet.sh b/packages/auditor-backoffice-ui/remove-link-stylesheet.sh new file mode 100644 index 000000000..fdf8f241c --- /dev/null +++ b/packages/auditor-backoffice-ui/remove-link-stylesheet.sh @@ -0,0 +1,8 @@ +# This script has been placed in the public domain. + +FILE=$(ls single/bundle.*.css) +BUNDLE=${FILE#single} +grep -q '<link href="'$BUNDLE'" rel="stylesheet">' single/index.html || { echo bundle $BUNDLE not found in index.html; exit 1; } +echo -n Removing link from index.html ... +sed 's_<link href="'$BUNDLE'" rel="stylesheet">__' -i single/index.html +echo done diff --git a/packages/auditor-backoffice-ui/src/AdminRoutes.tsx b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx new file mode 100644 index 000000000..91dec09b0 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx @@ -0,0 +1,53 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { h, VNode } from "preact"; +import { Router, route, Route } from "preact-router"; +import InstanceCreatePage from "./paths/admin/create/index.js"; +import InstanceListPage from "./paths/admin/list/index.js"; + +export enum AdminPaths { + list_instances = "/instances", + new_instance = "/instance/new", +} + +export function AdminRoutes(): VNode { + return ( + <Router> + <Route + path={AdminPaths.list_instances} + component={InstanceListPage} + onCreate={() => { + route(AdminPaths.new_instance); + }} + onUpdate={(id: string): void => { + route(`/instance/${id}/update`); + }} + /> + + <Route + path={AdminPaths.new_instance} + component={InstanceCreatePage} + onBack={() => route(AdminPaths.list_instances)} + onConfirm={() => { + // route(AdminPaths.list_instances); + }} + + // onError={(error: any) => { + // }} + /> + </Router> + ); +} diff --git a/packages/auditor-backoffice-ui/src/Application.tsx b/packages/auditor-backoffice-ui/src/Application.tsx new file mode 100644 index 000000000..e832d3107 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/Application.tsx @@ -0,0 +1,165 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util"; +import { + ErrorType, + TranslationProvider, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useMemo } from "preact/hooks"; +import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; +import { Loading } from "./components/exception/loading.js"; +import { + NotConnectedAppMenu, + NotificationCard +} from "./components/menu/index.js"; +import { + BackendContextProvider +} from "./context/backend.js"; +import { ConfigContextProvider } from "./context/config.js"; +import { useBackendConfig } from "./hooks/backend.js"; +import { strings } from "./i18n/strings.js"; + +export function Application(): VNode { + return ( + <BackendContextProvider> + <TranslationProvider source={strings}> + <ApplicationStatusRoutes /> + </TranslationProvider> + </BackendContextProvider> + ); +} + +/** + * Check connection testing against /config + * + * @returns + */ +function ApplicationStatusRoutes(): VNode { + const result = useBackendConfig(); + const { i18n } = useTranslationContext(); + + const { currency, version } = result.ok && result.data + ? result.data + : { currency: "unknown", version: "unknown" }; + const ctx = useMemo(() => ({ currency, version }), [currency, version]); + + if (!result.ok) { + if (result.loading) return <Loading />; + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) { + return ( + <Fragment> + <NotConnectedAppMenu title="Login" /> + <NotificationCard + notification={{ + message: i18n.str`Checking the /config endpoint got authorization error`, + type: "ERROR", + description: `The /config endpoint of the backend server should be accesible`, + }} + /> + </Fragment> + ); + } + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) { + return ( + <Fragment> + <NotConnectedAppMenu title="Error" /> + <NotificationCard + notification={{ + message: i18n.str`Could not find /config enpoint on this URL`, + type: "ERROR", + description: `Check the URL or contact the system administrator.`, + }} + /> + </Fragment> + ); + } + if (result.type === ErrorType.SERVER) { + <Fragment> + <NotConnectedAppMenu title="Error" /> + <NotificationCard + notification={{ + message: i18n.str`Server response with an error code`, + type: "ERROR", + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, + }} + /> + </Fragment>; + } + if (result.type === ErrorType.UNREADABLE) { + <Fragment> + <NotConnectedAppMenu title="Error" /> + <NotificationCard + notification={{ + message: i18n.str`Response from server is unreadable, http status: ${result.status}`, + type: "ERROR", + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, + }} + /> + </Fragment>; + } + return ( + <Fragment> + <NotConnectedAppMenu title="Error" /> + <NotificationCard + notification={{ + message: i18n.str`Unexpected Error`, + type: "ERROR", + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, + }} + /> + </Fragment> + ); + } + + const SUPPORTED_VERSION = "5:0:1" + if (result.data && !LibtoolVersion.compare( + SUPPORTED_VERSION, + result.data.version, + )?.compatible) { + return <Fragment> + <NotConnectedAppMenu title="Error" /> + <NotificationCard + notification={{ + message: i18n.str`Incompatible version`, + type: "ERROR", + description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`, + }} + /> + </Fragment> + } + + return ( + <div class="has-navbar-fixed-top"> + <ConfigContextProvider value={ctx}> + <ApplicationReadyRoutes /> + </ConfigContextProvider> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx new file mode 100644 index 000000000..414eee39d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx @@ -0,0 +1,175 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { createHashHistory } from "history"; +import { Fragment, VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useState } from "preact/hooks"; +import { InstanceRoutes } from "./InstanceRoutes.js"; +import { + NotConnectedAppMenu, + NotYetReadyAppMenu, + NotificationCard, +} from "./components/menu/index.js"; +import { useBackendContext } from "./context/backend.js"; +import { LoginToken } from "./declaration.js"; +import { useBackendInstancesTestForAdmin } from "./hooks/backend.js"; +import { LoginPage } from "./paths/login/index.js"; +import { Settings } from "./paths/settings/index.js"; +import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; + +/** + * Check if admin against /management/instances + * @returns + */ +export function ApplicationReadyRoutes(): VNode { + const { i18n } = useTranslationContext(); + const [unauthorized, setUnauthorized] = useState(false) + const { + url: backendURL, + updateToken, + alreadyTriedLogin, + } = useBackendContext(); + + function updateLoginStatus(token: LoginToken | undefined) { + updateToken(token) + setUnauthorized(false) + } + + const result = useBackendInstancesTestForAdmin(); + + const clearTokenAndGoToRoot = () => { + route("/"); + }; + const [showSettings, setShowSettings] = useState(false) + const unauthorizedAdmin = !result.loading + && !result.ok + && result.type === ErrorType.CLIENT + && result.status === HttpStatusCode.Unauthorized; + + if (!alreadyTriedLogin && !result.ok) { + return ( + <Fragment> + <NotConnectedAppMenu title="Welcome!" /> + <LoginPage onConfirm={updateToken} /> + </Fragment> + ); + } + + if (showSettings) { + return <Fragment> + <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + <Settings onClose={() => setShowSettings(false)} /> + </Fragment> + } + + if (result.loading) { + return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />; + } + + let admin = result.ok || unauthorizedAdmin; + let instanceNameByBackendURL: string | undefined; + + if (!admin) { + // * the testing against admin endpoint failed and it's not + // an authorization problem + // * merchant backend will return this SPA under the main + // endpoint or /instance/<id> endpoint + // => trying to infer the instance id + const path = new URL(backendURL).pathname; + const match = INSTANCE_ID_LOOKUP.exec(path); + if (!match || !match[1]) { + // this should be rare because + // query to /config is ok but the URL + // does not match our pattern + return ( + <Fragment> + <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + <NotificationCard + notification={{ + message: i18n.str`Couldn't access the server.`, + description: i18n.str`Could not infer instance id from url ${backendURL}`, + type: "ERROR", + }} + /> + {/* <ConnectionPage onConfirm={changeBackend} /> */} + </Fragment> + ); + } + + instanceNameByBackendURL = match[1]; + } + + if (unauthorized || unauthorizedAdmin) { + return <Fragment> + <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + <NotificationCard + notification={{ + message: i18n.str`Access denied`, + description: i18n.str`Check your token is valid`, + type: "ERROR", + }} + /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + } + + const history = createHashHistory(); + return ( + <Router history={history}> + <Route + default + component={DefaultMainRoute} + admin={admin} + onUnauthorized={() => setUnauthorized(true)} + onLoginPass={() => { + setUnauthorized(false) + }} + instanceNameByBackendURL={instanceNameByBackendURL} + /> + </Router> + ); +} + +function DefaultMainRoute({ + instance, + admin, + onUnauthorized, + onLoginPass, + instanceNameByBackendURL, + url, //from preact-router +}: any): VNode { + const [instanceName, setInstanceName] = useState( + instanceNameByBackendURL || instance || "default", + ); + + return ( + <InstanceRoutes + admin={admin} + path={url} + onUnauthorized={onUnauthorized} + onLoginPass={onLoginPass} + id={instanceName} + setInstanceName={setInstanceName} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx new file mode 100644 index 000000000..14ccf773a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx @@ -0,0 +1,800 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel + */ + +import { + useTranslationContext, + HttpError, + ErrorType, +} from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { Fragment, FunctionComponent, h, VNode } from "preact"; +import { Route, route, Router } from "preact-router"; +import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; +import { Loading } from "./components/exception/loading.js"; +import { Menu, NotificationCard } from "./components/menu/index.js"; +import { useBackendContext } from "./context/backend.js"; +import { InstanceContextProvider } from "./context/instance.js"; +import { + useBackendDefaultToken, + useBackendInstanceToken, + useSimpleLocalStorage, +} from "./hooks/index.js"; +import { useInstanceKYCDetails } from "./hooks/instance.js"; +import InstanceCreatePage from "./paths/admin/create/index.js"; +import InstanceListPage from "./paths/admin/list/index.js"; +import TokenPage from "./paths/instance/token/index.js"; +import ListKYCPage from "./paths/instance/kyc/list/index.js"; +import OrderCreatePage from "./paths/instance/orders/create/index.js"; +import OrderDetailsPage from "./paths/instance/orders/details/index.js"; +import OrderListPage from "./paths/instance/orders/list/index.js"; +import DepositConfirmationCreatePage from "./paths/instance/deposit_confirmations/create/index.js"; +import DepositConfirmationListPage from "./paths/instance/deposit_confirmations/list/index.js"; +import DepositConfirmationUpdatePage from "./paths/instance/deposit_confirmations/update/index.js"; +import ProductCreatePage from "./paths/instance/products/create/index.js"; +import ProductListPage from "./paths/instance/products/list/index.js"; +import ProductUpdatePage from "./paths/instance/products/update/index.js"; +import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; +import BankAccountListPage from "./paths/instance/accounts/list/index.js"; +import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; +import ReservesCreatePage from "./paths/instance/reserves/create/index.js"; +import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; +import ReservesListPage from "./paths/instance/reserves/list/index.js"; +import TemplateCreatePage from "./paths/instance/templates/create/index.js"; +import TemplateUsePage from "./paths/instance/templates/use/index.js"; +import TemplateQrPage from "./paths/instance/templates/qr/index.js"; +import TemplateListPage from "./paths/instance/templates/list/index.js"; +import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; +import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; +import WebhookListPage from "./paths/instance/webhooks/list/index.js"; +import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; +import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js"; +import ValidatorListPage from "./paths/instance/otp_devices/list/index.js"; +import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js"; +import TransferCreatePage from "./paths/instance/transfers/create/index.js"; +import TransferListPage from "./paths/instance/transfers/list/index.js"; +import InstanceUpdatePage, { + AdminUpdate as InstanceAdminUpdatePage, + Props as InstanceUpdatePageProps, +} from "./paths/instance/update/index.js"; +import { LoginPage } from "./paths/login/index.js"; +import NotFoundPage from "./paths/notfound/index.js"; +import { Notification } from "./utils/types.js"; +import { LoginToken, MerchantBackend } from "./declaration.js"; +import { Settings } from "./paths/settings/index.js"; +import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; + +export enum InstancePaths { + error = "/error", + settings = "/settings", + token = "/token", + + inventory_list = "/inventory", + inventory_update = "/inventory/:pid/update", + inventory_new = "/inventory/new", + + deposit_confirmation_list = "/deposit-confirmation", + deposit_confirmation_update = "/deposit-confirmation/:pid/update", + deposit_confirmation_new = "/deposit-confirmation/new", + + interface = "/interface", +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => { }; + +export enum AdminPaths { + list_instances = "/instances", + new_instance = "/instance/new", + update_instance = "/instance/:id/update", +} + +export interface Props { + id: string; + admin?: boolean; + path: string; + onUnauthorized: () => void; + onLoginPass: () => void; + setInstanceName: (s: string) => void; +} + +export function InstanceRoutes({ + id, + admin, + path, + // onUnauthorized, + onLoginPass, + setInstanceName, +}: Props): VNode { + const [defaultToken, updateDefaultToken] = useBackendDefaultToken(); + const [token, updateToken] = useBackendInstanceToken(id); + const { i18n } = useTranslationContext(); + + type GlobalNotifState = (Notification & { to: string }) | undefined; + const [globalNotification, setGlobalNotification] = + useState<GlobalNotifState>(undefined); + + const changeToken = (token?: LoginToken) => { + if (admin) { + updateToken(token); + } else { + updateDefaultToken(token); + } + onLoginPass() + }; + // const updateLoginStatus = (url: string, token?: string) => { + // changeToken(token); + // }; + + const value = useMemo( + () => ({ id, token, admin, changeToken }), + [id, token, admin], + ); + + function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) { + return function ServerErrorRedirectToImpl( + error: HttpError<MerchantBackend.ErrorDetail>, + ) { + if (error.type === ErrorType.TIMEOUT) { + setGlobalNotification({ + message: i18n.str`The request to the backend take too long and was cancelled`, + description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`, + type: "ERROR", + to, + }); + } else { + setGlobalNotification({ + message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + details: + error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER + ? error.payload.detail + : undefined, + type: "ERROR", + to, + }); + } + return <Redirect to={to} />; + }; + } + + // const LoginPageAccessDeniend = onUnauthorized + const LoginPageAccessDenied = () => { + return <Fragment> + <NotificationCard + notification={{ + message: i18n.str`Access denied`, + description: i18n.str`Session expired or password changed.`, + type: "ERROR", + }} + /> + <LoginPage onConfirm={changeToken} /> + </Fragment> + + } + + function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) { + return function IfAdminCreateDefaultOrImpl(props?: T) { + if (admin && id === "default") { + return ( + <Fragment> + <NotificationCard + notification={{ + message: i18n.str`No 'default' instance configured yet.`, + description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`, + type: "INFO", + }} + /> + <InstanceCreatePage + forceId="default" + onConfirm={() => { + route(InstancePaths.order_list); + }} + /> + </Fragment> + ); + } + if (props) { + return <Next {...props} />; + } + return <Next />; + }; + } + + const clearTokenAndGoToRoot = () => { + route("/"); + // clear all tokens + updateToken(undefined) + updateDefaultToken(undefined) + }; + + return ( + <InstanceContextProvider value={value}> + <Menu + instance={id} + admin={admin} + onShowSettings={() => { + route(InstancePaths.interface) + }} + path={path} + onLogout={clearTokenAndGoToRoot} + setInstanceName={setInstanceName} + isPasswordOk={defaultToken !== undefined} + /> + <KycBanner /> + <NotificationCard notification={globalNotification} /> + + <Router + onChange={(e) => { + const movingOutFromNotification = + globalNotification && e.url !== globalNotification.to; + if (movingOutFromNotification) { + setGlobalNotification(undefined); + } + }} + > + <Route path="/" component={Redirect} to={InstancePaths.order_list} /> + {/** + * Admin pages + */} + {admin && ( + <Route + path={AdminPaths.list_instances} + component={InstanceListPage} + onCreate={() => { + route(AdminPaths.new_instance); + }} + onUpdate={(id: string): void => { + route(`/instance/${id}/update`); + }} + setInstanceName={setInstanceName} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> + )} + {admin && ( + <Route + path={AdminPaths.new_instance} + component={InstanceCreatePage} + onBack={() => route(AdminPaths.list_instances)} + onConfirm={() => { + route(InstancePaths.order_list); + }} + /> + )} + {admin && ( + <Route + path={AdminPaths.update_instance} + component={AdminInstanceUpdatePage} + onBack={() => route(AdminPaths.list_instances)} + onConfirm={() => { + route(AdminPaths.list_instances); + }} + onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)} + onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)} + onNotFound={NotFoundPage} + /> + )} + {/** + * Update instance page + */} + <Route + path={InstancePaths.settings} + component={InstanceUpdatePage} + onBack={() => { + route(`/`); + }} + onConfirm={() => { + route(`/`); + }} + onUpdateError={noop} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> + {/** + * Update instance page + */} + <Route + path={InstancePaths.token} + component={TokenPage} + onChange={() => { + route(`/`); + }} + onCancel={() => { + route(InstancePaths.order_list) + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> + {/** + * Inventory pages + */} + <Route + path={InstancePaths.inventory_list} + component={ProductListPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.inventory_new); + }} + onSelect={(id: string) => { + route(InstancePaths.inventory_update.replace(":pid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.inventory_update} + component={ProductUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)} + onConfirm={() => { + route(InstancePaths.inventory_list); + }} + onBack={() => { + route(InstancePaths.inventory_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.inventory_new} + component={ProductCreatePage} + onConfirm={() => { + route(InstancePaths.inventory_list); + }} + onBack={() => { + route(InstancePaths.inventory_list); + }} + /> + {/** + * Deposit confirmation pages + */} + <Route + path={InstancePaths.deposit_confirmation_list} + component={DepositConfirmationListPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.deposit_confirmation_new); + }} + onSelect={(id: string) => { + route(InstancePaths.deposit_confirmation_update.replace(":pid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.deposit_confirmation_update} + component={DepositConfirmationUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.deposit_confirmation_list)} + onConfirm={() => { + route(InstancePaths.deposit_confirmation_list); + }} + onBack={() => { + route(InstancePaths.deposit_confirmation_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.deposit_confirmation_new} + component={DepositConfirmationCreatePage} + onConfirm={() => { + route(InstancePaths.deposit_confirmation_list); + }} + onBack={() => { + route(InstancePaths.deposit_confirmation_list); + }} + /> + {/** + * Bank pages + */} + <Route + path={InstancePaths.bank_list} + component={BankAccountListPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.bank_new); + }} + onSelect={(id: string) => { + route(InstancePaths.bank_update.replace(":bid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.bank_update} + component={BankAccountUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)} + onConfirm={() => { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.bank_new} + component={BankAccountCreatePage} + onConfirm={() => { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + /> + {/** + * Order pages + */} + <Route + path={InstancePaths.order_list} + component={OrderListPage} + onCreate={() => { + route(InstancePaths.order_new); + }} + onSelect={(id: string) => { + route(InstancePaths.order_details.replace(":oid", id)); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.order_details} + component={OrderDetailsPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.order_list); + }} + /> + <Route + path={InstancePaths.order_new} + component={OrderCreatePage} + onConfirm={(orderId: string) => { + route(InstancePaths.order_details.replace(":oid", orderId)); + }} + onBack={() => { + route(InstancePaths.order_list); + }} + /> + {/** + * Transfer pages + */} + <Route + path={InstancePaths.transfers_list} + component={TransferListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.transfers_new); + }} + /> + <Route + path={InstancePaths.transfers_new} + component={TransferCreatePage} + onConfirm={() => { + route(InstancePaths.transfers_list); + }} + onBack={() => { + route(InstancePaths.transfers_list); + }} + /> + {/** + * Webhooks pages + */} + <Route + path={InstancePaths.webhooks_list} + component={WebhookListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.webhooks_new); + }} + onSelect={(id: string) => { + route(InstancePaths.webhooks_update.replace(":tid", id)); + }} + /> + <Route + path={InstancePaths.webhooks_update} + component={WebhookUpdatePage} + onConfirm={() => { + route(InstancePaths.webhooks_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.webhooks_list); + }} + /> + <Route + path={InstancePaths.webhooks_new} + component={WebhookCreatePage} + onConfirm={() => { + route(InstancePaths.webhooks_list); + }} + onBack={() => { + route(InstancePaths.webhooks_list); + }} + /> + {/** + * Validator pages + */} + <Route + path={InstancePaths.otp_devices_list} + component={ValidatorListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.otp_devices_new); + }} + onSelect={(id: string) => { + route(InstancePaths.otp_devices_update.replace(":vid", id)); + }} + /> + <Route + path={InstancePaths.otp_devices_update} + component={ValidatorUpdatePage} + onConfirm={() => { + route(InstancePaths.otp_devices_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.otp_devices_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.otp_devices_list); + }} + /> + <Route + path={InstancePaths.otp_devices_new} + component={ValidatorCreatePage} + onConfirm={() => { + route(InstancePaths.otp_devices_list); + }} + onBack={() => { + route(InstancePaths.otp_devices_list); + }} + /> + {/** + * Templates pages + */} + <Route + path={InstancePaths.templates_list} + component={TemplateListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.templates_new); + }} + onNewOrder={(id: string) => { + route(InstancePaths.templates_use.replace(":tid", id)); + }} + onQR={(id: string) => { + route(InstancePaths.templates_qr.replace(":tid", id)); + }} + onSelect={(id: string) => { + route(InstancePaths.templates_update.replace(":tid", id)); + }} + /> + <Route + path={InstancePaths.templates_update} + component={TemplateUpdatePage} + onConfirm={() => { + route(InstancePaths.templates_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_new} + component={TemplateCreatePage} + onConfirm={() => { + route(InstancePaths.templates_list); + }} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_use} + component={TemplateUsePage} + onOrderCreated={(id: string) => { + route(InstancePaths.order_details.replace(":oid", id)); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_qr} + component={TemplateQrPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + + {/** + * reserves pages + */} + <Route + path={InstancePaths.reserves_list} + component={ReservesListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onSelect={(id: string) => { + route(InstancePaths.reserves_details.replace(":rid", id)); + }} + onCreate={() => { + route(InstancePaths.reserves_new); + }} + /> + <Route + path={InstancePaths.reserves_details} + component={ReservesDetailsPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.reserves_list); + }} + /> + <Route + path={InstancePaths.reserves_new} + component={ReservesCreatePage} + onConfirm={() => { + route(InstancePaths.reserves_list); + }} + onBack={() => { + route(InstancePaths.reserves_list); + }} + /> + <Route path={InstancePaths.kyc} component={ListKYCPage} /> + <Route path={InstancePaths.interface} component={Settings} /> + {/** + * Example pages + */} + <Route path="/loading" component={Loading} /> + <Route default component={NotFoundPage} /> + </Router> + </InstanceContextProvider> + ); +} + +export function Redirect({ to }: { to: string }): null { + useEffect(() => { + route(to, true); + }); + return null; +} + +function AdminInstanceUpdatePage({ + id, + ...rest +}: { id: string } & InstanceUpdatePageProps): VNode { + const [token, changeToken] = useBackendInstanceToken(id); + const updateLoginStatus = (token?: LoginToken): void => { + changeToken(token); + }; + const value = useMemo( + () => ({ id, token, admin: true, changeToken }), + [id, token], + ); + const { i18n } = useTranslationContext(); + + return ( + <InstanceContextProvider value={value}> + <InstanceAdminUpdatePage + {...rest} + instanceId={id} + onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => { + const notif = + error.type === ErrorType.TIMEOUT + ? { + message: i18n.str`The request to the backend take too long and was cancelled`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + type: "ERROR" as const, + } + : { + message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + details: + error.type === ErrorType.CLIENT || + error.type === ErrorType.SERVER + ? error.payload.detail + : undefined, + type: "ERROR" as const, + }; + return ( + <Fragment> + <NotificationCard notification={notif} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + ); + }} + onUnauthorized={() => { + return ( + <Fragment> + <NotificationCard + notification={{ + message: i18n.str`Access denied`, + description: i18n.str`The access token provided is invalid`, + type: "ERROR", + }} + /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + ); + }} + /> + </InstanceContextProvider> + ); +} + +function KycBanner(): VNode { + const kycStatus = useInstanceKYCDetails(); + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + const today = format(new Date(), dateFormatForSettings(settings)); + const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide"); + const hasBeenHidden = today === lastHide; + const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; + if (hasBeenHidden || !needsToBeShown) return <Fragment />; + return ( + <NotificationCard + notification={{ + type: "WARN", + message: "KYC verification needed", + description: ( + <div> + <p> + Some transfer are on hold until a KYC process is completed. Go to + the KYC section in the left panel for more information + </p> + <div class="buttons is-right"> + <button class="button" onClick={() => setLastHide(today)}> + <i18n.Translate>Hide for today</i18n.Translate> + </button> + </div> + </div> + ), + }} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/assets/empty.png b/packages/auditor-backoffice-ui/src/assets/empty.png Binary files differnew file mode 100644 index 000000000..5120d3138 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/empty.png diff --git a/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png Binary files differnew file mode 100644 index 000000000..93ebe2e2c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png diff --git a/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png Binary files differnew file mode 100644 index 000000000..52d1623ea --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png diff --git a/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png b/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png Binary files differnew file mode 100644 index 000000000..254e4bb4d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png diff --git a/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png b/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png Binary files differnew file mode 100644 index 000000000..e81177dcb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png diff --git a/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png b/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png Binary files differnew file mode 100644 index 000000000..40e9b5b47 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png diff --git a/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg b/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg new file mode 100644 index 000000000..22d58da65 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 2794;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;}
+ .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g id="Layer_2">
+</g>
+<g id="Layer_x5F_1_x5F_1">
+ <g>
+ <polygon points="1204.6,359.2 271.8,30 271.8,2060.1 1204.6,1758.3 "/>
+ <polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 1182.2,1757.3 "/>
+ <polygon class="st0" points="30,2415.4 1182.2,2031.4 1182.2,357.9 30,742 "/>
+ <polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8 "/>
+ <g>
+ <path d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
+ c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
+ c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
+ <path d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
+ c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
+ c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
+ c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
+ c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
+ c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
+ c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
+ C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
+ </g>
+ <path class="st1" d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
+ c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
+ c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
+ c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
+ c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
+ <path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
+ c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
+ <path class="st1" d="M2120,2017l-907-282V380l907-308V2017z M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
+ c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
+ c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
+ <polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 2411.2,757.2 "/>
+ <g>
+ <path class="st2" d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
+ l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png b/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png Binary files differnew file mode 100644 index 000000000..9cfb889be --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png diff --git a/packages/auditor-backoffice-ui/src/assets/logo-2021.svg b/packages/auditor-backoffice-ui/src/assets/logo-2021.svg new file mode 100644 index 000000000..8c5ff3e5b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/logo-2021.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90"> + <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3"> + <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" /> + <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" /> + <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" /> + </g> + <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" /> +</svg>
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/assets/logo.jpeg b/packages/auditor-backoffice-ui/src/assets/logo.jpeg Binary files differnew file mode 100644 index 000000000..489832f7c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/assets/logo.jpeg diff --git a/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx new file mode 100644 index 000000000..b1fc33877 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx @@ -0,0 +1,55 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ComponentChildren, h } from "preact"; +import { LoadingModal } from "../modal/index.js"; +import { useAsync } from "../../hooks/async.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; + +type Props = { + children: ComponentChildren; + disabled: boolean; + onClick?: () => Promise<void>; + [rest: string]: any; +}; + +export function AsyncButton({ onClick, disabled, children, ...rest }: Props) { + const { isSlow, isLoading, request, cancel } = useAsync(onClick); + const { i18n } = useTranslationContext(); + if (isSlow) { + return <LoadingModal onCancel={cancel} />; + } + if (isLoading) { + return ( + <button class="button"> + <i18n.Translate>Loading...</i18n.Translate> + </button> + ); + } + + return ( + <span {...rest}> + <button class="button is-success" onClick={request} disabled={disabled}> + {children} + </button> + </span> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/exception/QR.tsx b/packages/auditor-backoffice-ui/src/components/exception/QR.tsx new file mode 100644 index 000000000..c9340ea76 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/exception/QR.tsx @@ -0,0 +1,49 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { h, VNode } from "preact"; +import { useEffect, useRef } from "preact/hooks"; +import qrcode from "qrcode-generator"; + +export function QR({ text }: { text: string }): VNode { + const divRef = useRef<HTMLDivElement>(null); + useEffect(() => { + const qr = qrcode(0, "L"); + qr.addData(text); + qr.make(); + if (divRef.current) { + divRef.current.innerHTML = qr.createSvgTag({ + scalable: true, + }); + } + }); + + return ( + <div + style={{ + width: "100%", + display: "flex", + flexDirection: "column", + alignItems: "center", + }} + > + <div + style={{ width: "50%", minWidth: 200, maxWidth: 300 }} + ref={divRef} + /> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/exception/loading.tsx b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx new file mode 100644 index 000000000..a043b81eb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx @@ -0,0 +1,48 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; + +export function Loading(): VNode { + return ( + <div + class="columns is-centered is-vcentered" + style={{ + height: "calc(100% - 3rem)", + position: "absolute", + width: "100%", + }} + > + <Spinner /> + </div> + ); +} + +export function Spinner(): VNode { + return ( + <div class="lds-ring"> + <div /> + <div /> + <div /> + <div /> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx b/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx new file mode 100644 index 000000000..0d53c4d08 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx @@ -0,0 +1,109 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext, useMemo } from "preact/hooks"; + +type Updater<S> = (value: (prevState: S) => S) => void; + +export interface Props<T> { + object?: Partial<T>; + errors?: FormErrors<T>; + name?: string; + valueHandler: Updater<Partial<T>> | null; + children: ComponentChildren; +} + +const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s; + +export function FormProvider<T>({ + object = {}, + errors = {}, + name = "", + valueHandler, + children, +}: Props<T>): VNode { + const initialObject = useMemo(() => object, []); + const value = useMemo<FormType<T>>( + () => ({ + errors, + object, + initialObject, + valueHandler: valueHandler ? valueHandler : noUpdater, + name, + toStr: {}, + fromStr: {}, + }), + [errors, object, valueHandler], + ); + + return ( + <FormContext.Provider value={value}> + <form + class="field" + onSubmit={(e) => { + e.preventDefault(); + // if (valueHandler) valueHandler(object); + }} + > + {children} + </form> + </FormContext.Provider> + ); +} + +export interface FormType<T> { + object: Partial<T>; + initialObject: Partial<T>; + errors: FormErrors<T>; + toStr: FormtoStr<T>; + name: string; + fromStr: FormfromStr<T>; + valueHandler: Updater<Partial<T>>; +} + +const FormContext = createContext<FormType<unknown>>(null!); + +/** + * FIXME: + * USE MEMORY EVENTS INSTEAD OF CONTEXT + * @deprecated + */ + +export function useFormContext<T>() { + return useContext<FormType<T>>(FormContext); +} + +export type FormErrors<T> = { + [P in keyof T]?: string | FormErrors<T[P]>; +}; + +export type FormtoStr<T> = { + [P in keyof T]?: (f?: T[P]) => string; +}; + +export type FormfromStr<T> = { + [P in keyof T]?: (f: string) => T[P]; +}; + +export type FormUpdater<T> = { + [P in keyof T]?: (f: keyof T) => (v: T[P]) => void; +}; diff --git a/packages/auditor-backoffice-ui/src/components/form/Input.tsx b/packages/auditor-backoffice-ui/src/components/form/Input.tsx new file mode 100644 index 000000000..c1ddcb064 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/Input.tsx @@ -0,0 +1,116 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { useField, InputProps } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + inputType?: "text" | "number" | "multiline" | "password"; + expand?: boolean; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; + inputExtra?: any; + side?: ComponentChildren; + children?: ComponentChildren; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +const TextInput = ({ inputType, error, ...rest }: any) => + inputType === "multiline" ? ( + <textarea + {...rest} + class={error ? "textarea is-danger" : "textarea"} + rows="3" + /> + ) : ( + <input + {...rest} + class={error ? "input is-danger" : "input"} + type={inputType} + /> + ); + +export function Input<T>({ + name, + readonly, + placeholder, + tooltip, + label, + expand, + help, + children, + inputType, + inputExtra, + side, + fromStr = defaultFromString, + toStr = defaultToString, +}: Props<keyof T>): VNode { + const { error, value, onChange, required } = useField<T>(name); + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p + class={ + expand + ? "control is-expanded has-icons-right" + : "control has-icons-right" + } + > + <TextInput + error={error} + {...inputExtra} + inputType={inputType} + placeholder={placeholder} + readonly={readonly} + disabled={readonly} + name={String(name)} + value={toStr(value)} + onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => + onChange(fromStr(e.currentTarget.value)) + } + /> + {help} + {children} + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + {side} + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx b/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx new file mode 100644 index 000000000..4ed4c4b28 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx @@ -0,0 +1,139 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { InputProps, useField } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + isValid?: (e: any) => boolean; + addonBefore?: string; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +export function InputArray<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + addonBefore, + isValid = () => true, + fromStr = defaultFromString, + toStr = defaultToString, +}: Props<keyof T>): VNode { + const { error: formError, value, onChange, required } = useField<T>(name); + const [localError, setLocalError] = useState<string | null>(null); + + const error = localError || formError; + + const array: any[] = (value ? value! : []) as any; + const [currentValue, setCurrentValue] = useState(""); + const { i18n } = useTranslationContext(); + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <div class="field has-addons"> + {addonBefore && ( + <div class="control"> + <a class="button is-static">{addonBefore}</a> + </div> + )} + <p class="control is-expanded has-icons-right"> + <input + class={error ? "input is-danger" : "input"} + type="text" + placeholder={placeholder} + readonly={readonly} + disabled={readonly} + name={String(name)} + value={currentValue} + onChange={(e): void => setCurrentValue(e.currentTarget.value)} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + <p class="control"> + <button + class="button is-info has-tooltip-left" + disabled={!currentValue} + onClick={(): void => { + const v = fromStr(currentValue); + if (!isValid(v)) { + setLocalError( + i18n.str`The value ${v} is invalid for a payment url`, + ); + return; + } + setLocalError(null); + onChange([v, ...array] as any); + setCurrentValue(""); + }} + data-tooltip={i18n.str`add element to the list`} + > + <i18n.Translate>add</i18n.Translate> + </button> + </p> + </div> + {help} + {error && <p class="help is-danger"> {error} </p>} + {array.map((v, i) => ( + <div key={i} class="tags has-addons mt-3 mb-0"> + <span + class="tag is-medium is-info mb-0" + style={{ maxWidth: "90%" }} + > + {v} + </span> + <a + class="tag is-medium is-danger is-delete mb-0" + onClick={() => { + onChange(array.filter((f) => f !== v) as any); + setCurrentValue(toStr(v)); + }} + /> + </div> + ))} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx new file mode 100644 index 000000000..f79e16c07 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx @@ -0,0 +1,91 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + name: T; + readonly?: boolean; + expand?: boolean; + threeState?: boolean; + toBoolean?: (v?: any) => boolean | undefined; + fromBoolean?: (s: boolean | undefined) => any; +} + +const defaultToBoolean = (f?: any): boolean | undefined => f || ""; +const defaultFromBoolean = (v: boolean | undefined): any => v as any; + +export function InputBoolean<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + threeState, + expand, + fromBoolean = defaultFromBoolean, + toBoolean = defaultToBoolean, +}: Props<keyof T>): VNode { + const { error, value, onChange } = useField<T>(name); + + const onCheckboxClick = (): void => { + const c = toBoolean(value); + if (c === false && threeState) return onChange(undefined as any); + return onChange(fromBoolean(!c)); + }; + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class={expand ? "control is-expanded" : "control"}> + <label class="b-checkbox checkbox"> + <input + type="checkbox" + class={toBoolean(value) === undefined ? "is-indeterminate" : ""} + checked={toBoolean(value)} + placeholder={placeholder} + readonly={readonly} + name={String(name)} + disabled={readonly} + onChange={onCheckboxClick} + /> + <span class="check" /> + </label> + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx new file mode 100644 index 000000000..b02354d7c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { useConfigContext } from "../../context/config.js"; +import { Amount } from "../../declaration.js"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { InputProps } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + addonAfter?: ComponentChildren; + children?: ComponentChildren; + side?: ComponentChildren; +} + +export function InputCurrency<T>({ + name, + readonly, + label, + placeholder, + help, + tooltip, + expand, + addonAfter, + children, + side, +}: Props<keyof T>): VNode { + const config = useConfigContext(); + return ( + <InputWithAddon<T> + name={name} + readonly={readonly} + addonBefore={config.currency} + side={side} + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + addonAfter={addonAfter} + inputType="number" + expand={expand} + toStr={(v?: Amount) => v?.split(":")[1] || ""} + fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)} + inputExtra={{ min: 0 }} + > + {children} + </InputWithAddon> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx new file mode 100644 index 000000000..a398629dc --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx @@ -0,0 +1,164 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { ComponentChildren, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { DatePicker } from "../picker/DatePicker.js"; +import { InputProps, useField } from "./useField.js"; +import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js"; + +export interface Props<T> extends InputProps<T> { + readonly?: boolean; + expand?: boolean; + //FIXME: create separated components InputDate and InputTimestamp + withTimestampSupport?: boolean; + side?: ComponentChildren; +} + +export function InputDate<T>({ + name, + readonly, + label, + placeholder, + help, + tooltip, + expand, + withTimestampSupport, + side, +}: Props<keyof T>): VNode { + const [opened, setOpened] = useState(false); + const { i18n } = useTranslationContext(); + const [settings] = useSettings() + + const { error, required, value, onChange } = useField<T>(name); + + let strValue = ""; + if (!value) { + strValue = withTimestampSupport ? "unknown" : ""; + } else if (value instanceof Date) { + strValue = format(value, dateFormatForSettings(settings)); + } else if (value.t_s) { + strValue = + value.t_s === "never" + ? withTimestampSupport + ? "never" + : "" + : format(new Date(value.t_s * 1000), dateFormatForSettings(settings)); + } + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <div class="field has-addons"> + <p + class={ + expand + ? "control is-expanded has-icons-right" + : "control has-icons-right" + } + > + <input + class="input" + type="text" + readonly + value={strValue} + placeholder={placeholder} + onClick={() => { + if (!readonly) setOpened(true); + }} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + {help} + </p> + <div + class="control" + onClick={() => { + if (!readonly) setOpened(true); + }} + > + <a class="button is-static"> + <span class="icon"> + <i class="mdi mdi-calendar" /> + </span> + </a> + </div> + </div> + {error && <p class="help is-danger">{error}</p>} + </div> + + {!readonly && ( + <span + data-tooltip={ + withTimestampSupport + ? i18n.str`change value to unknown date` + : i18n.str`change value to empty` + } + > + <button + class="button is-info mr-3" + onClick={() => onChange(undefined as any)} + > + <i18n.Translate>clear</i18n.Translate> + </button> + </span> + )} + {withTimestampSupport && ( + <span data-tooltip={i18n.str`change value to never`}> + <button + class="button is-info" + onClick={() => onChange({ t_s: "never" } as any)} + > + <i18n.Translate>never</i18n.Translate> + </button> + </span> + )} + {side} + </div> + <DatePicker + opened={opened} + closeFunction={() => setOpened(false)} + dateReceiver={(d) => { + if (withTimestampSupport) { + onChange({ t_s: d.getTime() / 1000 } as any); + } else { + onChange(d as any); + } + }} + /> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx new file mode 100644 index 000000000..7aa2703a4 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx @@ -0,0 +1,186 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { formatDuration, intervalToDuration } from "date-fns"; +import { ComponentChildren, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { SimpleModal } from "../modal/index.js"; +import { DurationPicker } from "../picker/DurationPicker.js"; +import { InputProps, useField } from "./useField.js"; +import { Duration } from "@gnu-taler/taler-util"; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + readonly?: boolean; + withForever?: boolean; + side?: ComponentChildren; + withoutClear?: boolean; +} + +export function InputDuration<T>({ + name, + expand, + placeholder, + tooltip, + label, + help, + readonly, + withForever, + withoutClear, + side, +}: Props<keyof T>): VNode { + const [opened, setOpened] = useState(false); + const { i18n } = useTranslationContext(); + + const { error, required, value: anyValue, onChange } = useField<T>(name); + let strValue = ""; + const value: Duration = anyValue + if (!value) { + strValue = ""; + } else if (value.d_ms === "forever") { + strValue = i18n.str`forever`; + } else { + strValue = formatDuration( + intervalToDuration({ start: 0, end: value.d_ms }), + { + locale: { + formatDistance: (name, value) => { + switch (name) { + case "xMonths": + return i18n.str`${value}M`; + case "xYears": + return i18n.str`${value}Y`; + case "xDays": + return i18n.str`${value}d`; + case "xHours": + return i18n.str`${value}h`; + case "xMinutes": + return i18n.str`${value}min`; + case "xSeconds": + return i18n.str`${value}sec`; + } + }, + localize: { + day: () => "s", + month: () => "m", + ordinalNumber: () => "th", + dayPeriod: () => "p", + quarter: () => "w", + era: () => "e", + }, + }, + }, + ); + } + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal is-flex-grow-3"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + + <div class="is-flex-grow-3"> + <div class="field-body "> + <div class="field"> + <div class="field has-addons"> + <p class={expand ? "control is-expanded " : "control "}> + <input + class="input" + type="text" + readonly + value={strValue} + placeholder={placeholder} + onClick={() => { + if (!readonly) setOpened(true); + }} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + <div + class="control" + onClick={() => { + if (!readonly) setOpened(true); + }} + > + <a class="button is-static"> + <span class="icon"> + <i class="mdi mdi-clock" /> + </span> + </a> + </div> + </div> + {error && <p class="help is-danger">{error}</p>} + </div> + {withForever && ( + <span data-tooltip={i18n.str`change value to never`}> + <button + class="button is-info mr-3" + onClick={() => onChange({ d_ms: "forever" } as any)} + > + <i18n.Translate>forever</i18n.Translate> + </button> + </span> + )} + {!readonly && !withoutClear && ( + <span data-tooltip={i18n.str`change value to empty`}> + <button + class="button is-info " + onClick={() => onChange(undefined as any)} + > + <i18n.Translate>clear</i18n.Translate> + </button> + </span> + )} + {side} + </div> + <span> + {help} + </span> + </div> + + + {opened && ( + <SimpleModal onCancel={() => setOpened(false)}> + <DurationPicker + days + hours + minutes + value={!value || value.d_ms === "forever" ? 0 : value.d_ms} + onChange={(v) => { + onChange({ d_ms: v } as any); + }} + /> + </SimpleModal> + )} + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx b/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx new file mode 100644 index 000000000..b5e0bd52b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx @@ -0,0 +1,86 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useGroupField } from "./useGroupField.js"; + +export interface Props<T> { + name: T; + children: ComponentChildren; + label: ComponentChildren; + tooltip?: ComponentChildren; + alternative?: ComponentChildren; + fixed?: boolean; + initialActive?: boolean; +} + +export function InputGroup<T>({ + name, + label, + children, + tooltip, + alternative, + fixed, + initialActive, +}: Props<keyof T>): VNode { + const [active, setActive] = useState(initialActive || fixed); + const group = useGroupField<T>(name); + + return ( + <div class="card"> + <header class="card-header"> + <p class="card-header-title"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + {group?.hasError && ( + <span class="icon has-text-danger" data-tooltip={tooltip}> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + {!fixed && ( + <button + class="card-header-icon" + aria-label="more options" + onClick={(): void => setActive(!active)} + > + <span class="icon"> + {active ? ( + <i class="mdi mdi-arrow-up" /> + ) : ( + <i class="mdi mdi-arrow-down" /> + )} + </span> + </button> + )} + </header> + {active ? ( + <div class="card-content">{children}</div> + ) : alternative ? ( + <div class="card-content">{alternative}</div> + ) : undefined} + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx b/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx new file mode 100644 index 000000000..b024e2c6b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx @@ -0,0 +1,122 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, h, VNode } from "preact"; +import { useRef, useState } from "preact/hooks"; +import { MAX_IMAGE_SIZE as MAX_IMAGE_UPLOAD_SIZE } from "../../utils/constants.js"; +import { InputProps, useField } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + addonAfter?: ComponentChildren; + children?: ComponentChildren; +} + +export function InputImage<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + children, + expand, +}: Props<keyof T>): VNode { + const { error, value, onChange } = useField<T>(name); + + const image = useRef<HTMLInputElement>(null); + const { i18n } = useTranslationContext(); + const [sizeError, setSizeError] = useState(false); + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class={expand ? "control is-expanded" : "control"}> + {value && ( + <img + src={value} + style={{ width: 200, height: 200 }} + onClick={() => image.current?.click()} + /> + )} + <input + ref={image} + style={{ display: "none" }} + type="file" + name={String(name)} + placeholder={placeholder} + readonly={readonly} + onChange={(e) => { + const f: FileList | null = e.currentTarget.files; + if (!f || f.length != 1) { + return onChange(undefined!); + } + if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) { + setSizeError(true); + return onChange(undefined!); + } + setSizeError(false); + return f[0].arrayBuffer().then((b) => { + const b64 = window.btoa( + new Uint8Array(b).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ), + ); + return onChange(`data:${f[0].type};base64,${b64}` as any); + }); + }} + /> + {help} + {children} + </p> + {error && <p class="help is-danger">{error}</p>} + {sizeError && ( + <p class="help is-danger"> + <i18n.Translate>Image should be smaller than 1 MB</i18n.Translate> + </p> + )} + {!value && ( + <button class="button" onClick={() => image.current?.click()}> + <i18n.Translate>Add</i18n.Translate> + </button> + )} + {value && ( + <button class="button" onClick={() => onChange(undefined!)}> + <i18n.Translate>Remove</i18n.Translate> + </button> + )} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx b/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx new file mode 100644 index 000000000..a2fc8113e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx @@ -0,0 +1,53 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { Fragment, h } from "preact"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Input } from "./Input.js"; + +export function InputLocation({ name }: { name: string }) { + const { i18n } = useTranslationContext(); + return ( + <> + <Input name={`${name}.country`} label={i18n.str`Country`} /> + <Input + name={`${name}.address_lines`} + inputType="multiline" + label={i18n.str`Address`} + toStr={(v: string[] | undefined) => (!v ? "" : v.join("\n"))} + fromStr={(v: string) => v.split("\n")} + /> + <Input + name={`${name}.building_number`} + label={i18n.str`Building number`} + /> + <Input name={`${name}.building_name`} label={i18n.str`Building name`} /> + <Input name={`${name}.street`} label={i18n.str`Street`} /> + <Input name={`${name}.post_code`} label={i18n.str`Post code`} /> + <Input name={`${name}.town_location`} label={i18n.str`Town location`} /> + <Input name={`${name}.town`} label={i18n.str`Town`} /> + <Input name={`${name}.district`} label={i18n.str`District`} /> + <Input + name={`${name}.country_subdivision`} + label={i18n.str`Country subdivision`} + /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx b/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx new file mode 100644 index 000000000..3b5df1474 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h } from "preact"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { InputProps } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + readonly?: boolean; + expand?: boolean; + side?: ComponentChildren; + children?: ComponentChildren; +} + +export function InputNumber<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + expand, + children, + side, +}: Props<keyof T>) { + return ( + <InputWithAddon<T> + name={name} + readonly={readonly} + fromStr={(v) => (!v ? undefined : parseInt(v, 10))} + toStr={(v) => `${v}`} + inputType="number" + expand={expand} + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + inputExtra={{ min: 0 }} + children={children} + side={side} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx new file mode 100644 index 000000000..6e88e8f2c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx @@ -0,0 +1,52 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputArray } from "./InputArray.js"; +import { PAYTO_REGEX } from "../../utils/constants.js"; +import { InputProps } from "./useField.js"; + +export type Props<T> = InputProps<T>; + +const PAYTO_START_REGEX = /^payto:\/\//; + +export function InputPayto<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, +}: Props<keyof T>): VNode { + return ( + <InputArray<T> + name={name} + readonly={readonly} + addonBefore="payto://" + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + isValid={(v) => v && PAYTO_REGEX.test(v)} + toStr={(v?: string) => (!v ? "" : v.replace(PAYTO_START_REGEX, ""))} + fromStr={(v: string) => `payto://${v}`} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx new file mode 100644 index 000000000..282e52278 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx @@ -0,0 +1,47 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h } from "preact"; +import * as tests from "@gnu-taler/web-util/testing"; +import { InputPaytoForm } from "./InputPaytoForm.js"; +import { FormProvider } from "./FormProvider.js"; +import { useState } from "preact/hooks"; + +export default { + title: "Components/Form/PayTo", + component: InputPaytoForm, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +export const Example = tests.createExample(() => { + const initial = { + accounts: [], + }; + const [form, updateForm] = useState<Partial<typeof initial>>(initial); + return ( + <FormProvider valueHandler={updateForm} object={form}> + <InputPaytoForm name="accounts" label="Accounts:" /> + </FormProvider> + ); +}, {}); diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx new file mode 100644 index 000000000..32545c89a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx @@ -0,0 +1,397 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { COUNTRY_TABLE } from "../../utils/constants.js"; +import { undefinedIfEmpty } from "../../utils/table.js"; +import { FormErrors, FormProvider } from "./FormProvider.js"; +import { Input } from "./Input.js"; +import { InputGroup } from "./InputGroup.js"; +import { InputSelector } from "./InputSelector.js"; +import { InputProps, useField } from "./useField.js"; +import { useEffect, useState } from "preact/hooks"; + +export interface Props<T> extends InputProps<T> { + isValid?: (e: any) => boolean; +} + +// type Entity = PaytoUriGeneric +// https://datatracker.ietf.org/doc/html/rfc8905 +type Entity = { + // iban, bitcoin, x-taler-bank. it defined the format + target: string; + // path1 if the first field to be used + path1?: string; + // path2 if the second field to be used, optional + path2?: string; + // params of the payto uri + params: { + "receiver-name"?: string; + sender?: string; + message?: string; + amount?: string; + instruction?: string; + [name: string]: string | undefined; + }; +}; + +function isEthereumAddress(address: string) { + if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) { + return false; + } else if ( + /^(0x|0X)?[0-9a-f]{40}$/.test(address) || + /^(0x|0X)?[0-9A-F]{40}$/.test(address) + ) { + return true; + } + return checkAddressChecksum(address); +} + +function checkAddressChecksum(address: string) { + //TODO implement ethereum checksum + return true; +} + +function validateBitcoin( + addr: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { + try { + const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr); + if (valid) return undefined; + } catch (e) { + console.log(e); + } + return i18n.str`This is not a valid bitcoin address.`; +} + +function validateEthereum( + addr: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { + try { + const valid = isEthereumAddress(addr); + if (valid) return undefined; + } catch (e) { + console.log(e); + } + return i18n.str`This is not a valid Ethereum address.`; +} + +/** + * An IBAN is validated by converting it into an integer and performing a + * basic mod-97 operation (as described in ISO 7064) on it. + * If the IBAN is valid, the remainder equals 1. + * + * The algorithm of IBAN validation is as follows: + * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid + * 2.- Move the four initial characters to the end of the string + * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35 + * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97 + * + * If the remainder is 1, the check digit test is passed and the IBAN might be valid. + * + */ +function validateIBAN( + iban: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { + // Check total length + if (iban.length < 4) + return i18n.str`IBAN numbers usually have more that 4 digits`; + if (iban.length > 34) + return i18n.str`IBAN numbers usually have less that 34 digits`; + + const A_code = "A".charCodeAt(0); + const Z_code = "Z".charCodeAt(0); + const IBAN = iban.toUpperCase(); + // check supported country + const code = IBAN.substr(0, 2); + const found = code in COUNTRY_TABLE; + if (!found) return i18n.str`IBAN country code not found`; + + // 2.- Move the four initial characters to the end of the string + const step2 = IBAN.substr(4) + iban.substr(0, 4); + const step3 = Array.from(step2) + .map((letter) => { + const code = letter.charCodeAt(0); + if (code < A_code || code > Z_code) return letter; + return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`; + }) + .join(""); + + function calculate_iban_checksum(str: string): number { + const numberStr = str.substr(0, 5); + const rest = str.substr(5); + const number = parseInt(numberStr, 10); + const result = number % 97; + if (rest.length > 0) { + return calculate_iban_checksum(`${result}${rest}`); + } + return result; + } + + const checksum = calculate_iban_checksum(step3); + if (checksum !== 1) + return i18n.str`IBAN number is not valid, checksum is wrong`; + return undefined; +} + +// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void', 'x-taler-bank'] +const targets = [ + "Choose one...", + "iban", + "x-taler-bank", + "bitcoin", + "ethereum", +]; +const noTargetValue = targets[0]; +const defaultTarget: Entity = { + target: noTargetValue, + params: {}, +}; + +export function InputPaytoForm<T>({ + name, + readonly, + label, + tooltip, +}: Props<keyof T>): VNode { + const { value: initialValueStr, onChange } = useField<T>(name); + + const initialPayto = parsePaytoUri(initialValueStr ?? "") + const paths = !initialPayto ? [] : initialPayto.targetPath.split("/") + const initialPath1 = paths.length >= 1 ? paths[0] : undefined; + const initialPath2 = paths.length >= 2 ? paths[1] : undefined; + const initial: Entity = initialPayto === undefined ? defaultTarget : { + target: initialPayto.targetType, + params: initialPayto.params, + path1: initialPath1, + path2: initialPath2, + } + const [value, setValue] = useState<Partial<Entity>>(initial) + + const { i18n } = useTranslationContext(); + + const errors: FormErrors<Entity> = { + target: + value.target === noTargetValue + ? i18n.str`required` + : undefined, + path1: !value.path1 + ? i18n.str`required` + : value.target === "iban" + ? validateIBAN(value.path1, i18n) + : value.target === "bitcoin" + ? validateBitcoin(value.path1, i18n) + : value.target === "ethereum" + ? validateEthereum(value.path1, i18n) + : undefined, + path2: + value.target === "x-taler-bank" + ? !value.path2 + ? i18n.str`required` + : undefined + : undefined, + params: undefinedIfEmpty({ + "receiver-name": !value.params?.["receiver-name"] + ? i18n.str`required` + : undefined, + }), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({ + targetType: value.target, + targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""), + params: value.params ?? {} as any, + isKnown: false, + }) + useEffect(() => { + onChange(str as any) + }, [str]) + + // const submit = useCallback((): void => { + // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos; + // // const alreadyExists = + // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; + // // if (!alreadyExists) { + // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = { + // payto_uri: paytoURL, + // }; + // if (value.auth) { + // if (value.auth.url) { + // newValue.credit_facade_url = value.auth.url; + // } + // if (value.auth.type === "none") { + // newValue.credit_facade_credentials = { + // type: "none", + // }; + // } + // if (value.auth.type === "basic") { + // newValue.credit_facade_credentials = { + // type: "basic", + // username: value.auth.username ?? "", + // password: value.auth.password ?? "", + // }; + // } + // } + // onChange(newValue as any); + // // } + // // valueHandler(defaultTarget); + // }, [value]); + + //FIXME: translating plural singular + return ( + <InputGroup name="payto" label={label} fixed tooltip={tooltip}> + <FormProvider<Entity> + name="tax" + errors={errors} + object={value} + valueHandler={setValue} + > + <InputSelector<Entity> + name="target" + label={i18n.str`Account type`} + tooltip={i18n.str`Method to use for wire transfer`} + values={targets} + readonly={readonly} + toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)} + /> + + {value.target === "ach" && ( + <Fragment> + <Input<Entity> + name="path1" + label={i18n.str`Routing`} + readonly={readonly} + tooltip={i18n.str`Routing number.`} + /> + <Input<Entity> + name="path2" + label={i18n.str`Account`} + readonly={readonly} + tooltip={i18n.str`Account number.`} + /> + </Fragment> + )} + {value.target === "bic" && ( + <Fragment> + <Input<Entity> + name="path1" + label={i18n.str`Code`} + readonly={readonly} + tooltip={i18n.str`Business Identifier Code.`} + /> + </Fragment> + )} + {value.target === "iban" && ( + <Fragment> + <Input<Entity> + name="path1" + label={i18n.str`IBAN`} + tooltip={i18n.str`International Bank Account Number.`} + readonly={readonly} + placeholder="DE1231231231" + inputExtra={{ style: { textTransform: "uppercase" } }} + /> + </Fragment> + )} + {value.target === "upi" && ( + <Fragment> + <Input<Entity> + name="path1" + readonly={readonly} + label={i18n.str`Account`} + tooltip={i18n.str`Unified Payment Interface.`} + /> + </Fragment> + )} + {value.target === "bitcoin" && ( + <Fragment> + <Input<Entity> + name="path1" + readonly={readonly} + label={i18n.str`Address`} + tooltip={i18n.str`Bitcoin protocol.`} + /> + </Fragment> + )} + {value.target === "ethereum" && ( + <Fragment> + <Input<Entity> + name="path1" + readonly={readonly} + label={i18n.str`Address`} + tooltip={i18n.str`Ethereum protocol.`} + /> + </Fragment> + )} + {value.target === "ilp" && ( + <Fragment> + <Input<Entity> + name="path1" + readonly={readonly} + label={i18n.str`Address`} + tooltip={i18n.str`Interledger protocol.`} + /> + </Fragment> + )} + {value.target === "void" && <Fragment />} + {value.target === "x-taler-bank" && ( + <Fragment> + <Input<Entity> + name="path1" + readonly={readonly} + label={i18n.str`Host`} + tooltip={i18n.str`Bank host.`} + /> + <Input<Entity> + name="path2" + readonly={readonly} + label={i18n.str`Account`} + tooltip={i18n.str`Bank account.`} + /> + </Fragment> + )} + + {/** + * Show additional fields apart from the payto + */} + {value.target !== noTargetValue && ( + <Fragment> + <Input + name="params.receiver-name" + readonly={readonly} + label={i18n.str`Owner's name`} + tooltip={i18n.str`Legal name of the person holding the account.`} + /> + </Fragment> + )} + + </FormProvider> + </InputGroup> + ); +} + diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx new file mode 100644 index 000000000..be5800d14 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -0,0 +1,204 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import emptyImage from "../../assets/empty.png"; +import { FormErrors, FormProvider } from "./FormProvider.js"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +type Entity = { + id: string, + description: string; + image?: string; + extra?: string; +}; + +export interface Props<T extends Entity> { + selected?: T; + onChange: (p?: T) => void; + label: TranslatedString; + list: T[]; + withImage?: boolean; +} + +interface Search { + name: string; +} + +export function InputSearchOnList<T extends Entity>({ + selected, + onChange, + label, + list, + withImage, +}: Props<T>): VNode { + const [nameForm, setNameForm] = useState<Partial<Search>>({ + name: "", + }); + + const errors: FormErrors<Search> = { + name: undefined, + }; + const { i18n } = useTranslationContext(); + + if (selected) { + return ( + <article class="media"> + {withImage && + <figure class="media-left"> + <p class="image is-128x128"> + <img src={selected.image ? selected.image : emptyImage} /> + </p> + </figure> + } + <div class="media-content"> + <div class="content"> + <p class="media-meta"> + <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b> + </p> + <p> + <i18n.Translate>Description</i18n.Translate>:{" "} + {selected.description} + </p> + <div class="buttons is-right mt-5"> + <button + class="button is-info" + onClick={() => onChange(undefined)} + > + clear + </button> + </div> + </div> + </div> + </article> + ); + } + + return ( + <FormProvider<Search> + errors={errors} + object={nameForm} + valueHandler={setNameForm} + > + <InputWithAddon<Search> + name="name" + label={label} + tooltip={i18n.str`enter description or id`} + addonAfter={ + <span class="icon"> + <i class="mdi mdi-magnify" /> + </span> + } + > + <div> + <DropdownList + name={nameForm.name} + list={list} + onSelect={(p) => { + setNameForm({ name: "" }); + onChange(p); + }} + withImage={!!withImage} + /> + </div> + </InputWithAddon> + </FormProvider> + ); +} + +interface DropdownListProps<T extends Entity> { + name?: string; + onSelect: (p: T) => void; + list: T[]; + withImage: boolean; +} + +function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) { + const { i18n } = useTranslationContext(); + if (!name) { + /* FIXME + this BR is added to occupy the space that will be added when the + dropdown appears + */ + return ( + <div> + <br /> + </div> + ); + } + const filtered = list.filter( + (p) => p.id.includes(name) || p.description.includes(name), + ); + + return ( + <div class="dropdown is-active"> + <div + class="dropdown-menu" + id="dropdown-menu" + role="menu" + style={{ minWidth: "20rem" }} + > + <div class="dropdown-content"> + {!filtered.length ? ( + <div class="dropdown-item"> + <i18n.Translate> + no match found with that description or id + </i18n.Translate> + </div> + ) : ( + filtered.map((p) => ( + <div + key={p.id} + class="dropdown-item" + onClick={() => onSelect(p)} + style={{ cursor: "pointer" }} + > + <article class="media"> + {withImage && + <div class="media-left"> + <div class="image" style={{ minWidth: 64 }}> + <img + src={p.image ? p.image : emptyImage} + style={{ width: 64, height: 64 }} + /> + </div> + </div> + } + <div class="media-content"> + <div class="content"> + <p> + <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined} + <br /> + {p.description} + </p> + </div> + </div> + </article> + </div> + )) + )} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx new file mode 100644 index 000000000..12ce6c6aa --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "./FormProvider.js"; +import { InputSecured } from "./InputSecured.js"; + +export default { + title: "Components/Form/InputSecured", + component: InputSecured, +}; + +type T = { auth_token: string | null }; + +export const InitialValueEmpty = (): VNode => { + const [state, setState] = useState<Partial<T>>({ auth_token: "" }); + return ( + <FormProvider<T> object={state} errors={{}} valueHandler={setState}> + Initial value: '' + <InputSecured<T> name="auth_token" label="Access token" /> + </FormProvider> + ); +}; + +export const InitialValueToken = (): VNode => { + const [state, setState] = useState<Partial<T>>({ auth_token: "token" }); + return ( + <FormProvider<T> object={state} errors={{}} valueHandler={setState}> + <InputSecured<T> name="auth_token" label="Access token" /> + </FormProvider> + ); +}; + +export const InitialValueNull = (): VNode => { + const [state, setState] = useState<Partial<T>>({ auth_token: null }); + return ( + <FormProvider<T> object={state} errors={{}} valueHandler={setState}> + Initial value: '' + <InputSecured<T> name="auth_token" label="Access token" /> + </FormProvider> + ); +}; diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx new file mode 100644 index 000000000..9d1a3ab8e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx @@ -0,0 +1,186 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { InputProps, useField } from "./useField.js"; + +export type Props<T> = InputProps<T>; + +const TokenStatus = ({ prev, post }: any) => { + const { i18n } = useTranslationContext(); + if ( + (prev === undefined || prev === null) && + (post === undefined || post === null) + ) + return null; + return prev === post ? null : post === null ? ( + <span class="tag is-danger is-align-self-center ml-2"> + <i18n.Translate>Deleting</i18n.Translate> + </span> + ) : ( + <span class="tag is-warning is-align-self-center ml-2"> + <i18n.Translate>Changing</i18n.Translate> + </span> + ); +}; + +export function InputSecured<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, +}: Props<keyof T>): VNode { + const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name); + + const [active, setActive] = useState(false); + const [newValue, setNuewValue] = useState(""); + + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + {!active ? ( + <Fragment> + <div class="field has-addons"> + <button + class="button" + onClick={(): void => { + setActive(!active); + }} + > + <div class="icon is-left"> + <i class="mdi mdi-lock-reset" /> + </div> + <span> + <i18n.Translate>Manage access token</i18n.Translate> + </span> + </button> + <TokenStatus prev={initial} post={value} /> + </div> + </Fragment> + ) : ( + <Fragment> + <div class="field has-addons"> + <div class="control"> + <a class="button is-static">secret-token:</a> + </div> + <div class="control is-expanded"> + <input + class="input" + type="text" + placeholder={placeholder} + readonly={readonly || !active} + disabled={readonly || !active} + name={String(name)} + value={newValue} + onInput={(e): void => { + setNuewValue(e.currentTarget.value); + }} + /> + {help} + </div> + <div class="control"> + <button + class="button is-info" + disabled={fromStr(newValue) === value} + onClick={(): void => { + onChange(fromStr(newValue)); + setActive(!active); + setNuewValue(""); + }} + > + <div class="icon is-left"> + <i class="mdi mdi-lock-outline" /> + </div> + <span> + <i18n.Translate>Update</i18n.Translate> + </span> + </button> + </div> + </div> + </Fragment> + )} + {error ? <p class="help is-danger">{error}</p> : null} + </div> + </div> + {active && ( + <div class="field is-horizontal"> + <div class="field-body is-flex-grow-3"> + <div class="level" style={{ width: "100%" }}> + <div class="level-right is-flex-grow-1"> + <div class="level-item"> + <button + class="button is-danger" + disabled={null === value || undefined === value} + onClick={(): void => { + onChange(null!); + setActive(!active); + setNuewValue(""); + }} + > + <div class="icon is-left"> + <i class="mdi mdi-lock-open-variant" /> + </div> + <span> + <i18n.Translate>Remove</i18n.Translate> + </span> + </button> + </div> + <div class="level-item"> + <button + class="button " + onClick={(): void => { + onChange(initial!); + setActive(!active); + setNuewValue(""); + }} + > + <div class="icon is-left"> + <i class="mdi mdi-lock-open-variant" /> + </div> + <span> + <i18n.Translate>Cancel</i18n.Translate> + </span> + </button> + </div> + </div> + </div> + </div> + </div> + )} + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx new file mode 100644 index 000000000..a8dad5d89 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx @@ -0,0 +1,94 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + readonly?: boolean; + expand?: boolean; + values: any[]; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +export function InputSelector<T>({ + name, + readonly, + expand, + placeholder, + tooltip, + label, + help, + values, + fromStr = defaultFromString, + toStr = defaultToString, +}: Props<keyof T>): VNode { + const { error, value, onChange, required } = useField<T>(name); + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field has-icons-right"> + <p class={expand ? "control is-expanded select" : "control select "}> + <select + class={error ? "select is-danger" : "select"} + name={String(name)} + disabled={readonly} + readonly={readonly} + onChange={(e) => { + onChange(fromStr(e.currentTarget.value)); + }} + > + {placeholder && <option>{placeholder}</option>} + {values.map((v, i) => { + return ( + <option key={i} value={v} selected={value === v}> + {toStr(v)} + </option> + ); + })} + </select> + + {help} + </p> + {required && ( + <span class="icon has-text-danger is-right" style={{height: "2.5em"}}> + <i class="mdi mdi-alert" /> + </span> + )} + {error && <p class="help is-danger">{error}</p>} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx new file mode 100644 index 000000000..668c65ea7 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx @@ -0,0 +1,162 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { addDays } from "date-fns"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "./FormProvider.js"; +import { InputStock, Stock } from "./InputStock.js"; + +export default { + title: "Components/Form/InputStock", + component: InputStock, +}; + +type T = { stock?: Stock }; + +export const CreateStockEmpty = () => { + const [state, setState] = useState<Partial<T>>({}); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; + +export const CreateStockUnknownRestock = () => { + const [state, setState] = useState<Partial<T>>({ + stock: { + current: 10, + lost: 0, + sold: 0, + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; + +export const CreateStockNoRestock = () => { + const [state, setState] = useState<Partial<T>>({ + stock: { + current: 10, + lost: 0, + sold: 0, + nextRestock: { t_s: "never" }, + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; + +export const CreateStockWithRestock = () => { + const [state, setState] = useState<Partial<T>>({ + stock: { + current: 15, + lost: 0, + sold: 0, + nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 }, + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; + +export const UpdatingProductWithManagedStock = () => { + const [state, setState] = useState<Partial<T>>({ + stock: { + current: 100, + lost: 0, + sold: 0, + nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 }, + }, + }); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" alreadyExist /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; + +export const UpdatingProductWithInfiniteStock = () => { + const [state, setState] = useState<Partial<T>>({}); + return ( + <FormProvider<T> + name="product" + object={state} + errors={{}} + valueHandler={setState} + > + <InputStock<T> name="stock" label="Stock" alreadyExist /> + <div> + <pre>{JSON.stringify(state, undefined, 2)}</pre> + </div> + </FormProvider> + ); +}; diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx new file mode 100644 index 000000000..1d18685c5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx @@ -0,0 +1,224 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h } from "preact"; +import { useLayoutEffect, useState } from "preact/hooks"; +import { MerchantBackend, Timestamp } from "../../declaration.js"; +import { FormErrors, FormProvider } from "./FormProvider.js"; +import { InputDate } from "./InputDate.js"; +import { InputGroup } from "./InputGroup.js"; +import { InputLocation } from "./InputLocation.js"; +import { InputNumber } from "./InputNumber.js"; +import { InputProps, useField } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + alreadyExist?: boolean; +} + +type Entity = Stock; + +export interface Stock { + current: number; + lost: number; + sold: number; + address?: MerchantBackend.Location; + nextRestock?: Timestamp; +} + +interface StockDelta { + incoming: number; + lost: number; +} + +export function InputStock<T>({ + name, + tooltip, + label, + alreadyExist, +}: Props<keyof T>) { + const { error, value, onChange } = useField<T>(name); + + const [errors, setErrors] = useState<FormErrors<Entity>>({}); + + const [formValue, valueHandler] = useState<Partial<Entity>>(value); + const [addedStock, setAddedStock] = useState<StockDelta>({ + incoming: 0, + lost: 0, + }); + const { i18n } = useTranslationContext(); + + useLayoutEffect(() => { + if (!formValue) { + onChange(undefined as any); + } else { + onChange({ + ...formValue, + current: (formValue?.current || 0) + addedStock.incoming, + lost: (formValue?.lost || 0) + addedStock.lost, + } as any); + } + }, [formValue, addedStock]); + + if (!formValue) { + return ( + <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field has-addons"> + {!alreadyExist ? ( + <button + class="button" + data-tooltip={i18n.str`click here to configure the stock of the product, leave it as is and the backend will not control stock`} + onClick={(): void => { + valueHandler({ + current: 0, + lost: 0, + sold: 0, + } as Stock as any); + }} + > + <span> + <i18n.Translate>Manage stock</i18n.Translate> + </span> + </button> + ) : ( + <button + class="button" + data-tooltip={i18n.str`this product has been configured without stock control`} + disabled + > + <span> + <i18n.Translate>Infinite</i18n.Translate> + </span> + </button> + )} + </div> + </div> + </div> + </Fragment> + ); + } + + const currentStock = + (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0); + + const stockAddedErrors: FormErrors<typeof addedStock> = { + lost: + currentStock + addedStock.incoming < addedStock.lost + ? i18n.str`lost cannot be greater than current and incoming (max ${ + currentStock + addedStock.incoming + })` + : undefined, + }; + + // const stockUpdateDescription = stockAddedErrors.lost ? '' : ( + // !!addedStock.incoming || !!addedStock.lost ? + // i18n.str`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` : + // i18n.str`current stock will stay at ${currentStock}` + // ) + + return ( + <Fragment> + <div class="card"> + <header class="card-header"> + <p class="card-header-title"> + {label} + {tooltip && ( + <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </p> + </header> + <div class="card-content"> + <FormProvider<Entity> + name="stock" + errors={errors} + object={formValue} + valueHandler={valueHandler} + > + {alreadyExist ? ( + <Fragment> + <FormProvider + name="added" + errors={stockAddedErrors} + object={addedStock} + valueHandler={setAddedStock as any} + > + <InputNumber name="incoming" label={i18n.str`Incoming`} /> + <InputNumber name="lost" label={i18n.str`Lost`} /> + </FormProvider> + + {/* <div class="field is-horizontal"> + <div class="field-label is-normal" /> + <div class="field-body is-flex-grow-3"> + <div class="field"> + {stockUpdateDescription} + </div> + </div> + </div> */} + </Fragment> + ) : ( + <InputNumber<Entity> + name="current" + label={i18n.str`Current`} + side={ + <button + class="button is-danger" + data-tooltip={i18n.str`remove stock control for this product`} + onClick={(): void => { + valueHandler(undefined as any); + }} + > + <span> + <i18n.Translate>without stock</i18n.Translate> + </span> + </button> + } + /> + )} + + <InputDate<Entity> + name="nextRestock" + label={i18n.str`Next restock`} + withTimestampSupport + /> + + <InputGroup<Entity> name="address" label={i18n.str`Warehouse address`}> + <InputLocation name="address" /> + </InputGroup> + </FormProvider> + </div> + </div> + </Fragment> + ); +} +// ( diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx new file mode 100644 index 000000000..2701768aa --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx @@ -0,0 +1,90 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + readonly?: boolean; + expand?: boolean; + values: any[]; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +export function InputTab<T>({ + name, + readonly, + expand, + placeholder, + tooltip, + label, + help, + values, + fromStr = defaultFromString, + toStr = defaultToString, +}: Props<keyof T>): VNode { + const { error, value, onChange, required } = useField<T>(name); + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field has-icons-right"> + <p class={expand ? "control is-expanded " : "control "}> + <div class="tabs is-toggle is-fullwidth is-small"> + <ul> + {values.map((v, i) => { + return ( + <li key={i} class={value === v ? "is-active" : ""} + onClick={(e) => { onChange(v) }} + > + <a style={{ cursor: "initial" }}> + <span>{toStr(v)}</span> + </a> + </li> + ); + })} + </ul> + </div> + {help} + </p> + {required && ( + <span class="icon has-text-danger is-right" style={{ height: "2.5em" }}> + <i class="mdi mdi-alert" /> + </span> + )} + {error && <p class="help is-danger">{error}</p>} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx new file mode 100644 index 000000000..b5722e4ec --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx @@ -0,0 +1,147 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useCallback, useState } from "preact/hooks"; +import * as yup from "yup"; +import { MerchantBackend } from "../../declaration.js"; +import { TaxSchema as schema } from "../../schemas/index.js"; +import { FormErrors, FormProvider } from "./FormProvider.js"; +import { Input } from "./Input.js"; +import { InputGroup } from "./InputGroup.js"; +import { InputProps, useField } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + isValid?: (e: any) => boolean; +} + +type Entity = MerchantBackend.Tax; +export function InputTaxes<T>({ + name, + readonly, + label, +}: Props<keyof T>): VNode { + const { value: taxes, onChange } = useField<T>(name); + + const [value, valueHandler] = useState<Partial<Entity>>({}); + // const [errors, setErrors] = useState<FormErrors<Entity>>({}) + + let errors: FormErrors<Entity> = {}; + + try { + schema.validateSync(value, { abortEarly: false }); + } catch (err) { + if (err instanceof yup.ValidationError) { + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); + } + } + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submit = useCallback((): void => { + onChange([value as any, ...taxes] as any); + valueHandler({}); + }, [value]); + + const { i18n } = useTranslationContext(); + + //FIXME: translating plural singular + return ( + <InputGroup + name="tax" + label={label} + alternative={ + taxes.length > 0 && ( + <p>This product has {taxes.length} applicable taxes configured.</p> + ) + } + > + <FormProvider<Entity> + name="tax" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <div class="field is-horizontal"> + <div class="field-label is-normal" /> + <div class="field-body" style={{ display: "block" }}> + {taxes.map((v: any, i: number) => ( + <div + key={i} + class="tags has-addons mt-3 mb-0 mr-3" + style={{ flexWrap: "nowrap" }} + > + <span + class="tag is-medium is-info mb-0" + style={{ maxWidth: "90%" }} + > + <b>{v.tax}</b>: {v.name} + </span> + <a + class="tag is-medium is-danger is-delete mb-0" + onClick={() => { + onChange(taxes.filter((f: any) => f !== v) as any); + valueHandler(v); + }} + /> + </div> + ))} + {!taxes.length && i18n.str`No taxes configured for this product.`} + </div> + </div> + + <Input<Entity> + name="tax" + label={i18n.str`Amount`} + tooltip={i18n.str`Taxes can be in currencies that differ from the main currency used by the merchant.`} + > + <i18n.Translate> + Enter currency and value separated with a colon, e.g. + "USD:2.3". + </i18n.Translate> + </Input> + + <Input<Entity> + name="name" + label={i18n.str`Description`} + tooltip={i18n.str`Legal name of the tax, e.g. VAT or import duties.`} + /> + + <div class="buttons is-right mt-5"> + <button + class="button is-info" + data-tooltip={i18n.str`add tax to the tax list`} + disabled={hasErrors} + onClick={submit} + > + <i18n.Translate>Add</i18n.Translate> + </button> + </div> + </FormProvider> + </InputGroup> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx b/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx new file mode 100644 index 000000000..f95dfcd05 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx @@ -0,0 +1,91 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + name: T; + readonly?: boolean; + expand?: boolean; + threeState?: boolean; + toBoolean?: (v?: any) => boolean | undefined; + fromBoolean?: (s: boolean | undefined) => any; +} + +const defaultToBoolean = (f?: any): boolean | undefined => f || ""; +const defaultFromBoolean = (v: boolean | undefined): any => v as any; + +export function InputToggle<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + threeState, + expand, + fromBoolean = defaultFromBoolean, + toBoolean = defaultToBoolean, +}: Props<keyof T>): VNode { + const { error, value, onChange } = useField<T>(name); + + const onCheckboxClick = (): void => { + const c = toBoolean(value); + if (c === false && threeState) return onChange(undefined as any); + return onChange(fromBoolean(!c)); + }; + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label" > + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class={expand ? "control is-expanded" : "control"}> + <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}> + <input + type="checkbox" + class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"} + checked={toBoolean(value)} + placeholder={placeholder} + readonly={readonly} + name={String(name)} + disabled={readonly} + onChange={onCheckboxClick} + /> + <div class="toggle-switch"></div> + </label> + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx new file mode 100644 index 000000000..e9fd88770 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx @@ -0,0 +1,116 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + inputType?: "text" | "number" | "password"; + addonBefore?: ComponentChildren; + addonAfter?: ComponentChildren; + addonAfterAction?: () => void; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; + inputExtra?: any; + children?: ComponentChildren; + side?: ComponentChildren; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +export function InputWithAddon<T>({ + name, + readonly, + addonBefore, + children, + expand, + label, + placeholder, + help, + tooltip, + inputType, + inputExtra, + side, + addonAfter, + addonAfterAction, + toStr = defaultToString, + fromStr = defaultFromString, +}: Props<keyof T>): VNode { + const { error, value, onChange, required } = useField<T>(name); + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <div class="field has-addons"> + {addonBefore && ( + <div class="control"> + <a class="button is-static">{addonBefore}</a> + </div> + )} + <p + class={`control${expand ? " is-expanded" : ""}${required ? " has-icons-right" : "" + }`} + > + <input + {...(inputExtra || {})} + class={error ? "input is-danger" : "input"} + type={inputType} + placeholder={placeholder} + readonly={readonly} + disabled={readonly} + name={String(name)} + value={toStr(value)} + onChange={(e): void => onChange(fromStr(e.currentTarget.value))} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + {children} + </p> + {addonAfter && ( + <div class="control" onClick={addonAfterAction} style={{ cursor: addonAfterAction ? "pointer" : undefined }}> + <a class="button is-static">{addonAfter}</a> + </div> + )} + </div> + {error && <p class="help is-danger">{error}</p>} + <span class="has-text-grey">{help}</span> + </div> + {expand ? <div>{side}</div> : side} + </div> + + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx new file mode 100644 index 000000000..2ff23dfd3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -0,0 +1,59 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; + +export function JumpToElementById({ testIfExist, onSelect, palceholder, description }: { palceholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<any>, onSelect: (id: string) => void }): VNode { + const { i18n } = useTranslationContext() + + const [error, setError] = useState<string | undefined>( + undefined, + ); + + const [id, setId] = useState<string>() + async function check(currentId: string | undefined): Promise<void> { + if (!currentId) { + setError(i18n.str`missing id`); + return; + } + try { + await testIfExist(currentId); + onSelect(currentId); + setError(undefined); + } catch { + setError(i18n.str`not found`); + } + } + + return <div class="level"> + <div class="level-left"> + <div class="level-item"> + <div class="field has-addons"> + <div class="control"> + <input + class={error ? "input is-danger" : "input"} + type="text" + value={id ?? ""} + onChange={(e) => setId(e.currentTarget.value)} + placeholder={palceholder} + /> + {error && <p class="help is-danger">{error}</p>} + </div> + <span + class="has-tooltip-bottom" + data-tooltip={description} + > + <button + class="button" + onClick={(e) => check(id)} + > + <span class="icon"> + <i class="mdi mdi-arrow-right" /> + </span> + </button> + </span> + </div> + </div> + </div> + </div> +} diff --git a/packages/auditor-backoffice-ui/src/components/form/TextField.tsx b/packages/auditor-backoffice-ui/src/components/form/TextField.tsx new file mode 100644 index 000000000..03f36dcbb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/TextField.tsx @@ -0,0 +1,71 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { useField, InputProps } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + inputType?: "text" | "number" | "multiline" | "password"; + expand?: boolean; + side?: ComponentChildren; + children: ComponentChildren; +} + +export function TextField<T>({ + name, + tooltip, + label, + expand, + help, + children, + side, +}: Props<keyof T>): VNode { + const { error } = useField<T>(name); + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p + class={ + expand + ? "control is-expanded has-icons-right" + : "control has-icons-right" + } + > + {children} + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + {side} + </div> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/form/useField.tsx b/packages/auditor-backoffice-ui/src/components/form/useField.tsx new file mode 100644 index 000000000..c7559faae --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/useField.tsx @@ -0,0 +1,92 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ComponentChildren, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useFormContext } from "./FormProvider.js"; + +interface Use<V> { + error?: string; + required: boolean; + value: any; + initial: any; + onChange: (v: V) => void; + toStr: (f: V | undefined) => string; + fromStr: (v: string) => V; +} + +export function useField<T>(name: keyof T): Use<T[typeof name]> { + const { errors, object, initialObject, toStr, fromStr, valueHandler } = + useFormContext<T>(); + type P = typeof name; + type V = T[P]; + const [isDirty, setDirty] = useState(false); + const updateField = + (field: P) => + (value: V): void => { + setDirty(true); + return valueHandler((prev) => { + return setValueDeeper(prev, String(field).split("."), value); + }); + }; + + const defaultToString = (f?: V): string => String(!f ? "" : f); + const defaultFromString = (v: string): V => v as any; + const value = readField(object, String(name)); + const initial = readField(initialObject, String(name)); + const hasError = readField(errors, String(name)); + return { + error: isDirty ? hasError : undefined, + required: !isDirty && hasError, + value, + initial, + onChange: updateField(name) as any, + toStr: toStr[name] ? toStr[name]! : defaultToString, + fromStr: fromStr[name] ? fromStr[name]! : defaultFromString, + }; +} +/** + * read the field of an object an support accessing it using '.' + * + * @param object + * @param name + * @returns + */ +const readField = (object: any, name: string) => { + return name + .split(".") + .reduce((prev, current) => prev && prev[current], object); +}; + +const setValueDeeper = (object: any, names: string[], value: any): any => { + if (names.length === 0) return value; + const [head, ...rest] = names; + return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) }; +}; + +export interface InputProps<T> { + name: T; + label: ComponentChildren; + placeholder?: string; + tooltip?: ComponentChildren; + readonly?: boolean; + help?: ComponentChildren; +} diff --git a/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx b/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx new file mode 100644 index 000000000..9a445eb32 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx @@ -0,0 +1,41 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useFormContext } from "./FormProvider.js"; + +interface Use { + hasError?: boolean; +} + +export function useGroupField<T>(name: keyof T): Use { + const f = useFormContext<T>(); + if (!f) return {}; + + return { + hasError: readField(f.errors, String(name)), + }; +} + +const readField = (object: any, name: string) => { + return name + .split(".") + .reduce((prev, current) => prev && prev[current], object); +}; diff --git a/packages/auditor-backoffice-ui/src/components/index.stories.ts b/packages/auditor-backoffice-ui/src/components/index.stories.ts new file mode 100644 index 000000000..c57ddab14 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/index.stories.ts @@ -0,0 +1,17 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +export * as payto from "./form/InputPaytoForm.stories.js"; diff --git a/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx new file mode 100644 index 000000000..6f5881fc0 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx @@ -0,0 +1,124 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useBackendContext } from "../../context/backend.js"; +import { Entity } from "../../paths/admin/create/CreatePage.js"; +import { Input } from "../form/Input.js"; +import { InputDuration } from "../form/InputDuration.js"; +import { InputGroup } from "../form/InputGroup.js"; +import { InputImage } from "../form/InputImage.js"; +import { InputLocation } from "../form/InputLocation.js"; +import { InputSelector } from "../form/InputSelector.js"; +import { InputToggle } from "../form/InputToggle.js"; +import { InputWithAddon } from "../form/InputWithAddon.js"; + +export function DefaultInstanceFormFields({ + readonlyId, + showId, +}: { + readonlyId?: boolean; + showId: boolean; +}): VNode { + const { i18n } = useTranslationContext(); + const { url: backendURL } = useBackendContext() + return ( + <Fragment> + {showId && ( + <InputWithAddon<Entity> + name="id" + addonBefore={`${backendURL}/instances/`} + readonly={readonlyId} + label={i18n.str`Identifier`} + tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`} + /> + )} + + <Input<Entity> + name="name" + label={i18n.str`Business name`} + tooltip={i18n.str`Legal name of the business represented by this instance.`} + /> + + <InputSelector<Entity> + name="user_type" + label={i18n.str`Type`} + tooltip={i18n.str`Different type of account can have different rules and requirements.`} + values={["business", "individual"]} + /> + + <Input<Entity> + name="email" + label={i18n.str`Email`} + tooltip={i18n.str`Contact email`} + /> + + <Input<Entity> + name="website" + label={i18n.str`Website URL`} + tooltip={i18n.str`URL.`} + /> + + <InputImage<Entity> + name="logo" + label={i18n.str`Logo`} + tooltip={i18n.str`Logo image.`} + /> + + <InputToggle<Entity> + name="use_stefan" + label={i18n.str`Pay transaction fee`} + tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`} + /> + + <InputGroup + name="address" + label={i18n.str`Address`} + tooltip={i18n.str`Physical location of the merchant.`} + > + <InputLocation name="address" /> + </InputGroup> + + <InputGroup + name="jurisdiction" + label={i18n.str`Jurisdiction`} + tooltip={i18n.str`Jurisdiction for legal disputes with the merchant.`} + > + <InputLocation name="jurisdiction" /> + </InputGroup> + + <InputDuration<Entity> + name="default_pay_delay" + label={i18n.str`Default payment delay`} + withForever + tooltip={i18n.str`Time customers have to pay an order before the offer expires by default.`} + /> + + <InputDuration<Entity> + name="default_wire_transfer_delay" + label={i18n.str`Default wire transfer delay`} + tooltip={i18n.str`Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.`} + withForever + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx new file mode 100644 index 000000000..41fe1374a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx @@ -0,0 +1,92 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import langIcon from "../../assets/icons/languageicon.svg"; +import { strings as messages } from "../../i18n/strings.js"; + +type LangsNames = { + [P in keyof typeof messages]: string; +}; + +const names: LangsNames = { + es: "Español [es]", + en: "English [en]", + fr: "Français [fr]", + de: "Deutsch [de]", + sv: "Svenska [sv]", + it: "Italiano [it]", +}; + +function getLangName(s: keyof LangsNames | string) { + if (names[s]) return names[s]; + return s; +} + +export function LangSelector(): VNode { + const [updatingLang, setUpdatingLang] = useState(false); + const { lang, changeLanguage } = useTranslationContext(); + + return ( + <div class="dropdown is-active "> + <div class="dropdown-trigger"> + <button + class="button has-tooltip-left" + data-tooltip="change language selection" + aria-haspopup="true" + aria-controls="dropdown-menu" + onClick={() => setUpdatingLang(!updatingLang)} + > + <div class="icon is-small is-left"> + <img src={langIcon} /> + </div> + <span>{getLangName(lang)}</span> + <div class="icon is-right"> + <i class="mdi mdi-chevron-down" /> + </div> + </button> + </div> + {updatingLang && ( + <div class="dropdown-menu" id="dropdown-menu" role="menu"> + <div class="dropdown-content"> + {Object.keys(messages) + .filter((l) => l !== lang) + .map((l) => ( + <a + key={l} + class="dropdown-item" + value={l} + onClick={() => { + changeLanguage(l); + setUpdatingLang(false); + }} + > + {getLangName(l)} + </a> + ))} + </div> + </div> + )} + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx new file mode 100644 index 000000000..9f1b33893 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx @@ -0,0 +1,72 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import logo from "../../assets/logo-2021.svg"; + +interface Props { + onMobileMenu: () => void; + title: string; +} + +export function NavigationBar({ onMobileMenu, title }: Props): VNode { + return ( + <nav + class="navbar is-fixed-top" + role="navigation" + aria-label="main navigation" + > + <div class="navbar-brand"> + <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}> + {title} + </span> + + <a + role="button" + class="navbar-burger" + aria-label="menu" + aria-expanded="false" + onClick={(e) => { + onMobileMenu(); + e.stopPropagation(); + }} + > + <span aria-hidden="true" /> + <span aria-hidden="true" /> + <span aria-hidden="true" /> + </a> + </div> + + <div class="navbar-menu "> + <a + class="navbar-start is-justify-content-center is-flex-grow-1" + href="https://taler.net" + > + <img src={logo} style={{ height: 35, margin: 10 }} /> + </a> + <div class="navbar-end"> + <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> + </div> + </div> + </div> + </nav> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx new file mode 100644 index 000000000..cfc00148e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx @@ -0,0 +1,284 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useBackendContext } from "../../context/backend.js"; +import { useConfigContext } from "../../context/config.js"; +import { useInstanceKYCDetails } from "../../hooks/instance.js"; +import { LangSelector } from "./LangSelector.js"; + +const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; +const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; + +interface Props { + onLogout: () => void; + onShowSettings: () => void; + mobile?: boolean; + instance: string; + admin?: boolean; + mimic?: boolean; + isPasswordOk: boolean; +} + +export function Sidebar({ + mobile, + instance, + onShowSettings, + onLogout, + admin, + mimic, + isPasswordOk +}: Props): VNode { + const config = useConfigContext(); + const { url: backendURL } = useBackendContext() + const { i18n } = useTranslationContext(); + const kycStatus = useInstanceKYCDetails(); + const needKYC = kycStatus.ok && kycStatus.data.type === "redirect"; + + return ( + <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}> + {mobile && ( + <div + class="footer" + onClick={(e) => { + return e.stopImmediatePropagation(); + }} + > + <LangSelector /> + </div> + )} + <div class="aside-tools"> + <div class="aside-tools-label"> + <div> + <b>Taler</b> Backoffice + </div> + <div + class="is-size-7 has-text-right" + style={{ lineHeight: 0, marginTop: -10 }} + > + {VERSION} ({config.version}) + </div> + </div> + </div> + <div class="menu is-menu-main"> + {instance ? ( + <Fragment> + <ul class="menu-list"> + <li> + <a href={"/orders"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-cash-register" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Orders</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/inventory"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-shopping" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Inventory</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/transfers"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-arrow-left-right" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Transfers</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/templates"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Templates</i18n.Translate> + </span> + </a> + </li> + {needKYC && ( + <li> + <a href={"/kyc"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-account-check" /> + </span> + <span class="menu-item-label">KYC Status</span> + </a> + </li> + )} + </ul> + <p class="menu-label"> + <i18n.Translate>Configuration</i18n.Translate> + </p> + <ul class="menu-list"> + <li> + <a href={"/bank"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-bank" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Bank account</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/otp-devices"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-lock" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>OTP Devices</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/reserves"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-cash" /> + </span> + <span class="menu-item-label">Reserves</span> + </a> + </li> + <li> + <a href={"/webhooks"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Webhooks</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/settings"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-square-edit-outline" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Settings</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/token"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-security" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Access token</i18n.Translate> + </span> + </a> + </li> + </ul> + </Fragment> + ) : undefined} + <p class="menu-label"> + <i18n.Translate>Connection</i18n.Translate> + </p> + <ul class="menu-list"> + <li> + <a class="has-icon is-state-info is-hoverable" + onClick={(): void => onShowSettings()} + > + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Interface</i18n.Translate> + </span> + </a> + </li> + <li> + <div> + <span style={{ width: "3rem" }} class="icon"> + <i class="mdi mdi-web" /> + </span> + <span class="menu-item-label"> + {new URL(backendURL).hostname} + </span> + </div> + </li> + <li> + <div> + <span style={{ width: "3rem" }} class="icon"> + ID + </span> + <span class="menu-item-label"> + {!instance ? "default" : instance} + </span> + </div> + </li> + {admin && !mimic && ( + <Fragment> + <p class="menu-label"> + <i18n.Translate>Instances</i18n.Translate> + </p> + <li> + <a href={"/instance/new"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-plus" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>New</i18n.Translate> + </span> + </a> + </li> + <li> + <a href={"/instances"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-format-list-bulleted" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>List</i18n.Translate> + </span> + </a> + </li> + </Fragment> + )} + {isPasswordOk ? + <li> + <a + class="has-icon is-state-info is-hoverable" + onClick={(): void => onLogout()} + > + <span class="icon"> + <i class="mdi mdi-logout default" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Log out</i18n.Translate> + </span> + </a> + </li> : undefined + } + </ul> + </div> + </aside> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/menu/index.tsx b/packages/auditor-backoffice-ui/src/components/menu/index.tsx new file mode 100644 index 000000000..03ae3b005 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx @@ -0,0 +1,269 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { AdminPaths } from "../../AdminRoutes.js"; +import { InstancePaths } from "../../InstanceRoutes.js"; +import { Notification } from "../../utils/types.js"; +import { NavigationBar } from "./NavigationBar.js"; +import { Sidebar } from "./SideBar.js"; + +function getInstanceTitle(path: string, id: string): string { + switch (path) { + case InstancePaths.settings: + return `${id}: Settings`; + case InstancePaths.order_list: + return `${id}: Orders`; + case InstancePaths.order_new: + return `${id}: New order`; + case InstancePaths.inventory_list: + return `${id}: Inventory`; + case InstancePaths.deposit_confirmation_list: + return `${id}: Deposit Confirmation`; + case InstancePaths.inventory_new: + return `${id}: New product`; + case InstancePaths.inventory_update: + return `${id}: Update product`; + case InstancePaths.reserves_new: + return `${id}: New reserve`; + case InstancePaths.reserves_list: + return `${id}: Reserves`; + case InstancePaths.transfers_list: + return `${id}: Transfers`; + case InstancePaths.transfers_new: + return `${id}: New transfer`; + case InstancePaths.webhooks_list: + return `${id}: Webhooks`; + case InstancePaths.webhooks_new: + return `${id}: New webhook`; + case InstancePaths.webhooks_update: + return `${id}: Update webhook`; + case InstancePaths.otp_devices_list: + return `${id}: otp devices`; + case InstancePaths.otp_devices_new: + return `${id}: New otp devices`; + case InstancePaths.otp_devices_update: + return `${id}: Update otp devices`; + case InstancePaths.templates_new: + return `${id}: New template`; + case InstancePaths.templates_update: + return `${id}: Update template`; + case InstancePaths.templates_list: + return `${id}: Templates`; + case InstancePaths.templates_use: + return `${id}: Use template`; + case InstancePaths.interface: + return `${id}: Interface`; + default: + return ""; + } +} + +function getAdminTitle(path: string, instance: string) { + if (path === AdminPaths.new_instance) return `New instance`; + if (path === AdminPaths.list_instances) return `Instances`; + return getInstanceTitle(path, instance); +} + +interface MenuProps { + title?: string; + path: string; + instance: string; + admin?: boolean; + onLogout?: () => void; + onShowSettings: () => void; + setInstanceName: (s: string) => void; + isPasswordOk: boolean; +} + +function WithTitle({ + title, + children, +}: { + title: string; + children: ComponentChildren; +}): VNode { + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + return <Fragment>{children}</Fragment>; +} + +export function Menu({ + onLogout, + onShowSettings, + title, + instance, + path, + admin, + setInstanceName, + isPasswordOk +}: MenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + + const titleWithSubtitle = title + ? title + : !admin + ? getInstanceTitle(path, instance) + : getAdminTitle(path, instance); + const adminInstance = instance === "default"; + const mimic = admin && !adminInstance; + return ( + <WithTitle title={titleWithSubtitle}> + <div + class={mobileOpen ? "has-aside-mobile-expanded" : ""} + onClick={() => setMobileOpen(false)} + > + <NavigationBar + onMobileMenu={() => setMobileOpen(!mobileOpen)} + title={titleWithSubtitle} + /> + + {onLogout && ( + <Sidebar + onShowSettings={onShowSettings} + onLogout={onLogout} + admin={admin} + mimic={mimic} + instance={instance} + mobile={mobileOpen} + isPasswordOk={isPasswordOk} + /> + )} + + {mimic && ( + <nav class="level" style={{ + zIndex: 100, + position: "fixed", + width: "50%", + marginLeft: "20%" + }}> + <div class="level-item has-text-centered has-background-warning"> + <p class="is-size-5"> + You are viewing the instance <b>"{instance}"</b>.{" "} + <a + href="#/instances" + onClick={(e) => { + setInstanceName("default"); + }} + > + go back + </a> + </p> + </div> + </nav> + )} + </div> + </WithTitle> + ); +} + +interface NotYetReadyAppMenuProps { + title: string; + onShowSettings: () => void; + onLogout?: () => void; + isPasswordOk: boolean; +} + +interface NotifProps { + notification?: Notification; +} +export function NotificationCard({ + notification: n, +}: NotifProps): VNode | null { + if (!n) return null; + return ( + <div class="notification"> + <div class="columns is-vcentered"> + <div class="column is-12"> + <article + class={ + n.type === "ERROR" + ? "message is-danger" + : n.type === "WARN" + ? "message is-warning" + : "message is-info" + } + > + <div class="message-header"> + <p>{n.message}</p> + </div> + {n.description && ( + <div class="message-body"> + <div>{n.description}</div> + {n.details && <pre>{n.details}</pre>} + </div> + )} + </article> + </div> + </div> + </div> + ); +} + +interface NotConnectedAppMenuProps { + title: string; +} +export function NotConnectedAppMenu({ + title, +}: NotConnectedAppMenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + + return ( + <div + class={mobileOpen ? "has-aside-mobile-expanded" : ""} + onClick={() => setMobileOpen(false)} + > + <NavigationBar + onMobileMenu={() => setMobileOpen(!mobileOpen)} + title={title} + /> + </div> + ); +} + +export function NotYetReadyAppMenu({ + onLogout, + onShowSettings, + title, + isPasswordOk +}: NotYetReadyAppMenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + document.title = `Taler Backoffice: ${title}`; + }, [title]); + + return ( + <div + class={mobileOpen ? "has-aside-mobile-expanded" : ""} + onClick={() => setMobileOpen(false)} + > + <NavigationBar + onMobileMenu={() => setMobileOpen(!mobileOpen)} + title={title} + /> + {onLogout && ( + <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} /> + )} + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/modal/index.tsx b/packages/auditor-backoffice-ui/src/components/modal/index.tsx new file mode 100644 index 000000000..8372c84cc --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/modal/index.tsx @@ -0,0 +1,496 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useInstanceContext } from "../../context/instance.js"; +import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js"; +import { Spinner } from "../exception/loading.js"; +import { FormProvider } from "../form/FormProvider.js"; +import { Input } from "../form/Input.js"; + +interface Props { + active?: boolean; + description?: string; + onCancel?: () => void; + onConfirm?: () => void; + label?: string; + children?: ComponentChildren; + danger?: boolean; + disabled?: boolean; +} + +export function ConfirmModal({ + active, + description, + onCancel, + onConfirm, + children, + danger, + disabled, + label = "Confirm", +}: Props): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card" style={{ maxWidth: 700 }}> + <header class="modal-card-head"> + {!description ? null : ( + <p class="modal-card-title"> + <b>{description}</b> + </p> + )} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body">{children}</section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + {onConfirm ? ( + <Fragment> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + + <button + class={danger ? "button is-danger " : "button is-info "} + disabled={disabled} + onClick={onConfirm} + > + <i18n.Translate>{label}</i18n.Translate> + </button> + </Fragment> + ) : ( + <button class="button " onClick={onCancel}> + <i18n.Translate>Close</i18n.Translate> + </button> + )} + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function ContinueModal({ + active, + description, + onCancel, + onConfirm, + children, + disabled, +}: Props): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head has-background-success"> + {!description ? null : <p class="modal-card-title">{description}</p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body">{children}</section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button + class="button is-success " + disabled={disabled} + onClick={onConfirm} + > + <i18n.Translate>Continue</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function SimpleModal({ onCancel, children }: any): VNode { + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <section class="modal-card-body is-main-section">{children}</section> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function ClearConfirmModal({ + description, + onCancel, + onClear, + onConfirm, + children, +}: Props & { onClear?: () => void }): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + {!description ? null : <p class="modal-card-title">{description}</p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body is-main-section">{children}</section> + <footer class="modal-card-foot"> + {onClear && ( + <button + class="button is-danger" + onClick={onClear} + disabled={onClear === undefined} + > + <i18n.Translate>Clear</i18n.Translate> + </button> + )} + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <button + class="button is-info" + onClick={onConfirm} + disabled={onConfirm === undefined} + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +interface DeleteModalProps { + element: { id: string; name: string }; + onCancel: () => void; + onConfirm: (id: string) => void; +} + +export function DeleteModal({ + element, + onCancel, + onConfirm, +}: DeleteModalProps): VNode { + return ( + <ConfirmModal + label={`Delete instance`} + description={`Delete the instance "${element.name}"`} + danger + active + onCancel={onCancel} + onConfirm={() => onConfirm(element.id)} + > + <p> + If you delete the instance named <b>"{element.name}"</b> (ID:{" "} + <b>{element.id}</b>), the merchant will no longer be able to process + orders or refunds + </p> + <p> + This action deletes the instance private key, but preserves all + transaction data. You can still access that data after deleting the + instance. + </p> + <p class="warning"> + Deleting an instance <b>cannot be undone</b>. + </p> + </ConfirmModal> + ); +} + +export function PurgeModal({ + element, + onCancel, + onConfirm, +}: DeleteModalProps): VNode { + return ( + <ConfirmModal + label={`Purge the instance`} + description={`Purge the instance "${element.name}"`} + danger + active + onCancel={onCancel} + onConfirm={() => onConfirm(element.id)} + > + <p> + If you purge the instance named <b>"{element.name}"</b> (ID:{" "} + <b>{element.id}</b>), you will also delete all it's transaction + data. + </p> + <p> + The instance will disappear from your list, and you will no longer be + able to access it's data. + </p> + <p class="warning"> + Purging an instance <b>cannot be undone</b>. + </p> + </ConfirmModal> + ); +} + +interface UpdateTokenModalProps { + oldToken?: string; + onCancel: () => void; + onConfirm: (value: string) => void; + onClear: () => void; +} + +//FIXME: merge UpdateTokenModal with SetTokenNewInstanceModal +export function UpdateTokenModal({ + onCancel, + onClear, + onConfirm, + oldToken, +}: UpdateTokenModalProps): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; + const [form, setValue] = useState<Partial<State>>({ + old_token: "", + new_token: "", + repeat_token: "", + }); + const { i18n } = useTranslationContext(); + + const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token; + const errors = { + old_token: hasInputTheCorrectOldToken + ? i18n.str`is not the same as the current access token` + : undefined, + new_token: !form.new_token + ? i18n.str`cannot be empty` + : form.new_token === form.old_token + ? i18n.str`cannot be the same as the old token` + : undefined, + repeat_token: + form.new_token !== form.repeat_token + ? i18n.str`is not the same` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const instance = useInstanceContext(); + + const text = i18n.str`You are updating the access token from instance with id ${instance.id}`; + + return ( + <ClearConfirmModal + description={text} + onCancel={onCancel} + onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined} + onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined} + > + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider errors={errors} object={form} valueHandler={setValue}> + {oldToken && ( + <Input<State> + name="old_token" + label={i18n.str`Old access token`} + tooltip={i18n.str`access token currently in use`} + inputType="password" + /> + )} + <Input<State> + name="new_token" + label={i18n.str`New access token`} + tooltip={i18n.str`next access token to be used`} + inputType="password" + /> + <Input<State> + name="repeat_token" + label={i18n.str`Repeat access token`} + tooltip={i18n.str`confirm the same access token`} + inputType="password" + /> + </FormProvider> + <p> + <i18n.Translate> + Clearing the access token will mean public access to the instance + </i18n.Translate> + </p> + </div> + <div class="column" /> + </div> + </ClearConfirmModal> + ); +} + +export function SetTokenNewInstanceModal({ + onCancel, + onClear, + onConfirm, +}: UpdateTokenModalProps): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; + const [form, setValue] = useState<Partial<State>>({ + new_token: "", + repeat_token: "", + }); + const { i18n } = useTranslationContext(); + + const errors = { + new_token: !form.new_token + ? i18n.str`cannot be empty` + : form.new_token === form.old_token + ? i18n.str`cannot be the same as the old access token` + : undefined, + repeat_token: + form.new_token !== form.repeat_token + ? i18n.str`is not the same` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">{i18n.str`You are setting the access token for the new instance`}</p> + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + errors={errors} + object={form} + valueHandler={setValue} + > + <Input<State> + name="new_token" + label={i18n.str`New access token`} + tooltip={i18n.str`next access token to be used`} + inputType="password" + /> + <Input<State> + name="repeat_token" + label={i18n.str`Repeat access token`} + tooltip={i18n.str`confirm the same access token`} + inputType="password" + /> + </FormProvider> + <p> + <i18n.Translate> + With external authorization method no check will be done by + the merchant backend + </i18n.Translate> + </p> + </div> + <div class="column" /> + </div> + </section> + <footer class="modal-card-foot"> + {onClear && ( + <button + class="button is-danger" + onClick={onClear} + disabled={onClear === undefined} + > + <i18n.Translate>Set external authorization</i18n.Translate> + </button> + )} + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <button + class="button is-info" + onClick={() => onConfirm(form.new_token!)} + disabled={hasErrors} + > + <i18n.Translate>Set access token</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} + +export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title"> + <i18n.Translate>Operation in progress...</i18n.Translate> + </p> + </header> + <section class="modal-card-body"> + <div class="columns"> + <div class="column" /> + <Spinner /> + <div class="column" /> + </div> + <p>{i18n.str`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p> + </section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx new file mode 100644 index 000000000..073382fb1 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx @@ -0,0 +1,57 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; + +interface Props { + onCreateAnother?: () => void; + onConfirm: () => void; + children: ComponentChildren; +} + +export function CreatedSuccessfully({ + children, + onConfirm, + onCreateAnother, +}: Props): VNode { + return ( + <div class="columns is-fullwidth is-vcentered mt-3"> + <div class="column" /> + <div class="column is-four-fifths"> + <div class="card"> + <header class="card-header has-background-success"> + <p class="card-header-title has-text-white-ter">Success.</p> + </header> + <div class="card-content">{children}</div> + </div> + <div class="buttons is-right"> + {onCreateAnother && ( + <button class="button is-info" onClick={onCreateAnother}> + Create another + </button> + )} + <button class="button is-info" onClick={onConfirm}> + Continue + </button> + </div> + </div> + <div class="column" /> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx b/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx new file mode 100644 index 000000000..af594de0f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx @@ -0,0 +1,62 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h } from "preact"; +import { Notifications } from "./index.js"; + +export default { + title: "Components/Notification", + component: Notifications, + argTypes: { + removeNotification: { action: "removeNotification" }, + }, +}; + +export const Info = (a: any) => <Notifications {...a} />; +Info.args = { + notifications: [ + { + message: "Title", + description: "Some large description", + type: "INFO", + }, + ], +}; +export const Warn = (a: any) => <Notifications {...a} />; +Warn.args = { + notifications: [ + { + message: "Title", + description: "Some large description", + type: "WARN", + }, + ], +}; +export const Error = (a: any) => <Notifications {...a} />; +Error.args = { + notifications: [ + { + message: "Title", + description: "Some large description", + type: "ERROR", + }, + ], +}; diff --git a/packages/auditor-backoffice-ui/src/components/notifications/index.tsx b/packages/auditor-backoffice-ui/src/components/notifications/index.tsx new file mode 100644 index 000000000..235c75577 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/notifications/index.tsx @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MessageType, Notification } from "../../utils/types.js"; + +interface Props { + notifications: Notification[]; + removeNotification?: (n: Notification) => void; +} + +function messageStyle(type: MessageType): string { + switch (type) { + case "INFO": + return "message is-info"; + case "WARN": + return "message is-warning"; + case "ERROR": + return "message is-danger"; + case "SUCCESS": + return "message is-success"; + default: + return "message"; + } +} + +export function Notifications({ + notifications, + removeNotification, +}: Props): VNode { + return ( + <div class="toast"> + {notifications.map((n, i) => ( + <article key={i} class={messageStyle(n.type)}> + <div class="message-header"> + <p>{n.message}</p> + <button + class="delete" + onClick={() => removeNotification && removeNotification(n)} + /> + </div> + {n.description && <div class="message-body">{n.description}</div>} + </article> + ))} + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx new file mode 100644 index 000000000..0bc629d46 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx @@ -0,0 +1,349 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, Component } from "preact"; + +interface Props { + closeFunction?: () => void; + dateReceiver?: (d: Date) => void; + opened?: boolean; +} +interface State { + displayedMonth: number; + displayedYear: number; + selectYearMode: boolean; + currentDate: Date; +} + +// inspired by https://codepen.io/m4r1vs/pen/MOOxyE +export class DatePicker extends Component<Props, State> { + closeDatePicker() { + this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent + } + + /** + * Gets fired when a day gets clicked. + * @param {object} e The event thrown by the <span /> element clicked + */ + dayClicked(e: any) { + const element = e.target; // the actual element clicked + + if (element.innerHTML === "") return false; // don't continue if <span /> empty + + // get date from clicked element (gets attached when rendered) + const date = new Date(element.getAttribute("data-value")); + + // update the state + this.setState({ currentDate: date }); + this.passDateToParent(date); + } + + /** + * returns days in month as array + * @param {number} month the month to display + * @param {number} year the year to display + */ + getDaysByMonth(month: number, year: number) { + const calendar = []; + + const date = new Date(year, month, 1); // month to display + + const firstDay = new Date(year, month, 1).getDay(); // first weekday of month + const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month + + let day: number | null = 0; + + // the calendar is 7*6 fields big, so 42 loops + for (let i = 0; i < 42; i++) { + if (i >= firstDay && day !== null) day = day + 1; + if (day !== null && day > lastDate) day = null; + + // append the calendar Array + calendar.push({ + day: day === 0 || day === null ? null : day, // null or number + date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date() + today: + day === now.getDate() && + month === now.getMonth() && + year === now.getFullYear(), // boolean + }); + } + + return calendar; + } + + /** + * Display previous month by updating state + */ + displayPrevMonth() { + if (this.state.displayedMonth <= 0) { + this.setState({ + displayedMonth: 11, + displayedYear: this.state.displayedYear - 1, + }); + } else { + this.setState({ + displayedMonth: this.state.displayedMonth - 1, + }); + } + } + + /** + * Display next month by updating state + */ + displayNextMonth() { + if (this.state.displayedMonth >= 11) { + this.setState({ + displayedMonth: 0, + displayedYear: this.state.displayedYear + 1, + }); + } else { + this.setState({ + displayedMonth: this.state.displayedMonth + 1, + }); + } + } + + /** + * Display the selected month (gets fired when clicking on the date string) + */ + displaySelectedMonth() { + if (this.state.selectYearMode) { + this.toggleYearSelector(); + } else { + if (!this.state.currentDate) return false; + this.setState({ + displayedMonth: this.state.currentDate.getMonth(), + displayedYear: this.state.currentDate.getFullYear(), + }); + } + } + + toggleYearSelector() { + this.setState({ selectYearMode: !this.state.selectYearMode }); + } + + changeDisplayedYear(e: any) { + const element = e.target; + this.toggleYearSelector(); + this.setState({ + displayedYear: parseInt(element.innerHTML, 10), + displayedMonth: 0, + }); + } + + /** + * Pass the selected date to parent when 'OK' is clicked + */ + passSavedDateDateToParent() { + this.passDateToParent(this.state.currentDate); + } + passDateToParent(date: Date) { + if (typeof this.props.dateReceiver === "function") + this.props.dateReceiver(date); + this.closeDatePicker(); + } + + componentDidUpdate() { + if (this.state.selectYearMode) { + document.getElementsByClassName("selected")[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it + } + } + + constructor() { + super(); + + this.closeDatePicker = this.closeDatePicker.bind(this); + this.dayClicked = this.dayClicked.bind(this); + this.displayNextMonth = this.displayNextMonth.bind(this); + this.displayPrevMonth = this.displayPrevMonth.bind(this); + this.getDaysByMonth = this.getDaysByMonth.bind(this); + this.changeDisplayedYear = this.changeDisplayedYear.bind(this); + this.passDateToParent = this.passDateToParent.bind(this); + this.toggleYearSelector = this.toggleYearSelector.bind(this); + this.displaySelectedMonth = this.displaySelectedMonth.bind(this); + + this.state = { + currentDate: now, + displayedMonth: now.getMonth(), + displayedYear: now.getFullYear(), + selectYearMode: false, + }; + } + + render() { + const { currentDate, displayedMonth, displayedYear, selectYearMode } = + this.state; + + return ( + <div> + <div class={`datePicker ${this.props.opened && "datePicker--opened"}`}> + <div class="datePicker--titles"> + <h3 + style={{ + color: selectYearMode + ? "rgba(255,255,255,.87)" + : "rgba(255,255,255,.57)", + }} + onClick={this.toggleYearSelector} + > + {currentDate.getFullYear()} + </h3> + <h2 + style={{ + color: !selectYearMode + ? "rgba(255,255,255,.87)" + : "rgba(255,255,255,.57)", + }} + onClick={this.displaySelectedMonth} + > + {dayArr[currentDate.getDay()]},{" "} + {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()} + </h2> + </div> + + {!selectYearMode && ( + <nav> + <span onClick={this.displayPrevMonth} class="icon"> + <i + style={{ transform: "rotate(180deg)" }} + class="mdi mdi-forward" + /> + </span> + <h4> + {monthArrShortFull[displayedMonth]} {displayedYear} + </h4> + <span onClick={this.displayNextMonth} class="icon"> + <i class="mdi mdi-forward" /> + </span> + </nav> + )} + + <div class="datePicker--scroll"> + {!selectYearMode && ( + <div class="datePicker--calendar"> + <div class="datePicker--dayNames"> + {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => ( + <span key={i}>{day}</span> + ))} + </div> + + <div onClick={this.dayClicked} class="datePicker--days"> + {/* + Loop through the calendar object returned by getDaysByMonth(). + */} + + {this.getDaysByMonth( + this.state.displayedMonth, + this.state.displayedYear, + ).map((day) => { + let selected = false; + + if (currentDate && day.date) + selected = + currentDate.toLocaleDateString() === + day.date.toLocaleDateString(); + + return ( + <span + key={day.day} + class={ + (day.today ? "datePicker--today " : "") + + (selected ? "datePicker--selected" : "") + } + disabled={!day.date} + data-value={day.date} + > + {day.day} + </span> + ); + })} + </div> + </div> + )} + + {selectYearMode && ( + <div class="datePicker--selectYear"> + {yearArr.map((year) => ( + <span + key={year} + class={year === displayedYear ? "selected" : ""} + onClick={this.changeDisplayedYear} + > + {year} + </span> + ))} + </div> + )} + </div> + </div> + + <div + class="datePicker--background" + onClick={this.closeDatePicker} + style={{ + display: this.props.opened ? "block" : "none", + }} + /> + </div> + ); + } +} + +const monthArrShortFull = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +const monthArrShort = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; + +const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +const now = new Date(); + +const yearArr: number[] = []; + +for (let i = 2010; i <= now.getFullYear() + 10; i++) { + yearArr.push(i); +} diff --git a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx new file mode 100644 index 000000000..8f74d55ac --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx @@ -0,0 +1,55 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, FunctionalComponent } from "preact"; +import { useState } from "preact/hooks"; +import { DurationPicker as TestedComponent } from "./DurationPicker.js"; + +export default { + title: "Components/Picker/Duration", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + days: true, + minutes: true, + hours: true, + seconds: true, + value: 10000000, +}); + +export const WithState = () => { + const [v, s] = useState<number>(1000000); + return <TestedComponent value={v} onChange={s} days minutes hours seconds />; +}; diff --git a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx new file mode 100644 index 000000000..ba003cce5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx @@ -0,0 +1,211 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import "../../scss/DurationPicker.scss"; + +export interface Props { + hours?: boolean; + minutes?: boolean; + seconds?: boolean; + days?: boolean; + onChange: (value: number) => void; + value: number; +} + +// inspiration taken from https://github.com/flurmbo/react-duration-picker +export function DurationPicker({ + days, + hours, + minutes, + seconds, + onChange, + value, +}: Props): VNode { + const ss = 1000; + const ms = ss * 60; + const hs = ms * 60; + const ds = hs * 24; + const { i18n } = useTranslationContext(); + + return ( + <div class="rdp-picker"> + {days && ( + <DurationColumn + unit={i18n.str`days`} + max={99} + value={Math.floor(value / ds)} + onDecrease={value >= ds ? () => onChange(value - ds) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined} + onChange={(diff) => onChange(value + diff * ds)} + /> + )} + {hours && ( + <DurationColumn + unit={i18n.str`hours`} + max={23} + min={1} + value={Math.floor(value / hs) % 24} + onDecrease={value >= hs ? () => onChange(value - hs) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined} + onChange={(diff) => onChange(value + diff * hs)} + /> + )} + {minutes && ( + <DurationColumn + unit={i18n.str`minutes`} + max={59} + min={1} + value={Math.floor(value / ms) % 60} + onDecrease={value >= ms ? () => onChange(value - ms) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined} + onChange={(diff) => onChange(value + diff * ms)} + /> + )} + {seconds && ( + <DurationColumn + unit={i18n.str`seconds`} + max={59} + value={Math.floor(value / ss) % 60} + onDecrease={value >= ss ? () => onChange(value - ss) : undefined} + onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined} + onChange={(diff) => onChange(value + diff * ss)} + /> + )} + </div> + ); +} + +interface ColProps { + unit: string; + min?: number; + max: number; + value: number; + onIncrease?: () => void; + onDecrease?: () => void; + onChange?: (diff: number) => void; +} + +function InputNumber({ + initial, + onChange, +}: { + initial: number; + onChange: (n: number) => void; +}) { + const [value, handler] = useState<{ v: string }>({ + v: toTwoDigitString(initial), + }); + + return ( + <input + value={value.v} + onBlur={(e) => onChange(parseInt(value.v, 10))} + onInput={(e) => { + e.preventDefault(); + const n = Number.parseInt(e.currentTarget.value, 10); + if (isNaN(n)) return handler({ v: toTwoDigitString(initial) }); + return handler({ v: toTwoDigitString(n) }); + }} + style={{ + width: 50, + border: "none", + fontSize: "inherit", + background: "inherit", + }} + /> + ); +} + +function DurationColumn({ + unit, + min = 0, + max, + value, + onIncrease, + onDecrease, + onChange, +}: ColProps): VNode { + const cellHeight = 35; + return ( + <div class="rdp-column-container"> + <div class="rdp-masked-div"> + <hr class="rdp-reticule" style={{ top: cellHeight * 2 - 1 }} /> + <hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} /> + + <div class="rdp-column" style={{ top: 0 }}> + <div class="rdp-cell" key={value - 2}> + {onDecrease && ( + <button + style={{ width: "100%", textAlign: "center", margin: 5 }} + onClick={onDecrease} + > + <span class="icon"> + <i class="mdi mdi-chevron-up" /> + </span> + </button> + )} + </div> + <div class="rdp-cell" key={value - 1}> + {value > min ? toTwoDigitString(value - 1) : ""} + </div> + <div class="rdp-cell rdp-center" key={value}> + {onChange ? ( + <InputNumber + initial={value} + onChange={(n) => onChange(n - value)} + /> + ) : ( + toTwoDigitString(value) + )} + <div>{unit}</div> + </div> + + <div class="rdp-cell" key={value + 1}> + {value < max ? toTwoDigitString(value + 1) : ""} + </div> + + <div class="rdp-cell" key={value + 2}> + {onIncrease && ( + <button + style={{ width: "100%", textAlign: "center", margin: 5 }} + onClick={onIncrease} + > + <span class="icon"> + <i class="mdi mdi-chevron-down" /> + </span> + </button> + )} + </div> + </div> + </div> + </div> + ); +} + +function toTwoDigitString(n: number) { + if (n < 10) { + return `0${n}`; + } + return `${n}`; +} diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx new file mode 100644 index 000000000..2d5a54cde --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx @@ -0,0 +1,62 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { InventoryProductForm as TestedComponent } from "./InventoryProductForm.js"; + +export default { + title: "Components/Product/Add", + component: TestedComponent, + argTypes: { + onAddProduct: { action: "onAddProduct" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const WithASimpleList = createExample(TestedComponent, { + inventory: [ + { + id: "this id", + description: "this is the description", + } as any, + ], +}); + +export const WithAProductSelected = createExample(TestedComponent, { + inventory: [], + currentProducts: { + thisid: { + quantity: 1, + product: { + id: "asd", + description: "asdsadsad", + } as any, + }, + }, +}); diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx new file mode 100644 index 000000000..377d9c1ba --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx @@ -0,0 +1,127 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { MerchantBackend, WithId } from "../../declaration.js"; +import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js"; +import { FormErrors, FormProvider } from "../form/FormProvider.js"; +import { InputNumber } from "../form/InputNumber.js"; +import { InputSearchOnList } from "../form/InputSearchOnList.js"; + +type Form = { + product: MerchantBackend.Products.ProductDetail & WithId; + quantity: number; +}; + +interface Props { + currentProducts: ProductMap; + onAddProduct: ( + product: MerchantBackend.Products.ProductDetail & WithId, + quantity: number, + ) => void; + inventory: (MerchantBackend.Products.ProductDetail & WithId)[]; +} + +export function InventoryProductForm({ + currentProducts, + onAddProduct, + inventory, +}: Props): VNode { + const initialState = { quantity: 1 }; + const [state, setState] = useState<Partial<Form>>(initialState); + const [errors, setErrors] = useState<FormErrors<Form>>({}); + + const { i18n } = useTranslationContext(); + + const productWithInfiniteStock = + state.product && state.product.total_stock === -1; + + const submit = (): void => { + if (!state.product) { + setErrors({ + product: i18n.str`You must enter a valid product identifier.`, + }); + return; + } + if (productWithInfiniteStock) { + onAddProduct(state.product, 1); + } else { + if (!state.quantity || state.quantity <= 0) { + setErrors({ quantity: i18n.str`Quantity must be greater than 0!` }); + return; + } + const currentStock = + state.product.total_stock - + state.product.total_lost - + state.product.total_sold; + const p = currentProducts[state.product.id]; + if (p) { + if (state.quantity + p.quantity > currentStock) { + const left = currentStock - p.quantity; + setErrors({ + quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`, + }); + return; + } + onAddProduct(state.product, state.quantity + p.quantity); + } else { + if (state.quantity > currentStock) { + const left = currentStock; + setErrors({ + quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`, + }); + return; + } + onAddProduct(state.product, state.quantity); + } + } + + setState(initialState); + }; + + return ( + <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> + <InputSearchOnList + label={i18n.str`Search product`} + selected={state.product} + onChange={(p) => setState((v) => ({ ...v, product: p }))} + list={inventory} + withImage + /> + {state.product && ( + <div class="columns mt-5"> + <div class="column is-two-thirds"> + {!productWithInfiniteStock && ( + <InputNumber<Form> + name="quantity" + label={i18n.str`Quantity`} + tooltip={i18n.str`how many products will be added`} + /> + )} + </div> + <div class="column"> + <div class="buttons is-right"> + <button class="button is-success" onClick={submit}> + <i18n.Translate>Add from inventory</i18n.Translate> + </button> + </div> + </div> + </div> + )} + </FormProvider> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx new file mode 100644 index 000000000..c6d280f94 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -0,0 +1,215 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import * as yup from "yup"; +import { MerchantBackend } from "../../declaration.js"; +import { useListener } from "../../hooks/listener.js"; +import { NonInventoryProductSchema as schema } from "../../schemas/index.js"; +import { FormErrors, FormProvider } from "../form/FormProvider.js"; +import { Input } from "../form/Input.js"; +import { InputCurrency } from "../form/InputCurrency.js"; +import { InputImage } from "../form/InputImage.js"; +import { InputNumber } from "../form/InputNumber.js"; +import { InputTaxes } from "../form/InputTaxes.js"; + +type Entity = MerchantBackend.Product; + +interface Props { + onAddProduct: (p: Entity) => Promise<void>; + productToEdit?: Entity; +} +export function NonInventoryProductFrom({ + productToEdit, + onAddProduct, +}: Props): VNode { + const [showCreateProduct, setShowCreateProduct] = useState(false); + + const isEditing = !!productToEdit; + + useEffect(() => { + setShowCreateProduct(isEditing); + }, [isEditing]); + + const [submitForm, addFormSubmitter] = useListener< + Partial<MerchantBackend.Product> | undefined + >((result) => { + if (result) { + setShowCreateProduct(false); + return onAddProduct({ + quantity: result.quantity || 0, + taxes: result.taxes || [], + description: result.description || "", + image: result.image || "", + price: result.price || "", + unit: result.unit || "", + }); + } + return Promise.resolve(); + }); + + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <div class="buttons"> + <button + class="button is-success" + data-tooltip={i18n.str`describe and add a product that is not in the inventory list`} + onClick={() => setShowCreateProduct(true)} + > + <i18n.Translate>Add custom product</i18n.Translate> + </button> + </div> + {showCreateProduct && ( + <div class="modal is-active"> + <div + class="modal-background " + onClick={() => setShowCreateProduct(false)} + /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">{i18n.str`Complete information of the product`}</p> + <button + class="delete " + aria-label="close" + onClick={() => setShowCreateProduct(false)} + /> + </header> + <section class="modal-card-body"> + <ProductForm + initial={productToEdit} + onSubscribe={addFormSubmitter} + /> + </section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button + class="button " + onClick={() => setShowCreateProduct(false)} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + <button + class="button is-info " + disabled={!submitForm} + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={() => setShowCreateProduct(false)} + /> + </div> + )} + </Fragment> + ); +} + +interface ProductProps { + onSubscribe: (c?: () => Entity | undefined) => void; + initial?: Partial<Entity>; +} + +interface NonInventoryProduct { + quantity: number; + description: string; + unit: string; + price: string; + image: string; + taxes: MerchantBackend.Tax[]; +} + +export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { + const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ + taxes: [], + ...initial, + }); + let errors: FormErrors<Entity> = {}; + try { + schema.validateSync(value, { abortEarly: false }); + } catch (err) { + if (err instanceof yup.ValidationError) { + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); + } + } + + const submit = useCallback((): Entity | undefined => { + return value as MerchantBackend.Product; + }, [value]); + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + useEffect(() => { + onSubscribe(hasErrors ? undefined : submit); + }, [submit, hasErrors]); + + const { i18n } = useTranslationContext(); + + return ( + <div> + <FormProvider<NonInventoryProduct> + name="product" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <InputImage<NonInventoryProduct> + name="image" + label={i18n.str`Image`} + tooltip={i18n.str`photo of the product`} + /> + <Input<NonInventoryProduct> + name="description" + inputType="multiline" + label={i18n.str`Description`} + tooltip={i18n.str`full product description`} + /> + <Input<NonInventoryProduct> + name="unit" + label={i18n.str`Unit`} + tooltip={i18n.str`name of the product unit`} + /> + <InputCurrency<NonInventoryProduct> + name="price" + label={i18n.str`Price`} + tooltip={i18n.str`amount in the current currency`} + /> + + <InputNumber<NonInventoryProduct> + name="quantity" + label={i18n.str`Quantity`} + tooltip={i18n.str`how many products will be added`} + /> + + <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} /> + </FormProvider> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx new file mode 100644 index 000000000..e91e8c876 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx @@ -0,0 +1,178 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import * as yup from "yup"; +import { useBackendContext } from "../../context/backend.js"; +import { MerchantBackend } from "../../declaration.js"; +import { + ProductCreateSchema as createSchema, + ProductUpdateSchema as updateSchema, +} from "../../schemas/index.js"; +import { FormErrors, FormProvider } from "../form/FormProvider.js"; +import { Input } from "../form/Input.js"; +import { InputCurrency } from "../form/InputCurrency.js"; +import { InputImage } from "../form/InputImage.js"; +import { InputNumber } from "../form/InputNumber.js"; +import { InputStock, Stock } from "../form/InputStock.js"; +import { InputTaxes } from "../form/InputTaxes.js"; +import { InputWithAddon } from "../form/InputWithAddon.js"; + +type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; + +interface Props { + onSubscribe: (c?: () => Entity | undefined) => void; + initial?: Partial<Entity>; + alreadyExist?: boolean; +} + +export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { + const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({ + address: {}, + description_i18n: {}, + taxes: [], + next_restock: { t_s: "never" }, + price: ":0", + ...initial, + stock: + !initial || initial.total_stock === -1 + ? undefined + : { + current: initial.total_stock || 0, + lost: initial.total_lost || 0, + sold: initial.total_sold || 0, + address: initial.address, + nextRestock: initial.next_restock, + }, + }); + let errors: FormErrors<Entity> = {}; + + try { + (alreadyExist ? updateSchema : createSchema).validateSync(value, { + abortEarly: false, + }); + } catch (err) { + if (err instanceof yup.ValidationError) { + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); + } + } + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submit = useCallback((): Entity | undefined => { + const stock: Stock = (value as any).stock; + + if (!stock) { + value.total_stock = -1; + } else { + value.total_stock = stock.current; + value.total_lost = stock.lost; + value.next_restock = + stock.nextRestock instanceof Date + ? { t_s: stock.nextRestock.getTime() / 1000 } + : stock.nextRestock; + value.address = stock.address; + } + delete (value as any).stock; + + if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) { + delete value.minimum_age; + } + + return value as MerchantBackend.Products.ProductDetail & { + product_id: string; + }; + }, [value]); + + useEffect(() => { + onSubscribe(hasErrors ? undefined : submit); + }, [submit, hasErrors]); + + const { url: backendURL } = useBackendContext() + const { i18n } = useTranslationContext(); + + return ( + <div> + <FormProvider<Entity> + name="product" + errors={errors} + object={value} + valueHandler={valueHandler} + > + {alreadyExist ? undefined : ( + <InputWithAddon<Entity> + name="product_id" + addonBefore={`${backendURL}/product/`} + label={i18n.str`ID`} + tooltip={i18n.str`product identification to use in URLs (for internal use only)`} + /> + )} + <InputImage<Entity> + name="image" + label={i18n.str`Image`} + tooltip={i18n.str`illustration of the product for customers`} + /> + <Input<Entity> + name="description" + inputType="multiline" + label={i18n.str`Description`} + tooltip={i18n.str`product description for customers`} + /> + <InputNumber<Entity> + name="minimum_age" + label={i18n.str`Age restriction`} + tooltip={i18n.str`is this product restricted for customer below certain age?`} + help={i18n.str`minimum age of the buyer`} + /> + <Input<Entity> + name="unit" + label={i18n.str`Unit name`} + tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} + help={i18n.str`exajmple: kg, items or liters`} + /> + <InputCurrency<Entity> + name="price" + label={i18n.str`Price per unit`} + tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`} + /> + <InputStock + name="stock" + label={i18n.str`Stock`} + alreadyExist={alreadyExist} + tooltip={i18n.str`inventory for products with finite supply (for internal use only)`} + /> + <InputTaxes<Entity> + name="taxes" + label={i18n.str`Taxes`} + tooltip={i18n.str`taxes included in the product price, exposed to customers`} + /> + </FormProvider> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx new file mode 100644 index 000000000..25751dd96 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx @@ -0,0 +1,106 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { Amounts } from "@gnu-taler/taler-util"; +import { h, VNode } from "preact"; +import emptyImage from "../../assets/empty.png"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { MerchantBackend } from "../../declaration.js"; + +interface Props { + list: MerchantBackend.Product[]; + actions?: { + name: string; + tooltip: string; + handler: (d: MerchantBackend.Product, index: number) => void; + }[]; +} +export function ProductList({ list, actions = [] }: Props): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>image</i18n.Translate> + </th> + <th> + <i18n.Translate>description</i18n.Translate> + </th> + <th> + <i18n.Translate>quantity</i18n.Translate> + </th> + <th> + <i18n.Translate>unit price</i18n.Translate> + </th> + <th> + <i18n.Translate>total price</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {list.map((entry, index) => { + const unitPrice = !entry.price ? "0" : entry.price; + const totalPrice = !entry.price + ? "0" + : Amounts.stringify( + Amounts.mult( + Amounts.parseOrThrow(entry.price), + entry.quantity, + ).amount, + ); + + return ( + <tr key={index}> + <td> + <img + style={{ height: 32, width: 32 }} + src={entry.image ? entry.image : emptyImage} + /> + </td> + <td>{entry.description}</td> + <td> + {entry.quantity === 0 + ? "--" + : `${entry.quantity} ${entry.unit}`} + </td> + <td>{unitPrice}</td> + <td>{totalPrice}</td> + <td class="is-actions-cell right-sticky"> + {actions.map((a, i) => { + return ( + <div key={i} class="buttons is-right"> + <button + class="button is-small is-danger has-tooltip-left" + data-tooltip={a.tooltip} + type="button" + onClick={() => a.handler(entry, index)} + > + {a.name} + </button> + </div> + ); + })} + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/context/backend.test.ts b/packages/auditor-backoffice-ui/src/context/backend.test.ts new file mode 100644 index 000000000..359859819 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/backend.test.ts @@ -0,0 +1,163 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { ComponentChildren, h, VNode } from "preact"; +import { AccessToken, MerchantBackend } from "../declaration.js"; +import { + useAdminAPI, + useInstanceAPI, + useManagementAPI, +} from "../hooks/instance.js"; +import { expect } from "chai"; +import { ApiMockEnvironment } from "../hooks/testing.js"; +import { + API_CREATE_INSTANCE, + API_NEW_LOGIN, + API_UPDATE_CURRENT_INSTANCE_AUTH, + API_UPDATE_INSTANCE_AUTH_BY_ID, +} from "../hooks/urls.js"; + +interface TestingContextProps { + children?: ComponentChildren; +} + +describe("backend context api ", () => { + it("should use new token after updating the instance token in the settings as user", async () => { + const env = new ApiMockEnvironment(); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const instance = useInstanceAPI(); + const management = useManagementAPI("default"); + const admin = useAdminAPI(); + + return { instance, management, admin }; + }, + {}, + [ + ({ instance, management, admin }) => { + env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), { + request: { + method: "token", + token: "another_token", + }, + response: { + name: "instance_name", + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + env.addRequestExpectation(API_NEW_LOGIN, { + auth: "another_token", + request: { + scope: "write", + duration: { + "d_us": "forever", + }, + refreshable: true, + }, + + }); + + management.setNewAccessToken(undefined,"another_token" as AccessToken); + }, + ({ instance, management, admin }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + + env.addRequestExpectation(API_CREATE_INSTANCE, { + // auth: "another_token", + request: { + id: "new_instance_id", + } as MerchantBackend.Instances.InstanceConfigurationMessage, + }); + + admin.createInstance({ + id: "new_instance_id", + } as MerchantBackend.Instances.InstanceConfigurationMessage); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should use new token after updating the instance token in the settings as admin", async () => { + const env = new ApiMockEnvironment(); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const instance = useInstanceAPI(); + const management = useManagementAPI("default"); + const admin = useAdminAPI(); + + return { instance, management, admin }; + }, + {}, + [ + ({ instance, management, admin }) => { + env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { + request: { + method: "token", + token: "another_token", + }, + response: { + name: "instance_name", + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + env.addRequestExpectation(API_NEW_LOGIN, { + auth: "another_token", + request: { + scope: "write", + duration: { + "d_us": "forever", + }, + refreshable: true, + }, + }); + instance.setNewAccessToken(undefined, "another_token" as AccessToken); + }, + ({ instance, management, admin }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + + env.addRequestExpectation(API_CREATE_INSTANCE, { + // auth: "another_token", + request: { + id: "new_instance_id", + } as MerchantBackend.Instances.InstanceConfigurationMessage, + }); + + admin.createInstance({ + id: "new_instance_id", + } as MerchantBackend.Instances.InstanceConfigurationMessage); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/context/backend.ts b/packages/auditor-backoffice-ui/src/context/backend.ts new file mode 100644 index 000000000..2dad11f6b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/backend.ts @@ -0,0 +1,69 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useMemoryStorage } from "@gnu-taler/web-util/browser"; +import { createContext, h, VNode } from "preact"; +import { useContext } from "preact/hooks"; +import { LoginToken } from "../declaration.js"; +import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js"; + +interface BackendContextType { + url: string, + alreadyTriedLogin: boolean; + token?: LoginToken; + updateToken: (token: LoginToken | undefined) => void; +} + +const BackendContext = createContext<BackendContextType>({ + url: "", + alreadyTriedLogin: false, + token: undefined, + updateToken: () => null, +}); + +function useBackendContextState( + defaultUrl?: string, +): BackendContextType { + const [url] = useBackendURL(defaultUrl); + const [token, updateToken] = useBackendDefaultToken(); + + return { + url, + token, + alreadyTriedLogin: token !== undefined, + updateToken, + }; +} + +export const BackendContextProvider = ({ + children, + defaultUrl, +}: { + children: any; + defaultUrl?: string; +}): VNode => { + const value = useBackendContextState(defaultUrl); + + return h(BackendContext.Provider, { value, children }); +}; + +export const useBackendContext = (): BackendContextType => + useContext(BackendContext); diff --git a/packages/auditor-backoffice-ui/src/context/config.ts b/packages/auditor-backoffice-ui/src/context/config.ts new file mode 100644 index 000000000..def45ea64 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/config.ts @@ -0,0 +1,32 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; + +interface Type { + currency: string; + version: string; +} +const Context = createContext<Type>(null!); + +export const ConfigContextProvider = Context.Provider; +export const useConfigContext = (): Type => useContext(Context); diff --git a/packages/auditor-backoffice-ui/src/context/instance.ts b/packages/auditor-backoffice-ui/src/context/instance.ts new file mode 100644 index 000000000..5800ade7e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/context/instance.ts @@ -0,0 +1,36 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; +import { LoginToken } from "../declaration.js"; + +interface Type { + id: string; + token?: LoginToken; + admin?: boolean; + changeToken: (t?: LoginToken) => void; +} + +const Context = createContext<Type>({} as any); + +export const InstanceContextProvider = Context.Provider; +export const useInstanceContext = (): Type => useContext(Context); diff --git a/packages/auditor-backoffice-ui/src/custom.d.ts b/packages/auditor-backoffice-ui/src/custom.d.ts new file mode 100644 index 000000000..34522a2dd --- /dev/null +++ b/packages/auditor-backoffice-ui/src/custom.d.ts @@ -0,0 +1,42 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +declare module "*.po" { + const content: any; + export default content; +} +declare module "jed" { + const x: any; + export = x; +} +declare module "*.jpeg" { + const content: any; + export default content; +} +declare module "*.png" { + const content: any; + export default content; +} +declare module "*.svg" { + const content: any; + export default content; +} + +declare module "*.scss" { + const content: Record<string, string>; + export default content; +} +declare const __VERSION__: string; +declare const __GIT_HASH__: string; diff --git a/packages/auditor-backoffice-ui/src/declaration.d.ts b/packages/auditor-backoffice-ui/src/declaration.d.ts new file mode 100644 index 000000000..f0c79268c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/declaration.d.ts @@ -0,0 +1,1830 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +type HashCode = string; +type EddsaPublicKey = string; +type EddsaSignature = string; +type WireTransferIdentifierRawP = string; +type RelativeTime = TalerProtocolDuration; +type ImageDataUrl = string; +type MerchantUserType = "business" | "individual"; + + +export interface WithId { + id: string; +} + +interface Timestamp { + // Milliseconds since epoch, or the special + // value "forever" to represent an event that will + // never happen. + t_s: number | "never"; +} +interface TalerProtocolDuration { + d_us: number | "forever"; +} +interface Duration { + d_ms: number | "forever"; +} + +interface WithId { + id: string; +} + +type Amount = string; +type UUID = string; +type Integer = number; + +interface WireAccount { + // payto:// URI identifying the account and wire method + payto_uri: string; + + // URI to convert amounts from or to the currency used by + // this wire account of the exchange. Missing if no + // conversion is applicable. + conversion_url?: string; + + // Restrictions that apply to bank accounts that would send + // funds to the exchange (crediting this exchange bank account). + // Optional, empty array for unrestricted. + credit_restrictions: AccountRestriction[]; + + // Restrictions that apply to bank accounts that would receive + // funds from the exchange (debiting this exchange bank account). + // Optional, empty array for unrestricted. + debit_restrictions: AccountRestriction[]; + + // Signature using the exchange's offline key over + // a TALER_MasterWireDetailsPS + // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS. + master_sig: EddsaSignature; +} + +type AccountRestriction = RegexAccountRestriction | DenyAllAccountRestriction; + +// Account restriction that disables this type of +// account for the indicated operation categorically. +interface DenyAllAccountRestriction { + type: "deny"; +} + +// Accounts interacting with this type of account +// restriction must have a payto://-URI matching +// the given regex. +interface RegexAccountRestriction { + type: "regex"; + + // Regular expression that the payto://-URI of the + // partner account must follow. The regular expression + // should follow posix-egrep, but without support for character + // classes, GNU extensions, back-references or intervals. See + // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html + // for a description of the posix-egrep syntax. Applications + // may support regexes with additional features, but exchanges + // must not use such regexes. + payto_regex: string; + + // Hint for a human to understand the restriction + // (that is hopefully easier to comprehend than the regex itself). + human_hint: string; + + // Map from IETF BCP 47 language tags to localized + // human hints. + human_hint_i18n?: { [lang_tag: string]: string }; +} +interface LoginToken { + token: string, + expiration: Timestamp, +} +// token used to get loginToken +// must forget after used +declare const __ac_token: unique symbol; +type AccessToken = string & { + [__ac_token]: true; +}; + +export namespace ExchangeBackend { + interface WireResponse { + // Master public key of the exchange, must match the key returned in /keys. + master_public_key: EddsaPublicKey; + + // Array of wire accounts operated by the exchange for + // incoming wire transfers. + accounts: WireAccount[]; + + // Object mapping names of wire methods (i.e. "sepa" or "x-taler-bank") + // to wire fees. + fees: { method: AggregateTransferFee }; + } + interface AggregateTransferFee { + // Per transfer wire transfer fee. + wire_fee: Amount; + + // Per transfer closing fee. + closing_fee: Amount; + + // What date (inclusive) does this fee go into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + start_date: Timestamp; + + // What date (exclusive) does this fee stop going into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + end_date: Timestamp; + + // Signature of TALER_MasterWireFeePS with + // purpose TALER_SIGNATURE_MASTER_WIRE_FEES. + sig: EddsaSignature; + } +} +export namespace AuditorBackend { + interface ErrorDetail { + // Numeric error code unique to the condition. + // The other arguments are specific to the error value reported here. + code: number; + + // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... + // Should give a human-readable hint about the error's nature. Optional, may change without notice! + hint?: string; + + // Optional detail about the specific input value that failed. May change without notice! + detail?: string; + + // Name of the parameter that was bogus (if applicable). + parameter?: string; + + // Path to the argument that was bogus (if applicable). + path?: string; + + // Offset of the argument that was bogus (if applicable). + offset?: string; + + // Index of the argument that was bogus (if applicable). + index?: string; + + // Name of the object that was bogus (if applicable). + object?: string; + + // Name of the currency than was problematic (if applicable). + currency?: string; + + // Expected type (if applicable). + type_expected?: string; + + // Type that was provided instead (if applicable). + type_actual?: string; + } + interface Exchange { + // the exchange's base URL + url: string; + + // master public key of the exchange + master_pub: EddsaPublicKey; + } + namespace DepositConfirmation { + // POST /deposit-confirmation + interface ProductAddDetail { + // product ID to use. + product_id: string; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + // PATCH /private/products/$PRODUCT_ID + interface ProductPatchDetail { + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + + // GET /deposit-confirmation + interface DepositConfirmationList { + depositConfirmations: DepositConfirmation []; + } + interface DepositConfirmation { + serial_id: string; + timestamp: string; + refund_deadline: string; + wire_deadline: string; + amount_without_fee: string; + } + + // GET /private/products/$PRODUCT_ID + interface DepositConfirmationDetail { + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that have already been sold. + total_sold: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + } + +} +export namespace MerchantBackend { + interface ErrorDetail { + // Numeric error code unique to the condition. + // The other arguments are specific to the error value reported here. + code: number; + + // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... + // Should give a human-readable hint about the error's nature. Optional, may change without notice! + hint?: string; + + // Optional detail about the specific input value that failed. May change without notice! + detail?: string; + + // Name of the parameter that was bogus (if applicable). + parameter?: string; + + // Path to the argument that was bogus (if applicable). + path?: string; + + // Offset of the argument that was bogus (if applicable). + offset?: string; + + // Index of the argument that was bogus (if applicable). + index?: string; + + // Name of the object that was bogus (if applicable). + object?: string; + + // Name of the currency than was problematic (if applicable). + currency?: string; + + // Expected type (if applicable). + type_expected?: string; + + // Type that was provided instead (if applicable). + type_actual?: string; + } + + // Delivery location, loosely modeled as a subset of + // ISO20022's PostalAddress25. + interface Tax { + // the name of the tax + name: string; + + // amount paid in tax + tax: Amount; + } + + interface Auditor { + // official name + name: string; + + // Auditor's public key + auditor_pub: EddsaPublicKey; + + // Base URL of the auditor + url: string; + } + interface Exchange { + // the exchange's base URL + url: string; + + // master public key of the exchange + master_pub: EddsaPublicKey; + } + + interface Product { + // merchant-internal identifier for the product. + product_id?: string; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n?: { [lang_tag: string]: string }; + + // The number of units of the product to deliver to the customer. + quantity: Integer; + + // The unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price of the product; this is the total price for quantity times unit of this product. + price?: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for this product. Can be empty. + taxes: Tax[]; + + // time indicating when this product should be delivered + delivery_date?: TalerProtocolTimestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + interface Merchant { + // label for a location with the business address of the merchant + address: Location; + + // the merchant's legal name of business + name: string; + + // label for a location that denotes the jurisdiction for disputes. + // Some of the typical fields for a location (such as a street address) may be absent. + jurisdiction: Location; + } + + interface VersionResponse { + // libtool-style representation of the Merchant protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Name of the protocol. + name: "taler-merchant"; + + // Currency supported by this backend. + currency: string; + } + interface Location { + // Nation with its own government. + country?: string; + + // Identifies a subdivision of a country such as state, region, county. + country_subdivision?: string; + + // Identifies a subdivision within a country sub-division. + district?: string; + + // Name of a built-up area, with defined boundaries, and a local government. + town?: string; + + // Specific location name within the town. + town_location?: string; + + // Identifier consisting of a group of letters and/or numbers that + // is added to a postal address to assist the sorting of mail. + post_code?: string; + + // Name of a street or thoroughfare. + street?: string; + + // Name of the building or house. + building_name?: string; + + // Number that identifies the position of a building on a street. + building_number?: string; + + // Free-form address lines, should not exceed 7 elements. + address_lines?: string[]; + } + namespace Instances { + //POST /private/instances/$INSTANCE/auth + interface InstanceAuthConfigurationMessage { + // Type of authentication. + // "external": The mechant backend does not do + // any authentication checks. Instead an API + // gateway must do the authentication. + // "token": The merchant checks an auth token. + // See "token" for details. + method: "external" | "token"; + + // For method "external", this field is mandatory. + // The token MUST begin with the string "secret-token:". + // After the auth token has been set (with method "token"), + // the value must be provided in a "Authorization: Bearer $token" + // header. + token?: string; + } + //POST /private/instances + interface InstanceConfigurationMessage { + // Name of the merchant instance to create (will become $INSTANCE). + id: string; + + // Merchant name corresponding to this instance. + name: string; + + // Type of the user (business or individual). + // Defaults to 'business'. Should become mandatory field + // in the future, left as optional for API compatibility for now. + user_type?: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // "Authentication" header required to authorize management access the instance. + // Optional, if not given authentication will be disabled for + // this instance (hopefully authentication checks are still + // done by some reverse proxy). + auth: InstanceAuthConfigurationMessage; + + // The merchant's physical address (to be put into contracts). + address: Location; + + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; + } + + // PATCH /private/instances/$INSTANCE + interface InstanceReconfigurationMessage { + + // Merchant name corresponding to this instance. + name: string; + + // Type of the user (business or individual). + // Defaults to 'business'. Should become mandatory field + // in the future, left as optional for API compatibility for now. + user_type?: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // The merchant's physical address (to be put into contracts). + address: Location; + + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; + } + + // GET /private/instances + interface InstancesResponse { + // List of instances that are present in the backend (see Instance) + instances: Instance[]; + } + + interface Instance { + // Merchant name corresponding to this instance. + name: string; + + // Type of the user ("business" or "individual"). + user_type: MerchantUserType; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // Merchant instance this response is about ($INSTANCE) + id: string; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKey; + + // List of the payment targets supported by this instance. Clients can + // specify the desired payment target in /order requests. Note that + // front-ends do not have to support wallets selecting payment targets. + payment_targets: string[]; + + // Has this instance been deleted (but not purged)? + deleted: boolean; + } + + //GET /private/instances/$INSTANCE + interface QueryInstancesResponse { + + // Merchant name corresponding to this instance. + name: string; + // Type of the user ("business" or "individual"). + user_type: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKey; + + // The merchant's physical address (to be put into contracts). + address: Location; + + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; + + // Authentication configuration. + // Does not contain the token when token auth is configured. + auth: { + method: "external" | "token"; + }; + } + // DELETE /private/instances/$INSTANCE + interface LoginTokenRequest { + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + duration?: RelativeTime; + + // Can this token be refreshed? + // Defaults to false. + refreshable?: boolean; + } + interface LoginTokenSuccessResponse { + // The login token that can be used to access resources + // that are in scope for some time. Must be prefixed + // with "Bearer " when used in the "Authorization" HTTP header. + // Will already begin with the RFC 8959 prefix. + token: string; + + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + expiration: Timestamp; + + // Can this token be refreshed? + refreshable: boolean; + } + } + + namespace KYC { + //GET /private/instances/$INSTANCE/kyc + interface AccountKycRedirects { + // Array of pending KYCs. + pending_kycs: MerchantAccountKycRedirect[]; + + // Array of exchanges with no reply. + timeout_kycs: ExchangeKycTimeout[]; + } + interface MerchantAccountKycRedirect { + // URL that the user should open in a browser to + // proceed with the KYC process (as returned + // by the exchange's /kyc-check/ endpoint). + // Optional, missing if the account is blocked + // due to AML and not due to KYC. + kyc_url?: string; + + // Base URL of the exchange this is about. + exchange_url: string; + + // AML status of the account. + aml_status: number; + + // Our bank wire account this is about. + payto_uri: string; + } + interface ExchangeKycTimeout { + // Base URL of the exchange this is about. + exchange_url: string; + + // Numeric error code indicating errors the exchange + // returned, or TALER_EC_INVALID for none. + exchange_code: number; + + // HTTP status code returned by the exchange when we asked for + // information about the KYC status. + // 0 if there was no response at all. + exchange_http_status: number; + } + + } + + namespace BankAccounts { + + interface AccountAddDetails { + + // payto:// URI of the account. + payto_uri: string; + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + // To really delete credentials, set them to the type: "none". + credit_facade_credentials?: FacadeCredentials; + + } + + type FacadeCredentials = + | NoFacadeCredentials + | BasicAuthFacadeCredentials; + + interface NoFacadeCredentials { + type: "none"; + } + + interface BasicAuthFacadeCredentials { + type: "basic"; + + // Username to use to authenticate + username: string; + + // Password to use to authenticate + password: string; + } + + interface AccountAddResponse { + // Hash over the wire details (including over the salt). + h_wire: HashCode; + + // Salt used to compute h_wire. + salt: HashCode; + } + + interface AccountPatchDetails { + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + // To really delete credentials, set them to the type: "none". + credit_facade_credentials?: FacadeCredentials; + } + + + interface AccountsSummaryResponse { + + // List of accounts that are known for the instance. + accounts: BankAccountEntry[]; + } + + interface BankAccountEntry { + // payto:// URI of the account. + payto_uri: string; + + // Hash over the wire details (including over the salt) + h_wire: HashCode; + + // salt used to compute h_wire + salt: HashCode; + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + credit_facade_credentials?: FacadeCredentials; + + // true if this account is active, + // false if it is historic. + active: boolean; + } + + } + + namespace Products { + // POST /private/products + interface ProductAddDetail { + // product ID to use. + product_id: string; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + // PATCH /private/products/$PRODUCT_ID + interface ProductPatchDetail { + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + + // GET /private/products + interface InventorySummaryResponse { + // List of products that are present in the inventory + products: InventoryEntry[]; + } + interface InventoryEntry { + // Product identifier, as found in the product. + product_id: string; + } + + // GET /private/products/$PRODUCT_ID + interface ProductDetail { + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that have already been sold. + total_sold: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + + // POST /private/products/$PRODUCT_ID/lock + interface LockRequest { + // UUID that identifies the frontend performing the lock + // It is suggested that clients use a timeflake for this, + // see https://github.com/anthonynsimon/timeflake + lock_uuid: UUID; + + // How long does the frontend intend to hold the lock + duration: RelativeTime; + + // How many units should be locked? + quantity: Integer; + } + + // DELETE /private/products/$PRODUCT_ID + } + + namespace Orders { + type MerchantOrderStatusResponse = + | CheckPaymentPaidResponse + | CheckPaymentClaimedResponse + | CheckPaymentUnpaidResponse; + interface CheckPaymentPaidResponse { + // The customer paid for this contract. + order_status: "paid"; + + // Was the payment refunded (even partially)? + refunded: boolean; + + // True if there are any approved refunds that the wallet has + // not yet obtained. + refund_pending: boolean; + + // Did the exchange wire us the funds? + wired: boolean; + + // Total amount the exchange deposited into our bank account + // for this contract, excluding fees. + deposit_total: Amount; + + // Numeric error code indicating errors the exchange + // encountered tracking the wire transfer for this purchase (before + // we even got to specific coin issues). + // 0 if there were no issues. + exchange_ec: number; + + // HTTP status code returned by the exchange when we asked for + // information to track the wire transfer for this purchase. + // 0 if there were no issues. + exchange_hc: number; + + // Total amount that was refunded, 0 if refunded is false. + refund_amount: Amount; + + // Contract terms. + contract_terms: ContractTerms; + + // The wire transfer status from the exchange for this order if + // available, otherwise empty array. + wire_details: TransactionWireTransfer[]; + + // Reports about trouble obtaining wire transfer details, + // empty array if no trouble were encountered. + wire_reports: TransactionWireReport[]; + + // The refund details for this order. One entry per + // refunded coin; empty array if there are no refunds. + refund_details: RefundDetails[]; + + // Status URL, can be used as a redirect target for the browser + // to show the order QR code / trigger the wallet. + order_status_url: string; + } + interface CheckPaymentClaimedResponse { + // A wallet claimed the order, but did not yet pay for the contract. + order_status: "claimed"; + + // Contract terms. + contract_terms: ContractTerms; + } + interface CheckPaymentUnpaidResponse { + // The order was neither claimed nor paid. + order_status: "unpaid"; + + // when was the order created + creation_time: Timestamp; + + // Order summary text. + summary: string; + + // Total amount of the order (to be paid by the customer). + total_amount: Amount; + + // URI that the wallet must process to complete the payment. + taler_pay_uri: string; + + // Alternative order ID which was paid for already in the same session. + // Only given if the same product was purchased before in the same session. + already_paid_order_id?: string; + + // Fulfillment URL of an already paid order. Only given if under this + // session an already paid order with a fulfillment URL exists. + already_paid_fulfillment_url?: string; + + // Status URL, can be used as a redirect target for the browser + // to show the order QR code / trigger the wallet. + order_status_url: string; + + // We do we NOT return the contract terms here because they may not + // exist in case the wallet did not yet claim them. + } + interface RefundDetails { + // Reason given for the refund. + reason: string; + + // When was the refund approved. + timestamp: Timestamp; + + // Set to true if a refund is still available for the wallet for this payment. + pending: boolean; + + // Total amount that was refunded (minus a refund fee). + amount: Amount; + } + interface TransactionWireTransfer { + // Responsible exchange. + exchange_url: string; + + // 32-byte wire transfer identifier. + wtid: Base32; + + // Execution time of the wire transfer. + execution_time: Timestamp; + + // Total amount that has been wire transferred + // to the merchant. + amount: Amount; + + // Was this transfer confirmed by the merchant via the + // POST /transfers API, or is it merely claimed by the exchange? + confirmed: boolean; + } + interface TransactionWireReport { + // Numerical error code. + code: number; + + // Human-readable error description. + hint: string; + + // Numerical error code from the exchange. + exchange_ec: number; + + // HTTP status code received from the exchange. + exchange_hc: number; + + // Public key of the coin for which we got the exchange error. + coin_pub: CoinPublicKey; + } + + interface OrderHistory { + // timestamp-sorted array of all orders matching the query. + // The order of the sorting depends on the sign of delta. + orders: OrderHistoryEntry[]; + } + interface OrderHistoryEntry { + // order ID of the transaction related to this entry. + order_id: string; + + // row ID of the order in the database + row_id: number; + + // when the order was created + timestamp: Timestamp; + + // the amount of money the order is for + amount: Amount; + + // the summary of the order + summary: string; + + // whether some part of the order is refundable, + // that is the refund deadline has not yet expired + // and the total amount refunded so far is below + // the value of the original transaction. + refundable: boolean; + + // whether the order has been paid or not + paid: boolean; + } + + interface PostOrderRequest { + // The order must at least contain the minimal + // order detail, but can override all + order: Order; + + // if set, the backend will then set the refund deadline to the current + // time plus the specified delay. If it's not set, refunds will not be + // possible. + refund_delay?: RelativeTime; + + // specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + payment_target?: string; + + // specifies that some products are to be included in the + // order from the inventory. For these inventory management + // is performed (so the products must be in stock) and + // details are completed from the product data of the backend. + inventory_products?: MinimalInventoryProduct[]; + + // Specifies a lock identifier that was used to + // lock a product in the inventory. Only useful if + // manage_inventory is set. Used in case a frontend + // reserved quantities of the individual products while + // the shopping card was being built. Multiple UUIDs can + // be used in case different UUIDs were used for different + // products (i.e. in case the user started with multiple + // shopping sessions that were combined during checkout). + lock_uuids?: UUID[]; + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + create_token?: boolean; + + // OTP device ID to associate with the order. + // This parameter is optional. + otp_id?: string; + } + type Order = MinimalOrderDetail | ContractTerms; + + interface MinimalOrderDetail { + // Amount to be paid by the customer + amount: Amount; + + // Short summary of the order + summary: string; + + // URL that will show that the order was successful after + // it has been paid for. Optional. When POSTing to the + // merchant, the placeholder "${ORDER_ID}" will be + // replaced with the actual order ID (useful if the + // order ID is generated server-side and needs to be + // in the URL). + fulfillment_url?: string; + } + + interface MinimalInventoryProduct { + // Which product is requested (here mandatory!) + product_id: string; + + // How many units of the product are requested + quantity: Integer; + } + interface PostOrderResponse { + // Order ID of the response that was just created + order_id: string; + + // Token that authorizes the wallet to claim the order. + // Provided only if "create_token" was set to 'true' + // in the request. + token?: ClaimToken; + } + interface OutOfStockResponse { + // Product ID of an out-of-stock item + product_id: string; + + // Requested quantity + requested_quantity: Integer; + + // Available quantity (must be below requested_quanitity) + available_quantity: Integer; + + // When do we expect the product to be again in stock? + // Optional, not given if unknown. + restock_expected?: Timestamp; + } + + interface ForgetRequest { + // Array of valid JSON paths to forgettable fields in the order's + // contract terms. + fields: string[]; + } + interface RefundRequest { + // Amount to be refunded + refund: Amount; + + // Human-readable refund justification + reason: string; + } + interface MerchantRefundResponse { + // URL (handled by the backend) that the wallet should access to + // trigger refund processing. + // taler://refund/... + taler_refund_uri: string; + + // Contract hash that a client may need to authenticate an + // HTTP request to obtain the above URI in a wallet-friendly way. + h_contract: HashCode; + } + } + + namespace Rewards { + // GET /private/reserves + interface RewardReserveStatus { + // Array of all known reserves (possibly empty!) + reserves: ReserveStatusEntry[]; + } + interface ReserveStatusEntry { + // Public key of the reserve + reserve_pub: EddsaPublicKey; + + // Timestamp when it was established + creation_time: Timestamp; + + // Timestamp when it expires + expiration_time: Timestamp; + + // Initial amount as per reserve creation call + merchant_initial_amount: Amount; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: Amount; + + // Amount picked up so far. + pickup_amount: Amount; + + // Amount approved for rewards that exceeds the pickup_amount. + committed_amount: Amount; + + // Is this reserve active (false if it was deleted but not purged) + active: boolean; + } + + interface ReserveCreateRequest { + // Amount that the merchant promises to put into the reserve + initial_balance: Amount; + + // Exchange the merchant intends to use for reward + exchange_url: string; + + // Desired wire method, for example "iban" or "x-taler-bank" + wire_method: string; + } + interface ReserveCreateConfirmation { + // Public key identifying the reserve + reserve_pub: EddsaPublicKey; + + // Wire accounts of the exchange where to transfer the funds. + accounts: WireAccount[]; + } + interface RewardCreateRequest { + // Amount that the customer should be reward + amount: Amount; + + // Justification for giving the reward + justification: string; + + // URL that the user should be directed to after rewarding, + // will be included in the reward_token. + next_url: string; + } + interface RewardCreateConfirmation { + // Unique reward identifier for the reward that was created. + reward_id: HashCode; + + // taler://reward URI for the reward + taler_reward_uri: string; + + // URL that will directly trigger processing + // the reward when the browser is redirected to it + reward_status_url: string; + + // when does the reward expire + reward_expiration: Timestamp; + } + + interface ReserveDetail { + // Timestamp when it was established. + creation_time: Timestamp; + + // Timestamp when it expires. + expiration_time: Timestamp; + + // Initial amount as per reserve creation call. + merchant_initial_amount: Amount; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: Amount; + + // Amount picked up so far. + pickup_amount: Amount; + + // Amount approved for rewards that exceeds the pickup_amount. + committed_amount: Amount; + + // Array of all rewards created by this reserves (possibly empty!). + // Only present if asked for explicitly. + rewards?: RewardStatusEntry[]; + + // Is this reserve active (false if it was deleted but not purged)? + active: boolean; + + // Array of wire accounts of the exchange that could + // be used to fill the reserve, can be NULL + // if the reserve is inactive or was already filled + accounts?: WireAccount[]; + + // URL of the exchange hosting the reserve, + // NULL if the reserve is inactive + exchange_url: string; + } + + interface RewardStatusEntry { + // Unique identifier for the reward. + reward_id: HashCode; + + // Total amount of the reward that can be withdrawn. + total_amount: Amount; + + // Human-readable reason for why the reward was granted. + reason: string; + } + + interface RewardDetails { + // Amount that we authorized for this reward. + total_authorized: Amount; + + // Amount that was picked up by the user already. + total_picked_up: Amount; + + // Human-readable reason given when authorizing the reward. + reason: string; + + // Timestamp indicating when the reward is set to expire (may be in the past). + expiration: Timestamp; + + // Reserve public key from which the reward is funded. + reserve_pub: EddsaPublicKey; + + // Array showing the pickup operations of the wallet (possibly empty!). + // Only present if asked for explicitly. + pickups?: PickupDetail[]; + } + interface PickupDetail { + // Unique identifier for the pickup operation. + pickup_id: HashCode; + + // Number of planchets involved. + num_planchets: Integer; + + // Total amount requested for this pickup_id. + requested_amount: Amount; + } + } + + namespace Transfers { + interface TransferList { + // list of all the transfers that fit the filter that we know + transfers: TransferDetails[]; + } + interface TransferDetails { + // how much was wired to the merchant (minus fees) + credit_amount: Amount; + + // raw wire transfer identifier identifying the wire transfer (a base32-encoded value) + wtid: string; + + // target account that received the wire transfer + payto_uri: string; + + // base URL of the exchange that made the wire transfer + exchange_url: string; + + // Serial number identifying the transfer in the merchant backend. + // Used for filgering via offset. + transfer_serial_id: number; + + // Time of the execution of the wire transfer by the exchange, according to the exchange + // Only provided if we did get an answer from the exchange. + execution_time?: Timestamp; + + // True if we checked the exchange's answer and are happy with it. + // False if we have an answer and are unhappy, missing if we + // do not have an answer from the exchange. + verified?: boolean; + + // True if the merchant uses the POST /transfers API to confirm + // that this wire transfer took place (and it is thus not + // something merely claimed by the exchange). + confirmed?: boolean; + } + + interface TransferInformation { + // how much was wired to the merchant (minus fees) + credit_amount: Amount; + + // raw wire transfer identifier identifying the wire transfer (a base32-encoded value) + wtid: WireTransferIdentifierRawP; + + // target account that received the wire transfer + payto_uri: string; + + // base URL of the exchange that made the wire transfer + exchange_url: string; + } + } + + namespace OTP { + interface OtpDeviceAddDetails { + // Device ID to use. + otp_device_id: string; + + // Human-readable description for the device. + otp_device_description: string; + + // A base64-encoded key + otp_key: string; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + interface OtpDevicePatchDetails { + // Human-readable description for the device. + otp_device_description: string; + + // A base64-encoded key + otp_key: string | undefined; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + interface OtpDeviceSummaryResponse { + // Array of devices that are present in our backend. + otp_devices: OtpDeviceEntry[]; + } + interface OtpDeviceEntry { + // Device identifier. + otp_device_id: string; + + // Human-readable description for the device. + device_description: string; + } + + interface OtpDeviceDetails { + // Human-readable description for the device. + device_description: string; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + + } + namespace Template { + interface TemplateAddDetails { + // Template ID to use. + template_id: string; + + // Human-readable description for the template. + template_description: string; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + + // Additional information in a separate template. + template_contract: TemplateContractDetails; + } + interface TemplateContractDetails { + // Human-readable summary for the template. + summary?: string; + + // The price is imposed by the merchant and cannot be changed by the customer. + // This parameter is optional. + amount?: Amount; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age: Integer; + + // The time the customer need to pay before his order will be deleted. + // It is deleted if the customer did not pay and if the duration is over. + pay_duration: RelativeTime; + } + interface TemplatePatchDetails { + // Human-readable description for the template. + template_description: string; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + + // Additional information in a separate template. + template_contract: TemplateContractDetails; + } + + interface TemplateSummaryResponse { + // List of templates that are present in our backend. + templates: TemplateEntry[]; + } + + interface TemplateEntry { + // Template identifier, as found in the template. + template_id: string; + + // Human-readable description for the template. + template_description: string; + } + + interface TemplateDetails { + // Human-readable description for the template. + template_description: string; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + + // Additional information in a separate template. + template_contract: TemplateContractDetails; + } + + interface UsingTemplateDetails { + // Subject of the template + summary?: string; + + // The amount entered by the customer. + amount?: Amount; + } + + interface UsingTemplateResponse { + // After enter the request. The user will be pay with a taler URL. + order_id: string; + token: string; + } + } + + namespace Webhooks { + type MerchantWebhookType = "pay" | "refund"; + interface WebhookAddDetails { + // Webhook ID to use. + webhook_id: string; + + // The event of the webhook: why the webhook is used. + event_type: MerchantWebhookType; + + // URL of the webhook where the customer will be redirected. + url: string; + + // Method used by the webhook + http_method: string; + + // Header template of the webhook + header_template?: string; + + // Body template by the webhook + body_template?: string; + } + interface WebhookPatchDetails { + // The event of the webhook: why the webhook is used. + event_type: string; + + // URL of the webhook where the customer will be redirected. + url: string; + + // Method used by the webhook + http_method: string; + + // Header template of the webhook + header_template?: string; + + // Body template by the webhook + body_template?: string; + } + interface WebhookSummaryResponse { + // List of webhooks that are present in our backend. + webhooks: WebhookEntry[]; + } + interface WebhookEntry { + // Webhook identifier, as found in the webhook. + webhook_id: string; + + // The event of the webhook: why the webhook is used. + event_type: string; + } + interface WebhookDetails { + // The event of the webhook: why the webhook is used. + event_type: string; + + // URL of the webhook where the customer will be redirected. + url: string; + + // Method used by the webhook + http_method: string; + + // Header template of the webhook + header_template?: string; + + // Body template by the webhook + body_template?: string; + } + } + + interface ContractTerms { + // Human-readable description of the whole purchase + summary: string; + + // Map from IETF BCP 47 language tags to localized summaries + summary_i18n?: { [lang_tag: string]: string }; + + // Unique, free-form identifier for the proposal. + // Must be unique within a merchant instance. + // For merchants that do not store proposals in their DB + // before the customer paid for them, the order_id can be used + // by the frontend to restore a proposal from the information + // encoded in it (such as a short product identifier and timestamp). + order_id: string; + + // Total price for the transaction. + // The exchange will subtract deposit fees from that amount + // before transferring it to the merchant. + amount: Amount; + + // The URL for this purchase. Every time is is visited, the merchant + // will send back to the customer the same proposal. Clearly, this URL + // can be bookmarked and shared by users. + fulfillment_url?: string; + + // Maximum total deposit fee accepted by the merchant for this contract + max_fee: Amount; + + // List of products that are part of the purchase (see Product). + products: Product[]; + + // Time when this contract was generated + timestamp: TalerProtocolTimestamp; + + // After this deadline has passed, no refunds will be accepted. + refund_deadline: TalerProtocolTimestamp; + + // After this deadline, the merchant won't accept payments for the contact + pay_deadline: TalerProtocolTimestamp; + + // Transfer deadline for the exchange. Must be in the + // deposit permissions of coins used to pay for this order. + wire_transfer_deadline: TalerProtocolTimestamp; + + // Merchant's public key used to sign this proposal; this information + // is typically added by the backend Note that this can be an ephemeral key. + merchant_pub: EddsaPublicKey; + + // Base URL of the (public!) merchant backend API. + // Must be an absolute URL that ends with a slash. + merchant_base_url: string; + + // More info about the merchant, see below + merchant: Merchant; + + // The hash of the merchant instance's wire details. + h_wire: HashCode; + + // Wire transfer method identifier for the wire method associated with h_wire. + // The wallet may only select exchanges via a matching auditor if the + // exchange also supports this wire method. + // The wire transfer fees must be added based on this wire transfer method. + wire_method: string; + + // Any exchanges audited by these auditors are accepted by the merchant. + auditors: Auditor[]; + + // Exchanges that the merchant accepts even if it does not accept any auditors that audit them. + exchanges: Exchange[]; + + // Delivery location for (all!) products. + delivery_location?: Location; + + // Time indicating when the order should be delivered. + // May be overwritten by individual products. + delivery_date?: TalerProtocolTimestamp; + + // Nonce generated by the wallet and echoed by the merchant + // in this field when the proposal is generated. + nonce: string; + + // Specifies for how long the wallet should try to get an + // automatic refund for the purchase. If this field is + // present, the wallet should wait for a few seconds after + // the purchase and then automatically attempt to obtain + // a refund. The wallet should probe until "delay" + // after the payment was successful (i.e. via long polling + // or via explicit requests with exponential back-off). + // + // In particular, if the wallet is offline + // at that time, it MUST repeat the request until it gets + // one response from the merchant after the delay has expired. + // If the refund is granted, the wallet MUST automatically + // recover the payment. This is used in case a merchant + // knows that it might be unable to satisfy the contract and + // desires for the wallet to attempt to get the refund without any + // customer interaction. Note that it is NOT an error if the + // merchant does not grant a refund. + auto_refund?: RelativeTime; + + // Extra data that is only interpreted by the merchant frontend. + // Useful when the merchant needs to store extra information on a + // contract without storing it separately in their database. + extra?: any; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } +} diff --git a/packages/auditor-backoffice-ui/src/hooks/async.ts b/packages/auditor-backoffice-ui/src/hooks/async.ts new file mode 100644 index 000000000..f22badc88 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/async.ts @@ -0,0 +1,77 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useState } from "preact/hooks"; + +export interface Options { + slowTolerance: number; +} + +export interface AsyncOperationApi<T> { + request: (...a: any) => void; + cancel: () => void; + data: T | undefined; + isSlow: boolean; + isLoading: boolean; + error: string | undefined; +} + +export function useAsync<T>( + fn?: (...args: any) => Promise<T>, + { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }, +): AsyncOperationApi<T> { + const [data, setData] = useState<T | undefined>(undefined); + const [isLoading, setLoading] = useState<boolean>(false); + const [error, setError] = useState<any>(undefined); + const [isSlow, setSlow] = useState(false); + + const request = async (...args: any) => { + if (!fn) return; + setLoading(true); + + const handler = setTimeout(() => { + setSlow(true); + }, tooLong); + + try { + const result = await fn(...args); + setData(result); + } catch (error) { + setError(error); + } + setLoading(false); + setSlow(false); + clearTimeout(handler); + }; + + function cancel(): void { + setLoading(false); + setSlow(false); + } + + return { + request, + cancel, + data, + isSlow, + isLoading, + error, + }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts new file mode 100644 index 000000000..8d99546a8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts @@ -0,0 +1,477 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + HttpResponse, + HttpResponseOk, + RequestError, + RequestOptions, + useApiContext, +} from "@gnu-taler/web-util/browser"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useSWRConfig } from "swr"; +import { useBackendContext } from "../context/backend.js"; +import { useInstanceContext } from "../context/instance.js"; +import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js"; + + +export function useMatchMutate(): ( + re?: RegExp, + value?: unknown, +) => Promise<any> { + const { cache, mutate } = useSWRConfig(); + + if (!(cache instanceof Map)) { + throw new Error( + "matchMutate requires the cache provider to be a Map instance", + ); + } + + return function matchRegexMutate(re?: RegExp) { + return mutate((key) => { + // evict if no key or regex === all + if (!key || !re) return true + // match string + if (typeof key === 'string' && re.test(key)) return true + // record or object have the path at [0] + if (typeof key === 'object' && re.test(key[0])) return true + //key didn't match regex + return false + }, undefined, { + revalidate: true, + }); + }; +} + +export function useBackendInstancesTestForAdmin(): HttpResponse< + MerchantBackend.Instances.InstancesResponse, + MerchantBackend.ErrorDetail +> { + const { request } = useBackendBaseRequest(); + + type Type = MerchantBackend.Instances.InstancesResponse; + + const [result, setResult] = useState< + HttpResponse<Type, MerchantBackend.ErrorDetail> + >({ loading: true }); + + useEffect(() => { + request<Type>(`/management/instances`) + .then((data) => setResult(data)) + .catch((error: RequestError<MerchantBackend.ErrorDetail>) => + setResult(error.cause), + ); + }, [request]); + + return result; +} + +const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; +const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + +export function useBackendConfig(): HttpResponse< + MerchantBackend.VersionResponse | undefined, + RequestError<MerchantBackend.ErrorDetail> +> { + const { request } = useBackendBaseRequest(); + + type Type = MerchantBackend.VersionResponse; + type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number } + const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 }); + + useEffect(() => { + if (result.timer) { + clearTimeout(result.timer) + } + function tryConfig(): void { + request<Type>(`/config`) + .then((data) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_OK) + setResult({ data, timer }) + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_FAIL) + const data = error.cause + setResult({ data, timer }) + }); + } + tryConfig() + }, [request]); + + return result.data; +} + +interface useBackendInstanceRequestType { + request: <T>( + endpoint: string, + options?: RequestOptions, + ) => Promise<HttpResponseOk<T>>; + fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>; + orderFetcher: <T>( + params: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] + ) => Promise<HttpResponseOk<T>>; + transferFetcher: <T>( + params: [endpoint: string, + payto_uri?: string, + verified?: string, + position?: string, + delta?: number,] + ) => Promise<HttpResponseOk<T>>; + templateFetcher: <T>( + params: [endpoint: string, + position?: string, + delta?: number] + ) => Promise<HttpResponseOk<T>>; + webhookFetcher: <T>( + params: [endpoint: string, + position?: string, + delta?: number] + ) => Promise<HttpResponseOk<T>>; +} +interface useBackendBaseRequestType { + request: <T>( + endpoint: string, + options?: RequestOptions, + ) => Promise<HttpResponseOk<T>>; +} + +type YesOrNo = "yes" | "no"; +type LoginResult = { + valid: true; + token: string; + expiration: Timestamp; +} | { + valid: false; + cause: HttpError<{}>; +} + +export function useCredentialsChecker() { + const { request } = useApiContext(); + //check against instance details endpoint + //while merchant backend doesn't have a login endpoint + async function requestNewLoginToken( + baseUrl: string, + token: AccessToken, + ): Promise<LoginResult> { + const data: MerchantBackend.Instances.LoginTokenRequest = { + scope: "write", + duration: { + d_us: "forever" + }, + refreshable: true, + } + try { + const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, { + method: "POST", + token, + data + }); + return { valid: true, token: response.data.token, expiration: response.data.expiration }; + } catch (error) { + if (error instanceof RequestError) { + return { valid: false, cause: error.cause }; + } + + return { + valid: false, cause: { + type: ErrorType.UNEXPECTED, + loading: false, + info: { + hasToken: true, + status: 0, + options: {}, + url: `/private/token`, + payload: {} + }, + exception: error, + message: (error instanceof Error ? error.message : "unpexepected error") + } + }; + } + }; + + async function refreshLoginToken( + baseUrl: string, + token: LoginToken + ): Promise<LoginResult> { + + if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { + return { + valid: false, cause: { + type: ErrorType.CLIENT, + status: HttpStatusCode.Unauthorized, + message: "login token expired, login again.", + info: { + hasToken: true, + status: 401, + options: {}, + url: `/private/token`, + payload: {} + }, + payload: {} + }, + } + } + + return requestNewLoginToken(baseUrl, token.token as AccessToken) + } + return { requestNewLoginToken, refreshLoginToken } +} + +/** + * + * @param root the request is intended to the base URL and no the instance URL + * @returns request handler to + */ +export function useBackendBaseRequest(): useBackendBaseRequestType { + const { url: backend, token: loginToken } = useBackendContext(); + const { request: requestHandler } = useApiContext(); + const token = loginToken?.token; + + const request = useCallback( + function requestImpl<T>( + endpoint: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => { + return res + }).catch(err => { + throw err + }); + }, + [backend, token], + ); + + return { request }; +} + +export function useBackendInstanceRequest(): useBackendInstanceRequestType { + const { url: rootBackendUrl, token: rootToken } = useBackendContext(); + const { token: instanceToken, id, admin } = useInstanceContext(); + const { request: requestHandler } = useApiContext(); + + const { baseUrl, token: loginToken } = !admin + ? { baseUrl: rootBackendUrl, token: rootToken } + : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken }; + + const token = loginToken?.token; + + const request = useCallback( + function requestImpl<T>( + endpoint: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { token, ...options }); + }, + [baseUrl, token], + ); + + const multiFetcher = useCallback( + function multiFetcherImpl<T>( + args: [endpoints: string[]], + ): Promise<HttpResponseOk<T>[]> { + const [endpoints] = args + return Promise.all( + endpoints.map((endpoint) => + requestHandler<T>(baseUrl, endpoint, { token }), + ), + ); + }, + [baseUrl, token], + ); + + const fetcher = useCallback( + function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { token }); + }, + [baseUrl, token], + ); + + const orderFetcher = useCallback( + function orderFetcherImpl<T>( + args: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, paid, refunded, wired, searchDate, delta] = args + const date_s = + delta && delta < 0 && searchDate + ? Math.floor(searchDate.getTime() / 1000) + 1 + : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined; + const params: any = {}; + if (paid !== undefined) params.paid = paid; + if (delta !== undefined) params.delta = delta; + if (refunded !== undefined) params.refunded = refunded; + if (wired !== undefined) params.wired = wired; + if (date_s !== undefined) params.date_s = date_s; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { orders: [] } as T, + }) + } + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + const reserveDetailFetcher = useCallback( + function reserveDetailFetcherImpl<T>( + endpoint: string, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { + params: { + rewards: "yes", + }, + token, + }); + }, + [baseUrl, token], + ); + + const rewardsDetailFetcher = useCallback( + function rewardsDetailFetcherImpl<T>( + endpoint: string, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { + params: { + pickups: "yes", + }, + token, + }); + }, + [baseUrl, token], + ); + + const transferFetcher = useCallback( + function transferFetcherImpl<T>( + args: [endpoint: string, + payto_uri?: string, + verified?: string, + position?: string, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, payto_uri, verified, position, delta] = args + const params: any = {}; + if (payto_uri !== undefined) params.payto_uri = payto_uri; + if (verified !== undefined) params.verified = verified; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { transfers: [] } as T, + }) + } + if (delta !== undefined) { + params.limit = delta; + } + if (position !== undefined) params.offset = position; + + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + const templateFetcher = useCallback( + function templateFetcherImpl<T>( + args: [endpoint: string, + position?: string, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args + const params: any = {}; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { templates: [] } as T, + }) + } + if (delta !== undefined) { + params.limit = delta; + } + if (position !== undefined) params.offset = position; + + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + const webhookFetcher = useCallback( + function webhookFetcherImpl<T>( + args: [endpoint: string, + position?: string, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args + const params: any = {}; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { webhooks: [] } as T, + }) + } + if (delta !== undefined) { + params.limit = delta; + } + if (position !== undefined) params.offset = position; + + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + return { + request, + fetcher, + multiFetcher, + orderFetcher, + reserveDetailFetcher, + rewardsDetailFetcher, + transferFetcher, + templateFetcher, + webhookFetcher, + }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/bank.ts b/packages/auditor-backoffice-ui/src/hooks/bank.ts new file mode 100644 index 000000000..03b064646 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/bank.ts @@ -0,0 +1,217 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = { +// "hwire1": { +// h_wire: "hwire1", +// payto_uri: "payto://fake/iban/123", +// salt: "qwe", +// }, +// "hwire2": { +// h_wire: "hwire2", +// payto_uri: "payto://fake/iban/123", +// salt: "qwe2", +// }, +// } + +export function useBankAccountAPI(): BankAccountAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const createBankAccount = async ( + data: MerchantBackend.BankAccounts.AccountAddDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_ACCOUNTS[data.h_wire] = data + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/accounts`, { + method: "POST", + data, + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + const updateBankAccount = async ( + h_wire: string, + data: MerchantBackend.BankAccounts.AccountPatchDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials + // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/accounts/${h_wire}`, { + method: "PATCH", + data, + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + const deleteBankAccount = async ( + h_wire: string, + ): Promise<HttpResponseOk<void>> => { + // delete MOCKED_ACCOUNTS[h_wire] + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/accounts/${h_wire}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + return { + createBankAccount, + updateBankAccount, + deleteBankAccount, + }; +} + +export interface BankAccountAPI { + createBankAccount: ( + data: MerchantBackend.BankAccounts.AccountAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateBankAccount: ( + id: string, + data: MerchantBackend.BankAccounts.AccountPatchDetails, + ) => Promise<HttpResponseOk<void>>; + deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; +} + +export interface InstanceBankAccountFilter { +} + +export function useInstanceBankAccounts( + args?: InstanceBankAccountFilter, + updatePosition?: (id: string) => void, +): HttpResponsePaginated< + MerchantBackend.BankAccounts.AccountsSummaryResponse, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // loadMore() { }, + // loadMorePrev() { }, + // data: { + // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({ + // ...e, + // active: true, + // })) + // } + // } + const { fetcher } = useBackendInstanceRequest(); + + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/accounts`], fetcher); + + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.BankAccounts.AccountsSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + }, [afterData /*, beforeData*/]); + + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data.accounts.length < totalAfter; + const isReachingStart = false; + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.accounts.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = `${afterData.data.accounts[afterData.data.accounts.length - 1] + .h_wire + }`; + if (from && updatePosition) updatePosition(from); + } + }, + loadMorePrev: () => { + }, + }; + + const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts; + if (loadingAfter /* || loadingBefore */) + return { loading: true, data: { accounts } }; + if (/*beforeData &&*/ afterData) { + return { ok: true, data: { accounts }, ...pagination }; + } + return { loading: true }; +} + +export function useBankAccountDetails( + h_wire: string, +): HttpResponse< + MerchantBackend.BankAccounts.BankAccountEntry, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // data: { + // ...MOCKED_ACCOUNTS[h_wire], + // active: true, + // } + // } + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/accounts/${h_wire}`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) { + return data; + } + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts new file mode 100644 index 000000000..e4ec9a2f2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts @@ -0,0 +1,161 @@ +/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend, MerchantBackend, WithId } from "../declaration.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface DepositConfirmationAPI {
+ getDepositConfirmation: (
+ id: string,
+ ) => Promise<void>;
+ createDepositConfirmation: (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ) => Promise<void>;
+ updateDepositConfirmation: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ deleteDepositConfirmation: (id: string) => Promise<void>;
+}
+
+export function useDepositConfirmationAPI(): DepositConfirmationAPI {
+ const mutateAll = useMatchMutate();
+ const { mutate } = useSWRConfig();
+
+ const { request } = useBackendInstanceRequest();
+
+ const createDepositConfirmation = async (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ): Promise<void> => {
+ const res = await request(`/private/products`, {
+ method: "POST",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const updateDepositConfirmation = async (
+ productId: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ): Promise<void> => {
+ const r = await request(`/private/products/${productId}`, {
+ method: "PATCH",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const deleteDepositConfirmation = async (productId: string): Promise<void> => {
+ await request(`/private/products/${productId}`, {
+ method: "DELETE",
+ });
+ await mutate([`/private/products`]);
+ };
+
+ const getDepositConfirmation = async (
+ serialId: string,
+ ): Promise<void> => {
+ await request(`/deposit-confirmation/${serialId}`, {
+ method: "GET",
+ });
+
+ return
+ };
+
+ return {createDepositConfirmation, updateDepositConfirmation, deleteDepositConfirmation, getDepositConfirmation};
+}
+
+export function useDepositConfirmation(): HttpResponse<
+ (AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId)[],
+ AuditorBackend.ErrorDetail
+> {
+ const { fetcher, multiFetcher } = useBackendInstanceRequest();
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationList>,
+ RequestError<AuditorBackend.ErrorDetail>
+ >([`/deposit-confirmation`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ const paths = (list?.data.depositConfirmations || []).map(
+ (p) => `/deposit-confirmation/${p.serial_id}`,
+ );
+ const { data: depositConfirmations, error: depositConfirmationError } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>[],
+ RequestError<AuditorBackend.ErrorDetail>
+ >([paths], multiFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+ if (depositConfirmationError) return depositConfirmationError.cause;
+
+ if (depositConfirmations) {
+ const dataWithId = depositConfirmations.map((d) => {
+ //take the id from the queried url
+ return {
+ ...d.data,
+ id: d.info?.url.replace(/.*\/deposit-confirmation\//, "") || "",
+ };
+ });
+ return { ok: true, data: dataWithId };
+ }
+ return { loading: true };
+}
+
+export function useDepositConfirmationDetails(
+ serialId: string,
+): HttpResponse<
+ AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ AuditorBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>,
+ RequestError<AuditorBackend.ErrorDetail>
+ >([`/deposit-confirmation/${serialId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts new file mode 100644 index 000000000..61afbc94a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/index.ts @@ -0,0 +1,151 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util"; +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; +import { LoginToken } from "../declaration.js"; +import { ValueOrFunction } from "../utils/types.js"; +import { useMatchMutate } from "./backend.js"; + +const calculateRootPath = () => { + const rootPath = + typeof window !== undefined + ? window.location.origin + window.location.pathname + : "/"; + + /** + * By default, merchant backend serves the html content + * from the /webui root. This should cover most of the + * cases and the rootPath will be the merchant backend + * URL where the instances are + */ + return rootPath.replace("/webui/", ""); +}; + +const loginTokenCodec = buildCodecForObject<LoginToken>() + .property("token", codecForString()) + .property("expiration", codecForTimestamp) + .build("loginToken") +const TOKENS_KEY = buildStorageKey("auditor-token", codecForMap(loginTokenCodec)); + + +export function useBackendURL( + url?: string, +): [string, StateUpdater<string>] { + const [value, setter] = useSimpleLocalStorage( + "auditor-base-url", + url || calculateRootPath(), + ); + + const checkedSetter = (v: ValueOrFunction<string>) => { + return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); + }; + + return [value!, checkedSetter]; +} + +export function useBackendDefaultToken( +): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { + const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) + + const tokenOfDefaultInstance = tokenMap["default"] + const clearCache = useMatchMutate() + useEffect(() => { + clearCache() + }, [tokenOfDefaultInstance]) + + function updateToken( + value: (LoginToken | undefined) + ): void { + if (value === undefined) { + reset() + } else { + const res = { ...tokenMap, "default": value } + setToken(res) + } + } + return [tokenMap["default"], updateToken]; +} + +export function useBackendInstanceToken( + id: string, +): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { + const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) + const [defaultToken, defaultSetToken] = useBackendDefaultToken(); + + // instance named 'default' use the default token + if (id === "default") { + return [defaultToken, defaultSetToken]; + } + function updateToken( + value: (LoginToken | undefined) + ): void { + if (value === undefined) { + reset() + } else { + const res = { ...tokenMap, [id]: value } + setToken(res) + } + } + + return [tokenMap[id], updateToken]; +} + +export function useLang(initial?: string): [string, StateUpdater<string>] { + const browserLang = + typeof window !== "undefined" + ? navigator.language || (navigator as any).userLanguage + : undefined; + const defaultLang = (browserLang || initial || "en").substring(0, 2); + return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>]; +} + +export function useSimpleLocalStorage( + key: string, + initialValue?: string, +): [string | undefined, StateUpdater<string | undefined>] { + const [storedValue, setStoredValue] = useState<string | undefined>( + (): string | undefined => { + return typeof window !== "undefined" + ? window.localStorage.getItem(key) || initialValue + : initialValue; + }, + ); + + const setValue = ( + value?: string | ((val?: string) => string | undefined), + ) => { + setStoredValue((p) => { + const toStore = value instanceof Function ? value(p) : value; + if (typeof window !== "undefined") { + if (!toStore) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, toStore); + } + } + return toStore; + }); + }; + + return [storedValue, setValue]; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts new file mode 100644 index 000000000..ee1576764 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts @@ -0,0 +1,741 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { expect } from "chai"; +import { AccessToken, MerchantBackend } from "../declaration.js"; +import { + useAdminAPI, + useBackendInstances, + useInstanceAPI, + useInstanceDetails, + useManagementAPI, +} from "./instance.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { + API_CREATE_INSTANCE, + API_DELETE_INSTANCE, + API_GET_CURRENT_INSTANCE, + API_LIST_INSTANCES, + API_NEW_LOGIN, + API_UPDATE_CURRENT_INSTANCE, + API_UPDATE_CURRENT_INSTANCE_AUTH, + API_UPDATE_INSTANCE_BY_ID, +} from "./urls.js"; + +describe("instance api interaction with details", () => { + it("should evict cache when updating an instance", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + response: { + name: "instance_name", + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useInstanceAPI(); + const query = useInstanceDetails(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + name: "instance_name", + }); + env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { + request: { + name: "other_name", + } as MerchantBackend.Instances.InstanceReconfigurationMessage, + }); + env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + response: { + name: "other_name", + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + api.updateInstance({ + name: "other_name", + } as MerchantBackend.Instances.InstanceReconfigurationMessage); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + name: "other_name", + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when setting the instance's token", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + response: { + name: "instance_name", + auth: { + method: "token", + // token: "not-secret", + }, + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useInstanceAPI(); + const query = useInstanceDetails(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + name: "instance_name", + auth: { + method: "token", + }, + }); + env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { + request: { + method: "token", + token: "secret", + } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, + }); + env.addRequestExpectation(API_NEW_LOGIN, { + auth: "secret", + request: { + scope: "write", + duration: { + "d_us": "forever", + }, + refreshable: true, + }, + }); + env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + response: { + name: "instance_name", + auth: { + method: "token", + // token: "secret", + }, + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + api.setNewAccessToken(undefined, "secret" as AccessToken); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + name: "instance_name", + auth: { + method: "token", + // token: "secret", + }, + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when clearing the instance's token", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + response: { + name: "instance_name", + auth: { + method: "token", + // token: "not-secret", + }, + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useInstanceAPI(); + const query = useInstanceDetails(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + name: "instance_name", + auth: { + method: "token", + // token: "not-secret", + }, + }); + env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { + request: { + method: "external", + } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, + }); + env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + response: { + name: "instance_name", + auth: { + method: "external", + }, + } as MerchantBackend.Instances.QueryInstancesResponse, + }); + + api.clearAccessToken(undefined); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + name: "instance_name", + auth: { + method: "external", + }, + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + // const { result, waitForNextUpdate } = renderHook( + // () => { + // const api = useInstanceAPI(); + // const query = useInstanceDetails(); + + // return { query, api }; + // }, + // { wrapper: TestingContext } + // ); + + // expect(result.current).not.undefined; + // if (!result.current) { + // return; + // } + // expect(result.current.query.loading).true; + + // await waitForNextUpdate({ timeout: 1 }); + + // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + // expect(result.current.query.loading).false; + + // expect(result.current?.query.ok).true; + // if (!result.current?.query.ok) return; + + // expect(result.current.query.data).equals({ + // name: 'instance_name', + // auth: { + // method: 'token', + // token: 'not-secret', + // } + // }); + + // act(async () => { + // await result.current?.api.clearToken(); + // }); + + // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + // expect(result.current.query.loading).false; + + // await waitForNextUpdate({ timeout: 1 }); + + // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + // expect(result.current.query.loading).false; + // expect(result.current.query.ok).true; + + // expect(result.current.query.data).equals({ + // name: 'instance_name', + // auth: { + // method: 'external', + // } + // }); + }); +}); + +describe("instance admin api interaction with listing", () => { + it("should evict cache when creating a new instance", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + name: "instance_name", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useAdminAPI(); + const query = useBackendInstances(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + name: "instance_name", + }, + ], + }); + + env.addRequestExpectation(API_CREATE_INSTANCE, { + request: { + name: "other_name", + } as MerchantBackend.Instances.InstanceConfigurationMessage, + }); + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + name: "instance_name", + } as MerchantBackend.Instances.Instance, + { + name: "other_name", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + api.createInstance({ + name: "other_name", + } as MerchantBackend.Instances.InstanceConfigurationMessage); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + name: "instance_name", + }, + { + name: "other_name", + }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when deleting an instance", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + id: "default", + name: "instance_name", + } as MerchantBackend.Instances.Instance, + { + id: "the_id", + name: "second_instance", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useAdminAPI(); + const query = useBackendInstances(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + id: "default", + name: "instance_name", + }, + { + id: "the_id", + name: "second_instance", + }, + ], + }); + + env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {}); + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + id: "default", + name: "instance_name", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + api.deleteInstance("the_id"); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + id: "default", + name: "instance_name", + }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + // const { result, waitForNextUpdate } = renderHook( + // () => { + // const api = useAdminAPI(); + // const query = useBackendInstances(); + + // return { query, api }; + // }, + // { wrapper: TestingContext } + // ); + + // expect(result.current).not.undefined; + // if (!result.current) { + // return; + // } + // expect(result.current.query.loading).true; + + // await waitForNextUpdate({ timeout: 1 }); + + // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + // expect(result.current.query.loading).false; + + // expect(result.current?.query.ok).true; + // if (!result.current?.query.ok) return; + + // expect(result.current.query.data).equals({ + // instances: [{ + // id: 'default', + // name: 'instance_name' + // }, { + // id: 'the_id', + // name: 'second_instance' + // }] + // }); + + // env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {}); + + // act(async () => { + // await result.current?.api.deleteInstance('the_id'); + // }); + + // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + // env.addRequestExpectation(API_LIST_INSTANCES, { + // response: { + // instances: [{ + // id: 'default', + // name: 'instance_name' + // } as MerchantBackend.Instances.Instance] + // }, + // }); + + // expect(result.current.query.loading).false; + + // await waitForNextUpdate({ timeout: 1 }); + + // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + // expect(result.current.query.loading).false; + // expect(result.current.query.ok).true; + + // expect(result.current.query.data).equals({ + // instances: [{ + // id: 'default', + // name: 'instance_name' + // }] + // }); + }); + + it("should evict cache when deleting (purge) an instance", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + id: "default", + name: "instance_name", + } as MerchantBackend.Instances.Instance, + { + id: "the_id", + name: "second_instance", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useAdminAPI(); + const query = useBackendInstances(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + id: "default", + name: "instance_name", + }, + { + id: "the_id", + name: "second_instance", + }, + ], + }); + + env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), { + qparam: { + purge: "YES", + }, + }); + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + id: "default", + name: "instance_name", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + api.purgeInstance("the_id"); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + id: "default", + name: "instance_name", + }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); + +describe("instance management api interaction with listing", () => { + it("should evict cache when updating an instance", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + id: "managed", + name: "instance_name", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useManagementAPI("managed"); + const query = useBackendInstances(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + id: "managed", + name: "instance_name", + }, + ], + }); + + env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), { + request: { + name: "other_name", + } as MerchantBackend.Instances.InstanceReconfigurationMessage, + }); + env.addRequestExpectation(API_LIST_INSTANCES, { + response: { + instances: [ + { + id: "managed", + name: "other_name", + } as MerchantBackend.Instances.Instance, + ], + }, + }); + + api.updateInstance({ + name: "other_name", + } as MerchantBackend.Instances.InstanceConfigurationMessage); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + instances: [ + { + id: "managed", + name: "other_name", + }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.ts b/packages/auditor-backoffice-ui/src/hooks/instance.ts new file mode 100644 index 000000000..0677191db --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/instance.ts @@ -0,0 +1,313 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useBackendContext } from "../context/backend.js"; +import { AccessToken, MerchantBackend } from "../declaration.js"; +import { + useBackendBaseRequest, + useBackendInstanceRequest, + useCredentialsChecker, + useMatchMutate, +} from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +interface InstanceAPI { + updateInstance: ( + data: MerchantBackend.Instances.InstanceReconfigurationMessage, + ) => Promise<void>; + deleteInstance: () => Promise<void>; + clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>; + setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>; +} + +export function useAdminAPI(): AdminAPI { + const { request } = useBackendBaseRequest(); + const mutateAll = useMatchMutate(); + + const createInstance = async ( + instance: MerchantBackend.Instances.InstanceConfigurationMessage, + ): Promise<void> => { + await request(`/management/instances`, { + method: "POST", + data: instance, + }); + + mutateAll(/\/management\/instances/); + }; + + const deleteInstance = async (id: string): Promise<void> => { + await request(`/management/instances/${id}`, { + method: "DELETE", + }); + + mutateAll(/\/management\/instances/); + }; + + const purgeInstance = async (id: string): Promise<void> => { + await request(`/management/instances/${id}`, { + method: "DELETE", + params: { + purge: "YES", + }, + }); + + mutateAll(/\/management\/instances/); + }; + + return { createInstance, deleteInstance, purgeInstance }; +} + +export interface AdminAPI { + createInstance: ( + data: MerchantBackend.Instances.InstanceConfigurationMessage, + ) => Promise<void>; + deleteInstance: (id: string) => Promise<void>; + purgeInstance: (id: string) => Promise<void>; +} + +export function useManagementAPI(instanceId: string): InstanceAPI { + const mutateAll = useMatchMutate(); + const { url: backendURL } = useBackendContext() + const { updateToken } = useBackendContext(); + const { request } = useBackendBaseRequest(); + const { requestNewLoginToken } = useCredentialsChecker() + + const updateInstance = async ( + instance: MerchantBackend.Instances.InstanceReconfigurationMessage, + ): Promise<void> => { + await request(`/management/instances/${instanceId}`, { + method: "PATCH", + data: instance, + }); + + mutateAll(/\/management\/instances/); + }; + + const deleteInstance = async (): Promise<void> => { + await request(`/management/instances/${instanceId}`, { + method: "DELETE", + }); + + mutateAll(/\/management\/instances/); + }; + + const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { + await request(`/management/instances/${instanceId}/auth`, { + method: "POST", + token: currentToken, + data: { method: "external" }, + }); + + mutateAll(/\/management\/instances/); + }; + + const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { + await request(`/management/instances/${instanceId}/auth`, { + method: "POST", + token: currentToken, + data: { method: "token", token: newToken }, + }); + + const resp = await requestNewLoginToken(backendURL, newToken) + if (resp.valid) { + const { token, expiration } = resp + updateToken({ token, expiration }); + } else { + updateToken(undefined) + } + + mutateAll(/\/management\/instances/); + }; + + return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; +} + +export function useInstanceAPI(): InstanceAPI { + const { mutate } = useSWRConfig(); + const { url: backendURL, updateToken } = useBackendContext() + + const { + token: adminToken, + } = useBackendContext(); + const { request } = useBackendInstanceRequest(); + const { requestNewLoginToken } = useCredentialsChecker() + + const updateInstance = async ( + instance: MerchantBackend.Instances.InstanceReconfigurationMessage, + ): Promise<void> => { + await request(`/private/`, { + method: "PATCH", + data: instance, + }); + + if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); + mutate([`/private/`], null); + }; + + const deleteInstance = async (): Promise<void> => { + await request(`/private/`, { + method: "DELETE", + // token: adminToken, + }); + + if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); + mutate([`/private/`], null); + }; + + const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { + await request(`/private/auth`, { + method: "POST", + token: currentToken, + data: { method: "external" }, + }); + + mutate([`/private/`], null); + }; + + const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { + await request(`/private/auth`, { + method: "POST", + token: currentToken, + data: { method: "token", token: newToken }, + }); + + const resp = await requestNewLoginToken(backendURL, newToken) + if (resp.valid) { + const { token, expiration } = resp + updateToken({ token, expiration }); + } else { + updateToken(undefined) + } + + mutate([`/private/`], null); + }; + + return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; +} + +export function useInstanceDetails(): HttpResponse< + MerchantBackend.Instances.QueryInstancesResponse, + MerchantBackend.ErrorDetail +> { + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + revalidateIfStale: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} + +type KYCStatus = + | { type: "ok" } + | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects }; + +export function useInstanceKYCDetails(): HttpResponse< + KYCStatus, + MerchantBackend.ErrorDetail +> { + const { fetcher } = useBackendInstanceRequest(); + + const { data, error } = useSWR< + HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/kyc`], fetcher, { + refreshInterval: 60 * 1000, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnMount: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + }); + + if (data) { + if (data.info?.status === 202) + return { ok: true, data: { type: "redirect", status: data.data } }; + return { ok: true, data: { type: "ok" } }; + } + if (error) return error.cause; + return { loading: true }; +} + +export function useManagedInstanceDetails( + instanceId: string, +): HttpResponse< + MerchantBackend.Instances.QueryInstancesResponse, + MerchantBackend.ErrorDetail +> { + const { request } = useBackendBaseRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/management/instances/${instanceId}`], request, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} + +export function useBackendInstances(): HttpResponse< + MerchantBackend.Instances.InstancesResponse, + MerchantBackend.ErrorDetail +> { + const { request } = useBackendBaseRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Instances.InstancesResponse>, + RequestError<MerchantBackend.ErrorDetail> + >(["/management/instances"], request); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/listener.ts b/packages/auditor-backoffice-ui/src/hooks/listener.ts new file mode 100644 index 000000000..d101f7bb8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/listener.ts @@ -0,0 +1,85 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useState } from "preact/hooks"; + +/** + * This component is used when a component wants one child to have a trigger for + * an action (a button) and other child have the action implemented (like + * gathering information with a form). The difference with other approaches is + * that in this case the parent component is not holding the state. + * + * It will return a subscriber and activator. + * + * The activator may be undefined, if it is undefined it is indicating that the + * subscriber is not ready to be called. + * + * The subscriber will receive a function (the listener) that will be call when the + * activator runs. The listener must return the collected information. + * + * As a result, when the activator is triggered by a child component, the + * @action function is called receives the information from the listener defined by other + * child component + * + * @param action from <T> to <R> + * @returns activator and subscriber, undefined activator means that there is not subscriber + */ + +export function useListener<T, R = any>( + action: (r: T) => Promise<R>, +): [undefined | (() => Promise<R>), (listener?: () => T) => void] { + type RunnerHandler = { toBeRan?: () => Promise<R> }; + const [state, setState] = useState<RunnerHandler>({}); + + /** + * subscriber will receive a method that will be call when the activator runs + * + * @param listener function to be run when the activator runs + */ + const subscriber = (listener?: () => T) => { + if (listener) { + setState({ + toBeRan: () => { + const whatWeGetFromTheListener = listener(); + return action(whatWeGetFromTheListener); + }, + }); + } else { + setState({ + toBeRan: undefined, + }); + } + }; + + /** + * activator will call runner if there is someone subscribed + */ + const activator = state.toBeRan + ? async () => { + if (state.toBeRan) { + return state.toBeRan(); + } + return Promise.reject(); + } + : undefined; + + return [activator, subscriber]; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/notifications.ts b/packages/auditor-backoffice-ui/src/hooks/notifications.ts new file mode 100644 index 000000000..133ddd80b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/notifications.ts @@ -0,0 +1,56 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useState } from "preact/hooks"; +import { Notification } from "../utils/types.js"; + +interface Result { + notifications: Notification[]; + pushNotification: (n: Notification) => void; + removeNotification: (n: Notification) => void; +} + +type NotificationWithDate = Notification & { since: Date }; + +export function useNotifications( + initial: Notification[] = [], + timeout = 3000, +): Result { + const [notifications, setNotifications] = useState<NotificationWithDate[]>( + initial.map((i) => ({ ...i, since: new Date() })), + ); + + const pushNotification = (n: Notification): void => { + const entry = { ...n, since: new Date() }; + setNotifications((ns) => [...ns, entry]); + if (n.type !== "ERROR") + setTimeout(() => { + setNotifications((ns) => ns.filter((x) => x.since !== entry.since)); + }, timeout); + }; + + const removeNotification = (notif: Notification) => { + setNotifications((ns: NotificationWithDate[]) => + ns.filter((n) => n !== notif), + ); + }; + return { notifications, pushNotification, removeNotification }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/order.test.ts b/packages/auditor-backoffice-ui/src/hooks/order.test.ts new file mode 100644 index 000000000..c243309a8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/order.test.ts @@ -0,0 +1,587 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { + API_CREATE_ORDER, + API_DELETE_ORDER, + API_FORGET_ORDER_BY_ID, + API_GET_ORDER_BY_ID, + API_LIST_ORDERS, + API_REFUND_ORDER_BY_ID, +} from "./urls.js"; + +describe("order api interaction with listing", () => { + it("should evict cache when creating an order", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, paid: "yes" }, + response: { + orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], + }, + }); + + const newDate = (d: Date) => { + //console.log("new date", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useInstanceOrders({ paid: "yes" }, newDate); + const api = useOrderAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [{ order_id: "1" }, { order_id: "2" }], + }); + + env.addRequestExpectation(API_CREATE_ORDER, { + request: { + order: { amount: "ARS:12", summary: "pay me" }, + }, + response: { order_id: "3" }, + }); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, paid: "yes" }, + response: { + orders: [{ order_id: "1" }, { order_id: "2" } as any, { order_id: "3" } as any], + }, + }); + + api.createOrder({ + order: { amount: "ARS:12", summary: "pay me" }, + } as any); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when doing a refund", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, paid: "yes" }, + response: { orders: [{ + order_id: "1", + amount: "EUR:12", + refundable: true, + } as MerchantBackend.Orders.OrderHistoryEntry] }, + }); + + const newDate = (d: Date) => { + //console.log("new date", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useInstanceOrders({ paid: "yes" }, newDate); + const api = useOrderAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [ + { + order_id: "1", + amount: "EUR:12", + refundable: true, + }, + ], + }); + env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { + request: { + reason: "double pay", + refund: "EUR:1", + }, + }); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, paid: "yes" }, + response: { orders: [ + { order_id: "1", amount: "EUR:12", refundable: false } as any, + ] }, + }); + + api.refundOrder("1", { + reason: "double pay", + refund: "EUR:1", + }); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [ + { + order_id: "1", + amount: "EUR:12", + refundable: false, + }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when deleting an order", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, paid: "yes" }, + response: { + orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], + }, + }); + + const newDate = (d: Date) => { + //console.log("new date", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useInstanceOrders({ paid: "yes" }, newDate); + const api = useOrderAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [{ order_id: "1" }, { order_id: "2" }], + }); + + env.addRequestExpectation(API_DELETE_ORDER("1"), {}); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, paid: "yes" }, + response: { + orders: [{ order_id: "2" } as any], + }, + }); + + api.deleteOrder("1"); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [{ order_id: "2" }], + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); + +describe("order api interaction with details", () => { + it("should evict cache when doing a refund", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { + // qparam: { delta: 0, paid: "yes" }, + response: { + summary: "description", + refund_amount: "EUR:0", + } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + }); + + const newDate = (d: Date) => { + //console.log("new date", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useOrderDetails("1"); + const api = useOrderAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + summary: "description", + refund_amount: "EUR:0", + }); + env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { + request: { + reason: "double pay", + refund: "EUR:1", + }, + }); + + env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { + response: { + summary: "description", + refund_amount: "EUR:1", + } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + }); + + api.refundOrder("1", { + reason: "double pay", + refund: "EUR:1", + }); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + summary: "description", + refund_amount: "EUR:1", + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when doing a forget", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { + // qparam: { delta: 0, paid: "yes" }, + response: { + summary: "description", + refund_amount: "EUR:0", + } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + }); + + const newDate = (d: Date) => { + //console.log("new date", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useOrderDetails("1"); + const api = useOrderAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + summary: "description", + refund_amount: "EUR:0", + }); + env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), { + request: { + fields: ["$.summary"], + }, + }); + + env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { + response: { + summary: undefined, + } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + }); + + api.forgetOrder("1", { + fields: ["$.summary"], + }); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + summary: undefined, + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); + +describe("order listing pagination", () => { + it("should not load more if has reach the end", async () => { + const env = new ApiMockEnvironment(); + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: 20, wired: "yes", date_s: 12 }, + response: { + orders: [{ order_id: "1" } as any], + }, + }); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, wired: "yes", date_s: 13 }, + response: { + orders: [{ order_id: "2" } as any], + }, + }); + + const newDate = (d: Date) => { + //console.log("new date", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const date = new Date(12000); + const query = useInstanceOrders({ wired: "yes", date }, newDate); + const api = useOrderAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [{ order_id: "1" }, { order_id: "2" }], + }); + expect(query.isReachingEnd).true; + expect(query.isReachingStart).true; + + // should not trigger new state update or query + query.loadMore(); + query.loadMorePrev(); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should load more if result brings more that PAGE_SIZE", async () => { + const env = new ApiMockEnvironment(); + + const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ + order_id: String(i), + })); + const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ + order_id: String(i + 20), + })); + const ordersFrom20to0 = [...ordersFrom0to20].reverse(); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: 20, wired: "yes", date_s: 12 }, + response: { + orders: ordersFrom0to20, + }, + }); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -20, wired: "yes", date_s: 13 }, + response: { + orders: ordersFrom20to40, + }, + }); + + const newDate = (d: Date) => { + //console.log("new date", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const date = new Date(12000); + const query = useInstanceOrders({ wired: "yes", date }, newDate); + const api = useOrderAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [...ordersFrom20to0, ...ordersFrom20to40], + }); + expect(query.isReachingEnd).false; + expect(query.isReachingStart).false; + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: -40, wired: "yes", date_s: 13 }, + response: { + orders: [...ordersFrom20to40, { order_id: "41" }], + }, + }); + + query.loadMore(); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + ({ query, api }) => { + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [ + ...ordersFrom20to0, + ...ordersFrom20to40, + { order_id: "41" }, + ], + }); + + env.addRequestExpectation(API_LIST_ORDERS, { + qparam: { delta: 40, wired: "yes", date_s: 12 }, + response: { + orders: [...ordersFrom0to20, { order_id: "-1" }], + }, + }); + + query.loadMorePrev(); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + ({ query, api }) => { + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + orders: [ + { order_id: "-1" }, + ...ordersFrom20to0, + ...ordersFrom20to40, + { order_id: "41" }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/hooks/order.ts b/packages/auditor-backoffice-ui/src/hooks/order.ts new file mode 100644 index 000000000..e7a893f2c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/order.ts @@ -0,0 +1,289 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +export interface OrderAPI { + //FIXME: add OutOfStockResponse on 410 + createOrder: ( + data: MerchantBackend.Orders.PostOrderRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>>; + forgetOrder: ( + id: string, + data: MerchantBackend.Orders.ForgetRequest, + ) => Promise<HttpResponseOk<void>>; + refundOrder: ( + id: string, + data: MerchantBackend.Orders.RefundRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>>; + deleteOrder: (id: string) => Promise<HttpResponseOk<void>>; + getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>; +} + +type YesOrNo = "yes" | "no"; + +export function useOrderAPI(): OrderAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const createOrder = async ( + data: MerchantBackend.Orders.PostOrderRequest, + ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => { + const res = await request<MerchantBackend.Orders.PostOrderResponse>( + `/private/orders`, + { + method: "POST", + data, + }, + ); + await mutateAll(/.*private\/orders.*/); + // mutate('') + return res; + }; + const refundOrder = async ( + orderId: string, + data: MerchantBackend.Orders.RefundRequest, + ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => { + mutateAll(/@"\/private\/orders"@/); + const res = request<MerchantBackend.Orders.MerchantRefundResponse>( + `/private/orders/${orderId}/refund`, + { + method: "POST", + data, + }, + ); + + // order list returns refundable information, so we must evict everything + await mutateAll(/.*private\/orders.*/); + return res; + }; + + const forgetOrder = async ( + orderId: string, + data: MerchantBackend.Orders.ForgetRequest, + ): Promise<HttpResponseOk<void>> => { + mutateAll(/@"\/private\/orders"@/); + const res = request<void>(`/private/orders/${orderId}/forget`, { + method: "PATCH", + data, + }); + // we may be forgetting some fields that are pare of the listing, so we must evict everything + await mutateAll(/.*private\/orders.*/); + return res; + }; + const deleteOrder = async ( + orderId: string, + ): Promise<HttpResponseOk<void>> => { + mutateAll(/@"\/private\/orders"@/); + const res = request<void>(`/private/orders/${orderId}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/orders.*/); + return res; + }; + + const getPaymentURL = async ( + orderId: string, + ): Promise<HttpResponseOk<string>> => { + return request<MerchantBackend.Orders.MerchantOrderStatusResponse>( + `/private/orders/${orderId}`, + { + method: "GET", + }, + ).then((res) => { + const url = + res.data.order_status === "unpaid" + ? res.data.taler_pay_uri + : res.data.contract_terms.fulfillment_url; + const response: HttpResponseOk<string> = res as any; + response.data = url || ""; + return response; + }); + }; + + return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL }; +} + +export function useOrderDetails( + oderId: string, +): HttpResponse< + MerchantBackend.Orders.MerchantOrderStatusResponse, + MerchantBackend.ErrorDetail +> { + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/orders/${oderId}`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} + +export interface InstanceOrderFilter { + paid?: YesOrNo; + refunded?: YesOrNo; + wired?: YesOrNo; + date?: Date; +} + +export function useInstanceOrders( + args?: InstanceOrderFilter, + updateFilter?: (d: Date) => void, +): HttpResponsePaginated< + MerchantBackend.Orders.OrderHistory, + MerchantBackend.ErrorDetail +> { + const { orderFetcher } = useBackendInstanceRequest(); + + const [pageBefore, setPageBefore] = useState(1); + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0; + + /** + * FIXME: this can be cleaned up a little + * + * the logic of double query should be inside the orderFetch so from the hook perspective and cache + * is just one query and one error status + */ + const { + data: beforeData, + error: beforeError, + isValidating: loadingBefore, + } = useSWR< + HttpResponseOk<MerchantBackend.Orders.OrderHistory>, + RequestError<MerchantBackend.ErrorDetail> + >( + [ + `/private/orders`, + args?.paid, + args?.refunded, + args?.wired, + args?.date, + totalBefore, + ], + orderFetcher, + ); + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.Orders.OrderHistory>, + RequestError<MerchantBackend.ErrorDetail> + >( + [ + `/private/orders`, + args?.paid, + args?.refunded, + args?.wired, + args?.date, + -totalAfter, + ], + orderFetcher, + ); + + //this will save last result + const [lastBefore, setLastBefore] = useState< + HttpResponse< + MerchantBackend.Orders.OrderHistory, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.Orders.OrderHistory, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + if (beforeData) setLastBefore(beforeData); + }, [afterData, beforeData]); + + if (beforeError) return beforeError.cause; + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = afterData && afterData.data.orders.length < totalAfter; + const isReachingStart = + args?.date === undefined || + (beforeData && beforeData.data.orders.length < totalBefore); + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.orders.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = + afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s; + if (from && from !== "never" && updateFilter) + updateFilter(new Date(from * 1000)); + } + }, + loadMorePrev: () => { + if (!beforeData || isReachingStart) return; + if (beforeData.data.orders.length < MAX_RESULT_SIZE) { + setPageBefore(pageBefore + 1); + } else if (beforeData) { + const from = + beforeData.data.orders[beforeData.data.orders.length - 1].timestamp + .t_s; + if (from && from !== "never" && updateFilter) + updateFilter(new Date(from * 1000)); + } + }, + }; + + const orders = + !beforeData || !afterData + ? [] + : (beforeData || lastBefore).data.orders + .slice() + .reverse() + .concat((afterData || lastAfter).data.orders); + if (loadingAfter || loadingBefore) return { loading: true, data: { orders } }; + if (beforeData && afterData) { + return { ok: true, data: { orders }, ...pagination }; + } + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/otp.ts b/packages/auditor-backoffice-ui/src/hooks/otp.ts new file mode 100644 index 000000000..b045e365a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/otp.ts @@ -0,0 +1,223 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = { + "1": { + otp_device_description: "first device", + otp_algorithm: 1, + otp_device_id: "1", + otp_key: "123", + }, + "2": { + otp_device_description: "second device", + otp_algorithm: 0, + otp_device_id: "2", + otp_key: "456", + } +} + +export function useOtpDeviceAPI(): OtpDeviceAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const createOtpDevice = async ( + data: MerchantBackend.OTP.OtpDeviceAddDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_DEVICES[data.otp_device_id] = data + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/otp-devices`, { + method: "POST", + data, + }); + await mutateAll(/.*private\/otp-devices.*/); + return res; + }; + + const updateOtpDevice = async ( + deviceId: string, + data: MerchantBackend.OTP.OtpDevicePatchDetails, + ): Promise<HttpResponseOk<void>> => { + // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm + // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr + // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description + // MOCKED_DEVICES[deviceId].otp_key = data.otp_key + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/otp-devices/${deviceId}`, { + method: "PATCH", + data, + }); + await mutateAll(/.*private\/otp-devices.*/); + return res; + }; + + const deleteOtpDevice = async ( + deviceId: string, + ): Promise<HttpResponseOk<void>> => { + // delete MOCKED_DEVICES[deviceId] + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/otp-devices/${deviceId}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/otp-devices.*/); + return res; + }; + + return { + createOtpDevice, + updateOtpDevice, + deleteOtpDevice, + }; +} + +export interface OtpDeviceAPI { + createOtpDevice: ( + data: MerchantBackend.OTP.OtpDeviceAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateOtpDevice: ( + id: string, + data: MerchantBackend.OTP.OtpDevicePatchDetails, + ) => Promise<HttpResponseOk<void>>; + deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; +} + +export interface InstanceOtpDeviceFilter { +} + +export function useInstanceOtpDevices( + args?: InstanceOtpDeviceFilter, + updatePosition?: (id: string) => void, +): HttpResponsePaginated< + MerchantBackend.OTP.OtpDeviceSummaryResponse, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // loadMore: () => { }, + // loadMorePrev: () => { }, + // data: { + // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({ + // device_description: d.otp_device_description, + // otp_device_id: d.otp_device_id + // })) + // } + // } + + const { fetcher } = useBackendInstanceRequest(); + + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/otp-devices`], fetcher); + + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.OTP.OtpDeviceSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + }, [afterData /*, beforeData*/]); + + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data.otp_devices.length < totalAfter; + const isReachingStart = true; + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1] + .otp_device_id + }`; + if (from && updatePosition) updatePosition(from); + } + }, + loadMorePrev: () => { + }, + }; + + const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices; + if (loadingAfter /* || loadingBefore */) + return { loading: true, data: { otp_devices } }; + if (/*beforeData &&*/ afterData) { + return { ok: true, data: { otp_devices }, ...pagination }; + } + return { loading: true }; +} + +export function useOtpDeviceDetails( + deviceId: string, +): HttpResponse< + MerchantBackend.OTP.OtpDeviceDetails, + MerchantBackend.ErrorDetail +> { + // return { + // ok: true, + // data: { + // device_description: MOCKED_DEVICES[deviceId].otp_device_description, + // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm, + // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr + // } + // } + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/otp-devices/${deviceId}`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) { + return data; + } + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/product.test.ts b/packages/auditor-backoffice-ui/src/hooks/product.test.ts new file mode 100644 index 000000000..7cac10e25 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/product.test.ts @@ -0,0 +1,362 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { + useInstanceProducts, + useProductAPI, + useProductDetails, +} from "./product.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { + API_CREATE_PRODUCT, + API_DELETE_PRODUCT, + API_GET_PRODUCT_BY_ID, + API_LIST_PRODUCTS, + API_UPDATE_PRODUCT_BY_ID, +} from "./urls.js"; + +describe("product api interaction with listing", () => { + it("should evict cache when creating a product", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_PRODUCTS, { + response: { + products: [{ product_id: "1234" }], + }, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { + response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useInstanceProducts(); + const api = useProductAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + ({ query, api }) => { + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + + env.addRequestExpectation(API_CREATE_PRODUCT, { + request: { + price: "ARS:23", + } as MerchantBackend.Products.ProductAddDetail, + }); + + env.addRequestExpectation(API_LIST_PRODUCTS, { + response: { + products: [{ product_id: "1234" }, { product_id: "2345" }], + }, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { + response: { + price: "ARS:12", + } as MerchantBackend.Products.ProductDetail, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { + response: { + price: "ARS:12", + } as MerchantBackend.Products.ProductDetail, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { + response: { + price: "ARS:23", + } as MerchantBackend.Products.ProductDetail, + }); + + api.createProduct({ + price: "ARS:23", + } as any); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals([ + { + id: "1234", + price: "ARS:12", + }, + { + id: "2345", + price: "ARS:23", + }, + ]); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when updating a product", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_PRODUCTS, { + response: { + products: [{ product_id: "1234" }], + }, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { + response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useInstanceProducts(); + const api = useProductAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + + env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { + request: { + price: "ARS:13", + } as MerchantBackend.Products.ProductPatchDetail, + }); + + env.addRequestExpectation(API_LIST_PRODUCTS, { + response: { + products: [{ product_id: "1234" }], + }, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { + response: { + price: "ARS:13", + } as MerchantBackend.Products.ProductDetail, + }); + + api.updateProduct("1234", { + price: "ARS:13", + } as any); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals([ + { + id: "1234", + price: "ARS:13", + }, + ]); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when deleting a product", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_PRODUCTS, { + response: { + products: [{ product_id: "1234" }, { product_id: "2345" }], + }, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { + response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + }); + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { + response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useInstanceProducts(); + const api = useProductAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + ({ query, api }) => { + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals([ + { id: "1234", price: "ARS:12" }, + { id: "2345", price: "ARS:23" }, + ]); + + env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); + + env.addRequestExpectation(API_LIST_PRODUCTS, { + response: { + products: [{ product_id: "1234" }], + }, + }); + + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { + response: { + price: "ARS:12", + } as MerchantBackend.Products.ProductDetail, + }); + api.deleteProduct("2345"); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); + +describe("product api interaction with details", () => { + it("should evict cache when updating a product", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { + response: { + description: "this is a description", + } as MerchantBackend.Products.ProductDetail, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useProductDetails("12"); + const api = useProductAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + description: "this is a description", + }); + + env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { + request: { + description: "other description", + } as MerchantBackend.Products.ProductPatchDetail, + }); + + env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { + response: { + description: "other description", + } as MerchantBackend.Products.ProductDetail, + }); + + api.updateProduct("12", { + description: "other description", + } as any); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + description: "other description", + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/hooks/product.ts b/packages/auditor-backoffice-ui/src/hooks/product.ts new file mode 100644 index 000000000..b8f55cb77 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/product.ts @@ -0,0 +1,177 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend, MerchantBackend, WithId } from "../declaration.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +export interface ProductAPI { + getProduct: ( + id: string, + ) => Promise<void>; + createProduct: ( + data: MerchantBackend.Products.ProductAddDetail, + ) => Promise<void>; + updateProduct: ( + id: string, + data: MerchantBackend.Products.ProductPatchDetail, + ) => Promise<void>; + deleteProduct: (id: string) => Promise<void>; + lockProduct: ( + id: string, + data: MerchantBackend.Products.LockRequest, + ) => Promise<void>; +} + +export function useProductAPI(): ProductAPI { + const mutateAll = useMatchMutate(); + const { mutate } = useSWRConfig(); + + const { request } = useBackendInstanceRequest(); + + const createProduct = async ( + data: MerchantBackend.Products.ProductAddDetail, + ): Promise<void> => { + const res = await request(`/private/products`, { + method: "POST", + data, + }); + + return await mutateAll(/.*\/private\/products.*/); + }; + + const updateProduct = async ( + productId: string, + data: MerchantBackend.Products.ProductPatchDetail, + ): Promise<void> => { + const r = await request(`/private/products/${productId}`, { + method: "PATCH", + data, + }); + + return await mutateAll(/.*\/private\/products.*/); + }; + + const deleteProduct = async (productId: string): Promise<void> => { + await request(`/private/products/${productId}`, { + method: "DELETE", + }); + await mutate([`/private/products`]); + }; + + const lockProduct = async ( + productId: string, + data: MerchantBackend.Products.LockRequest, + ): Promise<void> => { + await request(`/private/products/${productId}/lock`, { + method: "POST", + data, + }); + + return await mutateAll(/.*"\/private\/products.*/); + }; + + const getProduct = async ( + productId: string, + ): Promise<void> => { + await request(`/private/products/${productId}`, { + method: "GET", + }); + + return + }; + + return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct }; +} + +export function useInstanceProducts(): HttpResponse< + (MerchantBackend.Products.ProductDetail & WithId)[], + MerchantBackend.ErrorDetail +> { + const { fetcher, multiFetcher } = useBackendInstanceRequest(); + + const { data: list, error: listError } = useSWR< + HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/products`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + const paths = (list?.data.products || []).map( + (p) => `/deposit-confirmation/${p.serial_id}`, + ); + const { data: products, error: productError } = useSWR< + HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>[], + RequestError<AuditorBackend.ErrorDetail> + >([paths], multiFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + if (productError) return productError.cause; + + if (products) { + const dataWithId = products.map((d) => { + //take the id from the queried url + return { + ...d.data, + id: d.info?.url.replace(/.*\/private\/products\//, "") || "", + }; + }); + return { ok: true, data: dataWithId }; + } + return { loading: true }; +} + +export function useProductDetails( + productId: string, +): HttpResponse< + MerchantBackend.Products.ProductDetail, + MerchantBackend.ErrorDetail +> { + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Products.ProductDetail>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/products/${productId}`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts b/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts new file mode 100644 index 000000000..b3eecd754 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts @@ -0,0 +1,448 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { + useInstanceReserves, + useReserveDetails, + useReservesAPI, + useRewardDetails, +} from "./reserves.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { + API_AUTHORIZE_REWARD, + API_AUTHORIZE_REWARD_FOR_RESERVE, + API_CREATE_RESERVE, + API_DELETE_RESERVE, + API_GET_RESERVE_BY_ID, + API_GET_REWARD_BY_ID, + API_LIST_RESERVES, +} from "./urls.js"; +import * as tests from "@gnu-taler/web-util/testing"; + +describe("reserve api interaction with listing", () => { + it("should evict cache when creating a reserve", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_RESERVES, { + response: { + reserves: [ + { + reserve_pub: "11", + } as MerchantBackend.Rewards.ReserveStatusEntry, + ], + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useReservesAPI(); + const query = useInstanceReserves(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + reserves: [{ reserve_pub: "11" }], + }); + + env.addRequestExpectation(API_CREATE_RESERVE, { + request: { + initial_balance: "ARS:3333", + exchange_url: "http://url", + wire_method: "iban", + }, + response: { + reserve_pub: "22", + accounts: [], + }, + }); + + env.addRequestExpectation(API_LIST_RESERVES, { + response: { + reserves: [ + { + reserve_pub: "11", + } as MerchantBackend.Rewards.ReserveStatusEntry, + { + reserve_pub: "22", + } as MerchantBackend.Rewards.ReserveStatusEntry, + ], + }, + }); + + api.createReserve({ + initial_balance: "ARS:3333", + exchange_url: "http://url", + wire_method: "iban", + }); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + + expect(query.data).deep.equals({ + reserves: [ + { + reserve_pub: "11", + } as MerchantBackend.Rewards.ReserveStatusEntry, + { + reserve_pub: "22", + } as MerchantBackend.Rewards.ReserveStatusEntry, + ], + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when deleting a reserve", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_RESERVES, { + response: { + reserves: [ + { + reserve_pub: "11", + } as MerchantBackend.Rewards.ReserveStatusEntry, + { + reserve_pub: "22", + } as MerchantBackend.Rewards.ReserveStatusEntry, + { + reserve_pub: "33", + } as MerchantBackend.Rewards.ReserveStatusEntry, + ], + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useReservesAPI(); + const query = useInstanceReserves(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + reserves: [ + { reserve_pub: "11" }, + { reserve_pub: "22" }, + { reserve_pub: "33" }, + ], + }); + + env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); + env.addRequestExpectation(API_LIST_RESERVES, { + response: { + reserves: [ + { + reserve_pub: "22", + } as MerchantBackend.Rewards.ReserveStatusEntry, + { + reserve_pub: "33", + } as MerchantBackend.Rewards.ReserveStatusEntry, + ], + }, + }); + + api.deleteReserve("11"); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + reserves: [{ reserve_pub: "22" }, { reserve_pub: "33" }], + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); + +describe("reserve api interaction with details", () => { + it("should evict cache when adding a reward for a specific reserve", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { + response: { + accounts: [{ payto_uri: "payto://here" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + } as MerchantBackend.Rewards.ReserveDetail, + qparam: { + rewards: "yes", + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useReservesAPI(); + const query = useReserveDetails("11"); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + accounts: [{ payto_uri: "payto://here" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + }); + + env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), { + request: { + amount: "USD:12", + justification: "not", + next_url: "http://taler.net", + }, + response: { + reward_id: "id2", + taler_reward_uri: "uri", + reward_expiration: { t_s: 1 }, + reward_status_url: "url", + }, + }); + + env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { + response: { + accounts: [{ payto_uri: "payto://here" }], + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, + ], + } as MerchantBackend.Rewards.ReserveDetail, + qparam: { + rewards: "yes", + }, + }); + + api.authorizeRewardReserve("11", { + amount: "USD:12", + justification: "not", + next_url: "http://taler.net", + }); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + + expect(query.data).deep.equals({ + accounts: [{ payto_uri: "payto://here" }], + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); + + it("should evict cache when adding a reward for a random reserve", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { + response: { + accounts: [{ payto_uri: "payto://here" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + } as MerchantBackend.Rewards.ReserveDetail, + qparam: { + rewards: "yes", + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const api = useReservesAPI(); + const query = useReserveDetails("11"); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + accounts: [{ payto_uri: "payto://here" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + }); + + env.addRequestExpectation(API_AUTHORIZE_REWARD, { + request: { + amount: "USD:12", + justification: "not", + next_url: "http://taler.net", + }, + response: { + reward_id: "id2", + taler_reward_uri: "uri", + reward_expiration: { t_s: 1 }, + reward_status_url: "url", + }, + }); + + env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { + response: { + accounts: [{ payto_uri: "payto://here" }], + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, + ], + } as MerchantBackend.Rewards.ReserveDetail, + qparam: { + rewards: "yes", + }, + }); + + api.authorizeReward({ + amount: "USD:12", + justification: "not", + next_url: "http://taler.net", + }); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + + expect(query.data).deep.equals({ + accounts: [{ payto_uri: "payto://here" }], + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, + ], + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); + +describe("reserve api interaction with reward details", () => { + it("should list rewards", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), { + response: { + total_picked_up: "USD:12", + reason: "not", + } as MerchantBackend.Rewards.RewardDetails, + qparam: { + pickups: "yes", + }, + }); + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useRewardDetails("11"); + return { query }; + }, + {}, + [ + ({ query }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + ({ query }) => { + expect(query.loading).false; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + total_picked_up: "USD:12", + reason: "not", + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/hooks/reserves.ts b/packages/auditor-backoffice-ui/src/hooks/reserves.ts new file mode 100644 index 000000000..b719bfbe6 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/reserves.ts @@ -0,0 +1,181 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { MerchantBackend } from "../declaration.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +export function useReservesAPI(): ReserveMutateAPI { + const mutateAll = useMatchMutate(); + const { mutate } = useSWRConfig(); + const { request } = useBackendInstanceRequest(); + + const createReserve = async ( + data: MerchantBackend.Rewards.ReserveCreateRequest, + ): Promise< + HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation> + > => { + const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>( + `/private/reserves`, + { + method: "POST", + data, + }, + ); + + //evict reserve list query + await mutateAll(/.*private\/reserves.*/); + + return res; + }; + + const authorizeRewardReserve = async ( + pub: string, + data: MerchantBackend.Rewards.RewardCreateRequest, + ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { + const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( + `/private/reserves/${pub}/authorize-reward`, + { + method: "POST", + data, + }, + ); + + //evict reserve details query + await mutate([`/private/reserves/${pub}`]); + + return res; + }; + + const authorizeReward = async ( + data: MerchantBackend.Rewards.RewardCreateRequest, + ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { + const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( + `/private/rewards`, + { + method: "POST", + data, + }, + ); + + //evict all details query + await mutateAll(/.*private\/reserves\/.*/); + + return res; + }; + + const deleteReserve = async ( + pub: string, + ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => { + const res = await request<void>(`/private/reserves/${pub}`, { + method: "DELETE", + }); + + //evict reserve list query + await mutateAll(/.*private\/reserves.*/); + + return res; + }; + + return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve }; +} + +export interface ReserveMutateAPI { + createReserve: ( + data: MerchantBackend.Rewards.ReserveCreateRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>; + authorizeRewardReserve: ( + id: string, + data: MerchantBackend.Rewards.RewardCreateRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; + authorizeReward: ( + data: MerchantBackend.Rewards.RewardCreateRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; + deleteReserve: ( + id: string, + ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>; +} + +export function useInstanceReserves(): HttpResponse< + MerchantBackend.Rewards.RewardReserveStatus, + MerchantBackend.ErrorDetail +> { + const { fetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/reserves`], fetcher); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} + +export function useReserveDetails( + reserveId: string, +): HttpResponse< + MerchantBackend.Rewards.ReserveDetail, + MerchantBackend.ErrorDetail +> { + const { reserveDetailFetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/reserves/${reserveId}`], reserveDetailFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} + +export function useRewardDetails( + rewardId: string, +): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> { + const { rewardsDetailFetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Rewards.RewardDetails>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/templates.ts b/packages/auditor-backoffice-ui/src/hooks/templates.ts new file mode 100644 index 000000000..ee8728cc8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/templates.ts @@ -0,0 +1,266 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +export function useTemplateAPI(): TemplateAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const createTemplate = async ( + data: MerchantBackend.Template.TemplateAddDetails, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/templates`, { + method: "POST", + data, + }); + await mutateAll(/.*private\/templates.*/); + return res; + }; + + const updateTemplate = async ( + templateId: string, + data: MerchantBackend.Template.TemplatePatchDetails, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/templates/${templateId}`, { + method: "PATCH", + data, + }); + await mutateAll(/.*private\/templates.*/); + return res; + }; + + const deleteTemplate = async ( + templateId: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/templates/${templateId}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/templates.*/); + return res; + }; + + const createOrderFromTemplate = async ( + templateId: string, + data: MerchantBackend.Template.UsingTemplateDetails, + ): Promise< + HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse> + > => { + const res = await request<MerchantBackend.Template.UsingTemplateResponse>( + `/templates/${templateId}`, + { + method: "POST", + data, + }, + ); + await mutateAll(/.*private\/templates.*/); + return res; + }; + + const testTemplateExist = async ( + templateId: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", }); + return res; + }; + + + return { + createTemplate, + updateTemplate, + deleteTemplate, + testTemplateExist, + createOrderFromTemplate, + }; +} + +export interface TemplateAPI { + createTemplate: ( + data: MerchantBackend.Template.TemplateAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateTemplate: ( + id: string, + data: MerchantBackend.Template.TemplatePatchDetails, + ) => Promise<HttpResponseOk<void>>; + testTemplateExist: ( + id: string + ) => Promise<HttpResponseOk<void>>; + deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>; + createOrderFromTemplate: ( + id: string, + data: MerchantBackend.Template.UsingTemplateDetails, + ) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>; +} + +export interface InstanceTemplateFilter { + //FIXME: add filter to the template list + position?: string; +} + +export function useInstanceTemplates( + args?: InstanceTemplateFilter, + updatePosition?: (id: string) => void, +): HttpResponsePaginated< + MerchantBackend.Template.TemplateSummaryResponse, + MerchantBackend.ErrorDetail +> { + const { templateFetcher } = useBackendInstanceRequest(); + + const [pageBefore, setPageBefore] = useState(1); + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; + + /** + * FIXME: this can be cleaned up a little + * + * the logic of double query should be inside the orderFetch so from the hook perspective and cache + * is just one query and one error status + */ + const { + data: beforeData, + error: beforeError, + isValidating: loadingBefore, + } = useSWR< + HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail>>( + [ + `/private/templates`, + args?.position, + totalBefore, + ], + templateFetcher, + ); + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/templates`, args?.position, -totalAfter], templateFetcher); + + //this will save last result + const [lastBefore, setLastBefore] = useState< + HttpResponse< + MerchantBackend.Template.TemplateSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.Template.TemplateSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + if (beforeData) setLastBefore(beforeData); + }, [afterData, beforeData]); + + if (beforeError) return beforeError.cause; + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data.templates.length < totalAfter; + const isReachingStart = args?.position === undefined + || + (beforeData && beforeData.data.templates.length < totalBefore); + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.templates.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = `${afterData.data.templates[afterData.data.templates.length - 1] + .template_id + }`; + if (from && updatePosition) updatePosition(from); + } + }, + loadMorePrev: () => { + if (!beforeData || isReachingStart) return; + if (beforeData.data.templates.length < MAX_RESULT_SIZE) { + setPageBefore(pageBefore + 1); + } else if (beforeData) { + const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] + .template_id + }`; + if (from && updatePosition) updatePosition(from); + } + }, + }; + + // const templates = !afterData ? [] : (afterData || lastAfter).data.templates; + const templates = + !beforeData || !afterData + ? [] + : (beforeData || lastBefore).data.templates + .slice() + .reverse() + .concat((afterData || lastAfter).data.templates); + if (loadingAfter || loadingBefore) + return { loading: true, data: { templates } }; + if (beforeData && afterData) { + return { ok: true, data: { templates }, ...pagination }; + } + return { loading: true }; +} + +export function useTemplateDetails( + templateId: string, +): HttpResponse< + MerchantBackend.Template.TemplateDetails, + MerchantBackend.ErrorDetail +> { + const { templateFetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Template.TemplateDetails>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/templates/${templateId}`], templateFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) { + return data; + } + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/testing.tsx b/packages/auditor-backoffice-ui/src/hooks/testing.tsx new file mode 100644 index 000000000..3ea22475b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/testing.tsx @@ -0,0 +1,180 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { MockEnvironment } from "@gnu-taler/web-util/testing"; +import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; +import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; +import { SWRConfig } from "swr"; +import { ApiContextProvider } from "@gnu-taler/web-util/browser"; +import { BackendContextProvider } from "../context/backend.js"; +import { InstanceContextProvider } from "../context/instance.js"; +import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser"; +import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient } from "@gnu-taler/taler-util"; + +export class ApiMockEnvironment extends MockEnvironment { + constructor(debug = false) { + super(debug); + } + + mockApiIfNeeded(): void { + null; // do nothing + } + + public buildTestingContext(): FunctionalComponent<{ + children: ComponentChildren; + }> { + const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE = + this.saveRequestAndGetMockedResponse.bind(this); + + return function TestingContext({ + children, + }: { + children: ComponentChildren; + }): VNode { + + async function request<T>( + base: string, + path: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + const _url = new URL(`${base}${path}`); + // Object.entries(options.params ?? {}).forEach(([key, value]) => { + // _url.searchParams.set(key, String(value)); + // }); + + const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE( + { + method: options.method ?? "GET", + url: _url.href, + }, + { + qparam: options.params, + auth: options.token, + request: options.data, + }, + ); + const status = mocked.expectedQuery?.query.code ?? 200; + const requestPayload = mocked.expectedQuery?.params?.request; + const responsePayload = mocked.expectedQuery?.params?.response; + + return { + ok: true, + data: responsePayload as T, + loading: false, + clientError: false, + serverError: false, + info: { + hasToken: !!options.token, + status, + url: _url.href, + payload: options.data, + options: {}, + }, + }; + } + const SC: any = SWRConfig; + + const mockHttpClient = new class implements HttpRequestLibrary { + async fetch(url: string, options?: HttpRequestOptions | undefined): Promise<HttpResponse> { + const _url = new URL(url); + const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE( + { + method: options?.method ?? "GET", + url: _url.href, + }, + { + qparam: _url.searchParams, + auth: options as any, + request: options?.body as any, + }, + ); + const status = mocked.expectedQuery?.query.code ?? 200; + const requestPayload = mocked.expectedQuery?.params?.request; + const responsePayload = mocked.expectedQuery?.params?.response; + + // FIXME: complete this implementation to mock any query + const resp: HttpResponse = { + requestUrl: _url.href, + status: status, + headers: {} as any, + requestMethod: options?.method ?? "GET", + json: async () => responsePayload, + text: async () => responsePayload as any as string, + bytes: async () => responsePayload as ArrayBuffer, + }; + return resp + } + get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { + return this.fetch(url, { + method: "GET", + ...opt, + }); + } + + postJson( + url: string, + body: any, + opt?: HttpRequestOptions, + ): Promise<HttpResponse> { + return this.fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + ...opt, + }); + } + + } + const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient) + const bankIntegration = bankCore.getIntegrationAPI() + const bankRevenue = bankCore.getRevenueAPI("a") + const bankWire = bankCore.getWireGatewayAPI("b") + + return ( + <BackendContextProvider defaultUrl="http://backend"> + <InstanceContextProvider + value={{ + token: undefined, + id: "default", + admin: true, + changeToken: () => null, + }} + > + <ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}> + <SC + value={{ + loadingTimeout: 0, + dedupingInterval: 0, + shouldRetryOnError: false, + errorRetryInterval: 0, + errorRetryCount: 0, + provider: () => new Map(), + }} + > + {children} + </SC> + </ApiContextProvider> + </InstanceContextProvider> + </BackendContextProvider> + ); + }; + } +} diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts new file mode 100644 index 000000000..a7187af27 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts @@ -0,0 +1,254 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as tests from "@gnu-taler/web-util/testing"; +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { useInstanceTransfers, useTransferAPI } from "./transfer.js"; + +describe("transfer api interaction with listing", () => { + it("should evict cache when informing a transfer", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: -20 }, + response: { + transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], + }, + }); + + const moveCursor = (d: string) => { + console.log("new position", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + const query = useInstanceTransfers({}, moveCursor); + const api = useTransferAPI(); + return { query, api }; + }, + {}, + [ + ({ query, api }) => { + expect(query.loading).true; + }, + + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + transfers: [{ wtid: "2" }], + }); + + env.addRequestExpectation(API_INFORM_TRANSFERS, { + request: { + wtid: "3", + credit_amount: "EUR:1", + exchange_url: "exchange.url", + payto_uri: "payto://", + }, + response: { total: "" } as any, + }); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: -20 }, + response: { + transfers: [{ wtid: "3" } as any, { wtid: "2" } as any], + }, + }); + + api.informTransfer({ + wtid: "3", + credit_amount: "EUR:1", + exchange_url: "exchange.url", + payto_uri: "payto://", + }); + }, + ({ query, api }) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + + expect(query.data).deep.equals({ + transfers: [{ wtid: "3" }, { wtid: "2" }], + }); + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); + +describe("transfer listing pagination", () => { + it("should not load more if has reach the end", async () => { + const env = new ApiMockEnvironment(); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: -20, payto_uri: "payto://" }, + response: { + transfers: [{ wtid: "2" }, { wtid: "1" } as any], + }, + }); + + const moveCursor = (d: string) => { + console.log("new position", d); + }; + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor); + }, + {}, + [ + (query) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(query.loading).true; + }, + (query) => { + expect(query.loading).undefined; + expect(query.ok).true; + if (!query.ok) return; + expect(query.data).deep.equals({ + transfers: [{ wtid: "2" }, { wtid: "1" }], + }); + expect(query.isReachingEnd).true; + expect(query.isReachingStart).true; + + //check that this button won't trigger more updates since + //has reach end and start + query.loadMore(); + query.loadMorePrev(); + }, + ], + env.buildTestingContext(), + ); + + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + expect(hookBehavior).deep.eq({ result: "ok" }); + }); + + it("should load more if result brings more that PAGE_SIZE", async () => { + const env = new ApiMockEnvironment(); + + const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ + wtid: String(i), + })); + const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ + wtid: String(i + 20), + })); + const transfersFrom20to0 = [...transfersFrom0to20].reverse(); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: 20, payto_uri: "payto://", offset: "1" }, + response: { + transfers: transfersFrom0to20, + }, + }); + + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: -20, payto_uri: "payto://", offset: "1" }, + response: { + transfers: transfersFrom20to40, + }, + }); + + const moveCursor = (d: string) => { + console.log("new position", d); + }; + + const hookBehavior = await tests.hookBehaveLikeThis( + () => { + return useInstanceTransfers( + { payto_uri: "payto://", position: "1" }, + moveCursor, + ); + }, + {}, + [ + (result) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(result.loading).true; + }, + (result) => { + expect(result.loading).undefined; + expect(result.ok).true; + if (!result.ok) return; + expect(result.data).deep.equals({ + transfers: [...transfersFrom20to0, ...transfersFrom20to40], + }); + expect(result.isReachingEnd).false; + expect(result.isReachingStart).false; + + //query more + env.addRequestExpectation(API_LIST_TRANSFERS, { + qparam: { limit: -40, payto_uri: "payto://", offset: "1" }, + response: { + transfers: [...transfersFrom20to40, { wtid: "41" }], + }, + }); + result.loadMore(); + }, + (result) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(result.loading).true; + }, + (result) => { + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ + result: "ok", + }); + expect(result.loading).undefined; + expect(result.ok).true; + if (!result.ok) return; + expect(result.data).deep.equals({ + transfers: [ + ...transfersFrom20to0, + ...transfersFrom20to40, + { wtid: "41" }, + ], + }); + expect(result.isReachingEnd).true; + expect(result.isReachingStart).false; + }, + ], + env.buildTestingContext(), + ); + + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + expect(hookBehavior).deep.eq({ result: "ok" }); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.ts new file mode 100644 index 000000000..27c3bdc75 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/transfer.ts @@ -0,0 +1,188 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +export function useTransferAPI(): TransferAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const informTransfer = async ( + data: MerchantBackend.Transfers.TransferInformation, + ): Promise<HttpResponseOk<{}>> => { + const res = await request<{}>(`/private/transfers`, { + method: "POST", + data, + }); + + await mutateAll(/.*private\/transfers.*/); + return res; + }; + + return { informTransfer }; +} + +export interface TransferAPI { + informTransfer: ( + data: MerchantBackend.Transfers.TransferInformation, + ) => Promise<HttpResponseOk<{}>>; +} + +export interface InstanceTransferFilter { + payto_uri?: string; + verified?: "yes" | "no"; + position?: string; +} + +export function useInstanceTransfers( + args?: InstanceTransferFilter, + updatePosition?: (id: string) => void, +): HttpResponsePaginated< + MerchantBackend.Transfers.TransferList, + MerchantBackend.ErrorDetail +> { + const { transferFetcher } = useBackendInstanceRequest(); + + const [pageBefore, setPageBefore] = useState(1); + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0; + + /** + * FIXME: this can be cleaned up a little + * + * the logic of double query should be inside the orderFetch so from the hook perspective and cache + * is just one query and one error status + */ + const { + data: beforeData, + error: beforeError, + isValidating: loadingBefore, + } = useSWR< + HttpResponseOk<MerchantBackend.Transfers.TransferList>, + RequestError<MerchantBackend.ErrorDetail> + >( + [ + `/private/transfers`, + args?.payto_uri, + args?.verified, + args?.position, + totalBefore, + ], + transferFetcher, + ); + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.Transfers.TransferList>, + RequestError<MerchantBackend.ErrorDetail> + >( + [ + `/private/transfers`, + args?.payto_uri, + args?.verified, + args?.position, + -totalAfter, + ], + transferFetcher, + ); + + //this will save last result + const [lastBefore, setLastBefore] = useState< + HttpResponse< + MerchantBackend.Transfers.TransferList, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.Transfers.TransferList, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + if (beforeData) setLastBefore(beforeData); + }, [afterData, beforeData]); + + if (beforeError) return beforeError.cause; + if (afterError) return afterError.cause; + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data.transfers.length < totalAfter; + const isReachingStart = + args?.position === undefined || + (beforeData && beforeData.data.transfers.length < totalBefore); + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.transfers.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = `${ + afterData.data.transfers[afterData.data.transfers.length - 1] + .transfer_serial_id + }`; + if (from && updatePosition) updatePosition(from); + } + }, + loadMorePrev: () => { + if (!beforeData || isReachingStart) return; + if (beforeData.data.transfers.length < MAX_RESULT_SIZE) { + setPageBefore(pageBefore + 1); + } else if (beforeData) { + const from = `${ + beforeData.data.transfers[beforeData.data.transfers.length - 1] + .transfer_serial_id + }`; + if (from && updatePosition) updatePosition(from); + } + }, + }; + + const transfers = + !beforeData || !afterData + ? [] + : (beforeData || lastBefore).data.transfers + .slice() + .reverse() + .concat((afterData || lastAfter).data.transfers); + if (loadingAfter || loadingBefore) + return { loading: true, data: { transfers } }; + if (beforeData && afterData) { + return { ok: true, data: { transfers }, ...pagination }; + } + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/urls.ts b/packages/auditor-backoffice-ui/src/hooks/urls.ts new file mode 100644 index 000000000..b6485259f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/urls.ts @@ -0,0 +1,303 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { Query } from "@gnu-taler/web-util/testing"; +import { MerchantBackend } from "../declaration.js"; + +//////////////////// +// ORDER +//////////////////// + +export const API_CREATE_ORDER: Query< + MerchantBackend.Orders.PostOrderRequest, + MerchantBackend.Orders.PostOrderResponse +> = { + method: "POST", + url: "http://backend/instances/default/private/orders", +}; + +export const API_GET_ORDER_BY_ID = ( + id: string, +): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({ + method: "GET", + url: `http://backend/instances/default/private/orders/${id}`, +}); + +export const API_LIST_ORDERS: Query< + unknown, + MerchantBackend.Orders.OrderHistory +> = { + method: "GET", + url: "http://backend/instances/default/private/orders", +}; + +export const API_REFUND_ORDER_BY_ID = ( + id: string, +): Query< + MerchantBackend.Orders.RefundRequest, + MerchantBackend.Orders.MerchantRefundResponse +> => ({ + method: "POST", + url: `http://backend/instances/default/private/orders/${id}/refund`, +}); + +export const API_FORGET_ORDER_BY_ID = ( + id: string, +): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ + method: "PATCH", + url: `http://backend/instances/default/private/orders/${id}/forget`, +}); + +export const API_DELETE_ORDER = ( + id: string, +): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ + method: "DELETE", + url: `http://backend/instances/default/private/orders/${id}`, +}); + +//////////////////// +// TRANSFER +//////////////////// + +export const API_LIST_TRANSFERS: Query< + unknown, + MerchantBackend.Transfers.TransferList +> = { + method: "GET", + url: "http://backend/instances/default/private/transfers", +}; + +export const API_INFORM_TRANSFERS: Query< + MerchantBackend.Transfers.TransferInformation, + {} +> = { + method: "POST", + url: "http://backend/instances/default/private/transfers", +}; + +//////////////////// +// PRODUCT +//////////////////// + +export const API_CREATE_PRODUCT: Query< + MerchantBackend.Products.ProductAddDetail, + unknown +> = { + method: "POST", + url: "http://backend/instances/default/private/products", +}; + +export const API_LIST_PRODUCTS: Query< + unknown, + MerchantBackend.Products.InventorySummaryResponse +> = { + method: "GET", + url: "http://backend/instances/default/private/products", +}; + +export const API_GET_PRODUCT_BY_ID = ( + id: string, +): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ + method: "GET", + url: `http://backend/instances/default/private/products/${id}`, +}); + +export const API_UPDATE_PRODUCT_BY_ID = ( + id: string, +): Query< + MerchantBackend.Products.ProductPatchDetail, + MerchantBackend.Products.InventorySummaryResponse +> => ({ + method: "PATCH", + url: `http://backend/instances/default/private/products/${id}`, +}); + +export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ + method: "DELETE", + url: `http://backend/instances/default/private/products/${id}`, +}); + +//////////////////// +// RESERVES +//////////////////// + +export const API_CREATE_RESERVE: Query< + MerchantBackend.Rewards.ReserveCreateRequest, + MerchantBackend.Rewards.ReserveCreateConfirmation +> = { + method: "POST", + url: "http://backend/instances/default/private/reserves", +}; +export const API_LIST_RESERVES: Query< + unknown, + MerchantBackend.Rewards.RewardReserveStatus +> = { + method: "GET", + url: "http://backend/instances/default/private/reserves", +}; + +export const API_GET_RESERVE_BY_ID = ( + pub: string, +): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({ + method: "GET", + url: `http://backend/instances/default/private/reserves/${pub}`, +}); + +export const API_GET_REWARD_BY_ID = ( + pub: string, +): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({ + method: "GET", + url: `http://backend/instances/default/private/rewards/${pub}`, +}); + +export const API_AUTHORIZE_REWARD_FOR_RESERVE = ( + pub: string, +): Query< + MerchantBackend.Rewards.RewardCreateRequest, + MerchantBackend.Rewards.RewardCreateConfirmation +> => ({ + method: "POST", + url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`, +}); + +export const API_AUTHORIZE_REWARD: Query< + MerchantBackend.Rewards.RewardCreateRequest, + MerchantBackend.Rewards.RewardCreateConfirmation +> = { + method: "POST", + url: `http://backend/instances/default/private/rewards`, +}; + +export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ + method: "DELETE", + url: `http://backend/instances/default/private/reserves/${id}`, +}); + +//////////////////// +// INSTANCE ADMIN +//////////////////// + +export const API_CREATE_INSTANCE: Query< + MerchantBackend.Instances.InstanceConfigurationMessage, + unknown +> = { + method: "POST", + url: "http://backend/management/instances", +}; + +export const API_GET_INSTANCE_BY_ID = ( + id: string, +): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({ + method: "GET", + url: `http://backend/management/instances/${id}`, +}); + +export const API_GET_INSTANCE_KYC_BY_ID = ( + id: string, +): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({ + method: "GET", + url: `http://backend/management/instances/${id}/kyc`, +}); + +export const API_LIST_INSTANCES: Query< + unknown, + MerchantBackend.Instances.InstancesResponse +> = { + method: "GET", + url: "http://backend/management/instances", +}; + +export const API_UPDATE_INSTANCE_BY_ID = ( + id: string, +): Query< + MerchantBackend.Instances.InstanceReconfigurationMessage, + unknown +> => ({ + method: "PATCH", + url: `http://backend/management/instances/${id}`, +}); + +export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( + id: string, +): Query< + MerchantBackend.Instances.InstanceAuthConfigurationMessage, + unknown +> => ({ + method: "POST", + url: `http://backend/management/instances/${id}/auth`, +}); + +export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({ + method: "DELETE", + url: `http://backend/management/instances/${id}`, +}); + +//////////////////// +// AUTH +//////////////////// + +export const API_NEW_LOGIN: Query< + MerchantBackend.Instances.LoginTokenRequest, + unknown +> = ({ + method: "POST", + url: `http://backend/private/token`, +}); + +//////////////////// +// INSTANCE +//////////////////// + +export const API_GET_CURRENT_INSTANCE: Query< + unknown, + MerchantBackend.Instances.QueryInstancesResponse +> = { + method: "GET", + url: `http://backend/instances/default/private/`, +}; + +export const API_GET_CURRENT_INSTANCE_KYC: Query< + unknown, + MerchantBackend.KYC.AccountKycRedirects +> = { + method: "GET", + url: `http://backend/instances/default/private/kyc`, +}; + +export const API_UPDATE_CURRENT_INSTANCE: Query< + MerchantBackend.Instances.InstanceReconfigurationMessage, + unknown +> = { + method: "PATCH", + url: `http://backend/instances/default/private/`, +}; + +export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< + MerchantBackend.Instances.InstanceAuthConfigurationMessage, + unknown +> = { + method: "POST", + url: `http://backend/instances/default/private/auth`, +}; + +export const API_DELETE_CURRENT_INSTANCE: Query<unknown, unknown> = { + method: "DELETE", + url: `http://backend/instances/default/private`, +}; diff --git a/packages/auditor-backoffice-ui/src/hooks/useSettings.ts b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts new file mode 100644 index 000000000..8c1ebd9f6 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts @@ -0,0 +1,73 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { + Codec, + buildCodecForObject, + codecForBoolean, + codecForConstString, + codecForEither, + codecForString, +} from "@gnu-taler/taler-util"; + +export interface Settings { + advanceOrderMode: boolean; + dateFormat: "ymd" | "dmy" | "mdy"; +} + +const defaultSettings: Settings = { + advanceOrderMode: false, + dateFormat: "ymd", +} + +export const codecForSettings = (): Codec<Settings> => + buildCodecForObject<Settings>() + .property("advanceOrderMode", codecForBoolean()) + .property("dateFormat", codecForEither( + codecForConstString("ymd"), + codecForConstString("dmy"), + codecForConstString("mdy"), + )) + .build("Settings"); + +const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings()); + +export function useSettings(): [ + Readonly<Settings>, + (s: Settings) => void, +] { + const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings); + + // const parsed: Settings = value ?? defaultSettings; + // function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { + // const next = { ...parsed, [k]: v } + // update(next); + // } + return [value, update]; +} + +export function dateFormatForSettings(s: Settings): string { + switch (s.dateFormat) { + case "ymd": return "yyyy/MM/dd" + case "dmy": return "dd/MM/yyyy" + case "mdy": return "MM/dd/yyyy" + } +} + +export function datetimeFormatForSettings(s: Settings): string { + return dateFormatForSettings(s) + " HH:mm:ss" +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts new file mode 100644 index 000000000..ad6bf96e2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts @@ -0,0 +1,178 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +const useSWR = _useSWR as unknown as SWRHook; + +export function useWebhookAPI(): WebhookAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + const createWebhook = async ( + data: MerchantBackend.Webhooks.WebhookAddDetails, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/webhooks`, { + method: "POST", + data, + }); + await mutateAll(/.*private\/webhooks.*/); + return res; + }; + + const updateWebhook = async ( + webhookId: string, + data: MerchantBackend.Webhooks.WebhookPatchDetails, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/webhooks/${webhookId}`, { + method: "PATCH", + data, + }); + await mutateAll(/.*private\/webhooks.*/); + return res; + }; + + const deleteWebhook = async ( + webhookId: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/webhooks/${webhookId}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/webhooks.*/); + return res; + }; + + return { createWebhook, updateWebhook, deleteWebhook }; +} + +export interface WebhookAPI { + createWebhook: ( + data: MerchantBackend.Webhooks.WebhookAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateWebhook: ( + id: string, + data: MerchantBackend.Webhooks.WebhookPatchDetails, + ) => Promise<HttpResponseOk<void>>; + deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>; +} + +export interface InstanceWebhookFilter { + //FIXME: add filter to the webhook list + position?: string; +} + +export function useInstanceWebhooks( + args?: InstanceWebhookFilter, + updatePosition?: (id: string) => void, +): HttpResponsePaginated< + MerchantBackend.Webhooks.WebhookSummaryResponse, + MerchantBackend.ErrorDetail +> { + const { webhookFetcher } = useBackendInstanceRequest(); + + const [pageBefore, setPageBefore] = useState(1); + const [pageAfter, setPageAfter] = useState(1); + + const totalAfter = pageAfter * PAGE_SIZE; + const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; + + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher); + + const [lastAfter, setLastAfter] = useState< + HttpResponse< + MerchantBackend.Webhooks.WebhookSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + useEffect(() => { + if (afterData) setLastAfter(afterData); + }, [afterData]); + + if (afterError) return afterError.cause; + + const isReachingEnd = + afterData && afterData.data.webhooks.length < totalAfter; + const isReachingStart = true; + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data.webhooks.length < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1); + } else { + const from = `${ + afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id + }`; + if (from && updatePosition) updatePosition(from); + } + }, + loadMorePrev: () => { + return; + }, + }; + + const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks; + + if (loadingAfter) return { loading: true, data: { webhooks } }; + if (afterData) { + return { ok: true, data: { webhooks }, ...pagination }; + } + return { loading: true }; +} + +export function useWebhookDetails( + webhookId: string, +): HttpResponse< + MerchantBackend.Webhooks.WebhookDetails, + MerchantBackend.ErrorDetail +> { + const { webhookFetcher } = useBackendInstanceRequest(); + + const { data, error, isValidating } = useSWR< + HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>, + RequestError<MerchantBackend.ErrorDetail> + >([`/private/webhooks/${webhookId}`], webhookFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (isValidating) return { loading: true, data: data?.data }; + if (data) return data; + if (error) return error.cause; + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/i18n/de.po b/packages/auditor-backoffice-ui/src/i18n/de.po new file mode 100644 index 000000000..2cf0a7c1c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/de.po @@ -0,0 +1,2742 @@ +# This file is part of TALER +# (C) 2016 GNUnet e.V. +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"POT-Creation-Date: 2016-11-23 00:00+0100\n" +"PO-Revision-Date: 2023-12-04 13:44+0000\n" +"Last-Translator: Stefan Kügel <skuegel@web.de>\n" +"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/" +"merchant-backoffice/de/>\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.2.1\n" + +#: src/components/modal/index.tsx:71 +#, c-format +msgid "Cancel" +msgstr "" + +#: src/components/modal/index.tsx:79 +#, c-format +msgid "%1$s" +msgstr "" + +#: src/components/modal/index.tsx:84 +#, c-format +msgid "Close" +msgstr "" + +#: src/components/modal/index.tsx:124 +#, c-format +msgid "Continue" +msgstr "" + +#: src/components/modal/index.tsx:178 +#, c-format +msgid "Clear" +msgstr "" + +#: src/components/modal/index.tsx:190 +#, c-format +msgid "Confirm" +msgstr "" + +#: src/components/modal/index.tsx:296 +#, c-format +msgid "is not the same as the current access token" +msgstr "" + +#: src/components/modal/index.tsx:299 +#, c-format +msgid "cannot be empty" +msgstr "" + +#: src/components/modal/index.tsx:301 +#, c-format +msgid "cannot be the same as the old token" +msgstr "" + +#: src/components/modal/index.tsx:305 +#, c-format +msgid "is not the same" +msgstr "" + +#: src/components/modal/index.tsx:315 +#, c-format +msgid "You are updating the access token from instance with id %1$s" +msgstr "" + +#: src/components/modal/index.tsx:331 +#, c-format +msgid "Old access token" +msgstr "" + +#: src/components/modal/index.tsx:332 +#, c-format +msgid "access token currently in use" +msgstr "" + +#: src/components/modal/index.tsx:338 +#, c-format +msgid "New access token" +msgstr "" + +#: src/components/modal/index.tsx:339 +#, c-format +msgid "next access token to be used" +msgstr "" + +#: src/components/modal/index.tsx:344 +#, c-format +msgid "Repeat access token" +msgstr "" + +#: src/components/modal/index.tsx:345 +#, c-format +msgid "confirm the same access token" +msgstr "" + +#: src/components/modal/index.tsx:350 +#, c-format +msgid "Clearing the access token will mean public access to the instance" +msgstr "" + +#: src/components/modal/index.tsx:377 +#, c-format +msgid "cannot be the same as the old access token" +msgstr "" + +#: src/components/modal/index.tsx:394 +#, c-format +msgid "You are setting the access token for the new instance" +msgstr "" + +#: src/components/modal/index.tsx:420 +#, c-format +msgid "" +"With external authorization method no check will be done by the merchant " +"backend" +msgstr "" + +#: src/components/modal/index.tsx:436 +#, c-format +msgid "Set external authorization" +msgstr "" + +#: src/components/modal/index.tsx:448 +#, c-format +msgid "Set access token" +msgstr "" + +#: src/components/modal/index.tsx:470 +#, c-format +msgid "Operation in progress..." +msgstr "" + +#: src/components/modal/index.tsx:479 +#, c-format +msgid "The operation will be automatically canceled after %1$s seconds" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:80 +#, c-format +msgid "Instances" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:93 +#, c-format +msgid "Delete" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:99 +#, c-format +msgid "add new instance" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:178 +#, c-format +msgid "ID" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:181 +#, c-format +msgid "Name" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:220 +#, c-format +msgid "Edit" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:237 +#, c-format +msgid "Purge" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:261 +#, c-format +msgid "There is no instances yet, add more pressing the + sign" +msgstr "" + +#: src/paths/admin/list/View.tsx:68 +#, c-format +msgid "Only show active instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:71 +#, c-format +msgid "Active" +msgstr "" + +#: src/paths/admin/list/View.tsx:78 +#, c-format +msgid "Only show deleted instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:81 +#, c-format +msgid "Deleted" +msgstr "" + +#: src/paths/admin/list/View.tsx:88 +#, c-format +msgid "Show all instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:91 +#, c-format +msgid "All" +msgstr "" + +#: src/paths/admin/list/index.tsx:101 +#, c-format +msgid "Instance \"%1$s\" (ID: %2$s) has been deleted" +msgstr "" + +#: src/paths/admin/list/index.tsx:106 +#, c-format +msgid "Failed to delete instance" +msgstr "" + +#: src/paths/admin/list/index.tsx:124 +#, c-format +msgid "Instance '%1$s' (ID: %2$s) has been disabled" +msgstr "" + +#: src/paths/admin/list/index.tsx:129 +#, c-format +msgid "Failed to purge instance" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:41 +#, c-format +msgid "Pending KYC verification" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:66 +#, c-format +msgid "Timed out" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:103 +#, c-format +msgid "Exchange" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:106 +#, c-format +msgid "Target account" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:109 +#, c-format +msgid "KYC URL" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:144 +#, c-format +msgid "Code" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:147 +#, c-format +msgid "Http Status" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:177 +#, c-format +msgid "No pending kyc verification!" +msgstr "" + +#: src/components/form/InputDate.tsx:123 +#, c-format +msgid "change value to unknown date" +msgstr "" + +#: src/components/form/InputDate.tsx:124 +#, c-format +msgid "change value to empty" +msgstr "" + +#: src/components/form/InputDate.tsx:131 +#, c-format +msgid "clear" +msgstr "" + +#: src/components/form/InputDate.tsx:136 +#, c-format +msgid "change value to never" +msgstr "" + +#: src/components/form/InputDate.tsx:141 +#, c-format +msgid "never" +msgstr "" + +#: src/components/form/InputLocation.tsx:29 +#, c-format +msgid "Country" +msgstr "" + +#: src/components/form/InputLocation.tsx:33 +#, c-format +msgid "Address" +msgstr "" + +#: src/components/form/InputLocation.tsx:39 +#, c-format +msgid "Building number" +msgstr "" + +#: src/components/form/InputLocation.tsx:41 +#, c-format +msgid "Building name" +msgstr "" + +#: src/components/form/InputLocation.tsx:42 +#, c-format +msgid "Street" +msgstr "" + +#: src/components/form/InputLocation.tsx:43 +#, c-format +msgid "Post code" +msgstr "" + +#: src/components/form/InputLocation.tsx:44 +#, c-format +msgid "Town location" +msgstr "" + +#: src/components/form/InputLocation.tsx:45 +#, c-format +msgid "Town" +msgstr "" + +#: src/components/form/InputLocation.tsx:46 +#, c-format +msgid "District" +msgstr "" + +#: src/components/form/InputLocation.tsx:49 +#, c-format +msgid "Country subdivision" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:66 +#, c-format +msgid "Product id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:69 +#, c-format +msgid "Description" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:94 +#, c-format +msgid "Product" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:95 +#, c-format +msgid "search products by it's description or id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:151 +#, c-format +msgid "no products found with that description" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:56 +#, c-format +msgid "You must enter a valid product identifier." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:64 +#, c-format +msgid "Quantity must be greater than 0!" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:76 +#, c-format +msgid "" +"This quantity exceeds remaining stock. Currently, only %1$s units remain " +"unreserved in stock." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:109 +#, c-format +msgid "Quantity" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:110 +#, c-format +msgid "how many products will be added" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:117 +#, c-format +msgid "Add from inventory" +msgstr "" + +#: src/components/form/InputImage.tsx:105 +#, c-format +msgid "Image should be smaller than 1 MB" +msgstr "" + +#: src/components/form/InputImage.tsx:110 +#, c-format +msgid "Add" +msgstr "" + +#: src/components/form/InputImage.tsx:115 +#, c-format +msgid "Remove" +msgstr "" + +#: src/components/form/InputTaxes.tsx:113 +#, c-format +msgid "No taxes configured for this product." +msgstr "" + +#: src/components/form/InputTaxes.tsx:119 +#, c-format +msgid "Amount" +msgstr "" + +#: src/components/form/InputTaxes.tsx:120 +#, c-format +msgid "" +"Taxes can be in currencies that differ from the main currency used by the " +"merchant." +msgstr "" + +#: src/components/form/InputTaxes.tsx:122 +#, c-format +msgid "" +"Enter currency and value separated with a colon, e.g. "USD:2.3"." +msgstr "" + +#: src/components/form/InputTaxes.tsx:131 +#, c-format +msgid "Legal name of the tax, e.g. VAT or import duties." +msgstr "" + +#: src/components/form/InputTaxes.tsx:137 +#, c-format +msgid "add tax to the tax list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:72 +#, c-format +msgid "describe and add a product that is not in the inventory list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:75 +#, c-format +msgid "Add custom product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:86 +#, c-format +msgid "Complete information of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:185 +#, c-format +msgid "Image" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:186 +#, c-format +msgid "photo of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:192 +#, c-format +msgid "full product description" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:196 +#, c-format +msgid "Unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:197 +#, c-format +msgid "name of the product unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:201 +#, c-format +msgid "Price" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:202 +#, c-format +msgid "amount in the current currency" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:211 +#, c-format +msgid "Taxes" +msgstr "" + +#: src/components/product/ProductList.tsx:38 +#, c-format +msgid "image" +msgstr "" + +#: src/components/product/ProductList.tsx:41 +#, c-format +msgid "description" +msgstr "" + +#: src/components/product/ProductList.tsx:44 +#, c-format +msgid "quantity" +msgstr "" + +#: src/components/product/ProductList.tsx:47 +#, c-format +msgid "unit price" +msgstr "" + +#: src/components/product/ProductList.tsx:50 +#, c-format +msgid "total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:153 +#, c-format +msgid "required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:157 +#, c-format +msgid "not valid" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:159 +#, c-format +msgid "must be greater than 0" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:164 +#, c-format +msgid "not a valid json" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:170 +#, c-format +msgid "should be in the future" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:173 +#, c-format +msgid "refund deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:179 +#, c-format +msgid "wire transfer deadline cannot be before refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:190 +#, c-format +msgid "wire transfer deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:197 +#, c-format +msgid "should have a refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:202 +#, c-format +msgid "auto refund cannot be after refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:360 +#, c-format +msgid "Manage products in order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:369 +#, c-format +msgid "Manage list of products in the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:391 +#, c-format +msgid "Remove this product from the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:415 +#, c-format +msgid "Total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:417 +#, c-format +msgid "total product price added up" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:430 +#, c-format +msgid "Amount to be paid by the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:436 +#, c-format +msgid "Order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:437 +#, c-format +msgid "final order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:444 +#, c-format +msgid "Summary" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:445 +#, c-format +msgid "Title of the order to be shown to the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:450 +#, c-format +msgid "Shipping and Fulfillment" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:455 +#, c-format +msgid "Delivery date" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:456 +#, c-format +msgid "Deadline for physical delivery assured by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:461 +#, c-format +msgid "Location" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:462 +#, c-format +msgid "address where the products will be delivered" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:469 +#, c-format +msgid "Fulfillment URL" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:470 +#, c-format +msgid "URL to which the user will be redirected after successful payment." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:476 +#, c-format +msgid "Taler payment options" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:477 +#, c-format +msgid "Override default Taler payment settings for this order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:481 +#, c-format +msgid "Payment deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:482 +#, c-format +msgid "" +"Deadline for the customer to pay for the offer before it expires. Inventory " +"products will be reserved until this deadline." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:486 +#, c-format +msgid "Refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:487 +#, c-format +msgid "Time until which the order can be refunded by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:491 +#, c-format +msgid "Wire transfer deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:492 +#, c-format +msgid "Deadline for the exchange to make the wire transfer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:496 +#, c-format +msgid "Auto-refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:497 +#, c-format +msgid "" +"Time until which the wallet will automatically check for refunds without " +"user interaction." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:502 +#, c-format +msgid "Maximum deposit fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:503 +#, c-format +msgid "" +"Maximum deposit fees the merchant is willing to cover for this order. Higher " +"deposit fees must be covered in full by the consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:507 +#, c-format +msgid "Maximum wire fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:508 +#, c-format +msgid "" +"Maximum aggregate wire fees the merchant is willing to cover for this order. " +"Wire fees exceeding this amount are to be covered by the customers." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:512 +#, c-format +msgid "Wire fee amortization" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:513 +#, c-format +msgid "" +"Factor by which wire fees exceeding the above threshold are divided to " +"determine the share of excess wire fees to be paid explicitly by the " +"consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:517 +#, c-format +msgid "Create token" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:518 +#, c-format +msgid "" +"Uncheck this option if the merchant backend generated an order ID with " +"enough entropy to prevent adversarial claims." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:522 +#, c-format +msgid "Minimum age required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:523 +#, c-format +msgid "" +"Any value greater than 0 will limit the coins able be used to pay this " +"contract. If empty the age restriction will be defined by the products" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:526 +#, c-format +msgid "Min age defined by the producs is %1$s" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:534 +#, c-format +msgid "Additional information" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:535 +#, c-format +msgid "Custom information to be included in the contract for this order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:541 +#, c-format +msgid "You must enter a value in JavaScript Object Notation (JSON)." +msgstr "" + +#: src/components/picker/DurationPicker.tsx:55 +#, c-format +msgid "days" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:65 +#, c-format +msgid "hours" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:76 +#, c-format +msgid "minutes" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:87 +#, c-format +msgid "seconds" +msgstr "" + +#: src/components/form/InputDuration.tsx:53 +#, c-format +msgid "forever" +msgstr "" + +#: src/components/form/InputDuration.tsx:62 +#, c-format +msgid "%1$sM" +msgstr "" + +#: src/components/form/InputDuration.tsx:64 +#, c-format +msgid "%1$sY" +msgstr "" + +#: src/components/form/InputDuration.tsx:66 +#, c-format +msgid "%1$sd" +msgstr "" + +#: src/components/form/InputDuration.tsx:68 +#, c-format +msgid "%1$sh" +msgstr "" + +#: src/components/form/InputDuration.tsx:70 +#, c-format +msgid "%1$smin" +msgstr "" + +#: src/components/form/InputDuration.tsx:72 +#, c-format +msgid "%1$ssec" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:75 +#, c-format +msgid "Orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:81 +#, c-format +msgid "create order" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:147 +#, c-format +msgid "load newer orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:154 +#, c-format +msgid "Date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:200 +#, c-format +msgid "Refund" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:209 +#, c-format +msgid "copy url" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:225 +#, c-format +msgid "load older orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:242 +#, c-format +msgid "No orders have been found matching your query!" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:288 +#, c-format +msgid "duplicated" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:299 +#, c-format +msgid "invalid format" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:301 +#, c-format +msgid "this value exceed the refundable amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:346 +#, c-format +msgid "date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:349 +#, c-format +msgid "amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:352 +#, c-format +msgid "reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:389 +#, c-format +msgid "amount to be refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:391 +#, c-format +msgid "Max refundable:" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:396 +#, c-format +msgid "Reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:397 +#, c-format +msgid "Choose one..." +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:399 +#, c-format +msgid "requested by the customer" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:400 +#, c-format +msgid "other" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:403 +#, c-format +msgid "why this order is being refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:409 +#, c-format +msgid "more information to give context" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:62 +#, c-format +msgid "Contract Terms" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:68 +#, c-format +msgid "human-readable description of the whole purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:74 +#, c-format +msgid "total price for the transaction" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:81 +#, c-format +msgid "URL for this purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:87 +#, c-format +msgid "Max fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:88 +#, c-format +msgid "maximum total deposit fee accepted by the merchant for this contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:93 +#, c-format +msgid "Max wire fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:94 +#, c-format +msgid "maximum wire fee accepted by the merchant" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:100 +#, c-format +msgid "" +"over how many customer transactions does the merchant expect to amortize " +"wire fees on average" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:105 +#, c-format +msgid "Created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:106 +#, c-format +msgid "time when this contract was generated" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:112 +#, c-format +msgid "after this deadline has passed no refunds will be accepted" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:118 +#, c-format +msgid "" +"after this deadline, the merchant won't accept payments for the contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:124 +#, c-format +msgid "transfer deadline for the exchange" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:130 +#, c-format +msgid "time indicating when the order should be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:136 +#, c-format +msgid "where the order will be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:144 +#, c-format +msgid "Auto-refund delay" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:145 +#, c-format +msgid "" +"how long the wallet should try to get an automatic refund for the purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:150 +#, c-format +msgid "Extra info" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:151 +#, c-format +msgid "extra data that is only interpreted by the merchant frontend" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:219 +#, c-format +msgid "Order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:221 +#, c-format +msgid "claimed" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:247 +#, c-format +msgid "claimed at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:265 +#, c-format +msgid "Timeline" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:271 +#, c-format +msgid "Payment details" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:291 +#, c-format +msgid "Order status" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:301 +#, c-format +msgid "Product list" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:451 +#, c-format +msgid "paid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:455 +#, c-format +msgid "wired" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:460 +#, c-format +msgid "refunded" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:480 +#, c-format +msgid "refund order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:481 +#, c-format +msgid "not refundable" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:489 +#, c-format +msgid "refund" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:553 +#, c-format +msgid "Refunded amount" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:560 +#, c-format +msgid "Refund taken" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:570 +#, c-format +msgid "Status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:583 +#, c-format +msgid "Refund URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:636 +#, c-format +msgid "unpaid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:654 +#, c-format +msgid "pay at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:666 +#, c-format +msgid "created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:707 +#, c-format +msgid "Order status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:711 +#, c-format +msgid "Payment URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:740 +#, c-format +msgid "" +"Unknown order status. This is an error, please contact the administrator." +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:767 +#, c-format +msgid "Back" +msgstr "Zurück" + +#: src/paths/instance/orders/details/index.tsx:79 +#, c-format +msgid "refund created successfully" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:85 +#, c-format +msgid "could not create the refund" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:78 +#, c-format +msgid "select date to show nearby orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:94 +#, c-format +msgid "order id" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:100 +#, c-format +msgid "jump to order with the given order ID" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:122 +#, c-format +msgid "remove all filters" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:132 +#, c-format +msgid "only show paid orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:135 +#, c-format +msgid "Paid" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:142 +#, c-format +msgid "only show orders with refunds" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:145 +#, c-format +msgid "Refunded" +msgstr "Rückerstattet" + +#: src/paths/instance/orders/list/ListPage.tsx:152 +#, c-format +msgid "" +"only show orders where customers paid, but wire payments from payment " +"provider are still pending" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:155 +#, c-format +msgid "Not wired" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:170 +#, c-format +msgid "clear date filter" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:184 +#, c-format +msgid "date (YYYY/MM/DD)" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:103 +#, c-format +msgid "Enter an order id" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:111 +#, c-format +msgid "order not found" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:178 +#, c-format +msgid "could not get the order to refund" +msgstr "" + +#: src/components/exception/AsyncButton.tsx:43 +#, c-format +msgid "Loading..." +msgstr "" + +#: src/components/form/InputStock.tsx:99 +#, c-format +msgid "" +"click here to configure the stock of the product, leave it as is and the " +"backend will not control stock" +msgstr "" + +#: src/components/form/InputStock.tsx:109 +#, c-format +msgid "Manage stock" +msgstr "" + +#: src/components/form/InputStock.tsx:115 +#, c-format +msgid "this product has been configured without stock control" +msgstr "" + +#: src/components/form/InputStock.tsx:119 +#, c-format +msgid "Infinite" +msgstr "" + +#: src/components/form/InputStock.tsx:136 +#, c-format +msgid "lost cannot be greater than current and incoming (max %1$s)" +msgstr "" + +#: src/components/form/InputStock.tsx:176 +#, c-format +msgid "Incoming" +msgstr "" + +#: src/components/form/InputStock.tsx:177 +#, c-format +msgid "Lost" +msgstr "" + +#: src/components/form/InputStock.tsx:192 +#, c-format +msgid "Current" +msgstr "" + +#: src/components/form/InputStock.tsx:196 +#, c-format +msgid "remove stock control for this product" +msgstr "" + +#: src/components/form/InputStock.tsx:202 +#, c-format +msgid "without stock" +msgstr "" + +#: src/components/form/InputStock.tsx:211 +#, c-format +msgid "Next restock" +msgstr "" + +#: src/components/form/InputStock.tsx:217 +#, c-format +msgid "Delivery address" +msgstr "" + +#: src/components/product/ProductForm.tsx:133 +#, c-format +msgid "product identification to use in URLs (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:139 +#, c-format +msgid "illustration of the product for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:145 +#, c-format +msgid "product description for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:149 +#, c-format +msgid "Age restricted" +msgstr "" + +#: src/components/product/ProductForm.tsx:150 +#, c-format +msgid "is this product restricted for customer below certain age?" +msgstr "" + +#: src/components/product/ProductForm.tsx:155 +#, c-format +msgid "" +"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 " +"items, 5 meters) for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:160 +#, c-format +msgid "" +"sale price for customers, including taxes, for above units of the product" +msgstr "" + +#: src/components/product/ProductForm.tsx:164 +#, c-format +msgid "Stock" +msgstr "" + +#: src/components/product/ProductForm.tsx:166 +#, c-format +msgid "" +"product inventory for products with finite supply (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:171 +#, c-format +msgid "taxes included in the product price, exposed to customers" +msgstr "" + +#: src/paths/instance/products/create/CreatePage.tsx:66 +#, c-format +msgid "Need to complete marked fields" +msgstr "" + +#: src/paths/instance/products/create/index.tsx:51 +#, c-format +msgid "could not create product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:68 +#, c-format +msgid "Products" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:73 +#, c-format +msgid "add product to inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:137 +#, c-format +msgid "Sell" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:143 +#, c-format +msgid "Profit" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:149 +#, c-format +msgid "Sold" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:210 +#, c-format +msgid "free" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:248 +#, c-format +msgid "go to product update page" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:255 +#, c-format +msgid "Update" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:260 +#, c-format +msgid "remove this product from the database" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:331 +#, c-format +msgid "update the product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:341 +#, c-format +msgid "update product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:399 +#, c-format +msgid "add more elements to the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:404 +#, c-format +msgid "report elements lost in the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:409 +#, c-format +msgid "new price for the product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:421 +#, c-format +msgid "the are value with errors" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:422 +#, c-format +msgid "update product with new stock and price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:463 +#, c-format +msgid "There is no products yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:86 +#, c-format +msgid "product updated successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:92 +#, c-format +msgid "could not update the product" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:103 +#, c-format +msgid "product delete successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:109 +#, c-format +msgid "could not delete the product" +msgstr "" + +#: src/paths/instance/products/update/UpdatePage.tsx:56 +#, c-format +msgid "Product id:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95 +#, c-format +msgid "" +"To complete the setup of the reserve, you must now initiate a wire transfer " +"using the given wire transfer subject and crediting the specified amount to " +"the indicated account of the exchange." +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102 +#, c-format +msgid "If your system supports RFC 8905, you can do this by opening this URI:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:83 +#, c-format +msgid "it should be greater than 0" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:88 +#, c-format +msgid "must be a valid URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:107 +#, c-format +msgid "Initial balance" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:108 +#, c-format +msgid "balance prior to deposit" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:112 +#, c-format +msgid "Exchange URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:113 +#, c-format +msgid "URL of exchange" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:148 +#, c-format +msgid "Next" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:186 +#, c-format +msgid "Wire method" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:187 +#, c-format +msgid "method to use for wire transfer" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:189 +#, c-format +msgid "Select one wire method" +msgstr "" + +#: src/paths/instance/reserves/create/index.tsx:62 +#, c-format +msgid "could not create reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:77 +#, c-format +msgid "Valid until" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:82 +#, c-format +msgid "Created balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:99 +#, c-format +msgid "Exchange balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:104 +#, c-format +msgid "Picked up" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:109 +#, c-format +msgid "Committed" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:116 +#, c-format +msgid "Account address" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:119 +#, c-format +msgid "Subject" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:130 +#, c-format +msgid "Tips" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:193 +#, c-format +msgid "No tips has been authorized from this reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:213 +#, c-format +msgid "Authorized" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:222 +#, c-format +msgid "Expiration" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108 +#, c-format +msgid "amount of tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112 +#, c-format +msgid "Justification" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114 +#, c-format +msgid "reason for the tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118 +#, c-format +msgid "URL after tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119 +#, c-format +msgid "URL to visit after tip payment" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:65 +#, c-format +msgid "Reserves not yet funded" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:89 +#, c-format +msgid "Reserves ready" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:95 +#, c-format +msgid "add new reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:143 +#, c-format +msgid "Expires at" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:146 +#, c-format +msgid "Initial" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:202 +#, c-format +msgid "delete selected reserve from the database" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:210 +#, c-format +msgid "authorize new tip from selected reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:237 +#, c-format +msgid "" +"There is no ready reserves yet, add more pressing the + sign or fund them" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:264 +#, c-format +msgid "Expected Balance" +msgstr "" + +#: src/paths/instance/reserves/list/index.tsx:110 +#, c-format +msgid "could not create the tip" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:77 +#, c-format +msgid "should not be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:93 +#, c-format +msgid "should be greater that 0" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:96 +#, c-format +msgid "can't be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:100 +#, c-format +msgid "to short" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:108 +#, c-format +msgid "just letters and numbers from 2 to 7" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:110 +#, c-format +msgid "size of the key should be 32" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:137 +#, c-format +msgid "Identifier" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:138 +#, c-format +msgid "Name of the template in URLs." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:144 +#, c-format +msgid "Describe what this template stands for" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:149 +#, c-format +msgid "Fixed summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:150 +#, c-format +msgid "If specified, this template will create order with the same summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:154 +#, c-format +msgid "Fixed price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:155 +#, c-format +msgid "If specified, this template will create order with the same price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:159 +#, c-format +msgid "Minimum age" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:161 +#, c-format +msgid "Is this contract restricted to some age?" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:165 +#, c-format +msgid "Payment timeout" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:167 +#, c-format +msgid "" +"How much time has the customer to complete the payment once the order was " +"created." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:171 +#, c-format +msgid "Verification algorithm" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:172 +#, c-format +msgid "Algorithm to use to verify transaction in offline mode" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:180 +#, c-format +msgid "Point-of-sale key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:182 +#, c-format +msgid "Useful to validate the purchase" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:196 +#, c-format +msgid "generate random secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:203 +#, c-format +msgid "random" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:208 +#, c-format +msgid "show secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:209 +#, c-format +msgid "hide secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:216 +#, c-format +msgid "hide" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:218 +#, c-format +msgid "show" +msgstr "" + +#: src/paths/instance/templates/create/index.tsx:52 +#, c-format +msgid "could not inform template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:54 +#, c-format +msgid "Amount is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:58 +#, c-format +msgid "Order summary is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:86 +#, c-format +msgid "New order for template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:108 +#, c-format +msgid "Amount of the order" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:113 +#, c-format +msgid "Order summary" +msgstr "" + +#: src/paths/instance/templates/use/index.tsx:92 +#, c-format +msgid "could not create order from template" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:131 +#, c-format +msgid "" +"Here you can specify a default value for fields that are not fixed. Default " +"values can be edited by the customer before the payment." +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:148 +#, c-format +msgid "Fixed amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:149 +#, c-format +msgid "Default amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:161 +#, c-format +msgid "Default summary" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:177 +#, c-format +msgid "Print" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:184 +#, c-format +msgid "Setup TOTP" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:65 +#, c-format +msgid "Templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:70 +#, c-format +msgid "add new templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:142 +#, c-format +msgid "load more templates before the first one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:146 +#, c-format +msgid "load newer templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:181 +#, c-format +msgid "delete selected templates from the database" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:188 +#, c-format +msgid "use template to create new order" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:195 +#, c-format +msgid "create qr code for the template" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:210 +#, c-format +msgid "load more templates after the last one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:214 +#, c-format +msgid "load older templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:231 +#, c-format +msgid "There is no templates yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:104 +#, c-format +msgid "template delete successfully" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:110 +#, c-format +msgid "could not delete the template" +msgstr "" + +#: src/paths/instance/templates/update/index.tsx:90 +#, c-format +msgid "could not update template" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:57 +#, c-format +msgid "should be one of '%1$s'" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:85 +#, c-format +msgid "Webhook ID to use" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:89 +#, c-format +msgid "Event" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:90 +#, c-format +msgid "The event of the webhook: why the webhook is used" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:94 +#, c-format +msgid "Method" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:95 +#, c-format +msgid "Method used by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:99 +#, c-format +msgid "URL" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:100 +#, c-format +msgid "URL of the webhook where the customer will be redirected" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:104 +#, c-format +msgid "Header" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:106 +#, c-format +msgid "Header template of the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:111 +#, c-format +msgid "Body" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:112 +#, c-format +msgid "Body template by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:61 +#, c-format +msgid "Webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:66 +#, c-format +msgid "add new webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:137 +#, c-format +msgid "load more webhooks before the first one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:141 +#, c-format +msgid "load newer webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:151 +#, c-format +msgid "Event type" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:176 +#, c-format +msgid "delete selected webhook from the database" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:198 +#, c-format +msgid "load more webhooks after the last one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:202 +#, c-format +msgid "load older webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:219 +#, c-format +msgid "There is no webhooks yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:94 +#, c-format +msgid "webhook delete successfully" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:100 +#, c-format +msgid "could not delete the webhook" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:63 +#, c-format +msgid "check the id, does not look valid" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:65 +#, c-format +msgid "should have 52 characters, current %1$s" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:72 +#, c-format +msgid "URL doesn't have the right format" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:98 +#, c-format +msgid "Credited bank account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:100 +#, c-format +msgid "Select one account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:101 +#, c-format +msgid "Bank account of the merchant where the payment was received" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:105 +#, c-format +msgid "Wire transfer ID" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:107 +#, c-format +msgid "" +"unique identifier of the wire transfer used by the exchange, must be 52 " +"characters long" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:112 +#, c-format +msgid "" +"Base URL of the exchange that made the transfer, should have been in the " +"wire transfer subject" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:117 +#, c-format +msgid "Amount credited" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:118 +#, c-format +msgid "Actual amount that was wired to the merchant's bank account" +msgstr "" + +#: src/paths/instance/transfers/create/index.tsx:58 +#, c-format +msgid "could not inform transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:61 +#, c-format +msgid "Transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:66 +#, c-format +msgid "add new transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:129 +#, c-format +msgid "load more transfers before the first one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:133 +#, c-format +msgid "load newer transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:143 +#, c-format +msgid "Credit" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:152 +#, c-format +msgid "Confirmed" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:155 +#, c-format +msgid "Verified" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:158 +#, c-format +msgid "Executed at" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "yes" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "no" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:181 +#, c-format +msgid "unknown" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:187 +#, c-format +msgid "delete selected transfer from the database" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:202 +#, c-format +msgid "load more transfer after the last one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:206 +#, c-format +msgid "load older transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:223 +#, c-format +msgid "There is no transfer yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:79 +#, c-format +msgid "filter by account address" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:100 +#, c-format +msgid "only show wire transfers confirmed by the merchant" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:110 +#, c-format +msgid "only show wire transfers claimed by the exchange" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:113 +#, c-format +msgid "Unverified" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:69 +#, c-format +msgid "is not valid" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:94 +#, c-format +msgid "is not a number" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:96 +#, c-format +msgid "must be 1 or greater" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:107 +#, c-format +msgid "max 7 lines" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:178 +#, c-format +msgid "change authorization configuration" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:217 +#, c-format +msgid "Need to complete marked fields and choose authorization method" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:82 +#, c-format +msgid "This is not a valid bitcoin address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:95 +#, c-format +msgid "This is not a valid Ethereum address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:118 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:120 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:128 +#, c-format +msgid "IBAN country code not found" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:153 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:248 +#, c-format +msgid "Target type" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:249 +#, c-format +msgid "Method to use for wire transfer" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:258 +#, c-format +msgid "Routing" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:259 +#, c-format +msgid "Routing number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:263 +#, c-format +msgid "Account" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:264 +#, c-format +msgid "Account number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:273 +#, c-format +msgid "Business Identifier Code." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:282 +#, c-format +msgid "Bank Account Number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:292 +#, c-format +msgid "Unified Payment Interface." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:301 +#, c-format +msgid "Bitcoin protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:310 +#, c-format +msgid "Ethereum protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:319 +#, c-format +msgid "Interledger protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:328 +#, c-format +msgid "Host" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:329 +#, c-format +msgid "Bank host." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:334 +#, c-format +msgid "Bank account." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:343 +#, c-format +msgid "Bank account owner's name." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:370 +#, c-format +msgid "No accounts yet." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:52 +#, c-format +msgid "" +"Name of the instance in URLs. The 'default' instance is special in that it " +"is used to administer other instances." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:58 +#, c-format +msgid "Business name" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:59 +#, c-format +msgid "Legal name of the business represented by this instance." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:64 +#, c-format +msgid "Email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:65 +#, c-format +msgid "Contact email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:70 +#, c-format +msgid "Website URL" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:71 +#, c-format +msgid "URL." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:76 +#, c-format +msgid "Logo" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:77 +#, c-format +msgid "Logo image." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:82 +#, c-format +msgid "Bank account" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:83 +#, c-format +msgid "URI specifying bank account for crediting revenue." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:88 +#, c-format +msgid "Default max deposit fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:89 +#, c-format +msgid "" +"Maximum deposit fees this merchant is willing to pay per order by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:94 +#, c-format +msgid "Default max wire fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:95 +#, c-format +msgid "" +"Maximum wire fees this merchant is willing to pay per wire transfer by " +"default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:100 +#, c-format +msgid "Default wire fee amortization" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:101 +#, c-format +msgid "" +"Number of orders excess wire transfer fees will be divided by to compute per " +"order surcharge." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:107 +#, c-format +msgid "Physical location of the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:114 +#, c-format +msgid "Jurisdiction" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:115 +#, c-format +msgid "Jurisdiction for legal disputes with the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:122 +#, c-format +msgid "Default payment delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:124 +#, c-format +msgid "" +"Time customers have to pay an order before the offer expires by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:129 +#, c-format +msgid "Default wire transfer delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:130 +#, c-format +msgid "" +"Maximum time an exchange is allowed to delay wiring funds to the merchant, " +"enabling it to aggregate smaller payments into larger wire transfers and " +"reducing wire fees." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:164 +#, c-format +msgid "Instance id" +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:173 +#, c-format +msgid "Change the authorization method use for this instance." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:182 +#, c-format +msgid "Manage access token" +msgstr "" + +#: src/paths/instance/update/index.tsx:112 +#, c-format +msgid "Failed to create instance" +msgstr "" + +#: src/components/exception/login.tsx:74 +#, c-format +msgid "Login required" +msgstr "" + +#: src/components/exception/login.tsx:80 +#, c-format +msgid "Please enter your access token." +msgstr "" + +#: src/components/exception/login.tsx:108 +#, c-format +msgid "Access Token" +msgstr "" + +#: src/InstanceRoutes.tsx:171 +#, c-format +msgid "The request to the backend take too long and was cancelled" +msgstr "" + +#: src/InstanceRoutes.tsx:172 +#, c-format +msgid "Diagnostic from %1$s is \"%2$s\"" +msgstr "" + +#: src/InstanceRoutes.tsx:178 +#, c-format +msgid "The backend reported a problem: HTTP status #%1$s" +msgstr "" + +#: src/InstanceRoutes.tsx:179 +#, c-format +msgid "Diagnostic from %1$s is '%2$s'" +msgstr "" + +#: src/InstanceRoutes.tsx:196 +#, c-format +msgid "Access denied" +msgstr "" + +#: src/InstanceRoutes.tsx:197 +#, c-format +msgid "The access token provided is invalid." +msgstr "" + +#: src/InstanceRoutes.tsx:212 +#, c-format +msgid "No 'default' instance configured yet." +msgstr "" + +#: src/InstanceRoutes.tsx:213 +#, c-format +msgid "Create a 'default' instance to begin using the merchant backoffice." +msgstr "" + +#: src/InstanceRoutes.tsx:630 +#, c-format +msgid "The access token provided is invalid" +msgstr "" + +#: src/InstanceRoutes.tsx:664 +#, c-format +msgid "Hide for today" +msgstr "" + +#: src/components/menu/SideBar.tsx:82 +#, c-format +msgid "Instance" +msgstr "" + +#: src/components/menu/SideBar.tsx:91 +#, c-format +msgid "Settings" +msgstr "" + +#: src/components/menu/SideBar.tsx:167 +#, c-format +msgid "Connection" +msgstr "" + +#: src/components/menu/SideBar.tsx:209 +#, c-format +msgid "New" +msgstr "" + +#: src/components/menu/SideBar.tsx:219 +#, c-format +msgid "List" +msgstr "" + +#: src/components/menu/SideBar.tsx:234 +#, c-format +msgid "Log out" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:71 +#, c-format +msgid "Check your token is valid" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:90 +#, c-format +msgid "Couldn't access the server." +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:91 +#, c-format +msgid "Could not infer instance id from url %1$s" +msgstr "" + +#: src/Application.tsx:104 +#, c-format +msgid "Server not found" +msgstr "" + +#: src/Application.tsx:118 +#, c-format +msgid "Server response with an error code" +msgstr "" + +#: src/Application.tsx:120 +#, c-format +msgid "Got message %1$s from %2$s" +msgstr "" + +#: src/Application.tsx:131 +#, c-format +msgid "Response from server is unreadable, http status: %1$s" +msgstr "" + +#: src/Application.tsx:144 +#, c-format +msgid "Unexpected Error" +msgstr "" + +#: src/components/form/InputArray.tsx:101 +#, c-format +msgid "The value %1$s is invalid for a payment url" +msgstr "" + +#: src/components/form/InputArray.tsx:110 +#, c-format +msgid "add element to the list" +msgstr "" + +#: src/components/form/InputArray.tsx:112 +#, c-format +msgid "add" +msgstr "" + +#: src/components/form/InputSecured.tsx:37 +#, c-format +msgid "Deleting" +msgstr "" + +#: src/components/form/InputSecured.tsx:41 +#, c-format +msgid "Changing" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87 +#, c-format +msgid "Order ID" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101 +#, c-format +msgid "Payment URL" +msgstr "" diff --git a/packages/auditor-backoffice-ui/src/i18n/en.po b/packages/auditor-backoffice-ui/src/i18n/en.po new file mode 100644 index 000000000..d8d0bae29 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/en.po @@ -0,0 +1,2741 @@ +# This file is part of TALER +# (C) 2016 GNUnet e.V. +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"POT-Creation-Date: 2016-11-23 00:00+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/components/modal/index.tsx:71 +#, c-format +msgid "Cancel" +msgstr "" + +#: src/components/modal/index.tsx:79 +#, c-format +msgid "%1$s" +msgstr "" + +#: src/components/modal/index.tsx:84 +#, c-format +msgid "Close" +msgstr "" + +#: src/components/modal/index.tsx:124 +#, c-format +msgid "Continue" +msgstr "" + +#: src/components/modal/index.tsx:178 +#, c-format +msgid "Clear" +msgstr "" + +#: src/components/modal/index.tsx:190 +#, c-format +msgid "Confirm" +msgstr "" + +#: src/components/modal/index.tsx:296 +#, c-format +msgid "is not the same as the current access token" +msgstr "" + +#: src/components/modal/index.tsx:299 +#, c-format +msgid "cannot be empty" +msgstr "" + +#: src/components/modal/index.tsx:301 +#, c-format +msgid "cannot be the same as the old token" +msgstr "" + +#: src/components/modal/index.tsx:305 +#, c-format +msgid "is not the same" +msgstr "" + +#: src/components/modal/index.tsx:315 +#, c-format +msgid "You are updating the access token from instance with id %1$s" +msgstr "" + +#: src/components/modal/index.tsx:331 +#, c-format +msgid "Old access token" +msgstr "" + +#: src/components/modal/index.tsx:332 +#, c-format +msgid "access token currently in use" +msgstr "" + +#: src/components/modal/index.tsx:338 +#, c-format +msgid "New access token" +msgstr "" + +#: src/components/modal/index.tsx:339 +#, c-format +msgid "next access token to be used" +msgstr "" + +#: src/components/modal/index.tsx:344 +#, c-format +msgid "Repeat access token" +msgstr "" + +#: src/components/modal/index.tsx:345 +#, c-format +msgid "confirm the same access token" +msgstr "" + +#: src/components/modal/index.tsx:350 +#, c-format +msgid "Clearing the access token will mean public access to the instance" +msgstr "" + +#: src/components/modal/index.tsx:377 +#, c-format +msgid "cannot be the same as the old access token" +msgstr "" + +#: src/components/modal/index.tsx:394 +#, c-format +msgid "You are setting the access token for the new instance" +msgstr "" + +#: src/components/modal/index.tsx:420 +#, c-format +msgid "" +"With external authorization method no check will be done by the merchant " +"backend" +msgstr "" + +#: src/components/modal/index.tsx:436 +#, c-format +msgid "Set external authorization" +msgstr "" + +#: src/components/modal/index.tsx:448 +#, c-format +msgid "Set access token" +msgstr "" + +#: src/components/modal/index.tsx:470 +#, c-format +msgid "Operation in progress..." +msgstr "" + +#: src/components/modal/index.tsx:479 +#, c-format +msgid "The operation will be automatically canceled after %1$s seconds" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:80 +#, c-format +msgid "Instances" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:93 +#, c-format +msgid "Delete" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:99 +#, c-format +msgid "add new instance" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:178 +#, c-format +msgid "ID" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:181 +#, c-format +msgid "Name" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:220 +#, c-format +msgid "Edit" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:237 +#, c-format +msgid "Purge" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:261 +#, c-format +msgid "There is no instances yet, add more pressing the + sign" +msgstr "" + +#: src/paths/admin/list/View.tsx:68 +#, c-format +msgid "Only show active instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:71 +#, c-format +msgid "Active" +msgstr "" + +#: src/paths/admin/list/View.tsx:78 +#, c-format +msgid "Only show deleted instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:81 +#, c-format +msgid "Deleted" +msgstr "" + +#: src/paths/admin/list/View.tsx:88 +#, c-format +msgid "Show all instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:91 +#, c-format +msgid "All" +msgstr "" + +#: src/paths/admin/list/index.tsx:101 +#, c-format +msgid "Instance \"%1$s\" (ID: %2$s) has been deleted" +msgstr "" + +#: src/paths/admin/list/index.tsx:106 +#, c-format +msgid "Failed to delete instance" +msgstr "" + +#: src/paths/admin/list/index.tsx:124 +#, c-format +msgid "Instance '%1$s' (ID: %2$s) has been disabled" +msgstr "" + +#: src/paths/admin/list/index.tsx:129 +#, c-format +msgid "Failed to purge instance" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:41 +#, c-format +msgid "Pending KYC verification" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:66 +#, c-format +msgid "Timed out" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:103 +#, c-format +msgid "Exchange" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:106 +#, c-format +msgid "Target account" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:109 +#, c-format +msgid "KYC URL" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:144 +#, c-format +msgid "Code" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:147 +#, c-format +msgid "Http Status" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:177 +#, c-format +msgid "No pending kyc verification!" +msgstr "" + +#: src/components/form/InputDate.tsx:123 +#, c-format +msgid "change value to unknown date" +msgstr "" + +#: src/components/form/InputDate.tsx:124 +#, c-format +msgid "change value to empty" +msgstr "" + +#: src/components/form/InputDate.tsx:131 +#, c-format +msgid "clear" +msgstr "" + +#: src/components/form/InputDate.tsx:136 +#, c-format +msgid "change value to never" +msgstr "" + +#: src/components/form/InputDate.tsx:141 +#, c-format +msgid "never" +msgstr "" + +#: src/components/form/InputLocation.tsx:29 +#, c-format +msgid "Country" +msgstr "" + +#: src/components/form/InputLocation.tsx:33 +#, c-format +msgid "Address" +msgstr "" + +#: src/components/form/InputLocation.tsx:39 +#, c-format +msgid "Building number" +msgstr "" + +#: src/components/form/InputLocation.tsx:41 +#, c-format +msgid "Building name" +msgstr "" + +#: src/components/form/InputLocation.tsx:42 +#, c-format +msgid "Street" +msgstr "" + +#: src/components/form/InputLocation.tsx:43 +#, c-format +msgid "Post code" +msgstr "" + +#: src/components/form/InputLocation.tsx:44 +#, c-format +msgid "Town location" +msgstr "" + +#: src/components/form/InputLocation.tsx:45 +#, c-format +msgid "Town" +msgstr "" + +#: src/components/form/InputLocation.tsx:46 +#, c-format +msgid "District" +msgstr "" + +#: src/components/form/InputLocation.tsx:49 +#, c-format +msgid "Country subdivision" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:66 +#, c-format +msgid "Product id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:69 +#, c-format +msgid "Description" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:94 +#, c-format +msgid "Product" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:95 +#, c-format +msgid "search products by it's description or id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:151 +#, c-format +msgid "no products found with that description" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:56 +#, c-format +msgid "You must enter a valid product identifier." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:64 +#, c-format +msgid "Quantity must be greater than 0!" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:76 +#, c-format +msgid "" +"This quantity exceeds remaining stock. Currently, only %1$s units remain " +"unreserved in stock." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:109 +#, c-format +msgid "Quantity" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:110 +#, c-format +msgid "how many products will be added" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:117 +#, c-format +msgid "Add from inventory" +msgstr "" + +#: src/components/form/InputImage.tsx:105 +#, c-format +msgid "Image should be smaller than 1 MB" +msgstr "" + +#: src/components/form/InputImage.tsx:110 +#, c-format +msgid "Add" +msgstr "" + +#: src/components/form/InputImage.tsx:115 +#, c-format +msgid "Remove" +msgstr "" + +#: src/components/form/InputTaxes.tsx:113 +#, c-format +msgid "No taxes configured for this product." +msgstr "" + +#: src/components/form/InputTaxes.tsx:119 +#, c-format +msgid "Amount" +msgstr "" + +#: src/components/form/InputTaxes.tsx:120 +#, c-format +msgid "" +"Taxes can be in currencies that differ from the main currency used by the " +"merchant." +msgstr "" + +#: src/components/form/InputTaxes.tsx:122 +#, c-format +msgid "" +"Enter currency and value separated with a colon, e.g. "USD:2.3"." +msgstr "" + +#: src/components/form/InputTaxes.tsx:131 +#, c-format +msgid "Legal name of the tax, e.g. VAT or import duties." +msgstr "" + +#: src/components/form/InputTaxes.tsx:137 +#, c-format +msgid "add tax to the tax list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:72 +#, c-format +msgid "describe and add a product that is not in the inventory list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:75 +#, c-format +msgid "Add custom product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:86 +#, c-format +msgid "Complete information of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:185 +#, c-format +msgid "Image" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:186 +#, c-format +msgid "photo of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:192 +#, c-format +msgid "full product description" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:196 +#, c-format +msgid "Unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:197 +#, c-format +msgid "name of the product unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:201 +#, c-format +msgid "Price" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:202 +#, c-format +msgid "amount in the current currency" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:211 +#, c-format +msgid "Taxes" +msgstr "" + +#: src/components/product/ProductList.tsx:38 +#, c-format +msgid "image" +msgstr "" + +#: src/components/product/ProductList.tsx:41 +#, c-format +msgid "description" +msgstr "" + +#: src/components/product/ProductList.tsx:44 +#, c-format +msgid "quantity" +msgstr "" + +#: src/components/product/ProductList.tsx:47 +#, c-format +msgid "unit price" +msgstr "" + +#: src/components/product/ProductList.tsx:50 +#, c-format +msgid "total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:153 +#, c-format +msgid "required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:157 +#, c-format +msgid "not valid" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:159 +#, c-format +msgid "must be greater than 0" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:164 +#, c-format +msgid "not a valid json" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:170 +#, c-format +msgid "should be in the future" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:173 +#, c-format +msgid "refund deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:179 +#, c-format +msgid "wire transfer deadline cannot be before refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:190 +#, c-format +msgid "wire transfer deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:197 +#, c-format +msgid "should have a refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:202 +#, c-format +msgid "auto refund cannot be after refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:360 +#, c-format +msgid "Manage products in order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:369 +#, c-format +msgid "Manage list of products in the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:391 +#, c-format +msgid "Remove this product from the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:415 +#, c-format +msgid "Total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:417 +#, c-format +msgid "total product price added up" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:430 +#, c-format +msgid "Amount to be paid by the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:436 +#, c-format +msgid "Order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:437 +#, c-format +msgid "final order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:444 +#, c-format +msgid "Summary" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:445 +#, c-format +msgid "Title of the order to be shown to the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:450 +#, c-format +msgid "Shipping and Fulfillment" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:455 +#, c-format +msgid "Delivery date" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:456 +#, c-format +msgid "Deadline for physical delivery assured by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:461 +#, c-format +msgid "Location" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:462 +#, c-format +msgid "address where the products will be delivered" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:469 +#, c-format +msgid "Fulfillment URL" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:470 +#, c-format +msgid "URL to which the user will be redirected after successful payment." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:476 +#, c-format +msgid "Taler payment options" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:477 +#, c-format +msgid "Override default Taler payment settings for this order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:481 +#, c-format +msgid "Payment deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:482 +#, c-format +msgid "" +"Deadline for the customer to pay for the offer before it expires. Inventory " +"products will be reserved until this deadline." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:486 +#, c-format +msgid "Refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:487 +#, c-format +msgid "Time until which the order can be refunded by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:491 +#, c-format +msgid "Wire transfer deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:492 +#, c-format +msgid "Deadline for the exchange to make the wire transfer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:496 +#, c-format +msgid "Auto-refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:497 +#, c-format +msgid "" +"Time until which the wallet will automatically check for refunds without " +"user interaction." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:502 +#, c-format +msgid "Maximum deposit fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:503 +#, c-format +msgid "" +"Maximum deposit fees the merchant is willing to cover for this order. Higher " +"deposit fees must be covered in full by the consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:507 +#, c-format +msgid "Maximum wire fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:508 +#, c-format +msgid "" +"Maximum aggregate wire fees the merchant is willing to cover for this order. " +"Wire fees exceeding this amount are to be covered by the customers." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:512 +#, c-format +msgid "Wire fee amortization" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:513 +#, c-format +msgid "" +"Factor by which wire fees exceeding the above threshold are divided to " +"determine the share of excess wire fees to be paid explicitly by the " +"consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:517 +#, c-format +msgid "Create token" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:518 +#, c-format +msgid "" +"Uncheck this option if the merchant backend generated an order ID with " +"enough entropy to prevent adversarial claims." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:522 +#, c-format +msgid "Minimum age required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:523 +#, c-format +msgid "" +"Any value greater than 0 will limit the coins able be used to pay this " +"contract. If empty the age restriction will be defined by the products" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:526 +#, c-format +msgid "Min age defined by the producs is %1$s" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:534 +#, c-format +msgid "Additional information" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:535 +#, c-format +msgid "Custom information to be included in the contract for this order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:541 +#, c-format +msgid "You must enter a value in JavaScript Object Notation (JSON)." +msgstr "" + +#: src/components/picker/DurationPicker.tsx:55 +#, c-format +msgid "days" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:65 +#, c-format +msgid "hours" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:76 +#, c-format +msgid "minutes" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:87 +#, c-format +msgid "seconds" +msgstr "" + +#: src/components/form/InputDuration.tsx:53 +#, c-format +msgid "forever" +msgstr "" + +#: src/components/form/InputDuration.tsx:62 +#, c-format +msgid "%1$sM" +msgstr "" + +#: src/components/form/InputDuration.tsx:64 +#, c-format +msgid "%1$sY" +msgstr "" + +#: src/components/form/InputDuration.tsx:66 +#, c-format +msgid "%1$sd" +msgstr "" + +#: src/components/form/InputDuration.tsx:68 +#, c-format +msgid "%1$sh" +msgstr "" + +#: src/components/form/InputDuration.tsx:70 +#, c-format +msgid "%1$smin" +msgstr "" + +#: src/components/form/InputDuration.tsx:72 +#, c-format +msgid "%1$ssec" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:75 +#, c-format +msgid "Orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:81 +#, c-format +msgid "create order" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:147 +#, c-format +msgid "load newer orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:154 +#, c-format +msgid "Date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:200 +#, c-format +msgid "Refund" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:209 +#, c-format +msgid "copy url" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:225 +#, c-format +msgid "load older orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:242 +#, c-format +msgid "No orders have been found matching your query!" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:288 +#, c-format +msgid "duplicated" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:299 +#, c-format +msgid "invalid format" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:301 +#, c-format +msgid "this value exceed the refundable amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:346 +#, c-format +msgid "date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:349 +#, c-format +msgid "amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:352 +#, c-format +msgid "reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:389 +#, c-format +msgid "amount to be refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:391 +#, c-format +msgid "Max refundable:" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:396 +#, c-format +msgid "Reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:397 +#, c-format +msgid "Choose one..." +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:399 +#, c-format +msgid "requested by the customer" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:400 +#, c-format +msgid "other" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:403 +#, c-format +msgid "why this order is being refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:409 +#, c-format +msgid "more information to give context" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:62 +#, c-format +msgid "Contract Terms" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:68 +#, c-format +msgid "human-readable description of the whole purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:74 +#, c-format +msgid "total price for the transaction" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:81 +#, c-format +msgid "URL for this purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:87 +#, c-format +msgid "Max fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:88 +#, c-format +msgid "maximum total deposit fee accepted by the merchant for this contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:93 +#, c-format +msgid "Max wire fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:94 +#, c-format +msgid "maximum wire fee accepted by the merchant" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:100 +#, c-format +msgid "" +"over how many customer transactions does the merchant expect to amortize " +"wire fees on average" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:105 +#, c-format +msgid "Created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:106 +#, c-format +msgid "time when this contract was generated" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:112 +#, c-format +msgid "after this deadline has passed no refunds will be accepted" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:118 +#, c-format +msgid "" +"after this deadline, the merchant won't accept payments for the contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:124 +#, c-format +msgid "transfer deadline for the exchange" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:130 +#, c-format +msgid "time indicating when the order should be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:136 +#, c-format +msgid "where the order will be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:144 +#, c-format +msgid "Auto-refund delay" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:145 +#, c-format +msgid "" +"how long the wallet should try to get an automatic refund for the purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:150 +#, c-format +msgid "Extra info" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:151 +#, c-format +msgid "extra data that is only interpreted by the merchant frontend" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:219 +#, c-format +msgid "Order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:221 +#, c-format +msgid "claimed" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:247 +#, c-format +msgid "claimed at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:265 +#, c-format +msgid "Timeline" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:271 +#, c-format +msgid "Payment details" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:291 +#, c-format +msgid "Order status" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:301 +#, c-format +msgid "Product list" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:451 +#, c-format +msgid "paid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:455 +#, c-format +msgid "wired" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:460 +#, c-format +msgid "refunded" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:480 +#, c-format +msgid "refund order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:481 +#, c-format +msgid "not refundable" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:489 +#, c-format +msgid "refund" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:553 +#, c-format +msgid "Refunded amount" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:560 +#, c-format +msgid "Refund taken" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:570 +#, c-format +msgid "Status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:583 +#, c-format +msgid "Refund URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:636 +#, c-format +msgid "unpaid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:654 +#, c-format +msgid "pay at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:666 +#, c-format +msgid "created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:707 +#, c-format +msgid "Order status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:711 +#, c-format +msgid "Payment URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:740 +#, c-format +msgid "" +"Unknown order status. This is an error, please contact the administrator." +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:767 +#, c-format +msgid "Back" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:79 +#, c-format +msgid "refund created successfully" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:85 +#, c-format +msgid "could not create the refund" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:78 +#, c-format +msgid "select date to show nearby orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:94 +#, c-format +msgid "order id" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:100 +#, c-format +msgid "jump to order with the given order ID" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:122 +#, c-format +msgid "remove all filters" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:132 +#, c-format +msgid "only show paid orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:135 +#, c-format +msgid "Paid" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:142 +#, c-format +msgid "only show orders with refunds" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:145 +#, c-format +msgid "Refunded" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:152 +#, c-format +msgid "" +"only show orders where customers paid, but wire payments from payment " +"provider are still pending" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:155 +#, c-format +msgid "Not wired" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:170 +#, c-format +msgid "clear date filter" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:184 +#, c-format +msgid "date (YYYY/MM/DD)" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:103 +#, c-format +msgid "Enter an order id" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:111 +#, c-format +msgid "order not found" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:178 +#, c-format +msgid "could not get the order to refund" +msgstr "" + +#: src/components/exception/AsyncButton.tsx:43 +#, c-format +msgid "Loading..." +msgstr "" + +#: src/components/form/InputStock.tsx:99 +#, c-format +msgid "" +"click here to configure the stock of the product, leave it as is and the " +"backend will not control stock" +msgstr "" + +#: src/components/form/InputStock.tsx:109 +#, c-format +msgid "Manage stock" +msgstr "" + +#: src/components/form/InputStock.tsx:115 +#, c-format +msgid "this product has been configured without stock control" +msgstr "" + +#: src/components/form/InputStock.tsx:119 +#, c-format +msgid "Infinite" +msgstr "" + +#: src/components/form/InputStock.tsx:136 +#, c-format +msgid "lost cannot be greater than current and incoming (max %1$s)" +msgstr "" + +#: src/components/form/InputStock.tsx:176 +#, c-format +msgid "Incoming" +msgstr "" + +#: src/components/form/InputStock.tsx:177 +#, c-format +msgid "Lost" +msgstr "" + +#: src/components/form/InputStock.tsx:192 +#, c-format +msgid "Current" +msgstr "" + +#: src/components/form/InputStock.tsx:196 +#, c-format +msgid "remove stock control for this product" +msgstr "" + +#: src/components/form/InputStock.tsx:202 +#, c-format +msgid "without stock" +msgstr "" + +#: src/components/form/InputStock.tsx:211 +#, c-format +msgid "Next restock" +msgstr "" + +#: src/components/form/InputStock.tsx:217 +#, c-format +msgid "Delivery address" +msgstr "" + +#: src/components/product/ProductForm.tsx:133 +#, c-format +msgid "product identification to use in URLs (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:139 +#, c-format +msgid "illustration of the product for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:145 +#, c-format +msgid "product description for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:149 +#, c-format +msgid "Age restricted" +msgstr "" + +#: src/components/product/ProductForm.tsx:150 +#, c-format +msgid "is this product restricted for customer below certain age?" +msgstr "" + +#: src/components/product/ProductForm.tsx:155 +#, c-format +msgid "" +"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 " +"items, 5 meters) for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:160 +#, c-format +msgid "" +"sale price for customers, including taxes, for above units of the product" +msgstr "" + +#: src/components/product/ProductForm.tsx:164 +#, c-format +msgid "Stock" +msgstr "" + +#: src/components/product/ProductForm.tsx:166 +#, c-format +msgid "" +"product inventory for products with finite supply (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:171 +#, c-format +msgid "taxes included in the product price, exposed to customers" +msgstr "" + +#: src/paths/instance/products/create/CreatePage.tsx:66 +#, c-format +msgid "Need to complete marked fields" +msgstr "" + +#: src/paths/instance/products/create/index.tsx:51 +#, c-format +msgid "could not create product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:68 +#, c-format +msgid "Products" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:73 +#, c-format +msgid "add product to inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:137 +#, c-format +msgid "Sell" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:143 +#, c-format +msgid "Profit" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:149 +#, c-format +msgid "Sold" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:210 +#, c-format +msgid "free" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:248 +#, c-format +msgid "go to product update page" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:255 +#, c-format +msgid "Update" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:260 +#, c-format +msgid "remove this product from the database" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:331 +#, c-format +msgid "update the product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:341 +#, c-format +msgid "update product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:399 +#, c-format +msgid "add more elements to the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:404 +#, c-format +msgid "report elements lost in the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:409 +#, c-format +msgid "new price for the product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:421 +#, c-format +msgid "the are value with errors" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:422 +#, c-format +msgid "update product with new stock and price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:463 +#, c-format +msgid "There is no products yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:86 +#, c-format +msgid "product updated successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:92 +#, c-format +msgid "could not update the product" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:103 +#, c-format +msgid "product delete successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:109 +#, c-format +msgid "could not delete the product" +msgstr "" + +#: src/paths/instance/products/update/UpdatePage.tsx:56 +#, c-format +msgid "Product id:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95 +#, c-format +msgid "" +"To complete the setup of the reserve, you must now initiate a wire transfer " +"using the given wire transfer subject and crediting the specified amount to " +"the indicated account of the exchange." +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102 +#, c-format +msgid "If your system supports RFC 8905, you can do this by opening this URI:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:83 +#, c-format +msgid "it should be greater than 0" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:88 +#, c-format +msgid "must be a valid URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:107 +#, c-format +msgid "Initial balance" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:108 +#, c-format +msgid "balance prior to deposit" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:112 +#, c-format +msgid "Exchange URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:113 +#, c-format +msgid "URL of exchange" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:148 +#, c-format +msgid "Next" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:186 +#, c-format +msgid "Wire method" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:187 +#, c-format +msgid "method to use for wire transfer" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:189 +#, c-format +msgid "Select one wire method" +msgstr "" + +#: src/paths/instance/reserves/create/index.tsx:62 +#, c-format +msgid "could not create reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:77 +#, c-format +msgid "Valid until" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:82 +#, c-format +msgid "Created balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:99 +#, c-format +msgid "Exchange balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:104 +#, c-format +msgid "Picked up" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:109 +#, c-format +msgid "Committed" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:116 +#, c-format +msgid "Account address" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:119 +#, c-format +msgid "Subject" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:130 +#, c-format +msgid "Tips" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:193 +#, c-format +msgid "No tips has been authorized from this reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:213 +#, c-format +msgid "Authorized" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:222 +#, c-format +msgid "Expiration" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108 +#, c-format +msgid "amount of tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112 +#, c-format +msgid "Justification" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114 +#, c-format +msgid "reason for the tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118 +#, c-format +msgid "URL after tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119 +#, c-format +msgid "URL to visit after tip payment" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:65 +#, c-format +msgid "Reserves not yet funded" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:89 +#, c-format +msgid "Reserves ready" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:95 +#, c-format +msgid "add new reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:143 +#, c-format +msgid "Expires at" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:146 +#, c-format +msgid "Initial" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:202 +#, c-format +msgid "delete selected reserve from the database" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:210 +#, c-format +msgid "authorize new tip from selected reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:237 +#, c-format +msgid "" +"There is no ready reserves yet, add more pressing the + sign or fund them" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:264 +#, c-format +msgid "Expected Balance" +msgstr "" + +#: src/paths/instance/reserves/list/index.tsx:110 +#, c-format +msgid "could not create the tip" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:77 +#, c-format +msgid "should not be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:93 +#, c-format +msgid "should be greater that 0" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:96 +#, c-format +msgid "can't be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:100 +#, c-format +msgid "to short" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:108 +#, c-format +msgid "just letters and numbers from 2 to 7" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:110 +#, c-format +msgid "size of the key should be 32" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:137 +#, c-format +msgid "Identifier" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:138 +#, c-format +msgid "Name of the template in URLs." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:144 +#, c-format +msgid "Describe what this template stands for" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:149 +#, c-format +msgid "Fixed summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:150 +#, c-format +msgid "If specified, this template will create order with the same summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:154 +#, c-format +msgid "Fixed price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:155 +#, c-format +msgid "If specified, this template will create order with the same price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:159 +#, c-format +msgid "Minimum age" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:161 +#, c-format +msgid "Is this contract restricted to some age?" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:165 +#, c-format +msgid "Payment timeout" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:167 +#, c-format +msgid "" +"How much time has the customer to complete the payment once the order was " +"created." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:171 +#, c-format +msgid "Verification algorithm" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:172 +#, c-format +msgid "Algorithm to use to verify transaction in offline mode" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:180 +#, c-format +msgid "Point-of-sale key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:182 +#, c-format +msgid "Useful to validate the purchase" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:196 +#, c-format +msgid "generate random secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:203 +#, c-format +msgid "random" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:208 +#, c-format +msgid "show secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:209 +#, c-format +msgid "hide secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:216 +#, c-format +msgid "hide" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:218 +#, c-format +msgid "show" +msgstr "" + +#: src/paths/instance/templates/create/index.tsx:52 +#, c-format +msgid "could not inform template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:54 +#, c-format +msgid "Amount is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:58 +#, c-format +msgid "Order summary is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:86 +#, c-format +msgid "New order for template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:108 +#, c-format +msgid "Amount of the order" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:113 +#, c-format +msgid "Order summary" +msgstr "" + +#: src/paths/instance/templates/use/index.tsx:92 +#, c-format +msgid "could not create order from template" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:131 +#, c-format +msgid "" +"Here you can specify a default value for fields that are not fixed. Default " +"values can be edited by the customer before the payment." +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:148 +#, c-format +msgid "Fixed amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:149 +#, c-format +msgid "Default amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:161 +#, c-format +msgid "Default summary" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:177 +#, c-format +msgid "Print" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:184 +#, c-format +msgid "Setup TOTP" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:65 +#, c-format +msgid "Templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:70 +#, c-format +msgid "add new templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:142 +#, c-format +msgid "load more templates before the first one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:146 +#, c-format +msgid "load newer templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:181 +#, c-format +msgid "delete selected templates from the database" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:188 +#, c-format +msgid "use template to create new order" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:195 +#, c-format +msgid "create qr code for the template" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:210 +#, c-format +msgid "load more templates after the last one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:214 +#, c-format +msgid "load older templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:231 +#, c-format +msgid "There is no templates yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:104 +#, c-format +msgid "template delete successfully" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:110 +#, c-format +msgid "could not delete the template" +msgstr "" + +#: src/paths/instance/templates/update/index.tsx:90 +#, c-format +msgid "could not update template" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:57 +#, c-format +msgid "should be one of '%1$s'" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:85 +#, c-format +msgid "Webhook ID to use" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:89 +#, c-format +msgid "Event" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:90 +#, c-format +msgid "The event of the webhook: why the webhook is used" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:94 +#, c-format +msgid "Method" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:95 +#, c-format +msgid "Method used by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:99 +#, c-format +msgid "URL" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:100 +#, c-format +msgid "URL of the webhook where the customer will be redirected" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:104 +#, c-format +msgid "Header" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:106 +#, c-format +msgid "Header template of the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:111 +#, c-format +msgid "Body" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:112 +#, c-format +msgid "Body template by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:61 +#, c-format +msgid "Webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:66 +#, c-format +msgid "add new webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:137 +#, c-format +msgid "load more webhooks before the first one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:141 +#, c-format +msgid "load newer webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:151 +#, c-format +msgid "Event type" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:176 +#, c-format +msgid "delete selected webhook from the database" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:198 +#, c-format +msgid "load more webhooks after the last one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:202 +#, c-format +msgid "load older webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:219 +#, c-format +msgid "There is no webhooks yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:94 +#, c-format +msgid "webhook delete successfully" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:100 +#, c-format +msgid "could not delete the webhook" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:63 +#, c-format +msgid "check the id, does not look valid" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:65 +#, c-format +msgid "should have 52 characters, current %1$s" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:72 +#, c-format +msgid "URL doesn't have the right format" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:98 +#, c-format +msgid "Credited bank account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:100 +#, c-format +msgid "Select one account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:101 +#, c-format +msgid "Bank account of the merchant where the payment was received" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:105 +#, c-format +msgid "Wire transfer ID" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:107 +#, c-format +msgid "" +"unique identifier of the wire transfer used by the exchange, must be 52 " +"characters long" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:112 +#, c-format +msgid "" +"Base URL of the exchange that made the transfer, should have been in the " +"wire transfer subject" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:117 +#, c-format +msgid "Amount credited" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:118 +#, c-format +msgid "Actual amount that was wired to the merchant's bank account" +msgstr "" + +#: src/paths/instance/transfers/create/index.tsx:58 +#, c-format +msgid "could not inform transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:61 +#, c-format +msgid "Transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:66 +#, c-format +msgid "add new transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:129 +#, c-format +msgid "load more transfers before the first one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:133 +#, c-format +msgid "load newer transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:143 +#, c-format +msgid "Credit" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:152 +#, c-format +msgid "Confirmed" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:155 +#, c-format +msgid "Verified" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:158 +#, c-format +msgid "Executed at" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "yes" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "no" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:181 +#, c-format +msgid "unknown" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:187 +#, c-format +msgid "delete selected transfer from the database" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:202 +#, c-format +msgid "load more transfer after the last one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:206 +#, c-format +msgid "load older transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:223 +#, c-format +msgid "There is no transfer yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:79 +#, c-format +msgid "filter by account address" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:100 +#, c-format +msgid "only show wire transfers confirmed by the merchant" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:110 +#, c-format +msgid "only show wire transfers claimed by the exchange" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:113 +#, c-format +msgid "Unverified" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:69 +#, c-format +msgid "is not valid" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:94 +#, c-format +msgid "is not a number" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:96 +#, c-format +msgid "must be 1 or greater" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:107 +#, c-format +msgid "max 7 lines" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:178 +#, c-format +msgid "change authorization configuration" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:217 +#, c-format +msgid "Need to complete marked fields and choose authorization method" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:82 +#, c-format +msgid "This is not a valid bitcoin address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:95 +#, c-format +msgid "This is not a valid Ethereum address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:118 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:120 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:128 +#, c-format +msgid "IBAN country code not found" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:153 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:248 +#, c-format +msgid "Target type" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:249 +#, c-format +msgid "Method to use for wire transfer" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:258 +#, c-format +msgid "Routing" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:259 +#, c-format +msgid "Routing number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:263 +#, c-format +msgid "Account" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:264 +#, c-format +msgid "Account number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:273 +#, c-format +msgid "Business Identifier Code." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:282 +#, c-format +msgid "Bank Account Number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:292 +#, c-format +msgid "Unified Payment Interface." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:301 +#, c-format +msgid "Bitcoin protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:310 +#, c-format +msgid "Ethereum protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:319 +#, c-format +msgid "Interledger protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:328 +#, c-format +msgid "Host" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:329 +#, c-format +msgid "Bank host." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:334 +#, c-format +msgid "Bank account." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:343 +#, c-format +msgid "Bank account owner's name." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:370 +#, c-format +msgid "No accounts yet." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:52 +#, c-format +msgid "" +"Name of the instance in URLs. The 'default' instance is special in that it " +"is used to administer other instances." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:58 +#, c-format +msgid "Business name" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:59 +#, c-format +msgid "Legal name of the business represented by this instance." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:64 +#, c-format +msgid "Email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:65 +#, c-format +msgid "Contact email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:70 +#, c-format +msgid "Website URL" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:71 +#, c-format +msgid "URL." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:76 +#, c-format +msgid "Logo" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:77 +#, c-format +msgid "Logo image." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:82 +#, c-format +msgid "Bank account" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:83 +#, c-format +msgid "URI specifying bank account for crediting revenue." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:88 +#, c-format +msgid "Default max deposit fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:89 +#, c-format +msgid "" +"Maximum deposit fees this merchant is willing to pay per order by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:94 +#, c-format +msgid "Default max wire fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:95 +#, c-format +msgid "" +"Maximum wire fees this merchant is willing to pay per wire transfer by " +"default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:100 +#, c-format +msgid "Default wire fee amortization" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:101 +#, c-format +msgid "" +"Number of orders excess wire transfer fees will be divided by to compute per " +"order surcharge." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:107 +#, c-format +msgid "Physical location of the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:114 +#, c-format +msgid "Jurisdiction" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:115 +#, c-format +msgid "Jurisdiction for legal disputes with the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:122 +#, c-format +msgid "Default payment delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:124 +#, c-format +msgid "" +"Time customers have to pay an order before the offer expires by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:129 +#, c-format +msgid "Default wire transfer delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:130 +#, c-format +msgid "" +"Maximum time an exchange is allowed to delay wiring funds to the merchant, " +"enabling it to aggregate smaller payments into larger wire transfers and " +"reducing wire fees." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:164 +#, c-format +msgid "Instance id" +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:173 +#, c-format +msgid "Change the authorization method use for this instance." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:182 +#, c-format +msgid "Manage access token" +msgstr "" + +#: src/paths/instance/update/index.tsx:112 +#, c-format +msgid "Failed to create instance" +msgstr "" + +#: src/components/exception/login.tsx:74 +#, c-format +msgid "Login required" +msgstr "" + +#: src/components/exception/login.tsx:80 +#, c-format +msgid "Please enter your access token." +msgstr "" + +#: src/components/exception/login.tsx:108 +#, c-format +msgid "Access Token" +msgstr "" + +#: src/InstanceRoutes.tsx:171 +#, c-format +msgid "The request to the backend take too long and was cancelled" +msgstr "" + +#: src/InstanceRoutes.tsx:172 +#, c-format +msgid "Diagnostic from %1$s is \"%2$s\"" +msgstr "" + +#: src/InstanceRoutes.tsx:178 +#, c-format +msgid "The backend reported a problem: HTTP status #%1$s" +msgstr "" + +#: src/InstanceRoutes.tsx:179 +#, c-format +msgid "Diagnostic from %1$s is '%2$s'" +msgstr "" + +#: src/InstanceRoutes.tsx:196 +#, c-format +msgid "Access denied" +msgstr "" + +#: src/InstanceRoutes.tsx:197 +#, c-format +msgid "The access token provided is invalid." +msgstr "" + +#: src/InstanceRoutes.tsx:212 +#, c-format +msgid "No 'default' instance configured yet." +msgstr "" + +#: src/InstanceRoutes.tsx:213 +#, c-format +msgid "Create a 'default' instance to begin using the merchant backoffice." +msgstr "" + +#: src/InstanceRoutes.tsx:630 +#, c-format +msgid "The access token provided is invalid" +msgstr "" + +#: src/InstanceRoutes.tsx:664 +#, c-format +msgid "Hide for today" +msgstr "" + +#: src/components/menu/SideBar.tsx:82 +#, c-format +msgid "Instance" +msgstr "" + +#: src/components/menu/SideBar.tsx:91 +#, c-format +msgid "Settings" +msgstr "" + +#: src/components/menu/SideBar.tsx:167 +#, c-format +msgid "Connection" +msgstr "" + +#: src/components/menu/SideBar.tsx:209 +#, c-format +msgid "New" +msgstr "" + +#: src/components/menu/SideBar.tsx:219 +#, c-format +msgid "List" +msgstr "" + +#: src/components/menu/SideBar.tsx:234 +#, c-format +msgid "Log out" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:71 +#, c-format +msgid "Check your token is valid" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:90 +#, c-format +msgid "Couldn't access the server." +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:91 +#, c-format +msgid "Could not infer instance id from url %1$s" +msgstr "" + +#: src/Application.tsx:104 +#, c-format +msgid "Server not found" +msgstr "" + +#: src/Application.tsx:118 +#, c-format +msgid "Server response with an error code" +msgstr "" + +#: src/Application.tsx:120 +#, c-format +msgid "Got message %1$s from %2$s" +msgstr "" + +#: src/Application.tsx:131 +#, c-format +msgid "Response from server is unreadable, http status: %1$s" +msgstr "" + +#: src/Application.tsx:144 +#, c-format +msgid "Unexpected Error" +msgstr "" + +#: src/components/form/InputArray.tsx:101 +#, c-format +msgid "The value %1$s is invalid for a payment url" +msgstr "" + +#: src/components/form/InputArray.tsx:110 +#, c-format +msgid "add element to the list" +msgstr "" + +#: src/components/form/InputArray.tsx:112 +#, c-format +msgid "add" +msgstr "" + +#: src/components/form/InputSecured.tsx:37 +#, c-format +msgid "Deleting" +msgstr "" + +#: src/components/form/InputSecured.tsx:41 +#, c-format +msgid "Changing" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87 +#, c-format +msgid "Order ID" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101 +#, c-format +msgid "Payment URL" +msgstr "" diff --git a/packages/auditor-backoffice-ui/src/i18n/es.po b/packages/auditor-backoffice-ui/src/i18n/es.po new file mode 100644 index 000000000..10ec0cf3b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/es.po @@ -0,0 +1,2854 @@ +# This file is part of TALER +# (C) 2016 GNUnet e.V. +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"POT-Creation-Date: 2016-11-23 00:00+0100\n" +"PO-Revision-Date: 2023-08-13 10:14+0000\n" +"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n" +"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/" +"merchant-backoffice/es/>\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.13.1\n" + +#: src/components/modal/index.tsx:71 +#, c-format +msgid "Cancel" +msgstr "Cancelar" + +#: src/components/modal/index.tsx:79 +#, c-format +msgid "%1$s" +msgstr "%1$s" + +#: src/components/modal/index.tsx:84 +#, c-format +msgid "Close" +msgstr "" + +#: src/components/modal/index.tsx:124 +#, c-format +msgid "Continue" +msgstr "Continuar" + +#: src/components/modal/index.tsx:178 +#, c-format +msgid "Clear" +msgstr "Limpiar" + +#: src/components/modal/index.tsx:190 +#, c-format +msgid "Confirm" +msgstr "Confirmar" + +#: src/components/modal/index.tsx:296 +#, c-format +msgid "is not the same as the current access token" +msgstr "no es el mismo que el token de acceso actual" + +#: src/components/modal/index.tsx:299 +#, c-format +msgid "cannot be empty" +msgstr "no puede ser vacÃo" + +#: src/components/modal/index.tsx:301 +#, c-format +msgid "cannot be the same as the old token" +msgstr "no puede ser igual al viejo token" + +#: src/components/modal/index.tsx:305 +#, c-format +msgid "is not the same" +msgstr "no son iguales" + +#: src/components/modal/index.tsx:315 +#, c-format +msgid "You are updating the access token from instance with id %1$s" +msgstr "Está actualizando el token de acceso para la instancia con id %1$s" + +#: src/components/modal/index.tsx:331 +#, c-format +msgid "Old access token" +msgstr "Viejo token de acceso" + +#: src/components/modal/index.tsx:332 +#, c-format +msgid "access token currently in use" +msgstr "acceder al token en uso actualmente" + +#: src/components/modal/index.tsx:338 +#, c-format +msgid "New access token" +msgstr "Nuevo token de acceso" + +#: src/components/modal/index.tsx:339 +#, c-format +msgid "next access token to be used" +msgstr "siguiente token de acceso a usar" + +#: src/components/modal/index.tsx:344 +#, c-format +msgid "Repeat access token" +msgstr "Repetir token de acceso" + +#: src/components/modal/index.tsx:345 +#, c-format +msgid "confirm the same access token" +msgstr "confirmar el mismo token de acceso" + +#: src/components/modal/index.tsx:350 +#, c-format +msgid "Clearing the access token will mean public access to the instance" +msgstr "Limpiar el token de acceso significa acceso público a la instancia" + +#: src/components/modal/index.tsx:377 +#, c-format +msgid "cannot be the same as the old access token" +msgstr "no puede ser igual al anterior token de acceso" + +#: src/components/modal/index.tsx:394 +#, c-format +msgid "You are setting the access token for the new instance" +msgstr "Está estableciendo el token de acceso para la nueva instancia" + +#: src/components/modal/index.tsx:420 +#, c-format +msgid "" +"With external authorization method no check will be done by the merchant " +"backend" +msgstr "" +"Con el método de autorización externa no se hará ninguna revisión por el " +"backend del comerciante" + +#: src/components/modal/index.tsx:436 +#, c-format +msgid "Set external authorization" +msgstr "Establecer autorización externa" + +#: src/components/modal/index.tsx:448 +#, c-format +msgid "Set access token" +msgstr "Establecer token de acceso" + +#: src/components/modal/index.tsx:470 +#, c-format +msgid "Operation in progress..." +msgstr "Operación en progreso..." + +#: src/components/modal/index.tsx:479 +#, c-format +msgid "The operation will be automatically canceled after %1$s seconds" +msgstr "La operación será automáticamente cancelada luego de %1$s segundos" + +#: src/paths/admin/list/TableActive.tsx:80 +#, c-format +msgid "Instances" +msgstr "Instancias" + +#: src/paths/admin/list/TableActive.tsx:93 +#, c-format +msgid "Delete" +msgstr "Eliminar" + +#: src/paths/admin/list/TableActive.tsx:99 +#, c-format +msgid "add new instance" +msgstr "agregar nueva instancia" + +#: src/paths/admin/list/TableActive.tsx:178 +#, c-format +msgid "ID" +msgstr "ID" + +#: src/paths/admin/list/TableActive.tsx:181 +#, c-format +msgid "Name" +msgstr "Nombre" + +#: src/paths/admin/list/TableActive.tsx:220 +#, c-format +msgid "Edit" +msgstr "Editar" + +#: src/paths/admin/list/TableActive.tsx:237 +#, c-format +msgid "Purge" +msgstr "Purgar" + +#: src/paths/admin/list/TableActive.tsx:261 +#, c-format +msgid "There is no instances yet, add more pressing the + sign" +msgstr "TodavÃa no hay instancias, agregue más presionando el signo +" + +#: src/paths/admin/list/View.tsx:68 +#, c-format +msgid "Only show active instances" +msgstr "Solo mostrar instancias activas" + +#: src/paths/admin/list/View.tsx:71 +#, c-format +msgid "Active" +msgstr "Activo" + +#: src/paths/admin/list/View.tsx:78 +#, c-format +msgid "Only show deleted instances" +msgstr "Mostrar solo instancias eliminadas" + +#: src/paths/admin/list/View.tsx:81 +#, c-format +msgid "Deleted" +msgstr "Eliminado" + +#: src/paths/admin/list/View.tsx:88 +#, c-format +msgid "Show all instances" +msgstr "Mostrar todas las instancias" + +#: src/paths/admin/list/View.tsx:91 +#, c-format +msgid "All" +msgstr "Todo" + +#: src/paths/admin/list/index.tsx:101 +#, fuzzy, c-format +msgid "Instance \"%1$s\" (ID: %2$s) has been deleted" +msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada" + +#: src/paths/admin/list/index.tsx:106 +#, c-format +msgid "Failed to delete instance" +msgstr "Fallo al eliminar instancia" + +#: src/paths/admin/list/index.tsx:124 +#, c-format +msgid "Instance '%1$s' (ID: %2$s) has been disabled" +msgstr "Instance '%1$s' (ID: %2$s) ha sido deshabilitada" + +#: src/paths/admin/list/index.tsx:129 +#, c-format +msgid "Failed to purge instance" +msgstr "Fallo al purgar la instancia" + +#: src/paths/instance/kyc/list/ListPage.tsx:41 +#, c-format +msgid "Pending KYC verification" +msgstr "Verificación KYC pendiente" + +#: src/paths/instance/kyc/list/ListPage.tsx:66 +#, c-format +msgid "Timed out" +msgstr "Expirado" + +#: src/paths/instance/kyc/list/ListPage.tsx:103 +#, c-format +msgid "Exchange" +msgstr "Exchange" + +#: src/paths/instance/kyc/list/ListPage.tsx:106 +#, c-format +msgid "Target account" +msgstr "Cuenta objetivo" + +#: src/paths/instance/kyc/list/ListPage.tsx:109 +#, c-format +msgid "KYC URL" +msgstr "URL de KYC" + +#: src/paths/instance/kyc/list/ListPage.tsx:144 +#, c-format +msgid "Code" +msgstr "Código" + +#: src/paths/instance/kyc/list/ListPage.tsx:147 +#, c-format +msgid "Http Status" +msgstr "Estado http" + +#: src/paths/instance/kyc/list/ListPage.tsx:177 +#, c-format +msgid "No pending kyc verification!" +msgstr "¡No hay verificación kyc pendiente!" + +#: src/components/form/InputDate.tsx:123 +#, c-format +msgid "change value to unknown date" +msgstr "cambiar valor a fecha desconocida" + +#: src/components/form/InputDate.tsx:124 +#, c-format +msgid "change value to empty" +msgstr "cambiar valor a vacÃo" + +#: src/components/form/InputDate.tsx:131 +#, c-format +msgid "clear" +msgstr "limpiar" + +#: src/components/form/InputDate.tsx:136 +#, c-format +msgid "change value to never" +msgstr "cambiar valor a nunca" + +#: src/components/form/InputDate.tsx:141 +#, c-format +msgid "never" +msgstr "nunca" + +#: src/components/form/InputLocation.tsx:29 +#, c-format +msgid "Country" +msgstr "PaÃs" + +#: src/components/form/InputLocation.tsx:33 +#, c-format +msgid "Address" +msgstr "Dirección" + +#: src/components/form/InputLocation.tsx:39 +#, c-format +msgid "Building number" +msgstr "Número de edificio" + +#: src/components/form/InputLocation.tsx:41 +#, c-format +msgid "Building name" +msgstr "Nombre de edificio" + +#: src/components/form/InputLocation.tsx:42 +#, c-format +msgid "Street" +msgstr "Calle" + +#: src/components/form/InputLocation.tsx:43 +#, c-format +msgid "Post code" +msgstr "Código postal" + +#: src/components/form/InputLocation.tsx:44 +#, c-format +msgid "Town location" +msgstr "Ubicación de ciudad" + +#: src/components/form/InputLocation.tsx:45 +#, c-format +msgid "Town" +msgstr "Ciudad" + +#: src/components/form/InputLocation.tsx:46 +#, c-format +msgid "District" +msgstr "Distrito" + +#: src/components/form/InputLocation.tsx:49 +#, c-format +msgid "Country subdivision" +msgstr "Subdivisión de paÃs" + +#: src/components/form/InputSearchProduct.tsx:66 +#, c-format +msgid "Product id" +msgstr "Id de producto" + +#: src/components/form/InputSearchProduct.tsx:69 +#, c-format +msgid "Description" +msgstr "Descripcion" + +#: src/components/form/InputSearchProduct.tsx:94 +#, fuzzy, c-format +msgid "Product" +msgstr "Productos" + +#: src/components/form/InputSearchProduct.tsx:95 +#, c-format +msgid "search products by it's description or id" +msgstr "buscar productos por su descripción o ID" + +#: src/components/form/InputSearchProduct.tsx:151 +#, c-format +msgid "no products found with that description" +msgstr "no se encontraron productos con esa descripción" + +#: src/components/product/InventoryProductForm.tsx:56 +#, c-format +msgid "You must enter a valid product identifier." +msgstr "Debe ingresar un identificador de producto válido." + +#: src/components/product/InventoryProductForm.tsx:64 +#, c-format +msgid "Quantity must be greater than 0!" +msgstr "¡Cantidad debe ser mayor que 0!" + +#: src/components/product/InventoryProductForm.tsx:76 +#, fuzzy, c-format +msgid "" +"This quantity exceeds remaining stock. Currently, only %1$s units remain " +"unreserved in stock." +msgstr "" +"Esta cantidad excede las existencias restantes. Actualmente, solo quedan " +"%1$s unidades sin reservar en las existencias." + +#: src/components/product/InventoryProductForm.tsx:109 +#, c-format +msgid "Quantity" +msgstr "Cantidad" + +#: src/components/product/InventoryProductForm.tsx:110 +#, c-format +msgid "how many products will be added" +msgstr "cuántos productos serán agregados" + +#: src/components/product/InventoryProductForm.tsx:117 +#, c-format +msgid "Add from inventory" +msgstr "Agregar del inventario" + +#: src/components/form/InputImage.tsx:105 +#, c-format +msgid "Image should be smaller than 1 MB" +msgstr "La imagen debe ser mas chica que 1 MB" + +#: src/components/form/InputImage.tsx:110 +#, c-format +msgid "Add" +msgstr "Agregar" + +#: src/components/form/InputImage.tsx:115 +#, c-format +msgid "Remove" +msgstr "Eliminar" + +#: src/components/form/InputTaxes.tsx:113 +#, c-format +msgid "No taxes configured for this product." +msgstr "Ningun impuesto configurado para este producto." + +#: src/components/form/InputTaxes.tsx:119 +#, c-format +msgid "Amount" +msgstr "Monto" + +#: src/components/form/InputTaxes.tsx:120 +#, c-format +msgid "" +"Taxes can be in currencies that differ from the main currency used by the " +"merchant." +msgstr "" +"Impuestos pueden estar en divisas que difieren de la principal divisa usada " +"por el comerciante." + +#: src/components/form/InputTaxes.tsx:122 +#, c-format +msgid "" +"Enter currency and value separated with a colon, e.g. "USD:2.3"." +msgstr "" +"Ingrese divisa y valor separado por dos puntos, e.g. "USD:2.3"." + +#: src/components/form/InputTaxes.tsx:131 +#, c-format +msgid "Legal name of the tax, e.g. VAT or import duties." +msgstr "Nombre legal del impuesto, e.g. IVA o arancel." + +#: src/components/form/InputTaxes.tsx:137 +#, c-format +msgid "add tax to the tax list" +msgstr "agregar impuesto a la lista de impuestos" + +#: src/components/product/NonInventoryProductForm.tsx:72 +#, c-format +msgid "describe and add a product that is not in the inventory list" +msgstr "describa y agregue un producto que no está en la lista de inventarios" + +#: src/components/product/NonInventoryProductForm.tsx:75 +#, c-format +msgid "Add custom product" +msgstr "Agregue un producto personalizado" + +#: src/components/product/NonInventoryProductForm.tsx:86 +#, c-format +msgid "Complete information of the product" +msgstr "Complete información del producto" + +#: src/components/product/NonInventoryProductForm.tsx:185 +#, c-format +msgid "Image" +msgstr "Imagen" + +#: src/components/product/NonInventoryProductForm.tsx:186 +#, c-format +msgid "photo of the product" +msgstr "foto del producto" + +#: src/components/product/NonInventoryProductForm.tsx:192 +#, c-format +msgid "full product description" +msgstr "descripción completa del producto" + +#: src/components/product/NonInventoryProductForm.tsx:196 +#, c-format +msgid "Unit" +msgstr "Unidad" + +#: src/components/product/NonInventoryProductForm.tsx:197 +#, c-format +msgid "name of the product unit" +msgstr "nombre de la unidad del producto" + +#: src/components/product/NonInventoryProductForm.tsx:201 +#, c-format +msgid "Price" +msgstr "Precio" + +#: src/components/product/NonInventoryProductForm.tsx:202 +#, c-format +msgid "amount in the current currency" +msgstr "monto de la divisa actual" + +#: src/components/product/NonInventoryProductForm.tsx:211 +#, c-format +msgid "Taxes" +msgstr "Impuestos" + +#: src/components/product/ProductList.tsx:38 +#, c-format +msgid "image" +msgstr "imagen" + +#: src/components/product/ProductList.tsx:41 +#, c-format +msgid "description" +msgstr "descripción" + +#: src/components/product/ProductList.tsx:44 +#, c-format +msgid "quantity" +msgstr "cantidad" + +#: src/components/product/ProductList.tsx:47 +#, c-format +msgid "unit price" +msgstr "precio unitario" + +#: src/components/product/ProductList.tsx:50 +#, c-format +msgid "total price" +msgstr "precio total" + +#: src/paths/instance/orders/create/CreatePage.tsx:153 +#, c-format +msgid "required" +msgstr "requerido" + +#: src/paths/instance/orders/create/CreatePage.tsx:157 +#, fuzzy, c-format +msgid "not valid" +msgstr "no es un json válido" + +#: src/paths/instance/orders/create/CreatePage.tsx:159 +#, c-format +msgid "must be greater than 0" +msgstr "debe ser mayor que 0" + +#: src/paths/instance/orders/create/CreatePage.tsx:164 +#, c-format +msgid "not a valid json" +msgstr "no es un json válido" + +#: src/paths/instance/orders/create/CreatePage.tsx:170 +#, c-format +msgid "should be in the future" +msgstr "deberÃan ser en el futuro" + +#: src/paths/instance/orders/create/CreatePage.tsx:173 +#, c-format +msgid "refund deadline cannot be before pay deadline" +msgstr "plazo de reembolso no puede ser antes que el plazo de pago" + +#: src/paths/instance/orders/create/CreatePage.tsx:179 +#, c-format +msgid "wire transfer deadline cannot be before refund deadline" +msgstr "" +"el plazo de la transferencia bancaria no puede ser antes que el plazo de " +"reembolso" + +#: src/paths/instance/orders/create/CreatePage.tsx:190 +#, c-format +msgid "wire transfer deadline cannot be before pay deadline" +msgstr "" +"el plazo de la transferencia bancaria no puede ser antes que el plazo de pago" + +#: src/paths/instance/orders/create/CreatePage.tsx:197 +#, c-format +msgid "should have a refund deadline" +msgstr "deberÃa tener un plazo de reembolso" + +#: src/paths/instance/orders/create/CreatePage.tsx:202 +#, c-format +msgid "auto refund cannot be after refund deadline" +msgstr "reembolso automático no puede ser después qu el plazo de reembolso" + +#: src/paths/instance/orders/create/CreatePage.tsx:360 +#, c-format +msgid "Manage products in order" +msgstr "Manejar productos en orden" + +#: src/paths/instance/orders/create/CreatePage.tsx:369 +#, c-format +msgid "Manage list of products in the order." +msgstr "Manejar lista de productos en la orden." + +#: src/paths/instance/orders/create/CreatePage.tsx:391 +#, c-format +msgid "Remove this product from the order." +msgstr "Remover este producto de la orden." + +#: src/paths/instance/orders/create/CreatePage.tsx:415 +#, c-format +msgid "Total price" +msgstr "Precio total" + +#: src/paths/instance/orders/create/CreatePage.tsx:417 +#, c-format +msgid "total product price added up" +msgstr "precio total de producto agregado" + +#: src/paths/instance/orders/create/CreatePage.tsx:430 +#, c-format +msgid "Amount to be paid by the customer" +msgstr "Monto a ser pagado por el cliente" + +#: src/paths/instance/orders/create/CreatePage.tsx:436 +#, c-format +msgid "Order price" +msgstr "Precio de la orden" + +#: src/paths/instance/orders/create/CreatePage.tsx:437 +#, c-format +msgid "final order price" +msgstr "Precio final de la orden" + +#: src/paths/instance/orders/create/CreatePage.tsx:444 +#, c-format +msgid "Summary" +msgstr "Resumen" + +#: src/paths/instance/orders/create/CreatePage.tsx:445 +#, c-format +msgid "Title of the order to be shown to the customer" +msgstr "TÃtulo de la orden a ser mostrado al cliente" + +#: src/paths/instance/orders/create/CreatePage.tsx:450 +#, c-format +msgid "Shipping and Fulfillment" +msgstr "EnvÃo y cumplimiento" + +#: src/paths/instance/orders/create/CreatePage.tsx:455 +#, c-format +msgid "Delivery date" +msgstr "Fecha de entrega" + +#: src/paths/instance/orders/create/CreatePage.tsx:456 +#, c-format +msgid "Deadline for physical delivery assured by the merchant." +msgstr "Plazo para la entrega fÃsica asegurado por el comerciante." + +#: src/paths/instance/orders/create/CreatePage.tsx:461 +#, c-format +msgid "Location" +msgstr "Ubicación" + +#: src/paths/instance/orders/create/CreatePage.tsx:462 +#, c-format +msgid "address where the products will be delivered" +msgstr "dirección a donde los productos serán entregados" + +#: src/paths/instance/orders/create/CreatePage.tsx:469 +#, c-format +msgid "Fulfillment URL" +msgstr "URL de cumplimiento" + +#: src/paths/instance/orders/create/CreatePage.tsx:470 +#, c-format +msgid "URL to which the user will be redirected after successful payment." +msgstr "URL al cual el usuario será redirigido luego de pago exitoso." + +#: src/paths/instance/orders/create/CreatePage.tsx:476 +#, c-format +msgid "Taler payment options" +msgstr "Opciones de pago de Taler" + +#: src/paths/instance/orders/create/CreatePage.tsx:477 +#, c-format +msgid "Override default Taler payment settings for this order" +msgstr "Sobreescribir pagos por omisión de Taler para esta orden" + +#: src/paths/instance/orders/create/CreatePage.tsx:481 +#, fuzzy, c-format +msgid "Payment deadline" +msgstr "Plazo de pago" + +#: src/paths/instance/orders/create/CreatePage.tsx:482 +#, c-format +msgid "" +"Deadline for the customer to pay for the offer before it expires. Inventory " +"products will be reserved until this deadline." +msgstr "" +"Plazo lÃmite para que el cliente pague por la oferta antes de que expire. " +"Productos del inventario serán reservados hasta este plazo lÃmite." + +#: src/paths/instance/orders/create/CreatePage.tsx:486 +#, c-format +msgid "Refund deadline" +msgstr "Plazo de reembolso" + +#: src/paths/instance/orders/create/CreatePage.tsx:487 +#, c-format +msgid "Time until which the order can be refunded by the merchant." +msgstr "" +"Tiempo hasta el cual la orden puede ser reembolsada por el comerciante." + +#: src/paths/instance/orders/create/CreatePage.tsx:491 +#, c-format +msgid "Wire transfer deadline" +msgstr "Plazo de la transferencia" + +#: src/paths/instance/orders/create/CreatePage.tsx:492 +#, c-format +msgid "Deadline for the exchange to make the wire transfer." +msgstr "Plazo para que el exchange haga la transferencia." + +#: src/paths/instance/orders/create/CreatePage.tsx:496 +#, fuzzy, c-format +msgid "Auto-refund deadline" +msgstr "Plazo de reembolso automático" + +#: src/paths/instance/orders/create/CreatePage.tsx:497 +#, c-format +msgid "" +"Time until which the wallet will automatically check for refunds without " +"user interaction." +msgstr "" +"Tiempo hasta el cual la billetera será automáticamente revisada por " +"reembolsos win interación por parte del usuario." + +#: src/paths/instance/orders/create/CreatePage.tsx:502 +#, c-format +msgid "Maximum deposit fee" +msgstr "Máxima tarifa de depósito" + +#: src/paths/instance/orders/create/CreatePage.tsx:503 +#, c-format +msgid "" +"Maximum deposit fees the merchant is willing to cover for this order. Higher " +"deposit fees must be covered in full by the consumer." +msgstr "" +"Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para " +"esta orden. Mayores tarifas de depósito deben ser cubiertas completamente " +"por el consumidor." + +#: src/paths/instance/orders/create/CreatePage.tsx:507 +#, c-format +msgid "Maximum wire fee" +msgstr "Máxima tarifa de transferencia" + +#: src/paths/instance/orders/create/CreatePage.tsx:508 +#, c-format +msgid "" +"Maximum aggregate wire fees the merchant is willing to cover for this order. " +"Wire fees exceeding this amount are to be covered by the customers." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:512 +#, c-format +msgid "Wire fee amortization" +msgstr "Amortización de comisión de transferencia" + +#: src/paths/instance/orders/create/CreatePage.tsx:513 +#, c-format +msgid "" +"Factor by which wire fees exceeding the above threshold are divided to " +"determine the share of excess wire fees to be paid explicitly by the " +"consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:517 +#, fuzzy, c-format +msgid "Create token" +msgstr "Administrar token" + +#: src/paths/instance/orders/create/CreatePage.tsx:518 +#, c-format +msgid "" +"Uncheck this option if the merchant backend generated an order ID with " +"enough entropy to prevent adversarial claims." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:522 +#, fuzzy, c-format +msgid "Minimum age required" +msgstr "Login necesario" + +#: src/paths/instance/orders/create/CreatePage.tsx:523 +#, c-format +msgid "" +"Any value greater than 0 will limit the coins able be used to pay this " +"contract. If empty the age restriction will be defined by the products" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:526 +#, c-format +msgid "Min age defined by the producs is %1$s" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:534 +#, fuzzy, c-format +msgid "Additional information" +msgstr "Información extra" + +#: src/paths/instance/orders/create/CreatePage.tsx:535 +#, c-format +msgid "Custom information to be included in the contract for this order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:541 +#, c-format +msgid "You must enter a value in JavaScript Object Notation (JSON)." +msgstr "" + +#: src/components/picker/DurationPicker.tsx:55 +#, c-format +msgid "days" +msgstr "dÃas" + +#: src/components/picker/DurationPicker.tsx:65 +#, c-format +msgid "hours" +msgstr "horas" + +#: src/components/picker/DurationPicker.tsx:76 +#, c-format +msgid "minutes" +msgstr "minutos" + +#: src/components/picker/DurationPicker.tsx:87 +#, c-format +msgid "seconds" +msgstr "segundos" + +#: src/components/form/InputDuration.tsx:53 +#, fuzzy, c-format +msgid "forever" +msgstr "nunca" + +#: src/components/form/InputDuration.tsx:62 +#, c-format +msgid "%1$sM" +msgstr "" + +#: src/components/form/InputDuration.tsx:64 +#, c-format +msgid "%1$sY" +msgstr "" + +#: src/components/form/InputDuration.tsx:66 +#, c-format +msgid "%1$sd" +msgstr "" + +#: src/components/form/InputDuration.tsx:68 +#, c-format +msgid "%1$sh" +msgstr "" + +#: src/components/form/InputDuration.tsx:70 +#, c-format +msgid "%1$smin" +msgstr "" + +#: src/components/form/InputDuration.tsx:72 +#, c-format +msgid "%1$ssec" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:75 +#, c-format +msgid "Orders" +msgstr "Órdenes" + +#: src/paths/instance/orders/list/Table.tsx:81 +#, fuzzy, c-format +msgid "create order" +msgstr "creado" + +#: src/paths/instance/orders/list/Table.tsx:147 +#, c-format +msgid "load newer orders" +msgstr "cargar nuevas ordenes" + +#: src/paths/instance/orders/list/Table.tsx:154 +#, c-format +msgid "Date" +msgstr "Fecha" + +#: src/paths/instance/orders/list/Table.tsx:200 +#, c-format +msgid "Refund" +msgstr "Devolución" + +#: src/paths/instance/orders/list/Table.tsx:209 +#, c-format +msgid "copy url" +msgstr "copiar url" + +#: src/paths/instance/orders/list/Table.tsx:225 +#, c-format +msgid "load older orders" +msgstr "cargar viejas ordenes" + +#: src/paths/instance/orders/list/Table.tsx:242 +#, c-format +msgid "No orders have been found matching your query!" +msgstr "¡No se encontraron órdenes que emparejen su búsqueda!" + +#: src/paths/instance/orders/list/Table.tsx:288 +#, c-format +msgid "duplicated" +msgstr "duplicado" + +#: src/paths/instance/orders/list/Table.tsx:299 +#, c-format +msgid "invalid format" +msgstr "formato inválido" + +#: src/paths/instance/orders/list/Table.tsx:301 +#, c-format +msgid "this value exceed the refundable amount" +msgstr "este monto excede el monto reembolsable" + +#: src/paths/instance/orders/list/Table.tsx:346 +#, c-format +msgid "date" +msgstr "fecha" + +#: src/paths/instance/orders/list/Table.tsx:349 +#, c-format +msgid "amount" +msgstr "monto" + +#: src/paths/instance/orders/list/Table.tsx:352 +#, c-format +msgid "reason" +msgstr "razón" + +#: src/paths/instance/orders/list/Table.tsx:389 +#, c-format +msgid "amount to be refunded" +msgstr "monto a ser reembolsado" + +#: src/paths/instance/orders/list/Table.tsx:391 +#, c-format +msgid "Max refundable:" +msgstr "Máximo reembolzable:" + +#: src/paths/instance/orders/list/Table.tsx:396 +#, c-format +msgid "Reason" +msgstr "Razón" + +#: src/paths/instance/orders/list/Table.tsx:397 +#, c-format +msgid "Choose one..." +msgstr "Elija uno..." + +#: src/paths/instance/orders/list/Table.tsx:399 +#, c-format +msgid "requested by the customer" +msgstr "pedido por el consumidor" + +#: src/paths/instance/orders/list/Table.tsx:400 +#, c-format +msgid "other" +msgstr "otro" + +#: src/paths/instance/orders/list/Table.tsx:403 +#, c-format +msgid "why this order is being refunded" +msgstr "por qué esta orden está siendo reembolsada" + +#: src/paths/instance/orders/list/Table.tsx:409 +#, c-format +msgid "more information to give context" +msgstr "más información para dar contexto" + +#: src/paths/instance/orders/details/DetailPage.tsx:62 +#, c-format +msgid "Contract Terms" +msgstr "Términos de contrato" + +#: src/paths/instance/orders/details/DetailPage.tsx:68 +#, c-format +msgid "human-readable description of the whole purchase" +msgstr "descripción legible de toda la compra" + +#: src/paths/instance/orders/details/DetailPage.tsx:74 +#, c-format +msgid "total price for the transaction" +msgstr "precio total de la transacción" + +#: src/paths/instance/orders/details/DetailPage.tsx:81 +#, c-format +msgid "URL for this purchase" +msgstr "URL para esta compra" + +#: src/paths/instance/orders/details/DetailPage.tsx:87 +#, c-format +msgid "Max fee" +msgstr "Máxima comisión" + +#: src/paths/instance/orders/details/DetailPage.tsx:88 +#, c-format +msgid "maximum total deposit fee accepted by the merchant for this contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:93 +#, c-format +msgid "Max wire fee" +msgstr "Impuesto de transferencia máximo" + +#: src/paths/instance/orders/details/DetailPage.tsx:94 +#, c-format +msgid "maximum wire fee accepted by the merchant" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:100 +#, c-format +msgid "" +"over how many customer transactions does the merchant expect to amortize " +"wire fees on average" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:105 +#, c-format +msgid "Created at" +msgstr "Creado en" + +#: src/paths/instance/orders/details/DetailPage.tsx:106 +#, c-format +msgid "time when this contract was generated" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:112 +#, c-format +msgid "after this deadline has passed no refunds will be accepted" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:118 +#, c-format +msgid "" +"after this deadline, the merchant won't accept payments for the contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:124 +#, c-format +msgid "transfer deadline for the exchange" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:130 +#, c-format +msgid "time indicating when the order should be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:136 +#, c-format +msgid "where the order will be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:144 +#, fuzzy, c-format +msgid "Auto-refund delay" +msgstr "Plazo de reembolso automático" + +#: src/paths/instance/orders/details/DetailPage.tsx:145 +#, c-format +msgid "" +"how long the wallet should try to get an automatic refund for the purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:150 +#, fuzzy, c-format +msgid "Extra info" +msgstr "Información extra" + +#: src/paths/instance/orders/details/DetailPage.tsx:151 +#, c-format +msgid "extra data that is only interpreted by the merchant frontend" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:219 +#, c-format +msgid "Order" +msgstr "Orden" + +#: src/paths/instance/orders/details/DetailPage.tsx:221 +#, c-format +msgid "claimed" +msgstr "reclamado" + +#: src/paths/instance/orders/details/DetailPage.tsx:247 +#, fuzzy, c-format +msgid "claimed at" +msgstr "reclamado" + +#: src/paths/instance/orders/details/DetailPage.tsx:265 +#, c-format +msgid "Timeline" +msgstr "CronologÃa" + +#: src/paths/instance/orders/details/DetailPage.tsx:271 +#, c-format +msgid "Payment details" +msgstr "Detalles de pago" + +#: src/paths/instance/orders/details/DetailPage.tsx:291 +#, c-format +msgid "Order status" +msgstr "Estado de orden" + +#: src/paths/instance/orders/details/DetailPage.tsx:301 +#, c-format +msgid "Product list" +msgstr "Lista de producto" + +#: src/paths/instance/orders/details/DetailPage.tsx:451 +#, c-format +msgid "paid" +msgstr "pagados" + +#: src/paths/instance/orders/details/DetailPage.tsx:455 +#, c-format +msgid "wired" +msgstr "transferido" + +#: src/paths/instance/orders/details/DetailPage.tsx:460 +#, c-format +msgid "refunded" +msgstr "reembolzado" + +#: src/paths/instance/orders/details/DetailPage.tsx:480 +#, fuzzy, c-format +msgid "refund order" +msgstr "reembolzado" + +#: src/paths/instance/orders/details/DetailPage.tsx:481 +#, fuzzy, c-format +msgid "not refundable" +msgstr "Máximo reembolzable:" + +#: src/paths/instance/orders/details/DetailPage.tsx:489 +#, c-format +msgid "refund" +msgstr "reembolzar" + +#: src/paths/instance/orders/details/DetailPage.tsx:553 +#, c-format +msgid "Refunded amount" +msgstr "Monto reembolzado" + +#: src/paths/instance/orders/details/DetailPage.tsx:560 +#, fuzzy, c-format +msgid "Refund taken" +msgstr "Reembolzado" + +#: src/paths/instance/orders/details/DetailPage.tsx:570 +#, fuzzy, c-format +msgid "Status URL" +msgstr "URL de estado de orden" + +#: src/paths/instance/orders/details/DetailPage.tsx:583 +#, fuzzy, c-format +msgid "Refund URI" +msgstr "Devolución" + +#: src/paths/instance/orders/details/DetailPage.tsx:636 +#, c-format +msgid "unpaid" +msgstr "impago" + +#: src/paths/instance/orders/details/DetailPage.tsx:654 +#, c-format +msgid "pay at" +msgstr "pagar en" + +#: src/paths/instance/orders/details/DetailPage.tsx:666 +#, c-format +msgid "created at" +msgstr "creado" + +#: src/paths/instance/orders/details/DetailPage.tsx:707 +#, c-format +msgid "Order status URL" +msgstr "URL de estado de orden" + +#: src/paths/instance/orders/details/DetailPage.tsx:711 +#, fuzzy, c-format +msgid "Payment URI" +msgstr "URI de pago" + +#: src/paths/instance/orders/details/DetailPage.tsx:740 +#, c-format +msgid "" +"Unknown order status. This is an error, please contact the administrator." +msgstr "" +"Estado de orden desconocido. Esto es un error, por favor contacte a su " +"administrador." + +#: src/paths/instance/orders/details/DetailPage.tsx:767 +#, c-format +msgid "Back" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:79 +#, c-format +msgid "refund created successfully" +msgstr "reembolzo creado satisfactoriamente" + +#: src/paths/instance/orders/details/index.tsx:85 +#, c-format +msgid "could not create the refund" +msgstr "No se pudo create el reembolso" + +#: src/paths/instance/orders/list/ListPage.tsx:78 +#, c-format +msgid "select date to show nearby orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:94 +#, fuzzy, c-format +msgid "order id" +msgstr "ir a id de orden" + +#: src/paths/instance/orders/list/ListPage.tsx:100 +#, c-format +msgid "jump to order with the given order ID" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:122 +#, c-format +msgid "remove all filters" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:132 +#, c-format +msgid "only show paid orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:135 +#, c-format +msgid "Paid" +msgstr "Pagado" + +#: src/paths/instance/orders/list/ListPage.tsx:142 +#, fuzzy, c-format +msgid "only show orders with refunds" +msgstr "No se pudo create el reembolso" + +#: src/paths/instance/orders/list/ListPage.tsx:145 +#, c-format +msgid "Refunded" +msgstr "Reembolsado" + +#: src/paths/instance/orders/list/ListPage.tsx:152 +#, c-format +msgid "" +"only show orders where customers paid, but wire payments from payment " +"provider are still pending" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:155 +#, c-format +msgid "Not wired" +msgstr "No transferido" + +#: src/paths/instance/orders/list/ListPage.tsx:170 +#, c-format +msgid "clear date filter" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:184 +#, c-format +msgid "date (YYYY/MM/DD)" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:103 +#, fuzzy, c-format +msgid "Enter an order id" +msgstr "ir a id de orden" + +#: src/paths/instance/orders/list/index.tsx:111 +#, fuzzy, c-format +msgid "order not found" +msgstr "Servidor no encontrado" + +#: src/paths/instance/orders/list/index.tsx:178 +#, fuzzy, c-format +msgid "could not get the order to refund" +msgstr "No se pudo create el reembolso" + +#: src/components/exception/AsyncButton.tsx:43 +#, fuzzy, c-format +msgid "Loading..." +msgstr "Cargando..." + +#: src/components/form/InputStock.tsx:99 +#, c-format +msgid "" +"click here to configure the stock of the product, leave it as is and the " +"backend will not control stock" +msgstr "" + +#: src/components/form/InputStock.tsx:109 +#, c-format +msgid "Manage stock" +msgstr "Administrar stock" + +#: src/components/form/InputStock.tsx:115 +#, c-format +msgid "this product has been configured without stock control" +msgstr "" + +#: src/components/form/InputStock.tsx:119 +#, c-format +msgid "Infinite" +msgstr "Inifinito" + +#: src/components/form/InputStock.tsx:136 +#, fuzzy, c-format +msgid "lost cannot be greater than current and incoming (max %1$s)" +msgstr "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )" + +#: src/components/form/InputStock.tsx:176 +#, c-format +msgid "Incoming" +msgstr "Ingresando" + +#: src/components/form/InputStock.tsx:177 +#, c-format +msgid "Lost" +msgstr "Perdido" + +#: src/components/form/InputStock.tsx:192 +#, c-format +msgid "Current" +msgstr "Actual" + +#: src/components/form/InputStock.tsx:196 +#, c-format +msgid "remove stock control for this product" +msgstr "" + +#: src/components/form/InputStock.tsx:202 +#, c-format +msgid "without stock" +msgstr "sin stock" + +#: src/components/form/InputStock.tsx:211 +#, c-format +msgid "Next restock" +msgstr "Próximo reabastecimiento" + +#: src/components/form/InputStock.tsx:217 +#, c-format +msgid "Delivery address" +msgstr "Dirección de entrega" + +#: src/components/product/ProductForm.tsx:133 +#, c-format +msgid "product identification to use in URLs (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:139 +#, c-format +msgid "illustration of the product for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:145 +#, c-format +msgid "product description for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:149 +#, c-format +msgid "Age restricted" +msgstr "" + +#: src/components/product/ProductForm.tsx:150 +#, c-format +msgid "is this product restricted for customer below certain age?" +msgstr "" + +#: src/components/product/ProductForm.tsx:155 +#, c-format +msgid "" +"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 " +"items, 5 meters) for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:160 +#, c-format +msgid "" +"sale price for customers, including taxes, for above units of the product" +msgstr "" + +#: src/components/product/ProductForm.tsx:164 +#, c-format +msgid "Stock" +msgstr "Existencias" + +#: src/components/product/ProductForm.tsx:166 +#, c-format +msgid "" +"product inventory for products with finite supply (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:171 +#, c-format +msgid "taxes included in the product price, exposed to customers" +msgstr "" + +#: src/paths/instance/products/create/CreatePage.tsx:66 +#, c-format +msgid "Need to complete marked fields" +msgstr "" + +#: src/paths/instance/products/create/index.tsx:51 +#, c-format +msgid "could not create product" +msgstr "no se pudo crear el producto" + +#: src/paths/instance/products/list/Table.tsx:68 +#, c-format +msgid "Products" +msgstr "Productos" + +#: src/paths/instance/products/list/Table.tsx:73 +#, c-format +msgid "add product to inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:137 +#, c-format +msgid "Sell" +msgstr "Venta" + +#: src/paths/instance/products/list/Table.tsx:143 +#, c-format +msgid "Profit" +msgstr "Ganancia" + +#: src/paths/instance/products/list/Table.tsx:149 +#, c-format +msgid "Sold" +msgstr "Vendido" + +#: src/paths/instance/products/list/Table.tsx:210 +#, c-format +msgid "free" +msgstr "Gratis" + +#: src/paths/instance/products/list/Table.tsx:248 +#, fuzzy, c-format +msgid "go to product update page" +msgstr "producto actualizado correctamente" + +#: src/paths/instance/products/list/Table.tsx:255 +#, c-format +msgid "Update" +msgstr "Actualizar" + +#: src/paths/instance/products/list/Table.tsx:260 +#, c-format +msgid "remove this product from the database" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:331 +#, c-format +msgid "update the product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:341 +#, c-format +msgid "update product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:399 +#, c-format +msgid "add more elements to the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:404 +#, c-format +msgid "report elements lost in the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:409 +#, fuzzy, c-format +msgid "new price for the product" +msgstr "no se pudo actualizar el producto" + +#: src/paths/instance/products/list/Table.tsx:421 +#, c-format +msgid "the are value with errors" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:422 +#, c-format +msgid "update product with new stock and price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:463 +#, fuzzy, c-format +msgid "There is no products yet, add more pressing the + sign" +msgstr "No hay propinas todavÃa, agregar mas presionando el signo +" + +#: src/paths/instance/products/list/index.tsx:86 +#, c-format +msgid "product updated successfully" +msgstr "producto actualizado correctamente" + +#: src/paths/instance/products/list/index.tsx:92 +#, c-format +msgid "could not update the product" +msgstr "no se pudo actualizar el producto" + +#: src/paths/instance/products/list/index.tsx:103 +#, c-format +msgid "product delete successfully" +msgstr "producto fue eliminado correctamente" + +#: src/paths/instance/products/list/index.tsx:109 +#, c-format +msgid "could not delete the product" +msgstr "no se pudo eliminar el producto" + +#: src/paths/instance/products/update/UpdatePage.tsx:56 +#, fuzzy, c-format +msgid "Product id:" +msgstr "Id de producto" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95 +#, c-format +msgid "" +"To complete the setup of the reserve, you must now initiate a wire transfer " +"using the given wire transfer subject and crediting the specified amount to " +"the indicated account of the exchange." +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102 +#, c-format +msgid "If your system supports RFC 8905, you can do this by opening this URI:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:83 +#, fuzzy, c-format +msgid "it should be greater than 0" +msgstr "Debe ser mayor a 0" + +#: src/paths/instance/reserves/create/CreatePage.tsx:88 +#, c-format +msgid "must be a valid URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:107 +#, fuzzy, c-format +msgid "Initial balance" +msgstr "Instancia" + +#: src/paths/instance/reserves/create/CreatePage.tsx:108 +#, c-format +msgid "balance prior to deposit" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:112 +#, c-format +msgid "Exchange URL" +msgstr "URL del Exchange" + +#: src/paths/instance/reserves/create/CreatePage.tsx:113 +#, c-format +msgid "URL of exchange" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:148 +#, c-format +msgid "Next" +msgstr "Siguiente" + +#: src/paths/instance/reserves/create/CreatePage.tsx:186 +#, c-format +msgid "Wire method" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:187 +#, fuzzy, c-format +msgid "method to use for wire transfer" +msgstr "no se pudo informar la transferencia" + +#: src/paths/instance/reserves/create/CreatePage.tsx:189 +#, c-format +msgid "Select one wire method" +msgstr "" + +#: src/paths/instance/reserves/create/index.tsx:62 +#, fuzzy, c-format +msgid "could not create reserve" +msgstr "No se pudo create el reembolso" + +#: src/paths/instance/reserves/details/DetailPage.tsx:77 +#, c-format +msgid "Valid until" +msgstr "Válido hasta" + +#: src/paths/instance/reserves/details/DetailPage.tsx:82 +#, fuzzy, c-format +msgid "Created balance" +msgstr "creado" + +#: src/paths/instance/reserves/details/DetailPage.tsx:99 +#, fuzzy, c-format +msgid "Exchange balance" +msgstr "Monto inicial" + +#: src/paths/instance/reserves/details/DetailPage.tsx:104 +#, c-format +msgid "Picked up" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:109 +#, fuzzy, c-format +msgid "Committed" +msgstr "Monto confirmado" + +#: src/paths/instance/reserves/details/DetailPage.tsx:116 +#, c-format +msgid "Account address" +msgstr "Dirección de cuenta" + +#: src/paths/instance/reserves/details/DetailPage.tsx:119 +#, c-format +msgid "Subject" +msgstr "Asunto" + +#: src/paths/instance/reserves/details/DetailPage.tsx:130 +#, c-format +msgid "Tips" +msgstr "Propinas" + +#: src/paths/instance/reserves/details/DetailPage.tsx:193 +#, c-format +msgid "No tips has been authorized from this reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:213 +#, fuzzy, c-format +msgid "Authorized" +msgstr "Token de autorización" + +#: src/paths/instance/reserves/details/DetailPage.tsx:222 +#, fuzzy, c-format +msgid "Expiration" +msgstr "Información extra" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108 +#, fuzzy, c-format +msgid "amount of tip" +msgstr "monto" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112 +#, fuzzy, c-format +msgid "Justification" +msgstr "Jurisdicción" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114 +#, c-format +msgid "reason for the tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118 +#, c-format +msgid "URL after tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119 +#, c-format +msgid "URL to visit after tip payment" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:65 +#, fuzzy, c-format +msgid "Reserves not yet funded" +msgstr "Servidor no encontrado" + +#: src/paths/instance/reserves/list/Table.tsx:89 +#, c-format +msgid "Reserves ready" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:95 +#, fuzzy, c-format +msgid "add new reserve" +msgstr "cargar nuevas transferencias" + +#: src/paths/instance/reserves/list/Table.tsx:143 +#, c-format +msgid "Expires at" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:146 +#, c-format +msgid "Initial" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:202 +#, c-format +msgid "delete selected reserve from the database" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:210 +#, c-format +msgid "authorize new tip from selected reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:237 +#, fuzzy, c-format +msgid "" +"There is no ready reserves yet, add more pressing the + sign or fund them" +msgstr "No hay transferencias todavÃa, agregar mas presionando el signo +" + +#: src/paths/instance/reserves/list/Table.tsx:264 +#, fuzzy, c-format +msgid "Expected Balance" +msgstr "Ejecutado en" + +#: src/paths/instance/reserves/list/index.tsx:110 +#, fuzzy, c-format +msgid "could not create the tip" +msgstr "No se pudo create el reembolso" + +#: src/paths/instance/templates/create/CreatePage.tsx:77 +#, fuzzy, c-format +msgid "should not be empty" +msgstr "no puede ser vacÃo" + +#: src/paths/instance/templates/create/CreatePage.tsx:93 +#, fuzzy, c-format +msgid "should be greater that 0" +msgstr "Debe ser mayor a 0" + +#: src/paths/instance/templates/create/CreatePage.tsx:96 +#, fuzzy, c-format +msgid "can't be empty" +msgstr "no puede ser vacÃo" + +#: src/paths/instance/templates/create/CreatePage.tsx:100 +#, c-format +msgid "to short" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:108 +#, c-format +msgid "just letters and numbers from 2 to 7" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:110 +#, c-format +msgid "size of the key should be 32" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:137 +#, c-format +msgid "Identifier" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:138 +#, c-format +msgid "Name of the template in URLs." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:144 +#, c-format +msgid "Describe what this template stands for" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:149 +#, fuzzy, c-format +msgid "Fixed summary" +msgstr "Estado de orden" + +#: src/paths/instance/templates/create/CreatePage.tsx:150 +#, c-format +msgid "If specified, this template will create order with the same summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:154 +#, fuzzy, c-format +msgid "Fixed price" +msgstr "precio unitario" + +#: src/paths/instance/templates/create/CreatePage.tsx:155 +#, c-format +msgid "If specified, this template will create order with the same price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:159 +#, c-format +msgid "Minimum age" +msgstr "Edad mÃnima" + +#: src/paths/instance/templates/create/CreatePage.tsx:161 +#, c-format +msgid "Is this contract restricted to some age?" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:165 +#, fuzzy, c-format +msgid "Payment timeout" +msgstr "Opciones de pago" + +#: src/paths/instance/templates/create/CreatePage.tsx:167 +#, c-format +msgid "" +"How much time has the customer to complete the payment once the order was " +"created." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:171 +#, c-format +msgid "Verification algorithm" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:172 +#, c-format +msgid "Algorithm to use to verify transaction in offline mode" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:180 +#, c-format +msgid "Point-of-sale key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:182 +#, c-format +msgid "Useful to validate the purchase" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:196 +#, c-format +msgid "generate random secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:203 +#, c-format +msgid "random" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:208 +#, c-format +msgid "show secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:209 +#, c-format +msgid "hide secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:216 +#, c-format +msgid "hide" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:218 +#, c-format +msgid "show" +msgstr "" + +#: src/paths/instance/templates/create/index.tsx:52 +#, fuzzy, c-format +msgid "could not inform template" +msgstr "no se pudo informar la transferencia" + +#: src/paths/instance/templates/use/UsePage.tsx:54 +#, fuzzy, c-format +msgid "Amount is required" +msgstr "Login necesario" + +#: src/paths/instance/templates/use/UsePage.tsx:58 +#, c-format +msgid "Order summary is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:86 +#, fuzzy, c-format +msgid "New order for template" +msgstr "cargar viejas transferencias" + +#: src/paths/instance/templates/use/UsePage.tsx:108 +#, c-format +msgid "Amount of the order" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:113 +#, fuzzy, c-format +msgid "Order summary" +msgstr "Estado de orden" + +#: src/paths/instance/templates/use/index.tsx:92 +#, fuzzy, c-format +msgid "could not create order from template" +msgstr "No se pudo create el reembolso" + +#: src/paths/instance/templates/qr/QrPage.tsx:131 +#, c-format +msgid "" +"Here you can specify a default value for fields that are not fixed. Default " +"values can be edited by the customer before the payment." +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:148 +#, fuzzy, c-format +msgid "Fixed amount" +msgstr "Monto reembolzado" + +#: src/paths/instance/templates/qr/QrPage.tsx:149 +#, fuzzy, c-format +msgid "Default amount" +msgstr "Monto reembolzado" + +#: src/paths/instance/templates/qr/QrPage.tsx:161 +#, fuzzy, c-format +msgid "Default summary" +msgstr "Estado de orden" + +#: src/paths/instance/templates/qr/QrPage.tsx:177 +#, c-format +msgid "Print" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:184 +#, c-format +msgid "Setup TOTP" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:65 +#, c-format +msgid "Templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:70 +#, c-format +msgid "add new templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:142 +#, c-format +msgid "load more templates before the first one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:146 +#, fuzzy, c-format +msgid "load newer templates" +msgstr "cargar nuevas transferencias" + +#: src/paths/instance/templates/list/Table.tsx:181 +#, c-format +msgid "delete selected templates from the database" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:188 +#, c-format +msgid "use template to create new order" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:195 +#, fuzzy, c-format +msgid "create qr code for the template" +msgstr "No se pudo create el reembolso" + +#: src/paths/instance/templates/list/Table.tsx:210 +#, c-format +msgid "load more templates after the last one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:214 +#, fuzzy, c-format +msgid "load older templates" +msgstr "cargar viejas transferencias" + +#: src/paths/instance/templates/list/Table.tsx:231 +#, fuzzy, c-format +msgid "There is no templates yet, add more pressing the + sign" +msgstr "No hay propinas todavÃa, agregar mas presionando el signo +" + +#: src/paths/instance/templates/list/index.tsx:104 +#, fuzzy, c-format +msgid "template delete successfully" +msgstr "producto fue eliminado correctamente" + +#: src/paths/instance/templates/list/index.tsx:110 +#, fuzzy, c-format +msgid "could not delete the template" +msgstr "no se pudo eliminar el producto" + +#: src/paths/instance/templates/update/index.tsx:90 +#, fuzzy, c-format +msgid "could not update template" +msgstr "no se pudo actualizar el producto" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:57 +#, fuzzy, c-format +msgid "should be one of '%1$s'" +msgstr "deberÃan ser iguales" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:85 +#, c-format +msgid "Webhook ID to use" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:89 +#, c-format +msgid "Event" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:90 +#, c-format +msgid "The event of the webhook: why the webhook is used" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:94 +#, c-format +msgid "Method" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:95 +#, c-format +msgid "Method used by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:99 +#, c-format +msgid "URL" +msgstr "URL" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:100 +#, c-format +msgid "URL of the webhook where the customer will be redirected" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:104 +#, c-format +msgid "Header" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:106 +#, c-format +msgid "Header template of the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:111 +#, c-format +msgid "Body" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:112 +#, c-format +msgid "Body template by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:61 +#, c-format +msgid "Webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:66 +#, c-format +msgid "add new webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:137 +#, c-format +msgid "load more webhooks before the first one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:141 +#, fuzzy, c-format +msgid "load newer webhooks" +msgstr "cargar nuevas ordenes" + +#: src/paths/instance/webhooks/list/Table.tsx:151 +#, c-format +msgid "Event type" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:176 +#, c-format +msgid "delete selected webhook from the database" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:198 +#, c-format +msgid "load more webhooks after the last one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:202 +#, fuzzy, c-format +msgid "load older webhooks" +msgstr "cargar viejas ordenes" + +#: src/paths/instance/webhooks/list/Table.tsx:219 +#, fuzzy, c-format +msgid "There is no webhooks yet, add more pressing the + sign" +msgstr "No hay propinas todavÃa, agregar mas presionando el signo +" + +#: src/paths/instance/webhooks/list/index.tsx:94 +#, fuzzy, c-format +msgid "webhook delete successfully" +msgstr "producto fue eliminado correctamente" + +#: src/paths/instance/webhooks/list/index.tsx:100 +#, fuzzy, c-format +msgid "could not delete the webhook" +msgstr "no se pudo eliminar el producto" + +#: src/paths/instance/transfers/create/CreatePage.tsx:63 +#, fuzzy, c-format +msgid "check the id, does not look valid" +msgstr "verificar el id, no parece válido" + +#: src/paths/instance/transfers/create/CreatePage.tsx:65 +#, c-format +msgid "should have 52 characters, current %1$s" +msgstr "deberÃa tener 52 caracteres, actualmente %1$s" + +#: src/paths/instance/transfers/create/CreatePage.tsx:72 +#, c-format +msgid "URL doesn't have the right format" +msgstr "La URL no tiene el formato correcto" + +#: src/paths/instance/transfers/create/CreatePage.tsx:98 +#, c-format +msgid "Credited bank account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:100 +#, c-format +msgid "Select one account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:101 +#, c-format +msgid "Bank account of the merchant where the payment was received" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:105 +#, fuzzy, c-format +msgid "Wire transfer ID" +msgstr "Id de transferencia" + +#: src/paths/instance/transfers/create/CreatePage.tsx:107 +#, c-format +msgid "" +"unique identifier of the wire transfer used by the exchange, must be 52 " +"characters long" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:112 +#, c-format +msgid "" +"Base URL of the exchange that made the transfer, should have been in the " +"wire transfer subject" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:117 +#, c-format +msgid "Amount credited" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:118 +#, c-format +msgid "Actual amount that was wired to the merchant's bank account" +msgstr "" + +#: src/paths/instance/transfers/create/index.tsx:58 +#, c-format +msgid "could not inform transfer" +msgstr "no se pudo informar la transferencia" + +#: src/paths/instance/transfers/list/Table.tsx:61 +#, c-format +msgid "Transfers" +msgstr "Transferencias" + +#: src/paths/instance/transfers/list/Table.tsx:66 +#, fuzzy, c-format +msgid "add new transfer" +msgstr "cargar nuevas transferencias" + +#: src/paths/instance/transfers/list/Table.tsx:129 +#, c-format +msgid "load more transfers before the first one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:133 +#, c-format +msgid "load newer transfers" +msgstr "cargar nuevas transferencias" + +#: src/paths/instance/transfers/list/Table.tsx:143 +#, c-format +msgid "Credit" +msgstr "Crédito" + +#: src/paths/instance/transfers/list/Table.tsx:152 +#, c-format +msgid "Confirmed" +msgstr "Confirmado" + +#: src/paths/instance/transfers/list/Table.tsx:155 +#, c-format +msgid "Verified" +msgstr "Verificado" + +#: src/paths/instance/transfers/list/Table.tsx:158 +#, c-format +msgid "Executed at" +msgstr "Ejecutado en" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "yes" +msgstr "si" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "no" +msgstr "no" + +#: src/paths/instance/transfers/list/Table.tsx:181 +#, c-format +msgid "unknown" +msgstr "desconocido" + +#: src/paths/instance/transfers/list/Table.tsx:187 +#, c-format +msgid "delete selected transfer from the database" +msgstr "eliminar transferencia seleccionada de la base de datos" + +#: src/paths/instance/transfers/list/Table.tsx:202 +#, c-format +msgid "load more transfer after the last one" +msgstr "cargue más transferencia luego de la última" + +#: src/paths/instance/transfers/list/Table.tsx:206 +#, c-format +msgid "load older transfers" +msgstr "cargar viejas transferencias" + +#: src/paths/instance/transfers/list/Table.tsx:223 +#, c-format +msgid "There is no transfer yet, add more pressing the + sign" +msgstr "No hay transferencias todavÃa, agregar mas presionando el signo +" + +#: src/paths/instance/transfers/list/ListPage.tsx:79 +#, fuzzy, c-format +msgid "filter by account address" +msgstr "Dirección de cuenta" + +#: src/paths/instance/transfers/list/ListPage.tsx:100 +#, c-format +msgid "only show wire transfers confirmed by the merchant" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:110 +#, c-format +msgid "only show wire transfers claimed by the exchange" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:113 +#, fuzzy, c-format +msgid "Unverified" +msgstr "Verificado" + +#: src/paths/admin/create/CreatePage.tsx:69 +#, c-format +msgid "is not valid" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:94 +#, fuzzy, c-format +msgid "is not a number" +msgstr "Número de edificio" + +#: src/paths/admin/create/CreatePage.tsx:96 +#, c-format +msgid "must be 1 or greater" +msgstr "debe ser 1 o mayor" + +#: src/paths/admin/create/CreatePage.tsx:107 +#, c-format +msgid "max 7 lines" +msgstr "máximo 7 lÃneas" + +#: src/paths/admin/create/CreatePage.tsx:178 +#, c-format +msgid "change authorization configuration" +msgstr "cambiar configuración de autorización" + +#: src/paths/admin/create/CreatePage.tsx:217 +#, c-format +msgid "Need to complete marked fields and choose authorization method" +msgstr "Necesita completar campos marcados y escoger un método de autorización" + +#: src/components/form/InputPaytoForm.tsx:82 +#, c-format +msgid "This is not a valid bitcoin address." +msgstr "Esta no es una dirección de bitcoin válida." + +#: src/components/form/InputPaytoForm.tsx:95 +#, c-format +msgid "This is not a valid Ethereum address." +msgstr "Esta no es una dirección de Ethereum válida." + +#: src/components/form/InputPaytoForm.tsx:118 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "Números IBAN usualmente tienen más de 4 dÃgitos" + +#: src/components/form/InputPaytoForm.tsx:120 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "Número IBAN usualmente tienen menos de 34 dÃgitos" + +#: src/components/form/InputPaytoForm.tsx:128 +#, c-format +msgid "IBAN country code not found" +msgstr "Código IBAN de paÃs no encontrado" + +#: src/components/form/InputPaytoForm.tsx:153 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "Número IBAN no es válido, la suma de verificación es incorrecta" + +#: src/components/form/InputPaytoForm.tsx:248 +#, c-format +msgid "Target type" +msgstr "Tipo objetivo" + +#: src/components/form/InputPaytoForm.tsx:249 +#, c-format +msgid "Method to use for wire transfer" +msgstr "Método a usar para la transferencia" + +#: src/components/form/InputPaytoForm.tsx:258 +#, c-format +msgid "Routing" +msgstr "Enrutamiento" + +#: src/components/form/InputPaytoForm.tsx:259 +#, c-format +msgid "Routing number." +msgstr "Número de enrutamiento." + +#: src/components/form/InputPaytoForm.tsx:263 +#, c-format +msgid "Account" +msgstr "Cuenta" + +#: src/components/form/InputPaytoForm.tsx:264 +#, fuzzy, c-format +msgid "Account number." +msgstr "Dirección de cuenta" + +#: src/components/form/InputPaytoForm.tsx:273 +#, c-format +msgid "Business Identifier Code." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:282 +#, c-format +msgid "Bank Account Number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:292 +#, c-format +msgid "Unified Payment Interface." +msgstr "Interfaz de pago unificado." + +#: src/components/form/InputPaytoForm.tsx:301 +#, c-format +msgid "Bitcoin protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:310 +#, c-format +msgid "Ethereum protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:319 +#, c-format +msgid "Interledger protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:328 +#, c-format +msgid "Host" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:329 +#, c-format +msgid "Bank host." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:334 +#, c-format +msgid "Bank account." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:343 +#, c-format +msgid "Bank account owner's name." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:370 +#, c-format +msgid "No accounts yet." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:52 +#, c-format +msgid "" +"Name of the instance in URLs. The 'default' instance is special in that it " +"is used to administer other instances." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:58 +#, fuzzy, c-format +msgid "Business name" +msgstr "Nombre de edificio" + +#: src/components/instance/DefaultInstanceFormFields.tsx:59 +#, c-format +msgid "Legal name of the business represented by this instance." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:64 +#, c-format +msgid "Email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:65 +#, c-format +msgid "Contact email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:70 +#, c-format +msgid "Website URL" +msgstr "URL de sitio web" + +#: src/components/instance/DefaultInstanceFormFields.tsx:71 +#, c-format +msgid "URL." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:76 +#, c-format +msgid "Logo" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:77 +#, c-format +msgid "Logo image." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:82 +#, c-format +msgid "Bank account" +msgstr "Cuenta bancaria" + +#: src/components/instance/DefaultInstanceFormFields.tsx:83 +#, c-format +msgid "URI specifying bank account for crediting revenue." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:88 +#, c-format +msgid "Default max deposit fee" +msgstr "Impuesto máximo de deposito por omisión" + +#: src/components/instance/DefaultInstanceFormFields.tsx:89 +#, c-format +msgid "" +"Maximum deposit fees this merchant is willing to pay per order by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:94 +#, c-format +msgid "Default max wire fee" +msgstr "Impuesto máximo de transferencia por omisión" + +#: src/components/instance/DefaultInstanceFormFields.tsx:95 +#, c-format +msgid "" +"Maximum wire fees this merchant is willing to pay per wire transfer by " +"default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:100 +#, c-format +msgid "Default wire fee amortization" +msgstr "Amortización de impuesto de transferencia por omisión" + +#: src/components/instance/DefaultInstanceFormFields.tsx:101 +#, c-format +msgid "" +"Number of orders excess wire transfer fees will be divided by to compute per " +"order surcharge." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:107 +#, c-format +msgid "Physical location of the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:114 +#, c-format +msgid "Jurisdiction" +msgstr "Jurisdicción" + +#: src/components/instance/DefaultInstanceFormFields.tsx:115 +#, c-format +msgid "Jurisdiction for legal disputes with the merchant." +msgstr "Jurisdicción para disputas legales con el comerciante." + +#: src/components/instance/DefaultInstanceFormFields.tsx:122 +#, fuzzy, c-format +msgid "Default payment delay" +msgstr "Retrazo de pago por omisión" + +#: src/components/instance/DefaultInstanceFormFields.tsx:124 +#, c-format +msgid "" +"Time customers have to pay an order before the offer expires by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:129 +#, c-format +msgid "Default wire transfer delay" +msgstr "Retrazo de transferencia por omisión" + +#: src/components/instance/DefaultInstanceFormFields.tsx:130 +#, c-format +msgid "" +"Maximum time an exchange is allowed to delay wiring funds to the merchant, " +"enabling it to aggregate smaller payments into larger wire transfers and " +"reducing wire fees." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:164 +#, c-format +msgid "Instance id" +msgstr "ID de instancia" + +#: src/paths/instance/update/UpdatePage.tsx:173 +#, fuzzy, c-format +msgid "Change the authorization method use for this instance." +msgstr "" +"Limpiar el token de autorización significa acceso público a la instancia" + +#: src/paths/instance/update/UpdatePage.tsx:182 +#, c-format +msgid "Manage access token" +msgstr "Administrar token de acceso" + +#: src/paths/instance/update/index.tsx:112 +#, c-format +msgid "Failed to create instance" +msgstr "Fallo al crear la instancia" + +#: src/components/exception/login.tsx:74 +#, c-format +msgid "Login required" +msgstr "Login necesario" + +#: src/components/exception/login.tsx:80 +#, c-format +msgid "Please enter your access token." +msgstr "" + +#: src/components/exception/login.tsx:108 +#, fuzzy, c-format +msgid "Access Token" +msgstr "Acceso denegado" + +#: src/InstanceRoutes.tsx:171 +#, c-format +msgid "The request to the backend take too long and was cancelled" +msgstr "" + +#: src/InstanceRoutes.tsx:172 +#, c-format +msgid "Diagnostic from %1$s is \"%2$s\"" +msgstr "" + +#: src/InstanceRoutes.tsx:178 +#, fuzzy, c-format +msgid "The backend reported a problem: HTTP status #%1$s" +msgstr "Servidir reporto un problema: HTTP status #%1$s" + +#: src/InstanceRoutes.tsx:179 +#, c-format +msgid "Diagnostic from %1$s is '%2$s'" +msgstr "" + +#: src/InstanceRoutes.tsx:196 +#, c-format +msgid "Access denied" +msgstr "Acceso denegado" + +#: src/InstanceRoutes.tsx:197 +#, c-format +msgid "The access token provided is invalid." +msgstr "" + +#: src/InstanceRoutes.tsx:212 +#, fuzzy, c-format +msgid "No 'default' instance configured yet." +msgstr "Sin instancia default" + +#: src/InstanceRoutes.tsx:213 +#, c-format +msgid "Create a 'default' instance to begin using the merchant backoffice." +msgstr "" + +#: src/InstanceRoutes.tsx:630 +#, c-format +msgid "The access token provided is invalid" +msgstr "" + +#: src/InstanceRoutes.tsx:664 +#, c-format +msgid "Hide for today" +msgstr "" + +#: src/components/menu/SideBar.tsx:82 +#, c-format +msgid "Instance" +msgstr "Instancia" + +#: src/components/menu/SideBar.tsx:91 +#, c-format +msgid "Settings" +msgstr "Configuración" + +#: src/components/menu/SideBar.tsx:167 +#, c-format +msgid "Connection" +msgstr "Conexión" + +#: src/components/menu/SideBar.tsx:209 +#, c-format +msgid "New" +msgstr "Nuevo" + +#: src/components/menu/SideBar.tsx:219 +#, c-format +msgid "List" +msgstr "Lista" + +#: src/components/menu/SideBar.tsx:234 +#, c-format +msgid "Log out" +msgstr "Salir" + +#: src/ApplicationReadyRoutes.tsx:71 +#, c-format +msgid "Check your token is valid" +msgstr "Verifica que el token sea valido" + +#: src/ApplicationReadyRoutes.tsx:90 +#, c-format +msgid "Couldn't access the server." +msgstr "No se pudo acceder al servidor." + +#: src/ApplicationReadyRoutes.tsx:91 +#, c-format +msgid "Could not infer instance id from url %1$s" +msgstr "No se pudo inferir el id de la instancia con la url %1$s" + +#: src/Application.tsx:104 +#, c-format +msgid "Server not found" +msgstr "Servidor no encontrado" + +#: src/Application.tsx:118 +#, c-format +msgid "Server response with an error code" +msgstr "" + +#: src/Application.tsx:120 +#, c-format +msgid "Got message %1$s from %2$s" +msgstr "Recibimos el mensaje %1$s desde %2$s" + +#: src/Application.tsx:131 +#, c-format +msgid "Response from server is unreadable, http status: %1$s" +msgstr "" + +#: src/Application.tsx:144 +#, c-format +msgid "Unexpected Error" +msgstr "Error inesperado" + +#: src/components/form/InputArray.tsx:101 +#, c-format +msgid "The value %1$s is invalid for a payment url" +msgstr "El valor %1$s es invalido para una URL de pago" + +#: src/components/form/InputArray.tsx:110 +#, c-format +msgid "add element to the list" +msgstr "agregar elemento a la lista" + +#: src/components/form/InputArray.tsx:112 +#, c-format +msgid "add" +msgstr "Agregar" + +#: src/components/form/InputSecured.tsx:37 +#, c-format +msgid "Deleting" +msgstr "Borrando" + +#: src/components/form/InputSecured.tsx:41 +#, c-format +msgid "Changing" +msgstr "Cambiando" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87 +#, c-format +msgid "Order ID" +msgstr "ID de pedido" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101 +#, c-format +msgid "Payment URL" +msgstr "URL de pago" + +#, c-format +#~ msgid "Couldn't access the server" +#~ msgstr "No se pudo aceder al servidor" + +#, c-format +#~ msgid "HTTP status #%1$s: Server reported a problem" +#~ msgstr "HTTP status #%1$s: Servidor reporto un problema" + +#, c-format +#~ msgid "Got message: \"%1$s\" from: %2$s" +#~ msgstr "Recibimos el mensaje: %1$s desde %2$s" + +#, c-format +#~ msgid "" +#~ "in order to use merchant backoffice, you should create the default " +#~ "instance" +#~ msgstr "" +#~ "para usar el merchant backoffice, deberÃa crear la instancia default" + +#, c-format +#~ msgid "Got message: %1$s from: %2$s" +#~ msgstr "Recibimos el mensaje %1$s desde %2$s" + +#, c-format +#~ msgid "" +#~ "Please enter your auth token. Token should have \"secret-token:\" and " +#~ "start with Bearer or ApiKey" +#~ msgstr "" +#~ "Por favor ingrese su token de autorización. El token debe tener \"secret-" +#~ "token\" y comenzar con Bearer o ApiKey" + +#, c-format +#~ msgid "pick a date" +#~ msgstr "elegir una fecha" + +#, c-format +#~ msgid "no results" +#~ msgstr "Sin resultados" + +#, c-format +#~ msgid "current stock will change from %1$s to %2$s" +#~ msgstr "stock actual cambiará desde %1$s a %2$s" + +#, c-format +#~ msgid "current stock will stay at %1$s" +#~ msgstr "stock actual seguirá en %1$s" + +#, c-format +#~ msgid "this product has no taxes" +#~ msgstr "este producto no tiene impuestos" + +#, c-format +#~ msgid "Inventory products" +#~ msgstr "Productos de inventario" + +#, c-format +#~ msgid "Total tax" +#~ msgstr "Impuesto total" + +#, c-format +#~ msgid "Net" +#~ msgstr "Neto" + +#, c-format +#~ msgid "select a product first" +#~ msgstr "seleccione un producto primero" + +#, c-format +#~ msgid "" +#~ "cannot be greater than current stock and quantity previously added. max: " +#~ "%1$s" +#~ msgstr "" +#~ "no puede ser mayor al stock actual y la cantidad previamente agregada. " +#~ "máximo: %1$s" + +#, c-format +#~ msgid "cannot be greater than current stock %1$s" +#~ msgstr "no puede ser mayor al stock actual %1$s" + +#, c-format +#~ msgid "Deposit total" +#~ msgstr "Total depositado" + +#, c-format +#~ msgid "Merchant initial amount" +#~ msgstr "Monto inicial" + +#, c-format +#~ msgid "Account Address" +#~ msgstr "Dirección de cuenta" diff --git a/packages/auditor-backoffice-ui/src/i18n/fr.po b/packages/auditor-backoffice-ui/src/i18n/fr.po new file mode 100644 index 000000000..d8d0bae29 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/fr.po @@ -0,0 +1,2741 @@ +# This file is part of TALER +# (C) 2016 GNUnet e.V. +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"POT-Creation-Date: 2016-11-23 00:00+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/components/modal/index.tsx:71 +#, c-format +msgid "Cancel" +msgstr "" + +#: src/components/modal/index.tsx:79 +#, c-format +msgid "%1$s" +msgstr "" + +#: src/components/modal/index.tsx:84 +#, c-format +msgid "Close" +msgstr "" + +#: src/components/modal/index.tsx:124 +#, c-format +msgid "Continue" +msgstr "" + +#: src/components/modal/index.tsx:178 +#, c-format +msgid "Clear" +msgstr "" + +#: src/components/modal/index.tsx:190 +#, c-format +msgid "Confirm" +msgstr "" + +#: src/components/modal/index.tsx:296 +#, c-format +msgid "is not the same as the current access token" +msgstr "" + +#: src/components/modal/index.tsx:299 +#, c-format +msgid "cannot be empty" +msgstr "" + +#: src/components/modal/index.tsx:301 +#, c-format +msgid "cannot be the same as the old token" +msgstr "" + +#: src/components/modal/index.tsx:305 +#, c-format +msgid "is not the same" +msgstr "" + +#: src/components/modal/index.tsx:315 +#, c-format +msgid "You are updating the access token from instance with id %1$s" +msgstr "" + +#: src/components/modal/index.tsx:331 +#, c-format +msgid "Old access token" +msgstr "" + +#: src/components/modal/index.tsx:332 +#, c-format +msgid "access token currently in use" +msgstr "" + +#: src/components/modal/index.tsx:338 +#, c-format +msgid "New access token" +msgstr "" + +#: src/components/modal/index.tsx:339 +#, c-format +msgid "next access token to be used" +msgstr "" + +#: src/components/modal/index.tsx:344 +#, c-format +msgid "Repeat access token" +msgstr "" + +#: src/components/modal/index.tsx:345 +#, c-format +msgid "confirm the same access token" +msgstr "" + +#: src/components/modal/index.tsx:350 +#, c-format +msgid "Clearing the access token will mean public access to the instance" +msgstr "" + +#: src/components/modal/index.tsx:377 +#, c-format +msgid "cannot be the same as the old access token" +msgstr "" + +#: src/components/modal/index.tsx:394 +#, c-format +msgid "You are setting the access token for the new instance" +msgstr "" + +#: src/components/modal/index.tsx:420 +#, c-format +msgid "" +"With external authorization method no check will be done by the merchant " +"backend" +msgstr "" + +#: src/components/modal/index.tsx:436 +#, c-format +msgid "Set external authorization" +msgstr "" + +#: src/components/modal/index.tsx:448 +#, c-format +msgid "Set access token" +msgstr "" + +#: src/components/modal/index.tsx:470 +#, c-format +msgid "Operation in progress..." +msgstr "" + +#: src/components/modal/index.tsx:479 +#, c-format +msgid "The operation will be automatically canceled after %1$s seconds" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:80 +#, c-format +msgid "Instances" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:93 +#, c-format +msgid "Delete" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:99 +#, c-format +msgid "add new instance" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:178 +#, c-format +msgid "ID" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:181 +#, c-format +msgid "Name" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:220 +#, c-format +msgid "Edit" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:237 +#, c-format +msgid "Purge" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:261 +#, c-format +msgid "There is no instances yet, add more pressing the + sign" +msgstr "" + +#: src/paths/admin/list/View.tsx:68 +#, c-format +msgid "Only show active instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:71 +#, c-format +msgid "Active" +msgstr "" + +#: src/paths/admin/list/View.tsx:78 +#, c-format +msgid "Only show deleted instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:81 +#, c-format +msgid "Deleted" +msgstr "" + +#: src/paths/admin/list/View.tsx:88 +#, c-format +msgid "Show all instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:91 +#, c-format +msgid "All" +msgstr "" + +#: src/paths/admin/list/index.tsx:101 +#, c-format +msgid "Instance \"%1$s\" (ID: %2$s) has been deleted" +msgstr "" + +#: src/paths/admin/list/index.tsx:106 +#, c-format +msgid "Failed to delete instance" +msgstr "" + +#: src/paths/admin/list/index.tsx:124 +#, c-format +msgid "Instance '%1$s' (ID: %2$s) has been disabled" +msgstr "" + +#: src/paths/admin/list/index.tsx:129 +#, c-format +msgid "Failed to purge instance" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:41 +#, c-format +msgid "Pending KYC verification" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:66 +#, c-format +msgid "Timed out" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:103 +#, c-format +msgid "Exchange" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:106 +#, c-format +msgid "Target account" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:109 +#, c-format +msgid "KYC URL" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:144 +#, c-format +msgid "Code" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:147 +#, c-format +msgid "Http Status" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:177 +#, c-format +msgid "No pending kyc verification!" +msgstr "" + +#: src/components/form/InputDate.tsx:123 +#, c-format +msgid "change value to unknown date" +msgstr "" + +#: src/components/form/InputDate.tsx:124 +#, c-format +msgid "change value to empty" +msgstr "" + +#: src/components/form/InputDate.tsx:131 +#, c-format +msgid "clear" +msgstr "" + +#: src/components/form/InputDate.tsx:136 +#, c-format +msgid "change value to never" +msgstr "" + +#: src/components/form/InputDate.tsx:141 +#, c-format +msgid "never" +msgstr "" + +#: src/components/form/InputLocation.tsx:29 +#, c-format +msgid "Country" +msgstr "" + +#: src/components/form/InputLocation.tsx:33 +#, c-format +msgid "Address" +msgstr "" + +#: src/components/form/InputLocation.tsx:39 +#, c-format +msgid "Building number" +msgstr "" + +#: src/components/form/InputLocation.tsx:41 +#, c-format +msgid "Building name" +msgstr "" + +#: src/components/form/InputLocation.tsx:42 +#, c-format +msgid "Street" +msgstr "" + +#: src/components/form/InputLocation.tsx:43 +#, c-format +msgid "Post code" +msgstr "" + +#: src/components/form/InputLocation.tsx:44 +#, c-format +msgid "Town location" +msgstr "" + +#: src/components/form/InputLocation.tsx:45 +#, c-format +msgid "Town" +msgstr "" + +#: src/components/form/InputLocation.tsx:46 +#, c-format +msgid "District" +msgstr "" + +#: src/components/form/InputLocation.tsx:49 +#, c-format +msgid "Country subdivision" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:66 +#, c-format +msgid "Product id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:69 +#, c-format +msgid "Description" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:94 +#, c-format +msgid "Product" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:95 +#, c-format +msgid "search products by it's description or id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:151 +#, c-format +msgid "no products found with that description" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:56 +#, c-format +msgid "You must enter a valid product identifier." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:64 +#, c-format +msgid "Quantity must be greater than 0!" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:76 +#, c-format +msgid "" +"This quantity exceeds remaining stock. Currently, only %1$s units remain " +"unreserved in stock." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:109 +#, c-format +msgid "Quantity" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:110 +#, c-format +msgid "how many products will be added" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:117 +#, c-format +msgid "Add from inventory" +msgstr "" + +#: src/components/form/InputImage.tsx:105 +#, c-format +msgid "Image should be smaller than 1 MB" +msgstr "" + +#: src/components/form/InputImage.tsx:110 +#, c-format +msgid "Add" +msgstr "" + +#: src/components/form/InputImage.tsx:115 +#, c-format +msgid "Remove" +msgstr "" + +#: src/components/form/InputTaxes.tsx:113 +#, c-format +msgid "No taxes configured for this product." +msgstr "" + +#: src/components/form/InputTaxes.tsx:119 +#, c-format +msgid "Amount" +msgstr "" + +#: src/components/form/InputTaxes.tsx:120 +#, c-format +msgid "" +"Taxes can be in currencies that differ from the main currency used by the " +"merchant." +msgstr "" + +#: src/components/form/InputTaxes.tsx:122 +#, c-format +msgid "" +"Enter currency and value separated with a colon, e.g. "USD:2.3"." +msgstr "" + +#: src/components/form/InputTaxes.tsx:131 +#, c-format +msgid "Legal name of the tax, e.g. VAT or import duties." +msgstr "" + +#: src/components/form/InputTaxes.tsx:137 +#, c-format +msgid "add tax to the tax list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:72 +#, c-format +msgid "describe and add a product that is not in the inventory list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:75 +#, c-format +msgid "Add custom product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:86 +#, c-format +msgid "Complete information of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:185 +#, c-format +msgid "Image" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:186 +#, c-format +msgid "photo of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:192 +#, c-format +msgid "full product description" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:196 +#, c-format +msgid "Unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:197 +#, c-format +msgid "name of the product unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:201 +#, c-format +msgid "Price" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:202 +#, c-format +msgid "amount in the current currency" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:211 +#, c-format +msgid "Taxes" +msgstr "" + +#: src/components/product/ProductList.tsx:38 +#, c-format +msgid "image" +msgstr "" + +#: src/components/product/ProductList.tsx:41 +#, c-format +msgid "description" +msgstr "" + +#: src/components/product/ProductList.tsx:44 +#, c-format +msgid "quantity" +msgstr "" + +#: src/components/product/ProductList.tsx:47 +#, c-format +msgid "unit price" +msgstr "" + +#: src/components/product/ProductList.tsx:50 +#, c-format +msgid "total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:153 +#, c-format +msgid "required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:157 +#, c-format +msgid "not valid" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:159 +#, c-format +msgid "must be greater than 0" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:164 +#, c-format +msgid "not a valid json" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:170 +#, c-format +msgid "should be in the future" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:173 +#, c-format +msgid "refund deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:179 +#, c-format +msgid "wire transfer deadline cannot be before refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:190 +#, c-format +msgid "wire transfer deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:197 +#, c-format +msgid "should have a refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:202 +#, c-format +msgid "auto refund cannot be after refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:360 +#, c-format +msgid "Manage products in order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:369 +#, c-format +msgid "Manage list of products in the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:391 +#, c-format +msgid "Remove this product from the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:415 +#, c-format +msgid "Total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:417 +#, c-format +msgid "total product price added up" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:430 +#, c-format +msgid "Amount to be paid by the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:436 +#, c-format +msgid "Order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:437 +#, c-format +msgid "final order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:444 +#, c-format +msgid "Summary" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:445 +#, c-format +msgid "Title of the order to be shown to the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:450 +#, c-format +msgid "Shipping and Fulfillment" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:455 +#, c-format +msgid "Delivery date" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:456 +#, c-format +msgid "Deadline for physical delivery assured by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:461 +#, c-format +msgid "Location" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:462 +#, c-format +msgid "address where the products will be delivered" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:469 +#, c-format +msgid "Fulfillment URL" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:470 +#, c-format +msgid "URL to which the user will be redirected after successful payment." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:476 +#, c-format +msgid "Taler payment options" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:477 +#, c-format +msgid "Override default Taler payment settings for this order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:481 +#, c-format +msgid "Payment deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:482 +#, c-format +msgid "" +"Deadline for the customer to pay for the offer before it expires. Inventory " +"products will be reserved until this deadline." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:486 +#, c-format +msgid "Refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:487 +#, c-format +msgid "Time until which the order can be refunded by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:491 +#, c-format +msgid "Wire transfer deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:492 +#, c-format +msgid "Deadline for the exchange to make the wire transfer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:496 +#, c-format +msgid "Auto-refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:497 +#, c-format +msgid "" +"Time until which the wallet will automatically check for refunds without " +"user interaction." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:502 +#, c-format +msgid "Maximum deposit fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:503 +#, c-format +msgid "" +"Maximum deposit fees the merchant is willing to cover for this order. Higher " +"deposit fees must be covered in full by the consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:507 +#, c-format +msgid "Maximum wire fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:508 +#, c-format +msgid "" +"Maximum aggregate wire fees the merchant is willing to cover for this order. " +"Wire fees exceeding this amount are to be covered by the customers." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:512 +#, c-format +msgid "Wire fee amortization" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:513 +#, c-format +msgid "" +"Factor by which wire fees exceeding the above threshold are divided to " +"determine the share of excess wire fees to be paid explicitly by the " +"consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:517 +#, c-format +msgid "Create token" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:518 +#, c-format +msgid "" +"Uncheck this option if the merchant backend generated an order ID with " +"enough entropy to prevent adversarial claims." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:522 +#, c-format +msgid "Minimum age required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:523 +#, c-format +msgid "" +"Any value greater than 0 will limit the coins able be used to pay this " +"contract. If empty the age restriction will be defined by the products" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:526 +#, c-format +msgid "Min age defined by the producs is %1$s" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:534 +#, c-format +msgid "Additional information" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:535 +#, c-format +msgid "Custom information to be included in the contract for this order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:541 +#, c-format +msgid "You must enter a value in JavaScript Object Notation (JSON)." +msgstr "" + +#: src/components/picker/DurationPicker.tsx:55 +#, c-format +msgid "days" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:65 +#, c-format +msgid "hours" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:76 +#, c-format +msgid "minutes" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:87 +#, c-format +msgid "seconds" +msgstr "" + +#: src/components/form/InputDuration.tsx:53 +#, c-format +msgid "forever" +msgstr "" + +#: src/components/form/InputDuration.tsx:62 +#, c-format +msgid "%1$sM" +msgstr "" + +#: src/components/form/InputDuration.tsx:64 +#, c-format +msgid "%1$sY" +msgstr "" + +#: src/components/form/InputDuration.tsx:66 +#, c-format +msgid "%1$sd" +msgstr "" + +#: src/components/form/InputDuration.tsx:68 +#, c-format +msgid "%1$sh" +msgstr "" + +#: src/components/form/InputDuration.tsx:70 +#, c-format +msgid "%1$smin" +msgstr "" + +#: src/components/form/InputDuration.tsx:72 +#, c-format +msgid "%1$ssec" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:75 +#, c-format +msgid "Orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:81 +#, c-format +msgid "create order" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:147 +#, c-format +msgid "load newer orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:154 +#, c-format +msgid "Date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:200 +#, c-format +msgid "Refund" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:209 +#, c-format +msgid "copy url" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:225 +#, c-format +msgid "load older orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:242 +#, c-format +msgid "No orders have been found matching your query!" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:288 +#, c-format +msgid "duplicated" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:299 +#, c-format +msgid "invalid format" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:301 +#, c-format +msgid "this value exceed the refundable amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:346 +#, c-format +msgid "date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:349 +#, c-format +msgid "amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:352 +#, c-format +msgid "reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:389 +#, c-format +msgid "amount to be refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:391 +#, c-format +msgid "Max refundable:" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:396 +#, c-format +msgid "Reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:397 +#, c-format +msgid "Choose one..." +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:399 +#, c-format +msgid "requested by the customer" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:400 +#, c-format +msgid "other" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:403 +#, c-format +msgid "why this order is being refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:409 +#, c-format +msgid "more information to give context" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:62 +#, c-format +msgid "Contract Terms" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:68 +#, c-format +msgid "human-readable description of the whole purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:74 +#, c-format +msgid "total price for the transaction" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:81 +#, c-format +msgid "URL for this purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:87 +#, c-format +msgid "Max fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:88 +#, c-format +msgid "maximum total deposit fee accepted by the merchant for this contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:93 +#, c-format +msgid "Max wire fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:94 +#, c-format +msgid "maximum wire fee accepted by the merchant" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:100 +#, c-format +msgid "" +"over how many customer transactions does the merchant expect to amortize " +"wire fees on average" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:105 +#, c-format +msgid "Created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:106 +#, c-format +msgid "time when this contract was generated" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:112 +#, c-format +msgid "after this deadline has passed no refunds will be accepted" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:118 +#, c-format +msgid "" +"after this deadline, the merchant won't accept payments for the contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:124 +#, c-format +msgid "transfer deadline for the exchange" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:130 +#, c-format +msgid "time indicating when the order should be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:136 +#, c-format +msgid "where the order will be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:144 +#, c-format +msgid "Auto-refund delay" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:145 +#, c-format +msgid "" +"how long the wallet should try to get an automatic refund for the purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:150 +#, c-format +msgid "Extra info" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:151 +#, c-format +msgid "extra data that is only interpreted by the merchant frontend" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:219 +#, c-format +msgid "Order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:221 +#, c-format +msgid "claimed" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:247 +#, c-format +msgid "claimed at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:265 +#, c-format +msgid "Timeline" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:271 +#, c-format +msgid "Payment details" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:291 +#, c-format +msgid "Order status" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:301 +#, c-format +msgid "Product list" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:451 +#, c-format +msgid "paid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:455 +#, c-format +msgid "wired" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:460 +#, c-format +msgid "refunded" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:480 +#, c-format +msgid "refund order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:481 +#, c-format +msgid "not refundable" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:489 +#, c-format +msgid "refund" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:553 +#, c-format +msgid "Refunded amount" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:560 +#, c-format +msgid "Refund taken" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:570 +#, c-format +msgid "Status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:583 +#, c-format +msgid "Refund URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:636 +#, c-format +msgid "unpaid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:654 +#, c-format +msgid "pay at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:666 +#, c-format +msgid "created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:707 +#, c-format +msgid "Order status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:711 +#, c-format +msgid "Payment URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:740 +#, c-format +msgid "" +"Unknown order status. This is an error, please contact the administrator." +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:767 +#, c-format +msgid "Back" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:79 +#, c-format +msgid "refund created successfully" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:85 +#, c-format +msgid "could not create the refund" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:78 +#, c-format +msgid "select date to show nearby orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:94 +#, c-format +msgid "order id" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:100 +#, c-format +msgid "jump to order with the given order ID" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:122 +#, c-format +msgid "remove all filters" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:132 +#, c-format +msgid "only show paid orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:135 +#, c-format +msgid "Paid" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:142 +#, c-format +msgid "only show orders with refunds" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:145 +#, c-format +msgid "Refunded" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:152 +#, c-format +msgid "" +"only show orders where customers paid, but wire payments from payment " +"provider are still pending" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:155 +#, c-format +msgid "Not wired" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:170 +#, c-format +msgid "clear date filter" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:184 +#, c-format +msgid "date (YYYY/MM/DD)" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:103 +#, c-format +msgid "Enter an order id" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:111 +#, c-format +msgid "order not found" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:178 +#, c-format +msgid "could not get the order to refund" +msgstr "" + +#: src/components/exception/AsyncButton.tsx:43 +#, c-format +msgid "Loading..." +msgstr "" + +#: src/components/form/InputStock.tsx:99 +#, c-format +msgid "" +"click here to configure the stock of the product, leave it as is and the " +"backend will not control stock" +msgstr "" + +#: src/components/form/InputStock.tsx:109 +#, c-format +msgid "Manage stock" +msgstr "" + +#: src/components/form/InputStock.tsx:115 +#, c-format +msgid "this product has been configured without stock control" +msgstr "" + +#: src/components/form/InputStock.tsx:119 +#, c-format +msgid "Infinite" +msgstr "" + +#: src/components/form/InputStock.tsx:136 +#, c-format +msgid "lost cannot be greater than current and incoming (max %1$s)" +msgstr "" + +#: src/components/form/InputStock.tsx:176 +#, c-format +msgid "Incoming" +msgstr "" + +#: src/components/form/InputStock.tsx:177 +#, c-format +msgid "Lost" +msgstr "" + +#: src/components/form/InputStock.tsx:192 +#, c-format +msgid "Current" +msgstr "" + +#: src/components/form/InputStock.tsx:196 +#, c-format +msgid "remove stock control for this product" +msgstr "" + +#: src/components/form/InputStock.tsx:202 +#, c-format +msgid "without stock" +msgstr "" + +#: src/components/form/InputStock.tsx:211 +#, c-format +msgid "Next restock" +msgstr "" + +#: src/components/form/InputStock.tsx:217 +#, c-format +msgid "Delivery address" +msgstr "" + +#: src/components/product/ProductForm.tsx:133 +#, c-format +msgid "product identification to use in URLs (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:139 +#, c-format +msgid "illustration of the product for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:145 +#, c-format +msgid "product description for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:149 +#, c-format +msgid "Age restricted" +msgstr "" + +#: src/components/product/ProductForm.tsx:150 +#, c-format +msgid "is this product restricted for customer below certain age?" +msgstr "" + +#: src/components/product/ProductForm.tsx:155 +#, c-format +msgid "" +"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 " +"items, 5 meters) for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:160 +#, c-format +msgid "" +"sale price for customers, including taxes, for above units of the product" +msgstr "" + +#: src/components/product/ProductForm.tsx:164 +#, c-format +msgid "Stock" +msgstr "" + +#: src/components/product/ProductForm.tsx:166 +#, c-format +msgid "" +"product inventory for products with finite supply (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:171 +#, c-format +msgid "taxes included in the product price, exposed to customers" +msgstr "" + +#: src/paths/instance/products/create/CreatePage.tsx:66 +#, c-format +msgid "Need to complete marked fields" +msgstr "" + +#: src/paths/instance/products/create/index.tsx:51 +#, c-format +msgid "could not create product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:68 +#, c-format +msgid "Products" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:73 +#, c-format +msgid "add product to inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:137 +#, c-format +msgid "Sell" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:143 +#, c-format +msgid "Profit" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:149 +#, c-format +msgid "Sold" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:210 +#, c-format +msgid "free" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:248 +#, c-format +msgid "go to product update page" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:255 +#, c-format +msgid "Update" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:260 +#, c-format +msgid "remove this product from the database" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:331 +#, c-format +msgid "update the product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:341 +#, c-format +msgid "update product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:399 +#, c-format +msgid "add more elements to the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:404 +#, c-format +msgid "report elements lost in the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:409 +#, c-format +msgid "new price for the product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:421 +#, c-format +msgid "the are value with errors" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:422 +#, c-format +msgid "update product with new stock and price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:463 +#, c-format +msgid "There is no products yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:86 +#, c-format +msgid "product updated successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:92 +#, c-format +msgid "could not update the product" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:103 +#, c-format +msgid "product delete successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:109 +#, c-format +msgid "could not delete the product" +msgstr "" + +#: src/paths/instance/products/update/UpdatePage.tsx:56 +#, c-format +msgid "Product id:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95 +#, c-format +msgid "" +"To complete the setup of the reserve, you must now initiate a wire transfer " +"using the given wire transfer subject and crediting the specified amount to " +"the indicated account of the exchange." +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102 +#, c-format +msgid "If your system supports RFC 8905, you can do this by opening this URI:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:83 +#, c-format +msgid "it should be greater than 0" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:88 +#, c-format +msgid "must be a valid URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:107 +#, c-format +msgid "Initial balance" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:108 +#, c-format +msgid "balance prior to deposit" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:112 +#, c-format +msgid "Exchange URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:113 +#, c-format +msgid "URL of exchange" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:148 +#, c-format +msgid "Next" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:186 +#, c-format +msgid "Wire method" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:187 +#, c-format +msgid "method to use for wire transfer" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:189 +#, c-format +msgid "Select one wire method" +msgstr "" + +#: src/paths/instance/reserves/create/index.tsx:62 +#, c-format +msgid "could not create reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:77 +#, c-format +msgid "Valid until" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:82 +#, c-format +msgid "Created balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:99 +#, c-format +msgid "Exchange balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:104 +#, c-format +msgid "Picked up" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:109 +#, c-format +msgid "Committed" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:116 +#, c-format +msgid "Account address" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:119 +#, c-format +msgid "Subject" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:130 +#, c-format +msgid "Tips" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:193 +#, c-format +msgid "No tips has been authorized from this reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:213 +#, c-format +msgid "Authorized" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:222 +#, c-format +msgid "Expiration" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108 +#, c-format +msgid "amount of tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112 +#, c-format +msgid "Justification" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114 +#, c-format +msgid "reason for the tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118 +#, c-format +msgid "URL after tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119 +#, c-format +msgid "URL to visit after tip payment" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:65 +#, c-format +msgid "Reserves not yet funded" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:89 +#, c-format +msgid "Reserves ready" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:95 +#, c-format +msgid "add new reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:143 +#, c-format +msgid "Expires at" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:146 +#, c-format +msgid "Initial" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:202 +#, c-format +msgid "delete selected reserve from the database" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:210 +#, c-format +msgid "authorize new tip from selected reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:237 +#, c-format +msgid "" +"There is no ready reserves yet, add more pressing the + sign or fund them" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:264 +#, c-format +msgid "Expected Balance" +msgstr "" + +#: src/paths/instance/reserves/list/index.tsx:110 +#, c-format +msgid "could not create the tip" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:77 +#, c-format +msgid "should not be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:93 +#, c-format +msgid "should be greater that 0" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:96 +#, c-format +msgid "can't be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:100 +#, c-format +msgid "to short" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:108 +#, c-format +msgid "just letters and numbers from 2 to 7" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:110 +#, c-format +msgid "size of the key should be 32" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:137 +#, c-format +msgid "Identifier" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:138 +#, c-format +msgid "Name of the template in URLs." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:144 +#, c-format +msgid "Describe what this template stands for" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:149 +#, c-format +msgid "Fixed summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:150 +#, c-format +msgid "If specified, this template will create order with the same summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:154 +#, c-format +msgid "Fixed price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:155 +#, c-format +msgid "If specified, this template will create order with the same price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:159 +#, c-format +msgid "Minimum age" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:161 +#, c-format +msgid "Is this contract restricted to some age?" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:165 +#, c-format +msgid "Payment timeout" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:167 +#, c-format +msgid "" +"How much time has the customer to complete the payment once the order was " +"created." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:171 +#, c-format +msgid "Verification algorithm" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:172 +#, c-format +msgid "Algorithm to use to verify transaction in offline mode" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:180 +#, c-format +msgid "Point-of-sale key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:182 +#, c-format +msgid "Useful to validate the purchase" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:196 +#, c-format +msgid "generate random secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:203 +#, c-format +msgid "random" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:208 +#, c-format +msgid "show secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:209 +#, c-format +msgid "hide secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:216 +#, c-format +msgid "hide" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:218 +#, c-format +msgid "show" +msgstr "" + +#: src/paths/instance/templates/create/index.tsx:52 +#, c-format +msgid "could not inform template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:54 +#, c-format +msgid "Amount is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:58 +#, c-format +msgid "Order summary is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:86 +#, c-format +msgid "New order for template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:108 +#, c-format +msgid "Amount of the order" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:113 +#, c-format +msgid "Order summary" +msgstr "" + +#: src/paths/instance/templates/use/index.tsx:92 +#, c-format +msgid "could not create order from template" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:131 +#, c-format +msgid "" +"Here you can specify a default value for fields that are not fixed. Default " +"values can be edited by the customer before the payment." +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:148 +#, c-format +msgid "Fixed amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:149 +#, c-format +msgid "Default amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:161 +#, c-format +msgid "Default summary" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:177 +#, c-format +msgid "Print" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:184 +#, c-format +msgid "Setup TOTP" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:65 +#, c-format +msgid "Templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:70 +#, c-format +msgid "add new templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:142 +#, c-format +msgid "load more templates before the first one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:146 +#, c-format +msgid "load newer templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:181 +#, c-format +msgid "delete selected templates from the database" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:188 +#, c-format +msgid "use template to create new order" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:195 +#, c-format +msgid "create qr code for the template" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:210 +#, c-format +msgid "load more templates after the last one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:214 +#, c-format +msgid "load older templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:231 +#, c-format +msgid "There is no templates yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:104 +#, c-format +msgid "template delete successfully" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:110 +#, c-format +msgid "could not delete the template" +msgstr "" + +#: src/paths/instance/templates/update/index.tsx:90 +#, c-format +msgid "could not update template" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:57 +#, c-format +msgid "should be one of '%1$s'" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:85 +#, c-format +msgid "Webhook ID to use" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:89 +#, c-format +msgid "Event" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:90 +#, c-format +msgid "The event of the webhook: why the webhook is used" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:94 +#, c-format +msgid "Method" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:95 +#, c-format +msgid "Method used by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:99 +#, c-format +msgid "URL" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:100 +#, c-format +msgid "URL of the webhook where the customer will be redirected" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:104 +#, c-format +msgid "Header" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:106 +#, c-format +msgid "Header template of the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:111 +#, c-format +msgid "Body" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:112 +#, c-format +msgid "Body template by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:61 +#, c-format +msgid "Webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:66 +#, c-format +msgid "add new webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:137 +#, c-format +msgid "load more webhooks before the first one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:141 +#, c-format +msgid "load newer webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:151 +#, c-format +msgid "Event type" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:176 +#, c-format +msgid "delete selected webhook from the database" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:198 +#, c-format +msgid "load more webhooks after the last one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:202 +#, c-format +msgid "load older webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:219 +#, c-format +msgid "There is no webhooks yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:94 +#, c-format +msgid "webhook delete successfully" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:100 +#, c-format +msgid "could not delete the webhook" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:63 +#, c-format +msgid "check the id, does not look valid" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:65 +#, c-format +msgid "should have 52 characters, current %1$s" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:72 +#, c-format +msgid "URL doesn't have the right format" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:98 +#, c-format +msgid "Credited bank account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:100 +#, c-format +msgid "Select one account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:101 +#, c-format +msgid "Bank account of the merchant where the payment was received" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:105 +#, c-format +msgid "Wire transfer ID" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:107 +#, c-format +msgid "" +"unique identifier of the wire transfer used by the exchange, must be 52 " +"characters long" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:112 +#, c-format +msgid "" +"Base URL of the exchange that made the transfer, should have been in the " +"wire transfer subject" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:117 +#, c-format +msgid "Amount credited" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:118 +#, c-format +msgid "Actual amount that was wired to the merchant's bank account" +msgstr "" + +#: src/paths/instance/transfers/create/index.tsx:58 +#, c-format +msgid "could not inform transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:61 +#, c-format +msgid "Transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:66 +#, c-format +msgid "add new transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:129 +#, c-format +msgid "load more transfers before the first one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:133 +#, c-format +msgid "load newer transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:143 +#, c-format +msgid "Credit" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:152 +#, c-format +msgid "Confirmed" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:155 +#, c-format +msgid "Verified" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:158 +#, c-format +msgid "Executed at" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "yes" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "no" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:181 +#, c-format +msgid "unknown" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:187 +#, c-format +msgid "delete selected transfer from the database" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:202 +#, c-format +msgid "load more transfer after the last one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:206 +#, c-format +msgid "load older transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:223 +#, c-format +msgid "There is no transfer yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:79 +#, c-format +msgid "filter by account address" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:100 +#, c-format +msgid "only show wire transfers confirmed by the merchant" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:110 +#, c-format +msgid "only show wire transfers claimed by the exchange" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:113 +#, c-format +msgid "Unverified" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:69 +#, c-format +msgid "is not valid" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:94 +#, c-format +msgid "is not a number" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:96 +#, c-format +msgid "must be 1 or greater" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:107 +#, c-format +msgid "max 7 lines" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:178 +#, c-format +msgid "change authorization configuration" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:217 +#, c-format +msgid "Need to complete marked fields and choose authorization method" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:82 +#, c-format +msgid "This is not a valid bitcoin address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:95 +#, c-format +msgid "This is not a valid Ethereum address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:118 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:120 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:128 +#, c-format +msgid "IBAN country code not found" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:153 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:248 +#, c-format +msgid "Target type" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:249 +#, c-format +msgid "Method to use for wire transfer" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:258 +#, c-format +msgid "Routing" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:259 +#, c-format +msgid "Routing number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:263 +#, c-format +msgid "Account" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:264 +#, c-format +msgid "Account number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:273 +#, c-format +msgid "Business Identifier Code." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:282 +#, c-format +msgid "Bank Account Number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:292 +#, c-format +msgid "Unified Payment Interface." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:301 +#, c-format +msgid "Bitcoin protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:310 +#, c-format +msgid "Ethereum protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:319 +#, c-format +msgid "Interledger protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:328 +#, c-format +msgid "Host" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:329 +#, c-format +msgid "Bank host." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:334 +#, c-format +msgid "Bank account." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:343 +#, c-format +msgid "Bank account owner's name." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:370 +#, c-format +msgid "No accounts yet." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:52 +#, c-format +msgid "" +"Name of the instance in URLs. The 'default' instance is special in that it " +"is used to administer other instances." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:58 +#, c-format +msgid "Business name" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:59 +#, c-format +msgid "Legal name of the business represented by this instance." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:64 +#, c-format +msgid "Email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:65 +#, c-format +msgid "Contact email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:70 +#, c-format +msgid "Website URL" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:71 +#, c-format +msgid "URL." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:76 +#, c-format +msgid "Logo" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:77 +#, c-format +msgid "Logo image." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:82 +#, c-format +msgid "Bank account" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:83 +#, c-format +msgid "URI specifying bank account for crediting revenue." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:88 +#, c-format +msgid "Default max deposit fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:89 +#, c-format +msgid "" +"Maximum deposit fees this merchant is willing to pay per order by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:94 +#, c-format +msgid "Default max wire fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:95 +#, c-format +msgid "" +"Maximum wire fees this merchant is willing to pay per wire transfer by " +"default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:100 +#, c-format +msgid "Default wire fee amortization" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:101 +#, c-format +msgid "" +"Number of orders excess wire transfer fees will be divided by to compute per " +"order surcharge." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:107 +#, c-format +msgid "Physical location of the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:114 +#, c-format +msgid "Jurisdiction" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:115 +#, c-format +msgid "Jurisdiction for legal disputes with the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:122 +#, c-format +msgid "Default payment delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:124 +#, c-format +msgid "" +"Time customers have to pay an order before the offer expires by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:129 +#, c-format +msgid "Default wire transfer delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:130 +#, c-format +msgid "" +"Maximum time an exchange is allowed to delay wiring funds to the merchant, " +"enabling it to aggregate smaller payments into larger wire transfers and " +"reducing wire fees." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:164 +#, c-format +msgid "Instance id" +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:173 +#, c-format +msgid "Change the authorization method use for this instance." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:182 +#, c-format +msgid "Manage access token" +msgstr "" + +#: src/paths/instance/update/index.tsx:112 +#, c-format +msgid "Failed to create instance" +msgstr "" + +#: src/components/exception/login.tsx:74 +#, c-format +msgid "Login required" +msgstr "" + +#: src/components/exception/login.tsx:80 +#, c-format +msgid "Please enter your access token." +msgstr "" + +#: src/components/exception/login.tsx:108 +#, c-format +msgid "Access Token" +msgstr "" + +#: src/InstanceRoutes.tsx:171 +#, c-format +msgid "The request to the backend take too long and was cancelled" +msgstr "" + +#: src/InstanceRoutes.tsx:172 +#, c-format +msgid "Diagnostic from %1$s is \"%2$s\"" +msgstr "" + +#: src/InstanceRoutes.tsx:178 +#, c-format +msgid "The backend reported a problem: HTTP status #%1$s" +msgstr "" + +#: src/InstanceRoutes.tsx:179 +#, c-format +msgid "Diagnostic from %1$s is '%2$s'" +msgstr "" + +#: src/InstanceRoutes.tsx:196 +#, c-format +msgid "Access denied" +msgstr "" + +#: src/InstanceRoutes.tsx:197 +#, c-format +msgid "The access token provided is invalid." +msgstr "" + +#: src/InstanceRoutes.tsx:212 +#, c-format +msgid "No 'default' instance configured yet." +msgstr "" + +#: src/InstanceRoutes.tsx:213 +#, c-format +msgid "Create a 'default' instance to begin using the merchant backoffice." +msgstr "" + +#: src/InstanceRoutes.tsx:630 +#, c-format +msgid "The access token provided is invalid" +msgstr "" + +#: src/InstanceRoutes.tsx:664 +#, c-format +msgid "Hide for today" +msgstr "" + +#: src/components/menu/SideBar.tsx:82 +#, c-format +msgid "Instance" +msgstr "" + +#: src/components/menu/SideBar.tsx:91 +#, c-format +msgid "Settings" +msgstr "" + +#: src/components/menu/SideBar.tsx:167 +#, c-format +msgid "Connection" +msgstr "" + +#: src/components/menu/SideBar.tsx:209 +#, c-format +msgid "New" +msgstr "" + +#: src/components/menu/SideBar.tsx:219 +#, c-format +msgid "List" +msgstr "" + +#: src/components/menu/SideBar.tsx:234 +#, c-format +msgid "Log out" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:71 +#, c-format +msgid "Check your token is valid" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:90 +#, c-format +msgid "Couldn't access the server." +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:91 +#, c-format +msgid "Could not infer instance id from url %1$s" +msgstr "" + +#: src/Application.tsx:104 +#, c-format +msgid "Server not found" +msgstr "" + +#: src/Application.tsx:118 +#, c-format +msgid "Server response with an error code" +msgstr "" + +#: src/Application.tsx:120 +#, c-format +msgid "Got message %1$s from %2$s" +msgstr "" + +#: src/Application.tsx:131 +#, c-format +msgid "Response from server is unreadable, http status: %1$s" +msgstr "" + +#: src/Application.tsx:144 +#, c-format +msgid "Unexpected Error" +msgstr "" + +#: src/components/form/InputArray.tsx:101 +#, c-format +msgid "The value %1$s is invalid for a payment url" +msgstr "" + +#: src/components/form/InputArray.tsx:110 +#, c-format +msgid "add element to the list" +msgstr "" + +#: src/components/form/InputArray.tsx:112 +#, c-format +msgid "add" +msgstr "" + +#: src/components/form/InputSecured.tsx:37 +#, c-format +msgid "Deleting" +msgstr "" + +#: src/components/form/InputSecured.tsx:41 +#, c-format +msgid "Changing" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87 +#, c-format +msgid "Order ID" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101 +#, c-format +msgid "Payment URL" +msgstr "" diff --git a/packages/auditor-backoffice-ui/src/i18n/it.po b/packages/auditor-backoffice-ui/src/i18n/it.po new file mode 100644 index 000000000..4055af10e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/it.po @@ -0,0 +1,2742 @@ +# This file is part of TALER +# (C) 2016 GNUnet e.V. +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"POT-Creation-Date: 2016-11-23 00:00+0100\n" +"PO-Revision-Date: 2023-08-16 12:43+0000\n" +"Last-Translator: Krystian Baran <kiszkot@murena.io>\n" +"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/" +"merchant-backoffice/it/>\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.13.1\n" + +#: src/components/modal/index.tsx:71 +#, c-format +msgid "Cancel" +msgstr "" + +#: src/components/modal/index.tsx:79 +#, c-format +msgid "%1$s" +msgstr "" + +#: src/components/modal/index.tsx:84 +#, c-format +msgid "Close" +msgstr "" + +#: src/components/modal/index.tsx:124 +#, c-format +msgid "Continue" +msgstr "" + +#: src/components/modal/index.tsx:178 +#, c-format +msgid "Clear" +msgstr "" + +#: src/components/modal/index.tsx:190 +#, c-format +msgid "Confirm" +msgstr "" + +#: src/components/modal/index.tsx:296 +#, c-format +msgid "is not the same as the current access token" +msgstr "" + +#: src/components/modal/index.tsx:299 +#, c-format +msgid "cannot be empty" +msgstr "" + +#: src/components/modal/index.tsx:301 +#, c-format +msgid "cannot be the same as the old token" +msgstr "" + +#: src/components/modal/index.tsx:305 +#, c-format +msgid "is not the same" +msgstr "" + +#: src/components/modal/index.tsx:315 +#, c-format +msgid "You are updating the access token from instance with id %1$s" +msgstr "" + +#: src/components/modal/index.tsx:331 +#, c-format +msgid "Old access token" +msgstr "" + +#: src/components/modal/index.tsx:332 +#, c-format +msgid "access token currently in use" +msgstr "" + +#: src/components/modal/index.tsx:338 +#, c-format +msgid "New access token" +msgstr "" + +#: src/components/modal/index.tsx:339 +#, c-format +msgid "next access token to be used" +msgstr "" + +#: src/components/modal/index.tsx:344 +#, c-format +msgid "Repeat access token" +msgstr "" + +#: src/components/modal/index.tsx:345 +#, c-format +msgid "confirm the same access token" +msgstr "" + +#: src/components/modal/index.tsx:350 +#, c-format +msgid "Clearing the access token will mean public access to the instance" +msgstr "" + +#: src/components/modal/index.tsx:377 +#, c-format +msgid "cannot be the same as the old access token" +msgstr "" + +#: src/components/modal/index.tsx:394 +#, c-format +msgid "You are setting the access token for the new instance" +msgstr "" + +#: src/components/modal/index.tsx:420 +#, c-format +msgid "" +"With external authorization method no check will be done by the merchant " +"backend" +msgstr "" + +#: src/components/modal/index.tsx:436 +#, c-format +msgid "Set external authorization" +msgstr "" + +#: src/components/modal/index.tsx:448 +#, c-format +msgid "Set access token" +msgstr "" + +#: src/components/modal/index.tsx:470 +#, c-format +msgid "Operation in progress..." +msgstr "" + +#: src/components/modal/index.tsx:479 +#, c-format +msgid "The operation will be automatically canceled after %1$s seconds" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:80 +#, c-format +msgid "Instances" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:93 +#, c-format +msgid "Delete" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:99 +#, c-format +msgid "add new instance" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:178 +#, c-format +msgid "ID" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:181 +#, c-format +msgid "Name" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:220 +#, c-format +msgid "Edit" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:237 +#, c-format +msgid "Purge" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:261 +#, c-format +msgid "There is no instances yet, add more pressing the + sign" +msgstr "" + +#: src/paths/admin/list/View.tsx:68 +#, c-format +msgid "Only show active instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:71 +#, c-format +msgid "Active" +msgstr "" + +#: src/paths/admin/list/View.tsx:78 +#, c-format +msgid "Only show deleted instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:81 +#, c-format +msgid "Deleted" +msgstr "" + +#: src/paths/admin/list/View.tsx:88 +#, c-format +msgid "Show all instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:91 +#, c-format +msgid "All" +msgstr "" + +#: src/paths/admin/list/index.tsx:101 +#, c-format +msgid "Instance \"%1$s\" (ID: %2$s) has been deleted" +msgstr "" + +#: src/paths/admin/list/index.tsx:106 +#, c-format +msgid "Failed to delete instance" +msgstr "" + +#: src/paths/admin/list/index.tsx:124 +#, c-format +msgid "Instance '%1$s' (ID: %2$s) has been disabled" +msgstr "" + +#: src/paths/admin/list/index.tsx:129 +#, c-format +msgid "Failed to purge instance" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:41 +#, c-format +msgid "Pending KYC verification" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:66 +#, c-format +msgid "Timed out" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:103 +#, c-format +msgid "Exchange" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:106 +#, c-format +msgid "Target account" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:109 +#, c-format +msgid "KYC URL" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:144 +#, c-format +msgid "Code" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:147 +#, c-format +msgid "Http Status" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:177 +#, c-format +msgid "No pending kyc verification!" +msgstr "" + +#: src/components/form/InputDate.tsx:123 +#, c-format +msgid "change value to unknown date" +msgstr "" + +#: src/components/form/InputDate.tsx:124 +#, c-format +msgid "change value to empty" +msgstr "" + +#: src/components/form/InputDate.tsx:131 +#, c-format +msgid "clear" +msgstr "" + +#: src/components/form/InputDate.tsx:136 +#, c-format +msgid "change value to never" +msgstr "" + +#: src/components/form/InputDate.tsx:141 +#, c-format +msgid "never" +msgstr "" + +#: src/components/form/InputLocation.tsx:29 +#, c-format +msgid "Country" +msgstr "" + +#: src/components/form/InputLocation.tsx:33 +#, c-format +msgid "Address" +msgstr "" + +#: src/components/form/InputLocation.tsx:39 +#, c-format +msgid "Building number" +msgstr "" + +#: src/components/form/InputLocation.tsx:41 +#, c-format +msgid "Building name" +msgstr "" + +#: src/components/form/InputLocation.tsx:42 +#, c-format +msgid "Street" +msgstr "" + +#: src/components/form/InputLocation.tsx:43 +#, c-format +msgid "Post code" +msgstr "" + +#: src/components/form/InputLocation.tsx:44 +#, c-format +msgid "Town location" +msgstr "" + +#: src/components/form/InputLocation.tsx:45 +#, c-format +msgid "Town" +msgstr "" + +#: src/components/form/InputLocation.tsx:46 +#, c-format +msgid "District" +msgstr "" + +#: src/components/form/InputLocation.tsx:49 +#, c-format +msgid "Country subdivision" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:66 +#, c-format +msgid "Product id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:69 +#, c-format +msgid "Description" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:94 +#, c-format +msgid "Product" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:95 +#, c-format +msgid "search products by it's description or id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:151 +#, c-format +msgid "no products found with that description" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:56 +#, c-format +msgid "You must enter a valid product identifier." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:64 +#, c-format +msgid "Quantity must be greater than 0!" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:76 +#, c-format +msgid "" +"This quantity exceeds remaining stock. Currently, only %1$s units remain " +"unreserved in stock." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:109 +#, c-format +msgid "Quantity" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:110 +#, c-format +msgid "how many products will be added" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:117 +#, c-format +msgid "Add from inventory" +msgstr "" + +#: src/components/form/InputImage.tsx:105 +#, c-format +msgid "Image should be smaller than 1 MB" +msgstr "" + +#: src/components/form/InputImage.tsx:110 +#, c-format +msgid "Add" +msgstr "" + +#: src/components/form/InputImage.tsx:115 +#, c-format +msgid "Remove" +msgstr "" + +#: src/components/form/InputTaxes.tsx:113 +#, c-format +msgid "No taxes configured for this product." +msgstr "" + +#: src/components/form/InputTaxes.tsx:119 +#, c-format +msgid "Amount" +msgstr "Importo" + +#: src/components/form/InputTaxes.tsx:120 +#, c-format +msgid "" +"Taxes can be in currencies that differ from the main currency used by the " +"merchant." +msgstr "" + +#: src/components/form/InputTaxes.tsx:122 +#, c-format +msgid "" +"Enter currency and value separated with a colon, e.g. "USD:2.3"." +msgstr "" + +#: src/components/form/InputTaxes.tsx:131 +#, c-format +msgid "Legal name of the tax, e.g. VAT or import duties." +msgstr "" + +#: src/components/form/InputTaxes.tsx:137 +#, c-format +msgid "add tax to the tax list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:72 +#, c-format +msgid "describe and add a product that is not in the inventory list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:75 +#, c-format +msgid "Add custom product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:86 +#, c-format +msgid "Complete information of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:185 +#, c-format +msgid "Image" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:186 +#, c-format +msgid "photo of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:192 +#, c-format +msgid "full product description" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:196 +#, c-format +msgid "Unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:197 +#, c-format +msgid "name of the product unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:201 +#, c-format +msgid "Price" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:202 +#, c-format +msgid "amount in the current currency" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:211 +#, c-format +msgid "Taxes" +msgstr "" + +#: src/components/product/ProductList.tsx:38 +#, c-format +msgid "image" +msgstr "" + +#: src/components/product/ProductList.tsx:41 +#, c-format +msgid "description" +msgstr "" + +#: src/components/product/ProductList.tsx:44 +#, c-format +msgid "quantity" +msgstr "" + +#: src/components/product/ProductList.tsx:47 +#, c-format +msgid "unit price" +msgstr "" + +#: src/components/product/ProductList.tsx:50 +#, c-format +msgid "total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:153 +#, c-format +msgid "required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:157 +#, c-format +msgid "not valid" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:159 +#, c-format +msgid "must be greater than 0" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:164 +#, c-format +msgid "not a valid json" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:170 +#, c-format +msgid "should be in the future" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:173 +#, c-format +msgid "refund deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:179 +#, c-format +msgid "wire transfer deadline cannot be before refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:190 +#, c-format +msgid "wire transfer deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:197 +#, c-format +msgid "should have a refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:202 +#, c-format +msgid "auto refund cannot be after refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:360 +#, c-format +msgid "Manage products in order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:369 +#, c-format +msgid "Manage list of products in the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:391 +#, c-format +msgid "Remove this product from the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:415 +#, c-format +msgid "Total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:417 +#, c-format +msgid "total product price added up" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:430 +#, c-format +msgid "Amount to be paid by the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:436 +#, c-format +msgid "Order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:437 +#, c-format +msgid "final order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:444 +#, c-format +msgid "Summary" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:445 +#, c-format +msgid "Title of the order to be shown to the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:450 +#, c-format +msgid "Shipping and Fulfillment" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:455 +#, c-format +msgid "Delivery date" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:456 +#, c-format +msgid "Deadline for physical delivery assured by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:461 +#, c-format +msgid "Location" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:462 +#, c-format +msgid "address where the products will be delivered" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:469 +#, c-format +msgid "Fulfillment URL" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:470 +#, c-format +msgid "URL to which the user will be redirected after successful payment." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:476 +#, c-format +msgid "Taler payment options" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:477 +#, c-format +msgid "Override default Taler payment settings for this order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:481 +#, c-format +msgid "Payment deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:482 +#, c-format +msgid "" +"Deadline for the customer to pay for the offer before it expires. Inventory " +"products will be reserved until this deadline." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:486 +#, c-format +msgid "Refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:487 +#, c-format +msgid "Time until which the order can be refunded by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:491 +#, c-format +msgid "Wire transfer deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:492 +#, c-format +msgid "Deadline for the exchange to make the wire transfer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:496 +#, c-format +msgid "Auto-refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:497 +#, c-format +msgid "" +"Time until which the wallet will automatically check for refunds without " +"user interaction." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:502 +#, c-format +msgid "Maximum deposit fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:503 +#, c-format +msgid "" +"Maximum deposit fees the merchant is willing to cover for this order. Higher " +"deposit fees must be covered in full by the consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:507 +#, c-format +msgid "Maximum wire fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:508 +#, c-format +msgid "" +"Maximum aggregate wire fees the merchant is willing to cover for this order. " +"Wire fees exceeding this amount are to be covered by the customers." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:512 +#, c-format +msgid "Wire fee amortization" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:513 +#, c-format +msgid "" +"Factor by which wire fees exceeding the above threshold are divided to " +"determine the share of excess wire fees to be paid explicitly by the " +"consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:517 +#, c-format +msgid "Create token" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:518 +#, c-format +msgid "" +"Uncheck this option if the merchant backend generated an order ID with " +"enough entropy to prevent adversarial claims." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:522 +#, c-format +msgid "Minimum age required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:523 +#, c-format +msgid "" +"Any value greater than 0 will limit the coins able be used to pay this " +"contract. If empty the age restriction will be defined by the products" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:526 +#, c-format +msgid "Min age defined by the producs is %1$s" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:534 +#, c-format +msgid "Additional information" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:535 +#, c-format +msgid "Custom information to be included in the contract for this order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:541 +#, c-format +msgid "You must enter a value in JavaScript Object Notation (JSON)." +msgstr "" + +#: src/components/picker/DurationPicker.tsx:55 +#, c-format +msgid "days" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:65 +#, c-format +msgid "hours" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:76 +#, c-format +msgid "minutes" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:87 +#, c-format +msgid "seconds" +msgstr "" + +#: src/components/form/InputDuration.tsx:53 +#, c-format +msgid "forever" +msgstr "" + +#: src/components/form/InputDuration.tsx:62 +#, c-format +msgid "%1$sM" +msgstr "" + +#: src/components/form/InputDuration.tsx:64 +#, c-format +msgid "%1$sY" +msgstr "" + +#: src/components/form/InputDuration.tsx:66 +#, c-format +msgid "%1$sd" +msgstr "" + +#: src/components/form/InputDuration.tsx:68 +#, c-format +msgid "%1$sh" +msgstr "" + +#: src/components/form/InputDuration.tsx:70 +#, c-format +msgid "%1$smin" +msgstr "" + +#: src/components/form/InputDuration.tsx:72 +#, c-format +msgid "%1$ssec" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:75 +#, c-format +msgid "Orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:81 +#, c-format +msgid "create order" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:147 +#, c-format +msgid "load newer orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:154 +#, c-format +msgid "Date" +msgstr "Data" + +#: src/paths/instance/orders/list/Table.tsx:200 +#, c-format +msgid "Refund" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:209 +#, c-format +msgid "copy url" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:225 +#, c-format +msgid "load older orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:242 +#, c-format +msgid "No orders have been found matching your query!" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:288 +#, c-format +msgid "duplicated" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:299 +#, c-format +msgid "invalid format" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:301 +#, c-format +msgid "this value exceed the refundable amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:346 +#, c-format +msgid "date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:349 +#, c-format +msgid "amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:352 +#, c-format +msgid "reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:389 +#, c-format +msgid "amount to be refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:391 +#, c-format +msgid "Max refundable:" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:396 +#, c-format +msgid "Reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:397 +#, c-format +msgid "Choose one..." +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:399 +#, c-format +msgid "requested by the customer" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:400 +#, c-format +msgid "other" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:403 +#, c-format +msgid "why this order is being refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:409 +#, c-format +msgid "more information to give context" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:62 +#, c-format +msgid "Contract Terms" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:68 +#, c-format +msgid "human-readable description of the whole purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:74 +#, c-format +msgid "total price for the transaction" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:81 +#, c-format +msgid "URL for this purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:87 +#, c-format +msgid "Max fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:88 +#, c-format +msgid "maximum total deposit fee accepted by the merchant for this contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:93 +#, c-format +msgid "Max wire fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:94 +#, c-format +msgid "maximum wire fee accepted by the merchant" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:100 +#, c-format +msgid "" +"over how many customer transactions does the merchant expect to amortize " +"wire fees on average" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:105 +#, c-format +msgid "Created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:106 +#, c-format +msgid "time when this contract was generated" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:112 +#, c-format +msgid "after this deadline has passed no refunds will be accepted" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:118 +#, c-format +msgid "" +"after this deadline, the merchant won't accept payments for the contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:124 +#, c-format +msgid "transfer deadline for the exchange" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:130 +#, c-format +msgid "time indicating when the order should be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:136 +#, c-format +msgid "where the order will be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:144 +#, c-format +msgid "Auto-refund delay" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:145 +#, c-format +msgid "" +"how long the wallet should try to get an automatic refund for the purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:150 +#, c-format +msgid "Extra info" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:151 +#, c-format +msgid "extra data that is only interpreted by the merchant frontend" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:219 +#, c-format +msgid "Order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:221 +#, c-format +msgid "claimed" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:247 +#, c-format +msgid "claimed at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:265 +#, c-format +msgid "Timeline" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:271 +#, c-format +msgid "Payment details" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:291 +#, c-format +msgid "Order status" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:301 +#, c-format +msgid "Product list" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:451 +#, c-format +msgid "paid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:455 +#, c-format +msgid "wired" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:460 +#, c-format +msgid "refunded" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:480 +#, c-format +msgid "refund order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:481 +#, c-format +msgid "not refundable" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:489 +#, c-format +msgid "refund" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:553 +#, c-format +msgid "Refunded amount" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:560 +#, c-format +msgid "Refund taken" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:570 +#, c-format +msgid "Status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:583 +#, c-format +msgid "Refund URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:636 +#, c-format +msgid "unpaid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:654 +#, c-format +msgid "pay at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:666 +#, c-format +msgid "created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:707 +#, c-format +msgid "Order status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:711 +#, c-format +msgid "Payment URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:740 +#, c-format +msgid "" +"Unknown order status. This is an error, please contact the administrator." +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:767 +#, c-format +msgid "Back" +msgstr "Indietro" + +#: src/paths/instance/orders/details/index.tsx:79 +#, c-format +msgid "refund created successfully" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:85 +#, c-format +msgid "could not create the refund" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:78 +#, c-format +msgid "select date to show nearby orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:94 +#, c-format +msgid "order id" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:100 +#, c-format +msgid "jump to order with the given order ID" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:122 +#, c-format +msgid "remove all filters" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:132 +#, c-format +msgid "only show paid orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:135 +#, c-format +msgid "Paid" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:142 +#, c-format +msgid "only show orders with refunds" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:145 +#, c-format +msgid "Refunded" +msgstr "Rimborsato" + +#: src/paths/instance/orders/list/ListPage.tsx:152 +#, c-format +msgid "" +"only show orders where customers paid, but wire payments from payment " +"provider are still pending" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:155 +#, c-format +msgid "Not wired" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:170 +#, c-format +msgid "clear date filter" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:184 +#, c-format +msgid "date (YYYY/MM/DD)" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:103 +#, c-format +msgid "Enter an order id" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:111 +#, c-format +msgid "order not found" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:178 +#, c-format +msgid "could not get the order to refund" +msgstr "" + +#: src/components/exception/AsyncButton.tsx:43 +#, c-format +msgid "Loading..." +msgstr "" + +#: src/components/form/InputStock.tsx:99 +#, c-format +msgid "" +"click here to configure the stock of the product, leave it as is and the " +"backend will not control stock" +msgstr "" + +#: src/components/form/InputStock.tsx:109 +#, c-format +msgid "Manage stock" +msgstr "" + +#: src/components/form/InputStock.tsx:115 +#, c-format +msgid "this product has been configured without stock control" +msgstr "" + +#: src/components/form/InputStock.tsx:119 +#, c-format +msgid "Infinite" +msgstr "" + +#: src/components/form/InputStock.tsx:136 +#, c-format +msgid "lost cannot be greater than current and incoming (max %1$s)" +msgstr "" + +#: src/components/form/InputStock.tsx:176 +#, c-format +msgid "Incoming" +msgstr "" + +#: src/components/form/InputStock.tsx:177 +#, c-format +msgid "Lost" +msgstr "" + +#: src/components/form/InputStock.tsx:192 +#, c-format +msgid "Current" +msgstr "" + +#: src/components/form/InputStock.tsx:196 +#, c-format +msgid "remove stock control for this product" +msgstr "" + +#: src/components/form/InputStock.tsx:202 +#, c-format +msgid "without stock" +msgstr "" + +#: src/components/form/InputStock.tsx:211 +#, c-format +msgid "Next restock" +msgstr "" + +#: src/components/form/InputStock.tsx:217 +#, c-format +msgid "Delivery address" +msgstr "" + +#: src/components/product/ProductForm.tsx:133 +#, c-format +msgid "product identification to use in URLs (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:139 +#, c-format +msgid "illustration of the product for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:145 +#, c-format +msgid "product description for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:149 +#, c-format +msgid "Age restricted" +msgstr "" + +#: src/components/product/ProductForm.tsx:150 +#, c-format +msgid "is this product restricted for customer below certain age?" +msgstr "" + +#: src/components/product/ProductForm.tsx:155 +#, c-format +msgid "" +"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 " +"items, 5 meters) for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:160 +#, c-format +msgid "" +"sale price for customers, including taxes, for above units of the product" +msgstr "" + +#: src/components/product/ProductForm.tsx:164 +#, c-format +msgid "Stock" +msgstr "" + +#: src/components/product/ProductForm.tsx:166 +#, c-format +msgid "" +"product inventory for products with finite supply (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:171 +#, c-format +msgid "taxes included in the product price, exposed to customers" +msgstr "" + +#: src/paths/instance/products/create/CreatePage.tsx:66 +#, c-format +msgid "Need to complete marked fields" +msgstr "" + +#: src/paths/instance/products/create/index.tsx:51 +#, c-format +msgid "could not create product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:68 +#, c-format +msgid "Products" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:73 +#, c-format +msgid "add product to inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:137 +#, c-format +msgid "Sell" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:143 +#, c-format +msgid "Profit" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:149 +#, c-format +msgid "Sold" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:210 +#, c-format +msgid "free" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:248 +#, c-format +msgid "go to product update page" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:255 +#, c-format +msgid "Update" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:260 +#, c-format +msgid "remove this product from the database" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:331 +#, c-format +msgid "update the product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:341 +#, c-format +msgid "update product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:399 +#, c-format +msgid "add more elements to the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:404 +#, c-format +msgid "report elements lost in the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:409 +#, c-format +msgid "new price for the product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:421 +#, c-format +msgid "the are value with errors" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:422 +#, c-format +msgid "update product with new stock and price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:463 +#, c-format +msgid "There is no products yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:86 +#, c-format +msgid "product updated successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:92 +#, c-format +msgid "could not update the product" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:103 +#, c-format +msgid "product delete successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:109 +#, c-format +msgid "could not delete the product" +msgstr "" + +#: src/paths/instance/products/update/UpdatePage.tsx:56 +#, c-format +msgid "Product id:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95 +#, c-format +msgid "" +"To complete the setup of the reserve, you must now initiate a wire transfer " +"using the given wire transfer subject and crediting the specified amount to " +"the indicated account of the exchange." +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102 +#, c-format +msgid "If your system supports RFC 8905, you can do this by opening this URI:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:83 +#, c-format +msgid "it should be greater than 0" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:88 +#, c-format +msgid "must be a valid URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:107 +#, c-format +msgid "Initial balance" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:108 +#, c-format +msgid "balance prior to deposit" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:112 +#, c-format +msgid "Exchange URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:113 +#, c-format +msgid "URL of exchange" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:148 +#, c-format +msgid "Next" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:186 +#, c-format +msgid "Wire method" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:187 +#, c-format +msgid "method to use for wire transfer" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:189 +#, c-format +msgid "Select one wire method" +msgstr "" + +#: src/paths/instance/reserves/create/index.tsx:62 +#, c-format +msgid "could not create reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:77 +#, c-format +msgid "Valid until" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:82 +#, c-format +msgid "Created balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:99 +#, c-format +msgid "Exchange balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:104 +#, c-format +msgid "Picked up" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:109 +#, c-format +msgid "Committed" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:116 +#, c-format +msgid "Account address" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:119 +#, c-format +msgid "Subject" +msgstr "Soggetto" + +#: src/paths/instance/reserves/details/DetailPage.tsx:130 +#, c-format +msgid "Tips" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:193 +#, c-format +msgid "No tips has been authorized from this reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:213 +#, c-format +msgid "Authorized" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:222 +#, c-format +msgid "Expiration" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108 +#, c-format +msgid "amount of tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112 +#, c-format +msgid "Justification" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114 +#, c-format +msgid "reason for the tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118 +#, c-format +msgid "URL after tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119 +#, c-format +msgid "URL to visit after tip payment" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:65 +#, c-format +msgid "Reserves not yet funded" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:89 +#, c-format +msgid "Reserves ready" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:95 +#, c-format +msgid "add new reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:143 +#, c-format +msgid "Expires at" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:146 +#, c-format +msgid "Initial" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:202 +#, c-format +msgid "delete selected reserve from the database" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:210 +#, c-format +msgid "authorize new tip from selected reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:237 +#, c-format +msgid "" +"There is no ready reserves yet, add more pressing the + sign or fund them" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:264 +#, c-format +msgid "Expected Balance" +msgstr "" + +#: src/paths/instance/reserves/list/index.tsx:110 +#, c-format +msgid "could not create the tip" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:77 +#, c-format +msgid "should not be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:93 +#, c-format +msgid "should be greater that 0" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:96 +#, c-format +msgid "can't be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:100 +#, c-format +msgid "to short" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:108 +#, c-format +msgid "just letters and numbers from 2 to 7" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:110 +#, c-format +msgid "size of the key should be 32" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:137 +#, c-format +msgid "Identifier" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:138 +#, c-format +msgid "Name of the template in URLs." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:144 +#, c-format +msgid "Describe what this template stands for" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:149 +#, c-format +msgid "Fixed summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:150 +#, c-format +msgid "If specified, this template will create order with the same summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:154 +#, c-format +msgid "Fixed price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:155 +#, c-format +msgid "If specified, this template will create order with the same price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:159 +#, c-format +msgid "Minimum age" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:161 +#, c-format +msgid "Is this contract restricted to some age?" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:165 +#, c-format +msgid "Payment timeout" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:167 +#, c-format +msgid "" +"How much time has the customer to complete the payment once the order was " +"created." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:171 +#, c-format +msgid "Verification algorithm" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:172 +#, c-format +msgid "Algorithm to use to verify transaction in offline mode" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:180 +#, c-format +msgid "Point-of-sale key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:182 +#, c-format +msgid "Useful to validate the purchase" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:196 +#, c-format +msgid "generate random secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:203 +#, c-format +msgid "random" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:208 +#, c-format +msgid "show secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:209 +#, c-format +msgid "hide secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:216 +#, c-format +msgid "hide" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:218 +#, c-format +msgid "show" +msgstr "" + +#: src/paths/instance/templates/create/index.tsx:52 +#, c-format +msgid "could not inform template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:54 +#, c-format +msgid "Amount is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:58 +#, c-format +msgid "Order summary is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:86 +#, c-format +msgid "New order for template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:108 +#, c-format +msgid "Amount of the order" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:113 +#, c-format +msgid "Order summary" +msgstr "" + +#: src/paths/instance/templates/use/index.tsx:92 +#, c-format +msgid "could not create order from template" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:131 +#, c-format +msgid "" +"Here you can specify a default value for fields that are not fixed. Default " +"values can be edited by the customer before the payment." +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:148 +#, c-format +msgid "Fixed amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:149 +#, c-format +msgid "Default amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:161 +#, c-format +msgid "Default summary" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:177 +#, c-format +msgid "Print" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:184 +#, c-format +msgid "Setup TOTP" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:65 +#, c-format +msgid "Templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:70 +#, c-format +msgid "add new templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:142 +#, c-format +msgid "load more templates before the first one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:146 +#, c-format +msgid "load newer templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:181 +#, c-format +msgid "delete selected templates from the database" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:188 +#, c-format +msgid "use template to create new order" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:195 +#, c-format +msgid "create qr code for the template" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:210 +#, c-format +msgid "load more templates after the last one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:214 +#, c-format +msgid "load older templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:231 +#, c-format +msgid "There is no templates yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:104 +#, c-format +msgid "template delete successfully" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:110 +#, c-format +msgid "could not delete the template" +msgstr "" + +#: src/paths/instance/templates/update/index.tsx:90 +#, c-format +msgid "could not update template" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:57 +#, c-format +msgid "should be one of '%1$s'" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:85 +#, c-format +msgid "Webhook ID to use" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:89 +#, c-format +msgid "Event" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:90 +#, c-format +msgid "The event of the webhook: why the webhook is used" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:94 +#, c-format +msgid "Method" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:95 +#, c-format +msgid "Method used by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:99 +#, c-format +msgid "URL" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:100 +#, c-format +msgid "URL of the webhook where the customer will be redirected" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:104 +#, c-format +msgid "Header" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:106 +#, c-format +msgid "Header template of the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:111 +#, c-format +msgid "Body" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:112 +#, c-format +msgid "Body template by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:61 +#, c-format +msgid "Webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:66 +#, c-format +msgid "add new webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:137 +#, c-format +msgid "load more webhooks before the first one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:141 +#, c-format +msgid "load newer webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:151 +#, c-format +msgid "Event type" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:176 +#, c-format +msgid "delete selected webhook from the database" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:198 +#, c-format +msgid "load more webhooks after the last one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:202 +#, c-format +msgid "load older webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:219 +#, c-format +msgid "There is no webhooks yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:94 +#, c-format +msgid "webhook delete successfully" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:100 +#, c-format +msgid "could not delete the webhook" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:63 +#, c-format +msgid "check the id, does not look valid" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:65 +#, c-format +msgid "should have 52 characters, current %1$s" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:72 +#, c-format +msgid "URL doesn't have the right format" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:98 +#, c-format +msgid "Credited bank account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:100 +#, c-format +msgid "Select one account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:101 +#, c-format +msgid "Bank account of the merchant where the payment was received" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:105 +#, c-format +msgid "Wire transfer ID" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:107 +#, c-format +msgid "" +"unique identifier of the wire transfer used by the exchange, must be 52 " +"characters long" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:112 +#, c-format +msgid "" +"Base URL of the exchange that made the transfer, should have been in the " +"wire transfer subject" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:117 +#, c-format +msgid "Amount credited" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:118 +#, c-format +msgid "Actual amount that was wired to the merchant's bank account" +msgstr "" + +#: src/paths/instance/transfers/create/index.tsx:58 +#, c-format +msgid "could not inform transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:61 +#, c-format +msgid "Transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:66 +#, c-format +msgid "add new transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:129 +#, c-format +msgid "load more transfers before the first one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:133 +#, c-format +msgid "load newer transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:143 +#, c-format +msgid "Credit" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:152 +#, c-format +msgid "Confirmed" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:155 +#, c-format +msgid "Verified" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:158 +#, c-format +msgid "Executed at" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "yes" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "no" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:181 +#, c-format +msgid "unknown" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:187 +#, c-format +msgid "delete selected transfer from the database" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:202 +#, c-format +msgid "load more transfer after the last one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:206 +#, c-format +msgid "load older transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:223 +#, c-format +msgid "There is no transfer yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:79 +#, c-format +msgid "filter by account address" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:100 +#, c-format +msgid "only show wire transfers confirmed by the merchant" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:110 +#, c-format +msgid "only show wire transfers claimed by the exchange" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:113 +#, c-format +msgid "Unverified" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:69 +#, c-format +msgid "is not valid" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:94 +#, c-format +msgid "is not a number" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:96 +#, c-format +msgid "must be 1 or greater" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:107 +#, c-format +msgid "max 7 lines" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:178 +#, c-format +msgid "change authorization configuration" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:217 +#, c-format +msgid "Need to complete marked fields and choose authorization method" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:82 +#, c-format +msgid "This is not a valid bitcoin address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:95 +#, c-format +msgid "This is not a valid Ethereum address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:118 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:120 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:128 +#, c-format +msgid "IBAN country code not found" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:153 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:248 +#, c-format +msgid "Target type" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:249 +#, c-format +msgid "Method to use for wire transfer" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:258 +#, c-format +msgid "Routing" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:259 +#, c-format +msgid "Routing number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:263 +#, c-format +msgid "Account" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:264 +#, c-format +msgid "Account number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:273 +#, c-format +msgid "Business Identifier Code." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:282 +#, c-format +msgid "Bank Account Number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:292 +#, c-format +msgid "Unified Payment Interface." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:301 +#, c-format +msgid "Bitcoin protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:310 +#, c-format +msgid "Ethereum protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:319 +#, c-format +msgid "Interledger protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:328 +#, c-format +msgid "Host" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:329 +#, c-format +msgid "Bank host." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:334 +#, c-format +msgid "Bank account." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:343 +#, c-format +msgid "Bank account owner's name." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:370 +#, c-format +msgid "No accounts yet." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:52 +#, c-format +msgid "" +"Name of the instance in URLs. The 'default' instance is special in that it " +"is used to administer other instances." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:58 +#, c-format +msgid "Business name" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:59 +#, c-format +msgid "Legal name of the business represented by this instance." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:64 +#, c-format +msgid "Email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:65 +#, c-format +msgid "Contact email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:70 +#, c-format +msgid "Website URL" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:71 +#, c-format +msgid "URL." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:76 +#, c-format +msgid "Logo" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:77 +#, c-format +msgid "Logo image." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:82 +#, c-format +msgid "Bank account" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:83 +#, c-format +msgid "URI specifying bank account for crediting revenue." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:88 +#, c-format +msgid "Default max deposit fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:89 +#, c-format +msgid "" +"Maximum deposit fees this merchant is willing to pay per order by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:94 +#, c-format +msgid "Default max wire fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:95 +#, c-format +msgid "" +"Maximum wire fees this merchant is willing to pay per wire transfer by " +"default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:100 +#, c-format +msgid "Default wire fee amortization" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:101 +#, c-format +msgid "" +"Number of orders excess wire transfer fees will be divided by to compute per " +"order surcharge." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:107 +#, c-format +msgid "Physical location of the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:114 +#, c-format +msgid "Jurisdiction" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:115 +#, c-format +msgid "Jurisdiction for legal disputes with the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:122 +#, c-format +msgid "Default payment delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:124 +#, c-format +msgid "" +"Time customers have to pay an order before the offer expires by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:129 +#, c-format +msgid "Default wire transfer delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:130 +#, c-format +msgid "" +"Maximum time an exchange is allowed to delay wiring funds to the merchant, " +"enabling it to aggregate smaller payments into larger wire transfers and " +"reducing wire fees." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:164 +#, c-format +msgid "Instance id" +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:173 +#, c-format +msgid "Change the authorization method use for this instance." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:182 +#, c-format +msgid "Manage access token" +msgstr "" + +#: src/paths/instance/update/index.tsx:112 +#, c-format +msgid "Failed to create instance" +msgstr "" + +#: src/components/exception/login.tsx:74 +#, c-format +msgid "Login required" +msgstr "" + +#: src/components/exception/login.tsx:80 +#, c-format +msgid "Please enter your access token." +msgstr "" + +#: src/components/exception/login.tsx:108 +#, c-format +msgid "Access Token" +msgstr "" + +#: src/InstanceRoutes.tsx:171 +#, c-format +msgid "The request to the backend take too long and was cancelled" +msgstr "" + +#: src/InstanceRoutes.tsx:172 +#, c-format +msgid "Diagnostic from %1$s is \"%2$s\"" +msgstr "" + +#: src/InstanceRoutes.tsx:178 +#, c-format +msgid "The backend reported a problem: HTTP status #%1$s" +msgstr "" + +#: src/InstanceRoutes.tsx:179 +#, c-format +msgid "Diagnostic from %1$s is '%2$s'" +msgstr "" + +#: src/InstanceRoutes.tsx:196 +#, c-format +msgid "Access denied" +msgstr "" + +#: src/InstanceRoutes.tsx:197 +#, c-format +msgid "The access token provided is invalid." +msgstr "" + +#: src/InstanceRoutes.tsx:212 +#, c-format +msgid "No 'default' instance configured yet." +msgstr "" + +#: src/InstanceRoutes.tsx:213 +#, c-format +msgid "Create a 'default' instance to begin using the merchant backoffice." +msgstr "" + +#: src/InstanceRoutes.tsx:630 +#, c-format +msgid "The access token provided is invalid" +msgstr "" + +#: src/InstanceRoutes.tsx:664 +#, c-format +msgid "Hide for today" +msgstr "" + +#: src/components/menu/SideBar.tsx:82 +#, c-format +msgid "Instance" +msgstr "" + +#: src/components/menu/SideBar.tsx:91 +#, c-format +msgid "Settings" +msgstr "Impostazioni" + +#: src/components/menu/SideBar.tsx:167 +#, c-format +msgid "Connection" +msgstr "" + +#: src/components/menu/SideBar.tsx:209 +#, c-format +msgid "New" +msgstr "" + +#: src/components/menu/SideBar.tsx:219 +#, c-format +msgid "List" +msgstr "" + +#: src/components/menu/SideBar.tsx:234 +#, c-format +msgid "Log out" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:71 +#, c-format +msgid "Check your token is valid" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:90 +#, c-format +msgid "Couldn't access the server." +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:91 +#, c-format +msgid "Could not infer instance id from url %1$s" +msgstr "" + +#: src/Application.tsx:104 +#, c-format +msgid "Server not found" +msgstr "" + +#: src/Application.tsx:118 +#, c-format +msgid "Server response with an error code" +msgstr "" + +#: src/Application.tsx:120 +#, c-format +msgid "Got message %1$s from %2$s" +msgstr "" + +#: src/Application.tsx:131 +#, c-format +msgid "Response from server is unreadable, http status: %1$s" +msgstr "" + +#: src/Application.tsx:144 +#, c-format +msgid "Unexpected Error" +msgstr "" + +#: src/components/form/InputArray.tsx:101 +#, c-format +msgid "The value %1$s is invalid for a payment url" +msgstr "" + +#: src/components/form/InputArray.tsx:110 +#, c-format +msgid "add element to the list" +msgstr "" + +#: src/components/form/InputArray.tsx:112 +#, c-format +msgid "add" +msgstr "" + +#: src/components/form/InputSecured.tsx:37 +#, c-format +msgid "Deleting" +msgstr "" + +#: src/components/form/InputSecured.tsx:41 +#, c-format +msgid "Changing" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87 +#, c-format +msgid "Order ID" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101 +#, c-format +msgid "Payment URL" +msgstr "" diff --git a/packages/auditor-backoffice-ui/src/i18n/poheader b/packages/auditor-backoffice-ui/src/i18n/poheader new file mode 100644 index 000000000..7ddcf49b8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/poheader @@ -0,0 +1,27 @@ +# This file is part of GNU Taler +# (C) 2021-2023 Taler Systems S.A. + +# GNU Taler is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. + +# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" diff --git a/packages/auditor-backoffice-ui/src/i18n/strings-prelude b/packages/auditor-backoffice-ui/src/i18n/strings-prelude new file mode 100644 index 000000000..6c68662de --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/strings-prelude @@ -0,0 +1,19 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/*eslint quote-props: ["error", "consistent"]*/ +export const strings: {[s: string]: any} = {}; + diff --git a/packages/auditor-backoffice-ui/src/i18n/strings.ts b/packages/auditor-backoffice-ui/src/i18n/strings.ts new file mode 100644 index 000000000..65dc41358 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/strings.ts @@ -0,0 +1,9655 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/*eslint quote-props: ["error", "consistent"]*/ +export const strings: {[s: string]: any} = {}; + +strings['de'] = { + "domain": "messages", + "locale_data": { + "messages": { + "": { + "domain": "messages", + "plural_forms": "nplurals=2; plural=(n != 1);", + "lang": "" + }, + "Cancel": [ + "" + ], + "%1$s": [ + "" + ], + "Close": [ + "" + ], + "Continue": [ + "" + ], + "Clear": [ + "" + ], + "Confirm": [ + "" + ], + "is not the same as the current access token": [ + "" + ], + "cannot be empty": [ + "" + ], + "cannot be the same as the old token": [ + "" + ], + "is not the same": [ + "" + ], + "You are updating the access token from instance with id %1$s": [ + "" + ], + "Old access token": [ + "" + ], + "access token currently in use": [ + "" + ], + "New access token": [ + "" + ], + "next access token to be used": [ + "" + ], + "Repeat access token": [ + "" + ], + "confirm the same access token": [ + "" + ], + "Clearing the access token will mean public access to the instance": [ + "" + ], + "cannot be the same as the old access token": [ + "" + ], + "You are setting the access token for the new instance": [ + "" + ], + "With external authorization method no check will be done by the merchant backend": [ + "" + ], + "Set external authorization": [ + "" + ], + "Set access token": [ + "" + ], + "Operation in progress...": [ + "" + ], + "The operation will be automatically canceled after %1$s seconds": [ + "" + ], + "Instances": [ + "" + ], + "Delete": [ + "" + ], + "add new instance": [ + "" + ], + "ID": [ + "" + ], + "Name": [ + "" + ], + "Edit": [ + "" + ], + "Purge": [ + "" + ], + "There is no instances yet, add more pressing the + sign": [ + "" + ], + "Only show active instances": [ + "" + ], + "Active": [ + "" + ], + "Only show deleted instances": [ + "" + ], + "Deleted": [ + "" + ], + "Show all instances": [ + "" + ], + "All": [ + "" + ], + "Instance \"%1$s\" (ID: %2$s) has been deleted": [ + "" + ], + "Failed to delete instance": [ + "" + ], + "Instance '%1$s' (ID: %2$s) has been disabled": [ + "" + ], + "Failed to purge instance": [ + "" + ], + "Pending KYC verification": [ + "" + ], + "Timed out": [ + "" + ], + "Exchange": [ + "" + ], + "Target account": [ + "" + ], + "KYC URL": [ + "" + ], + "Code": [ + "" + ], + "Http Status": [ + "" + ], + "No pending kyc verification!": [ + "" + ], + "change value to unknown date": [ + "" + ], + "change value to empty": [ + "" + ], + "clear": [ + "" + ], + "change value to never": [ + "" + ], + "never": [ + "" + ], + "Country": [ + "" + ], + "Address": [ + "" + ], + "Building number": [ + "" + ], + "Building name": [ + "" + ], + "Street": [ + "" + ], + "Post code": [ + "" + ], + "Town location": [ + "" + ], + "Town": [ + "" + ], + "District": [ + "" + ], + "Country subdivision": [ + "" + ], + "Product id": [ + "" + ], + "Description": [ + "" + ], + "Product": [ + "" + ], + "search products by it's description or id": [ + "" + ], + "no products found with that description": [ + "" + ], + "You must enter a valid product identifier.": [ + "" + ], + "Quantity must be greater than 0!": [ + "" + ], + "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [ + "" + ], + "Quantity": [ + "" + ], + "how many products will be added": [ + "" + ], + "Add from inventory": [ + "" + ], + "Image should be smaller than 1 MB": [ + "" + ], + "Add": [ + "" + ], + "Remove": [ + "" + ], + "No taxes configured for this product.": [ + "" + ], + "Amount": [ + "" + ], + "Taxes can be in currencies that differ from the main currency used by the merchant.": [ + "" + ], + "Enter currency and value separated with a colon, e.g. "USD:2.3".": [ + "" + ], + "Legal name of the tax, e.g. VAT or import duties.": [ + "" + ], + "add tax to the tax list": [ + "" + ], + "describe and add a product that is not in the inventory list": [ + "" + ], + "Add custom product": [ + "" + ], + "Complete information of the product": [ + "" + ], + "Image": [ + "" + ], + "photo of the product": [ + "" + ], + "full product description": [ + "" + ], + "Unit": [ + "" + ], + "name of the product unit": [ + "" + ], + "Price": [ + "" + ], + "amount in the current currency": [ + "" + ], + "Taxes": [ + "" + ], + "image": [ + "" + ], + "description": [ + "" + ], + "quantity": [ + "" + ], + "unit price": [ + "" + ], + "total price": [ + "" + ], + "required": [ + "" + ], + "not valid": [ + "" + ], + "must be greater than 0": [ + "" + ], + "not a valid json": [ + "" + ], + "should be in the future": [ + "" + ], + "refund deadline cannot be before pay deadline": [ + "" + ], + "wire transfer deadline cannot be before refund deadline": [ + "" + ], + "wire transfer deadline cannot be before pay deadline": [ + "" + ], + "should have a refund deadline": [ + "" + ], + "auto refund cannot be after refund deadline": [ + "" + ], + "Manage products in order": [ + "" + ], + "Manage list of products in the order.": [ + "" + ], + "Remove this product from the order.": [ + "" + ], + "Total price": [ + "" + ], + "total product price added up": [ + "" + ], + "Amount to be paid by the customer": [ + "" + ], + "Order price": [ + "" + ], + "final order price": [ + "" + ], + "Summary": [ + "" + ], + "Title of the order to be shown to the customer": [ + "" + ], + "Shipping and Fulfillment": [ + "" + ], + "Delivery date": [ + "" + ], + "Deadline for physical delivery assured by the merchant.": [ + "" + ], + "Location": [ + "" + ], + "address where the products will be delivered": [ + "" + ], + "Fulfillment URL": [ + "" + ], + "URL to which the user will be redirected after successful payment.": [ + "" + ], + "Taler payment options": [ + "" + ], + "Override default Taler payment settings for this order": [ + "" + ], + "Payment deadline": [ + "" + ], + "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [ + "" + ], + "Refund deadline": [ + "" + ], + "Time until which the order can be refunded by the merchant.": [ + "" + ], + "Wire transfer deadline": [ + "" + ], + "Deadline for the exchange to make the wire transfer.": [ + "" + ], + "Auto-refund deadline": [ + "" + ], + "Time until which the wallet will automatically check for refunds without user interaction.": [ + "" + ], + "Maximum deposit fee": [ + "" + ], + "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [ + "" + ], + "Maximum wire fee": [ + "" + ], + "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [ + "" + ], + "Wire fee amortization": [ + "" + ], + "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [ + "" + ], + "Create token": [ + "" + ], + "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [ + "" + ], + "Minimum age required": [ + "" + ], + "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [ + "" + ], + "Min age defined by the producs is %1$s": [ + "" + ], + "Additional information": [ + "" + ], + "Custom information to be included in the contract for this order.": [ + "" + ], + "You must enter a value in JavaScript Object Notation (JSON).": [ + "" + ], + "days": [ + "" + ], + "hours": [ + "" + ], + "minutes": [ + "" + ], + "seconds": [ + "" + ], + "forever": [ + "" + ], + "%1$sM": [ + "" + ], + "%1$sY": [ + "" + ], + "%1$sd": [ + "" + ], + "%1$sh": [ + "" + ], + "%1$smin": [ + "" + ], + "%1$ssec": [ + "" + ], + "Orders": [ + "" + ], + "create order": [ + "" + ], + "load newer orders": [ + "" + ], + "Date": [ + "" + ], + "Refund": [ + "" + ], + "copy url": [ + "" + ], + "load older orders": [ + "" + ], + "No orders have been found matching your query!": [ + "" + ], + "duplicated": [ + "" + ], + "invalid format": [ + "" + ], + "this value exceed the refundable amount": [ + "" + ], + "date": [ + "" + ], + "amount": [ + "" + ], + "reason": [ + "" + ], + "amount to be refunded": [ + "" + ], + "Max refundable:": [ + "" + ], + "Reason": [ + "" + ], + "Choose one...": [ + "" + ], + "requested by the customer": [ + "" + ], + "other": [ + "" + ], + "why this order is being refunded": [ + "" + ], + "more information to give context": [ + "" + ], + "Contract Terms": [ + "" + ], + "human-readable description of the whole purchase": [ + "" + ], + "total price for the transaction": [ + "" + ], + "URL for this purchase": [ + "" + ], + "Max fee": [ + "" + ], + "maximum total deposit fee accepted by the merchant for this contract": [ + "" + ], + "Max wire fee": [ + "" + ], + "maximum wire fee accepted by the merchant": [ + "" + ], + "over how many customer transactions does the merchant expect to amortize wire fees on average": [ + "" + ], + "Created at": [ + "" + ], + "time when this contract was generated": [ + "" + ], + "after this deadline has passed no refunds will be accepted": [ + "" + ], + "after this deadline, the merchant won't accept payments for the contract": [ + "" + ], + "transfer deadline for the exchange": [ + "" + ], + "time indicating when the order should be delivered": [ + "" + ], + "where the order will be delivered": [ + "" + ], + "Auto-refund delay": [ + "" + ], + "how long the wallet should try to get an automatic refund for the purchase": [ + "" + ], + "Extra info": [ + "" + ], + "extra data that is only interpreted by the merchant frontend": [ + "" + ], + "Order": [ + "" + ], + "claimed": [ + "" + ], + "claimed at": [ + "" + ], + "Timeline": [ + "" + ], + "Payment details": [ + "" + ], + "Order status": [ + "" + ], + "Product list": [ + "" + ], + "paid": [ + "" + ], + "wired": [ + "" + ], + "refunded": [ + "" + ], + "refund order": [ + "" + ], + "not refundable": [ + "" + ], + "refund": [ + "" + ], + "Refunded amount": [ + "" + ], + "Refund taken": [ + "" + ], + "Status URL": [ + "" + ], + "Refund URI": [ + "" + ], + "unpaid": [ + "" + ], + "pay at": [ + "" + ], + "created at": [ + "" + ], + "Order status URL": [ + "" + ], + "Payment URI": [ + "" + ], + "Unknown order status. This is an error, please contact the administrator.": [ + "" + ], + "Back": [ + "" + ], + "refund created successfully": [ + "" + ], + "could not create the refund": [ + "" + ], + "select date to show nearby orders": [ + "" + ], + "order id": [ + "" + ], + "jump to order with the given order ID": [ + "" + ], + "remove all filters": [ + "" + ], + "only show paid orders": [ + "" + ], + "Paid": [ + "" + ], + "only show orders with refunds": [ + "" + ], + "Refunded": [ + "" + ], + "only show orders where customers paid, but wire payments from payment provider are still pending": [ + "" + ], + "Not wired": [ + "" + ], + "clear date filter": [ + "" + ], + "date (YYYY/MM/DD)": [ + "" + ], + "Enter an order id": [ + "" + ], + "order not found": [ + "" + ], + "could not get the order to refund": [ + "" + ], + "Loading...": [ + "" + ], + "click here to configure the stock of the product, leave it as is and the backend will not control stock": [ + "" + ], + "Manage stock": [ + "" + ], + "this product has been configured without stock control": [ + "" + ], + "Infinite": [ + "" + ], + "lost cannot be greater than current and incoming (max %1$s)": [ + "" + ], + "Incoming": [ + "" + ], + "Lost": [ + "" + ], + "Current": [ + "" + ], + "remove stock control for this product": [ + "" + ], + "without stock": [ + "" + ], + "Next restock": [ + "" + ], + "Delivery address": [ + "" + ], + "product identification to use in URLs (for internal use only)": [ + "" + ], + "illustration of the product for customers": [ + "" + ], + "product description for customers": [ + "" + ], + "Age restricted": [ + "" + ], + "is this product restricted for customer below certain age?": [ + "" + ], + "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [ + "" + ], + "sale price for customers, including taxes, for above units of the product": [ + "" + ], + "Stock": [ + "" + ], + "product inventory for products with finite supply (for internal use only)": [ + "" + ], + "taxes included in the product price, exposed to customers": [ + "" + ], + "Need to complete marked fields": [ + "" + ], + "could not create product": [ + "" + ], + "Products": [ + "" + ], + "add product to inventory": [ + "" + ], + "Sell": [ + "" + ], + "Profit": [ + "" + ], + "Sold": [ + "" + ], + "free": [ + "" + ], + "go to product update page": [ + "" + ], + "Update": [ + "" + ], + "remove this product from the database": [ + "" + ], + "update the product with new price": [ + "" + ], + "update product with new price": [ + "" + ], + "add more elements to the inventory": [ + "" + ], + "report elements lost in the inventory": [ + "" + ], + "new price for the product": [ + "" + ], + "the are value with errors": [ + "" + ], + "update product with new stock and price": [ + "" + ], + "There is no products yet, add more pressing the + sign": [ + "" + ], + "product updated successfully": [ + "" + ], + "could not update the product": [ + "" + ], + "product delete successfully": [ + "" + ], + "could not delete the product": [ + "" + ], + "Product id:": [ + "" + ], + "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [ + "" + ], + "If your system supports RFC 8905, you can do this by opening this URI:": [ + "" + ], + "it should be greater than 0": [ + "" + ], + "must be a valid URL": [ + "" + ], + "Initial balance": [ + "" + ], + "balance prior to deposit": [ + "" + ], + "Exchange URL": [ + "" + ], + "URL of exchange": [ + "" + ], + "Next": [ + "" + ], + "Wire method": [ + "" + ], + "method to use for wire transfer": [ + "" + ], + "Select one wire method": [ + "" + ], + "could not create reserve": [ + "" + ], + "Valid until": [ + "" + ], + "Created balance": [ + "" + ], + "Exchange balance": [ + "" + ], + "Picked up": [ + "" + ], + "Committed": [ + "" + ], + "Account address": [ + "" + ], + "Subject": [ + "" + ], + "Tips": [ + "" + ], + "No tips has been authorized from this reserve": [ + "" + ], + "Authorized": [ + "" + ], + "Expiration": [ + "" + ], + "amount of tip": [ + "" + ], + "Justification": [ + "" + ], + "reason for the tip": [ + "" + ], + "URL after tip": [ + "" + ], + "URL to visit after tip payment": [ + "" + ], + "Reserves not yet funded": [ + "" + ], + "Reserves ready": [ + "" + ], + "add new reserve": [ + "" + ], + "Expires at": [ + "" + ], + "Initial": [ + "" + ], + "delete selected reserve from the database": [ + "" + ], + "authorize new tip from selected reserve": [ + "" + ], + "There is no ready reserves yet, add more pressing the + sign or fund them": [ + "" + ], + "Expected Balance": [ + "" + ], + "could not create the tip": [ + "" + ], + "should not be empty": [ + "" + ], + "should be greater that 0": [ + "" + ], + "can't be empty": [ + "" + ], + "to short": [ + "" + ], + "just letters and numbers from 2 to 7": [ + "" + ], + "size of the key should be 32": [ + "" + ], + "Identifier": [ + "" + ], + "Name of the template in URLs.": [ + "" + ], + "Describe what this template stands for": [ + "" + ], + "Fixed summary": [ + "" + ], + "If specified, this template will create order with the same summary": [ + "" + ], + "Fixed price": [ + "" + ], + "If specified, this template will create order with the same price": [ + "" + ], + "Minimum age": [ + "" + ], + "Is this contract restricted to some age?": [ + "" + ], + "Payment timeout": [ + "" + ], + "How much time has the customer to complete the payment once the order was created.": [ + "" + ], + "Verification algorithm": [ + "" + ], + "Algorithm to use to verify transaction in offline mode": [ + "" + ], + "Point-of-sale key": [ + "" + ], + "Useful to validate the purchase": [ + "" + ], + "generate random secret key": [ + "" + ], + "random": [ + "" + ], + "show secret key": [ + "" + ], + "hide secret key": [ + "" + ], + "hide": [ + "" + ], + "show": [ + "" + ], + "could not inform template": [ + "" + ], + "Amount is required": [ + "" + ], + "Order summary is required": [ + "" + ], + "New order for template": [ + "" + ], + "Amount of the order": [ + "" + ], + "Order summary": [ + "" + ], + "could not create order from template": [ + "" + ], + "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [ + "" + ], + "Fixed amount": [ + "" + ], + "Default amount": [ + "" + ], + "Default summary": [ + "" + ], + "Print": [ + "" + ], + "Setup TOTP": [ + "" + ], + "Templates": [ + "" + ], + "add new templates": [ + "" + ], + "load more templates before the first one": [ + "" + ], + "load newer templates": [ + "" + ], + "delete selected templates from the database": [ + "" + ], + "use template to create new order": [ + "" + ], + "create qr code for the template": [ + "" + ], + "load more templates after the last one": [ + "" + ], + "load older templates": [ + "" + ], + "There is no templates yet, add more pressing the + sign": [ + "" + ], + "template delete successfully": [ + "" + ], + "could not delete the template": [ + "" + ], + "could not update template": [ + "" + ], + "should be one of '%1$s'": [ + "" + ], + "Webhook ID to use": [ + "" + ], + "Event": [ + "" + ], + "The event of the webhook: why the webhook is used": [ + "" + ], + "Method": [ + "" + ], + "Method used by the webhook": [ + "" + ], + "URL": [ + "" + ], + "URL of the webhook where the customer will be redirected": [ + "" + ], + "Header": [ + "" + ], + "Header template of the webhook": [ + "" + ], + "Body": [ + "" + ], + "Body template by the webhook": [ + "" + ], + "Webhooks": [ + "" + ], + "add new webhooks": [ + "" + ], + "load more webhooks before the first one": [ + "" + ], + "load newer webhooks": [ + "" + ], + "Event type": [ + "" + ], + "delete selected webhook from the database": [ + "" + ], + "load more webhooks after the last one": [ + "" + ], + "load older webhooks": [ + "" + ], + "There is no webhooks yet, add more pressing the + sign": [ + "" + ], + "webhook delete successfully": [ + "" + ], + "could not delete the webhook": [ + "" + ], + "check the id, does not look valid": [ + "" + ], + "should have 52 characters, current %1$s": [ + "" + ], + "URL doesn't have the right format": [ + "" + ], + "Credited bank account": [ + "" + ], + "Select one account": [ + "" + ], + "Bank account of the merchant where the payment was received": [ + "" + ], + "Wire transfer ID": [ + "" + ], + "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [ + "" + ], + "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [ + "" + ], + "Amount credited": [ + "" + ], + "Actual amount that was wired to the merchant's bank account": [ + "" + ], + "could not inform transfer": [ + "" + ], + "Transfers": [ + "" + ], + "add new transfer": [ + "" + ], + "load more transfers before the first one": [ + "" + ], + "load newer transfers": [ + "" + ], + "Credit": [ + "" + ], + "Confirmed": [ + "" + ], + "Verified": [ + "" + ], + "Executed at": [ + "" + ], + "yes": [ + "" + ], + "no": [ + "" + ], + "unknown": [ + "" + ], + "delete selected transfer from the database": [ + "" + ], + "load more transfer after the last one": [ + "" + ], + "load older transfers": [ + "" + ], + "There is no transfer yet, add more pressing the + sign": [ + "" + ], + "filter by account address": [ + "" + ], + "only show wire transfers confirmed by the merchant": [ + "" + ], + "only show wire transfers claimed by the exchange": [ + "" + ], + "Unverified": [ + "" + ], + "is not valid": [ + "" + ], + "is not a number": [ + "" + ], + "must be 1 or greater": [ + "" + ], + "max 7 lines": [ + "" + ], + "change authorization configuration": [ + "" + ], + "Need to complete marked fields and choose authorization method": [ + "" + ], + "This is not a valid bitcoin address.": [ + "" + ], + "This is not a valid Ethereum address.": [ + "" + ], + "IBAN numbers usually have more that 4 digits": [ + "" + ], + "IBAN numbers usually have less that 34 digits": [ + "" + ], + "IBAN country code not found": [ + "" + ], + "IBAN number is not valid, checksum is wrong": [ + "" + ], + "Target type": [ + "" + ], + "Method to use for wire transfer": [ + "" + ], + "Routing": [ + "" + ], + "Routing number.": [ + "" + ], + "Account": [ + "" + ], + "Account number.": [ + "" + ], + "Business Identifier Code.": [ + "" + ], + "Bank Account Number.": [ + "" + ], + "Unified Payment Interface.": [ + "" + ], + "Bitcoin protocol.": [ + "" + ], + "Ethereum protocol.": [ + "" + ], + "Interledger protocol.": [ + "" + ], + "Host": [ + "" + ], + "Bank host.": [ + "" + ], + "Bank account.": [ + "" + ], + "Bank account owner's name.": [ + "" + ], + "No accounts yet.": [ + "" + ], + "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [ + "" + ], + "Business name": [ + "" + ], + "Legal name of the business represented by this instance.": [ + "" + ], + "Email": [ + "" + ], + "Contact email": [ + "" + ], + "Website URL": [ + "" + ], + "URL.": [ + "" + ], + "Logo": [ + "" + ], + "Logo image.": [ + "" + ], + "Bank account": [ + "" + ], + "URI specifying bank account for crediting revenue.": [ + "" + ], + "Default max deposit fee": [ + "" + ], + "Maximum deposit fees this merchant is willing to pay per order by default.": [ + "" + ], + "Default max wire fee": [ + "" + ], + "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [ + "" + ], + "Default wire fee amortization": [ + "" + ], + "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [ + "" + ], + "Physical location of the merchant.": [ + "" + ], + "Jurisdiction": [ + "" + ], + "Jurisdiction for legal disputes with the merchant.": [ + "" + ], + "Default payment delay": [ + "" + ], + "Time customers have to pay an order before the offer expires by default.": [ + "" + ], + "Default wire transfer delay": [ + "" + ], + "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [ + "" + ], + "Instance id": [ + "" + ], + "Change the authorization method use for this instance.": [ + "" + ], + "Manage access token": [ + "" + ], + "Failed to create instance": [ + "" + ], + "Login required": [ + "" + ], + "Please enter your access token.": [ + "" + ], + "Access Token": [ + "" + ], + "The request to the backend take too long and was cancelled": [ + "" + ], + "Diagnostic from %1$s is \"%2$s\"": [ + "" + ], + "The backend reported a problem: HTTP status #%1$s": [ + "" + ], + "Diagnostic from %1$s is '%2$s'": [ + "" + ], + "Access denied": [ + "" + ], + "The access token provided is invalid.": [ + "" + ], + "No 'default' instance configured yet.": [ + "" + ], + "Create a 'default' instance to begin using the merchant backoffice.": [ + "" + ], + "The access token provided is invalid": [ + "" + ], + "Hide for today": [ + "" + ], + "Instance": [ + "" + ], + "Settings": [ + "" + ], + "Connection": [ + "" + ], + "New": [ + "" + ], + "List": [ + "" + ], + "Log out": [ + "" + ], + "Check your token is valid": [ + "" + ], + "Couldn't access the server.": [ + "" + ], + "Could not infer instance id from url %1$s": [ + "" + ], + "Server not found": [ + "" + ], + "Server response with an error code": [ + "" + ], + "Got message %1$s from %2$s": [ + "" + ], + "Response from server is unreadable, http status: %1$s": [ + "" + ], + "Unexpected Error": [ + "" + ], + "The value %1$s is invalid for a payment url": [ + "" + ], + "add element to the list": [ + "" + ], + "add": [ + "" + ], + "Deleting": [ + "" + ], + "Changing": [ + "" + ], + "Order ID": [ + "" + ], + "Payment URL": [ + "" + ] + } + } +}; + +strings['en'] = { + "domain": "messages", + "locale_data": { + "messages": { + "": { + "domain": "messages", + "plural_forms": "nplurals=2; plural=(n != 1);", + "lang": "" + }, + "Cancel": [ + "" + ], + "%1$s": [ + "" + ], + "Close": [ + "" + ], + "Continue": [ + "" + ], + "Clear": [ + "" + ], + "Confirm": [ + "" + ], + "is not the same as the current access token": [ + "" + ], + "cannot be empty": [ + "" + ], + "cannot be the same as the old token": [ + "" + ], + "is not the same": [ + "" + ], + "You are updating the access token from instance with id %1$s": [ + "" + ], + "Old access token": [ + "" + ], + "access token currently in use": [ + "" + ], + "New access token": [ + "" + ], + "next access token to be used": [ + "" + ], + "Repeat access token": [ + "" + ], + "confirm the same access token": [ + "" + ], + "Clearing the access token will mean public access to the instance": [ + "" + ], + "cannot be the same as the old access token": [ + "" + ], + "You are setting the access token for the new instance": [ + "" + ], + "With external authorization method no check will be done by the merchant backend": [ + "" + ], + "Set external authorization": [ + "" + ], + "Set access token": [ + "" + ], + "Operation in progress...": [ + "" + ], + "The operation will be automatically canceled after %1$s seconds": [ + "" + ], + "Instances": [ + "" + ], + "Delete": [ + "" + ], + "add new instance": [ + "" + ], + "ID": [ + "" + ], + "Name": [ + "" + ], + "Edit": [ + "" + ], + "Purge": [ + "" + ], + "There is no instances yet, add more pressing the + sign": [ + "" + ], + "Only show active instances": [ + "" + ], + "Active": [ + "" + ], + "Only show deleted instances": [ + "" + ], + "Deleted": [ + "" + ], + "Show all instances": [ + "" + ], + "All": [ + "" + ], + "Instance \"%1$s\" (ID: %2$s) has been deleted": [ + "" + ], + "Failed to delete instance": [ + "" + ], + "Instance '%1$s' (ID: %2$s) has been disabled": [ + "" + ], + "Failed to purge instance": [ + "" + ], + "Pending KYC verification": [ + "" + ], + "Timed out": [ + "" + ], + "Exchange": [ + "" + ], + "Target account": [ + "" + ], + "KYC URL": [ + "" + ], + "Code": [ + "" + ], + "Http Status": [ + "" + ], + "No pending kyc verification!": [ + "" + ], + "change value to unknown date": [ + "" + ], + "change value to empty": [ + "" + ], + "clear": [ + "" + ], + "change value to never": [ + "" + ], + "never": [ + "" + ], + "Country": [ + "" + ], + "Address": [ + "" + ], + "Building number": [ + "" + ], + "Building name": [ + "" + ], + "Street": [ + "" + ], + "Post code": [ + "" + ], + "Town location": [ + "" + ], + "Town": [ + "" + ], + "District": [ + "" + ], + "Country subdivision": [ + "" + ], + "Product id": [ + "" + ], + "Description": [ + "" + ], + "Product": [ + "" + ], + "search products by it's description or id": [ + "" + ], + "no products found with that description": [ + "" + ], + "You must enter a valid product identifier.": [ + "" + ], + "Quantity must be greater than 0!": [ + "" + ], + "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [ + "" + ], + "Quantity": [ + "" + ], + "how many products will be added": [ + "" + ], + "Add from inventory": [ + "" + ], + "Image should be smaller than 1 MB": [ + "" + ], + "Add": [ + "" + ], + "Remove": [ + "" + ], + "No taxes configured for this product.": [ + "" + ], + "Amount": [ + "" + ], + "Taxes can be in currencies that differ from the main currency used by the merchant.": [ + "" + ], + "Enter currency and value separated with a colon, e.g. "USD:2.3".": [ + "" + ], + "Legal name of the tax, e.g. VAT or import duties.": [ + "" + ], + "add tax to the tax list": [ + "" + ], + "describe and add a product that is not in the inventory list": [ + "" + ], + "Add custom product": [ + "" + ], + "Complete information of the product": [ + "" + ], + "Image": [ + "" + ], + "photo of the product": [ + "" + ], + "full product description": [ + "" + ], + "Unit": [ + "" + ], + "name of the product unit": [ + "" + ], + "Price": [ + "" + ], + "amount in the current currency": [ + "" + ], + "Taxes": [ + "" + ], + "image": [ + "" + ], + "description": [ + "" + ], + "quantity": [ + "" + ], + "unit price": [ + "" + ], + "total price": [ + "" + ], + "required": [ + "" + ], + "not valid": [ + "" + ], + "must be greater than 0": [ + "" + ], + "not a valid json": [ + "" + ], + "should be in the future": [ + "" + ], + "refund deadline cannot be before pay deadline": [ + "" + ], + "wire transfer deadline cannot be before refund deadline": [ + "" + ], + "wire transfer deadline cannot be before pay deadline": [ + "" + ], + "should have a refund deadline": [ + "" + ], + "auto refund cannot be after refund deadline": [ + "" + ], + "Manage products in order": [ + "" + ], + "Manage list of products in the order.": [ + "" + ], + "Remove this product from the order.": [ + "" + ], + "Total price": [ + "" + ], + "total product price added up": [ + "" + ], + "Amount to be paid by the customer": [ + "" + ], + "Order price": [ + "" + ], + "final order price": [ + "" + ], + "Summary": [ + "" + ], + "Title of the order to be shown to the customer": [ + "" + ], + "Shipping and Fulfillment": [ + "" + ], + "Delivery date": [ + "" + ], + "Deadline for physical delivery assured by the merchant.": [ + "" + ], + "Location": [ + "" + ], + "address where the products will be delivered": [ + "" + ], + "Fulfillment URL": [ + "" + ], + "URL to which the user will be redirected after successful payment.": [ + "" + ], + "Taler payment options": [ + "" + ], + "Override default Taler payment settings for this order": [ + "" + ], + "Payment deadline": [ + "" + ], + "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [ + "" + ], + "Refund deadline": [ + "" + ], + "Time until which the order can be refunded by the merchant.": [ + "" + ], + "Wire transfer deadline": [ + "" + ], + "Deadline for the exchange to make the wire transfer.": [ + "" + ], + "Auto-refund deadline": [ + "" + ], + "Time until which the wallet will automatically check for refunds without user interaction.": [ + "" + ], + "Maximum deposit fee": [ + "" + ], + "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [ + "" + ], + "Maximum wire fee": [ + "" + ], + "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [ + "" + ], + "Wire fee amortization": [ + "" + ], + "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [ + "" + ], + "Create token": [ + "" + ], + "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [ + "" + ], + "Minimum age required": [ + "" + ], + "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [ + "" + ], + "Min age defined by the producs is %1$s": [ + "" + ], + "Additional information": [ + "" + ], + "Custom information to be included in the contract for this order.": [ + "" + ], + "You must enter a value in JavaScript Object Notation (JSON).": [ + "" + ], + "days": [ + "" + ], + "hours": [ + "" + ], + "minutes": [ + "" + ], + "seconds": [ + "" + ], + "forever": [ + "" + ], + "%1$sM": [ + "" + ], + "%1$sY": [ + "" + ], + "%1$sd": [ + "" + ], + "%1$sh": [ + "" + ], + "%1$smin": [ + "" + ], + "%1$ssec": [ + "" + ], + "Orders": [ + "" + ], + "create order": [ + "" + ], + "load newer orders": [ + "" + ], + "Date": [ + "" + ], + "Refund": [ + "" + ], + "copy url": [ + "" + ], + "load older orders": [ + "" + ], + "No orders have been found matching your query!": [ + "" + ], + "duplicated": [ + "" + ], + "invalid format": [ + "" + ], + "this value exceed the refundable amount": [ + "" + ], + "date": [ + "" + ], + "amount": [ + "" + ], + "reason": [ + "" + ], + "amount to be refunded": [ + "" + ], + "Max refundable:": [ + "" + ], + "Reason": [ + "" + ], + "Choose one...": [ + "" + ], + "requested by the customer": [ + "" + ], + "other": [ + "" + ], + "why this order is being refunded": [ + "" + ], + "more information to give context": [ + "" + ], + "Contract Terms": [ + "" + ], + "human-readable description of the whole purchase": [ + "" + ], + "total price for the transaction": [ + "" + ], + "URL for this purchase": [ + "" + ], + "Max fee": [ + "" + ], + "maximum total deposit fee accepted by the merchant for this contract": [ + "" + ], + "Max wire fee": [ + "" + ], + "maximum wire fee accepted by the merchant": [ + "" + ], + "over how many customer transactions does the merchant expect to amortize wire fees on average": [ + "" + ], + "Created at": [ + "" + ], + "time when this contract was generated": [ + "" + ], + "after this deadline has passed no refunds will be accepted": [ + "" + ], + "after this deadline, the merchant won't accept payments for the contract": [ + "" + ], + "transfer deadline for the exchange": [ + "" + ], + "time indicating when the order should be delivered": [ + "" + ], + "where the order will be delivered": [ + "" + ], + "Auto-refund delay": [ + "" + ], + "how long the wallet should try to get an automatic refund for the purchase": [ + "" + ], + "Extra info": [ + "" + ], + "extra data that is only interpreted by the merchant frontend": [ + "" + ], + "Order": [ + "" + ], + "claimed": [ + "" + ], + "claimed at": [ + "" + ], + "Timeline": [ + "" + ], + "Payment details": [ + "" + ], + "Order status": [ + "" + ], + "Product list": [ + "" + ], + "paid": [ + "" + ], + "wired": [ + "" + ], + "refunded": [ + "" + ], + "refund order": [ + "" + ], + "not refundable": [ + "" + ], + "refund": [ + "" + ], + "Refunded amount": [ + "" + ], + "Refund taken": [ + "" + ], + "Status URL": [ + "" + ], + "Refund URI": [ + "" + ], + "unpaid": [ + "" + ], + "pay at": [ + "" + ], + "created at": [ + "" + ], + "Order status URL": [ + "" + ], + "Payment URI": [ + "" + ], + "Unknown order status. This is an error, please contact the administrator.": [ + "" + ], + "Back": [ + "" + ], + "refund created successfully": [ + "" + ], + "could not create the refund": [ + "" + ], + "select date to show nearby orders": [ + "" + ], + "order id": [ + "" + ], + "jump to order with the given order ID": [ + "" + ], + "remove all filters": [ + "" + ], + "only show paid orders": [ + "" + ], + "Paid": [ + "" + ], + "only show orders with refunds": [ + "" + ], + "Refunded": [ + "" + ], + "only show orders where customers paid, but wire payments from payment provider are still pending": [ + "" + ], + "Not wired": [ + "" + ], + "clear date filter": [ + "" + ], + "date (YYYY/MM/DD)": [ + "" + ], + "Enter an order id": [ + "" + ], + "order not found": [ + "" + ], + "could not get the order to refund": [ + "" + ], + "Loading...": [ + "" + ], + "click here to configure the stock of the product, leave it as is and the backend will not control stock": [ + "" + ], + "Manage stock": [ + "" + ], + "this product has been configured without stock control": [ + "" + ], + "Infinite": [ + "" + ], + "lost cannot be greater than current and incoming (max %1$s)": [ + "" + ], + "Incoming": [ + "" + ], + "Lost": [ + "" + ], + "Current": [ + "" + ], + "remove stock control for this product": [ + "" + ], + "without stock": [ + "" + ], + "Next restock": [ + "" + ], + "Delivery address": [ + "" + ], + "product identification to use in URLs (for internal use only)": [ + "" + ], + "illustration of the product for customers": [ + "" + ], + "product description for customers": [ + "" + ], + "Age restricted": [ + "" + ], + "is this product restricted for customer below certain age?": [ + "" + ], + "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [ + "" + ], + "sale price for customers, including taxes, for above units of the product": [ + "" + ], + "Stock": [ + "" + ], + "product inventory for products with finite supply (for internal use only)": [ + "" + ], + "taxes included in the product price, exposed to customers": [ + "" + ], + "Need to complete marked fields": [ + "" + ], + "could not create product": [ + "" + ], + "Products": [ + "" + ], + "add product to inventory": [ + "" + ], + "Sell": [ + "" + ], + "Profit": [ + "" + ], + "Sold": [ + "" + ], + "free": [ + "" + ], + "go to product update page": [ + "" + ], + "Update": [ + "" + ], + "remove this product from the database": [ + "" + ], + "update the product with new price": [ + "" + ], + "update product with new price": [ + "" + ], + "add more elements to the inventory": [ + "" + ], + "report elements lost in the inventory": [ + "" + ], + "new price for the product": [ + "" + ], + "the are value with errors": [ + "" + ], + "update product with new stock and price": [ + "" + ], + "There is no products yet, add more pressing the + sign": [ + "" + ], + "product updated successfully": [ + "" + ], + "could not update the product": [ + "" + ], + "product delete successfully": [ + "" + ], + "could not delete the product": [ + "" + ], + "Product id:": [ + "" + ], + "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [ + "" + ], + "If your system supports RFC 8905, you can do this by opening this URI:": [ + "" + ], + "it should be greater than 0": [ + "" + ], + "must be a valid URL": [ + "" + ], + "Initial balance": [ + "" + ], + "balance prior to deposit": [ + "" + ], + "Exchange URL": [ + "" + ], + "URL of exchange": [ + "" + ], + "Next": [ + "" + ], + "Wire method": [ + "" + ], + "method to use for wire transfer": [ + "" + ], + "Select one wire method": [ + "" + ], + "could not create reserve": [ + "" + ], + "Valid until": [ + "" + ], + "Created balance": [ + "" + ], + "Exchange balance": [ + "" + ], + "Picked up": [ + "" + ], + "Committed": [ + "" + ], + "Account address": [ + "" + ], + "Subject": [ + "" + ], + "Tips": [ + "" + ], + "No tips has been authorized from this reserve": [ + "" + ], + "Authorized": [ + "" + ], + "Expiration": [ + "" + ], + "amount of tip": [ + "" + ], + "Justification": [ + "" + ], + "reason for the tip": [ + "" + ], + "URL after tip": [ + "" + ], + "URL to visit after tip payment": [ + "" + ], + "Reserves not yet funded": [ + "" + ], + "Reserves ready": [ + "" + ], + "add new reserve": [ + "" + ], + "Expires at": [ + "" + ], + "Initial": [ + "" + ], + "delete selected reserve from the database": [ + "" + ], + "authorize new tip from selected reserve": [ + "" + ], + "There is no ready reserves yet, add more pressing the + sign or fund them": [ + "" + ], + "Expected Balance": [ + "" + ], + "could not create the tip": [ + "" + ], + "should not be empty": [ + "" + ], + "should be greater that 0": [ + "" + ], + "can't be empty": [ + "" + ], + "to short": [ + "" + ], + "just letters and numbers from 2 to 7": [ + "" + ], + "size of the key should be 32": [ + "" + ], + "Identifier": [ + "" + ], + "Name of the template in URLs.": [ + "" + ], + "Describe what this template stands for": [ + "" + ], + "Fixed summary": [ + "" + ], + "If specified, this template will create order with the same summary": [ + "" + ], + "Fixed price": [ + "" + ], + "If specified, this template will create order with the same price": [ + "" + ], + "Minimum age": [ + "" + ], + "Is this contract restricted to some age?": [ + "" + ], + "Payment timeout": [ + "" + ], + "How much time has the customer to complete the payment once the order was created.": [ + "" + ], + "Verification algorithm": [ + "" + ], + "Algorithm to use to verify transaction in offline mode": [ + "" + ], + "Point-of-sale key": [ + "" + ], + "Useful to validate the purchase": [ + "" + ], + "generate random secret key": [ + "" + ], + "random": [ + "" + ], + "show secret key": [ + "" + ], + "hide secret key": [ + "" + ], + "hide": [ + "" + ], + "show": [ + "" + ], + "could not inform template": [ + "" + ], + "Amount is required": [ + "" + ], + "Order summary is required": [ + "" + ], + "New order for template": [ + "" + ], + "Amount of the order": [ + "" + ], + "Order summary": [ + "" + ], + "could not create order from template": [ + "" + ], + "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [ + "" + ], + "Fixed amount": [ + "" + ], + "Default amount": [ + "" + ], + "Default summary": [ + "" + ], + "Print": [ + "" + ], + "Setup TOTP": [ + "" + ], + "Templates": [ + "" + ], + "add new templates": [ + "" + ], + "load more templates before the first one": [ + "" + ], + "load newer templates": [ + "" + ], + "delete selected templates from the database": [ + "" + ], + "use template to create new order": [ + "" + ], + "create qr code for the template": [ + "" + ], + "load more templates after the last one": [ + "" + ], + "load older templates": [ + "" + ], + "There is no templates yet, add more pressing the + sign": [ + "" + ], + "template delete successfully": [ + "" + ], + "could not delete the template": [ + "" + ], + "could not update template": [ + "" + ], + "should be one of '%1$s'": [ + "" + ], + "Webhook ID to use": [ + "" + ], + "Event": [ + "" + ], + "The event of the webhook: why the webhook is used": [ + "" + ], + "Method": [ + "" + ], + "Method used by the webhook": [ + "" + ], + "URL": [ + "" + ], + "URL of the webhook where the customer will be redirected": [ + "" + ], + "Header": [ + "" + ], + "Header template of the webhook": [ + "" + ], + "Body": [ + "" + ], + "Body template by the webhook": [ + "" + ], + "Webhooks": [ + "" + ], + "add new webhooks": [ + "" + ], + "load more webhooks before the first one": [ + "" + ], + "load newer webhooks": [ + "" + ], + "Event type": [ + "" + ], + "delete selected webhook from the database": [ + "" + ], + "load more webhooks after the last one": [ + "" + ], + "load older webhooks": [ + "" + ], + "There is no webhooks yet, add more pressing the + sign": [ + "" + ], + "webhook delete successfully": [ + "" + ], + "could not delete the webhook": [ + "" + ], + "check the id, does not look valid": [ + "" + ], + "should have 52 characters, current %1$s": [ + "" + ], + "URL doesn't have the right format": [ + "" + ], + "Credited bank account": [ + "" + ], + "Select one account": [ + "" + ], + "Bank account of the merchant where the payment was received": [ + "" + ], + "Wire transfer ID": [ + "" + ], + "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [ + "" + ], + "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [ + "" + ], + "Amount credited": [ + "" + ], + "Actual amount that was wired to the merchant's bank account": [ + "" + ], + "could not inform transfer": [ + "" + ], + "Transfers": [ + "" + ], + "add new transfer": [ + "" + ], + "load more transfers before the first one": [ + "" + ], + "load newer transfers": [ + "" + ], + "Credit": [ + "" + ], + "Confirmed": [ + "" + ], + "Verified": [ + "" + ], + "Executed at": [ + "" + ], + "yes": [ + "" + ], + "no": [ + "" + ], + "unknown": [ + "" + ], + "delete selected transfer from the database": [ + "" + ], + "load more transfer after the last one": [ + "" + ], + "load older transfers": [ + "" + ], + "There is no transfer yet, add more pressing the + sign": [ + "" + ], + "filter by account address": [ + "" + ], + "only show wire transfers confirmed by the merchant": [ + "" + ], + "only show wire transfers claimed by the exchange": [ + "" + ], + "Unverified": [ + "" + ], + "is not valid": [ + "" + ], + "is not a number": [ + "" + ], + "must be 1 or greater": [ + "" + ], + "max 7 lines": [ + "" + ], + "change authorization configuration": [ + "" + ], + "Need to complete marked fields and choose authorization method": [ + "" + ], + "This is not a valid bitcoin address.": [ + "" + ], + "This is not a valid Ethereum address.": [ + "" + ], + "IBAN numbers usually have more that 4 digits": [ + "" + ], + "IBAN numbers usually have less that 34 digits": [ + "" + ], + "IBAN country code not found": [ + "" + ], + "IBAN number is not valid, checksum is wrong": [ + "" + ], + "Target type": [ + "" + ], + "Method to use for wire transfer": [ + "" + ], + "Routing": [ + "" + ], + "Routing number.": [ + "" + ], + "Account": [ + "" + ], + "Account number.": [ + "" + ], + "Business Identifier Code.": [ + "" + ], + "Bank Account Number.": [ + "" + ], + "Unified Payment Interface.": [ + "" + ], + "Bitcoin protocol.": [ + "" + ], + "Ethereum protocol.": [ + "" + ], + "Interledger protocol.": [ + "" + ], + "Host": [ + "" + ], + "Bank host.": [ + "" + ], + "Bank account.": [ + "" + ], + "Bank account owner's name.": [ + "" + ], + "No accounts yet.": [ + "" + ], + "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [ + "" + ], + "Business name": [ + "" + ], + "Legal name of the business represented by this instance.": [ + "" + ], + "Email": [ + "" + ], + "Contact email": [ + "" + ], + "Website URL": [ + "" + ], + "URL.": [ + "" + ], + "Logo": [ + "" + ], + "Logo image.": [ + "" + ], + "Bank account": [ + "" + ], + "URI specifying bank account for crediting revenue.": [ + "" + ], + "Default max deposit fee": [ + "" + ], + "Maximum deposit fees this merchant is willing to pay per order by default.": [ + "" + ], + "Default max wire fee": [ + "" + ], + "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [ + "" + ], + "Default wire fee amortization": [ + "" + ], + "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [ + "" + ], + "Physical location of the merchant.": [ + "" + ], + "Jurisdiction": [ + "" + ], + "Jurisdiction for legal disputes with the merchant.": [ + "" + ], + "Default payment delay": [ + "" + ], + "Time customers have to pay an order before the offer expires by default.": [ + "" + ], + "Default wire transfer delay": [ + "" + ], + "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [ + "" + ], + "Instance id": [ + "" + ], + "Change the authorization method use for this instance.": [ + "" + ], + "Manage access token": [ + "" + ], + "Failed to create instance": [ + "" + ], + "Login required": [ + "" + ], + "Please enter your access token.": [ + "" + ], + "Access Token": [ + "" + ], + "The request to the backend take too long and was cancelled": [ + "" + ], + "Diagnostic from %1$s is \"%2$s\"": [ + "" + ], + "The backend reported a problem: HTTP status #%1$s": [ + "" + ], + "Diagnostic from %1$s is '%2$s'": [ + "" + ], + "Access denied": [ + "" + ], + "The access token provided is invalid.": [ + "" + ], + "No 'default' instance configured yet.": [ + "" + ], + "Create a 'default' instance to begin using the merchant backoffice.": [ + "" + ], + "The access token provided is invalid": [ + "" + ], + "Hide for today": [ + "" + ], + "Instance": [ + "" + ], + "Settings": [ + "" + ], + "Connection": [ + "" + ], + "New": [ + "" + ], + "List": [ + "" + ], + "Log out": [ + "" + ], + "Check your token is valid": [ + "" + ], + "Couldn't access the server.": [ + "" + ], + "Could not infer instance id from url %1$s": [ + "" + ], + "Server not found": [ + "" + ], + "Server response with an error code": [ + "" + ], + "Got message %1$s from %2$s": [ + "" + ], + "Response from server is unreadable, http status: %1$s": [ + "" + ], + "Unexpected Error": [ + "" + ], + "The value %1$s is invalid for a payment url": [ + "" + ], + "add element to the list": [ + "" + ], + "add": [ + "" + ], + "Deleting": [ + "" + ], + "Changing": [ + "" + ], + "Order ID": [ + "" + ], + "Payment URL": [ + "" + ] + } + } +}; + +strings['es'] = { + "domain": "messages", + "locale_data": { + "messages": { + "": { + "domain": "messages", + "plural_forms": "nplurals=2; plural=n != 1;", + "lang": "es" + }, + "Cancel": [ + "Cancelar" + ], + "%1$s": [ + "%1$s" + ], + "Close": [ + "" + ], + "Continue": [ + "Continuar" + ], + "Clear": [ + "Limpiar" + ], + "Confirm": [ + "Confirmar" + ], + "is not the same as the current access token": [ + "no es el mismo que el token de acceso actual" + ], + "cannot be empty": [ + "no puede ser vacÃo" + ], + "cannot be the same as the old token": [ + "no puede ser igual al viejo token" + ], + "is not the same": [ + "no son iguales" + ], + "You are updating the access token from instance with id %1$s": [ + "Está actualizando el token de acceso para la instancia con id %1$s" + ], + "Old access token": [ + "Viejo token de acceso" + ], + "access token currently in use": [ + "acceder al token en uso actualmente" + ], + "New access token": [ + "Nuevo token de acceso" + ], + "next access token to be used": [ + "siguiente token de acceso a usar" + ], + "Repeat access token": [ + "Repetir token de acceso" + ], + "confirm the same access token": [ + "confirmar el mismo token de acceso" + ], + "Clearing the access token will mean public access to the instance": [ + "Limpiar el token de acceso significa acceso público a la instancia" + ], + "cannot be the same as the old access token": [ + "no puede ser igual al anterior token de acceso" + ], + "You are setting the access token for the new instance": [ + "Está estableciendo el token de acceso para la nueva instancia" + ], + "With external authorization method no check will be done by the merchant backend": [ + "Con el método de autorización externa no se hará ninguna revisión por el backend del comerciante" + ], + "Set external authorization": [ + "Establecer autorización externa" + ], + "Set access token": [ + "Establecer token de acceso" + ], + "Operation in progress...": [ + "Operación en progreso..." + ], + "The operation will be automatically canceled after %1$s seconds": [ + "La operación será automáticamente cancelada luego de %1$s segundos" + ], + "Instances": [ + "Instancias" + ], + "Delete": [ + "Eliminar" + ], + "add new instance": [ + "agregar nueva instancia" + ], + "ID": [ + "ID" + ], + "Name": [ + "Nombre" + ], + "Edit": [ + "Editar" + ], + "Purge": [ + "Purgar" + ], + "There is no instances yet, add more pressing the + sign": [ + "TodavÃa no hay instancias, agregue más presionando el signo +" + ], + "Only show active instances": [ + "Solo mostrar instancias activas" + ], + "Active": [ + "Activo" + ], + "Only show deleted instances": [ + "Mostrar solo instancias eliminadas" + ], + "Deleted": [ + "Eliminado" + ], + "Show all instances": [ + "Mostrar todas las instancias" + ], + "All": [ + "Todo" + ], + "Instance \"%1$s\" (ID: %2$s) has been deleted": [ + "La instancia '%1$s' (ID: %2$s) fue eliminada" + ], + "Failed to delete instance": [ + "Fallo al eliminar instancia" + ], + "Instance '%1$s' (ID: %2$s) has been disabled": [ + "Instance '%1$s' (ID: %2$s) ha sido deshabilitada" + ], + "Failed to purge instance": [ + "Fallo al purgar la instancia" + ], + "Pending KYC verification": [ + "Verificación KYC pendiente" + ], + "Timed out": [ + "Expirado" + ], + "Exchange": [ + "Exchange" + ], + "Target account": [ + "Cuenta objetivo" + ], + "KYC URL": [ + "URL de KYC" + ], + "Code": [ + "Código" + ], + "Http Status": [ + "Estado http" + ], + "No pending kyc verification!": [ + "¡No hay verificación kyc pendiente!" + ], + "change value to unknown date": [ + "cambiar valor a fecha desconocida" + ], + "change value to empty": [ + "cambiar valor a vacÃo" + ], + "clear": [ + "limpiar" + ], + "change value to never": [ + "cambiar valor a nunca" + ], + "never": [ + "nunca" + ], + "Country": [ + "PaÃs" + ], + "Address": [ + "Dirección" + ], + "Building number": [ + "Número de edificio" + ], + "Building name": [ + "Nombre de edificio" + ], + "Street": [ + "Calle" + ], + "Post code": [ + "Código postal" + ], + "Town location": [ + "Ubicación de ciudad" + ], + "Town": [ + "Ciudad" + ], + "District": [ + "Distrito" + ], + "Country subdivision": [ + "Subdivisión de paÃs" + ], + "Product id": [ + "Id de producto" + ], + "Description": [ + "Descripcion" + ], + "Product": [ + "Productos" + ], + "search products by it's description or id": [ + "buscar productos por su descripción o ID" + ], + "no products found with that description": [ + "no se encontraron productos con esa descripción" + ], + "You must enter a valid product identifier.": [ + "Debe ingresar un identificador de producto válido." + ], + "Quantity must be greater than 0!": [ + "¡Cantidad debe ser mayor que 0!" + ], + "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [ + "Esta cantidad excede las existencias restantes. Actualmente, solo quedan %1$s unidades sin reservar en las existencias." + ], + "Quantity": [ + "Cantidad" + ], + "how many products will be added": [ + "cuántos productos serán agregados" + ], + "Add from inventory": [ + "Agregar del inventario" + ], + "Image should be smaller than 1 MB": [ + "La imagen debe ser mas chica que 1 MB" + ], + "Add": [ + "Agregar" + ], + "Remove": [ + "Eliminar" + ], + "No taxes configured for this product.": [ + "Ningun impuesto configurado para este producto." + ], + "Amount": [ + "Monto" + ], + "Taxes can be in currencies that differ from the main currency used by the merchant.": [ + "Impuestos pueden estar en divisas que difieren de la principal divisa usada por el comerciante." + ], + "Enter currency and value separated with a colon, e.g. "USD:2.3".": [ + "Ingrese divisa y valor separado por dos puntos, e.g. "USD:2.3"." + ], + "Legal name of the tax, e.g. VAT or import duties.": [ + "Nombre legal del impuesto, e.g. IVA o arancel." + ], + "add tax to the tax list": [ + "agregar impuesto a la lista de impuestos" + ], + "describe and add a product that is not in the inventory list": [ + "describa y agregue un producto que no está en la lista de inventarios" + ], + "Add custom product": [ + "Agregue un producto personalizado" + ], + "Complete information of the product": [ + "Complete información del producto" + ], + "Image": [ + "Imagen" + ], + "photo of the product": [ + "foto del producto" + ], + "full product description": [ + "descripción completa del producto" + ], + "Unit": [ + "Unidad" + ], + "name of the product unit": [ + "nombre de la unidad del producto" + ], + "Price": [ + "Precio" + ], + "amount in the current currency": [ + "monto de la divisa actual" + ], + "Taxes": [ + "Impuestos" + ], + "image": [ + "imagen" + ], + "description": [ + "descripción" + ], + "quantity": [ + "cantidad" + ], + "unit price": [ + "precio unitario" + ], + "total price": [ + "precio total" + ], + "required": [ + "requerido" + ], + "not valid": [ + "no es un json válido" + ], + "must be greater than 0": [ + "debe ser mayor que 0" + ], + "not a valid json": [ + "no es un json válido" + ], + "should be in the future": [ + "deberÃan ser en el futuro" + ], + "refund deadline cannot be before pay deadline": [ + "plazo de reembolso no puede ser antes que el plazo de pago" + ], + "wire transfer deadline cannot be before refund deadline": [ + "el plazo de la transferencia bancaria no puede ser antes que el plazo de reembolso" + ], + "wire transfer deadline cannot be before pay deadline": [ + "el plazo de la transferencia bancaria no puede ser antes que el plazo de pago" + ], + "should have a refund deadline": [ + "deberÃa tener un plazo de reembolso" + ], + "auto refund cannot be after refund deadline": [ + "reembolso automático no puede ser después qu el plazo de reembolso" + ], + "Manage products in order": [ + "Manejar productos en orden" + ], + "Manage list of products in the order.": [ + "Manejar lista de productos en la orden." + ], + "Remove this product from the order.": [ + "Remover este producto de la orden." + ], + "Total price": [ + "Precio total" + ], + "total product price added up": [ + "precio total de producto agregado" + ], + "Amount to be paid by the customer": [ + "Monto a ser pagado por el cliente" + ], + "Order price": [ + "Precio de la orden" + ], + "final order price": [ + "Precio final de la orden" + ], + "Summary": [ + "Resumen" + ], + "Title of the order to be shown to the customer": [ + "TÃtulo de la orden a ser mostrado al cliente" + ], + "Shipping and Fulfillment": [ + "EnvÃo y cumplimiento" + ], + "Delivery date": [ + "Fecha de entrega" + ], + "Deadline for physical delivery assured by the merchant.": [ + "Plazo para la entrega fÃsica asegurado por el comerciante." + ], + "Location": [ + "Ubicación" + ], + "address where the products will be delivered": [ + "dirección a donde los productos serán entregados" + ], + "Fulfillment URL": [ + "URL de cumplimiento" + ], + "URL to which the user will be redirected after successful payment.": [ + "URL al cual el usuario será redirigido luego de pago exitoso." + ], + "Taler payment options": [ + "Opciones de pago de Taler" + ], + "Override default Taler payment settings for this order": [ + "Sobreescribir pagos por omisión de Taler para esta orden" + ], + "Payment deadline": [ + "Plazo de pago" + ], + "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [ + "Plazo lÃmite para que el cliente pague por la oferta antes de que expire. Productos del inventario serán reservados hasta este plazo lÃmite." + ], + "Refund deadline": [ + "Plazo de reembolso" + ], + "Time until which the order can be refunded by the merchant.": [ + "Tiempo hasta el cual la orden puede ser reembolsada por el comerciante." + ], + "Wire transfer deadline": [ + "Plazo de la transferencia" + ], + "Deadline for the exchange to make the wire transfer.": [ + "Plazo para que el exchange haga la transferencia." + ], + "Auto-refund deadline": [ + "Plazo de reembolso automático" + ], + "Time until which the wallet will automatically check for refunds without user interaction.": [ + "Tiempo hasta el cual la billetera será automáticamente revisada por reembolsos win interación por parte del usuario." + ], + "Maximum deposit fee": [ + "Máxima tarifa de depósito" + ], + "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [ + "Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para esta orden. Mayores tarifas de depósito deben ser cubiertas completamente por el consumidor." + ], + "Maximum wire fee": [ + "Máxima tarifa de transferencia" + ], + "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [ + "" + ], + "Wire fee amortization": [ + "Amortización de comisión de transferencia" + ], + "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [ + "" + ], + "Create token": [ + "Administrar token" + ], + "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [ + "" + ], + "Minimum age required": [ + "Login necesario" + ], + "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [ + "" + ], + "Min age defined by the producs is %1$s": [ + "" + ], + "Additional information": [ + "Información extra" + ], + "Custom information to be included in the contract for this order.": [ + "" + ], + "You must enter a value in JavaScript Object Notation (JSON).": [ + "" + ], + "days": [ + "dÃas" + ], + "hours": [ + "horas" + ], + "minutes": [ + "minutos" + ], + "seconds": [ + "segundos" + ], + "forever": [ + "nunca" + ], + "%1$sM": [ + "" + ], + "%1$sY": [ + "" + ], + "%1$sd": [ + "" + ], + "%1$sh": [ + "" + ], + "%1$smin": [ + "" + ], + "%1$ssec": [ + "" + ], + "Orders": [ + "Órdenes" + ], + "create order": [ + "creado" + ], + "load newer orders": [ + "cargar nuevas ordenes" + ], + "Date": [ + "Fecha" + ], + "Refund": [ + "Devolución" + ], + "copy url": [ + "copiar url" + ], + "load older orders": [ + "cargar viejas ordenes" + ], + "No orders have been found matching your query!": [ + "¡No se encontraron órdenes que emparejen su búsqueda!" + ], + "duplicated": [ + "duplicado" + ], + "invalid format": [ + "formato inválido" + ], + "this value exceed the refundable amount": [ + "este monto excede el monto reembolsable" + ], + "date": [ + "fecha" + ], + "amount": [ + "monto" + ], + "reason": [ + "razón" + ], + "amount to be refunded": [ + "monto a ser reembolsado" + ], + "Max refundable:": [ + "Máximo reembolzable:" + ], + "Reason": [ + "Razón" + ], + "Choose one...": [ + "Elija uno..." + ], + "requested by the customer": [ + "pedido por el consumidor" + ], + "other": [ + "otro" + ], + "why this order is being refunded": [ + "por qué esta orden está siendo reembolsada" + ], + "more information to give context": [ + "más información para dar contexto" + ], + "Contract Terms": [ + "Términos de contrato" + ], + "human-readable description of the whole purchase": [ + "descripción legible de toda la compra" + ], + "total price for the transaction": [ + "precio total de la transacción" + ], + "URL for this purchase": [ + "URL para esta compra" + ], + "Max fee": [ + "Máxima comisión" + ], + "maximum total deposit fee accepted by the merchant for this contract": [ + "" + ], + "Max wire fee": [ + "Impuesto de transferencia máximo" + ], + "maximum wire fee accepted by the merchant": [ + "" + ], + "over how many customer transactions does the merchant expect to amortize wire fees on average": [ + "" + ], + "Created at": [ + "Creado en" + ], + "time when this contract was generated": [ + "" + ], + "after this deadline has passed no refunds will be accepted": [ + "" + ], + "after this deadline, the merchant won't accept payments for the contract": [ + "" + ], + "transfer deadline for the exchange": [ + "" + ], + "time indicating when the order should be delivered": [ + "" + ], + "where the order will be delivered": [ + "" + ], + "Auto-refund delay": [ + "Plazo de reembolso automático" + ], + "how long the wallet should try to get an automatic refund for the purchase": [ + "" + ], + "Extra info": [ + "Información extra" + ], + "extra data that is only interpreted by the merchant frontend": [ + "" + ], + "Order": [ + "Orden" + ], + "claimed": [ + "reclamado" + ], + "claimed at": [ + "reclamado" + ], + "Timeline": [ + "CronologÃa" + ], + "Payment details": [ + "Detalles de pago" + ], + "Order status": [ + "Estado de orden" + ], + "Product list": [ + "Lista de producto" + ], + "paid": [ + "pagados" + ], + "wired": [ + "transferido" + ], + "refunded": [ + "reembolzado" + ], + "refund order": [ + "reembolzado" + ], + "not refundable": [ + "Máximo reembolzable:" + ], + "refund": [ + "reembolzar" + ], + "Refunded amount": [ + "Monto reembolzado" + ], + "Refund taken": [ + "Reembolzado" + ], + "Status URL": [ + "URL de estado de orden" + ], + "Refund URI": [ + "Devolución" + ], + "unpaid": [ + "impago" + ], + "pay at": [ + "pagar en" + ], + "created at": [ + "creado" + ], + "Order status URL": [ + "URL de estado de orden" + ], + "Payment URI": [ + "URI de pago" + ], + "Unknown order status. This is an error, please contact the administrator.": [ + "Estado de orden desconocido. Esto es un error, por favor contacte a su administrador." + ], + "Back": [ + "" + ], + "refund created successfully": [ + "reembolzo creado satisfactoriamente" + ], + "could not create the refund": [ + "No se pudo create el reembolso" + ], + "select date to show nearby orders": [ + "" + ], + "order id": [ + "ir a id de orden" + ], + "jump to order with the given order ID": [ + "" + ], + "remove all filters": [ + "" + ], + "only show paid orders": [ + "" + ], + "Paid": [ + "Pagado" + ], + "only show orders with refunds": [ + "No se pudo create el reembolso" + ], + "Refunded": [ + "Reembolzado" + ], + "only show orders where customers paid, but wire payments from payment provider are still pending": [ + "" + ], + "Not wired": [ + "No transferido" + ], + "clear date filter": [ + "" + ], + "date (YYYY/MM/DD)": [ + "" + ], + "Enter an order id": [ + "ir a id de orden" + ], + "order not found": [ + "Servidor no encontrado" + ], + "could not get the order to refund": [ + "No se pudo create el reembolso" + ], + "Loading...": [ + "Cargando..." + ], + "click here to configure the stock of the product, leave it as is and the backend will not control stock": [ + "" + ], + "Manage stock": [ + "Administrar stock" + ], + "this product has been configured without stock control": [ + "" + ], + "Infinite": [ + "Inifinito" + ], + "lost cannot be greater than current and incoming (max %1$s)": [ + "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )" + ], + "Incoming": [ + "Ingresando" + ], + "Lost": [ + "Perdido" + ], + "Current": [ + "Actual" + ], + "remove stock control for this product": [ + "" + ], + "without stock": [ + "sin stock" + ], + "Next restock": [ + "Próximo reabastecimiento" + ], + "Delivery address": [ + "Dirección de entrega" + ], + "product identification to use in URLs (for internal use only)": [ + "" + ], + "illustration of the product for customers": [ + "" + ], + "product description for customers": [ + "" + ], + "Age restricted": [ + "" + ], + "is this product restricted for customer below certain age?": [ + "" + ], + "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [ + "" + ], + "sale price for customers, including taxes, for above units of the product": [ + "" + ], + "Stock": [ + "Existencias" + ], + "product inventory for products with finite supply (for internal use only)": [ + "" + ], + "taxes included in the product price, exposed to customers": [ + "" + ], + "Need to complete marked fields": [ + "" + ], + "could not create product": [ + "no se pudo crear el producto" + ], + "Products": [ + "Productos" + ], + "add product to inventory": [ + "" + ], + "Sell": [ + "Venta" + ], + "Profit": [ + "Ganancia" + ], + "Sold": [ + "Vendido" + ], + "free": [ + "Gratis" + ], + "go to product update page": [ + "producto actualizado correctamente" + ], + "Update": [ + "Actualizar" + ], + "remove this product from the database": [ + "" + ], + "update the product with new price": [ + "" + ], + "update product with new price": [ + "" + ], + "add more elements to the inventory": [ + "" + ], + "report elements lost in the inventory": [ + "" + ], + "new price for the product": [ + "no se pudo actualizar el producto" + ], + "the are value with errors": [ + "" + ], + "update product with new stock and price": [ + "" + ], + "There is no products yet, add more pressing the + sign": [ + "No hay propinas todavÃa, agregar mas presionando el signo +" + ], + "product updated successfully": [ + "producto actualizado correctamente" + ], + "could not update the product": [ + "no se pudo actualizar el producto" + ], + "product delete successfully": [ + "producto fue eliminado correctamente" + ], + "could not delete the product": [ + "no se pudo eliminar el producto" + ], + "Product id:": [ + "Id de producto" + ], + "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [ + "" + ], + "If your system supports RFC 8905, you can do this by opening this URI:": [ + "" + ], + "it should be greater than 0": [ + "Debe ser mayor a 0" + ], + "must be a valid URL": [ + "" + ], + "Initial balance": [ + "Instancia" + ], + "balance prior to deposit": [ + "" + ], + "Exchange URL": [ + "URL del Exchange" + ], + "URL of exchange": [ + "" + ], + "Next": [ + "Siguiente" + ], + "Wire method": [ + "" + ], + "method to use for wire transfer": [ + "no se pudo informar la transferencia" + ], + "Select one wire method": [ + "" + ], + "could not create reserve": [ + "No se pudo create el reembolso" + ], + "Valid until": [ + "Válido hasta" + ], + "Created balance": [ + "creado" + ], + "Exchange balance": [ + "Monto inicial" + ], + "Picked up": [ + "" + ], + "Committed": [ + "Monto confirmado" + ], + "Account address": [ + "Dirección de cuenta" + ], + "Subject": [ + "Asunto" + ], + "Tips": [ + "Propinas" + ], + "No tips has been authorized from this reserve": [ + "" + ], + "Authorized": [ + "Token de autorización" + ], + "Expiration": [ + "Información extra" + ], + "amount of tip": [ + "monto" + ], + "Justification": [ + "Jurisdicción" + ], + "reason for the tip": [ + "" + ], + "URL after tip": [ + "" + ], + "URL to visit after tip payment": [ + "" + ], + "Reserves not yet funded": [ + "Servidor no encontrado" + ], + "Reserves ready": [ + "" + ], + "add new reserve": [ + "cargar nuevas transferencias" + ], + "Expires at": [ + "" + ], + "Initial": [ + "" + ], + "delete selected reserve from the database": [ + "" + ], + "authorize new tip from selected reserve": [ + "" + ], + "There is no ready reserves yet, add more pressing the + sign or fund them": [ + "No hay transferencias todavÃa, agregar mas presionando el signo +" + ], + "Expected Balance": [ + "Ejecutado en" + ], + "could not create the tip": [ + "No se pudo create el reembolso" + ], + "should not be empty": [ + "no puede ser vacÃo" + ], + "should be greater that 0": [ + "Debe ser mayor a 0" + ], + "can't be empty": [ + "no puede ser vacÃo" + ], + "to short": [ + "" + ], + "just letters and numbers from 2 to 7": [ + "" + ], + "size of the key should be 32": [ + "" + ], + "Identifier": [ + "" + ], + "Name of the template in URLs.": [ + "" + ], + "Describe what this template stands for": [ + "" + ], + "Fixed summary": [ + "Estado de orden" + ], + "If specified, this template will create order with the same summary": [ + "" + ], + "Fixed price": [ + "precio unitario" + ], + "If specified, this template will create order with the same price": [ + "" + ], + "Minimum age": [ + "Edad mÃnima" + ], + "Is this contract restricted to some age?": [ + "" + ], + "Payment timeout": [ + "Opciones de pago" + ], + "How much time has the customer to complete the payment once the order was created.": [ + "" + ], + "Verification algorithm": [ + "" + ], + "Algorithm to use to verify transaction in offline mode": [ + "" + ], + "Point-of-sale key": [ + "" + ], + "Useful to validate the purchase": [ + "" + ], + "generate random secret key": [ + "" + ], + "random": [ + "" + ], + "show secret key": [ + "" + ], + "hide secret key": [ + "" + ], + "hide": [ + "" + ], + "show": [ + "" + ], + "could not inform template": [ + "no se pudo informar la transferencia" + ], + "Amount is required": [ + "Login necesario" + ], + "Order summary is required": [ + "" + ], + "New order for template": [ + "cargar viejas transferencias" + ], + "Amount of the order": [ + "" + ], + "Order summary": [ + "Estado de orden" + ], + "could not create order from template": [ + "No se pudo create el reembolso" + ], + "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [ + "" + ], + "Fixed amount": [ + "Monto reembolzado" + ], + "Default amount": [ + "Monto reembolzado" + ], + "Default summary": [ + "Estado de orden" + ], + "Print": [ + "" + ], + "Setup TOTP": [ + "" + ], + "Templates": [ + "" + ], + "add new templates": [ + "" + ], + "load more templates before the first one": [ + "" + ], + "load newer templates": [ + "cargar nuevas transferencias" + ], + "delete selected templates from the database": [ + "" + ], + "use template to create new order": [ + "" + ], + "create qr code for the template": [ + "No se pudo create el reembolso" + ], + "load more templates after the last one": [ + "" + ], + "load older templates": [ + "cargar viejas transferencias" + ], + "There is no templates yet, add more pressing the + sign": [ + "No hay propinas todavÃa, agregar mas presionando el signo +" + ], + "template delete successfully": [ + "producto fue eliminado correctamente" + ], + "could not delete the template": [ + "no se pudo eliminar el producto" + ], + "could not update template": [ + "no se pudo actualizar el producto" + ], + "should be one of '%1$s'": [ + "deberÃan ser iguales" + ], + "Webhook ID to use": [ + "" + ], + "Event": [ + "" + ], + "The event of the webhook: why the webhook is used": [ + "" + ], + "Method": [ + "" + ], + "Method used by the webhook": [ + "" + ], + "URL": [ + "URL" + ], + "URL of the webhook where the customer will be redirected": [ + "" + ], + "Header": [ + "" + ], + "Header template of the webhook": [ + "" + ], + "Body": [ + "" + ], + "Body template by the webhook": [ + "" + ], + "Webhooks": [ + "" + ], + "add new webhooks": [ + "" + ], + "load more webhooks before the first one": [ + "" + ], + "load newer webhooks": [ + "cargar nuevas ordenes" + ], + "Event type": [ + "" + ], + "delete selected webhook from the database": [ + "" + ], + "load more webhooks after the last one": [ + "" + ], + "load older webhooks": [ + "cargar viejas ordenes" + ], + "There is no webhooks yet, add more pressing the + sign": [ + "No hay propinas todavÃa, agregar mas presionando el signo +" + ], + "webhook delete successfully": [ + "producto fue eliminado correctamente" + ], + "could not delete the webhook": [ + "no se pudo eliminar el producto" + ], + "check the id, does not look valid": [ + "verificar el id, no parece válido" + ], + "should have 52 characters, current %1$s": [ + "deberÃa tener 52 caracteres, actualmente %1$s" + ], + "URL doesn't have the right format": [ + "La URL no tiene el formato correcto" + ], + "Credited bank account": [ + "" + ], + "Select one account": [ + "" + ], + "Bank account of the merchant where the payment was received": [ + "" + ], + "Wire transfer ID": [ + "Id de transferencia" + ], + "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [ + "" + ], + "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [ + "" + ], + "Amount credited": [ + "" + ], + "Actual amount that was wired to the merchant's bank account": [ + "" + ], + "could not inform transfer": [ + "no se pudo informar la transferencia" + ], + "Transfers": [ + "Transferencias" + ], + "add new transfer": [ + "cargar nuevas transferencias" + ], + "load more transfers before the first one": [ + "" + ], + "load newer transfers": [ + "cargar nuevas transferencias" + ], + "Credit": [ + "Crédito" + ], + "Confirmed": [ + "Confirmado" + ], + "Verified": [ + "Verificado" + ], + "Executed at": [ + "Ejecutado en" + ], + "yes": [ + "si" + ], + "no": [ + "no" + ], + "unknown": [ + "desconocido" + ], + "delete selected transfer from the database": [ + "eliminar transferencia seleccionada de la base de datos" + ], + "load more transfer after the last one": [ + "cargue más transferencia luego de la última" + ], + "load older transfers": [ + "cargar viejas transferencias" + ], + "There is no transfer yet, add more pressing the + sign": [ + "No hay transferencias todavÃa, agregar mas presionando el signo +" + ], + "filter by account address": [ + "Dirección de cuenta" + ], + "only show wire transfers confirmed by the merchant": [ + "" + ], + "only show wire transfers claimed by the exchange": [ + "" + ], + "Unverified": [ + "Verificado" + ], + "is not valid": [ + "" + ], + "is not a number": [ + "Número de edificio" + ], + "must be 1 or greater": [ + "debe ser 1 o mayor" + ], + "max 7 lines": [ + "máximo 7 lÃneas" + ], + "change authorization configuration": [ + "cambiar configuración de autorización" + ], + "Need to complete marked fields and choose authorization method": [ + "Necesita completar campos marcados y escoger un método de autorización" + ], + "This is not a valid bitcoin address.": [ + "Esta no es una dirección de bitcoin válida." + ], + "This is not a valid Ethereum address.": [ + "Esta no es una dirección de Ethereum válida." + ], + "IBAN numbers usually have more that 4 digits": [ + "Números IBAN usualmente tienen más de 4 dÃgitos" + ], + "IBAN numbers usually have less that 34 digits": [ + "Número IBAN usualmente tienen menos de 34 dÃgitos" + ], + "IBAN country code not found": [ + "Código IBAN de paÃs no encontrado" + ], + "IBAN number is not valid, checksum is wrong": [ + "Número IBAN no es válido, la suma de verificación es incorrecta" + ], + "Target type": [ + "Tipo objetivo" + ], + "Method to use for wire transfer": [ + "Método a usar para la transferencia" + ], + "Routing": [ + "Enrutamiento" + ], + "Routing number.": [ + "Número de enrutamiento." + ], + "Account": [ + "Cuenta" + ], + "Account number.": [ + "Dirección de cuenta" + ], + "Business Identifier Code.": [ + "" + ], + "Bank Account Number.": [ + "" + ], + "Unified Payment Interface.": [ + "Interfaz de pago unificado." + ], + "Bitcoin protocol.": [ + "" + ], + "Ethereum protocol.": [ + "" + ], + "Interledger protocol.": [ + "" + ], + "Host": [ + "" + ], + "Bank host.": [ + "" + ], + "Bank account.": [ + "" + ], + "Bank account owner's name.": [ + "" + ], + "No accounts yet.": [ + "" + ], + "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [ + "" + ], + "Business name": [ + "Nombre de edificio" + ], + "Legal name of the business represented by this instance.": [ + "" + ], + "Email": [ + "" + ], + "Contact email": [ + "" + ], + "Website URL": [ + "URL de sitio web" + ], + "URL.": [ + "" + ], + "Logo": [ + "" + ], + "Logo image.": [ + "" + ], + "Bank account": [ + "Cuenta bancaria" + ], + "URI specifying bank account for crediting revenue.": [ + "" + ], + "Default max deposit fee": [ + "Impuesto máximo de deposito por omisión" + ], + "Maximum deposit fees this merchant is willing to pay per order by default.": [ + "" + ], + "Default max wire fee": [ + "Impuesto máximo de transferencia por omisión" + ], + "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [ + "" + ], + "Default wire fee amortization": [ + "Amortización de impuesto de transferencia por omisión" + ], + "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [ + "" + ], + "Physical location of the merchant.": [ + "" + ], + "Jurisdiction": [ + "Jurisdicción" + ], + "Jurisdiction for legal disputes with the merchant.": [ + "Jurisdicción para disputas legales con el comerciante." + ], + "Default payment delay": [ + "Retrazo de pago por omisión" + ], + "Time customers have to pay an order before the offer expires by default.": [ + "" + ], + "Default wire transfer delay": [ + "Retrazo de transferencia por omisión" + ], + "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [ + "" + ], + "Instance id": [ + "ID de instancia" + ], + "Change the authorization method use for this instance.": [ + "Limpiar el token de autorización significa acceso público a la instancia" + ], + "Manage access token": [ + "Administrar token de acceso" + ], + "Failed to create instance": [ + "Fallo al crear la instancia" + ], + "Login required": [ + "Login necesario" + ], + "Please enter your access token.": [ + "" + ], + "Access Token": [ + "Acceso denegado" + ], + "The request to the backend take too long and was cancelled": [ + "" + ], + "Diagnostic from %1$s is \"%2$s\"": [ + "" + ], + "The backend reported a problem: HTTP status #%1$s": [ + "Servidir reporto un problema: HTTP status #%1$s" + ], + "Diagnostic from %1$s is '%2$s'": [ + "" + ], + "Access denied": [ + "Acceso denegado" + ], + "The access token provided is invalid.": [ + "" + ], + "No 'default' instance configured yet.": [ + "Sin instancia default" + ], + "Create a 'default' instance to begin using the merchant backoffice.": [ + "" + ], + "The access token provided is invalid": [ + "" + ], + "Hide for today": [ + "" + ], + "Instance": [ + "Instancia" + ], + "Settings": [ + "Configuración" + ], + "Connection": [ + "Conexión" + ], + "New": [ + "Nuevo" + ], + "List": [ + "Lista" + ], + "Log out": [ + "Salir" + ], + "Check your token is valid": [ + "Verifica que el token sea valido" + ], + "Couldn't access the server.": [ + "No se pudo acceder al servidor." + ], + "Could not infer instance id from url %1$s": [ + "No se pudo inferir el id de la instancia con la url %1$s" + ], + "Server not found": [ + "Servidor no encontrado" + ], + "Server response with an error code": [ + "" + ], + "Got message %1$s from %2$s": [ + "Recibimos el mensaje %1$s desde %2$s" + ], + "Response from server is unreadable, http status: %1$s": [ + "" + ], + "Unexpected Error": [ + "Error inesperado" + ], + "The value %1$s is invalid for a payment url": [ + "El valor %1$s es invalido para una URL de pago" + ], + "add element to the list": [ + "agregar elemento a la lista" + ], + "add": [ + "Agregar" + ], + "Deleting": [ + "Borrando" + ], + "Changing": [ + "Cambiando" + ], + "Order ID": [ + "ID de pedido" + ], + "Payment URL": [ + "URL de pago" + ] + } + } +}; + +strings['fr'] = { + "domain": "messages", + "locale_data": { + "messages": { + "": { + "domain": "messages", + "plural_forms": "nplurals=2; plural=(n != 1);", + "lang": "" + }, + "Cancel": [ + "" + ], + "%1$s": [ + "" + ], + "Close": [ + "" + ], + "Continue": [ + "" + ], + "Clear": [ + "" + ], + "Confirm": [ + "" + ], + "is not the same as the current access token": [ + "" + ], + "cannot be empty": [ + "" + ], + "cannot be the same as the old token": [ + "" + ], + "is not the same": [ + "" + ], + "You are updating the access token from instance with id %1$s": [ + "" + ], + "Old access token": [ + "" + ], + "access token currently in use": [ + "" + ], + "New access token": [ + "" + ], + "next access token to be used": [ + "" + ], + "Repeat access token": [ + "" + ], + "confirm the same access token": [ + "" + ], + "Clearing the access token will mean public access to the instance": [ + "" + ], + "cannot be the same as the old access token": [ + "" + ], + "You are setting the access token for the new instance": [ + "" + ], + "With external authorization method no check will be done by the merchant backend": [ + "" + ], + "Set external authorization": [ + "" + ], + "Set access token": [ + "" + ], + "Operation in progress...": [ + "" + ], + "The operation will be automatically canceled after %1$s seconds": [ + "" + ], + "Instances": [ + "" + ], + "Delete": [ + "" + ], + "add new instance": [ + "" + ], + "ID": [ + "" + ], + "Name": [ + "" + ], + "Edit": [ + "" + ], + "Purge": [ + "" + ], + "There is no instances yet, add more pressing the + sign": [ + "" + ], + "Only show active instances": [ + "" + ], + "Active": [ + "" + ], + "Only show deleted instances": [ + "" + ], + "Deleted": [ + "" + ], + "Show all instances": [ + "" + ], + "All": [ + "" + ], + "Instance \"%1$s\" (ID: %2$s) has been deleted": [ + "" + ], + "Failed to delete instance": [ + "" + ], + "Instance '%1$s' (ID: %2$s) has been disabled": [ + "" + ], + "Failed to purge instance": [ + "" + ], + "Pending KYC verification": [ + "" + ], + "Timed out": [ + "" + ], + "Exchange": [ + "" + ], + "Target account": [ + "" + ], + "KYC URL": [ + "" + ], + "Code": [ + "" + ], + "Http Status": [ + "" + ], + "No pending kyc verification!": [ + "" + ], + "change value to unknown date": [ + "" + ], + "change value to empty": [ + "" + ], + "clear": [ + "" + ], + "change value to never": [ + "" + ], + "never": [ + "" + ], + "Country": [ + "" + ], + "Address": [ + "" + ], + "Building number": [ + "" + ], + "Building name": [ + "" + ], + "Street": [ + "" + ], + "Post code": [ + "" + ], + "Town location": [ + "" + ], + "Town": [ + "" + ], + "District": [ + "" + ], + "Country subdivision": [ + "" + ], + "Product id": [ + "" + ], + "Description": [ + "" + ], + "Product": [ + "" + ], + "search products by it's description or id": [ + "" + ], + "no products found with that description": [ + "" + ], + "You must enter a valid product identifier.": [ + "" + ], + "Quantity must be greater than 0!": [ + "" + ], + "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [ + "" + ], + "Quantity": [ + "" + ], + "how many products will be added": [ + "" + ], + "Add from inventory": [ + "" + ], + "Image should be smaller than 1 MB": [ + "" + ], + "Add": [ + "" + ], + "Remove": [ + "" + ], + "No taxes configured for this product.": [ + "" + ], + "Amount": [ + "" + ], + "Taxes can be in currencies that differ from the main currency used by the merchant.": [ + "" + ], + "Enter currency and value separated with a colon, e.g. "USD:2.3".": [ + "" + ], + "Legal name of the tax, e.g. VAT or import duties.": [ + "" + ], + "add tax to the tax list": [ + "" + ], + "describe and add a product that is not in the inventory list": [ + "" + ], + "Add custom product": [ + "" + ], + "Complete information of the product": [ + "" + ], + "Image": [ + "" + ], + "photo of the product": [ + "" + ], + "full product description": [ + "" + ], + "Unit": [ + "" + ], + "name of the product unit": [ + "" + ], + "Price": [ + "" + ], + "amount in the current currency": [ + "" + ], + "Taxes": [ + "" + ], + "image": [ + "" + ], + "description": [ + "" + ], + "quantity": [ + "" + ], + "unit price": [ + "" + ], + "total price": [ + "" + ], + "required": [ + "" + ], + "not valid": [ + "" + ], + "must be greater than 0": [ + "" + ], + "not a valid json": [ + "" + ], + "should be in the future": [ + "" + ], + "refund deadline cannot be before pay deadline": [ + "" + ], + "wire transfer deadline cannot be before refund deadline": [ + "" + ], + "wire transfer deadline cannot be before pay deadline": [ + "" + ], + "should have a refund deadline": [ + "" + ], + "auto refund cannot be after refund deadline": [ + "" + ], + "Manage products in order": [ + "" + ], + "Manage list of products in the order.": [ + "" + ], + "Remove this product from the order.": [ + "" + ], + "Total price": [ + "" + ], + "total product price added up": [ + "" + ], + "Amount to be paid by the customer": [ + "" + ], + "Order price": [ + "" + ], + "final order price": [ + "" + ], + "Summary": [ + "" + ], + "Title of the order to be shown to the customer": [ + "" + ], + "Shipping and Fulfillment": [ + "" + ], + "Delivery date": [ + "" + ], + "Deadline for physical delivery assured by the merchant.": [ + "" + ], + "Location": [ + "" + ], + "address where the products will be delivered": [ + "" + ], + "Fulfillment URL": [ + "" + ], + "URL to which the user will be redirected after successful payment.": [ + "" + ], + "Taler payment options": [ + "" + ], + "Override default Taler payment settings for this order": [ + "" + ], + "Payment deadline": [ + "" + ], + "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [ + "" + ], + "Refund deadline": [ + "" + ], + "Time until which the order can be refunded by the merchant.": [ + "" + ], + "Wire transfer deadline": [ + "" + ], + "Deadline for the exchange to make the wire transfer.": [ + "" + ], + "Auto-refund deadline": [ + "" + ], + "Time until which the wallet will automatically check for refunds without user interaction.": [ + "" + ], + "Maximum deposit fee": [ + "" + ], + "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [ + "" + ], + "Maximum wire fee": [ + "" + ], + "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [ + "" + ], + "Wire fee amortization": [ + "" + ], + "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [ + "" + ], + "Create token": [ + "" + ], + "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [ + "" + ], + "Minimum age required": [ + "" + ], + "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [ + "" + ], + "Min age defined by the producs is %1$s": [ + "" + ], + "Additional information": [ + "" + ], + "Custom information to be included in the contract for this order.": [ + "" + ], + "You must enter a value in JavaScript Object Notation (JSON).": [ + "" + ], + "days": [ + "" + ], + "hours": [ + "" + ], + "minutes": [ + "" + ], + "seconds": [ + "" + ], + "forever": [ + "" + ], + "%1$sM": [ + "" + ], + "%1$sY": [ + "" + ], + "%1$sd": [ + "" + ], + "%1$sh": [ + "" + ], + "%1$smin": [ + "" + ], + "%1$ssec": [ + "" + ], + "Orders": [ + "" + ], + "create order": [ + "" + ], + "load newer orders": [ + "" + ], + "Date": [ + "" + ], + "Refund": [ + "" + ], + "copy url": [ + "" + ], + "load older orders": [ + "" + ], + "No orders have been found matching your query!": [ + "" + ], + "duplicated": [ + "" + ], + "invalid format": [ + "" + ], + "this value exceed the refundable amount": [ + "" + ], + "date": [ + "" + ], + "amount": [ + "" + ], + "reason": [ + "" + ], + "amount to be refunded": [ + "" + ], + "Max refundable:": [ + "" + ], + "Reason": [ + "" + ], + "Choose one...": [ + "" + ], + "requested by the customer": [ + "" + ], + "other": [ + "" + ], + "why this order is being refunded": [ + "" + ], + "more information to give context": [ + "" + ], + "Contract Terms": [ + "" + ], + "human-readable description of the whole purchase": [ + "" + ], + "total price for the transaction": [ + "" + ], + "URL for this purchase": [ + "" + ], + "Max fee": [ + "" + ], + "maximum total deposit fee accepted by the merchant for this contract": [ + "" + ], + "Max wire fee": [ + "" + ], + "maximum wire fee accepted by the merchant": [ + "" + ], + "over how many customer transactions does the merchant expect to amortize wire fees on average": [ + "" + ], + "Created at": [ + "" + ], + "time when this contract was generated": [ + "" + ], + "after this deadline has passed no refunds will be accepted": [ + "" + ], + "after this deadline, the merchant won't accept payments for the contract": [ + "" + ], + "transfer deadline for the exchange": [ + "" + ], + "time indicating when the order should be delivered": [ + "" + ], + "where the order will be delivered": [ + "" + ], + "Auto-refund delay": [ + "" + ], + "how long the wallet should try to get an automatic refund for the purchase": [ + "" + ], + "Extra info": [ + "" + ], + "extra data that is only interpreted by the merchant frontend": [ + "" + ], + "Order": [ + "" + ], + "claimed": [ + "" + ], + "claimed at": [ + "" + ], + "Timeline": [ + "" + ], + "Payment details": [ + "" + ], + "Order status": [ + "" + ], + "Product list": [ + "" + ], + "paid": [ + "" + ], + "wired": [ + "" + ], + "refunded": [ + "" + ], + "refund order": [ + "" + ], + "not refundable": [ + "" + ], + "refund": [ + "" + ], + "Refunded amount": [ + "" + ], + "Refund taken": [ + "" + ], + "Status URL": [ + "" + ], + "Refund URI": [ + "" + ], + "unpaid": [ + "" + ], + "pay at": [ + "" + ], + "created at": [ + "" + ], + "Order status URL": [ + "" + ], + "Payment URI": [ + "" + ], + "Unknown order status. This is an error, please contact the administrator.": [ + "" + ], + "Back": [ + "" + ], + "refund created successfully": [ + "" + ], + "could not create the refund": [ + "" + ], + "select date to show nearby orders": [ + "" + ], + "order id": [ + "" + ], + "jump to order with the given order ID": [ + "" + ], + "remove all filters": [ + "" + ], + "only show paid orders": [ + "" + ], + "Paid": [ + "" + ], + "only show orders with refunds": [ + "" + ], + "Refunded": [ + "" + ], + "only show orders where customers paid, but wire payments from payment provider are still pending": [ + "" + ], + "Not wired": [ + "" + ], + "clear date filter": [ + "" + ], + "date (YYYY/MM/DD)": [ + "" + ], + "Enter an order id": [ + "" + ], + "order not found": [ + "" + ], + "could not get the order to refund": [ + "" + ], + "Loading...": [ + "" + ], + "click here to configure the stock of the product, leave it as is and the backend will not control stock": [ + "" + ], + "Manage stock": [ + "" + ], + "this product has been configured without stock control": [ + "" + ], + "Infinite": [ + "" + ], + "lost cannot be greater than current and incoming (max %1$s)": [ + "" + ], + "Incoming": [ + "" + ], + "Lost": [ + "" + ], + "Current": [ + "" + ], + "remove stock control for this product": [ + "" + ], + "without stock": [ + "" + ], + "Next restock": [ + "" + ], + "Delivery address": [ + "" + ], + "product identification to use in URLs (for internal use only)": [ + "" + ], + "illustration of the product for customers": [ + "" + ], + "product description for customers": [ + "" + ], + "Age restricted": [ + "" + ], + "is this product restricted for customer below certain age?": [ + "" + ], + "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [ + "" + ], + "sale price for customers, including taxes, for above units of the product": [ + "" + ], + "Stock": [ + "" + ], + "product inventory for products with finite supply (for internal use only)": [ + "" + ], + "taxes included in the product price, exposed to customers": [ + "" + ], + "Need to complete marked fields": [ + "" + ], + "could not create product": [ + "" + ], + "Products": [ + "" + ], + "add product to inventory": [ + "" + ], + "Sell": [ + "" + ], + "Profit": [ + "" + ], + "Sold": [ + "" + ], + "free": [ + "" + ], + "go to product update page": [ + "" + ], + "Update": [ + "" + ], + "remove this product from the database": [ + "" + ], + "update the product with new price": [ + "" + ], + "update product with new price": [ + "" + ], + "add more elements to the inventory": [ + "" + ], + "report elements lost in the inventory": [ + "" + ], + "new price for the product": [ + "" + ], + "the are value with errors": [ + "" + ], + "update product with new stock and price": [ + "" + ], + "There is no products yet, add more pressing the + sign": [ + "" + ], + "product updated successfully": [ + "" + ], + "could not update the product": [ + "" + ], + "product delete successfully": [ + "" + ], + "could not delete the product": [ + "" + ], + "Product id:": [ + "" + ], + "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [ + "" + ], + "If your system supports RFC 8905, you can do this by opening this URI:": [ + "" + ], + "it should be greater than 0": [ + "" + ], + "must be a valid URL": [ + "" + ], + "Initial balance": [ + "" + ], + "balance prior to deposit": [ + "" + ], + "Exchange URL": [ + "" + ], + "URL of exchange": [ + "" + ], + "Next": [ + "" + ], + "Wire method": [ + "" + ], + "method to use for wire transfer": [ + "" + ], + "Select one wire method": [ + "" + ], + "could not create reserve": [ + "" + ], + "Valid until": [ + "" + ], + "Created balance": [ + "" + ], + "Exchange balance": [ + "" + ], + "Picked up": [ + "" + ], + "Committed": [ + "" + ], + "Account address": [ + "" + ], + "Subject": [ + "" + ], + "Tips": [ + "" + ], + "No tips has been authorized from this reserve": [ + "" + ], + "Authorized": [ + "" + ], + "Expiration": [ + "" + ], + "amount of tip": [ + "" + ], + "Justification": [ + "" + ], + "reason for the tip": [ + "" + ], + "URL after tip": [ + "" + ], + "URL to visit after tip payment": [ + "" + ], + "Reserves not yet funded": [ + "" + ], + "Reserves ready": [ + "" + ], + "add new reserve": [ + "" + ], + "Expires at": [ + "" + ], + "Initial": [ + "" + ], + "delete selected reserve from the database": [ + "" + ], + "authorize new tip from selected reserve": [ + "" + ], + "There is no ready reserves yet, add more pressing the + sign or fund them": [ + "" + ], + "Expected Balance": [ + "" + ], + "could not create the tip": [ + "" + ], + "should not be empty": [ + "" + ], + "should be greater that 0": [ + "" + ], + "can't be empty": [ + "" + ], + "to short": [ + "" + ], + "just letters and numbers from 2 to 7": [ + "" + ], + "size of the key should be 32": [ + "" + ], + "Identifier": [ + "" + ], + "Name of the template in URLs.": [ + "" + ], + "Describe what this template stands for": [ + "" + ], + "Fixed summary": [ + "" + ], + "If specified, this template will create order with the same summary": [ + "" + ], + "Fixed price": [ + "" + ], + "If specified, this template will create order with the same price": [ + "" + ], + "Minimum age": [ + "" + ], + "Is this contract restricted to some age?": [ + "" + ], + "Payment timeout": [ + "" + ], + "How much time has the customer to complete the payment once the order was created.": [ + "" + ], + "Verification algorithm": [ + "" + ], + "Algorithm to use to verify transaction in offline mode": [ + "" + ], + "Point-of-sale key": [ + "" + ], + "Useful to validate the purchase": [ + "" + ], + "generate random secret key": [ + "" + ], + "random": [ + "" + ], + "show secret key": [ + "" + ], + "hide secret key": [ + "" + ], + "hide": [ + "" + ], + "show": [ + "" + ], + "could not inform template": [ + "" + ], + "Amount is required": [ + "" + ], + "Order summary is required": [ + "" + ], + "New order for template": [ + "" + ], + "Amount of the order": [ + "" + ], + "Order summary": [ + "" + ], + "could not create order from template": [ + "" + ], + "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [ + "" + ], + "Fixed amount": [ + "" + ], + "Default amount": [ + "" + ], + "Default summary": [ + "" + ], + "Print": [ + "" + ], + "Setup TOTP": [ + "" + ], + "Templates": [ + "" + ], + "add new templates": [ + "" + ], + "load more templates before the first one": [ + "" + ], + "load newer templates": [ + "" + ], + "delete selected templates from the database": [ + "" + ], + "use template to create new order": [ + "" + ], + "create qr code for the template": [ + "" + ], + "load more templates after the last one": [ + "" + ], + "load older templates": [ + "" + ], + "There is no templates yet, add more pressing the + sign": [ + "" + ], + "template delete successfully": [ + "" + ], + "could not delete the template": [ + "" + ], + "could not update template": [ + "" + ], + "should be one of '%1$s'": [ + "" + ], + "Webhook ID to use": [ + "" + ], + "Event": [ + "" + ], + "The event of the webhook: why the webhook is used": [ + "" + ], + "Method": [ + "" + ], + "Method used by the webhook": [ + "" + ], + "URL": [ + "" + ], + "URL of the webhook where the customer will be redirected": [ + "" + ], + "Header": [ + "" + ], + "Header template of the webhook": [ + "" + ], + "Body": [ + "" + ], + "Body template by the webhook": [ + "" + ], + "Webhooks": [ + "" + ], + "add new webhooks": [ + "" + ], + "load more webhooks before the first one": [ + "" + ], + "load newer webhooks": [ + "" + ], + "Event type": [ + "" + ], + "delete selected webhook from the database": [ + "" + ], + "load more webhooks after the last one": [ + "" + ], + "load older webhooks": [ + "" + ], + "There is no webhooks yet, add more pressing the + sign": [ + "" + ], + "webhook delete successfully": [ + "" + ], + "could not delete the webhook": [ + "" + ], + "check the id, does not look valid": [ + "" + ], + "should have 52 characters, current %1$s": [ + "" + ], + "URL doesn't have the right format": [ + "" + ], + "Credited bank account": [ + "" + ], + "Select one account": [ + "" + ], + "Bank account of the merchant where the payment was received": [ + "" + ], + "Wire transfer ID": [ + "" + ], + "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [ + "" + ], + "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [ + "" + ], + "Amount credited": [ + "" + ], + "Actual amount that was wired to the merchant's bank account": [ + "" + ], + "could not inform transfer": [ + "" + ], + "Transfers": [ + "" + ], + "add new transfer": [ + "" + ], + "load more transfers before the first one": [ + "" + ], + "load newer transfers": [ + "" + ], + "Credit": [ + "" + ], + "Confirmed": [ + "" + ], + "Verified": [ + "" + ], + "Executed at": [ + "" + ], + "yes": [ + "" + ], + "no": [ + "" + ], + "unknown": [ + "" + ], + "delete selected transfer from the database": [ + "" + ], + "load more transfer after the last one": [ + "" + ], + "load older transfers": [ + "" + ], + "There is no transfer yet, add more pressing the + sign": [ + "" + ], + "filter by account address": [ + "" + ], + "only show wire transfers confirmed by the merchant": [ + "" + ], + "only show wire transfers claimed by the exchange": [ + "" + ], + "Unverified": [ + "" + ], + "is not valid": [ + "" + ], + "is not a number": [ + "" + ], + "must be 1 or greater": [ + "" + ], + "max 7 lines": [ + "" + ], + "change authorization configuration": [ + "" + ], + "Need to complete marked fields and choose authorization method": [ + "" + ], + "This is not a valid bitcoin address.": [ + "" + ], + "This is not a valid Ethereum address.": [ + "" + ], + "IBAN numbers usually have more that 4 digits": [ + "" + ], + "IBAN numbers usually have less that 34 digits": [ + "" + ], + "IBAN country code not found": [ + "" + ], + "IBAN number is not valid, checksum is wrong": [ + "" + ], + "Target type": [ + "" + ], + "Method to use for wire transfer": [ + "" + ], + "Routing": [ + "" + ], + "Routing number.": [ + "" + ], + "Account": [ + "" + ], + "Account number.": [ + "" + ], + "Business Identifier Code.": [ + "" + ], + "Bank Account Number.": [ + "" + ], + "Unified Payment Interface.": [ + "" + ], + "Bitcoin protocol.": [ + "" + ], + "Ethereum protocol.": [ + "" + ], + "Interledger protocol.": [ + "" + ], + "Host": [ + "" + ], + "Bank host.": [ + "" + ], + "Bank account.": [ + "" + ], + "Bank account owner's name.": [ + "" + ], + "No accounts yet.": [ + "" + ], + "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [ + "" + ], + "Business name": [ + "" + ], + "Legal name of the business represented by this instance.": [ + "" + ], + "Email": [ + "" + ], + "Contact email": [ + "" + ], + "Website URL": [ + "" + ], + "URL.": [ + "" + ], + "Logo": [ + "" + ], + "Logo image.": [ + "" + ], + "Bank account": [ + "" + ], + "URI specifying bank account for crediting revenue.": [ + "" + ], + "Default max deposit fee": [ + "" + ], + "Maximum deposit fees this merchant is willing to pay per order by default.": [ + "" + ], + "Default max wire fee": [ + "" + ], + "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [ + "" + ], + "Default wire fee amortization": [ + "" + ], + "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [ + "" + ], + "Physical location of the merchant.": [ + "" + ], + "Jurisdiction": [ + "" + ], + "Jurisdiction for legal disputes with the merchant.": [ + "" + ], + "Default payment delay": [ + "" + ], + "Time customers have to pay an order before the offer expires by default.": [ + "" + ], + "Default wire transfer delay": [ + "" + ], + "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [ + "" + ], + "Instance id": [ + "" + ], + "Change the authorization method use for this instance.": [ + "" + ], + "Manage access token": [ + "" + ], + "Failed to create instance": [ + "" + ], + "Login required": [ + "" + ], + "Please enter your access token.": [ + "" + ], + "Access Token": [ + "" + ], + "The request to the backend take too long and was cancelled": [ + "" + ], + "Diagnostic from %1$s is \"%2$s\"": [ + "" + ], + "The backend reported a problem: HTTP status #%1$s": [ + "" + ], + "Diagnostic from %1$s is '%2$s'": [ + "" + ], + "Access denied": [ + "" + ], + "The access token provided is invalid.": [ + "" + ], + "No 'default' instance configured yet.": [ + "" + ], + "Create a 'default' instance to begin using the merchant backoffice.": [ + "" + ], + "The access token provided is invalid": [ + "" + ], + "Hide for today": [ + "" + ], + "Instance": [ + "" + ], + "Settings": [ + "" + ], + "Connection": [ + "" + ], + "New": [ + "" + ], + "List": [ + "" + ], + "Log out": [ + "" + ], + "Check your token is valid": [ + "" + ], + "Couldn't access the server.": [ + "" + ], + "Could not infer instance id from url %1$s": [ + "" + ], + "Server not found": [ + "" + ], + "Server response with an error code": [ + "" + ], + "Got message %1$s from %2$s": [ + "" + ], + "Response from server is unreadable, http status: %1$s": [ + "" + ], + "Unexpected Error": [ + "" + ], + "The value %1$s is invalid for a payment url": [ + "" + ], + "add element to the list": [ + "" + ], + "add": [ + "" + ], + "Deleting": [ + "" + ], + "Changing": [ + "" + ], + "Order ID": [ + "" + ], + "Payment URL": [ + "" + ] + } + } +}; + +strings['it'] = { + "domain": "messages", + "locale_data": { + "messages": { + "": { + "domain": "messages", + "plural_forms": "nplurals=2; plural=(n != 1);", + "lang": "" + }, + "Cancel": [ + "" + ], + "%1$s": [ + "" + ], + "Close": [ + "" + ], + "Continue": [ + "" + ], + "Clear": [ + "" + ], + "Confirm": [ + "" + ], + "is not the same as the current access token": [ + "" + ], + "cannot be empty": [ + "" + ], + "cannot be the same as the old token": [ + "" + ], + "is not the same": [ + "" + ], + "You are updating the access token from instance with id %1$s": [ + "" + ], + "Old access token": [ + "" + ], + "access token currently in use": [ + "" + ], + "New access token": [ + "" + ], + "next access token to be used": [ + "" + ], + "Repeat access token": [ + "" + ], + "confirm the same access token": [ + "" + ], + "Clearing the access token will mean public access to the instance": [ + "" + ], + "cannot be the same as the old access token": [ + "" + ], + "You are setting the access token for the new instance": [ + "" + ], + "With external authorization method no check will be done by the merchant backend": [ + "" + ], + "Set external authorization": [ + "" + ], + "Set access token": [ + "" + ], + "Operation in progress...": [ + "" + ], + "The operation will be automatically canceled after %1$s seconds": [ + "" + ], + "Instances": [ + "" + ], + "Delete": [ + "" + ], + "add new instance": [ + "" + ], + "ID": [ + "" + ], + "Name": [ + "" + ], + "Edit": [ + "" + ], + "Purge": [ + "" + ], + "There is no instances yet, add more pressing the + sign": [ + "" + ], + "Only show active instances": [ + "" + ], + "Active": [ + "" + ], + "Only show deleted instances": [ + "" + ], + "Deleted": [ + "" + ], + "Show all instances": [ + "" + ], + "All": [ + "" + ], + "Instance \"%1$s\" (ID: %2$s) has been deleted": [ + "" + ], + "Failed to delete instance": [ + "" + ], + "Instance '%1$s' (ID: %2$s) has been disabled": [ + "" + ], + "Failed to purge instance": [ + "" + ], + "Pending KYC verification": [ + "" + ], + "Timed out": [ + "" + ], + "Exchange": [ + "" + ], + "Target account": [ + "" + ], + "KYC URL": [ + "" + ], + "Code": [ + "" + ], + "Http Status": [ + "" + ], + "No pending kyc verification!": [ + "" + ], + "change value to unknown date": [ + "" + ], + "change value to empty": [ + "" + ], + "clear": [ + "" + ], + "change value to never": [ + "" + ], + "never": [ + "" + ], + "Country": [ + "" + ], + "Address": [ + "" + ], + "Building number": [ + "" + ], + "Building name": [ + "" + ], + "Street": [ + "" + ], + "Post code": [ + "" + ], + "Town location": [ + "" + ], + "Town": [ + "" + ], + "District": [ + "" + ], + "Country subdivision": [ + "" + ], + "Product id": [ + "" + ], + "Description": [ + "" + ], + "Product": [ + "" + ], + "search products by it's description or id": [ + "" + ], + "no products found with that description": [ + "" + ], + "You must enter a valid product identifier.": [ + "" + ], + "Quantity must be greater than 0!": [ + "" + ], + "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [ + "" + ], + "Quantity": [ + "" + ], + "how many products will be added": [ + "" + ], + "Add from inventory": [ + "" + ], + "Image should be smaller than 1 MB": [ + "" + ], + "Add": [ + "" + ], + "Remove": [ + "" + ], + "No taxes configured for this product.": [ + "" + ], + "Amount": [ + "" + ], + "Taxes can be in currencies that differ from the main currency used by the merchant.": [ + "" + ], + "Enter currency and value separated with a colon, e.g. "USD:2.3".": [ + "" + ], + "Legal name of the tax, e.g. VAT or import duties.": [ + "" + ], + "add tax to the tax list": [ + "" + ], + "describe and add a product that is not in the inventory list": [ + "" + ], + "Add custom product": [ + "" + ], + "Complete information of the product": [ + "" + ], + "Image": [ + "" + ], + "photo of the product": [ + "" + ], + "full product description": [ + "" + ], + "Unit": [ + "" + ], + "name of the product unit": [ + "" + ], + "Price": [ + "" + ], + "amount in the current currency": [ + "" + ], + "Taxes": [ + "" + ], + "image": [ + "" + ], + "description": [ + "" + ], + "quantity": [ + "" + ], + "unit price": [ + "" + ], + "total price": [ + "" + ], + "required": [ + "" + ], + "not valid": [ + "" + ], + "must be greater than 0": [ + "" + ], + "not a valid json": [ + "" + ], + "should be in the future": [ + "" + ], + "refund deadline cannot be before pay deadline": [ + "" + ], + "wire transfer deadline cannot be before refund deadline": [ + "" + ], + "wire transfer deadline cannot be before pay deadline": [ + "" + ], + "should have a refund deadline": [ + "" + ], + "auto refund cannot be after refund deadline": [ + "" + ], + "Manage products in order": [ + "" + ], + "Manage list of products in the order.": [ + "" + ], + "Remove this product from the order.": [ + "" + ], + "Total price": [ + "" + ], + "total product price added up": [ + "" + ], + "Amount to be paid by the customer": [ + "" + ], + "Order price": [ + "" + ], + "final order price": [ + "" + ], + "Summary": [ + "" + ], + "Title of the order to be shown to the customer": [ + "" + ], + "Shipping and Fulfillment": [ + "" + ], + "Delivery date": [ + "" + ], + "Deadline for physical delivery assured by the merchant.": [ + "" + ], + "Location": [ + "" + ], + "address where the products will be delivered": [ + "" + ], + "Fulfillment URL": [ + "" + ], + "URL to which the user will be redirected after successful payment.": [ + "" + ], + "Taler payment options": [ + "" + ], + "Override default Taler payment settings for this order": [ + "" + ], + "Payment deadline": [ + "" + ], + "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [ + "" + ], + "Refund deadline": [ + "" + ], + "Time until which the order can be refunded by the merchant.": [ + "" + ], + "Wire transfer deadline": [ + "" + ], + "Deadline for the exchange to make the wire transfer.": [ + "" + ], + "Auto-refund deadline": [ + "" + ], + "Time until which the wallet will automatically check for refunds without user interaction.": [ + "" + ], + "Maximum deposit fee": [ + "" + ], + "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [ + "" + ], + "Maximum wire fee": [ + "" + ], + "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [ + "" + ], + "Wire fee amortization": [ + "" + ], + "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [ + "" + ], + "Create token": [ + "" + ], + "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [ + "" + ], + "Minimum age required": [ + "" + ], + "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [ + "" + ], + "Min age defined by the producs is %1$s": [ + "" + ], + "Additional information": [ + "" + ], + "Custom information to be included in the contract for this order.": [ + "" + ], + "You must enter a value in JavaScript Object Notation (JSON).": [ + "" + ], + "days": [ + "" + ], + "hours": [ + "" + ], + "minutes": [ + "" + ], + "seconds": [ + "" + ], + "forever": [ + "" + ], + "%1$sM": [ + "" + ], + "%1$sY": [ + "" + ], + "%1$sd": [ + "" + ], + "%1$sh": [ + "" + ], + "%1$smin": [ + "" + ], + "%1$ssec": [ + "" + ], + "Orders": [ + "" + ], + "create order": [ + "" + ], + "load newer orders": [ + "" + ], + "Date": [ + "" + ], + "Refund": [ + "" + ], + "copy url": [ + "" + ], + "load older orders": [ + "" + ], + "No orders have been found matching your query!": [ + "" + ], + "duplicated": [ + "" + ], + "invalid format": [ + "" + ], + "this value exceed the refundable amount": [ + "" + ], + "date": [ + "" + ], + "amount": [ + "" + ], + "reason": [ + "" + ], + "amount to be refunded": [ + "" + ], + "Max refundable:": [ + "" + ], + "Reason": [ + "" + ], + "Choose one...": [ + "" + ], + "requested by the customer": [ + "" + ], + "other": [ + "" + ], + "why this order is being refunded": [ + "" + ], + "more information to give context": [ + "" + ], + "Contract Terms": [ + "" + ], + "human-readable description of the whole purchase": [ + "" + ], + "total price for the transaction": [ + "" + ], + "URL for this purchase": [ + "" + ], + "Max fee": [ + "" + ], + "maximum total deposit fee accepted by the merchant for this contract": [ + "" + ], + "Max wire fee": [ + "" + ], + "maximum wire fee accepted by the merchant": [ + "" + ], + "over how many customer transactions does the merchant expect to amortize wire fees on average": [ + "" + ], + "Created at": [ + "" + ], + "time when this contract was generated": [ + "" + ], + "after this deadline has passed no refunds will be accepted": [ + "" + ], + "after this deadline, the merchant won't accept payments for the contract": [ + "" + ], + "transfer deadline for the exchange": [ + "" + ], + "time indicating when the order should be delivered": [ + "" + ], + "where the order will be delivered": [ + "" + ], + "Auto-refund delay": [ + "" + ], + "how long the wallet should try to get an automatic refund for the purchase": [ + "" + ], + "Extra info": [ + "" + ], + "extra data that is only interpreted by the merchant frontend": [ + "" + ], + "Order": [ + "" + ], + "claimed": [ + "" + ], + "claimed at": [ + "" + ], + "Timeline": [ + "" + ], + "Payment details": [ + "" + ], + "Order status": [ + "" + ], + "Product list": [ + "" + ], + "paid": [ + "" + ], + "wired": [ + "" + ], + "refunded": [ + "" + ], + "refund order": [ + "" + ], + "not refundable": [ + "" + ], + "refund": [ + "" + ], + "Refunded amount": [ + "" + ], + "Refund taken": [ + "" + ], + "Status URL": [ + "" + ], + "Refund URI": [ + "" + ], + "unpaid": [ + "" + ], + "pay at": [ + "" + ], + "created at": [ + "" + ], + "Order status URL": [ + "" + ], + "Payment URI": [ + "" + ], + "Unknown order status. This is an error, please contact the administrator.": [ + "" + ], + "Back": [ + "" + ], + "refund created successfully": [ + "" + ], + "could not create the refund": [ + "" + ], + "select date to show nearby orders": [ + "" + ], + "order id": [ + "" + ], + "jump to order with the given order ID": [ + "" + ], + "remove all filters": [ + "" + ], + "only show paid orders": [ + "" + ], + "Paid": [ + "" + ], + "only show orders with refunds": [ + "" + ], + "Refunded": [ + "" + ], + "only show orders where customers paid, but wire payments from payment provider are still pending": [ + "" + ], + "Not wired": [ + "" + ], + "clear date filter": [ + "" + ], + "date (YYYY/MM/DD)": [ + "" + ], + "Enter an order id": [ + "" + ], + "order not found": [ + "" + ], + "could not get the order to refund": [ + "" + ], + "Loading...": [ + "" + ], + "click here to configure the stock of the product, leave it as is and the backend will not control stock": [ + "" + ], + "Manage stock": [ + "" + ], + "this product has been configured without stock control": [ + "" + ], + "Infinite": [ + "" + ], + "lost cannot be greater than current and incoming (max %1$s)": [ + "" + ], + "Incoming": [ + "" + ], + "Lost": [ + "" + ], + "Current": [ + "" + ], + "remove stock control for this product": [ + "" + ], + "without stock": [ + "" + ], + "Next restock": [ + "" + ], + "Delivery address": [ + "" + ], + "product identification to use in URLs (for internal use only)": [ + "" + ], + "illustration of the product for customers": [ + "" + ], + "product description for customers": [ + "" + ], + "Age restricted": [ + "" + ], + "is this product restricted for customer below certain age?": [ + "" + ], + "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [ + "" + ], + "sale price for customers, including taxes, for above units of the product": [ + "" + ], + "Stock": [ + "" + ], + "product inventory for products with finite supply (for internal use only)": [ + "" + ], + "taxes included in the product price, exposed to customers": [ + "" + ], + "Need to complete marked fields": [ + "" + ], + "could not create product": [ + "" + ], + "Products": [ + "" + ], + "add product to inventory": [ + "" + ], + "Sell": [ + "" + ], + "Profit": [ + "" + ], + "Sold": [ + "" + ], + "free": [ + "" + ], + "go to product update page": [ + "" + ], + "Update": [ + "" + ], + "remove this product from the database": [ + "" + ], + "update the product with new price": [ + "" + ], + "update product with new price": [ + "" + ], + "add more elements to the inventory": [ + "" + ], + "report elements lost in the inventory": [ + "" + ], + "new price for the product": [ + "" + ], + "the are value with errors": [ + "" + ], + "update product with new stock and price": [ + "" + ], + "There is no products yet, add more pressing the + sign": [ + "" + ], + "product updated successfully": [ + "" + ], + "could not update the product": [ + "" + ], + "product delete successfully": [ + "" + ], + "could not delete the product": [ + "" + ], + "Product id:": [ + "" + ], + "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [ + "" + ], + "If your system supports RFC 8905, you can do this by opening this URI:": [ + "" + ], + "it should be greater than 0": [ + "" + ], + "must be a valid URL": [ + "" + ], + "Initial balance": [ + "" + ], + "balance prior to deposit": [ + "" + ], + "Exchange URL": [ + "" + ], + "URL of exchange": [ + "" + ], + "Next": [ + "" + ], + "Wire method": [ + "" + ], + "method to use for wire transfer": [ + "" + ], + "Select one wire method": [ + "" + ], + "could not create reserve": [ + "" + ], + "Valid until": [ + "" + ], + "Created balance": [ + "" + ], + "Exchange balance": [ + "" + ], + "Picked up": [ + "" + ], + "Committed": [ + "" + ], + "Account address": [ + "" + ], + "Subject": [ + "" + ], + "Tips": [ + "" + ], + "No tips has been authorized from this reserve": [ + "" + ], + "Authorized": [ + "" + ], + "Expiration": [ + "" + ], + "amount of tip": [ + "" + ], + "Justification": [ + "" + ], + "reason for the tip": [ + "" + ], + "URL after tip": [ + "" + ], + "URL to visit after tip payment": [ + "" + ], + "Reserves not yet funded": [ + "" + ], + "Reserves ready": [ + "" + ], + "add new reserve": [ + "" + ], + "Expires at": [ + "" + ], + "Initial": [ + "" + ], + "delete selected reserve from the database": [ + "" + ], + "authorize new tip from selected reserve": [ + "" + ], + "There is no ready reserves yet, add more pressing the + sign or fund them": [ + "" + ], + "Expected Balance": [ + "" + ], + "could not create the tip": [ + "" + ], + "should not be empty": [ + "" + ], + "should be greater that 0": [ + "" + ], + "can't be empty": [ + "" + ], + "to short": [ + "" + ], + "just letters and numbers from 2 to 7": [ + "" + ], + "size of the key should be 32": [ + "" + ], + "Identifier": [ + "" + ], + "Name of the template in URLs.": [ + "" + ], + "Describe what this template stands for": [ + "" + ], + "Fixed summary": [ + "" + ], + "If specified, this template will create order with the same summary": [ + "" + ], + "Fixed price": [ + "" + ], + "If specified, this template will create order with the same price": [ + "" + ], + "Minimum age": [ + "" + ], + "Is this contract restricted to some age?": [ + "" + ], + "Payment timeout": [ + "" + ], + "How much time has the customer to complete the payment once the order was created.": [ + "" + ], + "Verification algorithm": [ + "" + ], + "Algorithm to use to verify transaction in offline mode": [ + "" + ], + "Point-of-sale key": [ + "" + ], + "Useful to validate the purchase": [ + "" + ], + "generate random secret key": [ + "" + ], + "random": [ + "" + ], + "show secret key": [ + "" + ], + "hide secret key": [ + "" + ], + "hide": [ + "" + ], + "show": [ + "" + ], + "could not inform template": [ + "" + ], + "Amount is required": [ + "" + ], + "Order summary is required": [ + "" + ], + "New order for template": [ + "" + ], + "Amount of the order": [ + "" + ], + "Order summary": [ + "" + ], + "could not create order from template": [ + "" + ], + "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [ + "" + ], + "Fixed amount": [ + "" + ], + "Default amount": [ + "" + ], + "Default summary": [ + "" + ], + "Print": [ + "" + ], + "Setup TOTP": [ + "" + ], + "Templates": [ + "" + ], + "add new templates": [ + "" + ], + "load more templates before the first one": [ + "" + ], + "load newer templates": [ + "" + ], + "delete selected templates from the database": [ + "" + ], + "use template to create new order": [ + "" + ], + "create qr code for the template": [ + "" + ], + "load more templates after the last one": [ + "" + ], + "load older templates": [ + "" + ], + "There is no templates yet, add more pressing the + sign": [ + "" + ], + "template delete successfully": [ + "" + ], + "could not delete the template": [ + "" + ], + "could not update template": [ + "" + ], + "should be one of '%1$s'": [ + "" + ], + "Webhook ID to use": [ + "" + ], + "Event": [ + "" + ], + "The event of the webhook: why the webhook is used": [ + "" + ], + "Method": [ + "" + ], + "Method used by the webhook": [ + "" + ], + "URL": [ + "" + ], + "URL of the webhook where the customer will be redirected": [ + "" + ], + "Header": [ + "" + ], + "Header template of the webhook": [ + "" + ], + "Body": [ + "" + ], + "Body template by the webhook": [ + "" + ], + "Webhooks": [ + "" + ], + "add new webhooks": [ + "" + ], + "load more webhooks before the first one": [ + "" + ], + "load newer webhooks": [ + "" + ], + "Event type": [ + "" + ], + "delete selected webhook from the database": [ + "" + ], + "load more webhooks after the last one": [ + "" + ], + "load older webhooks": [ + "" + ], + "There is no webhooks yet, add more pressing the + sign": [ + "" + ], + "webhook delete successfully": [ + "" + ], + "could not delete the webhook": [ + "" + ], + "check the id, does not look valid": [ + "" + ], + "should have 52 characters, current %1$s": [ + "" + ], + "URL doesn't have the right format": [ + "" + ], + "Credited bank account": [ + "" + ], + "Select one account": [ + "" + ], + "Bank account of the merchant where the payment was received": [ + "" + ], + "Wire transfer ID": [ + "" + ], + "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [ + "" + ], + "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [ + "" + ], + "Amount credited": [ + "" + ], + "Actual amount that was wired to the merchant's bank account": [ + "" + ], + "could not inform transfer": [ + "" + ], + "Transfers": [ + "" + ], + "add new transfer": [ + "" + ], + "load more transfers before the first one": [ + "" + ], + "load newer transfers": [ + "" + ], + "Credit": [ + "" + ], + "Confirmed": [ + "" + ], + "Verified": [ + "" + ], + "Executed at": [ + "" + ], + "yes": [ + "" + ], + "no": [ + "" + ], + "unknown": [ + "" + ], + "delete selected transfer from the database": [ + "" + ], + "load more transfer after the last one": [ + "" + ], + "load older transfers": [ + "" + ], + "There is no transfer yet, add more pressing the + sign": [ + "" + ], + "filter by account address": [ + "" + ], + "only show wire transfers confirmed by the merchant": [ + "" + ], + "only show wire transfers claimed by the exchange": [ + "" + ], + "Unverified": [ + "" + ], + "is not valid": [ + "" + ], + "is not a number": [ + "" + ], + "must be 1 or greater": [ + "" + ], + "max 7 lines": [ + "" + ], + "change authorization configuration": [ + "" + ], + "Need to complete marked fields and choose authorization method": [ + "" + ], + "This is not a valid bitcoin address.": [ + "" + ], + "This is not a valid Ethereum address.": [ + "" + ], + "IBAN numbers usually have more that 4 digits": [ + "" + ], + "IBAN numbers usually have less that 34 digits": [ + "" + ], + "IBAN country code not found": [ + "" + ], + "IBAN number is not valid, checksum is wrong": [ + "" + ], + "Target type": [ + "" + ], + "Method to use for wire transfer": [ + "" + ], + "Routing": [ + "" + ], + "Routing number.": [ + "" + ], + "Account": [ + "" + ], + "Account number.": [ + "" + ], + "Business Identifier Code.": [ + "" + ], + "Bank Account Number.": [ + "" + ], + "Unified Payment Interface.": [ + "" + ], + "Bitcoin protocol.": [ + "" + ], + "Ethereum protocol.": [ + "" + ], + "Interledger protocol.": [ + "" + ], + "Host": [ + "" + ], + "Bank host.": [ + "" + ], + "Bank account.": [ + "" + ], + "Bank account owner's name.": [ + "" + ], + "No accounts yet.": [ + "" + ], + "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [ + "" + ], + "Business name": [ + "" + ], + "Legal name of the business represented by this instance.": [ + "" + ], + "Email": [ + "" + ], + "Contact email": [ + "" + ], + "Website URL": [ + "" + ], + "URL.": [ + "" + ], + "Logo": [ + "" + ], + "Logo image.": [ + "" + ], + "Bank account": [ + "" + ], + "URI specifying bank account for crediting revenue.": [ + "" + ], + "Default max deposit fee": [ + "" + ], + "Maximum deposit fees this merchant is willing to pay per order by default.": [ + "" + ], + "Default max wire fee": [ + "" + ], + "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [ + "" + ], + "Default wire fee amortization": [ + "" + ], + "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [ + "" + ], + "Physical location of the merchant.": [ + "" + ], + "Jurisdiction": [ + "" + ], + "Jurisdiction for legal disputes with the merchant.": [ + "" + ], + "Default payment delay": [ + "" + ], + "Time customers have to pay an order before the offer expires by default.": [ + "" + ], + "Default wire transfer delay": [ + "" + ], + "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [ + "" + ], + "Instance id": [ + "" + ], + "Change the authorization method use for this instance.": [ + "" + ], + "Manage access token": [ + "" + ], + "Failed to create instance": [ + "" + ], + "Login required": [ + "" + ], + "Please enter your access token.": [ + "" + ], + "Access Token": [ + "" + ], + "The request to the backend take too long and was cancelled": [ + "" + ], + "Diagnostic from %1$s is \"%2$s\"": [ + "" + ], + "The backend reported a problem: HTTP status #%1$s": [ + "" + ], + "Diagnostic from %1$s is '%2$s'": [ + "" + ], + "Access denied": [ + "" + ], + "The access token provided is invalid.": [ + "" + ], + "No 'default' instance configured yet.": [ + "" + ], + "Create a 'default' instance to begin using the merchant backoffice.": [ + "" + ], + "The access token provided is invalid": [ + "" + ], + "Hide for today": [ + "" + ], + "Instance": [ + "" + ], + "Settings": [ + "" + ], + "Connection": [ + "" + ], + "New": [ + "" + ], + "List": [ + "" + ], + "Log out": [ + "" + ], + "Check your token is valid": [ + "" + ], + "Couldn't access the server.": [ + "" + ], + "Could not infer instance id from url %1$s": [ + "" + ], + "Server not found": [ + "" + ], + "Server response with an error code": [ + "" + ], + "Got message %1$s from %2$s": [ + "" + ], + "Response from server is unreadable, http status: %1$s": [ + "" + ], + "Unexpected Error": [ + "" + ], + "The value %1$s is invalid for a payment url": [ + "" + ], + "add element to the list": [ + "" + ], + "add": [ + "" + ], + "Deleting": [ + "" + ], + "Changing": [ + "" + ], + "Order ID": [ + "" + ], + "Payment URL": [ + "" + ] + } + } +}; + +strings['sv'] = { + "domain": "messages", + "locale_data": { + "messages": { + "": { + "domain": "messages", + "plural_forms": "nplurals=2; plural=(n != 1);", + "lang": "" + }, + "Cancel": [ + "" + ], + "%1$s": [ + "" + ], + "Close": [ + "" + ], + "Continue": [ + "" + ], + "Clear": [ + "" + ], + "Confirm": [ + "" + ], + "is not the same as the current access token": [ + "" + ], + "cannot be empty": [ + "" + ], + "cannot be the same as the old token": [ + "" + ], + "is not the same": [ + "" + ], + "You are updating the access token from instance with id %1$s": [ + "" + ], + "Old access token": [ + "" + ], + "access token currently in use": [ + "" + ], + "New access token": [ + "" + ], + "next access token to be used": [ + "" + ], + "Repeat access token": [ + "" + ], + "confirm the same access token": [ + "" + ], + "Clearing the access token will mean public access to the instance": [ + "" + ], + "cannot be the same as the old access token": [ + "" + ], + "You are setting the access token for the new instance": [ + "" + ], + "With external authorization method no check will be done by the merchant backend": [ + "" + ], + "Set external authorization": [ + "" + ], + "Set access token": [ + "" + ], + "Operation in progress...": [ + "" + ], + "The operation will be automatically canceled after %1$s seconds": [ + "" + ], + "Instances": [ + "" + ], + "Delete": [ + "" + ], + "add new instance": [ + "" + ], + "ID": [ + "" + ], + "Name": [ + "" + ], + "Edit": [ + "" + ], + "Purge": [ + "" + ], + "There is no instances yet, add more pressing the + sign": [ + "" + ], + "Only show active instances": [ + "" + ], + "Active": [ + "" + ], + "Only show deleted instances": [ + "" + ], + "Deleted": [ + "" + ], + "Show all instances": [ + "" + ], + "All": [ + "" + ], + "Instance \"%1$s\" (ID: %2$s) has been deleted": [ + "" + ], + "Failed to delete instance": [ + "" + ], + "Instance '%1$s' (ID: %2$s) has been disabled": [ + "" + ], + "Failed to purge instance": [ + "" + ], + "Pending KYC verification": [ + "" + ], + "Timed out": [ + "" + ], + "Exchange": [ + "" + ], + "Target account": [ + "" + ], + "KYC URL": [ + "" + ], + "Code": [ + "" + ], + "Http Status": [ + "" + ], + "No pending kyc verification!": [ + "" + ], + "change value to unknown date": [ + "" + ], + "change value to empty": [ + "" + ], + "clear": [ + "" + ], + "change value to never": [ + "" + ], + "never": [ + "" + ], + "Country": [ + "" + ], + "Address": [ + "" + ], + "Building number": [ + "" + ], + "Building name": [ + "" + ], + "Street": [ + "" + ], + "Post code": [ + "" + ], + "Town location": [ + "" + ], + "Town": [ + "" + ], + "District": [ + "" + ], + "Country subdivision": [ + "" + ], + "Product id": [ + "" + ], + "Description": [ + "" + ], + "Product": [ + "" + ], + "search products by it's description or id": [ + "" + ], + "no products found with that description": [ + "" + ], + "You must enter a valid product identifier.": [ + "" + ], + "Quantity must be greater than 0!": [ + "" + ], + "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [ + "" + ], + "Quantity": [ + "" + ], + "how many products will be added": [ + "" + ], + "Add from inventory": [ + "" + ], + "Image should be smaller than 1 MB": [ + "" + ], + "Add": [ + "" + ], + "Remove": [ + "" + ], + "No taxes configured for this product.": [ + "" + ], + "Amount": [ + "" + ], + "Taxes can be in currencies that differ from the main currency used by the merchant.": [ + "" + ], + "Enter currency and value separated with a colon, e.g. "USD:2.3".": [ + "" + ], + "Legal name of the tax, e.g. VAT or import duties.": [ + "" + ], + "add tax to the tax list": [ + "" + ], + "describe and add a product that is not in the inventory list": [ + "" + ], + "Add custom product": [ + "" + ], + "Complete information of the product": [ + "" + ], + "Image": [ + "" + ], + "photo of the product": [ + "" + ], + "full product description": [ + "" + ], + "Unit": [ + "" + ], + "name of the product unit": [ + "" + ], + "Price": [ + "" + ], + "amount in the current currency": [ + "" + ], + "Taxes": [ + "" + ], + "image": [ + "" + ], + "description": [ + "" + ], + "quantity": [ + "" + ], + "unit price": [ + "" + ], + "total price": [ + "" + ], + "required": [ + "" + ], + "not valid": [ + "" + ], + "must be greater than 0": [ + "" + ], + "not a valid json": [ + "" + ], + "should be in the future": [ + "" + ], + "refund deadline cannot be before pay deadline": [ + "" + ], + "wire transfer deadline cannot be before refund deadline": [ + "" + ], + "wire transfer deadline cannot be before pay deadline": [ + "" + ], + "should have a refund deadline": [ + "" + ], + "auto refund cannot be after refund deadline": [ + "" + ], + "Manage products in order": [ + "" + ], + "Manage list of products in the order.": [ + "" + ], + "Remove this product from the order.": [ + "" + ], + "Total price": [ + "" + ], + "total product price added up": [ + "" + ], + "Amount to be paid by the customer": [ + "" + ], + "Order price": [ + "" + ], + "final order price": [ + "" + ], + "Summary": [ + "" + ], + "Title of the order to be shown to the customer": [ + "" + ], + "Shipping and Fulfillment": [ + "" + ], + "Delivery date": [ + "" + ], + "Deadline for physical delivery assured by the merchant.": [ + "" + ], + "Location": [ + "" + ], + "address where the products will be delivered": [ + "" + ], + "Fulfillment URL": [ + "" + ], + "URL to which the user will be redirected after successful payment.": [ + "" + ], + "Taler payment options": [ + "" + ], + "Override default Taler payment settings for this order": [ + "" + ], + "Payment deadline": [ + "" + ], + "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [ + "" + ], + "Refund deadline": [ + "" + ], + "Time until which the order can be refunded by the merchant.": [ + "" + ], + "Wire transfer deadline": [ + "" + ], + "Deadline for the exchange to make the wire transfer.": [ + "" + ], + "Auto-refund deadline": [ + "" + ], + "Time until which the wallet will automatically check for refunds without user interaction.": [ + "" + ], + "Maximum deposit fee": [ + "" + ], + "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [ + "" + ], + "Maximum wire fee": [ + "" + ], + "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [ + "" + ], + "Wire fee amortization": [ + "" + ], + "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [ + "" + ], + "Create token": [ + "" + ], + "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [ + "" + ], + "Minimum age required": [ + "" + ], + "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [ + "" + ], + "Min age defined by the producs is %1$s": [ + "" + ], + "Additional information": [ + "" + ], + "Custom information to be included in the contract for this order.": [ + "" + ], + "You must enter a value in JavaScript Object Notation (JSON).": [ + "" + ], + "days": [ + "" + ], + "hours": [ + "" + ], + "minutes": [ + "" + ], + "seconds": [ + "" + ], + "forever": [ + "" + ], + "%1$sM": [ + "" + ], + "%1$sY": [ + "" + ], + "%1$sd": [ + "" + ], + "%1$sh": [ + "" + ], + "%1$smin": [ + "" + ], + "%1$ssec": [ + "" + ], + "Orders": [ + "" + ], + "create order": [ + "" + ], + "load newer orders": [ + "" + ], + "Date": [ + "" + ], + "Refund": [ + "" + ], + "copy url": [ + "" + ], + "load older orders": [ + "" + ], + "No orders have been found matching your query!": [ + "" + ], + "duplicated": [ + "" + ], + "invalid format": [ + "" + ], + "this value exceed the refundable amount": [ + "" + ], + "date": [ + "" + ], + "amount": [ + "" + ], + "reason": [ + "" + ], + "amount to be refunded": [ + "" + ], + "Max refundable:": [ + "" + ], + "Reason": [ + "" + ], + "Choose one...": [ + "" + ], + "requested by the customer": [ + "" + ], + "other": [ + "" + ], + "why this order is being refunded": [ + "" + ], + "more information to give context": [ + "" + ], + "Contract Terms": [ + "" + ], + "human-readable description of the whole purchase": [ + "" + ], + "total price for the transaction": [ + "" + ], + "URL for this purchase": [ + "" + ], + "Max fee": [ + "" + ], + "maximum total deposit fee accepted by the merchant for this contract": [ + "" + ], + "Max wire fee": [ + "" + ], + "maximum wire fee accepted by the merchant": [ + "" + ], + "over how many customer transactions does the merchant expect to amortize wire fees on average": [ + "" + ], + "Created at": [ + "" + ], + "time when this contract was generated": [ + "" + ], + "after this deadline has passed no refunds will be accepted": [ + "" + ], + "after this deadline, the merchant won't accept payments for the contract": [ + "" + ], + "transfer deadline for the exchange": [ + "" + ], + "time indicating when the order should be delivered": [ + "" + ], + "where the order will be delivered": [ + "" + ], + "Auto-refund delay": [ + "" + ], + "how long the wallet should try to get an automatic refund for the purchase": [ + "" + ], + "Extra info": [ + "" + ], + "extra data that is only interpreted by the merchant frontend": [ + "" + ], + "Order": [ + "" + ], + "claimed": [ + "" + ], + "claimed at": [ + "" + ], + "Timeline": [ + "" + ], + "Payment details": [ + "" + ], + "Order status": [ + "" + ], + "Product list": [ + "" + ], + "paid": [ + "" + ], + "wired": [ + "" + ], + "refunded": [ + "" + ], + "refund order": [ + "" + ], + "not refundable": [ + "" + ], + "refund": [ + "" + ], + "Refunded amount": [ + "" + ], + "Refund taken": [ + "" + ], + "Status URL": [ + "" + ], + "Refund URI": [ + "" + ], + "unpaid": [ + "" + ], + "pay at": [ + "" + ], + "created at": [ + "" + ], + "Order status URL": [ + "" + ], + "Payment URI": [ + "" + ], + "Unknown order status. This is an error, please contact the administrator.": [ + "" + ], + "Back": [ + "" + ], + "refund created successfully": [ + "" + ], + "could not create the refund": [ + "" + ], + "select date to show nearby orders": [ + "" + ], + "order id": [ + "" + ], + "jump to order with the given order ID": [ + "" + ], + "remove all filters": [ + "" + ], + "only show paid orders": [ + "" + ], + "Paid": [ + "" + ], + "only show orders with refunds": [ + "" + ], + "Refunded": [ + "" + ], + "only show orders where customers paid, but wire payments from payment provider are still pending": [ + "" + ], + "Not wired": [ + "" + ], + "clear date filter": [ + "" + ], + "date (YYYY/MM/DD)": [ + "" + ], + "Enter an order id": [ + "" + ], + "order not found": [ + "" + ], + "could not get the order to refund": [ + "" + ], + "Loading...": [ + "" + ], + "click here to configure the stock of the product, leave it as is and the backend will not control stock": [ + "" + ], + "Manage stock": [ + "" + ], + "this product has been configured without stock control": [ + "" + ], + "Infinite": [ + "" + ], + "lost cannot be greater than current and incoming (max %1$s)": [ + "" + ], + "Incoming": [ + "" + ], + "Lost": [ + "" + ], + "Current": [ + "" + ], + "remove stock control for this product": [ + "" + ], + "without stock": [ + "" + ], + "Next restock": [ + "" + ], + "Delivery address": [ + "" + ], + "product identification to use in URLs (for internal use only)": [ + "" + ], + "illustration of the product for customers": [ + "" + ], + "product description for customers": [ + "" + ], + "Age restricted": [ + "" + ], + "is this product restricted for customer below certain age?": [ + "" + ], + "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [ + "" + ], + "sale price for customers, including taxes, for above units of the product": [ + "" + ], + "Stock": [ + "" + ], + "product inventory for products with finite supply (for internal use only)": [ + "" + ], + "taxes included in the product price, exposed to customers": [ + "" + ], + "Need to complete marked fields": [ + "" + ], + "could not create product": [ + "" + ], + "Products": [ + "" + ], + "add product to inventory": [ + "" + ], + "Sell": [ + "" + ], + "Profit": [ + "" + ], + "Sold": [ + "" + ], + "free": [ + "" + ], + "go to product update page": [ + "" + ], + "Update": [ + "" + ], + "remove this product from the database": [ + "" + ], + "update the product with new price": [ + "" + ], + "update product with new price": [ + "" + ], + "add more elements to the inventory": [ + "" + ], + "report elements lost in the inventory": [ + "" + ], + "new price for the product": [ + "" + ], + "the are value with errors": [ + "" + ], + "update product with new stock and price": [ + "" + ], + "There is no products yet, add more pressing the + sign": [ + "" + ], + "product updated successfully": [ + "" + ], + "could not update the product": [ + "" + ], + "product delete successfully": [ + "" + ], + "could not delete the product": [ + "" + ], + "Product id:": [ + "" + ], + "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [ + "" + ], + "If your system supports RFC 8905, you can do this by opening this URI:": [ + "" + ], + "it should be greater than 0": [ + "" + ], + "must be a valid URL": [ + "" + ], + "Initial balance": [ + "" + ], + "balance prior to deposit": [ + "" + ], + "Exchange URL": [ + "" + ], + "URL of exchange": [ + "" + ], + "Next": [ + "" + ], + "Wire method": [ + "" + ], + "method to use for wire transfer": [ + "" + ], + "Select one wire method": [ + "" + ], + "could not create reserve": [ + "" + ], + "Valid until": [ + "" + ], + "Created balance": [ + "" + ], + "Exchange balance": [ + "" + ], + "Picked up": [ + "" + ], + "Committed": [ + "" + ], + "Account address": [ + "" + ], + "Subject": [ + "" + ], + "Tips": [ + "" + ], + "No tips has been authorized from this reserve": [ + "" + ], + "Authorized": [ + "" + ], + "Expiration": [ + "" + ], + "amount of tip": [ + "" + ], + "Justification": [ + "" + ], + "reason for the tip": [ + "" + ], + "URL after tip": [ + "" + ], + "URL to visit after tip payment": [ + "" + ], + "Reserves not yet funded": [ + "" + ], + "Reserves ready": [ + "" + ], + "add new reserve": [ + "" + ], + "Expires at": [ + "" + ], + "Initial": [ + "" + ], + "delete selected reserve from the database": [ + "" + ], + "authorize new tip from selected reserve": [ + "" + ], + "There is no ready reserves yet, add more pressing the + sign or fund them": [ + "" + ], + "Expected Balance": [ + "" + ], + "could not create the tip": [ + "" + ], + "should not be empty": [ + "" + ], + "should be greater that 0": [ + "" + ], + "can't be empty": [ + "" + ], + "to short": [ + "" + ], + "just letters and numbers from 2 to 7": [ + "" + ], + "size of the key should be 32": [ + "" + ], + "Identifier": [ + "" + ], + "Name of the template in URLs.": [ + "" + ], + "Describe what this template stands for": [ + "" + ], + "Fixed summary": [ + "" + ], + "If specified, this template will create order with the same summary": [ + "" + ], + "Fixed price": [ + "" + ], + "If specified, this template will create order with the same price": [ + "" + ], + "Minimum age": [ + "" + ], + "Is this contract restricted to some age?": [ + "" + ], + "Payment timeout": [ + "" + ], + "How much time has the customer to complete the payment once the order was created.": [ + "" + ], + "Verification algorithm": [ + "" + ], + "Algorithm to use to verify transaction in offline mode": [ + "" + ], + "Point-of-sale key": [ + "" + ], + "Useful to validate the purchase": [ + "" + ], + "generate random secret key": [ + "" + ], + "random": [ + "" + ], + "show secret key": [ + "" + ], + "hide secret key": [ + "" + ], + "hide": [ + "" + ], + "show": [ + "" + ], + "could not inform template": [ + "" + ], + "Amount is required": [ + "" + ], + "Order summary is required": [ + "" + ], + "New order for template": [ + "" + ], + "Amount of the order": [ + "" + ], + "Order summary": [ + "" + ], + "could not create order from template": [ + "" + ], + "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [ + "" + ], + "Fixed amount": [ + "" + ], + "Default amount": [ + "" + ], + "Default summary": [ + "" + ], + "Print": [ + "" + ], + "Setup TOTP": [ + "" + ], + "Templates": [ + "" + ], + "add new templates": [ + "" + ], + "load more templates before the first one": [ + "" + ], + "load newer templates": [ + "" + ], + "delete selected templates from the database": [ + "" + ], + "use template to create new order": [ + "" + ], + "create qr code for the template": [ + "" + ], + "load more templates after the last one": [ + "" + ], + "load older templates": [ + "" + ], + "There is no templates yet, add more pressing the + sign": [ + "" + ], + "template delete successfully": [ + "" + ], + "could not delete the template": [ + "" + ], + "could not update template": [ + "" + ], + "should be one of '%1$s'": [ + "" + ], + "Webhook ID to use": [ + "" + ], + "Event": [ + "" + ], + "The event of the webhook: why the webhook is used": [ + "" + ], + "Method": [ + "" + ], + "Method used by the webhook": [ + "" + ], + "URL": [ + "" + ], + "URL of the webhook where the customer will be redirected": [ + "" + ], + "Header": [ + "" + ], + "Header template of the webhook": [ + "" + ], + "Body": [ + "" + ], + "Body template by the webhook": [ + "" + ], + "Webhooks": [ + "" + ], + "add new webhooks": [ + "" + ], + "load more webhooks before the first one": [ + "" + ], + "load newer webhooks": [ + "" + ], + "Event type": [ + "" + ], + "delete selected webhook from the database": [ + "" + ], + "load more webhooks after the last one": [ + "" + ], + "load older webhooks": [ + "" + ], + "There is no webhooks yet, add more pressing the + sign": [ + "" + ], + "webhook delete successfully": [ + "" + ], + "could not delete the webhook": [ + "" + ], + "check the id, does not look valid": [ + "" + ], + "should have 52 characters, current %1$s": [ + "" + ], + "URL doesn't have the right format": [ + "" + ], + "Credited bank account": [ + "" + ], + "Select one account": [ + "" + ], + "Bank account of the merchant where the payment was received": [ + "" + ], + "Wire transfer ID": [ + "" + ], + "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [ + "" + ], + "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [ + "" + ], + "Amount credited": [ + "" + ], + "Actual amount that was wired to the merchant's bank account": [ + "" + ], + "could not inform transfer": [ + "" + ], + "Transfers": [ + "" + ], + "add new transfer": [ + "" + ], + "load more transfers before the first one": [ + "" + ], + "load newer transfers": [ + "" + ], + "Credit": [ + "" + ], + "Confirmed": [ + "" + ], + "Verified": [ + "" + ], + "Executed at": [ + "" + ], + "yes": [ + "" + ], + "no": [ + "" + ], + "unknown": [ + "" + ], + "delete selected transfer from the database": [ + "" + ], + "load more transfer after the last one": [ + "" + ], + "load older transfers": [ + "" + ], + "There is no transfer yet, add more pressing the + sign": [ + "" + ], + "filter by account address": [ + "" + ], + "only show wire transfers confirmed by the merchant": [ + "" + ], + "only show wire transfers claimed by the exchange": [ + "" + ], + "Unverified": [ + "" + ], + "is not valid": [ + "" + ], + "is not a number": [ + "" + ], + "must be 1 or greater": [ + "" + ], + "max 7 lines": [ + "" + ], + "change authorization configuration": [ + "" + ], + "Need to complete marked fields and choose authorization method": [ + "" + ], + "This is not a valid bitcoin address.": [ + "" + ], + "This is not a valid Ethereum address.": [ + "" + ], + "IBAN numbers usually have more that 4 digits": [ + "" + ], + "IBAN numbers usually have less that 34 digits": [ + "" + ], + "IBAN country code not found": [ + "" + ], + "IBAN number is not valid, checksum is wrong": [ + "" + ], + "Target type": [ + "" + ], + "Method to use for wire transfer": [ + "" + ], + "Routing": [ + "" + ], + "Routing number.": [ + "" + ], + "Account": [ + "" + ], + "Account number.": [ + "" + ], + "Business Identifier Code.": [ + "" + ], + "Bank Account Number.": [ + "" + ], + "Unified Payment Interface.": [ + "" + ], + "Bitcoin protocol.": [ + "" + ], + "Ethereum protocol.": [ + "" + ], + "Interledger protocol.": [ + "" + ], + "Host": [ + "" + ], + "Bank host.": [ + "" + ], + "Bank account.": [ + "" + ], + "Bank account owner's name.": [ + "" + ], + "No accounts yet.": [ + "" + ], + "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [ + "" + ], + "Business name": [ + "" + ], + "Legal name of the business represented by this instance.": [ + "" + ], + "Email": [ + "" + ], + "Contact email": [ + "" + ], + "Website URL": [ + "" + ], + "URL.": [ + "" + ], + "Logo": [ + "" + ], + "Logo image.": [ + "" + ], + "Bank account": [ + "" + ], + "URI specifying bank account for crediting revenue.": [ + "" + ], + "Default max deposit fee": [ + "" + ], + "Maximum deposit fees this merchant is willing to pay per order by default.": [ + "" + ], + "Default max wire fee": [ + "" + ], + "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [ + "" + ], + "Default wire fee amortization": [ + "" + ], + "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [ + "" + ], + "Physical location of the merchant.": [ + "" + ], + "Jurisdiction": [ + "" + ], + "Jurisdiction for legal disputes with the merchant.": [ + "" + ], + "Default payment delay": [ + "" + ], + "Time customers have to pay an order before the offer expires by default.": [ + "" + ], + "Default wire transfer delay": [ + "" + ], + "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [ + "" + ], + "Instance id": [ + "" + ], + "Change the authorization method use for this instance.": [ + "" + ], + "Manage access token": [ + "" + ], + "Failed to create instance": [ + "" + ], + "Login required": [ + "" + ], + "Please enter your access token.": [ + "" + ], + "Access Token": [ + "" + ], + "The request to the backend take too long and was cancelled": [ + "" + ], + "Diagnostic from %1$s is \"%2$s\"": [ + "" + ], + "The backend reported a problem: HTTP status #%1$s": [ + "" + ], + "Diagnostic from %1$s is '%2$s'": [ + "" + ], + "Access denied": [ + "" + ], + "The access token provided is invalid.": [ + "" + ], + "No 'default' instance configured yet.": [ + "" + ], + "Create a 'default' instance to begin using the merchant backoffice.": [ + "" + ], + "The access token provided is invalid": [ + "" + ], + "Hide for today": [ + "" + ], + "Instance": [ + "" + ], + "Settings": [ + "" + ], + "Connection": [ + "" + ], + "New": [ + "" + ], + "List": [ + "" + ], + "Log out": [ + "" + ], + "Check your token is valid": [ + "" + ], + "Couldn't access the server.": [ + "" + ], + "Could not infer instance id from url %1$s": [ + "" + ], + "Server not found": [ + "" + ], + "Server response with an error code": [ + "" + ], + "Got message %1$s from %2$s": [ + "" + ], + "Response from server is unreadable, http status: %1$s": [ + "" + ], + "Unexpected Error": [ + "" + ], + "The value %1$s is invalid for a payment url": [ + "" + ], + "add element to the list": [ + "" + ], + "add": [ + "" + ], + "Deleting": [ + "" + ], + "Changing": [ + "" + ], + "Order ID": [ + "" + ], + "Payment URL": [ + "" + ] + } + } +}; + diff --git a/packages/auditor-backoffice-ui/src/i18n/sv.po b/packages/auditor-backoffice-ui/src/i18n/sv.po new file mode 100644 index 000000000..d8d0bae29 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/sv.po @@ -0,0 +1,2741 @@ +# This file is part of TALER +# (C) 2016 GNUnet e.V. +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"POT-Creation-Date: 2016-11-23 00:00+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/components/modal/index.tsx:71 +#, c-format +msgid "Cancel" +msgstr "" + +#: src/components/modal/index.tsx:79 +#, c-format +msgid "%1$s" +msgstr "" + +#: src/components/modal/index.tsx:84 +#, c-format +msgid "Close" +msgstr "" + +#: src/components/modal/index.tsx:124 +#, c-format +msgid "Continue" +msgstr "" + +#: src/components/modal/index.tsx:178 +#, c-format +msgid "Clear" +msgstr "" + +#: src/components/modal/index.tsx:190 +#, c-format +msgid "Confirm" +msgstr "" + +#: src/components/modal/index.tsx:296 +#, c-format +msgid "is not the same as the current access token" +msgstr "" + +#: src/components/modal/index.tsx:299 +#, c-format +msgid "cannot be empty" +msgstr "" + +#: src/components/modal/index.tsx:301 +#, c-format +msgid "cannot be the same as the old token" +msgstr "" + +#: src/components/modal/index.tsx:305 +#, c-format +msgid "is not the same" +msgstr "" + +#: src/components/modal/index.tsx:315 +#, c-format +msgid "You are updating the access token from instance with id %1$s" +msgstr "" + +#: src/components/modal/index.tsx:331 +#, c-format +msgid "Old access token" +msgstr "" + +#: src/components/modal/index.tsx:332 +#, c-format +msgid "access token currently in use" +msgstr "" + +#: src/components/modal/index.tsx:338 +#, c-format +msgid "New access token" +msgstr "" + +#: src/components/modal/index.tsx:339 +#, c-format +msgid "next access token to be used" +msgstr "" + +#: src/components/modal/index.tsx:344 +#, c-format +msgid "Repeat access token" +msgstr "" + +#: src/components/modal/index.tsx:345 +#, c-format +msgid "confirm the same access token" +msgstr "" + +#: src/components/modal/index.tsx:350 +#, c-format +msgid "Clearing the access token will mean public access to the instance" +msgstr "" + +#: src/components/modal/index.tsx:377 +#, c-format +msgid "cannot be the same as the old access token" +msgstr "" + +#: src/components/modal/index.tsx:394 +#, c-format +msgid "You are setting the access token for the new instance" +msgstr "" + +#: src/components/modal/index.tsx:420 +#, c-format +msgid "" +"With external authorization method no check will be done by the merchant " +"backend" +msgstr "" + +#: src/components/modal/index.tsx:436 +#, c-format +msgid "Set external authorization" +msgstr "" + +#: src/components/modal/index.tsx:448 +#, c-format +msgid "Set access token" +msgstr "" + +#: src/components/modal/index.tsx:470 +#, c-format +msgid "Operation in progress..." +msgstr "" + +#: src/components/modal/index.tsx:479 +#, c-format +msgid "The operation will be automatically canceled after %1$s seconds" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:80 +#, c-format +msgid "Instances" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:93 +#, c-format +msgid "Delete" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:99 +#, c-format +msgid "add new instance" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:178 +#, c-format +msgid "ID" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:181 +#, c-format +msgid "Name" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:220 +#, c-format +msgid "Edit" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:237 +#, c-format +msgid "Purge" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:261 +#, c-format +msgid "There is no instances yet, add more pressing the + sign" +msgstr "" + +#: src/paths/admin/list/View.tsx:68 +#, c-format +msgid "Only show active instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:71 +#, c-format +msgid "Active" +msgstr "" + +#: src/paths/admin/list/View.tsx:78 +#, c-format +msgid "Only show deleted instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:81 +#, c-format +msgid "Deleted" +msgstr "" + +#: src/paths/admin/list/View.tsx:88 +#, c-format +msgid "Show all instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:91 +#, c-format +msgid "All" +msgstr "" + +#: src/paths/admin/list/index.tsx:101 +#, c-format +msgid "Instance \"%1$s\" (ID: %2$s) has been deleted" +msgstr "" + +#: src/paths/admin/list/index.tsx:106 +#, c-format +msgid "Failed to delete instance" +msgstr "" + +#: src/paths/admin/list/index.tsx:124 +#, c-format +msgid "Instance '%1$s' (ID: %2$s) has been disabled" +msgstr "" + +#: src/paths/admin/list/index.tsx:129 +#, c-format +msgid "Failed to purge instance" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:41 +#, c-format +msgid "Pending KYC verification" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:66 +#, c-format +msgid "Timed out" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:103 +#, c-format +msgid "Exchange" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:106 +#, c-format +msgid "Target account" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:109 +#, c-format +msgid "KYC URL" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:144 +#, c-format +msgid "Code" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:147 +#, c-format +msgid "Http Status" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:177 +#, c-format +msgid "No pending kyc verification!" +msgstr "" + +#: src/components/form/InputDate.tsx:123 +#, c-format +msgid "change value to unknown date" +msgstr "" + +#: src/components/form/InputDate.tsx:124 +#, c-format +msgid "change value to empty" +msgstr "" + +#: src/components/form/InputDate.tsx:131 +#, c-format +msgid "clear" +msgstr "" + +#: src/components/form/InputDate.tsx:136 +#, c-format +msgid "change value to never" +msgstr "" + +#: src/components/form/InputDate.tsx:141 +#, c-format +msgid "never" +msgstr "" + +#: src/components/form/InputLocation.tsx:29 +#, c-format +msgid "Country" +msgstr "" + +#: src/components/form/InputLocation.tsx:33 +#, c-format +msgid "Address" +msgstr "" + +#: src/components/form/InputLocation.tsx:39 +#, c-format +msgid "Building number" +msgstr "" + +#: src/components/form/InputLocation.tsx:41 +#, c-format +msgid "Building name" +msgstr "" + +#: src/components/form/InputLocation.tsx:42 +#, c-format +msgid "Street" +msgstr "" + +#: src/components/form/InputLocation.tsx:43 +#, c-format +msgid "Post code" +msgstr "" + +#: src/components/form/InputLocation.tsx:44 +#, c-format +msgid "Town location" +msgstr "" + +#: src/components/form/InputLocation.tsx:45 +#, c-format +msgid "Town" +msgstr "" + +#: src/components/form/InputLocation.tsx:46 +#, c-format +msgid "District" +msgstr "" + +#: src/components/form/InputLocation.tsx:49 +#, c-format +msgid "Country subdivision" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:66 +#, c-format +msgid "Product id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:69 +#, c-format +msgid "Description" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:94 +#, c-format +msgid "Product" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:95 +#, c-format +msgid "search products by it's description or id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:151 +#, c-format +msgid "no products found with that description" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:56 +#, c-format +msgid "You must enter a valid product identifier." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:64 +#, c-format +msgid "Quantity must be greater than 0!" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:76 +#, c-format +msgid "" +"This quantity exceeds remaining stock. Currently, only %1$s units remain " +"unreserved in stock." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:109 +#, c-format +msgid "Quantity" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:110 +#, c-format +msgid "how many products will be added" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:117 +#, c-format +msgid "Add from inventory" +msgstr "" + +#: src/components/form/InputImage.tsx:105 +#, c-format +msgid "Image should be smaller than 1 MB" +msgstr "" + +#: src/components/form/InputImage.tsx:110 +#, c-format +msgid "Add" +msgstr "" + +#: src/components/form/InputImage.tsx:115 +#, c-format +msgid "Remove" +msgstr "" + +#: src/components/form/InputTaxes.tsx:113 +#, c-format +msgid "No taxes configured for this product." +msgstr "" + +#: src/components/form/InputTaxes.tsx:119 +#, c-format +msgid "Amount" +msgstr "" + +#: src/components/form/InputTaxes.tsx:120 +#, c-format +msgid "" +"Taxes can be in currencies that differ from the main currency used by the " +"merchant." +msgstr "" + +#: src/components/form/InputTaxes.tsx:122 +#, c-format +msgid "" +"Enter currency and value separated with a colon, e.g. "USD:2.3"." +msgstr "" + +#: src/components/form/InputTaxes.tsx:131 +#, c-format +msgid "Legal name of the tax, e.g. VAT or import duties." +msgstr "" + +#: src/components/form/InputTaxes.tsx:137 +#, c-format +msgid "add tax to the tax list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:72 +#, c-format +msgid "describe and add a product that is not in the inventory list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:75 +#, c-format +msgid "Add custom product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:86 +#, c-format +msgid "Complete information of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:185 +#, c-format +msgid "Image" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:186 +#, c-format +msgid "photo of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:192 +#, c-format +msgid "full product description" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:196 +#, c-format +msgid "Unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:197 +#, c-format +msgid "name of the product unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:201 +#, c-format +msgid "Price" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:202 +#, c-format +msgid "amount in the current currency" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:211 +#, c-format +msgid "Taxes" +msgstr "" + +#: src/components/product/ProductList.tsx:38 +#, c-format +msgid "image" +msgstr "" + +#: src/components/product/ProductList.tsx:41 +#, c-format +msgid "description" +msgstr "" + +#: src/components/product/ProductList.tsx:44 +#, c-format +msgid "quantity" +msgstr "" + +#: src/components/product/ProductList.tsx:47 +#, c-format +msgid "unit price" +msgstr "" + +#: src/components/product/ProductList.tsx:50 +#, c-format +msgid "total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:153 +#, c-format +msgid "required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:157 +#, c-format +msgid "not valid" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:159 +#, c-format +msgid "must be greater than 0" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:164 +#, c-format +msgid "not a valid json" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:170 +#, c-format +msgid "should be in the future" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:173 +#, c-format +msgid "refund deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:179 +#, c-format +msgid "wire transfer deadline cannot be before refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:190 +#, c-format +msgid "wire transfer deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:197 +#, c-format +msgid "should have a refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:202 +#, c-format +msgid "auto refund cannot be after refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:360 +#, c-format +msgid "Manage products in order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:369 +#, c-format +msgid "Manage list of products in the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:391 +#, c-format +msgid "Remove this product from the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:415 +#, c-format +msgid "Total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:417 +#, c-format +msgid "total product price added up" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:430 +#, c-format +msgid "Amount to be paid by the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:436 +#, c-format +msgid "Order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:437 +#, c-format +msgid "final order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:444 +#, c-format +msgid "Summary" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:445 +#, c-format +msgid "Title of the order to be shown to the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:450 +#, c-format +msgid "Shipping and Fulfillment" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:455 +#, c-format +msgid "Delivery date" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:456 +#, c-format +msgid "Deadline for physical delivery assured by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:461 +#, c-format +msgid "Location" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:462 +#, c-format +msgid "address where the products will be delivered" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:469 +#, c-format +msgid "Fulfillment URL" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:470 +#, c-format +msgid "URL to which the user will be redirected after successful payment." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:476 +#, c-format +msgid "Taler payment options" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:477 +#, c-format +msgid "Override default Taler payment settings for this order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:481 +#, c-format +msgid "Payment deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:482 +#, c-format +msgid "" +"Deadline for the customer to pay for the offer before it expires. Inventory " +"products will be reserved until this deadline." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:486 +#, c-format +msgid "Refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:487 +#, c-format +msgid "Time until which the order can be refunded by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:491 +#, c-format +msgid "Wire transfer deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:492 +#, c-format +msgid "Deadline for the exchange to make the wire transfer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:496 +#, c-format +msgid "Auto-refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:497 +#, c-format +msgid "" +"Time until which the wallet will automatically check for refunds without " +"user interaction." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:502 +#, c-format +msgid "Maximum deposit fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:503 +#, c-format +msgid "" +"Maximum deposit fees the merchant is willing to cover for this order. Higher " +"deposit fees must be covered in full by the consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:507 +#, c-format +msgid "Maximum wire fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:508 +#, c-format +msgid "" +"Maximum aggregate wire fees the merchant is willing to cover for this order. " +"Wire fees exceeding this amount are to be covered by the customers." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:512 +#, c-format +msgid "Wire fee amortization" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:513 +#, c-format +msgid "" +"Factor by which wire fees exceeding the above threshold are divided to " +"determine the share of excess wire fees to be paid explicitly by the " +"consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:517 +#, c-format +msgid "Create token" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:518 +#, c-format +msgid "" +"Uncheck this option if the merchant backend generated an order ID with " +"enough entropy to prevent adversarial claims." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:522 +#, c-format +msgid "Minimum age required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:523 +#, c-format +msgid "" +"Any value greater than 0 will limit the coins able be used to pay this " +"contract. If empty the age restriction will be defined by the products" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:526 +#, c-format +msgid "Min age defined by the producs is %1$s" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:534 +#, c-format +msgid "Additional information" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:535 +#, c-format +msgid "Custom information to be included in the contract for this order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:541 +#, c-format +msgid "You must enter a value in JavaScript Object Notation (JSON)." +msgstr "" + +#: src/components/picker/DurationPicker.tsx:55 +#, c-format +msgid "days" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:65 +#, c-format +msgid "hours" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:76 +#, c-format +msgid "minutes" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:87 +#, c-format +msgid "seconds" +msgstr "" + +#: src/components/form/InputDuration.tsx:53 +#, c-format +msgid "forever" +msgstr "" + +#: src/components/form/InputDuration.tsx:62 +#, c-format +msgid "%1$sM" +msgstr "" + +#: src/components/form/InputDuration.tsx:64 +#, c-format +msgid "%1$sY" +msgstr "" + +#: src/components/form/InputDuration.tsx:66 +#, c-format +msgid "%1$sd" +msgstr "" + +#: src/components/form/InputDuration.tsx:68 +#, c-format +msgid "%1$sh" +msgstr "" + +#: src/components/form/InputDuration.tsx:70 +#, c-format +msgid "%1$smin" +msgstr "" + +#: src/components/form/InputDuration.tsx:72 +#, c-format +msgid "%1$ssec" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:75 +#, c-format +msgid "Orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:81 +#, c-format +msgid "create order" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:147 +#, c-format +msgid "load newer orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:154 +#, c-format +msgid "Date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:200 +#, c-format +msgid "Refund" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:209 +#, c-format +msgid "copy url" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:225 +#, c-format +msgid "load older orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:242 +#, c-format +msgid "No orders have been found matching your query!" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:288 +#, c-format +msgid "duplicated" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:299 +#, c-format +msgid "invalid format" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:301 +#, c-format +msgid "this value exceed the refundable amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:346 +#, c-format +msgid "date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:349 +#, c-format +msgid "amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:352 +#, c-format +msgid "reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:389 +#, c-format +msgid "amount to be refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:391 +#, c-format +msgid "Max refundable:" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:396 +#, c-format +msgid "Reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:397 +#, c-format +msgid "Choose one..." +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:399 +#, c-format +msgid "requested by the customer" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:400 +#, c-format +msgid "other" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:403 +#, c-format +msgid "why this order is being refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:409 +#, c-format +msgid "more information to give context" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:62 +#, c-format +msgid "Contract Terms" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:68 +#, c-format +msgid "human-readable description of the whole purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:74 +#, c-format +msgid "total price for the transaction" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:81 +#, c-format +msgid "URL for this purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:87 +#, c-format +msgid "Max fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:88 +#, c-format +msgid "maximum total deposit fee accepted by the merchant for this contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:93 +#, c-format +msgid "Max wire fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:94 +#, c-format +msgid "maximum wire fee accepted by the merchant" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:100 +#, c-format +msgid "" +"over how many customer transactions does the merchant expect to amortize " +"wire fees on average" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:105 +#, c-format +msgid "Created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:106 +#, c-format +msgid "time when this contract was generated" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:112 +#, c-format +msgid "after this deadline has passed no refunds will be accepted" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:118 +#, c-format +msgid "" +"after this deadline, the merchant won't accept payments for the contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:124 +#, c-format +msgid "transfer deadline for the exchange" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:130 +#, c-format +msgid "time indicating when the order should be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:136 +#, c-format +msgid "where the order will be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:144 +#, c-format +msgid "Auto-refund delay" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:145 +#, c-format +msgid "" +"how long the wallet should try to get an automatic refund for the purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:150 +#, c-format +msgid "Extra info" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:151 +#, c-format +msgid "extra data that is only interpreted by the merchant frontend" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:219 +#, c-format +msgid "Order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:221 +#, c-format +msgid "claimed" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:247 +#, c-format +msgid "claimed at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:265 +#, c-format +msgid "Timeline" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:271 +#, c-format +msgid "Payment details" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:291 +#, c-format +msgid "Order status" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:301 +#, c-format +msgid "Product list" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:451 +#, c-format +msgid "paid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:455 +#, c-format +msgid "wired" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:460 +#, c-format +msgid "refunded" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:480 +#, c-format +msgid "refund order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:481 +#, c-format +msgid "not refundable" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:489 +#, c-format +msgid "refund" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:553 +#, c-format +msgid "Refunded amount" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:560 +#, c-format +msgid "Refund taken" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:570 +#, c-format +msgid "Status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:583 +#, c-format +msgid "Refund URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:636 +#, c-format +msgid "unpaid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:654 +#, c-format +msgid "pay at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:666 +#, c-format +msgid "created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:707 +#, c-format +msgid "Order status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:711 +#, c-format +msgid "Payment URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:740 +#, c-format +msgid "" +"Unknown order status. This is an error, please contact the administrator." +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:767 +#, c-format +msgid "Back" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:79 +#, c-format +msgid "refund created successfully" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:85 +#, c-format +msgid "could not create the refund" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:78 +#, c-format +msgid "select date to show nearby orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:94 +#, c-format +msgid "order id" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:100 +#, c-format +msgid "jump to order with the given order ID" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:122 +#, c-format +msgid "remove all filters" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:132 +#, c-format +msgid "only show paid orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:135 +#, c-format +msgid "Paid" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:142 +#, c-format +msgid "only show orders with refunds" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:145 +#, c-format +msgid "Refunded" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:152 +#, c-format +msgid "" +"only show orders where customers paid, but wire payments from payment " +"provider are still pending" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:155 +#, c-format +msgid "Not wired" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:170 +#, c-format +msgid "clear date filter" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:184 +#, c-format +msgid "date (YYYY/MM/DD)" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:103 +#, c-format +msgid "Enter an order id" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:111 +#, c-format +msgid "order not found" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:178 +#, c-format +msgid "could not get the order to refund" +msgstr "" + +#: src/components/exception/AsyncButton.tsx:43 +#, c-format +msgid "Loading..." +msgstr "" + +#: src/components/form/InputStock.tsx:99 +#, c-format +msgid "" +"click here to configure the stock of the product, leave it as is and the " +"backend will not control stock" +msgstr "" + +#: src/components/form/InputStock.tsx:109 +#, c-format +msgid "Manage stock" +msgstr "" + +#: src/components/form/InputStock.tsx:115 +#, c-format +msgid "this product has been configured without stock control" +msgstr "" + +#: src/components/form/InputStock.tsx:119 +#, c-format +msgid "Infinite" +msgstr "" + +#: src/components/form/InputStock.tsx:136 +#, c-format +msgid "lost cannot be greater than current and incoming (max %1$s)" +msgstr "" + +#: src/components/form/InputStock.tsx:176 +#, c-format +msgid "Incoming" +msgstr "" + +#: src/components/form/InputStock.tsx:177 +#, c-format +msgid "Lost" +msgstr "" + +#: src/components/form/InputStock.tsx:192 +#, c-format +msgid "Current" +msgstr "" + +#: src/components/form/InputStock.tsx:196 +#, c-format +msgid "remove stock control for this product" +msgstr "" + +#: src/components/form/InputStock.tsx:202 +#, c-format +msgid "without stock" +msgstr "" + +#: src/components/form/InputStock.tsx:211 +#, c-format +msgid "Next restock" +msgstr "" + +#: src/components/form/InputStock.tsx:217 +#, c-format +msgid "Delivery address" +msgstr "" + +#: src/components/product/ProductForm.tsx:133 +#, c-format +msgid "product identification to use in URLs (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:139 +#, c-format +msgid "illustration of the product for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:145 +#, c-format +msgid "product description for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:149 +#, c-format +msgid "Age restricted" +msgstr "" + +#: src/components/product/ProductForm.tsx:150 +#, c-format +msgid "is this product restricted for customer below certain age?" +msgstr "" + +#: src/components/product/ProductForm.tsx:155 +#, c-format +msgid "" +"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 " +"items, 5 meters) for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:160 +#, c-format +msgid "" +"sale price for customers, including taxes, for above units of the product" +msgstr "" + +#: src/components/product/ProductForm.tsx:164 +#, c-format +msgid "Stock" +msgstr "" + +#: src/components/product/ProductForm.tsx:166 +#, c-format +msgid "" +"product inventory for products with finite supply (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:171 +#, c-format +msgid "taxes included in the product price, exposed to customers" +msgstr "" + +#: src/paths/instance/products/create/CreatePage.tsx:66 +#, c-format +msgid "Need to complete marked fields" +msgstr "" + +#: src/paths/instance/products/create/index.tsx:51 +#, c-format +msgid "could not create product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:68 +#, c-format +msgid "Products" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:73 +#, c-format +msgid "add product to inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:137 +#, c-format +msgid "Sell" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:143 +#, c-format +msgid "Profit" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:149 +#, c-format +msgid "Sold" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:210 +#, c-format +msgid "free" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:248 +#, c-format +msgid "go to product update page" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:255 +#, c-format +msgid "Update" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:260 +#, c-format +msgid "remove this product from the database" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:331 +#, c-format +msgid "update the product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:341 +#, c-format +msgid "update product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:399 +#, c-format +msgid "add more elements to the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:404 +#, c-format +msgid "report elements lost in the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:409 +#, c-format +msgid "new price for the product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:421 +#, c-format +msgid "the are value with errors" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:422 +#, c-format +msgid "update product with new stock and price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:463 +#, c-format +msgid "There is no products yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:86 +#, c-format +msgid "product updated successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:92 +#, c-format +msgid "could not update the product" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:103 +#, c-format +msgid "product delete successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:109 +#, c-format +msgid "could not delete the product" +msgstr "" + +#: src/paths/instance/products/update/UpdatePage.tsx:56 +#, c-format +msgid "Product id:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95 +#, c-format +msgid "" +"To complete the setup of the reserve, you must now initiate a wire transfer " +"using the given wire transfer subject and crediting the specified amount to " +"the indicated account of the exchange." +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102 +#, c-format +msgid "If your system supports RFC 8905, you can do this by opening this URI:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:83 +#, c-format +msgid "it should be greater than 0" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:88 +#, c-format +msgid "must be a valid URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:107 +#, c-format +msgid "Initial balance" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:108 +#, c-format +msgid "balance prior to deposit" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:112 +#, c-format +msgid "Exchange URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:113 +#, c-format +msgid "URL of exchange" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:148 +#, c-format +msgid "Next" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:186 +#, c-format +msgid "Wire method" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:187 +#, c-format +msgid "method to use for wire transfer" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:189 +#, c-format +msgid "Select one wire method" +msgstr "" + +#: src/paths/instance/reserves/create/index.tsx:62 +#, c-format +msgid "could not create reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:77 +#, c-format +msgid "Valid until" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:82 +#, c-format +msgid "Created balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:99 +#, c-format +msgid "Exchange balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:104 +#, c-format +msgid "Picked up" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:109 +#, c-format +msgid "Committed" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:116 +#, c-format +msgid "Account address" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:119 +#, c-format +msgid "Subject" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:130 +#, c-format +msgid "Tips" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:193 +#, c-format +msgid "No tips has been authorized from this reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:213 +#, c-format +msgid "Authorized" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:222 +#, c-format +msgid "Expiration" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108 +#, c-format +msgid "amount of tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112 +#, c-format +msgid "Justification" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114 +#, c-format +msgid "reason for the tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118 +#, c-format +msgid "URL after tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119 +#, c-format +msgid "URL to visit after tip payment" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:65 +#, c-format +msgid "Reserves not yet funded" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:89 +#, c-format +msgid "Reserves ready" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:95 +#, c-format +msgid "add new reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:143 +#, c-format +msgid "Expires at" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:146 +#, c-format +msgid "Initial" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:202 +#, c-format +msgid "delete selected reserve from the database" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:210 +#, c-format +msgid "authorize new tip from selected reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:237 +#, c-format +msgid "" +"There is no ready reserves yet, add more pressing the + sign or fund them" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:264 +#, c-format +msgid "Expected Balance" +msgstr "" + +#: src/paths/instance/reserves/list/index.tsx:110 +#, c-format +msgid "could not create the tip" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:77 +#, c-format +msgid "should not be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:93 +#, c-format +msgid "should be greater that 0" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:96 +#, c-format +msgid "can't be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:100 +#, c-format +msgid "to short" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:108 +#, c-format +msgid "just letters and numbers from 2 to 7" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:110 +#, c-format +msgid "size of the key should be 32" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:137 +#, c-format +msgid "Identifier" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:138 +#, c-format +msgid "Name of the template in URLs." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:144 +#, c-format +msgid "Describe what this template stands for" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:149 +#, c-format +msgid "Fixed summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:150 +#, c-format +msgid "If specified, this template will create order with the same summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:154 +#, c-format +msgid "Fixed price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:155 +#, c-format +msgid "If specified, this template will create order with the same price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:159 +#, c-format +msgid "Minimum age" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:161 +#, c-format +msgid "Is this contract restricted to some age?" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:165 +#, c-format +msgid "Payment timeout" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:167 +#, c-format +msgid "" +"How much time has the customer to complete the payment once the order was " +"created." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:171 +#, c-format +msgid "Verification algorithm" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:172 +#, c-format +msgid "Algorithm to use to verify transaction in offline mode" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:180 +#, c-format +msgid "Point-of-sale key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:182 +#, c-format +msgid "Useful to validate the purchase" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:196 +#, c-format +msgid "generate random secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:203 +#, c-format +msgid "random" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:208 +#, c-format +msgid "show secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:209 +#, c-format +msgid "hide secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:216 +#, c-format +msgid "hide" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:218 +#, c-format +msgid "show" +msgstr "" + +#: src/paths/instance/templates/create/index.tsx:52 +#, c-format +msgid "could not inform template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:54 +#, c-format +msgid "Amount is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:58 +#, c-format +msgid "Order summary is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:86 +#, c-format +msgid "New order for template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:108 +#, c-format +msgid "Amount of the order" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:113 +#, c-format +msgid "Order summary" +msgstr "" + +#: src/paths/instance/templates/use/index.tsx:92 +#, c-format +msgid "could not create order from template" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:131 +#, c-format +msgid "" +"Here you can specify a default value for fields that are not fixed. Default " +"values can be edited by the customer before the payment." +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:148 +#, c-format +msgid "Fixed amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:149 +#, c-format +msgid "Default amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:161 +#, c-format +msgid "Default summary" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:177 +#, c-format +msgid "Print" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:184 +#, c-format +msgid "Setup TOTP" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:65 +#, c-format +msgid "Templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:70 +#, c-format +msgid "add new templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:142 +#, c-format +msgid "load more templates before the first one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:146 +#, c-format +msgid "load newer templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:181 +#, c-format +msgid "delete selected templates from the database" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:188 +#, c-format +msgid "use template to create new order" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:195 +#, c-format +msgid "create qr code for the template" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:210 +#, c-format +msgid "load more templates after the last one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:214 +#, c-format +msgid "load older templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:231 +#, c-format +msgid "There is no templates yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:104 +#, c-format +msgid "template delete successfully" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:110 +#, c-format +msgid "could not delete the template" +msgstr "" + +#: src/paths/instance/templates/update/index.tsx:90 +#, c-format +msgid "could not update template" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:57 +#, c-format +msgid "should be one of '%1$s'" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:85 +#, c-format +msgid "Webhook ID to use" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:89 +#, c-format +msgid "Event" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:90 +#, c-format +msgid "The event of the webhook: why the webhook is used" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:94 +#, c-format +msgid "Method" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:95 +#, c-format +msgid "Method used by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:99 +#, c-format +msgid "URL" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:100 +#, c-format +msgid "URL of the webhook where the customer will be redirected" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:104 +#, c-format +msgid "Header" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:106 +#, c-format +msgid "Header template of the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:111 +#, c-format +msgid "Body" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:112 +#, c-format +msgid "Body template by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:61 +#, c-format +msgid "Webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:66 +#, c-format +msgid "add new webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:137 +#, c-format +msgid "load more webhooks before the first one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:141 +#, c-format +msgid "load newer webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:151 +#, c-format +msgid "Event type" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:176 +#, c-format +msgid "delete selected webhook from the database" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:198 +#, c-format +msgid "load more webhooks after the last one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:202 +#, c-format +msgid "load older webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:219 +#, c-format +msgid "There is no webhooks yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:94 +#, c-format +msgid "webhook delete successfully" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:100 +#, c-format +msgid "could not delete the webhook" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:63 +#, c-format +msgid "check the id, does not look valid" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:65 +#, c-format +msgid "should have 52 characters, current %1$s" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:72 +#, c-format +msgid "URL doesn't have the right format" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:98 +#, c-format +msgid "Credited bank account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:100 +#, c-format +msgid "Select one account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:101 +#, c-format +msgid "Bank account of the merchant where the payment was received" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:105 +#, c-format +msgid "Wire transfer ID" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:107 +#, c-format +msgid "" +"unique identifier of the wire transfer used by the exchange, must be 52 " +"characters long" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:112 +#, c-format +msgid "" +"Base URL of the exchange that made the transfer, should have been in the " +"wire transfer subject" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:117 +#, c-format +msgid "Amount credited" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:118 +#, c-format +msgid "Actual amount that was wired to the merchant's bank account" +msgstr "" + +#: src/paths/instance/transfers/create/index.tsx:58 +#, c-format +msgid "could not inform transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:61 +#, c-format +msgid "Transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:66 +#, c-format +msgid "add new transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:129 +#, c-format +msgid "load more transfers before the first one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:133 +#, c-format +msgid "load newer transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:143 +#, c-format +msgid "Credit" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:152 +#, c-format +msgid "Confirmed" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:155 +#, c-format +msgid "Verified" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:158 +#, c-format +msgid "Executed at" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "yes" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "no" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:181 +#, c-format +msgid "unknown" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:187 +#, c-format +msgid "delete selected transfer from the database" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:202 +#, c-format +msgid "load more transfer after the last one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:206 +#, c-format +msgid "load older transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:223 +#, c-format +msgid "There is no transfer yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:79 +#, c-format +msgid "filter by account address" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:100 +#, c-format +msgid "only show wire transfers confirmed by the merchant" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:110 +#, c-format +msgid "only show wire transfers claimed by the exchange" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:113 +#, c-format +msgid "Unverified" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:69 +#, c-format +msgid "is not valid" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:94 +#, c-format +msgid "is not a number" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:96 +#, c-format +msgid "must be 1 or greater" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:107 +#, c-format +msgid "max 7 lines" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:178 +#, c-format +msgid "change authorization configuration" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:217 +#, c-format +msgid "Need to complete marked fields and choose authorization method" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:82 +#, c-format +msgid "This is not a valid bitcoin address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:95 +#, c-format +msgid "This is not a valid Ethereum address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:118 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:120 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:128 +#, c-format +msgid "IBAN country code not found" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:153 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:248 +#, c-format +msgid "Target type" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:249 +#, c-format +msgid "Method to use for wire transfer" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:258 +#, c-format +msgid "Routing" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:259 +#, c-format +msgid "Routing number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:263 +#, c-format +msgid "Account" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:264 +#, c-format +msgid "Account number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:273 +#, c-format +msgid "Business Identifier Code." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:282 +#, c-format +msgid "Bank Account Number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:292 +#, c-format +msgid "Unified Payment Interface." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:301 +#, c-format +msgid "Bitcoin protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:310 +#, c-format +msgid "Ethereum protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:319 +#, c-format +msgid "Interledger protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:328 +#, c-format +msgid "Host" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:329 +#, c-format +msgid "Bank host." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:334 +#, c-format +msgid "Bank account." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:343 +#, c-format +msgid "Bank account owner's name." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:370 +#, c-format +msgid "No accounts yet." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:52 +#, c-format +msgid "" +"Name of the instance in URLs. The 'default' instance is special in that it " +"is used to administer other instances." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:58 +#, c-format +msgid "Business name" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:59 +#, c-format +msgid "Legal name of the business represented by this instance." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:64 +#, c-format +msgid "Email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:65 +#, c-format +msgid "Contact email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:70 +#, c-format +msgid "Website URL" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:71 +#, c-format +msgid "URL." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:76 +#, c-format +msgid "Logo" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:77 +#, c-format +msgid "Logo image." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:82 +#, c-format +msgid "Bank account" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:83 +#, c-format +msgid "URI specifying bank account for crediting revenue." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:88 +#, c-format +msgid "Default max deposit fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:89 +#, c-format +msgid "" +"Maximum deposit fees this merchant is willing to pay per order by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:94 +#, c-format +msgid "Default max wire fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:95 +#, c-format +msgid "" +"Maximum wire fees this merchant is willing to pay per wire transfer by " +"default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:100 +#, c-format +msgid "Default wire fee amortization" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:101 +#, c-format +msgid "" +"Number of orders excess wire transfer fees will be divided by to compute per " +"order surcharge." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:107 +#, c-format +msgid "Physical location of the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:114 +#, c-format +msgid "Jurisdiction" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:115 +#, c-format +msgid "Jurisdiction for legal disputes with the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:122 +#, c-format +msgid "Default payment delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:124 +#, c-format +msgid "" +"Time customers have to pay an order before the offer expires by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:129 +#, c-format +msgid "Default wire transfer delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:130 +#, c-format +msgid "" +"Maximum time an exchange is allowed to delay wiring funds to the merchant, " +"enabling it to aggregate smaller payments into larger wire transfers and " +"reducing wire fees." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:164 +#, c-format +msgid "Instance id" +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:173 +#, c-format +msgid "Change the authorization method use for this instance." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:182 +#, c-format +msgid "Manage access token" +msgstr "" + +#: src/paths/instance/update/index.tsx:112 +#, c-format +msgid "Failed to create instance" +msgstr "" + +#: src/components/exception/login.tsx:74 +#, c-format +msgid "Login required" +msgstr "" + +#: src/components/exception/login.tsx:80 +#, c-format +msgid "Please enter your access token." +msgstr "" + +#: src/components/exception/login.tsx:108 +#, c-format +msgid "Access Token" +msgstr "" + +#: src/InstanceRoutes.tsx:171 +#, c-format +msgid "The request to the backend take too long and was cancelled" +msgstr "" + +#: src/InstanceRoutes.tsx:172 +#, c-format +msgid "Diagnostic from %1$s is \"%2$s\"" +msgstr "" + +#: src/InstanceRoutes.tsx:178 +#, c-format +msgid "The backend reported a problem: HTTP status #%1$s" +msgstr "" + +#: src/InstanceRoutes.tsx:179 +#, c-format +msgid "Diagnostic from %1$s is '%2$s'" +msgstr "" + +#: src/InstanceRoutes.tsx:196 +#, c-format +msgid "Access denied" +msgstr "" + +#: src/InstanceRoutes.tsx:197 +#, c-format +msgid "The access token provided is invalid." +msgstr "" + +#: src/InstanceRoutes.tsx:212 +#, c-format +msgid "No 'default' instance configured yet." +msgstr "" + +#: src/InstanceRoutes.tsx:213 +#, c-format +msgid "Create a 'default' instance to begin using the merchant backoffice." +msgstr "" + +#: src/InstanceRoutes.tsx:630 +#, c-format +msgid "The access token provided is invalid" +msgstr "" + +#: src/InstanceRoutes.tsx:664 +#, c-format +msgid "Hide for today" +msgstr "" + +#: src/components/menu/SideBar.tsx:82 +#, c-format +msgid "Instance" +msgstr "" + +#: src/components/menu/SideBar.tsx:91 +#, c-format +msgid "Settings" +msgstr "" + +#: src/components/menu/SideBar.tsx:167 +#, c-format +msgid "Connection" +msgstr "" + +#: src/components/menu/SideBar.tsx:209 +#, c-format +msgid "New" +msgstr "" + +#: src/components/menu/SideBar.tsx:219 +#, c-format +msgid "List" +msgstr "" + +#: src/components/menu/SideBar.tsx:234 +#, c-format +msgid "Log out" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:71 +#, c-format +msgid "Check your token is valid" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:90 +#, c-format +msgid "Couldn't access the server." +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:91 +#, c-format +msgid "Could not infer instance id from url %1$s" +msgstr "" + +#: src/Application.tsx:104 +#, c-format +msgid "Server not found" +msgstr "" + +#: src/Application.tsx:118 +#, c-format +msgid "Server response with an error code" +msgstr "" + +#: src/Application.tsx:120 +#, c-format +msgid "Got message %1$s from %2$s" +msgstr "" + +#: src/Application.tsx:131 +#, c-format +msgid "Response from server is unreadable, http status: %1$s" +msgstr "" + +#: src/Application.tsx:144 +#, c-format +msgid "Unexpected Error" +msgstr "" + +#: src/components/form/InputArray.tsx:101 +#, c-format +msgid "The value %1$s is invalid for a payment url" +msgstr "" + +#: src/components/form/InputArray.tsx:110 +#, c-format +msgid "add element to the list" +msgstr "" + +#: src/components/form/InputArray.tsx:112 +#, c-format +msgid "add" +msgstr "" + +#: src/components/form/InputSecured.tsx:37 +#, c-format +msgid "Deleting" +msgstr "" + +#: src/components/form/InputSecured.tsx:41 +#, c-format +msgid "Changing" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87 +#, c-format +msgid "Order ID" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101 +#, c-format +msgid "Payment URL" +msgstr "" diff --git a/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot new file mode 100644 index 000000000..5ef56ca05 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot @@ -0,0 +1,2726 @@ +# This file is part of GNU Taler +# (C) 2021-2023 Taler Systems S.A. + +# GNU Taler is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. + +# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with +# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Taler Wallet\n" +"Report-Msgid-Bugs-To: taler@gnu.org\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: src/components/modal/index.tsx:71 +#, c-format +msgid "Cancel" +msgstr "" + +#: src/components/modal/index.tsx:79 +#, c-format +msgid "%1$s" +msgstr "" + +#: src/components/modal/index.tsx:84 +#, c-format +msgid "Close" +msgstr "" + +#: src/components/modal/index.tsx:124 +#, c-format +msgid "Continue" +msgstr "" + +#: src/components/modal/index.tsx:178 +#, c-format +msgid "Clear" +msgstr "" + +#: src/components/modal/index.tsx:190 +#, c-format +msgid "Confirm" +msgstr "" + +#: src/components/modal/index.tsx:296 +#, c-format +msgid "is not the same as the current access token" +msgstr "" + +#: src/components/modal/index.tsx:299 +#, c-format +msgid "cannot be empty" +msgstr "" + +#: src/components/modal/index.tsx:301 +#, c-format +msgid "cannot be the same as the old token" +msgstr "" + +#: src/components/modal/index.tsx:305 +#, c-format +msgid "is not the same" +msgstr "" + +#: src/components/modal/index.tsx:315 +#, c-format +msgid "You are updating the access token from instance with id %1$s" +msgstr "" + +#: src/components/modal/index.tsx:331 +#, c-format +msgid "Old access token" +msgstr "" + +#: src/components/modal/index.tsx:332 +#, c-format +msgid "access token currently in use" +msgstr "" + +#: src/components/modal/index.tsx:338 +#, c-format +msgid "New access token" +msgstr "" + +#: src/components/modal/index.tsx:339 +#, c-format +msgid "next access token to be used" +msgstr "" + +#: src/components/modal/index.tsx:344 +#, c-format +msgid "Repeat access token" +msgstr "" + +#: src/components/modal/index.tsx:345 +#, c-format +msgid "confirm the same access token" +msgstr "" + +#: src/components/modal/index.tsx:350 +#, c-format +msgid "Clearing the access token will mean public access to the instance" +msgstr "" + +#: src/components/modal/index.tsx:377 +#, c-format +msgid "cannot be the same as the old access token" +msgstr "" + +#: src/components/modal/index.tsx:394 +#, c-format +msgid "You are setting the access token for the new instance" +msgstr "" + +#: src/components/modal/index.tsx:420 +#, c-format +msgid "With external authorization method no check will be done by the merchant backend" +msgstr "" + +#: src/components/modal/index.tsx:436 +#, c-format +msgid "Set external authorization" +msgstr "" + +#: src/components/modal/index.tsx:448 +#, c-format +msgid "Set access token" +msgstr "" + +#: src/components/modal/index.tsx:470 +#, c-format +msgid "Operation in progress..." +msgstr "" + +#: src/components/modal/index.tsx:479 +#, c-format +msgid "The operation will be automatically canceled after %1$s seconds" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:80 +#, c-format +msgid "Instances" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:93 +#, c-format +msgid "Delete" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:99 +#, c-format +msgid "add new instance" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:178 +#, c-format +msgid "ID" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:181 +#, c-format +msgid "Name" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:220 +#, c-format +msgid "Edit" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:237 +#, c-format +msgid "Purge" +msgstr "" + +#: src/paths/admin/list/TableActive.tsx:261 +#, c-format +msgid "There is no instances yet, add more pressing the + sign" +msgstr "" + +#: src/paths/admin/list/View.tsx:68 +#, c-format +msgid "Only show active instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:71 +#, c-format +msgid "Active" +msgstr "" + +#: src/paths/admin/list/View.tsx:78 +#, c-format +msgid "Only show deleted instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:81 +#, c-format +msgid "Deleted" +msgstr "" + +#: src/paths/admin/list/View.tsx:88 +#, c-format +msgid "Show all instances" +msgstr "" + +#: src/paths/admin/list/View.tsx:91 +#, c-format +msgid "All" +msgstr "" + +#: src/paths/admin/list/index.tsx:101 +#, c-format +msgid "Instance \"%1$s\" (ID: %2$s) has been deleted" +msgstr "" + +#: src/paths/admin/list/index.tsx:106 +#, c-format +msgid "Failed to delete instance" +msgstr "" + +#: src/paths/admin/list/index.tsx:124 +#, c-format +msgid "Instance '%1$s' (ID: %2$s) has been disabled" +msgstr "" + +#: src/paths/admin/list/index.tsx:129 +#, c-format +msgid "Failed to purge instance" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:41 +#, c-format +msgid "Pending KYC verification" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:66 +#, c-format +msgid "Timed out" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:103 +#, c-format +msgid "Exchange" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:106 +#, c-format +msgid "Target account" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:109 +#, c-format +msgid "KYC URL" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:144 +#, c-format +msgid "Code" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:147 +#, c-format +msgid "Http Status" +msgstr "" + +#: src/paths/instance/kyc/list/ListPage.tsx:177 +#, c-format +msgid "No pending kyc verification!" +msgstr "" + +#: src/components/form/InputDate.tsx:123 +#, c-format +msgid "change value to unknown date" +msgstr "" + +#: src/components/form/InputDate.tsx:124 +#, c-format +msgid "change value to empty" +msgstr "" + +#: src/components/form/InputDate.tsx:131 +#, c-format +msgid "clear" +msgstr "" + +#: src/components/form/InputDate.tsx:136 +#, c-format +msgid "change value to never" +msgstr "" + +#: src/components/form/InputDate.tsx:141 +#, c-format +msgid "never" +msgstr "" + +#: src/components/form/InputLocation.tsx:29 +#, c-format +msgid "Country" +msgstr "" + +#: src/components/form/InputLocation.tsx:33 +#, c-format +msgid "Address" +msgstr "" + +#: src/components/form/InputLocation.tsx:39 +#, c-format +msgid "Building number" +msgstr "" + +#: src/components/form/InputLocation.tsx:41 +#, c-format +msgid "Building name" +msgstr "" + +#: src/components/form/InputLocation.tsx:42 +#, c-format +msgid "Street" +msgstr "" + +#: src/components/form/InputLocation.tsx:43 +#, c-format +msgid "Post code" +msgstr "" + +#: src/components/form/InputLocation.tsx:44 +#, c-format +msgid "Town location" +msgstr "" + +#: src/components/form/InputLocation.tsx:45 +#, c-format +msgid "Town" +msgstr "" + +#: src/components/form/InputLocation.tsx:46 +#, c-format +msgid "District" +msgstr "" + +#: src/components/form/InputLocation.tsx:49 +#, c-format +msgid "Country subdivision" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:66 +#, c-format +msgid "Product id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:69 +#, c-format +msgid "Description" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:94 +#, c-format +msgid "Product" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:95 +#, c-format +msgid "search products by it's description or id" +msgstr "" + +#: src/components/form/InputSearchProduct.tsx:151 +#, c-format +msgid "no products found with that description" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:56 +#, c-format +msgid "You must enter a valid product identifier." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:64 +#, c-format +msgid "Quantity must be greater than 0!" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:76 +#, c-format +msgid "" +"This quantity exceeds remaining stock. Currently, only %1$s units remain " +"unreserved in stock." +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:109 +#, c-format +msgid "Quantity" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:110 +#, c-format +msgid "how many products will be added" +msgstr "" + +#: src/components/product/InventoryProductForm.tsx:117 +#, c-format +msgid "Add from inventory" +msgstr "" + +#: src/components/form/InputImage.tsx:105 +#, c-format +msgid "Image should be smaller than 1 MB" +msgstr "" + +#: src/components/form/InputImage.tsx:110 +#, c-format +msgid "Add" +msgstr "" + +#: src/components/form/InputImage.tsx:115 +#, c-format +msgid "Remove" +msgstr "" + +#: src/components/form/InputTaxes.tsx:113 +#, c-format +msgid "No taxes configured for this product." +msgstr "" + +#: src/components/form/InputTaxes.tsx:119 +#, c-format +msgid "Amount" +msgstr "" + +#: src/components/form/InputTaxes.tsx:120 +#, c-format +msgid "" +"Taxes can be in currencies that differ from the main currency used by the " +"merchant." +msgstr "" + +#: src/components/form/InputTaxes.tsx:122 +#, c-format +msgid "Enter currency and value separated with a colon, e.g. "USD:2.3"." +msgstr "" + +#: src/components/form/InputTaxes.tsx:131 +#, c-format +msgid "Legal name of the tax, e.g. VAT or import duties." +msgstr "" + +#: src/components/form/InputTaxes.tsx:137 +#, c-format +msgid "add tax to the tax list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:72 +#, c-format +msgid "describe and add a product that is not in the inventory list" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:75 +#, c-format +msgid "Add custom product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:86 +#, c-format +msgid "Complete information of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:185 +#, c-format +msgid "Image" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:186 +#, c-format +msgid "photo of the product" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:192 +#, c-format +msgid "full product description" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:196 +#, c-format +msgid "Unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:197 +#, c-format +msgid "name of the product unit" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:201 +#, c-format +msgid "Price" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:202 +#, c-format +msgid "amount in the current currency" +msgstr "" + +#: src/components/product/NonInventoryProductForm.tsx:211 +#, c-format +msgid "Taxes" +msgstr "" + +#: src/components/product/ProductList.tsx:38 +#, c-format +msgid "image" +msgstr "" + +#: src/components/product/ProductList.tsx:41 +#, c-format +msgid "description" +msgstr "" + +#: src/components/product/ProductList.tsx:44 +#, c-format +msgid "quantity" +msgstr "" + +#: src/components/product/ProductList.tsx:47 +#, c-format +msgid "unit price" +msgstr "" + +#: src/components/product/ProductList.tsx:50 +#, c-format +msgid "total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:153 +#, c-format +msgid "required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:157 +#, c-format +msgid "not valid" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:159 +#, c-format +msgid "must be greater than 0" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:164 +#, c-format +msgid "not a valid json" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:170 +#, c-format +msgid "should be in the future" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:173 +#, c-format +msgid "refund deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:179 +#, c-format +msgid "wire transfer deadline cannot be before refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:190 +#, c-format +msgid "wire transfer deadline cannot be before pay deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:197 +#, c-format +msgid "should have a refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:202 +#, c-format +msgid "auto refund cannot be after refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:360 +#, c-format +msgid "Manage products in order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:369 +#, c-format +msgid "Manage list of products in the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:391 +#, c-format +msgid "Remove this product from the order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:415 +#, c-format +msgid "Total price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:417 +#, c-format +msgid "total product price added up" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:430 +#, c-format +msgid "Amount to be paid by the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:436 +#, c-format +msgid "Order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:437 +#, c-format +msgid "final order price" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:444 +#, c-format +msgid "Summary" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:445 +#, c-format +msgid "Title of the order to be shown to the customer" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:450 +#, c-format +msgid "Shipping and Fulfillment" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:455 +#, c-format +msgid "Delivery date" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:456 +#, c-format +msgid "Deadline for physical delivery assured by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:461 +#, c-format +msgid "Location" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:462 +#, c-format +msgid "address where the products will be delivered" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:469 +#, c-format +msgid "Fulfillment URL" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:470 +#, c-format +msgid "URL to which the user will be redirected after successful payment." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:476 +#, c-format +msgid "Taler payment options" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:477 +#, c-format +msgid "Override default Taler payment settings for this order" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:481 +#, c-format +msgid "Payment deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:482 +#, c-format +msgid "" +"Deadline for the customer to pay for the offer before it expires. Inventory " +"products will be reserved until this deadline." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:486 +#, c-format +msgid "Refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:487 +#, c-format +msgid "Time until which the order can be refunded by the merchant." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:491 +#, c-format +msgid "Wire transfer deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:492 +#, c-format +msgid "Deadline for the exchange to make the wire transfer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:496 +#, c-format +msgid "Auto-refund deadline" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:497 +#, c-format +msgid "" +"Time until which the wallet will automatically check for refunds without user " +"interaction." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:502 +#, c-format +msgid "Maximum deposit fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:503 +#, c-format +msgid "" +"Maximum deposit fees the merchant is willing to cover for this order. Higher " +"deposit fees must be covered in full by the consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:507 +#, c-format +msgid "Maximum wire fee" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:508 +#, c-format +msgid "" +"Maximum aggregate wire fees the merchant is willing to cover for this order. " +"Wire fees exceeding this amount are to be covered by the customers." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:512 +#, c-format +msgid "Wire fee amortization" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:513 +#, c-format +msgid "" +"Factor by which wire fees exceeding the above threshold are divided to determine " +"the share of excess wire fees to be paid explicitly by the consumer." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:517 +#, c-format +msgid "Create token" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:518 +#, c-format +msgid "" +"Uncheck this option if the merchant backend generated an order ID with enough " +"entropy to prevent adversarial claims." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:522 +#, c-format +msgid "Minimum age required" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:523 +#, c-format +msgid "" +"Any value greater than 0 will limit the coins able be used to pay this contract. " +"If empty the age restriction will be defined by the products" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:526 +#, c-format +msgid "Min age defined by the producs is %1$s" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:534 +#, c-format +msgid "Additional information" +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:535 +#, c-format +msgid "Custom information to be included in the contract for this order." +msgstr "" + +#: src/paths/instance/orders/create/CreatePage.tsx:541 +#, c-format +msgid "You must enter a value in JavaScript Object Notation (JSON)." +msgstr "" + +#: src/components/picker/DurationPicker.tsx:55 +#, c-format +msgid "days" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:65 +#, c-format +msgid "hours" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:76 +#, c-format +msgid "minutes" +msgstr "" + +#: src/components/picker/DurationPicker.tsx:87 +#, c-format +msgid "seconds" +msgstr "" + +#: src/components/form/InputDuration.tsx:53 +#, c-format +msgid "forever" +msgstr "" + +#: src/components/form/InputDuration.tsx:62 +#, c-format +msgid "%1$sM" +msgstr "" + +#: src/components/form/InputDuration.tsx:64 +#, c-format +msgid "%1$sY" +msgstr "" + +#: src/components/form/InputDuration.tsx:66 +#, c-format +msgid "%1$sd" +msgstr "" + +#: src/components/form/InputDuration.tsx:68 +#, c-format +msgid "%1$sh" +msgstr "" + +#: src/components/form/InputDuration.tsx:70 +#, c-format +msgid "%1$smin" +msgstr "" + +#: src/components/form/InputDuration.tsx:72 +#, c-format +msgid "%1$ssec" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:75 +#, c-format +msgid "Orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:81 +#, c-format +msgid "create order" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:147 +#, c-format +msgid "load newer orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:154 +#, c-format +msgid "Date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:200 +#, c-format +msgid "Refund" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:209 +#, c-format +msgid "copy url" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:225 +#, c-format +msgid "load older orders" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:242 +#, c-format +msgid "No orders have been found matching your query!" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:288 +#, c-format +msgid "duplicated" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:299 +#, c-format +msgid "invalid format" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:301 +#, c-format +msgid "this value exceed the refundable amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:346 +#, c-format +msgid "date" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:349 +#, c-format +msgid "amount" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:352 +#, c-format +msgid "reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:389 +#, c-format +msgid "amount to be refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:391 +#, c-format +msgid "Max refundable:" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:396 +#, c-format +msgid "Reason" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:397 +#, c-format +msgid "Choose one..." +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:399 +#, c-format +msgid "requested by the customer" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:400 +#, c-format +msgid "other" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:403 +#, c-format +msgid "why this order is being refunded" +msgstr "" + +#: src/paths/instance/orders/list/Table.tsx:409 +#, c-format +msgid "more information to give context" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:62 +#, c-format +msgid "Contract Terms" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:68 +#, c-format +msgid "human-readable description of the whole purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:74 +#, c-format +msgid "total price for the transaction" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:81 +#, c-format +msgid "URL for this purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:87 +#, c-format +msgid "Max fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:88 +#, c-format +msgid "maximum total deposit fee accepted by the merchant for this contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:93 +#, c-format +msgid "Max wire fee" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:94 +#, c-format +msgid "maximum wire fee accepted by the merchant" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:100 +#, c-format +msgid "" +"over how many customer transactions does the merchant expect to amortize wire " +"fees on average" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:105 +#, c-format +msgid "Created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:106 +#, c-format +msgid "time when this contract was generated" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:112 +#, c-format +msgid "after this deadline has passed no refunds will be accepted" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:118 +#, c-format +msgid "after this deadline, the merchant won't accept payments for the contract" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:124 +#, c-format +msgid "transfer deadline for the exchange" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:130 +#, c-format +msgid "time indicating when the order should be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:136 +#, c-format +msgid "where the order will be delivered" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:144 +#, c-format +msgid "Auto-refund delay" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:145 +#, c-format +msgid "how long the wallet should try to get an automatic refund for the purchase" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:150 +#, c-format +msgid "Extra info" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:151 +#, c-format +msgid "extra data that is only interpreted by the merchant frontend" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:219 +#, c-format +msgid "Order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:221 +#, c-format +msgid "claimed" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:247 +#, c-format +msgid "claimed at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:265 +#, c-format +msgid "Timeline" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:271 +#, c-format +msgid "Payment details" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:291 +#, c-format +msgid "Order status" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:301 +#, c-format +msgid "Product list" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:451 +#, c-format +msgid "paid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:455 +#, c-format +msgid "wired" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:460 +#, c-format +msgid "refunded" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:480 +#, c-format +msgid "refund order" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:481 +#, c-format +msgid "not refundable" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:489 +#, c-format +msgid "refund" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:553 +#, c-format +msgid "Refunded amount" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:560 +#, c-format +msgid "Refund taken" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:570 +#, c-format +msgid "Status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:583 +#, c-format +msgid "Refund URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:636 +#, c-format +msgid "unpaid" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:654 +#, c-format +msgid "pay at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:666 +#, c-format +msgid "created at" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:707 +#, c-format +msgid "Order status URL" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:711 +#, c-format +msgid "Payment URI" +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:740 +#, c-format +msgid "Unknown order status. This is an error, please contact the administrator." +msgstr "" + +#: src/paths/instance/orders/details/DetailPage.tsx:767 +#, c-format +msgid "Back" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:79 +#, c-format +msgid "refund created successfully" +msgstr "" + +#: src/paths/instance/orders/details/index.tsx:85 +#, c-format +msgid "could not create the refund" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:78 +#, c-format +msgid "select date to show nearby orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:94 +#, c-format +msgid "order id" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:100 +#, c-format +msgid "jump to order with the given order ID" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:122 +#, c-format +msgid "remove all filters" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:132 +#, c-format +msgid "only show paid orders" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:135 +#, c-format +msgid "Paid" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:142 +#, c-format +msgid "only show orders with refunds" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:145 +#, c-format +msgid "Refunded" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:152 +#, c-format +msgid "" +"only show orders where customers paid, but wire payments from payment provider " +"are still pending" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:155 +#, c-format +msgid "Not wired" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:170 +#, c-format +msgid "clear date filter" +msgstr "" + +#: src/paths/instance/orders/list/ListPage.tsx:184 +#, c-format +msgid "date (YYYY/MM/DD)" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:103 +#, c-format +msgid "Enter an order id" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:111 +#, c-format +msgid "order not found" +msgstr "" + +#: src/paths/instance/orders/list/index.tsx:178 +#, c-format +msgid "could not get the order to refund" +msgstr "" + +#: src/components/exception/AsyncButton.tsx:43 +#, c-format +msgid "Loading..." +msgstr "" + +#: src/components/form/InputStock.tsx:99 +#, c-format +msgid "" +"click here to configure the stock of the product, leave it as is and the backend " +"will not control stock" +msgstr "" + +#: src/components/form/InputStock.tsx:109 +#, c-format +msgid "Manage stock" +msgstr "" + +#: src/components/form/InputStock.tsx:115 +#, c-format +msgid "this product has been configured without stock control" +msgstr "" + +#: src/components/form/InputStock.tsx:119 +#, c-format +msgid "Infinite" +msgstr "" + +#: src/components/form/InputStock.tsx:136 +#, c-format +msgid "lost cannot be greater than current and incoming (max %1$s)" +msgstr "" + +#: src/components/form/InputStock.tsx:176 +#, c-format +msgid "Incoming" +msgstr "" + +#: src/components/form/InputStock.tsx:177 +#, c-format +msgid "Lost" +msgstr "" + +#: src/components/form/InputStock.tsx:192 +#, c-format +msgid "Current" +msgstr "" + +#: src/components/form/InputStock.tsx:196 +#, c-format +msgid "remove stock control for this product" +msgstr "" + +#: src/components/form/InputStock.tsx:202 +#, c-format +msgid "without stock" +msgstr "" + +#: src/components/form/InputStock.tsx:211 +#, c-format +msgid "Next restock" +msgstr "" + +#: src/components/form/InputStock.tsx:217 +#, c-format +msgid "Delivery address" +msgstr "" + +#: src/components/product/ProductForm.tsx:133 +#, c-format +msgid "product identification to use in URLs (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:139 +#, c-format +msgid "illustration of the product for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:145 +#, c-format +msgid "product description for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:149 +#, c-format +msgid "Age restricted" +msgstr "" + +#: src/components/product/ProductForm.tsx:150 +#, c-format +msgid "is this product restricted for customer below certain age?" +msgstr "" + +#: src/components/product/ProductForm.tsx:155 +#, c-format +msgid "" +"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 " +"meters) for customers" +msgstr "" + +#: src/components/product/ProductForm.tsx:160 +#, c-format +msgid "sale price for customers, including taxes, for above units of the product" +msgstr "" + +#: src/components/product/ProductForm.tsx:164 +#, c-format +msgid "Stock" +msgstr "" + +#: src/components/product/ProductForm.tsx:166 +#, c-format +msgid "product inventory for products with finite supply (for internal use only)" +msgstr "" + +#: src/components/product/ProductForm.tsx:171 +#, c-format +msgid "taxes included in the product price, exposed to customers" +msgstr "" + +#: src/paths/instance/products/create/CreatePage.tsx:66 +#, c-format +msgid "Need to complete marked fields" +msgstr "" + +#: src/paths/instance/products/create/index.tsx:51 +#, c-format +msgid "could not create product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:68 +#, c-format +msgid "Products" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:73 +#, c-format +msgid "add product to inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:137 +#, c-format +msgid "Sell" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:143 +#, c-format +msgid "Profit" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:149 +#, c-format +msgid "Sold" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:210 +#, c-format +msgid "free" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:248 +#, c-format +msgid "go to product update page" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:255 +#, c-format +msgid "Update" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:260 +#, c-format +msgid "remove this product from the database" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:331 +#, c-format +msgid "update the product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:341 +#, c-format +msgid "update product with new price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:399 +#, c-format +msgid "add more elements to the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:404 +#, c-format +msgid "report elements lost in the inventory" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:409 +#, c-format +msgid "new price for the product" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:421 +#, c-format +msgid "the are value with errors" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:422 +#, c-format +msgid "update product with new stock and price" +msgstr "" + +#: src/paths/instance/products/list/Table.tsx:463 +#, c-format +msgid "There is no products yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:86 +#, c-format +msgid "product updated successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:92 +#, c-format +msgid "could not update the product" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:103 +#, c-format +msgid "product delete successfully" +msgstr "" + +#: src/paths/instance/products/list/index.tsx:109 +#, c-format +msgid "could not delete the product" +msgstr "" + +#: src/paths/instance/products/update/UpdatePage.tsx:56 +#, c-format +msgid "Product id:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95 +#, c-format +msgid "" +"To complete the setup of the reserve, you must now initiate a wire transfer " +"using the given wire transfer subject and crediting the specified amount to the " +"indicated account of the exchange." +msgstr "" + +#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102 +#, c-format +msgid "If your system supports RFC 8905, you can do this by opening this URI:" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:83 +#, c-format +msgid "it should be greater than 0" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:88 +#, c-format +msgid "must be a valid URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:107 +#, c-format +msgid "Initial balance" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:108 +#, c-format +msgid "balance prior to deposit" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:112 +#, c-format +msgid "Exchange URL" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:113 +#, c-format +msgid "URL of exchange" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:148 +#, c-format +msgid "Next" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:186 +#, c-format +msgid "Wire method" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:187 +#, c-format +msgid "method to use for wire transfer" +msgstr "" + +#: src/paths/instance/reserves/create/CreatePage.tsx:189 +#, c-format +msgid "Select one wire method" +msgstr "" + +#: src/paths/instance/reserves/create/index.tsx:62 +#, c-format +msgid "could not create reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:77 +#, c-format +msgid "Valid until" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:82 +#, c-format +msgid "Created balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:99 +#, c-format +msgid "Exchange balance" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:104 +#, c-format +msgid "Picked up" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:109 +#, c-format +msgid "Committed" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:116 +#, c-format +msgid "Account address" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:119 +#, c-format +msgid "Subject" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:130 +#, c-format +msgid "Tips" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:193 +#, c-format +msgid "No tips has been authorized from this reserve" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:213 +#, c-format +msgid "Authorized" +msgstr "" + +#: src/paths/instance/reserves/details/DetailPage.tsx:222 +#, c-format +msgid "Expiration" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108 +#, c-format +msgid "amount of tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112 +#, c-format +msgid "Justification" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114 +#, c-format +msgid "reason for the tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118 +#, c-format +msgid "URL after tip" +msgstr "" + +#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119 +#, c-format +msgid "URL to visit after tip payment" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:65 +#, c-format +msgid "Reserves not yet funded" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:89 +#, c-format +msgid "Reserves ready" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:95 +#, c-format +msgid "add new reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:143 +#, c-format +msgid "Expires at" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:146 +#, c-format +msgid "Initial" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:202 +#, c-format +msgid "delete selected reserve from the database" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:210 +#, c-format +msgid "authorize new tip from selected reserve" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:237 +#, c-format +msgid "There is no ready reserves yet, add more pressing the + sign or fund them" +msgstr "" + +#: src/paths/instance/reserves/list/Table.tsx:264 +#, c-format +msgid "Expected Balance" +msgstr "" + +#: src/paths/instance/reserves/list/index.tsx:110 +#, c-format +msgid "could not create the tip" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:77 +#, c-format +msgid "should not be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:93 +#, c-format +msgid "should be greater that 0" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:96 +#, c-format +msgid "can't be empty" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:100 +#, c-format +msgid "to short" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:108 +#, c-format +msgid "just letters and numbers from 2 to 7" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:110 +#, c-format +msgid "size of the key should be 32" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:137 +#, c-format +msgid "Identifier" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:138 +#, c-format +msgid "Name of the template in URLs." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:144 +#, c-format +msgid "Describe what this template stands for" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:149 +#, c-format +msgid "Fixed summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:150 +#, c-format +msgid "If specified, this template will create order with the same summary" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:154 +#, c-format +msgid "Fixed price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:155 +#, c-format +msgid "If specified, this template will create order with the same price" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:159 +#, c-format +msgid "Minimum age" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:161 +#, c-format +msgid "Is this contract restricted to some age?" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:165 +#, c-format +msgid "Payment timeout" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:167 +#, c-format +msgid "" +"How much time has the customer to complete the payment once the order was " +"created." +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:171 +#, c-format +msgid "Verification algorithm" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:172 +#, c-format +msgid "Algorithm to use to verify transaction in offline mode" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:180 +#, c-format +msgid "Point-of-sale key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:182 +#, c-format +msgid "Useful to validate the purchase" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:196 +#, c-format +msgid "generate random secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:203 +#, c-format +msgid "random" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:208 +#, c-format +msgid "show secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:209 +#, c-format +msgid "hide secret key" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:216 +#, c-format +msgid "hide" +msgstr "" + +#: src/paths/instance/templates/create/CreatePage.tsx:218 +#, c-format +msgid "show" +msgstr "" + +#: src/paths/instance/templates/create/index.tsx:52 +#, c-format +msgid "could not inform template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:54 +#, c-format +msgid "Amount is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:58 +#, c-format +msgid "Order summary is required" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:86 +#, c-format +msgid "New order for template" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:108 +#, c-format +msgid "Amount of the order" +msgstr "" + +#: src/paths/instance/templates/use/UsePage.tsx:113 +#, c-format +msgid "Order summary" +msgstr "" + +#: src/paths/instance/templates/use/index.tsx:92 +#, c-format +msgid "could not create order from template" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:131 +#, c-format +msgid "" +"Here you can specify a default value for fields that are not fixed. Default " +"values can be edited by the customer before the payment." +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:148 +#, c-format +msgid "Fixed amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:149 +#, c-format +msgid "Default amount" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:161 +#, c-format +msgid "Default summary" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:177 +#, c-format +msgid "Print" +msgstr "" + +#: src/paths/instance/templates/qr/QrPage.tsx:184 +#, c-format +msgid "Setup TOTP" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:65 +#, c-format +msgid "Templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:70 +#, c-format +msgid "add new templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:142 +#, c-format +msgid "load more templates before the first one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:146 +#, c-format +msgid "load newer templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:181 +#, c-format +msgid "delete selected templates from the database" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:188 +#, c-format +msgid "use template to create new order" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:195 +#, c-format +msgid "create qr code for the template" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:210 +#, c-format +msgid "load more templates after the last one" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:214 +#, c-format +msgid "load older templates" +msgstr "" + +#: src/paths/instance/templates/list/Table.tsx:231 +#, c-format +msgid "There is no templates yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:104 +#, c-format +msgid "template delete successfully" +msgstr "" + +#: src/paths/instance/templates/list/index.tsx:110 +#, c-format +msgid "could not delete the template" +msgstr "" + +#: src/paths/instance/templates/update/index.tsx:90 +#, c-format +msgid "could not update template" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:57 +#, c-format +msgid "should be one of '%1$s'" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:85 +#, c-format +msgid "Webhook ID to use" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:89 +#, c-format +msgid "Event" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:90 +#, c-format +msgid "The event of the webhook: why the webhook is used" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:94 +#, c-format +msgid "Method" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:95 +#, c-format +msgid "Method used by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:99 +#, c-format +msgid "URL" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:100 +#, c-format +msgid "URL of the webhook where the customer will be redirected" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:104 +#, c-format +msgid "Header" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:106 +#, c-format +msgid "Header template of the webhook" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:111 +#, c-format +msgid "Body" +msgstr "" + +#: src/paths/instance/webhooks/create/CreatePage.tsx:112 +#, c-format +msgid "Body template by the webhook" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:61 +#, c-format +msgid "Webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:66 +#, c-format +msgid "add new webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:137 +#, c-format +msgid "load more webhooks before the first one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:141 +#, c-format +msgid "load newer webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:151 +#, c-format +msgid "Event type" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:176 +#, c-format +msgid "delete selected webhook from the database" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:198 +#, c-format +msgid "load more webhooks after the last one" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:202 +#, c-format +msgid "load older webhooks" +msgstr "" + +#: src/paths/instance/webhooks/list/Table.tsx:219 +#, c-format +msgid "There is no webhooks yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:94 +#, c-format +msgid "webhook delete successfully" +msgstr "" + +#: src/paths/instance/webhooks/list/index.tsx:100 +#, c-format +msgid "could not delete the webhook" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:63 +#, c-format +msgid "check the id, does not look valid" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:65 +#, c-format +msgid "should have 52 characters, current %1$s" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:72 +#, c-format +msgid "URL doesn't have the right format" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:98 +#, c-format +msgid "Credited bank account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:100 +#, c-format +msgid "Select one account" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:101 +#, c-format +msgid "Bank account of the merchant where the payment was received" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:105 +#, c-format +msgid "Wire transfer ID" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:107 +#, c-format +msgid "" +"unique identifier of the wire transfer used by the exchange, must be 52 " +"characters long" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:112 +#, c-format +msgid "" +"Base URL of the exchange that made the transfer, should have been in the wire " +"transfer subject" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:117 +#, c-format +msgid "Amount credited" +msgstr "" + +#: src/paths/instance/transfers/create/CreatePage.tsx:118 +#, c-format +msgid "Actual amount that was wired to the merchant's bank account" +msgstr "" + +#: src/paths/instance/transfers/create/index.tsx:58 +#, c-format +msgid "could not inform transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:61 +#, c-format +msgid "Transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:66 +#, c-format +msgid "add new transfer" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:129 +#, c-format +msgid "load more transfers before the first one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:133 +#, c-format +msgid "load newer transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:143 +#, c-format +msgid "Credit" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:152 +#, c-format +msgid "Confirmed" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:155 +#, c-format +msgid "Verified" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:158 +#, c-format +msgid "Executed at" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "yes" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:171 +#, c-format +msgid "no" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:181 +#, c-format +msgid "unknown" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:187 +#, c-format +msgid "delete selected transfer from the database" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:202 +#, c-format +msgid "load more transfer after the last one" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:206 +#, c-format +msgid "load older transfers" +msgstr "" + +#: src/paths/instance/transfers/list/Table.tsx:223 +#, c-format +msgid "There is no transfer yet, add more pressing the + sign" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:79 +#, c-format +msgid "filter by account address" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:100 +#, c-format +msgid "only show wire transfers confirmed by the merchant" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:110 +#, c-format +msgid "only show wire transfers claimed by the exchange" +msgstr "" + +#: src/paths/instance/transfers/list/ListPage.tsx:113 +#, c-format +msgid "Unverified" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:69 +#, c-format +msgid "is not valid" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:94 +#, c-format +msgid "is not a number" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:96 +#, c-format +msgid "must be 1 or greater" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:107 +#, c-format +msgid "max 7 lines" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:178 +#, c-format +msgid "change authorization configuration" +msgstr "" + +#: src/paths/admin/create/CreatePage.tsx:217 +#, c-format +msgid "Need to complete marked fields and choose authorization method" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:82 +#, c-format +msgid "This is not a valid bitcoin address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:95 +#, c-format +msgid "This is not a valid Ethereum address." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:118 +#, c-format +msgid "IBAN numbers usually have more that 4 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:120 +#, c-format +msgid "IBAN numbers usually have less that 34 digits" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:128 +#, c-format +msgid "IBAN country code not found" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:153 +#, c-format +msgid "IBAN number is not valid, checksum is wrong" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:248 +#, c-format +msgid "Target type" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:249 +#, c-format +msgid "Method to use for wire transfer" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:258 +#, c-format +msgid "Routing" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:259 +#, c-format +msgid "Routing number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:263 +#, c-format +msgid "Account" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:264 +#, c-format +msgid "Account number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:273 +#, c-format +msgid "Business Identifier Code." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:282 +#, c-format +msgid "Bank Account Number." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:292 +#, c-format +msgid "Unified Payment Interface." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:301 +#, c-format +msgid "Bitcoin protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:310 +#, c-format +msgid "Ethereum protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:319 +#, c-format +msgid "Interledger protocol." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:328 +#, c-format +msgid "Host" +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:329 +#, c-format +msgid "Bank host." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:334 +#, c-format +msgid "Bank account." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:343 +#, c-format +msgid "Bank account owner's name." +msgstr "" + +#: src/components/form/InputPaytoForm.tsx:370 +#, c-format +msgid "No accounts yet." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:52 +#, c-format +msgid "" +"Name of the instance in URLs. The 'default' instance is special in that it is " +"used to administer other instances." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:58 +#, c-format +msgid "Business name" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:59 +#, c-format +msgid "Legal name of the business represented by this instance." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:64 +#, c-format +msgid "Email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:65 +#, c-format +msgid "Contact email" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:70 +#, c-format +msgid "Website URL" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:71 +#, c-format +msgid "URL." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:76 +#, c-format +msgid "Logo" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:77 +#, c-format +msgid "Logo image." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:82 +#, c-format +msgid "Bank account" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:83 +#, c-format +msgid "URI specifying bank account for crediting revenue." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:88 +#, c-format +msgid "Default max deposit fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:89 +#, c-format +msgid "Maximum deposit fees this merchant is willing to pay per order by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:94 +#, c-format +msgid "Default max wire fee" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:95 +#, c-format +msgid "Maximum wire fees this merchant is willing to pay per wire transfer by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:100 +#, c-format +msgid "Default wire fee amortization" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:101 +#, c-format +msgid "" +"Number of orders excess wire transfer fees will be divided by to compute per " +"order surcharge." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:107 +#, c-format +msgid "Physical location of the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:114 +#, c-format +msgid "Jurisdiction" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:115 +#, c-format +msgid "Jurisdiction for legal disputes with the merchant." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:122 +#, c-format +msgid "Default payment delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:124 +#, c-format +msgid "Time customers have to pay an order before the offer expires by default." +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:129 +#, c-format +msgid "Default wire transfer delay" +msgstr "" + +#: src/components/instance/DefaultInstanceFormFields.tsx:130 +#, c-format +msgid "" +"Maximum time an exchange is allowed to delay wiring funds to the merchant, " +"enabling it to aggregate smaller payments into larger wire transfers and " +"reducing wire fees." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:164 +#, c-format +msgid "Instance id" +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:173 +#, c-format +msgid "Change the authorization method use for this instance." +msgstr "" + +#: src/paths/instance/update/UpdatePage.tsx:182 +#, c-format +msgid "Manage access token" +msgstr "" + +#: src/paths/instance/update/index.tsx:112 +#, c-format +msgid "Failed to create instance" +msgstr "" + +#: src/components/exception/login.tsx:74 +#, c-format +msgid "Login required" +msgstr "" + +#: src/components/exception/login.tsx:80 +#, c-format +msgid "Please enter your access token." +msgstr "" + +#: src/components/exception/login.tsx:108 +#, c-format +msgid "Access Token" +msgstr "" + +#: src/InstanceRoutes.tsx:171 +#, c-format +msgid "The request to the backend take too long and was cancelled" +msgstr "" + +#: src/InstanceRoutes.tsx:172 +#, c-format +msgid "Diagnostic from %1$s is \"%2$s\"" +msgstr "" + +#: src/InstanceRoutes.tsx:178 +#, c-format +msgid "The backend reported a problem: HTTP status #%1$s" +msgstr "" + +#: src/InstanceRoutes.tsx:179 +#, c-format +msgid "Diagnostic from %1$s is '%2$s'" +msgstr "" + +#: src/InstanceRoutes.tsx:196 +#, c-format +msgid "Access denied" +msgstr "" + +#: src/InstanceRoutes.tsx:197 +#, c-format +msgid "The access token provided is invalid." +msgstr "" + +#: src/InstanceRoutes.tsx:212 +#, c-format +msgid "No 'default' instance configured yet." +msgstr "" + +#: src/InstanceRoutes.tsx:213 +#, c-format +msgid "Create a 'default' instance to begin using the merchant backoffice." +msgstr "" + +#: src/InstanceRoutes.tsx:630 +#, c-format +msgid "The access token provided is invalid" +msgstr "" + +#: src/InstanceRoutes.tsx:664 +#, c-format +msgid "Hide for today" +msgstr "" + +#: src/components/menu/SideBar.tsx:82 +#, c-format +msgid "Instance" +msgstr "" + +#: src/components/menu/SideBar.tsx:91 +#, c-format +msgid "Settings" +msgstr "" + +#: src/components/menu/SideBar.tsx:167 +#, c-format +msgid "Connection" +msgstr "" + +#: src/components/menu/SideBar.tsx:209 +#, c-format +msgid "New" +msgstr "" + +#: src/components/menu/SideBar.tsx:219 +#, c-format +msgid "List" +msgstr "" + +#: src/components/menu/SideBar.tsx:234 +#, c-format +msgid "Log out" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:71 +#, c-format +msgid "Check your token is valid" +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:90 +#, c-format +msgid "Couldn't access the server." +msgstr "" + +#: src/ApplicationReadyRoutes.tsx:91 +#, c-format +msgid "Could not infer instance id from url %1$s" +msgstr "" + +#: src/Application.tsx:104 +#, c-format +msgid "Server not found" +msgstr "" + +#: src/Application.tsx:118 +#, c-format +msgid "Server response with an error code" +msgstr "" + +#: src/Application.tsx:120 +#, c-format +msgid "Got message %1$s from %2$s" +msgstr "" + +#: src/Application.tsx:131 +#, c-format +msgid "Response from server is unreadable, http status: %1$s" +msgstr "" + +#: src/Application.tsx:144 +#, c-format +msgid "Unexpected Error" +msgstr "" + +#: src/components/form/InputArray.tsx:101 +#, c-format +msgid "The value %1$s is invalid for a payment url" +msgstr "" + +#: src/components/form/InputArray.tsx:110 +#, c-format +msgid "add element to the list" +msgstr "" + +#: src/components/form/InputArray.tsx:112 +#, c-format +msgid "add" +msgstr "" + +#: src/components/form/InputSecured.tsx:37 +#, c-format +msgid "Deleting" +msgstr "" + +#: src/components/form/InputSecured.tsx:41 +#, c-format +msgid "Changing" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87 +#, c-format +msgid "Order ID" +msgstr "" + +#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101 +#, c-format +msgid "Payment URL" +msgstr "" + diff --git a/packages/auditor-backoffice-ui/src/index.html b/packages/auditor-backoffice-ui/src/index.html new file mode 100644 index 000000000..d79bdf130 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/index.html @@ -0,0 +1,45 @@ +<!-- + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + + @author Sebastian Javier Marchano +--> +<!DOCTYPE html> +<html + lang="en" + class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded" +> + <head> + <meta charset="utf-8" /> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <meta name="mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-capable" content="yes" /> + + <link + rel="icon" + href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + /> + <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" /> + <title>Auditor Backoffice</title> + <!-- Optional customization script. --> + <script src="auditor-backoffice-ui-settings.js"></script> + <!-- Entry point for the demobank SPA. --> + <script type="module" src="index.js"></script> + <link rel="stylesheet" href="index.css" /> + </head> + <body> + <div id="app"></div> + </body> +</html> diff --git a/packages/auditor-backoffice-ui/src/index.tsx b/packages/auditor-backoffice-ui/src/index.tsx new file mode 100644 index 000000000..7fdf7c1c3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/index.tsx @@ -0,0 +1,24 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { Application } from "./Application.js"; + +import { h, render } from "preact"; +import "./scss/main.scss"; + +const app = document.getElementById("app"); + +render(<Application />, app as any); diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx new file mode 100644 index 000000000..91b6b4b56 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx @@ -0,0 +1,57 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ConfigContextProvider } from "../../../context/config.js"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Instance/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => ( + <ConfigContextProvider + value={{ + currency: "ARS", + version: "1", + }} + > + <Component {...args} /> + </ConfigContextProvider> + ); + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, {}); +// export const Example = (a: any): VNode => <CreatePage {...a} />; +// Example.args = { +// isLoading: false +// } diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx new file mode 100644 index 000000000..d13b7e929 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -0,0 +1,257 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../components/form/FormProvider.js"; +import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { INSTANCE_ID_REGEX } from "../../../utils/constants.js"; +import { undefinedIfEmpty } from "../../../utils/table.js"; +import { SetTokenNewInstanceModal } from "../../../components/modal/index.js"; +import { Duration } from "@gnu-taler/taler-util"; + +export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceConfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { + auth_token?: string; + default_pay_delay: Duration, + default_wire_transfer_delay: Duration, +}; + +interface Props { + onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>; + onBack?: () => void; + forceId?: string; +} + +function with_defaults(id?: string): Partial<Entity> { + return { + id, + // accounts: [], + user_type: "business", + use_stefan: true, + default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours + default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days + }; +} + +export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { + const [value, valueHandler] = useState(with_defaults(forceId)); + const [isTokenSet, updateIsTokenSet] = useState<boolean>(false); + const [isTokenDialogActive, updateIsTokenDialogActive] = + useState<boolean>(false); + + const { i18n } = useTranslationContext(); + + const errors: FormErrors<Entity> = { + id: !value.id + ? i18n.str`required` + : !INSTANCE_ID_REGEX.test(value.id) + ? i18n.str`is not valid` + : undefined, + name: !value.name ? i18n.str`required` : undefined, + + user_type: !value.user_type + ? i18n.str`required` + : value.user_type !== "business" && value.user_type !== "individual" + ? i18n.str`should be business or individual` + : undefined, + // accounts: + // !value.accounts || !value.accounts.length + // ? i18n.str`required` + // : undefinedIfEmpty( + // value.accounts.map((p) => { + // return !PAYTO_REGEX.test(p.payto_uri) + // ? i18n.str`is not valid` + // : undefined; + // }), + // ), + default_pay_delay: !value.default_pay_delay + ? i18n.str`required` + : !!value.default_wire_transfer_delay && + value.default_wire_transfer_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? + i18n.str`pay delay can't be greater than wire transfer delay` : undefined, + default_wire_transfer_delay: !value.default_wire_transfer_delay + ? i18n.str`required` + : undefined, + address: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n.str`max 7 lines` + : undefined, + }), + jurisdiction: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n.str`max 7 lines` + : undefined, + }), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submit = (): Promise<void> => { + // use conversion instead of this + const newToken = value.auth_token; + value.auth_token = undefined; + value.auth = newToken === null || newToken === undefined + ? { method: "external" } + : { method: "token", token: `secret-token:${newToken}` }; + if (!value.address) value.address = {}; + if (!value.jurisdiction) value.jurisdiction = {}; + // remove above use conversion + // schema.validateSync(value, { abortEarly: false }) + value.default_pay_delay = Duration.toTalerProtocolDuration(value.default_pay_delay!) as any + value.default_wire_transfer_delay = Duration.toTalerProtocolDuration(value.default_wire_transfer_delay!) as any + // delete value.default_pay_delay; + // delete value.default_wire_transfer_delay; + + return onCreate(value as any as MerchantBackend.Instances.InstanceConfigurationMessage); + }; + + function updateToken(token: string | null) { + valueHandler((old) => ({ + ...old, + auth_token: token === null ? undefined : token, + })); + } + + return ( + <div> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + {isTokenDialogActive && ( + <SetTokenNewInstanceModal + onCancel={() => { + updateIsTokenDialogActive(false); + updateIsTokenSet(false); + }} + onClear={() => { + updateToken(null); + updateIsTokenDialogActive(false); + updateIsTokenSet(true); + }} + onConfirm={(newToken) => { + updateToken(newToken); + updateIsTokenDialogActive(false); + updateIsTokenSet(true); + }} + /> + )} + </div> + <div class="column" /> + </div> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider<Entity> + errors={errors} + object={value} + valueHandler={valueHandler} + > + <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} /> + </FormProvider> + + <div class="level"> + <div class="level-item has-text-centered"> + <h1 class="title"> + <button + class={ + !isTokenSet + ? "button is-danger has-tooltip-bottom" + : !value.auth_token + ? "button has-tooltip-bottom" + : "button is-info has-tooltip-bottom" + } + data-tooltip={i18n.str`change authorization configuration`} + onClick={() => updateIsTokenDialogActive(true)} + > + <div class="icon is-centered"> + <i class="mdi mdi-lock-reset" /> + </div> + <span> + <i18n.Translate>Set access token</i18n.Translate> + </span> + </button> + </h1> + </div> + </div> + <div class="level"> + <div class="level-item has-text-centered"> + {!isTokenSet ? ( + <p class="is-size-6"> + <i18n.Translate> + Access token is not yet configured. This instance can't be + created. + </i18n.Translate> + </p> + ) : value.auth_token === undefined ? ( + <p class="is-size-6"> + <i18n.Translate> + No access token. Authorization must be handled externally. + </i18n.Translate> + </p> + ) : ( + <p class="is-size-6"> + <i18n.Translate> + Access token is set. Authorization is handled by the + merchant backend. + </i18n.Translate> + </p> + )} + </div> + </div> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submit} + disabled={hasErrors || !isTokenSet} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields and choose authorization method` + : "confirm operation" + } + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx new file mode 100644 index 000000000..c620c6482 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx @@ -0,0 +1,74 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully.js"; +import { Entity } from "./index.js"; + +export function InstanceCreatedSuccessfully({ + entity, + onConfirm, +}: { + entity: Entity; + onConfirm: () => void; +}): VNode { + return ( + <CreatedSuccessfully onConfirm={onConfirm}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">ID</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.id} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Business Name</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.name} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Access token</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + {entity.auth.method === "external" && "external"} + {entity.auth.method === "token" && ( + <input class="input" readonly value={entity.auth.token} /> + )} + </p> + </div> + </div> + </div> + </CreatedSuccessfully> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx new file mode 100644 index 000000000..23f41ecff --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx @@ -0,0 +1,82 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { AccessToken, MerchantBackend } from "../../../declaration.js"; +import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js"; +import { Notification } from "../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { useCredentialsChecker } from "../../../hooks/backend.js"; +import { useBackendContext } from "../../../context/backend.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + forceId?: string; +} +export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; + +export default function Create({ onBack, onConfirm, forceId }: Props): VNode { + const { createInstance } = useAdminAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const { requestNewLoginToken } = useCredentialsChecker() + const { url: backendURL, updateToken } = useBackendContext() + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <CreatePage + onBack={onBack} + forceId={forceId} + onCreate={async ( + d: MerchantBackend.Instances.InstanceConfigurationMessage, + ) => { + try { + await createInstance(d) + if (d.auth.token) { + const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken) + if (resp.valid) { + const { token, expiration } = resp + updateToken({ token, expiration }); + } else { + updateToken(undefined) + } + } + onConfirm(); + } catch (ex) { + if (ex instanceof Error) { + setNotif({ + message: i18n.str`Failed to create instance`, + type: "ERROR", + description: ex.message, + }); + } else { + console.error(ex) + } + } + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx new file mode 100644 index 000000000..0012f9b9b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx @@ -0,0 +1,52 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ConfigContextProvider } from "../../../context/config.js"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Instance/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, +}; + +function createExample<Props>( + Internal: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const component = (args: any) => ( + <ConfigContextProvider + value={{ + currency: "TESTKUDOS", + version: "1", + }} + > + <Internal {...(props as any)} /> + </ConfigContextProvider> + ); + return { component, props }; +} + +export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts new file mode 100644 index 000000000..fdae1a24d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts @@ -0,0 +1,18 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +// export * as list from "./list/stories.js"; +export * as create from "./create/stories.js"; diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx new file mode 100644 index 000000000..885a351d2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx @@ -0,0 +1,287 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration.js"; + +interface Props { + instances: MerchantBackend.Instances.Instance[]; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + onPurge: (id: MerchantBackend.Instances.Instance) => void; + onCreate: () => void; + selected?: boolean; + setInstanceName: (s: string) => void; +} + +export function CardTable({ + instances, + onCreate, + onUpdate, + onPurge, + setInstanceName, + onDelete, + selected, +}: Props): VNode { + const [actionQueue, actionQueueHandler] = useState<Actions[]>([]); + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + useEffect(() => { + if ( + actionQueue.length > 0 && + !selected && + actionQueue[0].type == "DELETE" + ) { + onDelete(actionQueue[0].element); + actionQueueHandler(actionQueue.slice(1)); + } + }, [actionQueue, selected, onDelete]); + + useEffect(() => { + if ( + actionQueue.length > 0 && + !selected && + actionQueue[0].type == "UPDATE" + ) { + onUpdate(actionQueue[0].element.id); + actionQueueHandler(actionQueue.slice(1)); + } + }, [actionQueue, selected, onUpdate]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-desktop-mac" /> + </span> + <i18n.Translate>Instances</i18n.Translate> + </p> + + <div class="card-header-icon" aria-label="more options"> + <button + class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} + type="button" + onClick={(): void => + actionQueueHandler( + buildActions(instances, rowSelection, "DELETE"), + ) + } + > + <i18n.Translate>Delete</i18n.Translate> + </button> + </div> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new instance`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {instances.length > 0 ? ( + <Table + instances={instances} + onPurge={onPurge} + onUpdate={onUpdate} + setInstanceName={setInstanceName} + onDelete={onDelete} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: MerchantBackend.Instances.Instance[]; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + onPurge: (id: MerchantBackend.Instances.Instance) => void; + rowSelectionHandler: StateUpdater<string[]>; + setInstanceName: (s: string) => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + rowSelection, + rowSelectionHandler, + setInstanceName, + instances, + onUpdate, + onDelete, + onPurge, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th class="is-checkbox-cell"> + <label class="b-checkbox checkbox"> + <input + type="checkbox" + checked={rowSelection.length === instances.length} + onClick={(): void => + rowSelectionHandler( + rowSelection.length === instances.length + ? [] + : instances.map((i) => i.id), + ) + } + /> + <span class="check" /> + </label> + </th> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Name</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td class="is-checkbox-cell"> + <label class="b-checkbox checkbox"> + <input + type="checkbox" + checked={rowSelection.indexOf(i.id) != -1} + onClick={(): void => + rowSelectionHandler(toggleSelected(i.id)) + } + /> + <span class="check" /> + </label> + </td> + <td> + <a + href={`#/orders?instance=${i.id}`} + onClick={(e) => { + setInstanceName(i.id); + }} + > + {i.id} + </a> + </td> + <td>{i.name}</td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-small is-success jb-modal" + type="button" + onClick={(): void => onUpdate(i.id)} + > + <i18n.Translate>Edit</i18n.Translate> + </button> + {!i.deleted && ( + <button + class="button is-small is-danger jb-modal is-outlined" + type="button" + onClick={(): void => onDelete(i)} + > + <i18n.Translate>Delete</i18n.Translate> + </button> + )} + {i.deleted && ( + <button + class="button is-small is-danger jb-modal" + type="button" + onClick={(): void => onPurge(i)} + > + <i18n.Translate>Purge</i18n.Translate> + </button> + )} + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no instances yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} + +interface Actions { + element: MerchantBackend.Instances.Instance; + type: "DELETE" | "UPDATE"; +} + +function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +function buildActions( + instances: MerchantBackend.Instances.Instance[], + selected: string[], + action: "DELETE", +): Actions[] { + return selected + .map((id) => instances.find((i) => i.id === id)) + .filter(notEmpty) + .map((id) => ({ element: id, type: action })); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx new file mode 100644 index 000000000..e0f5d5430 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx @@ -0,0 +1,90 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h } from "preact"; +import { View } from "./View.js"; + +export default { + title: "Pages/Instance/List", + component: View, + argTypes: { + onSelect: { action: "onSelect" }, + }, +}; + +export const Empty = (a: any) => <View {...a} />; +Empty.args = { + instances: [], +}; + +export const WithDefaultInstance = (a: any) => <View {...a} />; +WithDefaultInstance.args = { + instances: [ + { + id: "default", + name: "the default instance", + merchant_pub: "abcdef", + payment_targets: [], + }, + ], +}; + +export const WithFiveInstance = (a: any) => <View {...a} />; +WithFiveInstance.args = { + instances: [ + { + id: "first", + name: "the first instance", + merchant_pub: "abcdefgh", + payment_targets: ["asd"], + }, + { + id: "second", + name: "the second instance", + merchant_pub: "zxczxcz", + payment_targets: ["asd"], + }, + { + id: "third", + name: "the third instance", + merchant_pub: "QWEQWEWQE", + payment_targets: ["asd"], + }, + { + id: "other", + name: "the other instance", + merchant_pub: "FHJHGJGHJ", + payment_targets: ["asd"], + }, + { + id: "another", + name: "the another instance", + merchant_pub: "abcd3423423efgh", + payment_targets: ["asd"], + }, + { + id: "last", + name: "last instance", + merchant_pub: "zxcvvbnm", + payment_targets: ["pay-to", "asd"], + }, + ], +}; diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx new file mode 100644 index 000000000..b59112338 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx @@ -0,0 +1,110 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration.js"; +import { CardTable as CardTableActive } from "./TableActive.js"; + +interface Props { + instances: MerchantBackend.Instances.Instance[]; + onCreate: () => void; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + onPurge: (id: MerchantBackend.Instances.Instance) => void; + selected?: boolean; + setInstanceName: (s: string) => void; +} + +export function View({ + instances, + onCreate, + onDelete, + onPurge, + onUpdate, + setInstanceName, + selected, +}: Props): VNode { + const [show, setShow] = useState<"active" | "deleted" | null>("active"); + const showIsActive = show === "active" ? "is-active" : ""; + const showIsDeleted = show === "deleted" ? "is-active" : ""; + const showAll = show === null ? "is-active" : ""; + const { i18n } = useTranslationContext(); + + const showingInstances = showIsDeleted + ? instances.filter((i) => i.deleted) + : showIsActive + ? instances.filter((i) => !i.deleted) + : instances; + + return ( + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-two-thirds"> + <div class="tabs" style={{ overflow: "inherit" }}> + <ul> + <li class={showIsActive}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Only show active instances`} + > + <a onClick={() => setShow("active")}> + <i18n.Translate>Active</i18n.Translate> + </a> + </div> + </li> + <li class={showIsDeleted}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Only show deleted instances`} + > + <a onClick={() => setShow("deleted")}> + <i18n.Translate>Deleted</i18n.Translate> + </a> + </div> + </li> + <li class={showAll}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`Show all instances`} + > + <a onClick={() => setShow(null)}> + <i18n.Translate>All</i18n.Translate> + </a> + </div> + </li> + </ul> + </div> + </div> + </div> + <CardTableActive + instances={showingInstances} + onDelete={onDelete} + onPurge={onPurge} + setInstanceName={setInstanceName} + onUpdate={onUpdate} + selected={selected} + onCreate={onCreate} + /> + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx new file mode 100644 index 000000000..2f839291b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx @@ -0,0 +1,140 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading.js"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { DeleteModal, PurgeModal } from "../../../components/modal/index.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js"; +import { Notification } from "../../../utils/types.js"; +import { View } from "./View.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +interface Props { + onCreate: () => void; + onUpdate: (id: string) => void; + instances: MerchantBackend.Instances.Instance[]; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + setInstanceName: (s: string) => void; +} + +export default function Instances({ + onUnauthorized, + onLoadError, + onNotFound, + onCreate, + onUpdate, + setInstanceName, +}: Props): VNode { + const result = useBackendInstances(); + const [deleting, setDeleting] = + useState<MerchantBackend.Instances.Instance | null>(null); + const [purging, setPurging] = + useState<MerchantBackend.Instances.Instance | null>(null); + const { deleteInstance, purgeInstance } = useAdminAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <View + instances={result.data.instances} + onDelete={setDeleting} + onCreate={onCreate} + onPurge={setPurging} + onUpdate={onUpdate} + setInstanceName={setInstanceName} + selected={!!deleting} + /> + {deleting && ( + <DeleteModal + element={deleting} + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteInstance(deleting.id); + // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) + setNotif({ + message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to delete instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + // pushNotification({ message: 'delete_error', type: 'ERROR' }) + } + setDeleting(null); + }} + /> + )} + {purging && ( + <PurgeModal + element={purging} + onCancel={() => setPurging(null)} + onConfirm={async (): Promise<void> => { + try { + await purgeInstance(purging.id); + setNotif({ + message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to purge instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setPurging(null); + }} + /> + )} + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx new file mode 100644 index 000000000..3336c53a4 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Accounts/Create", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx new file mode 100644 index 000000000..6e4786a47 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -0,0 +1,173 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string }; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +const accountAuthType = ["none", "basic"]; + +function isValidURL(s: string): boolean { + try { + const u = new URL(s) + return true; + } catch (e) { + return false; + } +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + const errors: FormErrors<Entity> = { + payto_uri: !state.payto_uri ? i18n.str`required` : undefined, + + credit_facade_credentials: !state.credit_facade_credentials + ? undefined + : undefinedIfEmpty({ + username: + state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username + ? i18n.str`required` + : undefined, + password: + state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password + ? i18n.str`required` + : undefined, + }), + credit_facade_url: !state.credit_facade_url + ? undefined + : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url` + : undefined, + repeatPassword: + !state.credit_facade_credentials + ? undefined + : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword) + ? i18n.str`is not the same` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + delete state.repeatPassword + return onCreate(state as any); + }; + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputPaytoForm<Entity> + name="payto_uri" + label={i18n.str`Account`} + /> + <Input<Entity> + name="credit_facade_url" + label={i18n.str`Account info URL`} + help="https://bank.com" + expand + tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} + /> + <InputSelector + name="credit_facade_credentials.type" + label={i18n.str`Auth type`} + tooltip={i18n.str`Choose the authentication type for the account info URL`} + values={accountAuthType} + toStr={(str) => { + if (str === "none") return "Without authentication"; + return "Username and password"; + }} + /> + {state.credit_facade_credentials?.type === "basic" ? ( + <Fragment> + <Input + name="credit_facade_credentials.username" + label={i18n.str`Username`} + tooltip={i18n.str`Username to access the account information.`} + /> + <Input + name="credit_facade_credentials.password" + inputType="password" + label={i18n.str`Password`} + tooltip={i18n.str`Password to access the account information.`} + /> + <Input + name="repeatPassword" + inputType="password" + label={i18n.str`Repeat password`} + /> + </Fragment> + ) : undefined} + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx new file mode 100644 index 000000000..7d33d25ce --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { useBankAccountAPI } from "../../../../hooks/bank.js"; + +export type Entity = MerchantBackend.BankAccounts.AccountAddDetails; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateValidator({ onConfirm, onBack }: Props): VNode { + const { createBankAccount } = useBankAccountAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: Entity) => { + return createBankAccount(request) + .then((d) => { + onConfirm() + }) + .catch((error) => { + setNotif({ + message: i18n.str`could not create device`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx new file mode 100644 index 000000000..6b4b63735 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { FunctionalComponent, h } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Accounts/List", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx new file mode 100644 index 000000000..24da755b9 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx @@ -0,0 +1,64 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { CardTable } from "./Table.js"; + +export interface Props { + devices: MerchantBackend.BankAccounts.BankAccountEntry[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; + onCreate: () => void; + onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; + onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; +} + +export function ListPage({ + devices, + onCreate, + onDelete, + onSelect, + onLoadMoreBefore, + onLoadMoreAfter, +}: Props): VNode { + const form = { payto_uri: "" }; + + const { i18n } = useTranslationContext(); + return ( + <section class="section is-main-section"> + <CardTable + accounts={devices.map((o) => ({ + ...o, + id: String(o.h_wire), + }))} + onCreate={onCreate} + onDelete={onDelete} + onSelect={onSelect} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + hasMoreAfter={!onLoadMoreAfter} + /> + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx new file mode 100644 index 000000000..7d6db0782 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -0,0 +1,385 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration.js"; +import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util"; + +type Entity = MerchantBackend.BankAccounts.BankAccountEntry; + +interface Props { + accounts: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + accounts, + onCreate, + onDelete, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <i18n.Translate>Bank accounts</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new accounts`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {accounts.length > 0 ? ( + <Table + accounts={accounts} + onDelete={onDelete} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + accounts: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + accounts, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], } + const accountsByType = accounts.reduce((prev, acc) => { + const parsed = parsePaytoUri(acc.payto_uri) + if (!parsed) return prev //skip + if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") { + prev["unknown"].push({ parsed, acc }) + } else { + prev[parsed.targetType].push({ parsed, acc }) + } + return prev + }, emptyList) + + const bitcoinAccounts = accountsByType["bitcoin"] + const talerbankAccounts = accountsByType["x-taler-bank"] + const ibanAccounts = accountsByType["iban"] + const unkownAccounts = accountsByType["unknown"] + + + return ( + <Fragment> + + {bitcoinAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Address</i18n.Translate> + </th> + <th> + <i18n.Translate>Sewgit 1</i18n.Translate> + </th> + <th> + <i18n.Translate>Sewgit 2</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {bitcoinAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriBitcoin + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.targetPath} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.segwitAddrs[0]} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.segwitAddrs[1]} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + + + + {talerbankAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Host</i18n.Translate> + </th> + <th> + <i18n.Translate>Account name</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {talerbankAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriTalerBank + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.host} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.account} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + + {ibanAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Account name</i18n.Translate> + </th> + <th> + <i18n.Translate>IBAN</i18n.Translate> + </th> + <th> + <i18n.Translate>BIC</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {ibanAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriIBAN + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.params["receiver-name"]} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.iban} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.bic ?? ""} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + + {unkownAccounts.length > 0 && <div class="table-container"> + <p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Type</i18n.Translate> + </th> + <th> + <i18n.Translate>Path</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {unkownAccounts.map(({ parsed, acc }, idx) => { + const ac = parsed as PaytoUriUnknown + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.targetType} + </td> + <td + onClick={(): void => onSelect(acc)} + style={{ cursor: "pointer" }} + > + {ac.targetPath} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected accounts from the database`} + onClick={() => onDelete(acc)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div>} + </Fragment> + + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no accounts yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx new file mode 100644 index 000000000..100241e22 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -0,0 +1,107 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { Notification } from "../../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListOtpDevices({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const [position, setPosition] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { deleteBankAccount } = useBankAccountAPI(); + const result = useInstanceBankAccounts({ position }, (id) => setPosition(id)); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <ListPage + devices={result.data.accounts} + onLoadMoreBefore={ + result.isReachingStart ? result.loadMorePrev : undefined + } + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.h_wire); + }} + onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) => + deleteBankAccount(e.h_wire) + .then(() => + setNotif({ + message: i18n.str`bank account delete successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not delete the bank account`, + type: "ERROR", + description: error.message, + }), + ) + } + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx new file mode 100644 index 000000000..d6b1d65e0 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx @@ -0,0 +1,32 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/OtpDevices/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx new file mode 100644 index 000000000..e0e0ba7ed --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -0,0 +1,195 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +type Entity = MerchantBackend.BankAccounts.BankAccountEntry + & WithId; + +const accountAuthType = ["unedit", "none", "basic"]; +interface Props { + onUpdate: (d: MerchantBackend.BankAccounts.AccountPatchDetails) => Promise<void>; + onBack?: () => void; + account: Entity; +} + + +export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<MerchantBackend.BankAccounts.AccountPatchDetails>>(account); + + const errors: FormErrors<MerchantBackend.BankAccounts.AccountPatchDetails> = { + credit_facade_url: !state.credit_facade_url ? i18n.str`required` : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined, + credit_facade_credentials: undefinedIfEmpty({ + + username: state.credit_facade_credentials?.type !== "basic" ? undefined + : !state.credit_facade_credentials.username ? i18n.str`required` : undefined, + + password: state.credit_facade_credentials?.type !== "basic" ? undefined + : !state.credit_facade_credentials.password ? i18n.str`required` : undefined, + + repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined + : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` : + (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`doesnt match` + : undefined, + }), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + + const creds: typeof state.credit_facade_credentials = + state.credit_facade_credentials?.type === "basic" ? { + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } : state.credit_facade_credentials?.type === "none" ? { + type: "none" + } : undefined; + + return onUpdate({ + credit_facade_credentials: creds, + credit_facade_url: state.credit_facade_url, + }); + }; + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + Account: <b>{account.id.substring(0, 8)}...</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputPaytoForm<Entity> + name="payto_uri" + label={i18n.str`Account`} + readonly + /> + <Input<Entity> + name="credit_facade_url" + label={i18n.str`Account info URL`} + help="https://bank.com" + expand + tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} + /> + <InputSelector + name="credit_facade_credentials.type" + label={i18n.str`Auth type`} + tooltip={i18n.str`Choose the authentication type for the account info URL`} + values={accountAuthType} + toStr={(str) => { + if (str === "none") return "Without authentication"; + if (str === "basic") return "With authentication"; + return "Do not change" + }} + /> + {state.credit_facade_credentials?.type === "basic" ? ( + <Fragment> + <Input + name="credit_facade_credentials.username" + label={i18n.str`Username`} + tooltip={i18n.str`Username to access the account information.`} + /> + <Input + name="credit_facade_credentials.password" + inputType="password" + label={i18n.str`Password`} + tooltip={i18n.str`Password to access the account information.`} + /> + <Input + name="credit_facade_credentials.repeatPassword" + inputType="password" + label={i18n.str`Repeat password`} + /> + </Fragment> + ) : undefined} + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} + +function isValidURL(s: string): boolean { + try { + const u = new URL(s) + return true; + } catch (e) { + return false; + } +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx new file mode 100644 index 000000000..44dee7651 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -0,0 +1,96 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; + +export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + bid: string; +} +export default function UpdateValidator({ + bid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateBankAccount } = useBankAccountAPI(); + const result = useBankAccountDetails(bid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + account={{ ...result.data, id: bid }} + onBack={onBack} + onUpdate={(data) => { + return updateBankAccount(bid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not update account`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx new file mode 100644 index 000000000..2fc0819bb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Product/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx new file mode 100644 index 000000000..becaf8f3a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { ProductForm } from "../../../../components/product/ProductForm.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useListener } from "../../../../hooks/listener.js"; + +type Entity = MerchantBackend.Products.ProductAddDetail & { + product_id: string; +}; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( + (result) => { + if (result) return onCreate(result); + return Promise.reject(); + }, + ); + + const { i18n } = useTranslationContext(); + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <ProductForm onSubscribe={addFormSubmitter} /> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submitForm} + data-tooltip={ + !submitForm + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={!submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx new file mode 100644 index 000000000..6b02430cc --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx @@ -0,0 +1,72 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { h, VNode } from "preact"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; +import { Entity } from "./index.js"; +import emptyImage from "../../assets/empty.png"; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function CreatedSuccessfully({ + entity, + onConfirm, + onCreateAnother, +}: Props): VNode { + return ( + <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Image</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <img src={entity.image} style={{ width: 200, height: 200 }} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Description</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <textarea class="input" readonly value={entity.description} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Price</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.price} /> + </p> + </div> + </div> + </div> + </Template> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx new file mode 100644 index 000000000..4b59e9807 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { AuditorBackend } from "../../../../declaration.js"; +import { useDepositConfirmationAPI } from "../../../../hooks/deposit_confirmations.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; + +export type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} +export default function CreateProduct({ onConfirm, onBack }: Props): VNode { + const { createProduct } = useProductAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Products.ProductAddDetail) => { + return createProduct(request) + .then(() => onConfirm()) + .catch((error) => { + setNotif({ + message: i18n.str`could not create product`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx new file mode 100644 index 000000000..c2c4d548c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CardTable as TestedComponent } from "./Table.js"; + +export default { + title: "Pages/Product/List", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onSelect: { action: "onSelect" }, + onDelete: { action: "onDelete" }, + onUpdate: { action: "onUpdate" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + instances: [ + { + id: "orderid", + description: "description1", + description_i18n: {} as any, + image: "", + price: "TESTKUDOS:10", + taxes: [], + total_lost: 10, + total_sold: 5, + total_stock: 15, + unit: "bar", + address: {}, + }, + ], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx new file mode 100644 index 000000000..ffd1f12e5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx @@ -0,0 +1,496 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import emptyImage from "../../../../assets/empty.png"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { AuditorBackend, WithId } from "../../../../declaration.js"; +import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId; + +interface Props { + instances: Entity[]; + onDelete: (id: Entity) => void; + onSelect: (depositConfirmation: Entity) => void; + onUpdate: ( + id: string, + data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail, + ) => Promise<void>; + onCreate: () => void; + selected?: boolean; +} + +export function CardTable({ + instances, + onCreate, + onSelect, + onUpdate, + onDelete, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string | undefined>( + undefined, + ); + const { i18n } = useTranslationContext(); + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-shopping" /> + </span> + <i18n.Translate>Deposit Confirmations</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add deposit-confirmation`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {instances.length > 0 ? ( + <Table + instances={instances} + onSelect={onSelect} + onDelete={onDelete} + onUpdate={onUpdate} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string | undefined; + instances: Entity[]; + onSelect: (id: Entity) => void; + onUpdate: ( + id: string, + data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail, + ) => Promise<void>; + onDelete: (serial_id: Entity) => void; + rowSelectionHandler: StateUpdater<string | undefined>; +} + +function Table({ + rowSelection, + rowSelectionHandler, + instances, + onSelect, + onUpdate, + onDelete, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Image</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> + </th> + <th> + <i18n.Translate>Price per unit</i18n.Translate> + </th> + <th> + <i18n.Translate>Taxes</i18n.Translate> + </th> + <th> + <i18n.Translate>Sales</i18n.Translate> + </th> + <th> + <i18n.Translate>Stock</i18n.Translate> + </th> + <th> + <i18n.Translate>Sold</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + const restStockInfo = !i.next_restock + ? "" + : i.next_restock.t_s === "never" + ? "never" + : `restock at ${format( + new Date(i.next_restock.t_s * 1000), + dateFormatForSettings(settings), + )}`; + let stockInfo: ComponentChildren = ""; + if (i.total_stock < 0) { + stockInfo = "infinite"; + } else { + const totalStock = i.total_stock - i.total_lost - i.total_sold; + stockInfo = ( + <label title={restStockInfo}> + {totalStock} {i.unit} + </label> + ); + } + + const isFree = Amounts.isZero(Amounts.parseOrThrow(i.price)); + + return ( + <Fragment key={i.id}> + <tr key="info"> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + <img + src={i.image ? i.image : emptyImage} + style={{ + border: "solid black 1px", + maxHeight: "2em", + width: "auto", + height: "auto", + }} + /> + </td> + <td + class="has-tooltip-right" + data-tooltip={i.description} + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {sum(i.taxes)} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {difference(i.price, sum(i.taxes))} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {stockInfo} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + <span style={{"whiteSpace":"nowrap"}}> + + {i.total_sold} {i.unit} + </span> + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <span + class="has-tooltip-bottom" + data-tooltip={i18n.str`go to product update page`} + > + <button + class="button is-small is-success " + type="button" + onClick={(): void => onSelect(i)} + > + <i18n.Translate>Update</i18n.Translate> + </button> + </span> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`remove this product from the database`} + > + <button + class="button is-small is-danger" + type="button" + onClick={(): void => onDelete(i)} + > + <i18n.Translate>Delete</i18n.Translate> + </button> + </span> + </div> + </td> + </tr> + {rowSelection === i.id && ( + <tr key="form"> + <td colSpan={10}> + <FastProductUpdateForm + product={i} + onUpdate={(prod) => + onUpdate(i.id, prod).then((r) => + rowSelectionHandler(undefined), + ) + } + onCancel={() => rowSelectionHandler(undefined)} + /> + </td> + </tr> + )} + </Fragment> + ); + })} + </tbody> + </table> + </div> + ); +} + +interface FastProductUpdateFormProps { + product: Entity; + onUpdate: ( + data: MerchantBackend.Products.ProductPatchDetail, + ) => Promise<void>; + onCancel: () => void; +} +interface FastProductUpdate { + incoming: number; + lost: number; + price: string; +} +interface UpdatePrice { + price: string; +} + +function FastProductWithInfiniteStockUpdateForm({ + product, + onUpdate, + onCancel, +}: FastProductUpdateFormProps) { + const [value, valueHandler] = useState<UpdatePrice>({ price: product.price }); + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <FormProvider<FastProductUpdate> + name="added" + object={value} + valueHandler={valueHandler as any} + > + <InputCurrency<FastProductUpdate> + name="price" + label={i18n.str`Price`} + tooltip={i18n.str`update the product with new price`} + /> + </FormProvider> + + <div class="buttons is-expanded"> + + <div class="buttons mt-5"> + + <button class="button mt-5" onClick={onCancel}> + <i18n.Translate>Clone</i18n.Translate> + </button> + </div> + <div class="buttons is-right mt-5"> + <button class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`update product with new price`} + > + <button + class="button is-info" + onClick={() => + onUpdate({ + ...product, + price: value.price, + }) + } + > + <i18n.Translate>Confirm update</i18n.Translate> + </button> + </span> + </div> + </div> + </Fragment> + ); +} + +function FastProductWithManagedStockUpdateForm({ + product, + onUpdate, + onCancel, +}: FastProductUpdateFormProps) { + const [value, valueHandler] = useState<FastProductUpdate>({ + incoming: 0, + lost: 0, + price: product.price, + }); + + const currentStock = + product.total_stock - product.total_sold - product.total_lost; + + const errors: FormErrors<FastProductUpdate> = { + lost: + currentStock + value.incoming < value.lost + ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming + })` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <FormProvider<FastProductUpdate> + name="added" + errors={errors} + object={value} + valueHandler={valueHandler as any} + > + <InputNumber<FastProductUpdate> + name="incoming" + label={i18n.str`Incoming`} + tooltip={i18n.str`add more elements to the inventory`} + /> + <InputNumber<FastProductUpdate> + name="lost" + label={i18n.str`Lost`} + tooltip={i18n.str`report elements lost in the inventory`} + /> + <InputCurrency<FastProductUpdate> + name="price" + label={i18n.str`Price`} + tooltip={i18n.str`new price for the product`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + <button class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <span + class="has-tooltip-left" + data-tooltip={ + hasErrors + ? i18n.str`the are value with errors` + : i18n.str`update product with new stock and price` + } + > + <button + class="button is-info" + disabled={hasErrors} + onClick={() => + onUpdate({ + ...product, + total_stock: product.total_stock + value.incoming, + total_lost: product.total_lost + value.lost, + price: value.price, + }) + } + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </span> + </div> + </Fragment> + ); +} + +function FastProductUpdateForm(props: FastProductUpdateFormProps) { + return props.product.total_stock === -1 ? ( + <FastProductWithInfiniteStockUpdateForm {...props} /> + ) : ( + <FastProductWithManagedStockUpdateForm {...props} /> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no products yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} + +function difference(price: string, tax: number) { + if (!tax) return price; + const ps = price.split(":"); + const p = parseInt(ps[1], 10); + ps[1] = `${p - tax}`; + return ps.join(":"); +} +function sum(taxes: MerchantBackend.Tax[]) { + return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx new file mode 100644 index 000000000..dccb3ef25 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx @@ -0,0 +1,151 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { AuditorBackend, WithId } from "../../../../declaration.js"; +import { + useDepositConfirmation, + useDepositConfirmationAPI, +} from "../../../../hooks/deposit_confirmations.js"; +import { Notification } from "../../../../utils/types.js"; +import { CardTable } from "./Table.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; + +interface Props { + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; + onLoadError: (e: HttpError<AuditorBackend.ErrorDetail>) => VNode; +} +export default function DepositConfirmationList({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const result = useDepositConfirmation(); + const { deleteDepositConfirmation, updateDepositConfirmation, getDepositConfirmation } = useDepositConfirmationAPI(); + const [deleting, setDeleting] = + useState<AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId | null>(null); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + + <JumpToElementById + testIfExist={getDepositConfirmation} + onSelect={onSelect} + description={i18n.str`jump to deposit_confirmation with the given serial ID`} + palceholder={i18n.str`serial id`} + /> + + <CardTable + instances={result.data} + onCreate={onCreate} + onUpdate={(id, prod) => + updateDepositConfirmation(id, prod) + .then(() => + setNotif({ + message: i18n.str`deposit_confirmation updated successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not update the deposit_confirmation`, + type: "ERROR", + description: error.message, + }), + ) + } + onSelect={(depositConfirmation) => onSelect(depositConfirmation.id)} + onDelete={(depositConfirmation : AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId) => + setDeleting(depositConfirmation) + } + /> + + {deleting && ( + <ConfirmModal + label={`Delete deposit-confirmation`} + description={`Delete the deposit-cofirmation "${deleting.serial_id}"`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteDepositConfirmation(deleting.serial_id); + setNotif({ + message: i18n.str`Deposit-confirmation "${deleting.serial_id}" (ID: ${deleting.serial_id}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to delete deposit-confirmation`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the deposit-confirmation (ID:{" "} + <b>{deleting.serial_id}</b>), the stock and related information will be lost + </p> + <p class="warning"> + Deleting a deposit-confirmation <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx new file mode 100644 index 000000000..a85b13b8b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx @@ -0,0 +1,73 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/Product/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const WithManagedStock = createExample(TestedComponent, { + product: { + product_id: "20102-ASDAS-QWE", + description: "description1", + description_i18n: {} as any, + image: "", + price: "TESTKUDOS:10", + taxes: [], + total_lost: 10, + total_sold: 5, + total_stock: 15, + unit: "bar", + address: {}, + }, +}); + +export const WithInfiniteStock = createExample(TestedComponent, { + product: { + product_id: "20102-ASDAS-QWE", + description: "description1", + description_i18n: {} as any, + image: "", + price: "TESTKUDOS:10", + taxes: [], + total_lost: 10, + total_sold: 5, + total_stock: -1, + unit: "bar", + address: {}, + }, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx new file mode 100644 index 000000000..97715171e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx @@ -0,0 +1,99 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { ProductForm } from "../../../../components/product/ProductForm.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useListener } from "../../../../hooks/listener.js"; + +type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + product: Entity; +} + +export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { + const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( + (result) => { + if (result) return onUpdate(result); + return Promise.resolve(); + }, + ); + + const { i18n } = useTranslationContext(); + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>Product id:</i18n.Translate> + <b>{product.product_id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <ProductForm + initial={product} + onSubscribe={addFormSubmitter} + alreadyExist + /> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submitForm} + data-tooltip={ + !submitForm + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={!submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx new file mode 100644 index 000000000..8e0f7647f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx @@ -0,0 +1,95 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useProductAPI, useProductDetails } from "../../../../hooks/product.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export type Entity = MerchantBackend.Products.ProductAddDetail; +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + pid: string; +} +export default function UpdateProduct({ + pid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateProduct } = useProductAPI(); + const result = useProductDetails(pid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + product={{ ...result.data, product_id: pid }} + onBack={onBack} + onUpdate={(data) => { + return updateProduct(pid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not create product`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx new file mode 100644 index 000000000..21dadb1e3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx @@ -0,0 +1,83 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "../../../components/form/FormProvider.js"; +import { Input } from "../../../components/form/Input.js"; +import { MerchantBackend } from "../../../declaration.js"; + +type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage; +interface Props { + onUpdate: () => void; + onDelete: () => void; + selected: MerchantBackend.Instances.QueryInstancesResponse; +} + +function convert( + from: MerchantBackend.Instances.QueryInstancesResponse, +): Entity { + const defaults = { + default_wire_fee_amortization: 1, + use_stefan: true, + default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour + default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours + }; + return { ...defaults, ...from }; +} + +export function DetailPage({ selected }: Props): VNode { + const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); + + const { i18n } = useTranslationContext(); + + return ( + <div> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title">Here goes the instance description</h1> + </div> + </div> + <div class="level-right" style="display: none;"> + <div class="level-item" /> + </div> + </div> + </div> + </section> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-6"> + <FormProvider<Entity> object={value} valueHandler={valueHandler}> + <Input<Entity> name="name" readonly label={i18n.str`Name`} /> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx new file mode 100644 index 000000000..9b393b818 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx @@ -0,0 +1,87 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading.js"; +import { DeleteModal } from "../../../components/modal/index.js"; +import { useInstanceContext } from "../../../context/instance.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; +import { DetailPage } from "./DetailPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onUpdate: () => void; + onNotFound: () => VNode; + onDelete: () => void; +} + +export default function Detail({ + onUpdate, + onLoadError, + onUnauthorized, + onDelete, + onNotFound, +}: Props): VNode { + const { id } = useInstanceContext(); + const result = useInstanceDetails(); + const [deleting, setDeleting] = useState<boolean>(false); + + const { deleteInstance } = useInstanceAPI(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <DetailPage + selected={result.data} + onUpdate={onUpdate} + onDelete={() => setDeleting(true)} + /> + {deleting && ( + <DeleteModal + element={{ name: result.data.name, id }} + onCancel={() => setDeleting(false)} + onConfirm={async (): Promise<void> => { + try { + await deleteInstance(); + onDelete(); + } catch (error) { + //FIXME: show message error + } + setDeleting(false); + }} + /> + )} + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx new file mode 100644 index 000000000..367fabce2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ConfigContextProvider } from "../../../context/config.js"; +import { DetailPage as TestedComponent } from "./DetailPage.js"; + +export default { + title: "Pages/Instance/Detail", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Internal: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const component = (args: any) => ( + <ConfigContextProvider + value={{ + currency: "TESTKUDOS", + version: "1", + }} + > + <Internal {...(props as any)} /> + </ConfigContextProvider> + ); + return { component, props }; +} + +export const Example = createExample(TestedComponent, { + selected: { + name: "name", + auth: { method: "external" }, + address: {}, + user_type: "business", + jurisdiction: {}, + use_stefan: true, + default_pay_delay: { + d_us: 1000 * 1000, //one second + }, + default_wire_transfer_delay: { + d_us: 1000 * 1000, //one second + }, + merchant_pub: "ASDWQEKASJDKSADJ", + }, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts new file mode 100644 index 000000000..1d8c76ff9 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts @@ -0,0 +1,19 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +export * as details from "./details/stories.js"; +export * as kycList from "./kyc/list/ListPage.stories.js"; +export * as reserve from "./reserves/create/CreatedSuccessfully.stories.js"; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx new file mode 100644 index 000000000..d33f64ada --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx @@ -0,0 +1,58 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; +import * as tests from "@gnu-taler/web-util/testing"; +import { MerchantBackend } from "../../../../declaration.js"; + +export default { + title: "Pages/KYC/List", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +export const Example = tests.createExample(TestedComponent, { + status: { + timeout_kycs: [], + pending_kycs: [ + { + aml_status: 0, + exchange_url: "http://exchange.taler", + payto_uri: "payto://iban/de123123123", + kyc_url: "http://exchange.taler/kyc", + }, + { + aml_status: 1, + exchange_url: "http://exchange.taler", + payto_uri: "payto://iban/de123123123", + }, + { + aml_status: 2, + exchange_url: "http://exchange.taler", + payto_uri: "payto://iban/de123123123", + }, + ], + } as MerchantBackend.KYC.AccountKycRedirects, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx new file mode 100644 index 000000000..338081886 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -0,0 +1,208 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; + +export interface Props { + status: MerchantBackend.KYC.AccountKycRedirects; +} + +export function ListPage({ status }: Props): VNode { + const { i18n } = useTranslationContext(); + + return ( + <section class="section is-main-section"> + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-clock" /> + </span> + <i18n.Translate>Pending KYC verification</i18n.Translate> + </p> + + <div class="card-header-icon" aria-label="more options" /> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {status.pending_kycs.length > 0 ? ( + <PendingTable entries={status.pending_kycs} /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + + {status.timeout_kycs.length > 0 ? ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-clock" /> + </span> + <i18n.Translate>Timed out</i18n.Translate> + </p> + + <div class="card-header-icon" aria-label="more options" /> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {status.timeout_kycs.length > 0 ? ( + <TimedOutTable entries={status.timeout_kycs} /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ) : undefined} + </section> + ); +} +interface PendingTableProps { + entries: MerchantBackend.KYC.MerchantAccountKycRedirect[]; +} + +interface TimedOutTableProps { + entries: MerchantBackend.KYC.ExchangeKycTimeout[]; +} + +function PendingTable({ entries }: PendingTableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + <table class="table is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Exchange</i18n.Translate> + </th> + <th> + <i18n.Translate>Target account</i18n.Translate> + </th> + <th> + <i18n.Translate>Reason</i18n.Translate> + </th> + </tr> + </thead> + <tbody> + {entries.map((e, i) => { + if (e.kyc_url === undefined) { + // blocked by AML + return ( + <tr key={i}> + <td>{e.exchange_url}</td> + <td>{e.payto_uri}</td> + <td> + {e.aml_status === 1 ? ( + <i18n.Translate> + There is an anti-money laundering process pending to + complete. + </i18n.Translate> + ) : ( + <i18n.Translate> + The account is frozen due to the anti-money laundering + rules. Contact the exchange service provider for further + instructions. + </i18n.Translate> + )} + </td> + </tr> + ); + } else { + // blocked by KYC + return ( + <tr key={i}> + <td>{e.exchange_url}</td> + <td>{e.payto_uri}</td> + <td> + <a href={e.kyc_url} target="_black" rel="noreferrer"> + <i18n.Translate> + Pending KYC process, click here to complete + </i18n.Translate> + </a> + </td> + </tr> + ); + } + })} + </tbody> + </table> + </div> + ); +} + +function TimedOutTable({ entries }: TimedOutTableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + <table class="table is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Exchange</i18n.Translate> + </th> + <th> + <i18n.Translate>Code</i18n.Translate> + </th> + <th> + <i18n.Translate>Http Status</i18n.Translate> + </th> + </tr> + </thead> + <tbody> + {entries.map((e, i) => { + return ( + <tr key={i}> + <td>{e.exchange_url}</td> + <td>{e.exchange_code}</td> + <td>{e.exchange_http_status}</td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-happy mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate>No pending kyc verification!</i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx new file mode 100644 index 000000000..5b93ac169 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -0,0 +1,63 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { Loading } from "../../../../components/exception/loading.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceKYCDetails } from "../../../../hooks/instance.js"; +import { ListPage } from "./ListPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; +} + +export default function ListKYC({ + onUnauthorized, + onLoadError, + onNotFound, +}: Props): VNode { + const result = useInstanceKYCDetails(); + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + const status = result.data.type === "ok" ? undefined : result.data.status; + + if (!status) { + return <div>no kyc required</div>; + } + return <ListPage status={status} />; +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx new file mode 100644 index 000000000..bd9f65718 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx @@ -0,0 +1,71 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Order/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + instanceConfig: { + default_pay_delay: { + d_us: 1000 * 1000 * 60 * 60, //one hour + }, + default_wire_transfer_delay: { + d_us: 1000 * 1000 * 60 * 60, //one hour + }, + use_stefan: true, + }, + instanceInventory: [ + { + id: "t-shirt-1", + description: "a m size t-shirt", + price: "TESTKUDOS:1", + total_stock: -1, + }, + { + id: "t-shirt-2", + price: "TESTKUDOS:1", + description: "a xl size t-shirt", + } as any, + { + id: "t-shirt-3", + price: "TESTKUDOS:1", + description: "a s size t-shirt", + } as any, + ], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx new file mode 100644 index 000000000..fbfd023c1 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -0,0 +1,705 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format, isFuture } from "date-fns"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDate } from "../../../../components/form/InputDate.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputGroup } from "../../../../components/form/InputGroup.js"; +import { InputLocation } from "../../../../components/form/InputLocation.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; +import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js"; +import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js"; +import { ProductList } from "../../../../components/product/ProductList.js"; +import { useConfigContext } from "../../../../context/config.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useSettings } from "../../../../hooks/useSettings.js"; +import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; +import { rate } from "../../../../utils/amount.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +interface Props { + onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; + onBack?: () => void; + instanceConfig: InstanceConfig; + instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[]; +} +interface InstanceConfig { + use_stefan: boolean; + default_pay_delay: TalerProtocolDuration; + default_wire_transfer_delay: TalerProtocolDuration; +} + +function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> { + const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay); + const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay); + + return { + inventoryProducts: {}, + products: [], + pricing: {}, + payments: { + max_fee: undefined, + createToken: true, + pay_deadline: (defaultPayDeadline), + refund_deadline: (defaultPayDeadline), + wire_transfer_deadline: (defaultWireDeadline), + }, + shipping: {}, + extra: {}, + }; +} + +interface ProductAndQuantity { + product: MerchantBackend.Products.ProductDetail & WithId; + quantity: number; +} +export interface ProductMap { + [id: string]: ProductAndQuantity; +} + +interface Pricing { + products_price: string; + order_price: string; + summary: string; +} +interface Shipping { + delivery_date?: Date; + delivery_location?: MerchantBackend.Location; + fullfilment_url?: string; +} +interface Payments { + refund_deadline: Duration; + pay_deadline: Duration; + wire_transfer_deadline: Duration; + auto_refund_deadline: Duration; + max_fee?: string; + createToken: boolean; + minimum_age?: number; +} +interface Entity { + inventoryProducts: ProductMap; + products: MerchantBackend.Product[]; + pricing: Partial<Pricing>; + payments: Partial<Payments>; + shipping: Partial<Shipping>; + extra: Record<string, string>; +} + +const stringIsValidJSON = (value: string) => { + try { + JSON.parse(value.trim()); + return true; + } catch { + return false; + } +}; + +export function CreatePage({ + onCreate, + onBack, + instanceConfig, + instanceInventory, +}: Props): VNode { + const config = useConfigContext(); + const instance_default = with_defaults(instanceConfig, config.currency) + const [value, valueHandler] = useState(instance_default); + const zero = Amounts.zeroOfCurrency(config.currency); + const [settings, updateSettings] = useSettings() + const inventoryList = Object.values(value.inventoryProducts || {}); + const productList = Object.values(value.products || {}); + + const { i18n } = useTranslationContext(); + + const parsedPrice = !value.pricing?.order_price + ? undefined + : Amounts.parse(value.pricing.order_price); + + const errors: FormErrors<Entity> = { + pricing: undefinedIfEmpty({ + summary: !value.pricing?.summary ? i18n.str`required` : undefined, + order_price: !value.pricing?.order_price + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + }), + payments: undefinedIfEmpty({ + refund_deadline: !value.payments?.refund_deadline + ? undefined + : value.payments.pay_deadline && + Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1 + ? i18n.str`refund deadline cannot be before pay deadline` + : value.payments.wire_transfer_deadline && + Duration.cmp( + value.payments.wire_transfer_deadline, + value.payments.refund_deadline, + ) === -1 + ? i18n.str`wire transfer deadline cannot be before refund deadline` + : undefined, + pay_deadline: !value.payments?.pay_deadline + ? i18n.str`required` + : value.payments.wire_transfer_deadline && + Duration.cmp( + value.payments.wire_transfer_deadline, + value.payments.pay_deadline, + ) === -1 + ? i18n.str`wire transfer deadline cannot be before pay deadline` + : undefined, + wire_transfer_deadline: !value.payments?.wire_transfer_deadline + ? i18n.str`required` + : undefined, + auto_refund_deadline: !value.payments?.auto_refund_deadline + ? undefined + : !value.payments?.refund_deadline + ? i18n.str`should have a refund deadline` + : Duration.cmp( + value.payments.refund_deadline, + value.payments.auto_refund_deadline, + ) == -1 + ? i18n.str`auto refund cannot be after refund deadline` + : undefined, + + }), + shipping: undefinedIfEmpty({ + delivery_date: !value.shipping?.delivery_date + ? undefined + : !isFuture(value.shipping.delivery_date) + ? i18n.str`should be in the future` + : undefined, + }), + }; + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submit = (): void => { + const order = value as any; //schema.cast(value); + if (!value.payments) return; + if (!value.shipping) return; + + const request: MerchantBackend.Orders.PostOrderRequest = { + order: { + amount: order.pricing.order_price, + summary: order.pricing.summary, + products: productList, + extra: undefinedIfEmpty(value.extra), + pay_deadline: !value.payments.pay_deadline ? + i18n.str`required` : + AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline)) + ,// : undefined, + wire_transfer_deadline: value.payments.wire_transfer_deadline + ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline)) + : undefined, + refund_deadline: value.payments.refund_deadline + ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline)) + : undefined, + auto_refund: value.payments.auto_refund_deadline + ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline) + : undefined, + max_fee: value.payments.max_fee as string, + + delivery_date: value.shipping.delivery_date + ? { t_s: value.shipping.delivery_date.getTime() / 1000 } + : undefined, + delivery_location: value.shipping.delivery_location, + fulfillment_url: value.shipping.fullfilment_url, + minimum_age: value.payments.minimum_age, + }, + inventory_products: inventoryList.map((p) => ({ + product_id: p.product.id, + quantity: p.quantity, + })), + create_token: value.payments.createToken, + }; + + onCreate(request); + }; + + const addProductToTheInventoryList = ( + product: MerchantBackend.Products.ProductDetail & WithId, + quantity: number, + ) => { + valueHandler((v) => { + const inventoryProducts = { ...v.inventoryProducts }; + inventoryProducts[product.id] = { product, quantity }; + return { ...v, inventoryProducts }; + }); + }; + + const removeProductFromTheInventoryList = (id: string) => { + valueHandler((v) => { + const inventoryProducts = { ...v.inventoryProducts }; + delete inventoryProducts[id]; + return { ...v, inventoryProducts }; + }); + }; + + const addNewProduct = async (product: MerchantBackend.Product) => { + return valueHandler((v) => { + const products = v.products ? [...v.products, product] : []; + return { ...v, products }; + }); + }; + + const removeFromNewProduct = (index: number) => { + valueHandler((v) => { + const products = v.products ? [...v.products] : []; + products.splice(index, 1); + return { ...v, products }; + }); + }; + + const [editingProduct, setEditingProduct] = useState< + MerchantBackend.Product | undefined + >(undefined); + + const totalPriceInventory = inventoryList.reduce((prev, cur) => { + const p = Amounts.parseOrThrow(cur.product.price); + return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount; + }, zero); + + const totalPriceProducts = productList.reduce((prev, cur) => { + if (!cur.price) return zero; + const p = Amounts.parseOrThrow(cur.price); + return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount; + }, zero); + + const hasProducts = inventoryList.length > 0 || productList.length > 0; + const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts); + + const totalAsString = Amounts.stringify(totalPrice.amount); + const allProducts = productList.concat(inventoryList.map(asProduct)); + + const [newField, setNewField] = useState("") + + useEffect(() => { + valueHandler((v) => { + return { + ...v, + pricing: { + ...v.pricing, + products_price: hasProducts ? totalAsString : undefined, + order_price: hasProducts ? totalAsString : undefined, + }, + }; + }); + }, [hasProducts, totalAsString]); + + const discountOrRise = rate( + parsedPrice ?? Amounts.zeroOfCurrency(config.currency), + totalPrice.amount, + ); + + const minAgeByProducts = allProducts.reduce( + (cur, prev) => + !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age, + 0, + ); + + // if there is no default pay deadline + const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline + // and there is no defailt wire deadline + const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline + // user required to set the taler options + const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline + + + return ( + <div> + + <section class="section is-main-section"> + <div class="tabs is-toggle is-fullwidth is-small"> + <ul> + <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => { + updateSettings({ + ...settings, + advanceOrderMode: false + }) + }}> + <a > + <span><i18n.Translate>Simple</i18n.Translate></span> + </a> + </li> + <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => { + updateSettings({ + ...settings, + advanceOrderMode: true + }) + }}> + <a > + <span><i18n.Translate>Advanced</i18n.Translate></span> + </a> + </li> + </ul> + </div> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + {/* // FIXME: translating plural singular */} + <InputGroup + name="inventory_products" + label={i18n.str`Manage products in order`} + alternative={ + allProducts.length > 0 && ( + <p> + {allProducts.length} products with a total price of{" "} + {totalAsString}. + </p> + ) + } + tooltip={i18n.str`Manage list of products in the order.`} + > + <InventoryProductForm + currentProducts={value.inventoryProducts || {}} + onAddProduct={addProductToTheInventoryList} + inventory={instanceInventory} + /> + + {settings.advanceOrderMode && + <NonInventoryProductFrom + productToEdit={editingProduct} + onAddProduct={(p) => { + setEditingProduct(undefined); + return addNewProduct(p); + }} + /> + } + + {allProducts.length > 0 && ( + <ProductList + list={allProducts} + actions={[ + { + name: i18n.str`Remove`, + tooltip: i18n.str`Remove this product from the order.`, + handler: (e, index) => { + if (e.product_id) { + removeProductFromTheInventoryList(e.product_id); + } else { + removeFromNewProduct(index); + setEditingProduct(e); + } + }, + }, + ]} + /> + )} + </InputGroup> + + <FormProvider<Entity> + errors={errors} + object={value} + valueHandler={valueHandler as any} + > + {hasProducts ? ( + <Fragment> + <InputCurrency + name="pricing.products_price" + label={i18n.str`Total price`} + readonly + tooltip={i18n.str`total product price added up`} + /> + <InputCurrency + name="pricing.order_price" + label={i18n.str`Total price`} + addonAfter={ + discountOrRise > 0 && + (discountOrRise < 1 + ? `discount of %${Math.round( + (1 - discountOrRise) * 100, + )}` + : `rise of %${Math.round((discountOrRise - 1) * 100)}`) + } + tooltip={i18n.str`Amount to be paid by the customer`} + /> + </Fragment> + ) : ( + <InputCurrency + name="pricing.order_price" + label={i18n.str`Order price`} + tooltip={i18n.str`final order price`} + /> + )} + + <Input + name="pricing.summary" + inputType="multiline" + label={i18n.str`Summary`} + tooltip={i18n.str`Title of the order to be shown to the customer`} + /> + + {settings.advanceOrderMode && + <InputGroup + name="shipping" + label={i18n.str`Shipping and Fulfillment`} + initialActive + > + <InputDate + name="shipping.delivery_date" + label={i18n.str`Delivery date`} + tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} + /> + {value.shipping?.delivery_date && ( + <InputGroup + name="shipping.delivery_location" + label={i18n.str`Location`} + tooltip={i18n.str`address where the products will be delivered`} + > + <InputLocation name="shipping.delivery_location" /> + </InputGroup> + )} + <Input + name="shipping.fullfilment_url" + label={i18n.str`Fulfillment URL`} + tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} + /> + </InputGroup> + } + + {(settings.advanceOrderMode || requiresSomeTalerOptions) && + <InputGroup + name="payments" + label={i18n.str`Taler payment options`} + tooltip={i18n.str`Override default Taler payment settings for this order`} + > + {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDuration + name="payments.pay_deadline" + label={i18n.str`Payment time`} + help={<DeadlineHelp duration={value.payments?.pay_deadline} />} + withForever + withoutClear + tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`} + side={ + <span> + <button class="button" onClick={() => { + const c = { + ...value, + payments: { + ...(value.payments ?? {}), + pay_deadline: instance_default.payments?.pay_deadline + } + } + valueHandler(c) + }}> + <i18n.Translate>default</i18n.Translate> + </button> + </span> + } + />} + {settings.advanceOrderMode && <InputDuration + name="payments.refund_deadline" + label={i18n.str`Refund time`} + help={<DeadlineHelp duration={value.payments?.refund_deadline} />} + withForever + withoutClear + tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} + side={ + <span> + <button class="button" onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + refund_deadline: instance_default.payments?.refund_deadline + } + }) + }}> + <i18n.Translate>default</i18n.Translate> + </button> + </span> + } + />} + {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDuration + name="payments.wire_transfer_deadline" + label={i18n.str`Wire transfer time`} + help={<DeadlineHelp duration={value.payments?.wire_transfer_deadline} />} + withoutClear + withForever + tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} + side={ + <span> + <button class="button" onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline + } + }) + }}> + <i18n.Translate>default</i18n.Translate> + </button> + </span> + } + />} + {settings.advanceOrderMode && <InputDuration + name="payments.auto_refund_deadline" + label={i18n.str`Auto-refund time`} + help={<DeadlineHelp duration={value.payments?.auto_refund_deadline} />} + tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} + withForever + />} + + {settings.advanceOrderMode && <InputCurrency + name="payments.max_fee" + label={i18n.str`Maximum fee`} + tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} + />} + {settings.advanceOrderMode && <InputToggle + name="payments.createToken" + label={i18n.str`Create token`} + tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} + />} + {settings.advanceOrderMode && <InputNumber + name="payments.minimum_age" + label={i18n.str`Minimum age required`} + tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} + help={ + minAgeByProducts > 0 + ? i18n.str`Min age defined by the producs is ${minAgeByProducts}` + : i18n.str`No product with age restriction in this order` + } + />} + </InputGroup> + } + + {settings.advanceOrderMode && + <InputGroup + name="extra" + label={i18n.str`Additional information`} + tooltip={i18n.str`Custom information to be included in the contract for this order.`} + > + {Object.keys(value.extra ?? {}).map((key) => { + + return <Input + name={`extra.${key}`} + inputType="multiline" + label={key} + tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} + side={ + <button class="button" onClick={(e) => { + if (value.extra && value.extra[key] !== undefined) { + console.log(value.extra) + delete value.extra[key] + } + valueHandler({ + ...value, + }) + }}>remove</button> + } + /> + })} + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Custom field name</i18n.Translate> + <span class="icon has-tooltip-right" data-tooltip={"new extra field"}> + <i class="mdi mdi-information" /> + </span> + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} /> + </p> + </div> + </div> + <button class="button" onClick={(e) => { + setNewField("") + valueHandler({ + ...value, + extra: { + ...(value.extra ?? {}), + [newField]: "" + } + }) + }}>add</button> + </div> + </InputGroup> + } + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <button + class="button is-success" + onClick={submit} + disabled={hasErrors} + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} + +function asProduct(p: ProductAndQuantity): MerchantBackend.Product { + return { + product_id: p.product.id, + image: p.product.image, + price: p.product.price, + unit: p.product.unit, + quantity: p.quantity, + description: p.product.description, + taxes: p.product.taxes, + minimum_age: p.product.minimum_age, + }; +} + + +function DeadlineHelp({ duration }: { duration?: Duration }): VNode { + const { i18n } = useTranslationContext(); + const [now, setNow] = useState(AbsoluteTime.now()) + useEffect(() => { + const iid = setInterval(() => { + setNow(AbsoluteTime.now()) + }, 60 * 1000) + return () => { + clearInterval(iid) + } + }) + if (!duration) return <i18n.Translate>Disabled</i18n.Translate> + const when = AbsoluteTime.addDuration(now, duration) + if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate> + return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate> +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx new file mode 100644 index 000000000..88a984c97 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx @@ -0,0 +1,114 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js"; +import { useOrderAPI } from "../../../../hooks/order.js"; +import { Entity } from "./index.js"; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function OrderCreatedSuccessfully({ + entity, + onConfirm, + onCreateAnother, +}: Props): VNode { + const { getPaymentURL } = useOrderAPI(); + const [url, setURL] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + useEffect(() => { + getPaymentURL(entity.response.order_id).then((response) => { + setURL(response.data); + }); + }, [getPaymentURL, entity.response.order_id]); + + return ( + <CreatedSuccessfully + onConfirm={onConfirm} + onCreateAnother={onCreateAnother} + > + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Amount</i18n.Translate> + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={entity.request.order.amount} + /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Summary</i18n.Translate> + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={entity.request.order.summary} + /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Order ID</i18n.Translate> + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.response.order_id} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Payment URL</i18n.Translate> + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={url} /> + </p> + </div> + </div> + </div> + </CreatedSuccessfully> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx new file mode 100644 index 000000000..2474fd042 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -0,0 +1,114 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceDetails } from "../../../../hooks/instance.js"; +import { useOrderAPI } from "../../../../hooks/order.js"; +import { useInstanceProducts } from "../../../../hooks/product.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export type Entity = { + request: MerchantBackend.Orders.PostOrderRequest; + response: MerchantBackend.Orders.PostOrderResponse; +}; +interface Props { + onBack?: () => void; + onConfirm: (id: string) => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; +} +export default function OrderCreate({ + onConfirm, + onBack, + onLoadError, + onNotFound, + onUnauthorized, +}: Props): VNode { + const { createOrder } = useOrderAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const detailsResult = useInstanceDetails(); + const inventoryResult = useInstanceProducts(); + + if (detailsResult.loading) return <Loading />; + if (inventoryResult.loading) return <Loading />; + + if (!detailsResult.ok) { + if ( + detailsResult.type === ErrorType.CLIENT && + detailsResult.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + detailsResult.type === ErrorType.CLIENT && + detailsResult.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(detailsResult); + } + + if (!inventoryResult.ok) { + if ( + inventoryResult.type === ErrorType.CLIENT && + inventoryResult.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + inventoryResult.type === ErrorType.CLIENT && + inventoryResult.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(inventoryResult); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => { + createOrder(request) + .then((r) => { + return onConfirm(r.data.order_id) + }) + .catch((error) => { + setNotif({ + message: "could not create order", + type: "ERROR", + description: error.message, + }); + }); + }} + instanceConfig={detailsResult.data} + instanceInventory={inventoryResult.data} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx new file mode 100644 index 000000000..6e73a01a5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx @@ -0,0 +1,135 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { addDays } from "date-fns"; +import { h, VNode, FunctionalComponent } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; +import { DetailPage as TestedComponent } from "./DetailPage.js"; + +export default { + title: "Pages/Order/Detail", + component: TestedComponent, + argTypes: { + onRefund: { action: "onRefund" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +const defaultContractTerm = { + amount: "TESTKUDOS:10", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + auditors: [], + exchanges: [], + max_fee: "TESTKUDOS:1", + merchant: {} as any, + merchant_base_url: "http://merchant.url/", + order_id: "2021.165-03GDFC26Y1NNG", + products: [], + summary: "text summary", + wire_transfer_deadline: { + t_s: "never", + }, + refund_deadline: { t_s: "never" }, + merchant_pub: "ASDASDASDSd", + nonce: "QWEQWEQWE", + pay_deadline: { + t_s: "never", + }, + wire_method: "x-taler-bank", + h_wire: "asd", +} as MerchantBackend.ContractTerms; + +// contract_terms: defaultContracTerm, +export const Claimed = createExample(TestedComponent, { + id: "2021.165-03GDFC26Y1NNG", + selected: { + order_status: "claimed", + contract_terms: defaultContractTerm, + }, +}); + +export const PaidNotRefundable = createExample(TestedComponent, { + id: "2021.165-03GDFC26Y1NNG", + selected: { + order_status: "paid", + contract_terms: defaultContractTerm, + refunded: false, + deposit_total: "TESTKUDOS:10", + exchange_ec: 0, + order_status_url: "http://merchant.backend/status", + exchange_hc: 0, + refund_amount: "TESTKUDOS:0", + refund_details: [], + refund_pending: false, + wire_details: [], + wire_reports: [], + wired: false, + }, +}); + +export const PaidRefundable = createExample(TestedComponent, { + id: "2021.165-03GDFC26Y1NNG", + selected: { + order_status: "paid", + contract_terms: { + ...defaultContractTerm, + refund_deadline: { + t_s: addDays(new Date(), 2).getTime() / 1000, + }, + }, + refunded: false, + deposit_total: "TESTKUDOS:10", + exchange_ec: 0, + order_status_url: "http://merchant.backend/status", + exchange_hc: 0, + refund_amount: "TESTKUDOS:0", + refund_details: [], + refund_pending: false, + wire_details: [], + wire_reports: [], + wired: false, + }, +}); + +export const Unpaid = createExample(TestedComponent, { + id: "2021.165-03GDFC26Y1NNG", + selected: { + order_status: "unpaid", + order_status_url: "http://merchant.backend/status", + creation_time: { + t_s: new Date().getTime() / 1000, + }, + summary: "text summary", + taler_pay_uri: "pay uri", + total_amount: "TESTKUDOS:10", + }, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx new file mode 100644 index 000000000..5ff76e37a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -0,0 +1,770 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format, formatDistance } from "date-fns"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDate } from "../../../../components/form/InputDate.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputGroup } from "../../../../components/form/InputGroup.js"; +import { InputLocation } from "../../../../components/form/InputLocation.js"; +import { TextField } from "../../../../components/form/TextField.js"; +import { ProductList } from "../../../../components/product/ProductList.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { mergeRefunds } from "../../../../utils/amount.js"; +import { RefundModal } from "../list/Table.js"; +import { Event, Timeline } from "./Timeline.js"; + +type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; +type CT = MerchantBackend.ContractTerms; + +interface Props { + onBack: () => void; + selected: Entity; + id: string; + onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; +} + +type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & { + refund_taken: string; +}; +type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse; +type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse; + +function ContractTerms({ value }: { value: CT }) { + const { i18n } = useTranslationContext(); + + return ( + <InputGroup name="contract_terms" label={i18n.str`Contract Terms`}> + <FormProvider<CT> object={value} valueHandler={null}> + <Input<CT> + readonly + name="summary" + label={i18n.str`Summary`} + tooltip={i18n.str`human-readable description of the whole purchase`} + /> + <InputCurrency<CT> + readonly + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`total price for the transaction`} + /> + {value.fulfillment_url && ( + <Input<CT> + readonly + name="fulfillment_url" + label={i18n.str`Fulfillment URL`} + tooltip={i18n.str`URL for this purchase`} + /> + )} + <Input<CT> + readonly + name="max_fee" + label={i18n.str`Max fee`} + tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`} + /> + <InputDate<CT> + readonly + name="timestamp" + label={i18n.str`Created at`} + tooltip={i18n.str`time when this contract was generated`} + /> + <InputDate<CT> + readonly + name="refund_deadline" + label={i18n.str`Refund deadline`} + tooltip={i18n.str`after this deadline has passed no refunds will be accepted`} + /> + <InputDate<CT> + readonly + name="pay_deadline" + label={i18n.str`Payment deadline`} + tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`} + /> + <InputDate<CT> + readonly + name="wire_transfer_deadline" + label={i18n.str`Wire transfer deadline`} + tooltip={i18n.str`transfer deadline for the exchange`} + /> + <InputDate<CT> + readonly + name="delivery_date" + label={i18n.str`Delivery date`} + tooltip={i18n.str`time indicating when the order should be delivered`} + /> + {value.delivery_date && ( + <InputGroup + name="delivery_location" + label={i18n.str`Location`} + tooltip={i18n.str`where the order will be delivered`} + > + <InputLocation name="payments.delivery_location" /> + </InputGroup> + )} + <InputDuration<CT> + readonly + name="auto_refund" + label={i18n.str`Auto-refund delay`} + tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`} + /> + <Input<CT> + readonly + name="extra" + label={i18n.str`Extra info`} + tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`} + /> + </FormProvider> + </InputGroup> + ); +} + +function ClaimedPage({ + id, + order, +}: { + id: string; + order: MerchantBackend.Orders.CheckPaymentClaimedResponse; +}) { + const events: Event[] = []; + if (order.contract_terms.timestamp.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.timestamp.t_s * 1000), + description: "order created", + type: "start", + }); + } + if (order.contract_terms.pay_deadline.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.pay_deadline.t_s * 1000), + description: "pay deadline", + type: "deadline", + }); + } + if (order.contract_terms.refund_deadline.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.refund_deadline.t_s * 1000), + description: "refund deadline", + type: "deadline", + }); + } + if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), + description: "wire deadline", + type: "deadline", + }); + } + if ( + order.contract_terms.delivery_date && + order.contract_terms.delivery_date.t_s !== "never" + ) { + events.push({ + when: new Date(order.contract_terms.delivery_date?.t_s * 1000), + description: "delivery", + type: "delivery", + }); + } + + const [value, valueHandler] = useState<Partial<Claimed>>(order); + const { i18n } = useTranslationContext(); + const [settings] = useSettings() + + return ( + <div> + <section class="section"> + <div class="columns"> + <div class="column" /> + <div class="column is-10"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <i18n.Translate>Order</i18n.Translate> #{id} + <div class="tag is-info ml-4"> + <i18n.Translate>claimed</i18n.Translate> + </div> + </div> + </div> + </div> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title">{order.contract_terms.amount}</h1> + </div> + </div> + </div> + + <div class="level"> + <div class="level-left" style={{ maxWidth: "100%" }}> + <div class="level-item" style={{ maxWidth: "100%" }}> + <div + class="content" + style={{ + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }} + > + <p> + <b> + <i18n.Translate>claimed at</i18n.Translate>: + </b>{" "} + {format( + new Date(order.contract_terms.timestamp.t_s * 1000), + datetimeFormatForSettings(settings) + )} + </p> + </div> + </div> + </div> + </div> + </div> + </section> + + <section class="section"> + <div class="columns"> + <div class="column is-4"> + <div class="title"> + <i18n.Translate>Timeline</i18n.Translate> + </div> + <Timeline events={events} /> + </div> + <div class="column is-8"> + <div class="title"> + <i18n.Translate>Payment details</i18n.Translate> + </div> + <FormProvider<Claimed> + object={value} + valueHandler={valueHandler} + > + <Input + name="contract_terms.summary" + readonly + inputType="multiline" + label={i18n.str`Summary`} + /> + <InputCurrency + name="contract_terms.amount" + readonly + label={i18n.str`Amount`} + /> + <Input<Claimed> + name="order_status" + readonly + label={i18n.str`Order status`} + /> + </FormProvider> + </div> + </div> + </section> + + {order.contract_terms.products.length ? ( + <Fragment> + <div class="title"> + <i18n.Translate>Product list</i18n.Translate> + </div> + <ProductList list={order.contract_terms.products} /> + </Fragment> + ) : undefined} + + {value.contract_terms && ( + <ContractTerms value={value.contract_terms} /> + )} + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} +function PaidPage({ + id, + order, + onRefund, +}: { + id: string; + order: MerchantBackend.Orders.CheckPaymentPaidResponse; + onRefund: (id: string) => void; +}) { + const events: Event[] = []; + if (order.contract_terms.timestamp.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.timestamp.t_s * 1000), + description: "order created", + type: "start", + }); + } + if (order.contract_terms.pay_deadline.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.pay_deadline.t_s * 1000), + description: "pay deadline", + type: "deadline", + }); + } + if (order.contract_terms.refund_deadline.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.refund_deadline.t_s * 1000), + description: "refund deadline", + type: "deadline", + }); + } + if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { + events.push({ + when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), + description: "wire deadline", + type: "deadline", + }); + } + if ( + order.contract_terms.delivery_date && + order.contract_terms.delivery_date.t_s !== "never" + ) { + if (order.contract_terms.delivery_date) + events.push({ + when: new Date(order.contract_terms.delivery_date?.t_s * 1000), + description: "delivery", + type: "delivery", + }); + } + order.refund_details.reduce(mergeRefunds, []).forEach((e) => { + if (e.timestamp.t_s !== "never") { + events.push({ + when: new Date(e.timestamp.t_s * 1000), + description: `refund: ${e.amount}: ${e.reason}`, + type: e.pending ? "refund" : "refund-taken", + }); + } + }); + if (order.wire_details && order.wire_details.length) { + if (order.wire_details.length > 1) { + let last: MerchantBackend.Orders.TransactionWireTransfer | null = null; + let first: MerchantBackend.Orders.TransactionWireTransfer | null = null; + let total: AmountJson | null = null; + + order.wire_details.forEach((w) => { + if (last === null || last.execution_time.t_s < w.execution_time.t_s) { + last = w; + } + if (first === null || first.execution_time.t_s > w.execution_time.t_s) { + first = w; + } + total = + total === null + ? Amounts.parseOrThrow(w.amount) + : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount; + }); + const last_time = last!.execution_time.t_s; + if (last_time !== "never") { + events.push({ + when: new Date(last_time * 1000), + description: `wired ${Amounts.stringify(total!)}`, + type: "wired-range", + }); + } + const first_time = first!.execution_time.t_s; + if (first_time !== "never") { + events.push({ + when: new Date(first_time * 1000), + description: `wire transfer started...`, + type: "wired-range", + }); + } + } else { + order.wire_details.forEach((e) => { + if (e.execution_time.t_s !== "never") { + events.push({ + when: new Date(e.execution_time.t_s * 1000), + description: `wired ${e.amount}`, + type: "wired", + }); + } + }); + } + } + + const now = new Date() + const nextEvent = events.find((e) => { + return e.when.getTime() > now.getTime() + }) + + const [value, valueHandler] = useState<Partial<Paid>>(order); + const { url: backendURL } = useBackendContext() + const refundurl = stringifyRefundUri({ + merchantBaseUrl: backendURL, + orderId: order.contract_terms.order_id + }) + const refundable = + new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000; + const { i18n } = useTranslationContext(); + + const amount = Amounts.parseOrThrow(order.contract_terms.amount); + const refund_taken = order.refund_details.reduce((prev, cur) => { + if (cur.pending) return prev; + return Amounts.add(prev, Amounts.parseOrThrow(cur.amount)).amount; + }, Amounts.zeroOfCurrency(amount.currency)); + value.refund_taken = Amounts.stringify(refund_taken); + + return ( + <div> + <section class="section"> + <div class="columns"> + <div class="column" /> + <div class="column is-10"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <i18n.Translate>Order</i18n.Translate> #{id} + <div class="tag is-success ml-4"> + <i18n.Translate>paid</i18n.Translate> + </div> + {order.wired ? ( + <div class="tag is-success ml-4"> + <i18n.Translate>wired</i18n.Translate> + </div> + ) : null} + {order.refunded ? ( + <div class="tag is-danger ml-4"> + <i18n.Translate>refunded</i18n.Translate> + </div> + ) : null} + </div> + </div> + </div> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title">{order.contract_terms.amount}</h1> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <h1 class="title"> + <div class="buttons"> + <span + class="has-tooltip-left" + data-tooltip={ + refundable + ? i18n.str`refund order` + : i18n.str`not refundable` + } + > + <button + class="button is-danger" + disabled={!refundable} + onClick={() => onRefund(id)} + > + <i18n.Translate>refund</i18n.Translate> + </button> + </span> + </div> + </h1> + </div> + </div> + </div> + + <div class="level"> + <div class="level-left" style={{ maxWidth: "100%" }}> + <div class="level-item" style={{ maxWidth: "100%" }}> + <div + class="content" + style={{ + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }} + > + {nextEvent && + <p> + <i18n.Translate>Next event in </i18n.Translate> {formatDistance( + nextEvent.when, + new Date(), + // "yyyy/MM/dd HH:mm:ss", + )} + </p> + } + </div> + </div> + </div> + </div> + </div> + </section> + + <section class="section"> + <div class="columns"> + <div class="column is-4"> + <div class="title"> + <i18n.Translate>Timeline</i18n.Translate> + </div> + <Timeline events={events} /> + </div> + <div class="column is-8"> + <div class="title"> + <i18n.Translate>Payment details</i18n.Translate> + </div> + <FormProvider<Paid> + object={value} + valueHandler={valueHandler} + > + {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n.str`Deposit total`} /> */} + {order.refunded && ( + <InputCurrency<Paid> + name="refund_amount" + readonly + label={i18n.str`Refunded amount`} + /> + )} + {order.refunded && ( + <InputCurrency<Paid> + name="refund_taken" + readonly + label={i18n.str`Refund taken`} + /> + )} + <Input<Paid> + name="order_status" + readonly + label={i18n.str`Order status`} + /> + <TextField<Paid> + name="order_status_url" + label={i18n.str`Status URL`} + > + <a + target="_blank" + rel="noreferrer" + href={order.order_status_url} + > + {order.order_status_url} + </a> + </TextField> + {order.refunded && ( + <TextField<Paid> + name="order_status_url" + label={i18n.str`Refund URI`} + > + <a target="_blank" rel="noreferrer" href={refundurl}> + {refundurl} + </a> + </TextField> + )} + </FormProvider> + </div> + </div> + </section> + + {order.contract_terms.products.length ? ( + <Fragment> + <div class="title"> + <i18n.Translate>Product list</i18n.Translate> + </div> + <ProductList list={order.contract_terms.products} /> + </Fragment> + ) : undefined} + + {value.contract_terms && ( + <ContractTerms value={value.contract_terms} /> + )} + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} + +function UnpaidPage({ + id, + order, +}: { + id: string; + order: MerchantBackend.Orders.CheckPaymentUnpaidResponse; +}) { + const [value, valueHandler] = useState<Partial<Unpaid>>(order); + const { i18n } = useTranslationContext(); + const [settings] = useSettings() + return ( + <div> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title"> + <i18n.Translate>Order</i18n.Translate> #{id} + </h1> + </div> + <div class="tag is-dark"> + <i18n.Translate>unpaid</i18n.Translate> + </div> + </div> + </div> + + <div class="level"> + <div class="level-left" style={{ maxWidth: "100%" }}> + <div class="level-item" style={{ maxWidth: "100%" }}> + <div + class="content" + style={{ + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }} + > + <p> + <b> + <i18n.Translate>pay at</i18n.Translate>: + </b>{" "} + <a + href={order.order_status_url} + rel="nofollow" + target="new" + > + {order.order_status_url} + </a> + </p> + <p> + <b> + <i18n.Translate>created at</i18n.Translate>: + </b>{" "} + {order.creation_time.t_s === "never" + ? "never" + : format( + new Date(order.creation_time.t_s * 1000), + datetimeFormatForSettings(settings) + )} + </p> + </div> + </div> + </div> + </div> + </div> + </section> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider<Unpaid> object={value} valueHandler={valueHandler}> + <Input<Unpaid> + readonly + name="summary" + label={i18n.str`Summary`} + tooltip={i18n.str`human-readable description of the whole purchase`} + /> + <InputCurrency<Unpaid> + readonly + name="total_amount" + label={i18n.str`Amount`} + tooltip={i18n.str`total price for the transaction`} + /> + <Input<Unpaid> + name="order_status" + readonly + label={i18n.str`Order status`} + /> + <Input<Unpaid> + name="order_status_url" + readonly + label={i18n.str`Order status URL`} + /> + <TextField<Unpaid> + name="taler_pay_uri" + label={i18n.str`Payment URI`} + > + <a target="_blank" rel="noreferrer" href={value.taler_pay_uri}> + {value.taler_pay_uri} + </a> + </TextField> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} + +export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { + const [showRefund, setShowRefund] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const DetailByStatus = function () { + switch (selected.order_status) { + case "claimed": + return <ClaimedPage id={id} order={selected} />; + case "paid": + return <PaidPage id={id} order={selected} onRefund={setShowRefund} />; + case "unpaid": + return <UnpaidPage id={id} order={selected} />; + default: + return ( + <div> + <i18n.Translate> + Unknown order status. This is an error, please contact the + administrator. + </i18n.Translate> + </div> + ); + } + }; + + return ( + <Fragment> + {DetailByStatus()} + {showRefund && ( + <RefundModal + order={selected} + onCancel={() => setShowRefund(undefined)} + onConfirm={(value) => { + onRefund(showRefund, value); + setShowRefund(undefined); + }} + /> + )} + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div class="buttons is-right mt-5"> + <button class="button" onClick={onBack}> + <i18n.Translate>Back</i18n.Translate> + </button> + </div> + </div> + <div class="column" /> + </div> + </Fragment> + ); +} + +async function copyToClipboard(text: string) { + return navigator.clipboard.writeText(text); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx new file mode 100644 index 000000000..8c863f386 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx @@ -0,0 +1,129 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { format } from "date-fns"; +import { h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +interface Props { + events: Event[]; +} + +export function Timeline({ events: e }: Props) { + const events = [...e]; + events.push({ + when: new Date(), + description: "now", + type: "now", + }); + + events.sort((a, b) => a.when.getTime() - b.when.getTime()); + const [settings] = useSettings(); + const [state, setState] = useState(events); + useEffect(() => { + const handle = setTimeout(() => { + const eventsWithoutNow = state.filter((e) => e.type !== "now"); + eventsWithoutNow.push({ + when: new Date(), + description: "now", + type: "now", + }); + setState(eventsWithoutNow); + }, 1000); + return () => { + clearTimeout(handle); + }; + }); + return ( + <div class="timeline"> + {events.map((e, i) => { + return ( + <div key={i} class="timeline-item"> + {(() => { + switch (e.type) { + case "deadline": + return ( + <div class="timeline-marker is-icon "> + <i class="mdi mdi-flag" /> + </div> + ); + case "delivery": + return ( + <div class="timeline-marker is-icon "> + <i class="mdi mdi-delivery" /> + </div> + ); + case "start": + return ( + <div class="timeline-marker is-icon"> + <i class="mdi mdi-flag " /> + </div> + ); + case "wired": + return ( + <div class="timeline-marker is-icon is-success"> + <i class="mdi mdi-cash" /> + </div> + ); + case "wired-range": + return ( + <div class="timeline-marker is-icon is-success"> + <i class="mdi mdi-cash" /> + </div> + ); + case "refund": + return ( + <div class="timeline-marker is-icon is-danger"> + <i class="mdi mdi-cash" /> + </div> + ); + case "refund-taken": + return ( + <div class="timeline-marker is-icon is-success"> + <i class="mdi mdi-cash" /> + </div> + ); + case "now": + return ( + <div class="timeline-marker is-icon is-info"> + <i class="mdi mdi-clock" /> + </div> + ); + } + })()} + <div class="timeline-content"> + {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>} + <p>{e.description}</p> + </div> + </div> + ); + })} + </div> + ); +} +export interface Event { + when: Date; + description: string; + type: + | "start" + | "refund" + | "refund-taken" + | "wired" + | "wired-range" + | "deadline" + | "delivery" + | "now"; +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx new file mode 100644 index 000000000..1517a3c42 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -0,0 +1,95 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + useTranslationContext, + HttpError, + ErrorType, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js"; +import { Notification } from "../../../../utils/types.js"; +import { DetailPage } from "./DetailPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export interface Props { + oid: string; + + onBack: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; +} + +export default function Update({ + oid, + onBack, + onLoadError, + onNotFound, + onUnauthorized, +}: Props): VNode { + const { refundOrder } = useOrderAPI(); + const result = useOrderDetails(oid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <DetailPage + onBack={onBack} + id={oid} + onRefund={(id, value) => + refundOrder(id, value) + .then(() => + setNotif({ + message: i18n.str`refund created successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not create the refund`, + type: "ERROR", + description: error.message, + }), + ) + } + selected={result.data} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx new file mode 100644 index 000000000..156c577f4 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx @@ -0,0 +1,107 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Order/List", + component: TestedComponent, + argTypes: { + onShowAll: { action: "onShowAll" }, + onShowPaid: { action: "onShowPaid" }, + onShowRefunded: { action: "onShowRefunded" }, + onShowNotWired: { action: "onShowNotWired" }, + onCopyURL: { action: "onCopyURL" }, + onSelectDate: { action: "onSelectDate" }, + onLoadMoreBefore: { action: "onLoadMoreBefore" }, + onLoadMoreAfter: { action: "onLoadMoreAfter" }, + onSelectOrder: { action: "onSelectOrder" }, + onRefundOrder: { action: "onRefundOrder" }, + onSearchOrderById: { action: "onSearchOrderById" }, + onCreate: { action: "onCreate" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + orders: [ + { + id: "123", + amount: "TESTKUDOS:10", + paid: false, + refundable: true, + row_id: 1, + summary: "summary", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "123", + }, + { + id: "234", + amount: "TESTKUDOS:12", + paid: true, + refundable: true, + row_id: 2, + summary: + "summary with long text, very very long text that someone want to add as a description of the order", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "234", + }, + { + id: "456", + amount: "TESTKUDOS:1", + paid: false, + refundable: false, + row_id: 3, + summary: + "summary with long text, very very long text that someone want to add as a description of the order", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "456", + }, + { + id: "234", + amount: "TESTKUDOS:12", + paid: false, + refundable: false, + row_id: 4, + summary: + "summary with long text, very very long text that someone want to add as a description of the order", + timestamp: { + t_s: new Date().getTime() / 1000, + }, + order_id: "234", + }, + ], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx new file mode 100644 index 000000000..9f80719a1 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -0,0 +1,226 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { h, VNode, Fragment } from "preact"; +import { useState } from "preact/hooks"; +import { DatePicker } from "../../../../components/picker/DatePicker.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { CardTable } from "./Table.js"; +import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +export interface ListPageProps { + onShowAll: () => void; + onShowNotPaid: () => void; + onShowPaid: () => void; + onShowRefunded: () => void; + onShowNotWired: () => void; + onShowWired: () => void; + onCopyURL: (id: string) => void; + isAllActive: string; + isPaidActive: string; + isNotPaidActive: string; + isRefundedActive: string; + isNotWiredActive: string; + isWiredActive: string; + + jumpToDate?: Date; + onSelectDate: (date?: Date) => void; + + orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[]; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; + + onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; + onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; + onCreate: () => void; +} + +export function ListPage({ + hasMoreAfter, + hasMoreBefore, + onLoadMoreAfter, + onLoadMoreBefore, + orders, + isAllActive, + onSelectOrder, + onRefundOrder, + jumpToDate, + onCopyURL, + onShowAll, + onShowPaid, + onShowNotPaid, + onShowRefunded, + onShowNotWired, + onShowWired, + onSelectDate, + isPaidActive, + isRefundedActive, + isNotWiredActive, + onCreate, + isNotPaidActive, + isWiredActive, +}: ListPageProps): VNode { + const { i18n } = useTranslationContext(); + const dateTooltip = i18n.str`select date to show nearby orders`; + const [pickDate, setPickDate] = useState(false); + const [settings] = useSettings(); + + return ( + <Fragment> + <div class="columns"> + <div class="column is-two-thirds"> + <div class="tabs" style={{ overflow: "inherit" }}> + <ul> + <li class={isNotPaidActive}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`only show paid orders`} + > + <a onClick={onShowNotPaid}> + <i18n.Translate>New</i18n.Translate> + </a> + </div> + </li> + <li class={isPaidActive}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`only show paid orders`} + > + <a onClick={onShowPaid}> + <i18n.Translate>Paid</i18n.Translate> + </a> + </div> + </li> + <li class={isRefundedActive}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`only show orders with refunds`} + > + <a onClick={onShowRefunded}> + <i18n.Translate>Refunded</i18n.Translate> + </a> + </div> + </li> + <li class={isNotWiredActive}> + <div + class="has-tooltip-left" + data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} + > + <a onClick={onShowNotWired}> + <i18n.Translate>Not wired</i18n.Translate> + </a> + </div> + </li> + <li class={isWiredActive}> + <div + class="has-tooltip-left" + data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} + > + <a onClick={onShowWired}> + <i18n.Translate>Completed</i18n.Translate> + </a> + </div> + </li> + <li class={isAllActive}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`remove all filters`} + > + <a onClick={onShowAll}> + <i18n.Translate>All</i18n.Translate> + </a> + </div> + </li> + </ul> + </div> + </div> + <div class="column "> + <div class="buttons is-right"> + <div class="field has-addons"> + {jumpToDate && ( + <div class="control"> + <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}> + <span + class="icon" + data-tooltip={i18n.str`clear date filter`} + > + <i class="mdi mdi-close" /> + </span> + </a> + </div> + )} + <div class="control"> + <span class="has-tooltip-top" data-tooltip={dateTooltip}> + <input + class="input" + type="text" + readonly + value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))} + placeholder={i18n.str`date (${dateFormatForSettings(settings)})`} + onClick={() => { + setPickDate(true); + }} + /> + </span> + </div> + <div class="control"> + <span class="has-tooltip-left" data-tooltip={dateTooltip}> + <a + class="button is-fullwidth" + onClick={() => { + setPickDate(true); + }} + > + <span class="icon"> + <i class="mdi mdi-calendar" /> + </span> + </a> + </span> + </div> + </div> + </div> + </div> + </div> + + <DatePicker + opened={pickDate} + closeFunction={() => setPickDate(false)} + dateReceiver={onSelectDate} + /> + + <CardTable + orders={orders} + onCreate={onCreate} + onCopyURL={onCopyURL} + onSelect={onSelectOrder} + onRefund={onRefundOrder} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx new file mode 100644 index 000000000..b2806bb79 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -0,0 +1,417 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputGroup } from "../../../../components/form/InputGroup.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { ConfirmModal } from "../../../../components/modal/index.js"; +import { useConfigContext } from "../../../../context/config.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { mergeRefunds } from "../../../../utils/amount.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId; +interface Props { + orders: Entity[]; + onRefund: (value: Entity) => void; + onCopyURL: (id: string) => void; + onCreate: () => void; + onSelect: (order: Entity) => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + orders, + onCreate, + onRefund, + onCopyURL, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-cash-register" /> + </span> + <i18n.Translate>Orders</i18n.Translate> + </p> + + <div class="card-header-icon" aria-label="more options" /> + + <div class="card-header-icon" aria-label="more options"> + <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {orders.length > 0 ? ( + <Table + instances={orders} + onSelect={onSelect} + onRefund={onRefund} + onCopyURL={(o) => onCopyURL(o.id)} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onRefund: (id: Entity) => void; + onCopyURL: (id: Entity) => void; + onSelect: (id: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function Table({ + instances, + onSelect, + onRefund, + onCopyURL, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + return ( + <div class="table-container"> + {hasMoreBefore && ( + <button + class="button is-fullwidth" + onClick={onLoadMoreBefore} + > + <i18n.Translate>load newer orders</i18n.Translate> + </button> + )} + <table class="table is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th style={{ minWidth: 100 }}> + <i18n.Translate>Date</i18n.Translate> + </th> + <th style={{ minWidth: 100 }}> + <i18n.Translate>Amount</i18n.Translate> + </th> + <th style={{ minWidth: 400 }}> + <i18n.Translate>Summary</i18n.Translate> + </th> + <th style={{ minWidth: 50 }} /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.timestamp.t_s === "never" + ? "never" + : format( + new Date(i.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.amount} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.summary} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + {i.refundable && ( + <button + class="button is-small is-danger jb-modal" + type="button" + onClick={(): void => onRefund(i)} + > + <i18n.Translate>Refund</i18n.Translate> + </button> + )} + {!i.paid && ( + <button + class="button is-small is-info jb-modal" + type="button" + onClick={(): void => onCopyURL(i)} + > + <i18n.Translate>copy url</i18n.Translate> + </button> + )} + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {hasMoreAfter && ( + <button + class="button is-fullwidth" + onClick={onLoadMoreAfter} + > + <i18n.Translate>load older orders</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + No orders have been found matching your query! + </i18n.Translate> + </p> + </div> + ); +} + +interface RefundModalProps { + onCancel: () => void; + onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void; + order: MerchantBackend.Orders.MerchantOrderStatusResponse; +} + +export function RefundModal({ + order, + onCancel, + onConfirm, +}: RefundModalProps): VNode { + type State = { mainReason?: string; description?: string; refund?: string }; + const [form, setValue] = useState<State>({}); + const [settings] = useSettings(); + const { i18n } = useTranslationContext(); + // const [errors, setErrors] = useState<FormErrors<State>>({}); + + const refunds = ( + order.order_status === "paid" ? order.refund_details : [] + ).reduce(mergeRefunds, []); + + const config = useConfigContext(); + const totalRefunded = refunds + .map((r) => r.amount) + .reduce( + (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount, + Amounts.zeroOfCurrency(config.currency), + ); + const orderPrice = + order.order_status === "paid" + ? Amounts.parseOrThrow(order.contract_terms.amount) + : undefined; + const totalRefundable = !orderPrice + ? Amounts.zeroOfCurrency(totalRefunded.currency) + : refunds.length + ? Amounts.sub(orderPrice, totalRefunded).amount + : orderPrice; + + const isRefundable = Amounts.isNonZero(totalRefundable); + const duplicatedText = i18n.str`duplicated`; + + const errors: FormErrors<State> = { + mainReason: !form.mainReason ? i18n.str`required` : undefined, + description: + !form.description && form.mainReason !== duplicatedText + ? i18n.str`required` + : undefined, + refund: !form.refund + ? i18n.str`required` + : !Amounts.parse(form.refund) + ? i18n.str`invalid format` + : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1 + ? i18n.str`this value exceed the refundable amount` + : undefined, + }; + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const validateAndConfirm = () => { + try { + if (!form.refund) return; + onConfirm({ + refund: Amounts.stringify( + Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount, + ), + reason: + form.description === undefined + ? form.mainReason || "" + : `${form.mainReason}: ${form.description}`, + }); + } catch (err) { + console.log(err); + } + }; + + //FIXME: parameters in the translation + return ( + <ConfirmModal + description="refund" + danger + active + disabled={!isRefundable || hasErrors} + onCancel={onCancel} + onConfirm={validateAndConfirm} + > + {refunds.length > 0 && ( + <div class="columns"> + <div class="column is-12"> + <InputGroup + name="asd" + label={`${Amounts.stringify(totalRefunded)} was already refunded`} + > + <table class="table is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>date</i18n.Translate> + </th> + <th> + <i18n.Translate>amount</i18n.Translate> + </th> + <th> + <i18n.Translate>reason</i18n.Translate> + </th> + </tr> + </thead> + <tbody> + {refunds.map((r) => { + return ( + <tr key={r.timestamp.t_s}> + <td> + {r.timestamp.t_s === "never" + ? "never" + : format( + new Date(r.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} + </td> + <td>{r.amount}</td> + <td>{r.reason}</td> + </tr> + ); + })} + </tbody> + </table> + </InputGroup> + </div> + </div> + )} + + {isRefundable && ( + <FormProvider<State> + errors={errors} + object={form} + valueHandler={(d) => setValue(d as any)} + > + <InputCurrency<State> + name="refund" + label={i18n.str`Refund`} + tooltip={i18n.str`amount to be refunded`} + > + <i18n.Translate>Max refundable:</i18n.Translate>{" "} + {Amounts.stringify(totalRefundable)} + </InputCurrency> + <InputSelector + name="mainReason" + label={i18n.str`Reason`} + values={[ + i18n.str`Choose one...`, + duplicatedText, + i18n.str`requested by the customer`, + i18n.str`other`, + ]} + tooltip={i18n.str`why this order is being refunded`} + /> + {form.mainReason && form.mainReason !== duplicatedText ? ( + <Input<State> + label={i18n.str`Description`} + name="description" + tooltip={i18n.str`more information to give context`} + /> + ) : undefined} + </FormProvider> + )} + </ConfirmModal> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx new file mode 100644 index 000000000..34c7d348a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -0,0 +1,231 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + InstanceOrderFilter, + useInstanceOrders, + useOrderAPI, + useOrderDetails, +} from "../../../../hooks/order.js"; +import { Notification } from "../../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { RefundModal } from "./Table.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onSelect: (id: string) => void; + onCreate: () => void; +} + +export default function OrderList({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" }); + const [orderToBeRefunded, setOrderToBeRefunded] = useState< + MerchantBackend.Orders.OrderHistoryEntry | undefined + >(undefined); + + const setNewDate = (date?: Date): void => + setFilter((prev) => ({ ...prev, date })); + + const result = useInstanceOrders(filter, setNewDate); + const { refundOrder, getPaymentURL } = useOrderAPI(); + + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + const isNotPaidActive = filter.paid === "no" ? "is-active" : ""; + const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : ""; + const isRefundedActive = filter.refunded === "yes" ? "is-active" : ""; + const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : ""; + const isWiredActive = filter.wired === "yes" ? "is-active" : ""; + const isAllActive = + filter.paid === undefined && + filter.refunded === undefined && + filter.wired === undefined + ? "is-active" + : ""; + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + + <JumpToElementById + testIfExist={getPaymentURL} + onSelect={onSelect} + description={i18n.str`jump to order with the given product ID`} + palceholder={i18n.str`order id`} + /> + + <ListPage + orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))} + onLoadMoreBefore={result.loadMorePrev} + hasMoreBefore={!result.isReachingStart} + onLoadMoreAfter={result.loadMore} + hasMoreAfter={!result.isReachingEnd} + onSelectOrder={(order) => onSelect(order.id)} + onRefundOrder={(value) => setOrderToBeRefunded(value)} + isAllActive={isAllActive} + isNotWiredActive={isNotWiredActive} + isWiredActive={isWiredActive} + isPaidActive={isPaidActive} + isNotPaidActive={isNotPaidActive} + isRefundedActive={isRefundedActive} + jumpToDate={filter.date} + onCopyURL={(id) => + getPaymentURL(id).then((resp) => copyToClipboard(resp.data)) + } + onCreate={onCreate} + onSelectDate={setNewDate} + onShowAll={() => setFilter({})} + onShowNotPaid={() => setFilter({ paid: "no" })} + onShowPaid={() => setFilter({ paid: "yes" })} + onShowRefunded={() => setFilter({ refunded: "yes" })} + onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })} + onShowWired={() => setFilter({ wired: "yes" })} + /> + + {orderToBeRefunded && ( + <RefundModalForTable + id={orderToBeRefunded.order_id} + onCancel={() => setOrderToBeRefunded(undefined)} + onConfirm={(value) => + refundOrder(orderToBeRefunded.order_id, value) + .then(() => + setNotif({ + message: i18n.str`refund created successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not create the refund`, + type: "ERROR", + description: error.message, + }), + ) + .then(() => setOrderToBeRefunded(undefined)) + } + onLoadError={(error) => { + setNotif({ + message: i18n.str`could not create the refund`, + type: "ERROR", + description: error.message, + }); + setOrderToBeRefunded(undefined); + return <div />; + }} + onUnauthorized={onUnauthorized} + onNotFound={() => { + setNotif({ + message: i18n.str`could not get the order to refund`, + type: "ERROR", + // description: error.message + }); + setOrderToBeRefunded(undefined); + return <div />; + }} + /> + )} + </section> + ); +} + +interface RefundProps { + id: string; + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCancel: () => void; + onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void; +} + +function RefundModalForTable({ + id, + onUnauthorized, + onLoadError, + onNotFound, + onConfirm, + onCancel, +}: RefundProps): VNode { + const result = useOrderDetails(id); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <RefundModal + order={result.data} + onCancel={onCancel} + onConfirm={onConfirm} + /> + ); +} + +async function copyToClipboard(text: string): Promise<void> { + return navigator.clipboard.writeText(text); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx new file mode 100644 index 000000000..26f851cc8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/OtpDevices/Create", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx new file mode 100644 index 000000000..5f1ae26a3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx @@ -0,0 +1,180 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { isBase32RFC3548Charset, randomBase32Key } from "../../../../utils/crypto.js"; +import { QR } from "../../../../components/exception/QR.js"; +import { useInstanceContext } from "../../../../context/instance.js"; + +type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +const algorithms = [0, 1, 2]; +const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; + + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const backend = useBackendContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + + const [showKey, setShowKey] = useState(false); + + const errors: FormErrors<Entity> = { + otp_device_id: !state.otp_device_id ? i18n.str`required` + : !/[a-zA-Z0-9]*/.test(state.otp_device_id) + ? i18n.str`no valid. only characters and numbers` + : undefined, + otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined, + otp_key: !state.otp_key ? i18n.str`required` : + !isBase32RFC3548Charset(state.otp_key) + ? i18n.str`just letters and numbers from 2 to 7` + : state.otp_key.length !== 32 + ? i18n.str`size of the key should be 32` + : undefined, + otp_device_description: !state.otp_device_description ? i18n.str`required` + : !/[a-zA-Z0-9]*/.test(state.otp_device_description) + ? i18n.str`no valid. only characters and numbers` + : undefined, + + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onCreate(state as any); + }; + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="otp_device_id" + label={i18n.str`ID`} + tooltip={i18n.str`Internal id on the system`} + /> + <Input<Entity> + name="otp_device_description" + label={i18n.str`Descripiton`} + tooltip={i18n.str`Useful to identify the device physically`} + /> + <InputSelector<Entity> + name="otp_algorithm" + label={i18n.str`Verification algorithm`} + tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} + values={algorithms} + toStr={(v) => algorithmsNames[v]} + fromStr={(v) => Number(v)} + /> + {state.otp_algorithm && state.otp_algorithm > 0 ? ( + <Fragment> + <InputWithAddon<Entity> + expand + name="otp_key" + label={i18n.str`Device key`} + inputType={showKey ? "text" : "password"} + help="Be sure to be very hard to guess or use the random generator" + tooltip={i18n.str`Your device need to have exactly the same value`} + fromStr={(v) => v.toUpperCase()} + addonAfterAction={() => { + setShowKey(!showKey); + }} + addonAfter={ + <span class="icon" > + {showKey ? ( + <i class="mdi mdi-eye" /> + ) : ( + <i class="mdi mdi-eye-off" /> + )} + </span> + } + side={ + <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-3" + onClick={(e) => { + setState((s) => ({ ...s, otp_key: randomBase32Key() })); + }} + > + <i18n.Translate>random</i18n.Translate> + </button> + } + /> + </Fragment> + ) : undefined} + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx new file mode 100644 index 000000000..db3842711 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx @@ -0,0 +1,104 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { QR } from "../../../../components/exception/QR.js"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; +import { useInstanceContext } from "../../../../context/instance.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useBackendContext } from "../../../../context/backend.js"; + +type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; + +interface Props { + entity: Entity; + onConfirm: () => void; +} + +function isNotUndefined<X>(x: X | undefined): x is X { + return !!x; +} + +export function CreatedSuccessfully({ + entity, + onConfirm, +}: Props): VNode { + const { i18n } = useTranslationContext(); + const { url: backendURL } = useBackendContext() + const { id: instanceId } = useInstanceContext(); + const issuer = new URL(backendURL).hostname; + const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; + const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; + + return ( + <Template onConfirm={onConfirm} > + <p class="is-size-5"> + <i18n.Translate> + You can scan the next QR code with your device or safe the key before continue. + </i18n.Translate> + </p> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">ID</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + readonly + class="input" + value={entity.otp_device_id} + /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"><i18n.Translate>Description</i18n.Translate></label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={entity.otp_device_description} + /> + </p> + </div> + </div> + </div> + <QR + text={qrText} + /> + <div + style={{ + color: "grey", + fontSize: "small", + width: 200, + textAlign: "center", + margin: "auto", + wordBreak: "break-all", + }} + > + {qrTextSafe} + </div> + </Template> + ); +} + diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx new file mode 100644 index 000000000..648846793 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; + +export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateValidator({ onConfirm, onBack }: Props): VNode { + const { createOtpDevice } = useOtpDeviceAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null) + + if (created) { + return <CreatedSuccessfully entity={created} onConfirm={onConfirm} /> + } + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: Entity) => { + return createOtpDevice(request) + .then((d) => { + setCreated(request) + }) + .catch((error) => { + setNotif({ + message: i18n.str`could not create device`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx new file mode 100644 index 000000000..b18049674 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { FunctionalComponent, h } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/OtpDevices/List", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx new file mode 100644 index 000000000..4efee9781 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx @@ -0,0 +1,64 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { CardTable } from "./Table.js"; + +export interface Props { + devices: MerchantBackend.OTP.OtpDeviceEntry[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; + onCreate: () => void; + onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; + onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; +} + +export function ListPage({ + devices, + onCreate, + onDelete, + onSelect, + onLoadMoreBefore, + onLoadMoreAfter, +}: Props): VNode { + const form = { payto_uri: "" }; + + const { i18n } = useTranslationContext(); + return ( + <section class="section is-main-section"> + <CardTable + devices={devices.map((o) => ({ + ...o, + id: String(o.otp_device_id), + }))} + onCreate={onCreate} + onDelete={onDelete} + onSelect={onSelect} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + hasMoreAfter={!onLoadMoreAfter} + /> + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx new file mode 100644 index 000000000..0c28027fe --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx @@ -0,0 +1,211 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.OTP.OtpDeviceEntry; + +interface Props { + devices: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + devices, + onCreate, + onDelete, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <i18n.Translate>OTP Devices</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new devices`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {devices.length > 0 ? ( + <Table + instances={devices} + onDelete={onDelete} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {hasMoreBefore && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more devices before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>load newer devices</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.otp_device_id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.otp_device_id} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.otp_device_id} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected devices from the database`} + onClick={() => onDelete(i)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {hasMoreAfter && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more devices after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>load older devices</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no devices yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx new file mode 100644 index 000000000..2aae8738a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx @@ -0,0 +1,106 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { Notification } from "../../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListOtpDevices({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const [position, setPosition] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { deleteOtpDevice } = useOtpDeviceAPI(); + const result = useInstanceOtpDevices({ position }, (id) => setPosition(id)); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <ListPage + devices={result.data.otp_devices} + onLoadMoreBefore={ + result.isReachingStart ? result.loadMorePrev : undefined + } + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.otp_device_id); + }} + onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) => + deleteOtpDevice(e.otp_device_id) + .then(() => + setNotif({ + message: i18n.str`validator delete successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not delete the validator`, + type: "ERROR", + description: error.message, + }), + ) + } + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx new file mode 100644 index 000000000..d6b1d65e0 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx @@ -0,0 +1,32 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/OtpDevices/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx new file mode 100644 index 000000000..b82807cc7 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx @@ -0,0 +1,171 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { randomBase32Key } from "../../../../utils/crypto.js"; + +type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + device: Entity; +} +const algorithms = [0, 1, 2]; +const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; +export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>(device); + const [showKey, setShowKey] = useState(false); + + const errors: FormErrors<Entity> = { + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onUpdate(state as any); + }; + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + Device: <b>{device.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="otp_device_description" + label={i18n.str`Description`} + tooltip={i18n.str`Useful to identify the device physically`} + /> + <InputSelector<Entity> + name="otp_algorithm" + label={i18n.str`Verification algorithm`} + tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} + values={algorithms} + toStr={(v) => algorithmsNames[v]} + fromStr={(v) => Number(v)} + /> + {state.otp_algorithm && state.otp_algorithm > 0 ? ( + <Fragment> + <InputWithAddon<Entity> + name="otp_key" + label={i18n.str`Device key`} + readonly={state.otp_key === undefined} + inputType={showKey ? "text" : "password"} + help={state.otp_key === undefined ? "Not modified" : "Be sure to be very hard to guess or use the random generator"} + tooltip={i18n.str`Your device need to have exactly the same value`} + fromStr={(v) => v.toUpperCase()} + addonAfterAction={() => { + setShowKey(!showKey); + }} + addonAfter={ + <span class="icon" onClick={() => { + setShowKey(!showKey); + }}> + {showKey ? ( + <i class="mdi mdi-eye" /> + ) : ( + <i class="mdi mdi-eye-off" /> + )} + </span> + } + side={ + state.otp_key === undefined ? <button + + onClick={(e) => { + setState((s) => ({ ...s, otp_key: "" })); + }} + class="button">change key</button> : + <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-3" + onClick={(e) => { + setState((s) => ({ ...s, otp_key: randomBase32Key() })); + }} + > + <i18n.Translate>random</i18n.Translate> + </button> + } + /> + </Fragment> + ) : undefined} </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx new file mode 100644 index 000000000..52f6c6c29 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx @@ -0,0 +1,102 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js"; + +export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + vid: string; +} +export default function UpdateValidator({ + vid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateOtpDevice } = useOtpDeviceAPI(); + const result = useOtpDeviceDetails(vid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + device={{ + id: vid, + otp_algorithm: result.data.otp_algorithm, + otp_device_description: result.data.device_description, + otp_key: undefined, + otp_ctr: result.data.otp_ctr + }} + onBack={onBack} + onUpdate={(data) => { + return updateOtpDevice(vid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not update template`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx new file mode 100644 index 000000000..2fc0819bb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Product/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx new file mode 100644 index 000000000..becaf8f3a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { ProductForm } from "../../../../components/product/ProductForm.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useListener } from "../../../../hooks/listener.js"; + +type Entity = MerchantBackend.Products.ProductAddDetail & { + product_id: string; +}; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( + (result) => { + if (result) return onCreate(result); + return Promise.reject(); + }, + ); + + const { i18n } = useTranslationContext(); + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <ProductForm onSubscribe={addFormSubmitter} /> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submitForm} + data-tooltip={ + !submitForm + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={!submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx new file mode 100644 index 000000000..6b02430cc --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx @@ -0,0 +1,72 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { h, VNode } from "preact"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; +import { Entity } from "./index.js"; +import emptyImage from "../../assets/empty.png"; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function CreatedSuccessfully({ + entity, + onConfirm, + onCreateAnother, +}: Props): VNode { + return ( + <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Image</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <img src={entity.image} style={{ width: 200, height: 200 }} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Description</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <textarea class="input" readonly value={entity.description} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Price</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.price} /> + </p> + </div> + </div> + </div> + </Template> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx new file mode 100644 index 000000000..775690bd1 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useProductAPI } from "../../../../hooks/product.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; + +export type Entity = MerchantBackend.Products.ProductAddDetail; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} +export default function CreateProduct({ onConfirm, onBack }: Props): VNode { + const { createProduct } = useProductAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Products.ProductAddDetail) => { + return createProduct(request) + .then(() => onConfirm()) + .catch((error) => { + setNotif({ + message: i18n.str`could not create product`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx new file mode 100644 index 000000000..c2c4d548c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CardTable as TestedComponent } from "./Table.js"; + +export default { + title: "Pages/Product/List", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onSelect: { action: "onSelect" }, + onDelete: { action: "onDelete" }, + onUpdate: { action: "onUpdate" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + instances: [ + { + id: "orderid", + description: "description1", + description_i18n: {} as any, + image: "", + price: "TESTKUDOS:10", + taxes: [], + total_lost: 10, + total_sold: 5, + total_stock: 15, + unit: "bar", + address: {}, + }, + ], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx new file mode 100644 index 000000000..275f855cb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -0,0 +1,496 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import emptyImage from "../../../../assets/empty.png"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Products.ProductDetail & WithId; + +interface Props { + instances: Entity[]; + onDelete: (id: Entity) => void; + onSelect: (product: Entity) => void; + onUpdate: ( + id: string, + data: MerchantBackend.Products.ProductPatchDetail, + ) => Promise<void>; + onCreate: () => void; + selected?: boolean; +} + +export function CardTable({ + instances, + onCreate, + onSelect, + onUpdate, + onDelete, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string | undefined>( + undefined, + ); + const { i18n } = useTranslationContext(); + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-shopping" /> + </span> + <i18n.Translate>Inventory</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add product to inventory`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {instances.length > 0 ? ( + <Table + instances={instances} + onSelect={onSelect} + onDelete={onDelete} + onUpdate={onUpdate} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string | undefined; + instances: Entity[]; + onSelect: (id: Entity) => void; + onUpdate: ( + id: string, + data: MerchantBackend.Products.ProductPatchDetail, + ) => Promise<void>; + onDelete: (id: Entity) => void; + rowSelectionHandler: StateUpdater<string | undefined>; +} + +function Table({ + rowSelection, + rowSelectionHandler, + instances, + onSelect, + onUpdate, + onDelete, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Image</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> + </th> + <th> + <i18n.Translate>Price per unit</i18n.Translate> + </th> + <th> + <i18n.Translate>Taxes</i18n.Translate> + </th> + <th> + <i18n.Translate>Sales</i18n.Translate> + </th> + <th> + <i18n.Translate>Stock</i18n.Translate> + </th> + <th> + <i18n.Translate>Sold</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + const restStockInfo = !i.next_restock + ? "" + : i.next_restock.t_s === "never" + ? "never" + : `restock at ${format( + new Date(i.next_restock.t_s * 1000), + dateFormatForSettings(settings), + )}`; + let stockInfo: ComponentChildren = ""; + if (i.total_stock < 0) { + stockInfo = "infinite"; + } else { + const totalStock = i.total_stock - i.total_lost - i.total_sold; + stockInfo = ( + <label title={restStockInfo}> + {totalStock} {i.unit} + </label> + ); + } + + const isFree = Amounts.isZero(Amounts.parseOrThrow(i.price)); + + return ( + <Fragment key={i.id}> + <tr key="info"> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + <img + src={i.image ? i.image : emptyImage} + style={{ + border: "solid black 1px", + maxHeight: "2em", + width: "auto", + height: "auto", + }} + /> + </td> + <td + class="has-tooltip-right" + data-tooltip={i.description} + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {sum(i.taxes)} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {difference(i.price, sum(i.taxes))} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {stockInfo} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + <span style={{"whiteSpace":"nowrap"}}> + + {i.total_sold} {i.unit} + </span> + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <span + class="has-tooltip-bottom" + data-tooltip={i18n.str`go to product update page`} + > + <button + class="button is-small is-success " + type="button" + onClick={(): void => onSelect(i)} + > + <i18n.Translate>Update</i18n.Translate> + </button> + </span> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`remove this product from the database`} + > + <button + class="button is-small is-danger" + type="button" + onClick={(): void => onDelete(i)} + > + <i18n.Translate>Delete</i18n.Translate> + </button> + </span> + </div> + </td> + </tr> + {rowSelection === i.id && ( + <tr key="form"> + <td colSpan={10}> + <FastProductUpdateForm + product={i} + onUpdate={(prod) => + onUpdate(i.id, prod).then((r) => + rowSelectionHandler(undefined), + ) + } + onCancel={() => rowSelectionHandler(undefined)} + /> + </td> + </tr> + )} + </Fragment> + ); + })} + </tbody> + </table> + </div> + ); +} + +interface FastProductUpdateFormProps { + product: Entity; + onUpdate: ( + data: MerchantBackend.Products.ProductPatchDetail, + ) => Promise<void>; + onCancel: () => void; +} +interface FastProductUpdate { + incoming: number; + lost: number; + price: string; +} +interface UpdatePrice { + price: string; +} + +function FastProductWithInfiniteStockUpdateForm({ + product, + onUpdate, + onCancel, +}: FastProductUpdateFormProps) { + const [value, valueHandler] = useState<UpdatePrice>({ price: product.price }); + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <FormProvider<FastProductUpdate> + name="added" + object={value} + valueHandler={valueHandler as any} + > + <InputCurrency<FastProductUpdate> + name="price" + label={i18n.str`Price`} + tooltip={i18n.str`update the product with new price`} + /> + </FormProvider> + + <div class="buttons is-expanded"> + + <div class="buttons mt-5"> + + <button class="button mt-5" onClick={onCancel}> + <i18n.Translate>Clone</i18n.Translate> + </button> + </div> + <div class="buttons is-right mt-5"> + <button class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`update product with new price`} + > + <button + class="button is-info" + onClick={() => + onUpdate({ + ...product, + price: value.price, + }) + } + > + <i18n.Translate>Confirm update</i18n.Translate> + </button> + </span> + </div> + </div> + </Fragment> + ); +} + +function FastProductWithManagedStockUpdateForm({ + product, + onUpdate, + onCancel, +}: FastProductUpdateFormProps) { + const [value, valueHandler] = useState<FastProductUpdate>({ + incoming: 0, + lost: 0, + price: product.price, + }); + + const currentStock = + product.total_stock - product.total_sold - product.total_lost; + + const errors: FormErrors<FastProductUpdate> = { + lost: + currentStock + value.incoming < value.lost + ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming + })` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <FormProvider<FastProductUpdate> + name="added" + errors={errors} + object={value} + valueHandler={valueHandler as any} + > + <InputNumber<FastProductUpdate> + name="incoming" + label={i18n.str`Incoming`} + tooltip={i18n.str`add more elements to the inventory`} + /> + <InputNumber<FastProductUpdate> + name="lost" + label={i18n.str`Lost`} + tooltip={i18n.str`report elements lost in the inventory`} + /> + <InputCurrency<FastProductUpdate> + name="price" + label={i18n.str`Price`} + tooltip={i18n.str`new price for the product`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + <button class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <span + class="has-tooltip-left" + data-tooltip={ + hasErrors + ? i18n.str`the are value with errors` + : i18n.str`update product with new stock and price` + } + > + <button + class="button is-info" + disabled={hasErrors} + onClick={() => + onUpdate({ + ...product, + total_stock: product.total_stock + value.incoming, + total_lost: product.total_lost + value.lost, + price: value.price, + }) + } + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </span> + </div> + </Fragment> + ); +} + +function FastProductUpdateForm(props: FastProductUpdateFormProps) { + return props.product.total_stock === -1 ? ( + <FastProductWithInfiniteStockUpdateForm {...props} /> + ) : ( + <FastProductWithManagedStockUpdateForm {...props} /> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no products yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} + +function difference(price: string, tax: number) { + if (!tax) return price; + const ps = price.split(":"); + const p = parseInt(ps[1], 10); + ps[1] = `${p - tax}`; + return ps.join(":"); +} +function sum(taxes: MerchantBackend.Tax[]) { + return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx new file mode 100644 index 000000000..942b5d0ac --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -0,0 +1,150 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { + useInstanceProducts, + useProductAPI, +} from "../../../../hooks/product.js"; +import { Notification } from "../../../../utils/types.js"; +import { CardTable } from "./Table.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; + +interface Props { + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; +} +export default function ProductList({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const result = useInstanceProducts(); + const { deleteProduct, updateProduct, getProduct } = useProductAPI(); + const [deleting, setDeleting] = + useState<MerchantBackend.Products.ProductDetail & WithId | null>(null); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + + <JumpToElementById + testIfExist={getProduct} + onSelect={onSelect} + description={i18n.str`jump to product with the given product ID`} + palceholder={i18n.str`product id`} + /> + + <CardTable + instances={result.data} + onCreate={onCreate} + onUpdate={(id, prod) => + updateProduct(id, prod) + .then(() => + setNotif({ + message: i18n.str`product updated successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not update the product`, + type: "ERROR", + description: error.message, + }), + ) + } + onSelect={(product) => onSelect(product.id)} + onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) => + setDeleting(prod) + } + /> + + {deleting && ( + <ConfirmModal + label={`Delete product`} + description={`Delete the product "${deleting.description}"`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteProduct(deleting.id); + setNotif({ + message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to delete product`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the product named <b>"{deleting.description}"</b> (ID:{" "} + <b>{deleting.id}</b>), the stock and related information will be lost + </p> + <p class="warning"> + Deleting an product <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx new file mode 100644 index 000000000..a85b13b8b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx @@ -0,0 +1,73 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/Product/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const WithManagedStock = createExample(TestedComponent, { + product: { + product_id: "20102-ASDAS-QWE", + description: "description1", + description_i18n: {} as any, + image: "", + price: "TESTKUDOS:10", + taxes: [], + total_lost: 10, + total_sold: 5, + total_stock: 15, + unit: "bar", + address: {}, + }, +}); + +export const WithInfiniteStock = createExample(TestedComponent, { + product: { + product_id: "20102-ASDAS-QWE", + description: "description1", + description_i18n: {} as any, + image: "", + price: "TESTKUDOS:10", + taxes: [], + total_lost: 10, + total_sold: 5, + total_stock: -1, + unit: "bar", + address: {}, + }, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx new file mode 100644 index 000000000..97715171e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx @@ -0,0 +1,99 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { ProductForm } from "../../../../components/product/ProductForm.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useListener } from "../../../../hooks/listener.js"; + +type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + product: Entity; +} + +export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { + const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( + (result) => { + if (result) return onUpdate(result); + return Promise.resolve(); + }, + ); + + const { i18n } = useTranslationContext(); + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>Product id:</i18n.Translate> + <b>{product.product_id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <ProductForm + initial={product} + onSubscribe={addFormSubmitter} + alreadyExist + /> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submitForm} + data-tooltip={ + !submitForm + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={!submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx new file mode 100644 index 000000000..8e0f7647f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -0,0 +1,95 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useProductAPI, useProductDetails } from "../../../../hooks/product.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export type Entity = MerchantBackend.Products.ProductAddDetail; +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + pid: string; +} +export default function UpdateProduct({ + pid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateProduct } = useProductAPI(); + const result = useProductDetails(pid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + product={{ ...result.data, product_id: pid }} + onBack={onBack} + onUpdate={(data) => { + return updateProduct(pid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not create product`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx new file mode 100644 index 000000000..5542c028a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Reserve/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx new file mode 100644 index 000000000..e46941b6d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx @@ -0,0 +1,277 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { HttpError, RequestError, useApiContext, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + PAYTO_WIRE_METHOD_LOOKUP, + URL_REGEX, +} from "../../../../utils/constants.js"; +import { useBackendBaseRequest } from "../../../../hooks/backend.js"; +import { parsePaytoUri } from "@gnu-taler/taler-util"; + +type Entity = MerchantBackend.Rewards.ReserveCreateRequest; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +enum Steps { + EXCHANGE, + WIRE_METHOD, +} + +interface ViewProps { + step: Steps; + setCurrentStep: (s: Steps) => void; + reserve: Partial<Entity>; + onBack?: () => void; + submitForm: () => Promise<void>; + setReserve: StateUpdater<Partial<Entity>>; +} +function ViewStep({ + step, + setCurrentStep, + reserve, + onBack, + submitForm, + setReserve, +}: ViewProps): VNode { + const { i18n } = useTranslationContext(); + const {request} = useApiContext() + const [wireMethods, setWireMethods] = useState<Array<string>>([]); + const [exchangeQueryError, setExchangeQueryError] = useState< + string | undefined + >(undefined); + + useEffect(() => { + setExchangeQueryError(undefined); + }, [reserve.exchange_url]); + + switch (step) { + case Steps.EXCHANGE: { + const errors: FormErrors<Entity> = { + initial_balance: !reserve.initial_balance + ? "cannot be empty" + : !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0) + ? i18n.str`it should be greater than 0` + : undefined, + exchange_url: !reserve.exchange_url + ? i18n.str`cannot be empty` + : !URL_REGEX.test(reserve.exchange_url) + ? i18n.str`must be a valid URL` + : !exchangeQueryError + ? undefined + : exchangeQueryError, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + return ( + <Fragment> + <FormProvider<Entity> + object={reserve} + errors={errors} + valueHandler={setReserve} + > + <InputCurrency<Entity> + name="initial_balance" + label={i18n.str`Initial balance`} + tooltip={i18n.str`balance prior to deposit`} + /> + <Input<Entity> + name="exchange_url" + label={i18n.str`Exchange URL`} + tooltip={i18n.str`URL of exchange`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + class="has-tooltip-left" + onClick={() => { + if (!reserve.exchange_url) { + return Promise.resolve(); + } + + return request<any>(reserve.exchange_url, "keys") + .then((r) => { + console.log(r) + if (r.loading) return; + if (r.ok) { + const wireMethods = r.data.accounts.map((a: any) => { + const p = parsePaytoUri(a.payto_uri); + const r = p?.targetType + return r + }).filter((x:any) => !!x); + setWireMethods(Array.from(new Set(wireMethods))); + } + setCurrentStep(Steps.WIRE_METHOD); + return; + }) + .catch((r: RequestError<{}>) => { + console.log(r.cause) + setExchangeQueryError(r.cause.message); + }); + }} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={hasErrors} + > + <i18n.Translate>Next</i18n.Translate> + </AsyncButton> + </div> + </Fragment> + ); + } + + case Steps.WIRE_METHOD: { + const errors: FormErrors<Entity> = { + wire_method: !reserve.wire_method + ? i18n.str`cannot be empty` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + return ( + <Fragment> + <FormProvider<Entity> + object={reserve} + errors={errors} + valueHandler={setReserve} + > + <InputCurrency<Entity> + name="initial_balance" + label={i18n.str`Initial balance`} + tooltip={i18n.str`balance prior to deposit`} + readonly + /> + <Input<Entity> + name="exchange_url" + label={i18n.str`Exchange URL`} + tooltip={i18n.str`URL of exchange`} + readonly + /> + <InputSelector<Entity> + name="wire_method" + label={i18n.str`Wire method`} + tooltip={i18n.str`method to use for wire transfer`} + values={wireMethods} + placeholder={i18n.str`Select one wire method`} + /> + </FormProvider> + <div class="buttons is-right mt-5"> + {onBack && ( + <button + class="button" + onClick={() => setCurrentStep(Steps.EXCHANGE)} + > + <i18n.Translate>Back</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submitForm} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={hasErrors} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </Fragment> + ); + } + } +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const [reserve, setReserve] = useState<Partial<Entity>>({}); + + const submitForm = () => { + return onCreate(reserve as Entity); + }; + + const [currentStep, setCurrentStep] = useState(Steps.EXCHANGE); + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div class="tabs is-toggle is-fullwidth is-small"> + <ul> + <li class={currentStep === Steps.EXCHANGE ? "is-active" : ""}> + <a style={{ cursor: "initial" }}> + <span>Step 1: Specify exchange</span> + </a> + </li> + <li + class={currentStep === Steps.WIRE_METHOD ? "is-active" : ""} + > + <a style={{ cursor: "initial" }}> + <span>Step 2: Select wire method</span> + </a> + </li> + </ul> + </div> + + <ViewStep + step={currentStep} + reserve={reserve} + setCurrentStep={setCurrentStep} + setReserve={setReserve} + submitForm={submitForm} + onBack={onBack} + /> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx new file mode 100644 index 000000000..445ca3ef0 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx @@ -0,0 +1,120 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatedSuccessfully as TestedComponent } from "./CreatedSuccessfully.js"; +import * as tests from "@gnu-taler/web-util/testing"; + +export default { + title: "Pages/Reserve/CreatedSuccessfully", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onBack: { action: "onBack" }, + }, +}; + +export const OneBankAccount = tests.createExample(TestedComponent, { + entity: { + request: { + exchange_url: "http://exchange.taler/", + initial_balance: "TESTKUDOS:1", + wire_method: "x-taler-bank", + }, + response: { + accounts: [ + { + payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "asd", + conversion_url: "", + }, + ], + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", + }, + }, +}); + +export const ThreeBankAccount = tests.createExample(TestedComponent, { + entity: { + request: { + exchange_url: "http://exchange.taler/", + initial_balance: "TESTKUDOS:1", + wire_method: "x-taler-bank", + }, + response: { + accounts: [ + { + payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "asd", + conversion_url: "", + }, + { + payto_uri: "payto://x-taler-bank/bank1.taler:8080/asd", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "asd", + conversion_url: "", + }, + { + payto_uri: "payto://x-taler-bank/bank2.taler:8080/qwe", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "asd", + conversion_url: "", + }, + ], + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", + }, + }, +}); + +export const NoBankAccount = tests.createExample(TestedComponent, { + entity: { + request: { + exchange_url: "http://exchange.taler/", + initial_balance: "TESTKUDOS:1", + wire_method: "x-taler-bank", + }, + response: { + accounts: [ + { + payto_uri: "payo://x-talr-bank/bank.taler:8080/exchange_account", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "asd", + conversion_url: "", + }, + { + payto_uri: "payto://x-taler-bank", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "asd", + conversion_url: "", + }, + ], + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", + }, + }, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx new file mode 100644 index 000000000..1d512c843 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx @@ -0,0 +1,190 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { QR } from "../../../../components/exception/QR.js"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; +import { MerchantBackend, WireAccount } from "../../../../declaration.js"; + +type Entity = { + request: MerchantBackend.Rewards.ReserveCreateRequest; + response: MerchantBackend.Rewards.ReserveCreateConfirmation; +}; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +function isNotUndefined<X>(x: X | undefined): x is X { + return !!x; +} + +export function CreatedSuccessfully({ + entity, + onConfirm, + onCreateAnother, +}: Props): VNode { + const { i18n } = useTranslationContext(); + return ( + <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Amount</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + readonly + class="input" + value={entity.request.initial_balance} + /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Wire transfer subject</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={entity.response.reserve_pub} + /> + </p> + </div> + </div> + </div> + <ShowAccountsOfReserveAsQRWithLink + accounts={entity.response.accounts ?? []} + message={entity.response.reserve_pub} + amount={entity.request.initial_balance} + /> + </Template> + ); +} + +export function ShowAccountsOfReserveAsQRWithLink({ + accounts, + message, + amount, +}: { + accounts: WireAccount[]; + message: string; + amount: string; +}): VNode { + const { i18n } = useTranslationContext(); + const accountsInfo = !accounts + ? [] + : accounts + .map((acc) => { + const p = parsePaytoUri(acc.payto_uri); + if (p) { + p.params["message"] = message; + p.params["amount"] = amount; + } + return p; + }) + .filter(isNotUndefined); + + const links = accountsInfo.map((a) => stringifyPaytoUri(a)); + + if (links.length === 0) { + return ( + <Fragment> + <p class="is-size-5"> + The reserve have invalid accounts. List of invalid payto URIs below: + </p> + <ul> + {accounts.map((a, idx) => { + return <li key={idx}>{a.payto_uri}</li>; + })} + </ul> + </Fragment> + ); + } + + if (links.length === 1) { + return ( + <Fragment> + <p class="is-size-5"> + <i18n.Translate> + To complete the setup of the reserve, you must now initiate a wire + transfer using the given wire transfer subject and crediting the + specified amount to the indicated account of the exchange. + </i18n.Translate> + </p> + <p style={{ margin: 10 }}> + <b>Exchange bank account</b> + </p> + <QR text={links[0]} /> + <p class="is-size-5"> + <i18n.Translate> + If your system supports RFC 8905, you can do this by opening this + URI: + </i18n.Translate> + </p> + <pre> + <a target="_blank" rel="noreferrer" href={links[0]}> + {links[0]} + </a> + </pre> + </Fragment> + ); + } + + return ( + <div> + <p class="is-size-5"> + <i18n.Translate> + To complete the setup of the reserve, you must now initiate a wire + transfer using the given wire transfer subject and crediting the + specified amount to one of the indicated account of the exchange. + </i18n.Translate> + </p> + + <p style={{ margin: 10 }}> + <b>Exchange bank accounts</b> + </p> + <p class="is-size-5"> + <i18n.Translate> + If your system supports RFC 8905, you can do this by clicking on the + URI below the QR code: + </i18n.Translate> + </p> + {links.map((link) => { + return ( + <Fragment> + <QR text={link} /> + <pre> + <a target="_blank" rel="noreferrer" href={link}> + {link} + </a> + </pre> + </Fragment> + ); + })} + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx new file mode 100644 index 000000000..4bbaf1459 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx @@ -0,0 +1,71 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useReservesAPI } from "../../../../hooks/reserves.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; +import { CreatePage } from "./CreatePage.js"; +interface Props { + onBack: () => void; + onConfirm: () => void; +} +export default function CreateReserve({ onBack, onConfirm }: Props): VNode { + const { createReserve } = useReservesAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + const [createdOk, setCreatedOk] = useState< + | { + request: MerchantBackend.Rewards.ReserveCreateRequest; + response: MerchantBackend.Rewards.ReserveCreateConfirmation; + } + | undefined + >(undefined); + + if (createdOk) { + return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />; + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => { + return createReserve(request) + .then((r) => setCreatedOk({ request, response: r.data })) + .catch((error) => { + setNotif({ + message: i18n.str`could not create reserve`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx new file mode 100644 index 000000000..d8840eeac --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx @@ -0,0 +1,266 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + Amounts, + parsePaytoUri, + stringifyPaytoUri, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { QR } from "../../../../components/exception/QR.js"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDate } from "../../../../components/form/InputDate.js"; +import { TextField } from "../../../../components/form/TextField.js"; +import { SimpleModal } from "../../../../components/modal/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useRewardDetails } from "../../../../hooks/reserves.js"; +import { RewardInfo } from "./RewardInfo.js"; +import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Rewards.ReserveDetail; +type CT = MerchantBackend.ContractTerms; + +interface Props { + onBack: () => void; + selected: Entity; + id: string; +} + +export function DetailPage({ id, selected, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const didExchangeAckTransfer = Amounts.isNonZero( + Amounts.parseOrThrow(selected.exchange_initial_amount), + ); + + return ( + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div class="section main-section"> + <FormProvider object={{ ...selected, id }} valueHandler={null}> + <InputDate<Entity> + name="creation_time" + label={i18n.str`Created at`} + readonly + /> + <InputDate<Entity> + name="expiration_time" + label={i18n.str`Valid until`} + readonly + /> + <InputCurrency<Entity> + name="merchant_initial_amount" + label={i18n.str`Created balance`} + readonly + /> + <TextField<Entity> + name="exchange_url" + label={i18n.str`Exchange URL`} + readonly + > + <a target="_blank" rel="noreferrer" href={selected.exchange_url}> + {selected.exchange_url} + </a> + </TextField> + + {didExchangeAckTransfer && ( + <Fragment> + <InputCurrency<Entity> + name="exchange_initial_amount" + label={i18n.str`Exchange balance`} + readonly + /> + <InputCurrency<Entity> + name="pickup_amount" + label={i18n.str`Picked up`} + readonly + /> + <InputCurrency<Entity> + name="committed_amount" + label={i18n.str`Committed`} + readonly + /> + </Fragment> + )} + <Input name="id" label={i18n.str`Subject`} readonly /> + </FormProvider> + + {didExchangeAckTransfer ? ( + <Fragment> + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-cash-register" /> + </span> + <i18n.Translate>Rewards</i18n.Translate> + </p> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {selected.rewards && selected.rewards.length > 0 ? ( + <Table rewards={selected.rewards} /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + </Fragment> + ) : selected.accounts ? ( + <ShowAccountsOfReserveAsQRWithLink + accounts={selected.accounts} + amount={selected.merchant_initial_amount} + message={id} + /> + ) : undefined} + + <div class="buttons is-right mt-5"> + <button class="button" onClick={onBack}> + <i18n.Translate>Back</i18n.Translate> + </button> + </div> + </div> + </div> + <div class="column" /> + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + No reward has been authorized from this reserve + </i18n.Translate> + </p> + </div> + ); +} + +interface TableProps { + rewards: MerchantBackend.Rewards.RewardStatusEntry[]; +} + +function Table({ rewards }: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Authorized</i18n.Translate> + </th> + <th> + <i18n.Translate>Picked up</i18n.Translate> + </th> + <th> + <i18n.Translate>Reason</i18n.Translate> + </th> + <th> + <i18n.Translate>Expiration</i18n.Translate> + </th> + </tr> + </thead> + <tbody> + {rewards.map((t, i) => { + return <RewardRow id={t.reward_id} key={i} entry={t} />; + })} + </tbody> + </table> + </div> + ); +} + +function RewardRow({ + id, + entry, +}: { + id: string; + entry: MerchantBackend.Rewards.RewardStatusEntry; +}) { + const [selected, setSelected] = useState(false); + const result = useRewardDetails(id); + const [settings] = useSettings(); + if (result.loading) { + return ( + <tr> + <td>...</td> + <td>...</td> + <td>...</td> + <td>...</td> + </tr> + ); + } + if (!result.ok) { + return ( + <tr> + <td>...</td> {/* authorized */} + <td>{entry.total_amount}</td> + <td>{entry.reason}</td> + <td>...</td> {/* expired */} + </tr> + ); + } + const info = result.data; + function onSelect() { + setSelected(true); + } + return ( + <Fragment> + {selected && ( + <SimpleModal + description="reward" + active + onCancel={() => setSelected(false)} + > + <RewardInfo id={id} amount={info.total_authorized} entity={info} /> + </SimpleModal> + )} + <tr> + <td onClick={onSelect}>{info.total_authorized}</td> + <td onClick={onSelect}>{info.total_picked_up}</td> + <td onClick={onSelect}>{info.reason}</td> + <td onClick={onSelect}> + {info.expiration.t_s === "never" + ? "never" + : format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))} + </td> + </tr> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx new file mode 100644 index 000000000..41c715f20 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx @@ -0,0 +1,126 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { DetailPage as TestedComponent } from "./DetailPage.js"; + +export default { + title: "Pages/Reserve/Detail", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Funded = createExample(TestedComponent, { + id: "THISISTHERESERVEID", + selected: { + active: true, + committed_amount: "TESTKUDOS:10", + creation_time: { + t_s: new Date().getTime() / 1000, + }, + exchange_initial_amount: "TESTKUDOS:10", + expiration_time: { + t_s: new Date().getTime() / 1000, + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + accounts: [ + { + payto_uri: "payto://x-taler-bank/bank.taler:8080/account", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "", + }, + ], + exchange_url: "http://exchange.taler/", + }, +}); + +export const NotYetFunded = createExample(TestedComponent, { + id: "THISISTHERESERVEID", + selected: { + active: true, + committed_amount: "TESTKUDOS:10", + creation_time: { + t_s: new Date().getTime() / 1000, + }, + exchange_initial_amount: "TESTKUDOS:0", + expiration_time: { + t_s: new Date().getTime() / 1000, + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + accounts: [ + { + payto_uri: "payto://x-taler-bank/bank.taler:8080/account", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "", + }, + ], + exchange_url: "http://exchange.taler/", + }, +}); + +export const FundedWithEmptyRewards = createExample(TestedComponent, { + id: "THISISTHERESERVEID", + selected: { + active: true, + committed_amount: "TESTKUDOS:10", + creation_time: { + t_s: new Date().getTime() / 1000, + }, + exchange_initial_amount: "TESTKUDOS:10", + expiration_time: { + t_s: new Date().getTime() / 1000, + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + accounts: [ + { + payto_uri: "payto://x-taler-bank/bank.taler:8080/account", + credit_restrictions: [], + debit_restrictions: [], + master_sig: "", + }, + ], + exchange_url: "http://exchange.taler/", + rewards: [ + { + reason: "asdasd", + reward_id: "123", + total_amount: "TESTKUDOS:1", + }, + ], + }, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx new file mode 100644 index 000000000..780068a91 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx @@ -0,0 +1,88 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { stringifyRewardUri } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Rewards.RewardDetails; + +interface Props { + id: string; + entity: Entity; + amount: string; +} + +export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode { + const { url: backendURL } = useBackendContext() + const [settings] = useSettings(); + const rewardURL = stringifyRewardUri({ merchantBaseUrl: backendURL, merchantRewardId }) + return ( + <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Amount</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={amount} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">URL</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field" style={{ overflowWrap: "anywhere" }}> + <p class="control"> + <a target="_blank" rel="noreferrer" href={rewardURL}> + {rewardURL} + </a> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Valid until</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={ + !entity.expiration || entity.expiration.t_s === "never" + ? "never" + : format( + entity.expiration.t_s * 1000, + datetimeFormatForSettings(settings), + ) + } + /> + </p> + </div> + </div> + </div> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx new file mode 100644 index 000000000..8e2a74529 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { Loading } from "../../../../components/exception/loading.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useReserveDetails } from "../../../../hooks/reserves.js"; +import { DetailPage } from "./DetailPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +interface Props { + rid: string; + + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onDelete: () => void; + onBack: () => void; +} +export default function DetailReserve({ + rid, + onUnauthorized, + onLoadError, + onNotFound, + onBack, + onDelete, +}: Props): VNode { + const result = useReserveDetails(rid); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + return ( + <Fragment> + <DetailPage selected={result.data} onBack={onBack} id={rid} /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx new file mode 100644 index 000000000..e205ee621 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx @@ -0,0 +1,124 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import * as yup from "yup"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { + ConfirmModal, + ContinueModal, +} from "../../../../components/modal/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { AuthorizeRewardSchema } from "../../../../schemas/index.js"; +import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; + +interface AuthorizeRewardModalProps { + onCancel: () => void; + onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void; + rewardAuthorized?: { + response: MerchantBackend.Rewards.RewardCreateConfirmation; + request: MerchantBackend.Rewards.RewardCreateRequest; + }; +} + +export function AuthorizeRewardModal({ + onCancel, + onConfirm, + rewardAuthorized, +}: AuthorizeRewardModalProps): VNode { + // const result = useOrderDetails(id) + type State = MerchantBackend.Rewards.RewardCreateRequest; + const [form, setValue] = useState<Partial<State>>({}); + const { i18n } = useTranslationContext(); + + // const [errors, setErrors] = useState<FormErrors<State>>({}) + let errors: FormErrors<State> = {}; + try { + AuthorizeRewardSchema.validateSync(form, { abortEarly: false }); + } catch (err) { + if (err instanceof yup.ValidationError) { + const yupErrors = err.inner as any[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); + } + } + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const validateAndConfirm = () => { + onConfirm(form as State); + }; + if (rewardAuthorized) { + return ( + <ContinueModal description="reward" active onConfirm={onCancel}> + <CreatedSuccessfully + entity={rewardAuthorized.response} + request={rewardAuthorized.request} + onConfirm={onCancel} + /> + </ContinueModal> + ); + } + + return ( + <ConfirmModal + description="New reward" + active + onCancel={onCancel} + disabled={hasErrors} + onConfirm={validateAndConfirm} + > + <FormProvider<State> + errors={errors} + object={form} + valueHandler={setValue} + > + <InputCurrency<State> + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`amount of reward`} + /> + <Input<State> + name="justification" + label={i18n.str`Justification`} + inputType="multiline" + tooltip={i18n.str`reason for the reward`} + /> + <Input<State> + name="next_url" + label={i18n.str`URL after reward`} + tooltip={i18n.str`URL to visit after reward payment`} + /> + </FormProvider> + </ConfirmModal> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx new file mode 100644 index 000000000..b78236bc7 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx @@ -0,0 +1,102 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { format } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Rewards.RewardCreateConfirmation; + +interface Props { + entity: Entity; + request: MerchantBackend.Rewards.RewardCreateRequest; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function CreatedSuccessfully({ + request, + entity, + onConfirm, + onCreateAnother, +}: Props): VNode { + const [settings] = useSettings(); + return ( + <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Amount</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={request.amount} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Justification</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={request.justification} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">URL</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={entity.reward_status_url} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Valid until</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input + class="input" + readonly + value={ + !entity.reward_expiration || + entity.reward_expiration.t_s === "never" + ? "never" + : format( + entity.reward_expiration.t_s * 1000, + datetimeFormatForSettings(settings), + ) + } + /> + </p> + </div> + </div> + </div> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx new file mode 100644 index 000000000..b070bbde3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx @@ -0,0 +1,96 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CardTable as TestedComponent } from "./Table.js"; + +export default { + title: "Pages/Reserve/List", + component: TestedComponent, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const AllFunded = createExample(TestedComponent, { + instances: [ + { + id: "reseverId", + active: true, + committed_amount: "TESTKUDOS:10", + creation_time: { + t_s: new Date().getTime() / 1000, + }, + exchange_initial_amount: "TESTKUDOS:10", + expiration_time: { + t_s: new Date().getTime() / 1000, + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", + }, + { + id: "reseverId2", + active: true, + committed_amount: "TESTKUDOS:13", + creation_time: { + t_s: new Date().getTime() / 1000, + }, + exchange_initial_amount: "TESTKUDOS:10", + expiration_time: { + t_s: new Date().getTime() / 1000, + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", + }, + ], +}); + +export const Empty = createExample(TestedComponent, { + instances: [], +}); + +export const OneNotYetFunded = createExample(TestedComponent, { + instances: [ + { + id: "reseverId", + active: true, + committed_amount: "TESTKUDOS:0", + creation_time: { + t_s: new Date().getTime() / 1000, + }, + exchange_initial_amount: "TESTKUDOS:0", + expiration_time: { + t_s: new Date().getTime() / 1000, + }, + merchant_initial_amount: "TESTKUDOS:10", + pickup_amount: "TESTKUDOS:10", + reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", + }, + ], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx new file mode 100644 index 000000000..795e7ec82 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx @@ -0,0 +1,320 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId; + +interface Props { + instances: Entity[]; + onNewReward: (id: Entity) => void; + onSelect: (id: Entity) => void; + onDelete: (id: Entity) => void; + onCreate: () => void; +} + +export function CardTable({ + instances, + onCreate, + onSelect, + onNewReward, + onDelete, +}: Props): VNode { + const [withoutFunds, withFunds] = instances.reduce((prev, current) => { + const amount = current.exchange_initial_amount; + if (amount.endsWith(":0")) { + prev[0] = prev[0].concat(current); + } else { + prev[1] = prev[1].concat(current); + } + return prev; + }, new Array<Array<Entity>>([], [])); + + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + {withoutFunds.length > 0 && ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-cash" /> + </span> + <i18n.Translate>Reserves not yet funded</i18n.Translate> + </p> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + <TableWithoutFund + instances={withoutFunds} + onNewReward={onNewReward} + onSelect={onSelect} + onDelete={onDelete} + /> + </div> + </div> + </div> + </div> + )} + + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-cash" /> + </span> + <i18n.Translate>Reserves ready</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options" /> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new reserve`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {withFunds.length > 0 ? ( + <Table + instances={withFunds} + onNewReward={onNewReward} + onSelect={onSelect} + onDelete={onDelete} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + </Fragment> + ); +} +interface TableProps { + instances: Entity[]; + onNewReward: (id: Entity) => void; + onDelete: (id: Entity) => void; + onSelect: (id: Entity) => void; +} + +function Table({ instances, onNewReward, onSelect, onDelete }: TableProps): VNode { + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Created at</i18n.Translate> + </th> + <th> + <i18n.Translate>Expires at</i18n.Translate> + </th> + <th> + <i18n.Translate>Initial</i18n.Translate> + </th> + <th> + <i18n.Translate>Picked up</i18n.Translate> + </th> + <th> + <i18n.Translate>Committed</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.creation_time.t_s === "never" + ? "never" + : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.expiration_time.t_s === "never" + ? "never" + : format( + i.expiration_time.t_s * 1000, + datetimeFormatForSettings(settings), + )} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.exchange_initial_amount} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.pickup_amount} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.committed_amount} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-small is-danger has-tooltip-left" + data-tooltip={i18n.str`delete selected reserve from the database`} + type="button" + onClick={(): void => onDelete(i)} + > + Delete + </button> + <button + class="button is-small is-info has-tooltip-left" + data-tooltip={i18n.str`authorize new reward from selected reserve`} + type="button" + onClick={(): void => onNewReward(i)} + > + New Reward + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no ready reserves yet, add more pressing the + sign or fund + them + </i18n.Translate> + </p> + </div> + ); +} + +function TableWithoutFund({ + instances, + onSelect, + onDelete, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Created at</i18n.Translate> + </th> + <th> + <i18n.Translate>Expires at</i18n.Translate> + </th> + <th> + <i18n.Translate>Expected Balance</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.creation_time.t_s === "never" + ? "never" + : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.expiration_time.t_s === "never" + ? "never" + : format( + i.expiration_time.t_s * 1000, + datetimeFormatForSettings(settings), + )} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.merchant_initial_amount} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-small is-danger jb-modal has-tooltip-left" + type="button" + data-tooltip={i18n.str`delete selected reserve from the database`} + onClick={(): void => onDelete(i)} + > + Delete + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx new file mode 100644 index 000000000..b26ff0000 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx @@ -0,0 +1,171 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + useInstanceReserves, + useReservesAPI, +} from "../../../../hooks/reserves.js"; +import { Notification } from "../../../../utils/types.js"; +import { AuthorizeRewardModal } from "./AutorizeRewardModal.js"; +import { CardTable } from "./Table.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConfirmModal } from "../../../../components/modal/index.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onSelect: (id: string) => void; + onNotFound: () => VNode; + onCreate: () => void; +} + +interface RewardConfirmation { + response: MerchantBackend.Rewards.RewardCreateConfirmation; + request: MerchantBackend.Rewards.RewardCreateRequest; +} + +export default function ListRewards({ + onUnauthorized, + onLoadError, + onNotFound, + onSelect, + onCreate, +}: Props): VNode { + const result = useInstanceReserves(); + const { deleteReserve, authorizeRewardReserve } = useReservesAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [reserveForReward, setReserveForReward] = useState<string | undefined>( + undefined, + ); + const [deleting, setDeleting] = + useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null); + const [rewardAuthorized, setRewardAuthorized] = useState< + RewardConfirmation | undefined + >(undefined); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + + {reserveForReward && ( + <AuthorizeRewardModal + onCancel={() => { + setReserveForReward(undefined); + setRewardAuthorized(undefined); + }} + rewardAuthorized={rewardAuthorized} + onConfirm={async (request) => { + try { + const response = await authorizeRewardReserve( + reserveForReward, + request, + ); + setRewardAuthorized({ + request, + response: response.data, + }); + } catch (error) { + setNotif({ + message: i18n.str`could not create the reward`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + setReserveForReward(undefined); + } + }} + /> + )} + + <CardTable + instances={result.data.reserves + .filter((r) => r.active) + .map((o) => ({ ...o, id: o.reserve_pub }))} + onCreate={onCreate} + onDelete={(reserve) => { + setDeleting(reserve) + }} + onSelect={(reserve) => onSelect(reserve.id)} + onNewReward={(reserve) => setReserveForReward(reserve.id)} + /> + + {deleting && ( + <ConfirmModal + label={`Delete reserve`} + description={`Delete the reserve`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteReserve(deleting.reserve_pub); + setNotif({ + message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to delete reserve`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the reserve for <b>"{deleting.merchant_initial_amount}"</b> you won't be able to create more rewards. <br /> + Reserve ID: <b>{deleting.reserve_pub}</b> + </p> + <p class="warning"> + Deleting an template <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} + + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx new file mode 100644 index 000000000..c9d17ea3b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Templates/Create", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx new file mode 100644 index 000000000..947f3572c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -0,0 +1,259 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + Amounts, + MerchantTemplateContractDetails, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { InputTab } from "../../../../components/form/InputTab.js"; + +enum Steps { + BOTH_FIXED, + FIXED_PRICE, + FIXED_SUMMARY, + NON_FIXED, +} + +type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps }; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { url: backendURL } = useBackendContext() + const devices = useInstanceOtpDevices() + + const [state, setState] = useState<Partial<Entity>>({ + template_contract: { + minimum_age: 0, + pay_duration: { + d_us: 1000 * 1000 * 60 * 30, //30 min + }, + }, + type: Steps.NON_FIXED, + }); + + const parsedPrice = !state.template_contract?.amount + ? undefined + : Amounts.parse(state.template_contract?.amount); + + const errors: FormErrors<Entity> = { + template_id: !state.template_id + ? i18n.str`should not be empty` + : !/[a-zA-Z0-9]*/.test(state.template_id) + ? i18n.str`no valid. only characters and numbers` + : undefined, + template_description: !state.template_description + ? i18n.str`should not be empty` + : undefined, + template_contract: !state.template_contract + ? undefined + : undefinedIfEmpty({ + amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) + ? undefined + : !state.template_contract?.amount + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) + ? undefined + : !state.template_contract?.summary + ? i18n.str`required` + : undefined, + minimum_age: + state.template_contract.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.template_contract.pay_duration + ? i18n.str`can't be empty` + : state.template_contract.pay_duration.d_us === "forever" + ? undefined + : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second + ? i18n.str`to short` + : undefined, + } as Partial<MerchantTemplateContractDetails>), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + if (state.template_contract) { + if (state.type === Steps.NON_FIXED) { + delete state.template_contract.amount; + delete state.template_contract.summary; + } else if (state.type === Steps.FIXED_SUMMARY) { + delete state.template_contract.amount; + } else if (state.type === Steps.FIXED_PRICE) { + delete state.template_contract.summary; + } + } + delete state.type + return onCreate(state as any); + }; + + const deviceList = !devices.ok ? [] : devices.data.otp_devices + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputWithAddon<Entity> + name="template_id" + help={`${backendURL}/templates/${state.template_id ?? ""}`} + label={i18n.str`Identifier`} + tooltip={i18n.str`Name of the template in URLs.`} + /> + <Input<Entity> + name="template_description" + label={i18n.str`Description`} + help="" + tooltip={i18n.str`Describe what this template stands for`} + /> + <InputTab + name="type" + label={i18n.str`Type`} + help={(() => { + switch (state.type) { + case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` + case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` + case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` + case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` + } + })()} + tooltip={i18n.str`Define what the user be allowed to modify`} + values={[ + Steps.NON_FIXED, + Steps.FIXED_PRICE, + Steps.FIXED_SUMMARY, + Steps.BOTH_FIXED, + ]} + toStr={(v: Steps): string => { + switch (v) { + case Steps.NON_FIXED: return i18n.str`Simple` + case Steps.FIXED_PRICE: return i18n.str`With price` + case Steps.FIXED_SUMMARY: return i18n.str`With summary` + case Steps.BOTH_FIXED: return i18n.str`With price and summary` + } + }} + /> + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? + <Input + name="template_contract.summary" + inputType="multiline" + label={i18n.str`Fixed summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} + /> + : undefined} + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + <InputCurrency + name="template_contract.amount" + label={i18n.str`Fixed price`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + : undefined} + <InputNumber + name="template_contract.minimum_age" + label={i18n.str`Minimum age`} + help="" + tooltip={i18n.str`Is this contract restricted to some age?`} + /> + <InputDuration + name="template_contract.pay_duration" + label={i18n.str`Payment timeout`} + help="" + tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} + /> + <Input<Entity> + name="otp_id" + label={i18n.str`OTP device`} + readonly + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + <InputSearchOnList + label={i18n.str`Search device`} + onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))} + list={deviceList.map(e => ({ + description: e.device_description, + id: e.otp_device_id + }))} + /> + + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx new file mode 100644 index 000000000..a29ee53b6 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useTemplateAPI } from "../../../../hooks/templates.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; + +export type Entity = MerchantBackend.Transfers.TransferInformation; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { + const { createTemplate } = useTemplateAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => { + return createTemplate(request) + .then(() => onConfirm()) + .catch((error) => { + setNotif({ + message: i18n.str`could not inform template`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx new file mode 100644 index 000000000..702e9ba4a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { FunctionalComponent, h } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Templates/List", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx new file mode 100644 index 000000000..bf6062c34 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { CardTable } from "./Table.js"; + +export interface Props { + templates: MerchantBackend.Template.TemplateEntry[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; + onCreate: () => void; + onDelete: (e: MerchantBackend.Template.TemplateEntry) => void; + onSelect: (e: MerchantBackend.Template.TemplateEntry) => void; + onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void; + onQR: (e: MerchantBackend.Template.TemplateEntry) => void; +} + +export function ListPage({ + templates, + onCreate, + onDelete, + onSelect, + onNewOrder, + onQR, + onLoadMoreBefore, + onLoadMoreAfter, +}: Props): VNode { + const form = { payto_uri: "" }; + + const { i18n } = useTranslationContext(); + return ( + <CardTable + templates={templates.map((o) => ({ + ...o, + id: String(o.template_id), + }))} + onQR={onQR} + onCreate={onCreate} + onDelete={onDelete} + onSelect={onSelect} + onNewOrder={onNewOrder} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + hasMoreAfter={!onLoadMoreAfter} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx new file mode 100644 index 000000000..9fdf4ead9 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -0,0 +1,235 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.Template.TemplateEntry; + +interface Props { + templates: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + onNewOrder: (e: Entity) => void; + onQR: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + templates, + onCreate, + onDelete, + onSelect, + onQR, + onNewOrder, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <i18n.Translate>Templates</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new templates`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {templates.length > 0 ? ( + <Table + instances={templates} + onDelete={onDelete} + onSelect={onSelect} + onNewOrder={onNewOrder} + onQR={onQR} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: (e: Entity) => void; + onNewOrder: (e: Entity) => void; + onQR: (e: Entity) => void; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onNewOrder, + onQR, + onSelect, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {hasMoreBefore && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more templates before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>load newer templates</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.template_id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.template_id} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.template_description} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected templates from the database`} + onClick={() => onDelete(i)} + > + Delete + </button> + <button + class="button is-info is-small has-tooltip-left" + data-tooltip={i18n.str`use template to create new order`} + onClick={() => onNewOrder(i)} + > + Use template + </button> + <button + class="button is-info is-small has-tooltip-left" + data-tooltip={i18n.str`create qr code for the template`} + onClick={() => onQR(i)} + > + QR + </button> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {hasMoreAfter && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more templates after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>load older templates</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no templates yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx new file mode 100644 index 000000000..b9767442f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -0,0 +1,152 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + useInstanceTemplates, + useTemplateAPI, +} from "../../../../hooks/templates.js"; +import { Notification } from "../../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { ConfirmModal } from "../../../../components/modal/index.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; + onNewOrder: (id: string) => void; + onQR: (id: string) => void; +} + +export default function ListTemplates({ + onUnauthorized, + onLoadError, + onCreate, + onQR, + onSelect, + onNewOrder, + onNotFound, +}: Props): VNode { + const [position, setPosition] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { deleteTemplate, testTemplateExist } = useTemplateAPI(); + const result = useInstanceTemplates({ position }, (id) => setPosition(id)); + const [deleting, setDeleting] = + useState<MerchantBackend.Template.TemplateEntry | null>(null); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + + <JumpToElementById + testIfExist={testTemplateExist} + onSelect={onSelect} + description={i18n.str`jump to template with the given template ID`} + palceholder={i18n.str`template id`} + /> + + <ListPage + templates={result.data.templates} + onLoadMoreBefore={ + result.isReachingStart ? result.loadMorePrev : undefined + } + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.template_id); + }} + onNewOrder={(e) => { + onNewOrder(e.template_id); + }} + onQR={(e) => { + onQR(e.template_id); + }} + onDelete={(e: MerchantBackend.Template.TemplateEntry) => { + setDeleting(e) + } + } + /> + + {deleting && ( + <ConfirmModal + label={`Delete template`} + description={`Delete the template "${deleting.template_description}"`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteTemplate(deleting.template_id); + setNotif({ + message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to delete template`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the template <b>"{deleting.template_description}"</b> (ID:{" "} + <b>{deleting.template_id}</b>) you may loose information + </p> + <p class="warning"> + Deleting an template <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx new file mode 100644 index 000000000..eb853c8ff --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx @@ -0,0 +1,27 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { QrPage as TestedComponent } from "./QrPage.js"; + +export default { + title: "Pages/Templates/QR", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx new file mode 100644 index 000000000..5140aae3a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -0,0 +1,172 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { QR } from "../../../../components/exception/QR.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { useConfigContext } from "../../../../context/config.js"; +import { useInstanceContext } from "../../../../context/instance.js"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.Template.UsingTemplateDetails; + +interface Props { + contract: MerchantBackend.Template.TemplateContractDetails; + id: string; + onBack?: () => void; +} + +export function QrPage({ contract, id: templateId, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { url: backendURL } = useBackendContext() + const { id: instanceId } = useInstanceContext(); + const config = useConfigContext(); + + const [state, setState] = useState<Partial<Entity>>({ + amount: contract.amount, + summary: contract.summary, + }); + + const errors: FormErrors<Entity> = {}; + + const fixedAmount = !!contract.amount; + const fixedSummary = !!contract.summary; + + const templateParams: Record<string, string> = {} + if (!fixedAmount) { + if (state.amount) { + templateParams.amount = state.amount + } else { + templateParams.amount = config.currency + } + } + + if (!fixedSummary) { + templateParams.summary = state.summary ?? "" + } + + const merchantBaseUrl = new URL(backendURL).href; + + const payTemplateUri = stringifyPayTemplateUri({ + merchantBaseUrl, + templateId, + templateParams + }) + + const issuer = encodeURIComponent( + `${new URL(backendURL).host}/${instanceId}`, + ); + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <p class="is-size-5 mt-5 mb-5"> + <i18n.Translate> + Here you can specify a default value for fields that are not + fixed. Default values can be edited by the customer before the + payment. + </i18n.Translate> + </p> + + <p></p> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputCurrency<Entity> + name="amount" + label={ + fixedAmount + ? i18n.str`Fixed amount` + : i18n.str`Default amount` + } + readonly={fixedAmount} + tooltip={i18n.str`Amount of the order`} + /> + <Input<Entity> + name="summary" + inputType="multiline" + readonly={fixedSummary} + label={ + fixedSummary + ? i18n.str`Fixed summary` + : i18n.str`Default summary` + } + tooltip={i18n.str`Title of the order to be shown to the customer`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <button + class="button is-info" + onClick={() => saveAsPDF(templateId)} + > + <i18n.Translate>Print</i18n.Translate> + </button> + </div> + </div> + <div class="column" /> + </div> + </section> + <section id="printThis"> + <QR text={payTemplateUri} /> + <pre style={{ textAlign: "center" }}> + <a href={payTemplateUri}>{payTemplateUri}</a> + </pre> + </section> + </div> + ); +} + +function saveAsPDF(name: string): void { + const printWindow = window.open("", "", "height=400,width=800"); + if (!printWindow) return; + const divContents = document.getElementById("printThis"); + if (!divContents) return; + printWindow.document.write( + `<html><head><title>Order template for ${name}</title><style>`, + ); + printWindow.document.write("</style></head><body> </body></html>"); + printWindow.document.close(); + printWindow.document.body.appendChild(divContents.cloneNode(true)); + printWindow.addEventListener("load", () => { + printWindow.print(); + printWindow.close(); + }); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx new file mode 100644 index 000000000..7db7478f7 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + useTemplateAPI, + useTemplateDetails, +} from "../../../../hooks/templates.js"; +import { Notification } from "../../../../utils/types.js"; +import { QrPage } from "./QrPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export type Entity = MerchantBackend.Transfers.TransferInformation; +interface Props { + onBack?: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + tid: string; +} + +export default function TemplateQrPage({ + tid, + onBack, + onLoadError, + onNotFound, + onUnauthorized, +}: Props): VNode { + const result = useTemplateDetails(tid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <> + <NotificationCard notification={notif} /> + <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx new file mode 100644 index 000000000..8d07cb31f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx @@ -0,0 +1,32 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/Templates/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx new file mode 100644 index 000000000..b578d4664 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -0,0 +1,254 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + Amounts, + MerchantTemplateContractDetails, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { InputTab } from "../../../../components/form/InputTab.js"; + +enum Steps { + BOTH_FIXED, + FIXED_PRICE, + FIXED_SUMMARY, + NON_FIXED, +} + +type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + template: Entity; +} + +export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { url: backendURL } = useBackendContext() + + const intialStep = + template.template_contract?.amount === undefined && template.template_contract?.summary === undefined + ? Steps.NON_FIXED + : template.template_contract?.summary === undefined + ? Steps.FIXED_PRICE + : template.template_contract?.amount === undefined + ? Steps.FIXED_SUMMARY + : Steps.BOTH_FIXED; + + const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ ...template, type: intialStep }); + + const parsedPrice = !state.template_contract?.amount + ? undefined + : Amounts.parse(state.template_contract?.amount); + + const errors: FormErrors<Entity> = { + template_description: !state.template_description + ? i18n.str`should not be empty` + : undefined, + template_contract: !state.template_contract + ? undefined + : undefinedIfEmpty({ + amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) + ? undefined + : !state.template_contract?.amount + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) + ? undefined + : !state.template_contract?.summary + ? i18n.str`required` + : undefined, + minimum_age: + state.template_contract.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.template_contract.pay_duration + ? i18n.str`can't be empty` + : state.template_contract.pay_duration.d_us === "forever" + ? undefined + : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second + ? i18n.str`to short` + : undefined, + } as Partial<MerchantTemplateContractDetails>), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + if (state.template_contract) { + if (state.type === Steps.NON_FIXED) { + delete state.template_contract.amount; + delete state.template_contract.summary; + } else if (state.type === Steps.FIXED_SUMMARY) { + delete state.template_contract.amount; + } else if (state.type === Steps.FIXED_PRICE) { + delete state.template_contract.summary; + } + } + delete state.type + return onUpdate(state as any); + }; + + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + {backendURL}/templates/{template.id} + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputWithAddon<Entity> + name="id" + addonBefore={`templates/`} + readonly + label={i18n.str`Identifier`} + tooltip={i18n.str`Name of the template in URLs.`} + /> + + <Input<Entity> + name="template_description" + label={i18n.str`Description`} + help="" + tooltip={i18n.str`Describe what this template stands for`} + /> + <InputTab + name="type" + label={i18n.str`Type`} + help={(() => { + switch (state.type) { + case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` + case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` + case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` + case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` + } + })()} + tooltip={i18n.str`Define what the user be allowed to modify`} + values={[ + Steps.NON_FIXED, + Steps.FIXED_PRICE, + Steps.FIXED_SUMMARY, + Steps.BOTH_FIXED, + ]} + toStr={(v: Steps): string => { + switch (v) { + case Steps.NON_FIXED: return i18n.str`Simple` + case Steps.FIXED_PRICE: return i18n.str`With price` + case Steps.FIXED_SUMMARY: return i18n.str`With summary` + case Steps.BOTH_FIXED: return i18n.str`With price and summary` + } + }} + /> + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? + <Input + name="template_contract.summary" + inputType="multiline" + label={i18n.str`Fixed summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} + /> + : undefined} + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + <InputCurrency + name="template_contract.amount" + label={i18n.str`Fixed price`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + : undefined} + <InputNumber + name="template_contract.minimum_age" + label={i18n.str`Minimum age`} + help="" + tooltip={i18n.str`Is this contract restricted to some age?`} + /> + <InputDuration + name="template_contract.pay_duration" + label={i18n.str`Payment timeout`} + help="" + tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx new file mode 100644 index 000000000..3adca45db --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -0,0 +1,99 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { + useTemplateAPI, + useTemplateDetails, +} from "../../../../hooks/templates.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + tid: string; +} +export default function UpdateTemplate({ + tid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateTemplate } = useTemplateAPI(); + const result = useTemplateDetails(tid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + template={{ ...result.data, id: tid }} + onBack={onBack} + onUpdate={(data) => { + return updateTemplate(tid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not update template`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx new file mode 100644 index 000000000..13576d94d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx @@ -0,0 +1,27 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { UsePage as TestedComponent } from "./UsePage.js"; + +export default { + title: "Pages/Templates/Create", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx new file mode 100644 index 000000000..983804d3e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -0,0 +1,143 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.Template.UsingTemplateDetails; + +interface Props { + id: string; + template: MerchantBackend.Template.TemplateDetails; + onCreateOrder: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({ + amount: template.template_contract.amount, + summary: template.template_contract.summary, + }); + + const errors: FormErrors<Entity> = { + amount: + !template.template_contract.amount && !state.amount + ? i18n.str`Amount is required` + : undefined, + summary: + !template.template_contract.summary && !state.summary + ? i18n.str`Order summary is required` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + if (template.template_contract.amount) { + delete state.amount; + } + if (template.template_contract.summary) { + delete state.summary; + } + return onCreateOrder(state as any); + }; + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>New order for template</i18n.Translate>:{" "} + <b>{id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + </section> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + readonly={!!template.template_contract.amount} + tooltip={i18n.str`Amount of the order`} + /> + <Input<Entity> + name="summary" + inputType="multiline" + label={i18n.str`Order summary`} + readonly={!!template.template_contract.summary} + tooltip={i18n.str`Title of the order to be shown to the customer`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx new file mode 100644 index 000000000..ed1242ef5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -0,0 +1,101 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + useTemplateAPI, + useTemplateDetails, +} from "../../../../hooks/templates.js"; +import { Notification } from "../../../../utils/types.js"; +import { UsePage } from "./UsePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export type Entity = MerchantBackend.Transfers.TransferInformation; +interface Props { + onBack?: () => void; + onOrderCreated: (id: string) => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + tid: string; +} + +export default function TemplateUsePage({ + tid, + onOrderCreated, + onBack, + onLoadError, + onNotFound, + onUnauthorized, +}: Props): VNode { + const { createOrderFromTemplate } = useTemplateAPI(); + const result = useTemplateDetails(tid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <> + <NotificationCard notification={notif} /> + <UsePage + template={result.data} + id={tid} + onBack={onBack} + onCreateOrder={( + request: MerchantBackend.Template.UsingTemplateDetails, + ) => { + return createOrderFromTemplate(tid, request) + .then((res) => onOrderCreated(res.data.order_id)) + .catch((error) => { + setNotif({ + message: i18n.str`could not create order from template`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx new file mode 100644 index 000000000..d22a9e4d4 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -0,0 +1,183 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../components/exception/AsyncButton.js"; +import { FormProvider } from "../../../components/form/FormProvider.js"; +import { Input } from "../../../components/form/Input.js"; +import { useInstanceContext } from "../../../context/instance.js"; +import { AccessToken } from "../../../declaration.js"; +import { NotificationCard } from "../../../components/menu/index.js"; + +interface Props { + instanceId: string; + hasToken: boolean | undefined; + onClearToken: (c: AccessToken | undefined) => void; + onNewToken: (c: AccessToken | undefined, s: AccessToken) => void; + onBack?: () => void; +} + +export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; + const [form, setValue] = useState<Partial<State>>({ + old_token: "", + new_token: "", + repeat_token: "", + }); + const { i18n } = useTranslationContext(); + + const errors = { + old_token: hasToken && !form.old_token + ? i18n.str`you need your access token to perform the operation` + : undefined, + new_token: !form.new_token + ? i18n.str`cannot be empty` + : form.new_token === form.old_token + ? i18n.str`cannot be the same as the old token` + : undefined, + repeat_token: + form.new_token !== form.repeat_token + ? i18n.str`is not the same` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const instance = useInstanceContext(); + + const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`; + + async function submitForm() { + if (hasErrors) return; + const ot = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined; + const nt = `secret-token:${form.new_token}` as AccessToken; + onNewToken(ot, nt) + } + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + {text} + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + {!hasToken && + <NotificationCard + notification={{ + message: i18n.str`This instance doesn't have authentication token.`, + description: i18n.str`You can leave it empty if there is another layer of security.`, + type: "WARN", + }} + /> + } + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider errors={errors} object={form} valueHandler={setValue}> + <Fragment> + {hasToken && ( + <Fragment> + <Input<State> + name="old_token" + label={i18n.str`Current access token`} + tooltip={i18n.str`access token currently in use`} + inputType="password" + /> + <p> + <i18n.Translate> + Clearing the access token will mean public access to the instance. + </i18n.Translate> + </p> + <div class="buttons is-right mt-5"> + <button + class="button" + onClick={() => { + if (hasToken) { + const ot = `secret-token:${form.old_token}` as AccessToken; + onClearToken(ot) + } else { + onClearToken(undefined) + } + }} + > + <i18n.Translate>Clear token</i18n.Translate> + </button> + </div> + </Fragment> + )} + + + <Input<State> + name="new_token" + label={i18n.str`New access token`} + tooltip={i18n.str`next access token to be used`} + inputType="password" + /> + <Input<State> + name="repeat_token" + label={i18n.str`Repeat access token`} + tooltip={i18n.str`confirm the same access token`} + inputType="password" + /> + </Fragment> + </FormProvider> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm change</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx new file mode 100644 index 000000000..22365c9e1 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx @@ -0,0 +1,106 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { Loading } from "../../../components/exception/loading.js"; +import { AccessToken, MerchantBackend } from "../../../declaration.js"; +import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; +import { DetailPage } from "./DetailPage.js"; +import { useInstanceContext } from "../../../context/instance.js"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { Notification } from "../../../utils/types.js"; +import { useBackendContext } from "../../../context/backend.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onChange: () => void; + onNotFound: () => VNode; + onCancel: () => void; +} + +export default function Token({ + onLoadError, + onChange, + onUnauthorized, + onNotFound, + onCancel, +}: Props): VNode { + const { i18n } = useTranslationContext(); + + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { clearAccessToken, setNewAccessToken } = useInstanceAPI(); + const { id } = useInstanceContext(); + const result = useInstanceDetails() + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + const hasToken = result.data.auth.method === "token" + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <DetailPage + instanceId={id} + onBack={onCancel} + hasToken={hasToken} + onClearToken={async (currentToken): Promise<void> => { + try { + await clearAccessToken(currentToken); + onChange(); + } catch (error) { + if (error instanceof Error) { + setNotif({ + message: i18n.str`Failed to clear token`, + type: "ERROR", + description: error.message, + }); + } + } + }} + onNewToken={async (currentToken, newToken): Promise<void> => { + try { + await setNewAccessToken(currentToken, newToken); + onChange(); + } catch (error) { + if (error instanceof Error) { + setNotif({ + message: i18n.str`Failed to set new token`, + type: "ERROR", + description: error.message, + }); + } + } + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx new file mode 100644 index 000000000..5f0f56f2d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { DetailPage as TestedComponent } from "./DetailPage.js"; + +export default { + title: "Pages/Token", + component: TestedComponent, +}; + diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx new file mode 100644 index 000000000..64b67335c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx @@ -0,0 +1,45 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Transfer/Create", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + accounts: ["payto://x-taler-bank/account1", "payto://x-taler-bank/account2"], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx new file mode 100644 index 000000000..13f5f3c12 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -0,0 +1,146 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { useConfigContext } from "../../../../context/config.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + CROCKFORD_BASE32_REGEX, + URL_REGEX, +} from "../../../../utils/constants.js"; + +type Entity = MerchantBackend.Transfers.TransferInformation; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; + accounts: string[]; +} + +export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { currency } = useConfigContext(); + + const [state, setState] = useState<Partial<Entity>>({ + wtid: "", + // payto_uri: , + // exchange_url: 'http://exchange.taler:8081/', + credit_amount: ``, + }); + + const errors: FormErrors<Entity> = { + wtid: !state.wtid + ? i18n.str`cannot be empty` + : !CROCKFORD_BASE32_REGEX.test(state.wtid) + ? i18n.str`check the id, does not look valid` + : state.wtid.length !== 52 + ? i18n.str`should have 52 characters, current ${state.wtid.length}` + : undefined, + payto_uri: !state.payto_uri ? i18n.str`cannot be empty` : undefined, + credit_amount: !state.credit_amount ? i18n.str`cannot be empty` : undefined, + exchange_url: !state.exchange_url + ? i18n.str`cannot be empty` + : !URL_REGEX.test(state.exchange_url) + ? i18n.str`URL doesn't have the right format` + : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onCreate(state as any); + }; + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <InputSelector + name="payto_uri" + label={i18n.str`Credited bank account`} + values={accounts} + placeholder={i18n.str`Select one account`} + tooltip={i18n.str`Bank account of the merchant where the payment was received`} + /> + <Input<Entity> + name="wtid" + label={i18n.str`Wire transfer ID`} + help="" + tooltip={i18n.str`unique identifier of the wire transfer used by the exchange, must be 52 characters long`} + /> + <Input<Entity> + name="exchange_url" + label={i18n.str`Exchange URL`} + tooltip={i18n.str`Base URL of the exchange that made the transfer, should have been in the wire transfer subject`} + help="http://exchange.taler:8081/" + /> + <InputCurrency<Entity> + name="credit_amount" + label={i18n.str`Amount credited`} + tooltip={i18n.str`Actual amount that was wired to the merchant's bank account`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx new file mode 100644 index 000000000..25551a031 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceDetails } from "../../../../hooks/instance.js"; +import { useTransferAPI } from "../../../../hooks/transfer.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js"; + +export type Entity = MerchantBackend.Transfers.TransferInformation; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { + const { informTransfer } = useTransferAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const instance = useInstanceBankAccounts(); + const accounts = !instance.ok + ? [] + : instance.data.accounts.map((a) => a.payto_uri); + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + accounts={accounts} + onCreate={(request: MerchantBackend.Transfers.TransferInformation) => { + return informTransfer(request) + .then(() => onConfirm()) + .catch((error) => { + setNotif({ + message: i18n.str`could not inform transfer`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx new file mode 100644 index 000000000..92b3f9853 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx @@ -0,0 +1,93 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Transfer/List", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onDelete: { action: "onDelete" }, + onLoadMoreBefore: { action: "onLoadMoreBefore" }, + onLoadMoreAfter: { action: "onLoadMoreAfter" }, + onShowAll: { action: "onShowAll" }, + onShowVerified: { action: "onShowVerified" }, + onShowUnverified: { action: "onShowUnverified" }, + onChangePayTo: { action: "onChangePayTo" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + transfers: [ + { + exchange_url: "http://exchange.url/", + credit_amount: "TESTKUDOS:10", + payto_uri: "payto//x-taler-bank/bank:8080/account", + transfer_serial_id: 123123123, + wtid: "!@KJELQKWEJ!L@K#!J@", + confirmed: true, + execution_time: { + t_s: new Date().getTime() / 1000, + }, + verified: false, + }, + { + exchange_url: "http://exchange.url/", + credit_amount: "TESTKUDOS:10", + payto_uri: "payto//x-taler-bank/bank:8080/account", + transfer_serial_id: 123123123, + wtid: "!@KJELQKWEJ!L@K#!J@", + confirmed: true, + execution_time: { + t_s: new Date().getTime() / 1000, + }, + verified: false, + }, + { + exchange_url: "http://exchange.url/", + credit_amount: "TESTKUDOS:10", + payto_uri: "payto//x-taler-bank/bank:8080/account", + transfer_serial_id: 123123123, + wtid: "!@KJELQKWEJ!L@K#!J@", + confirmed: true, + execution_time: { + t_s: new Date().getTime() / 1000, + }, + verified: false, + }, + ], + accounts: ["payto://x-taler-bank/bank/some_account"], +}); +export const Empty = createExample(TestedComponent, { + transfers: [], + accounts: [], +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx new file mode 100644 index 000000000..02b12c4c2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -0,0 +1,134 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { CardTable } from "./Table.js"; + +export interface Props { + transfers: MerchantBackend.Transfers.TransferDetails[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; + onShowAll: () => void; + onShowVerified: () => void; + onShowUnverified: () => void; + isVerifiedTransfers?: boolean; + isNonVerifiedTransfers?: boolean; + isAllTransfers?: boolean; + accounts: string[]; + onChangePayTo: (p?: string) => void; + payTo?: string; + onCreate: () => void; + onDelete: () => void; +} + +export function ListPage({ + payTo, + onChangePayTo, + transfers, + onCreate, + onDelete, + accounts, + onLoadMoreBefore, + onLoadMoreAfter, + isAllTransfers, + isNonVerifiedTransfers, + isVerifiedTransfers, + onShowAll, + onShowUnverified, + onShowVerified, +}: Props): VNode { + const form = { payto_uri: payTo }; + + const { i18n } = useTranslationContext(); + return ( + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-10"> + <FormProvider + object={form} + valueHandler={(updater) => onChangePayTo(updater(form).payto_uri)} + > + <InputSelector + name="payto_uri" + label={i18n.str`Account URI`} + values={accounts} + placeholder={i18n.str`Select one account`} + tooltip={i18n.str`filter by account address`} + /> + </FormProvider> + </div> + <div class="column" /> + </div> + <div class="tabs"> + <ul> + <li class={isAllTransfers ? "is-active" : ""}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`remove all filters`} + > + <a onClick={onShowAll}> + <i18n.Translate>All</i18n.Translate> + </a> + </div> + </li> + <li class={isVerifiedTransfers ? "is-active" : ""}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`} + > + <a onClick={onShowVerified}> + <i18n.Translate>Verified</i18n.Translate> + </a> + </div> + </li> + <li class={isNonVerifiedTransfers ? "is-active" : ""}> + <div + class="has-tooltip-right" + data-tooltip={i18n.str`only show wire transfers claimed by the exchange`} + > + <a onClick={onShowUnverified}> + <i18n.Translate>Unverified</i18n.Translate> + </a> + </div> + </li> + </ul> + </div> + <CardTable + transfers={transfers.map((o) => ({ + ...o, + id: String(o.transfer_serial_id), + }))} + accounts={accounts} + onCreate={onCreate} + onDelete={onDelete} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + hasMoreAfter={!onLoadMoreAfter} + /> + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx new file mode 100644 index 000000000..b6b1cf328 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -0,0 +1,229 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; + +type Entity = MerchantBackend.Transfers.TransferDetails & WithId; + +interface Props { + transfers: Entity[]; + onDelete: (id: Entity) => void; + onCreate: () => void; + accounts: string[]; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + transfers, + onCreate, + onDelete, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-arrow-left-right" /> + </span> + <i18n.Translate>Transfers</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new transfer`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {transfers.length > 0 ? ( + <Table + instances={transfers} + onDelete={onDelete} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: (id: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + const [settings] = useSettings(); + return ( + <div class="table-container"> + {hasMoreBefore && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more transfers before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>load newer transfers</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Credit</i18n.Translate> + </th> + <th> + <i18n.Translate>Address</i18n.Translate> + </th> + <th> + <i18n.Translate>Exchange URL</i18n.Translate> + </th> + <th> + <i18n.Translate>Confirmed</i18n.Translate> + </th> + <th> + <i18n.Translate>Verified</i18n.Translate> + </th> + <th> + <i18n.Translate>Executed at</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.id}> + <td>{i.id}</td> + <td>{i.credit_amount}</td> + <td>{i.payto_uri}</td> + <td>{i.exchange_url}</td> + <td>{i.confirmed ? i18n.str`yes` : i18n.str`no`}</td> + <td>{i.verified ? i18n.str`yes` : i18n.str`no`}</td> + <td> + {i.execution_time + ? i.execution_time.t_s == "never" + ? i18n.str`never` + : format( + i.execution_time.t_s * 1000, + datetimeFormatForSettings(settings), + ) + : i18n.str`unknown`} + </td> + <td> + {i.verified === undefined ? ( + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected transfer from the database`} + onClick={() => onDelete(i)} + > + Delete + </button> + ) : undefined} + </td> + </tr> + ); + })} + </tbody> + </table> + {hasMoreAfter && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more transfer after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>load older transfers</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no transfer yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx new file mode 100644 index 000000000..0fdbb9bc3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -0,0 +1,118 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useInstanceDetails } from "../../../../hooks/instance.js"; +import { useInstanceTransfers } from "../../../../hooks/transfer.js"; +import { ListPage } from "./ListPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCreate: () => void; +} +interface Form { + verified?: "yes" | "no"; + payto_uri?: string; +} + +export default function ListTransfer({ + onUnauthorized, + onLoadError, + onCreate, + onNotFound, +}: Props): VNode { + const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s }); + + const [position, setPosition] = useState<string | undefined>(undefined); + + const instance = useInstanceBankAccounts(); + const accounts = !instance.ok + ? [] + : instance.data.accounts.map((a) => a.payto_uri); + const [form, setForm] = useState<Form>({ payto_uri: "" }); + + const shoulUseDefaultAccount = accounts.length === 1 + useEffect(() => { + if (shoulUseDefaultAccount) { + setForm({...form, payto_uri: accounts[0]}) + } + }, [shoulUseDefaultAccount]) + + const isVerifiedTransfers = form.verified === "yes"; + const isNonVerifiedTransfers = form.verified === "no"; + const isAllTransfers = form.verified === undefined; + + const result = useInstanceTransfers( + { + position, + payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, + verified: form.verified, + }, + (id) => setPosition(id), + ); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <ListPage + accounts={accounts} + transfers={result.data.transfers} + onLoadMoreBefore={ + result.isReachingStart ? result.loadMorePrev : undefined + } + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onCreate={onCreate} + onDelete={() => { + null; + }} + // position={position} setPosition={setPosition} + onShowAll={() => setFilter(undefined)} + onShowUnverified={() => setFilter("no")} + onShowVerified={() => setFilter("yes")} + isAllTransfers={isAllTransfers} + isVerifiedTransfers={isVerifiedTransfers} + isNonVerifiedTransfers={isNonVerifiedTransfers} + payTo={form.payto_uri} + onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))} + /> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx new file mode 100644 index 000000000..84cc95e72 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx @@ -0,0 +1,26 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; + +export default function UpdateTransfer(): VNode { + return <div>order transfer page</div>; +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx new file mode 100644 index 000000000..817a7025c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx @@ -0,0 +1,59 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/Instance/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, { + selected: { + name: "name", + auth: { method: "external" }, + address: {}, + user_type: "business", + use_stefan: true, + jurisdiction: {}, + default_pay_delay: { + d_us: 1000 * 1000, //one second + }, + default_wire_transfer_delay: { + d_us: 1000 * 1000, //one second + }, + merchant_pub: "ASDWQEKASJDKSADJ", + }, +}); diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx new file mode 100644 index 000000000..a27a0cb06 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -0,0 +1,176 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../components/form/FormProvider.js"; +import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; +import { useInstanceContext } from "../../../context/instance.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { undefinedIfEmpty } from "../../../utils/table.js"; +import { Duration } from "@gnu-taler/taler-util"; + +export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { + default_pay_delay: Duration, + default_wire_transfer_delay: Duration, +}; + +//MerchantBackend.Instances.InstanceAuthConfigurationMessage +interface Props { + onUpdate: (d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void; + selected: MerchantBackend.Instances.QueryInstancesResponse; + isLoading: boolean; + onBack: () => void; +} + +function convert( + from: MerchantBackend.Instances.QueryInstancesResponse, +): Entity { + const { default_pay_delay, default_wire_transfer_delay, ...rest } = from; + + const defaults = { + use_stefan: false, + default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay), + default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay), + }; + return { ...defaults, ...rest }; +} + +export function UpdatePage({ + onUpdate, + selected, + onBack, +}: Props): VNode { + const { id } = useInstanceContext(); + + const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); + + const { i18n } = useTranslationContext(); + + const errors: FormErrors<Entity> = { + name: !value.name ? i18n.str`required` : undefined, + user_type: !value.user_type + ? i18n.str`required` + : value.user_type !== "business" && value.user_type !== "individual" + ? i18n.str`should be business or individual` + : undefined, + default_pay_delay: !value.default_pay_delay + ? i18n.str`required` + : !!value.default_wire_transfer_delay && + value.default_wire_transfer_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? + i18n.str`pay delay can't be greater than wire transfer delay` : undefined, + default_wire_transfer_delay: !value.default_wire_transfer_delay + ? i18n.str`required` + : undefined, + address: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n.str`max 7 lines` + : undefined, + }), + jurisdiction: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n.str`max 7 lines` + : undefined, + }), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submit = async (): Promise<void> => { + const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>; + const result: MerchantBackend.Instances.InstanceReconfigurationMessage = { + default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay), + default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay), + ...rest, + } + await onUpdate(result); + }; + // const [active, setActive] = useState(false); + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>Instance id</i18n.Translate>: <b>{id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + + <hr /> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider<Entity> + errors={errors} + object={value} + valueHandler={valueHandler} + > + <DefaultInstanceFormFields showId={false} /> + </FormProvider> + + <div class="buttons is-right mt-4"> + <button + class="button" + onClick={onBack} + data-tooltip="cancel operation" + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + + <AsyncButton + onClick={submit} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={hasErrors} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx new file mode 100644 index 000000000..e44cf5c0f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx @@ -0,0 +1,118 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + HttpResponse, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading.js"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { useInstanceContext } from "../../../context/instance.js"; +import { AccessToken, MerchantBackend } from "../../../declaration.js"; +import { + useInstanceAPI, + useInstanceDetails, + useManagedInstanceDetails, + useManagementAPI, +} from "../../../hooks/instance.js"; +import { Notification } from "../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; + +export interface Props { + onBack: () => void; + onConfirm: () => void; + + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onUpdateError: (e: HttpError<MerchantBackend.ErrorDetail>) => void; +} + +export default function Update(props: Props): VNode { + const { updateInstance } = useInstanceAPI(); + const result = useInstanceDetails(); + return CommonUpdate(props, result, updateInstance, ); +} + +export function AdminUpdate(props: Props & { instanceId: string }): VNode { + const { updateInstance } = useManagementAPI( + props.instanceId, + ); + const result = useManagedInstanceDetails(props.instanceId); + return CommonUpdate(props, result, updateInstance, ); +} + +function CommonUpdate( + { + onBack, + onConfirm, + onLoadError, + onNotFound, + onUpdateError, + onUnauthorized, + }: Props, + result: HttpResponse< + MerchantBackend.Instances.QueryInstancesResponse, + MerchantBackend.ErrorDetail + >, + updateInstance: any, +): VNode { + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + onBack={onBack} + isLoading={false} + selected={result.data} + onUpdate={( + d: MerchantBackend.Instances.InstanceReconfigurationMessage, + ): Promise<void> => { + return updateInstance(d) + .then(onConfirm) + .catch((error: Error) => + setNotif({ + message: i18n.str`Failed to create instance`, + type: "ERROR", + description: error.message, + }), + ); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx new file mode 100644 index 000000000..4857ede97 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Webhooks/Create", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx new file mode 100644 index 000000000..434d69412 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx @@ -0,0 +1,183 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; + +type Entity = MerchantBackend.Webhooks.WebhookAddDetails; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"]; + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + + const errors: FormErrors<Entity> = { + webhook_id: !state.webhook_id ? i18n.str`required` : undefined, + event_type: !state.event_type ? i18n.str`required` + : state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"` + : undefined, + http_method: !state.http_method + ? i18n.str`required` + : !validMethod.includes(state.http_method) + ? i18n.str`should be one of '${validMethod.join(", ")}'` + : undefined, + url: !state.url ? i18n.str`required` : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onCreate(state as any); + }; + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="webhook_id" + label={i18n.str`ID`} + tooltip={i18n.str`Webhook ID to use`} + /> + <InputSelector + name="event_type" + label={i18n.str`Event`} + values={[ + i18n.str`Choose one...`, + i18n.str`pay`, + i18n.str`refund`, + ]} + tooltip={i18n.str`The event of the webhook: why the webhook is used`} + /> + <InputSelector + name="http_method" + label={i18n.str`Method`} + values={[ + i18n.str`Choose one...`, + i18n.str`GET`, + i18n.str`POST`, + i18n.str`PUT`, + i18n.str`PATCH`, + i18n.str`HEAD`, + ]} + tooltip={i18n.str`Method used by the webhook`} + /> + + <Input<Entity> + name="url" + label={i18n.str`URL`} + tooltip={i18n.str`URL of the webhook where the customer will be redirected`} + /> + + <p> + The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string + between <pre style={{ display: "inline", padding: 0 }}>{{</pre> and <pre style={{ display: "inline", padding: 0 }}>}}</pre> will + be replaced with replaced with the value of the correspoding variable. + </p> + <p> + For example <pre style={{ display: "inline", padding: 0 }}>{{contract_terms.amount}}</pre> will be replaced + with the the order's price + </p> + <p> + The short list of variables are: + </p> + <div class="menu"> + + <ul class="menu-list" style={{ listStyleType: "disc", marginLeft: 20 }}> + <li><b>contract_terms.summary:</b> order's description </li> + <li><b>contract_terms.amount:</b> order's price </li> + <li><b>order_id:</b> order's unique identification </li> + {state.event_type === "refund" && <Fragment> + <li><b>refund_amout:</b> the amount that was being refunded</li> + <li><b>reason:</b> the reason entered by the merchant staff for granting the refund</li> + <li><b>timestamp:</b> time of the refund in nanoseconds since 1970</li> + </Fragment>} + </ul> + </div> + {/* <Input<Entity> + name="header_template" + label={i18n.str`Http header`} + inputType="multiline" + tooltip={i18n.str`Header template of the webhook`} + /> */} + <Input<Entity> + name="body_template" + inputType="multiline" + label={i18n.str`Http body`} + tooltip={i18n.str`Body template by the webhook`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx new file mode 100644 index 000000000..924e6d9b8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; + +export type Entity = MerchantBackend.Webhooks.WebhookAddDetails; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateWebhook({ onConfirm, onBack }: Props): VNode { + const { createWebhook } = useWebhookAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => { + return createWebhook(request) + .then(() => onConfirm()) + .catch((error) => { + setNotif({ + message: i18n.str`could not inform template`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx new file mode 100644 index 000000000..702e9ba4a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { FunctionalComponent, h } from "preact"; +import { ListPage as TestedComponent } from "./ListPage.js"; + +export default { + title: "Pages/Templates/List", + component: TestedComponent, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx new file mode 100644 index 000000000..87e221e3c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx @@ -0,0 +1,64 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { CardTable } from "./Table.js"; + +export interface Props { + webhooks: MerchantBackend.Webhooks.WebhookEntry[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; + onCreate: () => void; + onDelete: (e: MerchantBackend.Webhooks.WebhookEntry) => void; + onSelect: (e: MerchantBackend.Webhooks.WebhookEntry) => void; +} + +export function ListPage({ + webhooks, + onCreate, + onDelete, + onSelect, + onLoadMoreBefore, + onLoadMoreAfter, +}: Props): VNode { + const form = { payto_uri: "" }; + + const { i18n } = useTranslationContext(); + return ( + <section class="section is-main-section"> + <CardTable + webhooks={webhooks.map((o) => ({ + ...o, + id: String(o.webhook_id), + }))} + onCreate={onCreate} + onDelete={onDelete} + onSelect={onSelect} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + hasMoreAfter={!onLoadMoreAfter} + /> + </section> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx new file mode 100644 index 000000000..42a179d2c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -0,0 +1,218 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration.js"; + +type Entity = MerchantBackend.Webhooks.WebhookEntry; + +interface Props { + webhooks: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + webhooks, + onCreate, + onDelete, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-newspaper" /> + </span> + <i18n.Translate>Webhooks</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add new webhooks`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {webhooks.length > 0 ? ( + <Table + instances={webhooks} + onDelete={onDelete} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: (e: Entity) => void; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => + prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, + hasMoreAfter, + hasMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {hasMoreBefore && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more webhooks before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>load newer webhooks</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Event type</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.webhook_id}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.webhook_id} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.event_type} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`delete selected webhook from the database`} + onClick={() => onDelete(i)} + > + Delete + </button> + {/* <button + class="button is-info is-small has-tooltip-left" + data-tooltip={i18n.str`test webhook`} + onClick={() => onNewOrder(i)} + > + Test + </button> */} + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {hasMoreAfter && ( + <button + class="button is-fullwidth" + data-tooltip={i18n.str`load more webhooks after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>load older webhooks</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-sad mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There is no webhooks yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx new file mode 100644 index 000000000..a6f6f1511 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx @@ -0,0 +1,109 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + useInstanceWebhooks, + useWebhookAPI, +} from "../../../../hooks/webhooks.js"; +import { Notification } from "../../../../utils/types.js"; +import { ListPage } from "./ListPage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListWebhooks({ + onUnauthorized, + onLoadError, + onCreate, + onSelect, + onNotFound, +}: Props): VNode { + const [position, setPosition] = useState<string | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { deleteWebhook } = useWebhookAPI(); + const result = useInstanceWebhooks({ position }, (id) => setPosition(id)); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <ListPage + webhooks={result.data.webhooks} + onLoadMoreBefore={ + result.isReachingStart ? result.loadMorePrev : undefined + } + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onCreate={onCreate} + onSelect={(e) => { + onSelect(e.webhook_id); + }} + onDelete={(e: MerchantBackend.Webhooks.WebhookEntry) => + deleteWebhook(e.webhook_id) + .then(() => + setNotif({ + message: i18n.str`webhook delete successfully`, + type: "SUCCESS", + }), + ) + .catch((error) => + setNotif({ + message: i18n.str`could not delete the webhook`, + type: "ERROR", + description: error.message, + }), + ) + } + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx new file mode 100644 index 000000000..8d07cb31f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx @@ -0,0 +1,32 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage.js"; + +export default { + title: "Pages/Templates/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx new file mode 100644 index 000000000..76a23b6e5 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -0,0 +1,146 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { useBackendContext } from "../../../../context/backend.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; + +type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId; + +interface Props { + onUpdate: (d: Entity) => Promise<void>; + onBack?: () => void; + webhook: Entity; +} +const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"]; + +export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>(webhook); + + const errors: FormErrors<Entity> = { + event_type: !state.event_type ? i18n.str`required` : undefined, + http_method: !state.http_method + ? i18n.str`required` + : !validMethod.includes(state.http_method) + ? i18n.str`should be one of '${validMethod.join(", ")}'` + : undefined, + url: !state.url ? i18n.str`required` : undefined, + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + return onUpdate(state as any); + }; + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + Webhook: <b>{webhook.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="event_type" + label={i18n.str`Event`} + tooltip={i18n.str`The event of the webhook: why the webhook is used`} + /> + <Input<Entity> + name="http_method" + label={i18n.str`Method`} + tooltip={i18n.str`Method used by the webhook`} + /> + <Input<Entity> + name="url" + label={i18n.str`URL`} + tooltip={i18n.str`URL of the webhook where the customer will be redirected`} + /> + <Input<Entity> + name="header_template" + label={i18n.str`Header`} + inputType="multiline" + tooltip={i18n.str`Header template of the webhook`} + /> + <Input<Entity> + name="body_template" + inputType="multiline" + label={i18n.str`Body`} + tooltip={i18n.str`Body template by the webhook`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx new file mode 100644 index 000000000..3f723ed87 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx @@ -0,0 +1,99 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { + useWebhookAPI, + useWebhookDetails, +} from "../../../../hooks/webhooks.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { HttpStatusCode } from "@gnu-taler/taler-util"; + +export type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; + tid: string; +} +export default function UpdateWebhook({ + tid, + onConfirm, + onBack, + onUnauthorized, + onNotFound, + onLoadError, +}: Props): VNode { + const { updateWebhook } = useWebhookAPI(); + const result = useWebhookDetails(tid); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const { i18n } = useTranslationContext(); + + if (result.loading) return <Loading />; + if (!result.ok) { + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.Unauthorized + ) + return onUnauthorized(); + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) + return onNotFound(); + return onLoadError(result); + } + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + webhook={{ ...result.data, id: tid }} + onBack={onBack} + onUpdate={(data) => { + return updateWebhook(tid, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not update template`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/login/index.tsx b/packages/auditor-backoffice-ui/src/paths/login/index.tsx new file mode 100644 index 000000000..1c98b7c9b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/login/index.tsx @@ -0,0 +1,202 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, h, VNode } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useBackendContext } from "../../context/backend.js"; +import { useInstanceContext } from "../../context/instance.js"; +import { AccessToken, LoginToken } from "../../declaration.js"; +import { useCredentialsChecker } from "../../hooks/backend.js"; + +interface Props { + onConfirm: (token: LoginToken | undefined) => void; +} + +function normalizeToken(r: string): AccessToken { + return `secret-token:${r}` as AccessToken; +} + +export function LoginPage({ onConfirm }: Props): VNode { + const { url: backendURL } = useBackendContext(); + const { admin, id } = useInstanceContext(); + const { requestNewLoginToken } = useCredentialsChecker(); + const [token, setToken] = useState(""); + + const { i18n } = useTranslationContext(); + + + const doLogin = useCallback(async function doLoginImpl() { + const secretToken = normalizeToken(token); + const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}` + const result = await requestNewLoginToken(baseUrl, secretToken); + if (result.valid) { + const { token, expiration } = result + onConfirm({ token, expiration }); + } else { + onConfirm(undefined); + } + }, [id, token]) + + if (admin && id !== "default") { + //admin trying to access another instance + return (<div class="columns is-centered" style={{ margin: "auto" }}> + <div class="column is-two-thirds "> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} + > + <p class="modal-card-title">{i18n.str`Login required`}</p> + </header> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + <p> + <i18n.Translate>Need the access token for the instance.</i18n.Translate> + </p> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Access Token</i18n.Translate> + </label> + </div> + <div class="field-body"> + <div class="field"> + <p class="control is-expanded"> + <input + class="input" + type="password" + placeholder={"current access token"} + name="token" + onKeyPress={(e) => + e.keyCode === 13 + ? doLogin() + : null + } + value={token} + onInput={(e): void => setToken(e?.currentTarget.value)} + /> + </p> + </div> + </div> + </div> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "flex-end", + border: "1px solid", + borderTop: 0, + }} + > + <AsyncButton + onClick={doLogin} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </footer> + </div> + </div> + </div>) + } + + return ( + <div class="columns is-centered" style={{ margin: "auto" }}> + <div class="column is-two-thirds "> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} + > + <p class="modal-card-title">{i18n.str`Login required`}</p> + </header> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + <i18n.Translate>Please enter your access token.</i18n.Translate> + + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Access Token</i18n.Translate> + </label> + </div> + <div class="field-body"> + <div class="field"> + <p class="control is-expanded"> + <input + class="input" + type="password" + placeholder={"current access token"} + name="token" + onKeyPress={(e) => + e.keyCode === 13 + ? doLogin() + : null + } + value={token} + onInput={(e): void => setToken(e?.currentTarget.value)} + /> + </p> + </div> + </div> + </div> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + <div /> + <AsyncButton + type="is-info" + onClick={doLogin} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </footer> + </div> + </div> + </div> + ); +} + +function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode { + const [running, setRunning] = useState(false) + return <button class={"button " + type} disabled={disabled || running} onClick={() => { + setRunning(true) + onClick().then(() => { + setRunning(false) + }).catch(() => { + setRunning(false) + }) + }}> + {children} + </button> +} + + diff --git a/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx new file mode 100644 index 000000000..061a67025 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx @@ -0,0 +1,34 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { Link } from "preact-router"; + +export default function NotFoundPage(): VNode { + return ( + <div> + <p>That page doesn't exist.</p> + <Link href="/"> + <h4>Back to Home</h4> + </Link> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx new file mode 100644 index 000000000..87bd2fa39 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx @@ -0,0 +1,112 @@ +import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { FormErrors, FormProvider } from "../../components/form/FormProvider.js"; +import { InputSelector } from "../../components/form/InputSelector.js"; +import { InputToggle } from "../../components/form/InputToggle.js"; +import { LangSelector } from "../../components/menu/LangSelector.js"; +import { Settings, useSettings } from "../../hooks/useSettings.js"; + +function getBrowserLang(): string | undefined { + if (typeof window === "undefined") return undefined; + if (window.navigator.languages) return window.navigator.languages[0]; + if (window.navigator.language) return window.navigator.language; + return undefined; +} + +export function Settings({ onClose }: { onClose?: () => void }): VNode { + const { i18n } = useTranslationContext() + const borwserLang = getBrowserLang() + const { update } = useLang() + + const [value, updateValue] = useSettings() + const errors: FormErrors<Settings> = { + } + + function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void { + const next = s(value) + const v: Settings = { + advanceOrderMode: next.advanceOrderMode ?? false, + dateFormat: next.dateFormat ?? "ymd" + } + updateValue(v) + } + + return <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div> + + <FormProvider<Settings> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Language</i18n.Translate> + <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}> + <i class="mdi mdi-information" /> + </span> + </label> + </div> + <div class="field field-body has-addons is-flex-grow-3"> + <LangSelector /> + + {borwserLang !== undefined && <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-2" + onClick={(e) => { + update(borwserLang.substring(0, 2)) + }} + > + <i18n.Translate>Set default</i18n.Translate> + </button>} + </div> + </div> + <InputToggle<Settings> + label={i18n.str`Advance order creation`} + tooltip={i18n.str`Shows more options in the order creation form`} + name="advanceOrderMode" + /> + <InputSelector<Settings> + name="dateFormat" + label={i18n.str`Date format`} + expand={true} + help={ + value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : "" + } + toStr={(e) => { + if (e === "ymd") return "year month day" + if (e === "mdy") return "month day year" + if (e === "dmy") return "day month year" + return "choose one" + }} + values={[ + "ymd", + "mdy", + "dmy", + ]} + tooltip={i18n.str`how the date is going to be displayed`} + /> + </FormProvider> + </div> + </div> + <div class="column" /> + </div> + </section > + {onClose && + <section class="section is-main-section"> + <button + class="button" + onClick={onClose} + > + <i18n.Translate>Close</i18n.Translate> + </button> + </section> + } + </div > +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/schemas/index.ts b/packages/auditor-backoffice-ui/src/schemas/index.ts new file mode 100644 index 000000000..380466e13 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/schemas/index.ts @@ -0,0 +1,245 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { isAfter, isFuture } from "date-fns"; +import * as yup from "yup"; +import { AMOUNT_REGEX, PAYTO_REGEX } from "../utils/constants.js"; +import { Amounts } from "@gnu-taler/taler-util"; + +yup.setLocale({ + mixed: { + default: "field_invalid", + }, + number: { + min: ({ min }: any) => ({ key: "field_too_short", values: { min } }), + max: ({ max }: any) => ({ key: "field_too_big", values: { max } }), + }, +}); + +function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean { + return !!values && values.every((v) => v && PAYTO_REGEX.test(v)); +} + +function currencyWithAmountIsValid(value?: string): boolean { + return !!value && Amounts.parse(value) !== undefined; +} +function currencyGreaterThan0(value?: string) { + if (value) { + try { + const [, amount] = value.split(":"); + const intAmount = parseInt(amount, 10); + return intAmount > 0; + } catch { + return false; + } + } + return true; +} + +export const InstanceSchema = yup.object().shape({ + id: yup.string().required().meta({ type: "url" }), + name: yup.string().required(), + auth: yup.object().shape({ + method: yup.string().matches(/^(external|token)$/), + token: yup.string().optional().nullable(), + }), + payto_uris: yup + .array() + .of(yup.string()) + .min(1) + .meta({ type: "array" }) + .test("payto", "{path} is not valid", listOfPayToUrisAreValid), + default_max_deposit_fee: yup + .string() + .required() + .test("amount", "the amount is not valid", currencyWithAmountIsValid) + .meta({ type: "amount" }), + default_max_wire_fee: yup + .string() + .required() + .test("amount", "{path} is not valid", currencyWithAmountIsValid) + .meta({ type: "amount" }), + default_wire_fee_amortization: yup.number().required(), + address: yup + .object() + .shape({ + country: yup.string().optional(), + address_lines: yup.array().of(yup.string()).max(7).optional(), + building_number: yup.string().optional(), + building_name: yup.string().optional(), + street: yup.string().optional(), + post_code: yup.string().optional(), + town_location: yup.string().optional(), + town: yup.string(), + district: yup.string().optional(), + country_subdivision: yup.string().optional(), + }) + .meta({ type: "group" }), + jurisdiction: yup + .object() + .shape({ + country: yup.string().optional(), + address_lines: yup.array().of(yup.string()).max(7).optional(), + building_number: yup.string().optional(), + building_name: yup.string().optional(), + street: yup.string().optional(), + post_code: yup.string().optional(), + town_location: yup.string().optional(), + town: yup.string(), + district: yup.string().optional(), + country_subdivision: yup.string().optional(), + }) + .meta({ type: "group" }), + // default_pay_delay: yup.object() + // .shape({ d_us: yup.number() }) + // .required() + // .meta({ type: 'duration' }), + // .transform(numberToDuration), + default_wire_transfer_delay: yup + .object() + .shape({ d_us: yup.number() }) + .required() + .meta({ type: "duration" }), + // .transform(numberToDuration), +}); + +export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]); +export const InstanceCreateSchema = InstanceSchema.clone(); + +export const AuthorizeRewardSchema = yup.object().shape({ + justification: yup.string().required(), + amount: yup + .string() + .required() + .test("amount", "the amount is not valid", currencyWithAmountIsValid) + .test("amount_positive", "the amount is not valid", currencyGreaterThan0), + next_url: yup.string().required(), +}); + +const stringIsValidJSON = (value?: string) => { + const p = value?.trim(); + if (!p) return true; + try { + JSON.parse(p); + return true; + } catch { + return false; + } +}; + +export const OrderCreateSchema = yup.object().shape({ + pricing: yup + .object() + .required() + .shape({ + summary: yup.string().ensure().required(), + order_price: yup + .string() + .ensure() + .required() + .test("amount", "the amount is not valid", currencyWithAmountIsValid) + .test( + "amount_positive", + "the amount should be greater than 0", + currencyGreaterThan0, + ), + }), + // extra: yup.object().test("extra", "is not a JSON format", stringIsValidJSON), + payments: yup + .object() + .required() + .shape({ + refund_deadline: yup + .date() + .test("future", "should be in the future", (d) => + d ? isFuture(d) : true, + ), + pay_deadline: yup + .date() + .test("future", "should be in the future", (d) => + d ? isFuture(d) : true, + ), + auto_refund_deadline: yup + .date() + .test("future", "should be in the future", (d) => + d ? isFuture(d) : true, + ), + delivery_date: yup + .date() + .test("future", "should be in the future", (d) => + d ? isFuture(d) : true, + ), + }) + .test("payment", "dates", (d) => { + if ( + d.pay_deadline && + d.refund_deadline && + isAfter(d.refund_deadline, d.pay_deadline) + ) { + return new yup.ValidationError( + "pay deadline should be greater than refund", + "asd", + "payments.pay_deadline", + ); + } + return true; + }), +}); + +export const ProductCreateSchema = yup.object().shape({ + product_id: yup.string().ensure().required(), + description: yup.string().required(), + unit: yup.string().ensure().required(), + price: yup + .string() + .required() + .test("amount", "the amount is not valid", currencyWithAmountIsValid), + stock: yup.object({}).optional(), + minimum_age: yup.number().optional().min(0), +}); + +export const ProductUpdateSchema = yup.object().shape({ + description: yup.string().required(), + price: yup + .string() + .required() + .test("amount", "the amount is not valid", currencyWithAmountIsValid), + stock: yup.object({}).optional(), + minimum_age: yup.number().optional().min(0), +}); + +export const TaxSchema = yup.object().shape({ + name: yup.string().required().ensure(), + tax: yup + .string() + .required() + .test("amount", "the amount is not valid", currencyWithAmountIsValid), +}); + +export const NonInventoryProductSchema = yup.object().shape({ + quantity: yup.number().required().positive(), + description: yup.string().required(), + unit: yup.string().ensure().required(), + price: yup + .string() + .required() + .test("amount", "the amount is not valid", currencyWithAmountIsValid), +}); diff --git a/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss b/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss new file mode 100644 index 000000000..aa75b9916 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss @@ -0,0 +1,70 @@ +.rdp-picker { + display: flex; + height: 175px; +} + +@media (max-width: 400px) { + .rdp-picker { + width: 250px; + } +} + +.rdp-masked-div { + overflow: hidden; + height: 175px; + position: relative; +} + +.rdp-column-container { + flex-grow: 1; + display: inline-block; +} + +.rdp-column { + position: absolute; + z-index: 0; + width: 100%; +} + +.rdp-reticule { + border: 0; + border-top: 2px solid rgba(109, 202, 236, 1); + height: 2px; + position: absolute; + width: 80%; + margin: 0; + z-index: 100; + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.rdp-text-overlay { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + height: 35px; + font-size: 20px; + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.rdp-cell div { + font-size: 17px; + color: gray; + font-style: italic; +} + +.rdp-cell { + display: flex; + align-items: center; + justify-content: center; + height: 35px; + font-size: 18px; +} + +.rdp-center { + font-size: 25px; +} diff --git a/packages/auditor-backoffice-ui/src/scss/_aside.scss b/packages/auditor-backoffice-ui/src/scss/_aside.scss new file mode 100644 index 000000000..e0922093b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_aside.scss @@ -0,0 +1,181 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +@include desktop { + html { + &.has-aside-left { + &.has-aside-expanded { + nav.navbar, + body { + padding-left: $aside-width; + } + } + aside.is-placed-left { + display: block; + } + } + } + + aside.aside.is-expanded { + width: $aside-width; + + .menu-list { + @include icon-with-update-mark($aside-icon-width); + + span.menu-item-label { + display: inline-block; + } + + li.is-active { + ul { + display: block; + } + } + } + } +} + +aside.aside { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 40; + height: 100vh; + padding: 0; + box-shadow: $aside-box-shadow; + background: $aside-background-color; + + .aside-tools { + display: flex; + flex-direction: row; + width: 100%; + background-color: $aside-tools-background-color; + color: $aside-tools-color; + line-height: $navbar-height; + height: $navbar-height; + padding-left: $default-padding * 0.5; + flex: 1; + + .icon { + margin-right: $default-padding * 0.5; + } + } + + .menu-list { + li { + a { + &.has-dropdown-icon { + position: relative; + padding-right: $aside-icon-width; + + .dropdown-icon { + position: absolute; + top: $size-base * 0.5; + right: 0; + } + } + } + ul { + display: none; + border-left: 0; + background-color: darken($base-color, 2.5%); + padding-left: 0; + margin: 0 0 $default-padding * 0.5; + + li { + a { + padding: $default-padding * 0.5 0 $default-padding * 0.5 + $default-padding * 0.5; + font-size: $aside-submenu-font-size; + + &.has-icon { + padding-left: 0; + } + &.is-active { + &:not(:hover) { + background: transparent; + } + } + } + } + } + } + } + + .menu-label { + padding: 0 $default-padding * 0.5; + margin-top: $default-padding * 0.5; + margin-bottom: $default-padding * 0.5; + } +} + +@include touch { + nav.navbar { + @include transition(margin-left); + } + aside.aside { + @include transition(left); + } + html.has-aside-mobile-transition { + body { + overflow-x: hidden; + } + body, + nav.navbar { + width: 100vw; + } + aside.aside { + width: $aside-mobile-width; + display: block; + left: $aside-mobile-width * -1; + + .image { + img { + max-width: $aside-mobile-width * 0.33; + } + } + + .menu-list { + li.is-active { + ul { + display: block; + } + } + a { + @include icon-with-update-mark($aside-icon-width); + + span.menu-item-label { + display: inline-block; + } + } + } + } + } + div.has-aside-mobile-expanded { + nav.navbar { + margin-left: $aside-mobile-width; + } + aside.aside { + left: 0; + } + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_card.scss b/packages/auditor-backoffice-ui/src/scss/_card.scss new file mode 100644 index 000000000..62db7f457 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_card.scss @@ -0,0 +1,69 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +.card:not(:last-child) { + margin-bottom: $default-padding; +} + +.card { + border-radius: $radius-large; + border: $card-border; + + &.has-table { + .card-content { + padding: 0; + } + .b-table { + border-radius: $radius-large; + overflow: hidden; + } + } + + &.is-card-widget { + .card-content { + padding: $default-padding * 0.5; + } + } + + .card-header { + border-bottom: 1px solid $base-color-light; + } + + .card-content { + hr { + margin-left: $card-content-padding * -1; + margin-right: $card-content-padding * -1; + } + } + + .is-widget-icon { + .icon { + width: 5rem; + height: 5rem; + } + } + + .is-widget-label { + .subtitle { + color: $grey; + } + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss b/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss new file mode 100644 index 000000000..34c40092b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss @@ -0,0 +1,259 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +:root { + --primary-color: #3298dc; + + --primary-text-color-dark: rgba(0, 0, 0, 0.87); + --secondary-text-color-dark: rgba(0, 0, 0, 0.57); + --disabled-text-color-dark: rgba(0, 0, 0, 0.13); + + --primary-text-color-light: rgba(255, 255, 255, 0.87); + --secondary-text-color-light: rgba(255, 255, 255, 0.57); + --disabled-text-color-light: rgba(255, 255, 255, 0.13); + + --font-stack: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; + + --primary-card-color: #fff; + --primary-background-color: #f2f2f2; + + --box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); + --box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16), + 0 3px 6px rgba(0, 0, 0, 0.23); + --box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19), + 0 6px 6px rgba(0, 0, 0, 0.23); + --box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25), + 0 10px 10px rgba(0, 0, 0, 0.22); +} + +.datePicker { + text-align: left; + background: var(--primary-card-color); + border-radius: 3px; + z-index: 200; + position: fixed; + height: auto; + max-height: 90vh; + width: 90vw; + max-width: 448px; + transform-origin: top left; + transition: transform 0.22s ease-in-out, opacity 0.22s ease-in-out; + top: 50%; + left: 50%; + opacity: 0; + transform: scale(0) translate(-50%, -50%); + user-select: none; + + &.datePicker--opened { + opacity: 1; + transform: scale(1) translate(-50%, -50%); + } + + .datePicker--titles { + border-top-left-radius: 3px; + border-top-right-radius: 3px; + padding: 24px; + height: 100px; + background: var(--primary-color); + + h2, + h3 { + cursor: pointer; + color: #fff; + line-height: 1; + padding: 0; + margin: 0; + font-size: 32px; + } + + h3 { + color: rgba(255, 255, 255, 0.57); + font-size: 18px; + padding-bottom: 2px; + } + } + + nav { + padding: 20px; + height: 56px; + + h4 { + width: calc(100% - 60px); + text-align: center; + display: inline-block; + padding: 0; + font-size: 14px; + line-height: 24px; + margin: 0; + position: relative; + top: -9px; + color: var(--primary-text-color); + } + + i { + cursor: pointer; + color: var(--secondary-text-color); + font-size: 26px; + user-select: none; + border-radius: 50%; + + &:hover { + background: var(--disabled-text-color-dark); + } + } + } + + .datePicker--scroll { + overflow-y: auto; + max-height: calc(90vh - 56px - 100px); + } + + .datePicker--calendar { + padding: 0 20px; + + .datePicker--dayNames { + width: 100%; + display: grid; + text-align: center; + + // there's probably a better way to do this, but wanted to try out CSS grid + grid-template-columns: + calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) + calc(100% / 7) calc(100% / 7) calc(100% / 7); + + span { + color: var(--secondary-text-color-dark); + font-size: 14px; + line-height: 42px; + display: inline-grid; + } + } + + .datePicker--days { + width: 100%; + display: grid; + text-align: center; + grid-template-columns: + calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) + calc(100% / 7) calc(100% / 7) calc(100% / 7); + + span { + color: var(--primary-text-color-dark); + line-height: 42px; + font-size: 14px; + display: inline-grid; + transition: color 0.22s; + height: 42px; + position: relative; + cursor: pointer; + user-select: none; + border-radius: 50%; + + &::before { + content: ""; + position: absolute; + z-index: -1; + height: 42px; + width: 42px; + left: calc(50% - 21px); + background: var(--primary-color); + border-radius: 50%; + transition: transform 0.22s, opacity 0.22s; + transform: scale(0); + opacity: 0; + } + + &[disabled="true"] { + cursor: unset; + } + + &.datePicker--today { + font-weight: 700; + } + + &.datePicker--selected { + color: rgba(255, 255, 255, 0.87); + + &:before { + transform: scale(1); + opacity: 1; + } + } + } + } + } + + .datePicker--selectYear { + padding: 0 20px; + display: block; + width: 100%; + text-align: center; + max-height: 362px; + + span { + display: block; + width: 100%; + font-size: 24px; + margin: 20px auto; + cursor: pointer; + + &.selected { + font-size: 42px; + color: var(--primary-color); + } + } + } + + div.datePicker--actions { + width: 100%; + padding: 8px; + text-align: right; + + button { + margin-bottom: 0; + font-size: 15px; + cursor: pointer; + color: var(--primary-text-color); + border: none; + margin-left: 8px; + min-width: 64px; + line-height: 36px; + background-color: transparent; + appearance: none; + padding: 0 16px; + border-radius: 3px; + transition: background-color 0.13s; + + &:hover, + &:focus { + outline: none; + background-color: var(--disabled-text-color-dark); + } + } + } +} + +.datePicker--background { + z-index: 199; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgba(0, 0, 0, 0.52); + animation: fadeIn 0.22s forwards; +} diff --git a/packages/auditor-backoffice-ui/src/scss/_footer.scss b/packages/auditor-backoffice-ui/src/scss/_footer.scss new file mode 100644 index 000000000..5855af742 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_footer.scss @@ -0,0 +1,35 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +footer.footer { + .logo { + img { + width: auto; + height: $footer-logo-height; + } + } +} + +@include mobile { + .footer-copyright { + text-align: center; + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_form.scss b/packages/auditor-backoffice-ui/src/scss/_form.scss new file mode 100644 index 000000000..bd28a17cf --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_form.scss @@ -0,0 +1,71 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +.field { + &.has-check { + .field-body { + margin-top: $default-padding * 0.125; + } + } + .control { + .mdi-24px.mdi-set, + .mdi-24px.mdi:before { + font-size: inherit; + } + } +} +.upload { + .upload-draggable { + display: block; + } +} + +.input, +.textarea, +select { + box-shadow: none; + + &:focus, + &:active { + box-shadow: none !important; + } +} + +.switch input[type="checkbox"] + .check:before { + box-shadow: none; +} + +.switch, +.b-checkbox.checkbox { + input[type="checkbox"] { + &:focus + .check, + &:focus:checked + .check { + box-shadow: none !important; + } + } +} + +.b-checkbox.checkbox input[type="checkbox"], +.b-radio.radio input[type="radio"] { + & + .check { + border: $checkbox-border; + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss b/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss new file mode 100644 index 000000000..0276468d7 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss @@ -0,0 +1,55 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +section.hero.is-hero-bar { + background-color: $hero-bar-background; + border-bottom: $light-border; + + .hero-body { + padding: $default-padding; + + .level-item { + &.is-hero-avatar-item { + margin-right: $default-padding; + } + + > div > .level { + margin-bottom: $default-padding * 0.5; + } + + .subtitle + p { + margin-top: $default-padding * 0.5; + } + } + + .button { + &.is-hero-button { + background-color: rgba($white, 0.5); + font-weight: 300; + @include transition(background-color); + + &:hover { + background-color: $white; + } + } + } + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_loading.scss b/packages/auditor-backoffice-ui/src/scss/_loading.scss new file mode 100644 index 000000000..d88d8c355 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_loading.scss @@ -0,0 +1,51 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +.lds-ring { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-ring div { + box-sizing: border-box; + display: block; + position: absolute; + width: 64px; + height: 64px; + margin: 8px; + border: 8px solid black; + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: black transparent transparent transparent; +} +.lds-ring div:nth-child(1) { + animation-delay: -0.45s; +} +.lds-ring div:nth-child(2) { + animation-delay: -0.3s; +} +.lds-ring div:nth-child(3) { + animation-delay: -0.15s; +} +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_main-section.scss b/packages/auditor-backoffice-ui/src/scss/_main-section.scss new file mode 100644 index 000000000..5a8b20ba0 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_main-section.scss @@ -0,0 +1,24 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +section.section.is-main-section { + padding-top: $default-padding; +} diff --git a/packages/auditor-backoffice-ui/src/scss/_misc.scss b/packages/auditor-backoffice-ui/src/scss/_misc.scss new file mode 100644 index 000000000..045d087e2 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_misc.scss @@ -0,0 +1,50 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +.is-user-avatar { + &.has-max-width { + max-width: $size-base * 7; + } + + &.is-aligned-center { + margin: 0 auto; + } + + img { + margin: 0 auto; + border-radius: $radius-rounded; + } +} + +.icon.has-update-mark { + position: relative; + + &:after { + content: ""; + width: $icon-update-mark-size; + height: $icon-update-mark-size; + position: absolute; + top: 1px; + right: 1px; + background-color: $icon-update-mark-color; + border-radius: $radius-rounded; + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_mixins.scss b/packages/auditor-backoffice-ui/src/scss/_mixins.scss new file mode 100644 index 000000000..f119ec68a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_mixins.scss @@ -0,0 +1,34 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +@mixin transition($t) { + transition: $t 250ms ease-in-out 50ms; +} + +@mixin icon-with-update-mark($icon-base-width) { + .icon { + width: $icon-base-width; + + &.has-update-mark:after { + right: calc($icon-base-width / 2) - 0.85; + } + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_modal.scss b/packages/auditor-backoffice-ui/src/scss/_modal.scss new file mode 100644 index 000000000..b2bfd3e9e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_modal.scss @@ -0,0 +1,35 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +.modal-card { + width: $modal-card-width; +} + +.modal-card-foot { + background-color: $modal-card-foot-background-color; +} + +@include mobile { + .modal .animation-content .modal-card { + width: $modal-card-width-mobile; + margin: 0 auto; + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss b/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss new file mode 100644 index 000000000..406e0392f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss @@ -0,0 +1,144 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +nav.navbar { + box-shadow: $navbar-box-shadow; + + .navbar-item { + &.has-user-avatar { + .is-user-avatar { + margin-right: $default-padding * 0.5; + display: inline-flex; + width: $navbar-avatar-size; + height: $navbar-avatar-size; + } + } + + &.has-divider { + border-right: $navbar-divider-border; + } + + &.no-left-space { + padding-left: 0; + } + + &.has-dropdown { + padding-right: 0; + padding-left: 0; + + .navbar-link { + padding-right: $navbar-item-h-padding; + padding-left: $navbar-item-h-padding; + } + } + + &.has-control { + padding-top: 0; + padding-bottom: 0; + } + + .control { + .input { + color: $navbar-input-color; + border: 0; + box-shadow: none; + background: transparent; + + &::placeholder { + color: $navbar-input-placeholder-color; + } + } + } + } +} + +@include touch { + nav.navbar { + display: flex; + padding-right: 0; + + .navbar-brand { + flex: 1; + + &.is-right { + flex: none; + } + } + + .navbar-item { + &.no-left-space-touch { + padding-left: 0; + } + } + + .navbar-menu { + position: absolute; + width: 100vw; + padding-top: 0; + top: $navbar-height; + left: 0; + + .navbar-item { + .icon:first-child { + margin-right: $default-padding * 0.5; + } + + &.has-dropdown { + > .navbar-link { + background-color: $white-ter; + .icon:last-child { + display: none; + } + } + } + + &.has-user-avatar { + > .navbar-link { + display: flex; + align-items: center; + padding-top: $default-padding * 0.5; + padding-bottom: $default-padding * 0.5; + } + } + } + } + } +} + +@include desktop { + nav.navbar { + .navbar-item { + padding-right: $navbar-item-h-padding; + padding-left: $navbar-item-h-padding; + + &:not(.is-desktop-icon-only) { + .icon:first-child { + margin-right: $default-padding * 0.5; + } + } + &.is-desktop-icon-only { + span:not(.icon) { + display: none; + } + } + } + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_table.scss b/packages/auditor-backoffice-ui/src/scss/_table.scss new file mode 100644 index 000000000..e4fbfc7b3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_table.scss @@ -0,0 +1,179 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +table.table { + thead { + th { + border-bottom-width: 1px; + } + } + + td, + th { + &.checkbox-cell { + .b-checkbox.checkbox:not(.button) { + margin-right: 0; + width: 20px; + + .control-label { + display: none; + padding: 0; + } + } + } + } + + td { + .image { + margin: 0 auto; + width: $table-avatar-size; + height: $table-avatar-size; + } + + &.is-progress-col { + min-width: 5rem; + vertical-align: middle; + } + } +} + +.b-table { + .table { + border: 0; + border-radius: 0; + } + + /* This stylizes buefy's pagination */ + .table-wrapper { + margin-bottom: 0; + } + + .table-wrapper + .level { + padding: $notification-padding; + padding-left: $card-content-padding; + padding-right: $card-content-padding; + margin: 0; + border-top: $base-color-light; + background: $notification-background-color; + + .pagination-link { + background: $button-background-color; + color: $button-color; + border-color: $button-border-color; + + &.is-current { + border-color: $button-active-border-color; + } + } + + .pagination-previous, + .pagination-next, + .pagination-link { + border-color: $button-border-color; + color: $base-color; + + &[disabled] { + background-color: transparent; + } + } + } +} + +@include mobile { + .card { + &.has-table { + .b-table { + .table-wrapper + .level { + .level-left + .level-right { + margin-top: 0; + } + } + } + } + &.has-mobile-sort-spaced { + .b-table { + .field.table-mobile-sort { + padding-top: $default-padding * 0.5; + } + } + } + } + .b-table { + .field.table-mobile-sort { + padding: 0 $default-padding * 0.5; + } + + .table-wrapper.has-mobile-cards { + tr { + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1); + margin-bottom: 3px !important; + } + td { + &.is-progress-col { + span, + progress { + display: flex; + width: 45%; + align-items: center; + align-self: center; + } + } + + &.checkbox-cell, + &.is-image-cell { + border-bottom: 0 !important; + } + + &.checkbox-cell, + &.is-actions-cell { + &:before { + display: none; + } + } + + &.has-no-head-mobile { + &:before { + display: none; + } + + span { + display: block; + width: 100%; + } + + &.is-progress-col { + progress { + width: 100%; + } + } + + &.is-image-cell { + .image { + width: $table-avatar-size-mobile; + height: auto; + margin: 0 auto $default-padding * 0.25; + } + } + } + } + } + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_theme-default.scss b/packages/auditor-backoffice-ui/src/scss/_theme-default.scss new file mode 100644 index 000000000..e74ece0e9 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_theme-default.scss @@ -0,0 +1,136 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +/* We'll need some initial vars to use here */ +@import "node_modules/bulma/sass/utilities/initial-variables"; + +/* Base: Size */ +$size-base: 1rem; +$default-padding: $size-base * 1.5; + +/* Default font */ +$family-sans-serif: "Nunito", sans-serif; + +/* Base color */ +$base-color: #2e323a; +$base-color-light: rgba(24, 28, 33, 0.06); + +/* General overrides */ +$primary: $turquoise; +$body-background-color: #f8f8f8; +$link: $blue; +$link-visited: $purple; +$light-border: 1px solid $base-color-light; +$hr-height: 1px; + +/* NavBar: specifics */ +$navbar-input-color: $grey-darker; +$navbar-input-placeholder-color: $grey-lighter; +$navbar-box-shadow: 0 1px 0 rgba(24, 28, 33, 0.04); +$navbar-divider-border: 1px solid rgba($grey-lighter, 0.25); +$navbar-item-h-padding: $default-padding * 0.75; +$navbar-avatar-size: 1.75rem; + +/* Aside: Bulma override */ +$menu-item-radius: 0; +$menu-list-link-padding: $size-base * 0.5 0; +$menu-label-color: lighten($base-color, 25%); +$menu-item-color: lighten($base-color, 30%); +$menu-item-hover-color: $white; +$menu-item-hover-background-color: darken($base-color, 3.5%); +$menu-item-active-color: $white; +$menu-item-active-background-color: darken($base-color, 2.5%); + +/* Aside: specifics */ +$aside-width: $size-base * 14; +$aside-mobile-width: $size-base * 15; +$aside-icon-width: $size-base * 3; +$aside-submenu-font-size: $size-base * 0.95; +$aside-box-shadow: none; +$aside-background-color: $base-color; +$aside-tools-background-color: darken($aside-background-color, 10%); +$aside-tools-color: $white; + +/* Title Bar: specifics */ +$title-bar-color: $grey; +$title-bar-active-color: $black-ter; + +/* Hero Bar: specifics */ +$hero-bar-background: $white; + +/* Card: Bulma override */ +$card-shadow: none; +$card-header-shadow: none; + +/* Card: specifics */ +$card-border: 1px solid $base-color-light; +$card-header-border-bottom-color: $base-color-light; + +/* Table: Bulma override */ +$table-cell-border: 1px solid $white-bis; + +/* Table: specifics */ +$table-avatar-size: $size-base * 1.5; +$table-avatar-size-mobile: 25vw; + +/* Form */ +$checkbox-border: 1px solid $base-color; + +/* Modal card: Bulma override */ +$modal-card-head-background-color: $white-ter; +$modal-card-title-size: $size-base; +$modal-card-body-padding: $default-padding 20px; +$modal-card-head-border-bottom: 1px solid $white-ter; +$modal-card-foot-border-top: 0; + +/* Modal card: specifics */ +$modal-card-width: 80vw; +$modal-card-width-mobile: 90vw; +$modal-card-foot-background-color: $white-ter; + +/* Notification: Bulma override */ +$notification-padding: $default-padding * 0.75 $default-padding; + +/* Footer: Bulma override */ +$footer-background-color: $white; +$footer-padding: $default-padding * 0.33 $default-padding; + +/* Footer: specifics */ +$footer-logo-height: $size-base * 2; + +/* Progress: Bulma override */ +$progress-bar-background-color: $grey-lighter; + +/* Icon: specifics */ +$icon-update-mark-size: $size-base * 0.5; +$icon-update-mark-color: $yellow; + +$input-disabled-border-color: $grey-lighter; +$table-row-hover-background-color: hsl(0, 0%, 80%); + +.menu-list { + div { + border-radius: $menu-item-radius; + color: $menu-item-color; + display: block; + padding: $menu-list-link-padding; + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/_tiles.scss b/packages/auditor-backoffice-ui/src/scss/_tiles.scss new file mode 100644 index 000000000..94dd6c21d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_tiles.scss @@ -0,0 +1,24 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +.is-tiles-wrapper { + margin-bottom: $default-padding; +} diff --git a/packages/auditor-backoffice-ui/src/scss/_title-bar.scss b/packages/auditor-backoffice-ui/src/scss/_title-bar.scss new file mode 100644 index 000000000..bac3f6b42 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/_title-bar.scss @@ -0,0 +1,50 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +section.section.is-title-bar { + padding: $default-padding; + border-bottom: $light-border; + + ul { + li { + display: inline-block; + padding: 0 $default-padding * 0.5 0 0; + font-size: $default-padding; + color: $title-bar-color; + + &:after { + display: inline-block; + content: "/"; + padding-left: $default-padding * 0.5; + } + + &:last-child { + padding-right: 0; + font-weight: 900; + color: $title-bar-active-color; + + &:after { + display: none; + } + } + } + } +} diff --git a/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf b/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf Binary files differnew file mode 100644 index 000000000..7665ee336 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf diff --git a/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css b/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css new file mode 100644 index 000000000..a578506e8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css @@ -0,0 +1,22 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: url(./XRXV3I6Li01BKofINeaE.ttf) format("truetype"); +} diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot Binary files differnew file mode 100644 index 000000000..ab6b25ded --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf Binary files differnew file mode 100644 index 000000000..824be10fa --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff Binary files differnew file mode 100644 index 000000000..7e087c1de --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 Binary files differnew file mode 100644 index 000000000..b5caa4ddc --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 diff --git a/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css b/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css new file mode 100644 index 000000000..2b8a2b244 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css @@ -0,0 +1,15109 @@ +@font-face { + font-family: "Material Design Icons"; + src: url("./fonts/materialdesignicons-webfont-4.9.95.eot"); + src: url("./fonts/materialdesignicons-webfont-4.9.95.woff2") format("woff2"), + url("./fonts/materialdesignicons-webfont-4.9.95.woff") format("woff"), + url("./fonts/materialdesignicons-webfont-4.9.95.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} +.mdi:before, +.mdi-set { + display: inline-block; + font: normal normal normal 24px/1 "Material Design Icons"; + font-size: inherit; + text-rendering: auto; + line-height: inherit; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.mdi-ab-testing::before { + content: "\F001C"; +} +.mdi-abjad-arabic::before { + content: "\F0353"; +} +.mdi-abjad-hebrew::before { + content: "\F0354"; +} +.mdi-abugida-devanagari::before { + content: "\F0355"; +} +.mdi-abugida-thai::before { + content: "\F0356"; +} +.mdi-access-point::before { + content: "\F002"; +} +.mdi-access-point-network::before { + content: "\F003"; +} +.mdi-access-point-network-off::before { + content: "\FBBD"; +} +.mdi-account::before { + content: "\F004"; +} +.mdi-account-alert::before { + content: "\F005"; +} +.mdi-account-alert-outline::before { + content: "\FB2C"; +} +.mdi-account-arrow-left::before { + content: "\FB2D"; +} +.mdi-account-arrow-left-outline::before { + content: "\FB2E"; +} +.mdi-account-arrow-right::before { + content: "\FB2F"; +} +.mdi-account-arrow-right-outline::before { + content: "\FB30"; +} +.mdi-account-badge::before { + content: "\FD83"; +} +.mdi-account-badge-alert::before { + content: "\FD84"; +} +.mdi-account-badge-alert-outline::before { + content: "\FD85"; +} +.mdi-account-badge-horizontal::before { + content: "\FDF0"; +} +.mdi-account-badge-horizontal-outline::before { + content: "\FDF1"; +} +.mdi-account-badge-outline::before { + content: "\FD86"; +} +.mdi-account-box::before { + content: "\F006"; +} +.mdi-account-box-multiple::before { + content: "\F933"; +} +.mdi-account-box-multiple-outline::before { + content: "\F002C"; +} +.mdi-account-box-outline::before { + content: "\F007"; +} +.mdi-account-cancel::before { + content: "\F030A"; +} +.mdi-account-cancel-outline::before { + content: "\F030B"; +} +.mdi-account-card-details::before { + content: "\F5D2"; +} +.mdi-account-card-details-outline::before { + content: "\FD87"; +} +.mdi-account-cash::before { + content: "\F00C2"; +} +.mdi-account-cash-outline::before { + content: "\F00C3"; +} +.mdi-account-check::before { + content: "\F008"; +} +.mdi-account-check-outline::before { + content: "\FBBE"; +} +.mdi-account-child::before { + content: "\FA88"; +} +.mdi-account-child-circle::before { + content: "\FA89"; +} +.mdi-account-child-outline::before { + content: "\F00F3"; +} +.mdi-account-circle::before { + content: "\F009"; +} +.mdi-account-circle-outline::before { + content: "\FB31"; +} +.mdi-account-clock::before { + content: "\FB32"; +} +.mdi-account-clock-outline::before { + content: "\FB33"; +} +.mdi-account-cog::before { + content: "\F039B"; +} +.mdi-account-cog-outline::before { + content: "\F039C"; +} +.mdi-account-convert::before { + content: "\F00A"; +} +.mdi-account-convert-outline::before { + content: "\F032C"; +} +.mdi-account-details::before { + content: "\F631"; +} +.mdi-account-details-outline::before { + content: "\F039D"; +} +.mdi-account-edit::before { + content: "\F6BB"; +} +.mdi-account-edit-outline::before { + content: "\F001D"; +} +.mdi-account-group::before { + content: "\F848"; +} +.mdi-account-group-outline::before { + content: "\FB34"; +} +.mdi-account-heart::before { + content: "\F898"; +} +.mdi-account-heart-outline::before { + content: "\FBBF"; +} +.mdi-account-key::before { + content: "\F00B"; +} +.mdi-account-key-outline::before { + content: "\FBC0"; +} +.mdi-account-lock::before { + content: "\F0189"; +} +.mdi-account-lock-outline::before { + content: "\F018A"; +} +.mdi-account-minus::before { + content: "\F00D"; +} +.mdi-account-minus-outline::before { + content: "\FAEB"; +} +.mdi-account-multiple::before { + content: "\F00E"; +} +.mdi-account-multiple-check::before { + content: "\F8C4"; +} +.mdi-account-multiple-check-outline::before { + content: "\F0229"; +} +.mdi-account-multiple-minus::before { + content: "\F5D3"; +} +.mdi-account-multiple-minus-outline::before { + content: "\FBC1"; +} +.mdi-account-multiple-outline::before { + content: "\F00F"; +} +.mdi-account-multiple-plus::before { + content: "\F010"; +} +.mdi-account-multiple-plus-outline::before { + content: "\F7FF"; +} +.mdi-account-multiple-remove::before { + content: "\F0235"; +} +.mdi-account-multiple-remove-outline::before { + content: "\F0236"; +} +.mdi-account-network::before { + content: "\F011"; +} +.mdi-account-network-outline::before { + content: "\FBC2"; +} +.mdi-account-off::before { + content: "\F012"; +} +.mdi-account-off-outline::before { + content: "\FBC3"; +} +.mdi-account-outline::before { + content: "\F013"; +} +.mdi-account-plus::before { + content: "\F014"; +} +.mdi-account-plus-outline::before { + content: "\F800"; +} +.mdi-account-question::before { + content: "\FB35"; +} +.mdi-account-question-outline::before { + content: "\FB36"; +} +.mdi-account-remove::before { + content: "\F015"; +} +.mdi-account-remove-outline::before { + content: "\FAEC"; +} +.mdi-account-search::before { + content: "\F016"; +} +.mdi-account-search-outline::before { + content: "\F934"; +} +.mdi-account-settings::before { + content: "\F630"; +} +.mdi-account-settings-outline::before { + content: "\F00F4"; +} +.mdi-account-star::before { + content: "\F017"; +} +.mdi-account-star-outline::before { + content: "\FBC4"; +} +.mdi-account-supervisor::before { + content: "\FA8A"; +} +.mdi-account-supervisor-circle::before { + content: "\FA8B"; +} +.mdi-account-supervisor-outline::before { + content: "\F0158"; +} +.mdi-account-switch::before { + content: "\F019"; +} +.mdi-account-tie::before { + content: "\FCBF"; +} +.mdi-account-tie-outline::before { + content: "\F00F5"; +} +.mdi-account-tie-voice::before { + content: "\F0333"; +} +.mdi-account-tie-voice-off::before { + content: "\F0335"; +} +.mdi-account-tie-voice-off-outline::before { + content: "\F0336"; +} +.mdi-account-tie-voice-outline::before { + content: "\F0334"; +} +.mdi-accusoft::before { + content: "\F849"; +} +.mdi-adjust::before { + content: "\F01A"; +} +.mdi-adobe::before { + content: "\F935"; +} +.mdi-adobe-acrobat::before { + content: "\FFBD"; +} +.mdi-air-conditioner::before { + content: "\F01B"; +} +.mdi-air-filter::before { + content: "\FD1F"; +} +.mdi-air-horn::before { + content: "\FD88"; +} +.mdi-air-humidifier::before { + content: "\F00C4"; +} +.mdi-air-purifier::before { + content: "\FD20"; +} +.mdi-airbag::before { + content: "\FBC5"; +} +.mdi-airballoon::before { + content: "\F01C"; +} +.mdi-airballoon-outline::before { + content: "\F002D"; +} +.mdi-airplane::before { + content: "\F01D"; +} +.mdi-airplane-landing::before { + content: "\F5D4"; +} +.mdi-airplane-off::before { + content: "\F01E"; +} +.mdi-airplane-takeoff::before { + content: "\F5D5"; +} +.mdi-airplay::before { + content: "\F01F"; +} +.mdi-airport::before { + content: "\F84A"; +} +.mdi-alarm::before { + content: "\F020"; +} +.mdi-alarm-bell::before { + content: "\F78D"; +} +.mdi-alarm-check::before { + content: "\F021"; +} +.mdi-alarm-light::before { + content: "\F78E"; +} +.mdi-alarm-light-outline::before { + content: "\FBC6"; +} +.mdi-alarm-multiple::before { + content: "\F022"; +} +.mdi-alarm-note::before { + content: "\FE8E"; +} +.mdi-alarm-note-off::before { + content: "\FE8F"; +} +.mdi-alarm-off::before { + content: "\F023"; +} +.mdi-alarm-plus::before { + content: "\F024"; +} +.mdi-alarm-snooze::before { + content: "\F68D"; +} +.mdi-album::before { + content: "\F025"; +} +.mdi-alert::before { + content: "\F026"; +} +.mdi-alert-box::before { + content: "\F027"; +} +.mdi-alert-box-outline::before { + content: "\FCC0"; +} +.mdi-alert-circle::before { + content: "\F028"; +} +.mdi-alert-circle-check::before { + content: "\F0218"; +} +.mdi-alert-circle-check-outline::before { + content: "\F0219"; +} +.mdi-alert-circle-outline::before { + content: "\F5D6"; +} +.mdi-alert-decagram::before { + content: "\F6BC"; +} +.mdi-alert-decagram-outline::before { + content: "\FCC1"; +} +.mdi-alert-octagon::before { + content: "\F029"; +} +.mdi-alert-octagon-outline::before { + content: "\FCC2"; +} +.mdi-alert-octagram::before { + content: "\F766"; +} +.mdi-alert-octagram-outline::before { + content: "\FCC3"; +} +.mdi-alert-outline::before { + content: "\F02A"; +} +.mdi-alert-rhombus::before { + content: "\F01F9"; +} +.mdi-alert-rhombus-outline::before { + content: "\F01FA"; +} +.mdi-alien::before { + content: "\F899"; +} +.mdi-alien-outline::before { + content: "\F00F6"; +} +.mdi-align-horizontal-center::before { + content: "\F01EE"; +} +.mdi-align-horizontal-left::before { + content: "\F01ED"; +} +.mdi-align-horizontal-right::before { + content: "\F01EF"; +} +.mdi-align-vertical-bottom::before { + content: "\F01F0"; +} +.mdi-align-vertical-center::before { + content: "\F01F1"; +} +.mdi-align-vertical-top::before { + content: "\F01F2"; +} +.mdi-all-inclusive::before { + content: "\F6BD"; +} +.mdi-allergy::before { + content: "\F0283"; +} +.mdi-alpha::before { + content: "\F02B"; +} +.mdi-alpha-a::before { + content: "\41"; +} +.mdi-alpha-a-box::before { + content: "\FAED"; +} +.mdi-alpha-a-box-outline::before { + content: "\FBC7"; +} +.mdi-alpha-a-circle::before { + content: "\FBC8"; +} +.mdi-alpha-a-circle-outline::before { + content: "\FBC9"; +} +.mdi-alpha-b::before { + content: "\42"; +} +.mdi-alpha-b-box::before { + content: "\FAEE"; +} +.mdi-alpha-b-box-outline::before { + content: "\FBCA"; +} +.mdi-alpha-b-circle::before { + content: "\FBCB"; +} +.mdi-alpha-b-circle-outline::before { + content: "\FBCC"; +} +.mdi-alpha-c::before { + content: "\43"; +} +.mdi-alpha-c-box::before { + content: "\FAEF"; +} +.mdi-alpha-c-box-outline::before { + content: "\FBCD"; +} +.mdi-alpha-c-circle::before { + content: "\FBCE"; +} +.mdi-alpha-c-circle-outline::before { + content: "\FBCF"; +} +.mdi-alpha-d::before { + content: "\44"; +} +.mdi-alpha-d-box::before { + content: "\FAF0"; +} +.mdi-alpha-d-box-outline::before { + content: "\FBD0"; +} +.mdi-alpha-d-circle::before { + content: "\FBD1"; +} +.mdi-alpha-d-circle-outline::before { + content: "\FBD2"; +} +.mdi-alpha-e::before { + content: "\45"; +} +.mdi-alpha-e-box::before { + content: "\FAF1"; +} +.mdi-alpha-e-box-outline::before { + content: "\FBD3"; +} +.mdi-alpha-e-circle::before { + content: "\FBD4"; +} +.mdi-alpha-e-circle-outline::before { + content: "\FBD5"; +} +.mdi-alpha-f::before { + content: "\46"; +} +.mdi-alpha-f-box::before { + content: "\FAF2"; +} +.mdi-alpha-f-box-outline::before { + content: "\FBD6"; +} +.mdi-alpha-f-circle::before { + content: "\FBD7"; +} +.mdi-alpha-f-circle-outline::before { + content: "\FBD8"; +} +.mdi-alpha-g::before { + content: "\47"; +} +.mdi-alpha-g-box::before { + content: "\FAF3"; +} +.mdi-alpha-g-box-outline::before { + content: "\FBD9"; +} +.mdi-alpha-g-circle::before { + content: "\FBDA"; +} +.mdi-alpha-g-circle-outline::before { + content: "\FBDB"; +} +.mdi-alpha-h::before { + content: "\48"; +} +.mdi-alpha-h-box::before { + content: "\FAF4"; +} +.mdi-alpha-h-box-outline::before { + content: "\FBDC"; +} +.mdi-alpha-h-circle::before { + content: "\FBDD"; +} +.mdi-alpha-h-circle-outline::before { + content: "\FBDE"; +} +.mdi-alpha-i::before { + content: "\49"; +} +.mdi-alpha-i-box::before { + content: "\FAF5"; +} +.mdi-alpha-i-box-outline::before { + content: "\FBDF"; +} +.mdi-alpha-i-circle::before { + content: "\FBE0"; +} +.mdi-alpha-i-circle-outline::before { + content: "\FBE1"; +} +.mdi-alpha-j::before { + content: "\4A"; +} +.mdi-alpha-j-box::before { + content: "\FAF6"; +} +.mdi-alpha-j-box-outline::before { + content: "\FBE2"; +} +.mdi-alpha-j-circle::before { + content: "\FBE3"; +} +.mdi-alpha-j-circle-outline::before { + content: "\FBE4"; +} +.mdi-alpha-k::before { + content: "\4B"; +} +.mdi-alpha-k-box::before { + content: "\FAF7"; +} +.mdi-alpha-k-box-outline::before { + content: "\FBE5"; +} +.mdi-alpha-k-circle::before { + content: "\FBE6"; +} +.mdi-alpha-k-circle-outline::before { + content: "\FBE7"; +} +.mdi-alpha-l::before { + content: "\4C"; +} +.mdi-alpha-l-box::before { + content: "\FAF8"; +} +.mdi-alpha-l-box-outline::before { + content: "\FBE8"; +} +.mdi-alpha-l-circle::before { + content: "\FBE9"; +} +.mdi-alpha-l-circle-outline::before { + content: "\FBEA"; +} +.mdi-alpha-m::before { + content: "\4D"; +} +.mdi-alpha-m-box::before { + content: "\FAF9"; +} +.mdi-alpha-m-box-outline::before { + content: "\FBEB"; +} +.mdi-alpha-m-circle::before { + content: "\FBEC"; +} +.mdi-alpha-m-circle-outline::before { + content: "\FBED"; +} +.mdi-alpha-n::before { + content: "\4E"; +} +.mdi-alpha-n-box::before { + content: "\FAFA"; +} +.mdi-alpha-n-box-outline::before { + content: "\FBEE"; +} +.mdi-alpha-n-circle::before { + content: "\FBEF"; +} +.mdi-alpha-n-circle-outline::before { + content: "\FBF0"; +} +.mdi-alpha-o::before { + content: "\4F"; +} +.mdi-alpha-o-box::before { + content: "\FAFB"; +} +.mdi-alpha-o-box-outline::before { + content: "\FBF1"; +} +.mdi-alpha-o-circle::before { + content: "\FBF2"; +} +.mdi-alpha-o-circle-outline::before { + content: "\FBF3"; +} +.mdi-alpha-p::before { + content: "\50"; +} +.mdi-alpha-p-box::before { + content: "\FAFC"; +} +.mdi-alpha-p-box-outline::before { + content: "\FBF4"; +} +.mdi-alpha-p-circle::before { + content: "\FBF5"; +} +.mdi-alpha-p-circle-outline::before { + content: "\FBF6"; +} +.mdi-alpha-q::before { + content: "\51"; +} +.mdi-alpha-q-box::before { + content: "\FAFD"; +} +.mdi-alpha-q-box-outline::before { + content: "\FBF7"; +} +.mdi-alpha-q-circle::before { + content: "\FBF8"; +} +.mdi-alpha-q-circle-outline::before { + content: "\FBF9"; +} +.mdi-alpha-r::before { + content: "\52"; +} +.mdi-alpha-r-box::before { + content: "\FAFE"; +} +.mdi-alpha-r-box-outline::before { + content: "\FBFA"; +} +.mdi-alpha-r-circle::before { + content: "\FBFB"; +} +.mdi-alpha-r-circle-outline::before { + content: "\FBFC"; +} +.mdi-alpha-s::before { + content: "\53"; +} +.mdi-alpha-s-box::before { + content: "\FAFF"; +} +.mdi-alpha-s-box-outline::before { + content: "\FBFD"; +} +.mdi-alpha-s-circle::before { + content: "\FBFE"; +} +.mdi-alpha-s-circle-outline::before { + content: "\FBFF"; +} +.mdi-alpha-t::before { + content: "\54"; +} +.mdi-alpha-t-box::before { + content: "\FB00"; +} +.mdi-alpha-t-box-outline::before { + content: "\FC00"; +} +.mdi-alpha-t-circle::before { + content: "\FC01"; +} +.mdi-alpha-t-circle-outline::before { + content: "\FC02"; +} +.mdi-alpha-u::before { + content: "\55"; +} +.mdi-alpha-u-box::before { + content: "\FB01"; +} +.mdi-alpha-u-box-outline::before { + content: "\FC03"; +} +.mdi-alpha-u-circle::before { + content: "\FC04"; +} +.mdi-alpha-u-circle-outline::before { + content: "\FC05"; +} +.mdi-alpha-v::before { + content: "\56"; +} +.mdi-alpha-v-box::before { + content: "\FB02"; +} +.mdi-alpha-v-box-outline::before { + content: "\FC06"; +} +.mdi-alpha-v-circle::before { + content: "\FC07"; +} +.mdi-alpha-v-circle-outline::before { + content: "\FC08"; +} +.mdi-alpha-w::before { + content: "\57"; +} +.mdi-alpha-w-box::before { + content: "\FB03"; +} +.mdi-alpha-w-box-outline::before { + content: "\FC09"; +} +.mdi-alpha-w-circle::before { + content: "\FC0A"; +} +.mdi-alpha-w-circle-outline::before { + content: "\FC0B"; +} +.mdi-alpha-x::before { + content: "\58"; +} +.mdi-alpha-x-box::before { + content: "\FB04"; +} +.mdi-alpha-x-box-outline::before { + content: "\FC0C"; +} +.mdi-alpha-x-circle::before { + content: "\FC0D"; +} +.mdi-alpha-x-circle-outline::before { + content: "\FC0E"; +} +.mdi-alpha-y::before { + content: "\59"; +} +.mdi-alpha-y-box::before { + content: "\FB05"; +} +.mdi-alpha-y-box-outline::before { + content: "\FC0F"; +} +.mdi-alpha-y-circle::before { + content: "\FC10"; +} +.mdi-alpha-y-circle-outline::before { + content: "\FC11"; +} +.mdi-alpha-z::before { + content: "\5A"; +} +.mdi-alpha-z-box::before { + content: "\FB06"; +} +.mdi-alpha-z-box-outline::before { + content: "\FC12"; +} +.mdi-alpha-z-circle::before { + content: "\FC13"; +} +.mdi-alpha-z-circle-outline::before { + content: "\FC14"; +} +.mdi-alphabet-aurebesh::before { + content: "\F0357"; +} +.mdi-alphabet-cyrillic::before { + content: "\F0358"; +} +.mdi-alphabet-greek::before { + content: "\F0359"; +} +.mdi-alphabet-latin::before { + content: "\F035A"; +} +.mdi-alphabet-piqad::before { + content: "\F035B"; +} +.mdi-alphabet-tengwar::before { + content: "\F0362"; +} +.mdi-alphabetical::before { + content: "\F02C"; +} +.mdi-alphabetical-off::before { + content: "\F002E"; +} +.mdi-alphabetical-variant::before { + content: "\F002F"; +} +.mdi-alphabetical-variant-off::before { + content: "\F0030"; +} +.mdi-altimeter::before { + content: "\F5D7"; +} +.mdi-amazon::before { + content: "\F02D"; +} +.mdi-amazon-alexa::before { + content: "\F8C5"; +} +.mdi-amazon-drive::before { + content: "\F02E"; +} +.mdi-ambulance::before { + content: "\F02F"; +} +.mdi-ammunition::before { + content: "\FCC4"; +} +.mdi-ampersand::before { + content: "\FA8C"; +} +.mdi-amplifier::before { + content: "\F030"; +} +.mdi-amplifier-off::before { + content: "\F01E0"; +} +.mdi-anchor::before { + content: "\F031"; +} +.mdi-android::before { + content: "\F032"; +} +.mdi-android-auto::before { + content: "\FA8D"; +} +.mdi-android-debug-bridge::before { + content: "\F033"; +} +.mdi-android-head::before { + content: "\F78F"; +} +.mdi-android-messages::before { + content: "\FD21"; +} +.mdi-android-studio::before { + content: "\F034"; +} +.mdi-angle-acute::before { + content: "\F936"; +} +.mdi-angle-obtuse::before { + content: "\F937"; +} +.mdi-angle-right::before { + content: "\F938"; +} +.mdi-angular::before { + content: "\F6B1"; +} +.mdi-angularjs::before { + content: "\F6BE"; +} +.mdi-animation::before { + content: "\F5D8"; +} +.mdi-animation-outline::before { + content: "\FA8E"; +} +.mdi-animation-play::before { + content: "\F939"; +} +.mdi-animation-play-outline::before { + content: "\FA8F"; +} +.mdi-ansible::before { + content: "\F00C5"; +} +.mdi-antenna::before { + content: "\F0144"; +} +.mdi-anvil::before { + content: "\F89A"; +} +.mdi-apache-kafka::before { + content: "\F0031"; +} +.mdi-api::before { + content: "\F00C6"; +} +.mdi-api-off::before { + content: "\F0282"; +} +.mdi-apple::before { + content: "\F035"; +} +.mdi-apple-finder::before { + content: "\F036"; +} +.mdi-apple-icloud::before { + content: "\F038"; +} +.mdi-apple-ios::before { + content: "\F037"; +} +.mdi-apple-keyboard-caps::before { + content: "\F632"; +} +.mdi-apple-keyboard-command::before { + content: "\F633"; +} +.mdi-apple-keyboard-control::before { + content: "\F634"; +} +.mdi-apple-keyboard-option::before { + content: "\F635"; +} +.mdi-apple-keyboard-shift::before { + content: "\F636"; +} +.mdi-apple-safari::before { + content: "\F039"; +} +.mdi-application::before { + content: "\F614"; +} +.mdi-application-export::before { + content: "\FD89"; +} +.mdi-application-import::before { + content: "\FD8A"; +} +.mdi-approximately-equal::before { + content: "\FFBE"; +} +.mdi-approximately-equal-box::before { + content: "\FFBF"; +} +.mdi-apps::before { + content: "\F03B"; +} +.mdi-apps-box::before { + content: "\FD22"; +} +.mdi-arch::before { + content: "\F8C6"; +} +.mdi-archive::before { + content: "\F03C"; +} +.mdi-archive-arrow-down::before { + content: "\F0284"; +} +.mdi-archive-arrow-down-outline::before { + content: "\F0285"; +} +.mdi-archive-arrow-up::before { + content: "\F0286"; +} +.mdi-archive-arrow-up-outline::before { + content: "\F0287"; +} +.mdi-archive-outline::before { + content: "\F0239"; +} +.mdi-arm-flex::before { + content: "\F008F"; +} +.mdi-arm-flex-outline::before { + content: "\F0090"; +} +.mdi-arrange-bring-forward::before { + content: "\F03D"; +} +.mdi-arrange-bring-to-front::before { + content: "\F03E"; +} +.mdi-arrange-send-backward::before { + content: "\F03F"; +} +.mdi-arrange-send-to-back::before { + content: "\F040"; +} +.mdi-arrow-all::before { + content: "\F041"; +} +.mdi-arrow-bottom-left::before { + content: "\F042"; +} +.mdi-arrow-bottom-left-bold-outline::before { + content: "\F9B6"; +} +.mdi-arrow-bottom-left-thick::before { + content: "\F9B7"; +} +.mdi-arrow-bottom-right::before { + content: "\F043"; +} +.mdi-arrow-bottom-right-bold-outline::before { + content: "\F9B8"; +} +.mdi-arrow-bottom-right-thick::before { + content: "\F9B9"; +} +.mdi-arrow-collapse::before { + content: "\F615"; +} +.mdi-arrow-collapse-all::before { + content: "\F044"; +} +.mdi-arrow-collapse-down::before { + content: "\F791"; +} +.mdi-arrow-collapse-horizontal::before { + content: "\F84B"; +} +.mdi-arrow-collapse-left::before { + content: "\F792"; +} +.mdi-arrow-collapse-right::before { + content: "\F793"; +} +.mdi-arrow-collapse-up::before { + content: "\F794"; +} +.mdi-arrow-collapse-vertical::before { + content: "\F84C"; +} +.mdi-arrow-decision::before { + content: "\F9BA"; +} +.mdi-arrow-decision-auto::before { + content: "\F9BB"; +} +.mdi-arrow-decision-auto-outline::before { + content: "\F9BC"; +} +.mdi-arrow-decision-outline::before { + content: "\F9BD"; +} +.mdi-arrow-down::before { + content: "\F045"; +} +.mdi-arrow-down-bold::before { + content: "\F72D"; +} +.mdi-arrow-down-bold-box::before { + content: "\F72E"; +} +.mdi-arrow-down-bold-box-outline::before { + content: "\F72F"; +} +.mdi-arrow-down-bold-circle::before { + content: "\F047"; +} +.mdi-arrow-down-bold-circle-outline::before { + content: "\F048"; +} +.mdi-arrow-down-bold-hexagon-outline::before { + content: "\F049"; +} +.mdi-arrow-down-bold-outline::before { + content: "\F9BE"; +} +.mdi-arrow-down-box::before { + content: "\F6BF"; +} +.mdi-arrow-down-circle::before { + content: "\FCB7"; +} +.mdi-arrow-down-circle-outline::before { + content: "\FCB8"; +} +.mdi-arrow-down-drop-circle::before { + content: "\F04A"; +} +.mdi-arrow-down-drop-circle-outline::before { + content: "\F04B"; +} +.mdi-arrow-down-thick::before { + content: "\F046"; +} +.mdi-arrow-expand::before { + content: "\F616"; +} +.mdi-arrow-expand-all::before { + content: "\F04C"; +} +.mdi-arrow-expand-down::before { + content: "\F795"; +} +.mdi-arrow-expand-horizontal::before { + content: "\F84D"; +} +.mdi-arrow-expand-left::before { + content: "\F796"; +} +.mdi-arrow-expand-right::before { + content: "\F797"; +} +.mdi-arrow-expand-up::before { + content: "\F798"; +} +.mdi-arrow-expand-vertical::before { + content: "\F84E"; +} +.mdi-arrow-horizontal-lock::before { + content: "\F0186"; +} +.mdi-arrow-left::before { + content: "\F04D"; +} +.mdi-arrow-left-bold::before { + content: "\F730"; +} +.mdi-arrow-left-bold-box::before { + content: "\F731"; +} +.mdi-arrow-left-bold-box-outline::before { + content: "\F732"; +} +.mdi-arrow-left-bold-circle::before { + content: "\F04F"; +} +.mdi-arrow-left-bold-circle-outline::before { + content: "\F050"; +} +.mdi-arrow-left-bold-hexagon-outline::before { + content: "\F051"; +} +.mdi-arrow-left-bold-outline::before { + content: "\F9BF"; +} +.mdi-arrow-left-box::before { + content: "\F6C0"; +} +.mdi-arrow-left-circle::before { + content: "\FCB9"; +} +.mdi-arrow-left-circle-outline::before { + content: "\FCBA"; +} +.mdi-arrow-left-drop-circle::before { + content: "\F052"; +} +.mdi-arrow-left-drop-circle-outline::before { + content: "\F053"; +} +.mdi-arrow-left-right::before { + content: "\FE90"; +} +.mdi-arrow-left-right-bold::before { + content: "\FE91"; +} +.mdi-arrow-left-right-bold-outline::before { + content: "\F9C0"; +} +.mdi-arrow-left-thick::before { + content: "\F04E"; +} +.mdi-arrow-right::before { + content: "\F054"; +} +.mdi-arrow-right-bold::before { + content: "\F733"; +} +.mdi-arrow-right-bold-box::before { + content: "\F734"; +} +.mdi-arrow-right-bold-box-outline::before { + content: "\F735"; +} +.mdi-arrow-right-bold-circle::before { + content: "\F056"; +} +.mdi-arrow-right-bold-circle-outline::before { + content: "\F057"; +} +.mdi-arrow-right-bold-hexagon-outline::before { + content: "\F058"; +} +.mdi-arrow-right-bold-outline::before { + content: "\F9C1"; +} +.mdi-arrow-right-box::before { + content: "\F6C1"; +} +.mdi-arrow-right-circle::before { + content: "\FCBB"; +} +.mdi-arrow-right-circle-outline::before { + content: "\FCBC"; +} +.mdi-arrow-right-drop-circle::before { + content: "\F059"; +} +.mdi-arrow-right-drop-circle-outline::before { + content: "\F05A"; +} +.mdi-arrow-right-thick::before { + content: "\F055"; +} +.mdi-arrow-split-horizontal::before { + content: "\F93A"; +} +.mdi-arrow-split-vertical::before { + content: "\F93B"; +} +.mdi-arrow-top-left::before { + content: "\F05B"; +} +.mdi-arrow-top-left-bold-outline::before { + content: "\F9C2"; +} +.mdi-arrow-top-left-bottom-right::before { + content: "\FE92"; +} +.mdi-arrow-top-left-bottom-right-bold::before { + content: "\FE93"; +} +.mdi-arrow-top-left-thick::before { + content: "\F9C3"; +} +.mdi-arrow-top-right::before { + content: "\F05C"; +} +.mdi-arrow-top-right-bold-outline::before { + content: "\F9C4"; +} +.mdi-arrow-top-right-bottom-left::before { + content: "\FE94"; +} +.mdi-arrow-top-right-bottom-left-bold::before { + content: "\FE95"; +} +.mdi-arrow-top-right-thick::before { + content: "\F9C5"; +} +.mdi-arrow-up::before { + content: "\F05D"; +} +.mdi-arrow-up-bold::before { + content: "\F736"; +} +.mdi-arrow-up-bold-box::before { + content: "\F737"; +} +.mdi-arrow-up-bold-box-outline::before { + content: "\F738"; +} +.mdi-arrow-up-bold-circle::before { + content: "\F05F"; +} +.mdi-arrow-up-bold-circle-outline::before { + content: "\F060"; +} +.mdi-arrow-up-bold-hexagon-outline::before { + content: "\F061"; +} +.mdi-arrow-up-bold-outline::before { + content: "\F9C6"; +} +.mdi-arrow-up-box::before { + content: "\F6C2"; +} +.mdi-arrow-up-circle::before { + content: "\FCBD"; +} +.mdi-arrow-up-circle-outline::before { + content: "\FCBE"; +} +.mdi-arrow-up-down::before { + content: "\FE96"; +} +.mdi-arrow-up-down-bold::before { + content: "\FE97"; +} +.mdi-arrow-up-down-bold-outline::before { + content: "\F9C7"; +} +.mdi-arrow-up-drop-circle::before { + content: "\F062"; +} +.mdi-arrow-up-drop-circle-outline::before { + content: "\F063"; +} +.mdi-arrow-up-thick::before { + content: "\F05E"; +} +.mdi-arrow-vertical-lock::before { + content: "\F0187"; +} +.mdi-artist::before { + content: "\F802"; +} +.mdi-artist-outline::before { + content: "\FCC5"; +} +.mdi-artstation::before { + content: "\FB37"; +} +.mdi-aspect-ratio::before { + content: "\FA23"; +} +.mdi-assistant::before { + content: "\F064"; +} +.mdi-asterisk::before { + content: "\F6C3"; +} +.mdi-at::before { + content: "\F065"; +} +.mdi-atlassian::before { + content: "\F803"; +} +.mdi-atm::before { + content: "\FD23"; +} +.mdi-atom::before { + content: "\F767"; +} +.mdi-atom-variant::before { + content: "\FE98"; +} +.mdi-attachment::before { + content: "\F066"; +} +.mdi-audio-video::before { + content: "\F93C"; +} +.mdi-audio-video-off::before { + content: "\F01E1"; +} +.mdi-audiobook::before { + content: "\F067"; +} +.mdi-augmented-reality::before { + content: "\F84F"; +} +.mdi-auto-download::before { + content: "\F03A9"; +} +.mdi-auto-fix::before { + content: "\F068"; +} +.mdi-auto-upload::before { + content: "\F069"; +} +.mdi-autorenew::before { + content: "\F06A"; +} +.mdi-av-timer::before { + content: "\F06B"; +} +.mdi-aws::before { + content: "\FDF2"; +} +.mdi-axe::before { + content: "\F8C7"; +} +.mdi-axis::before { + content: "\FD24"; +} +.mdi-axis-arrow::before { + content: "\FD25"; +} +.mdi-axis-arrow-lock::before { + content: "\FD26"; +} +.mdi-axis-lock::before { + content: "\FD27"; +} +.mdi-axis-x-arrow::before { + content: "\FD28"; +} +.mdi-axis-x-arrow-lock::before { + content: "\FD29"; +} +.mdi-axis-x-rotate-clockwise::before { + content: "\FD2A"; +} +.mdi-axis-x-rotate-counterclockwise::before { + content: "\FD2B"; +} +.mdi-axis-x-y-arrow-lock::before { + content: "\FD2C"; +} +.mdi-axis-y-arrow::before { + content: "\FD2D"; +} +.mdi-axis-y-arrow-lock::before { + content: "\FD2E"; +} +.mdi-axis-y-rotate-clockwise::before { + content: "\FD2F"; +} +.mdi-axis-y-rotate-counterclockwise::before { + content: "\FD30"; +} +.mdi-axis-z-arrow::before { + content: "\FD31"; +} +.mdi-axis-z-arrow-lock::before { + content: "\FD32"; +} +.mdi-axis-z-rotate-clockwise::before { + content: "\FD33"; +} +.mdi-axis-z-rotate-counterclockwise::before { + content: "\FD34"; +} +.mdi-azure::before { + content: "\F804"; +} +.mdi-azure-devops::before { + content: "\F0091"; +} +.mdi-babel::before { + content: "\FA24"; +} +.mdi-baby::before { + content: "\F06C"; +} +.mdi-baby-bottle::before { + content: "\FF56"; +} +.mdi-baby-bottle-outline::before { + content: "\FF57"; +} +.mdi-baby-carriage::before { + content: "\F68E"; +} +.mdi-baby-carriage-off::before { + content: "\FFC0"; +} +.mdi-baby-face::before { + content: "\FE99"; +} +.mdi-baby-face-outline::before { + content: "\FE9A"; +} +.mdi-backburger::before { + content: "\F06D"; +} +.mdi-backspace::before { + content: "\F06E"; +} +.mdi-backspace-outline::before { + content: "\FB38"; +} +.mdi-backspace-reverse::before { + content: "\FE9B"; +} +.mdi-backspace-reverse-outline::before { + content: "\FE9C"; +} +.mdi-backup-restore::before { + content: "\F06F"; +} +.mdi-bacteria::before { + content: "\FEF2"; +} +.mdi-bacteria-outline::before { + content: "\FEF3"; +} +.mdi-badminton::before { + content: "\F850"; +} +.mdi-bag-carry-on::before { + content: "\FF58"; +} +.mdi-bag-carry-on-check::before { + content: "\FD41"; +} +.mdi-bag-carry-on-off::before { + content: "\FF59"; +} +.mdi-bag-checked::before { + content: "\FF5A"; +} +.mdi-bag-personal::before { + content: "\FDF3"; +} +.mdi-bag-personal-off::before { + content: "\FDF4"; +} +.mdi-bag-personal-off-outline::before { + content: "\FDF5"; +} +.mdi-bag-personal-outline::before { + content: "\FDF6"; +} +.mdi-baguette::before { + content: "\FF5B"; +} +.mdi-balloon::before { + content: "\FA25"; +} +.mdi-ballot::before { + content: "\F9C8"; +} +.mdi-ballot-outline::before { + content: "\F9C9"; +} +.mdi-ballot-recount::before { + content: "\FC15"; +} +.mdi-ballot-recount-outline::before { + content: "\FC16"; +} +.mdi-bandage::before { + content: "\FD8B"; +} +.mdi-bandcamp::before { + content: "\F674"; +} +.mdi-bank::before { + content: "\F070"; +} +.mdi-bank-minus::before { + content: "\FD8C"; +} +.mdi-bank-outline::before { + content: "\FE9D"; +} +.mdi-bank-plus::before { + content: "\FD8D"; +} +.mdi-bank-remove::before { + content: "\FD8E"; +} +.mdi-bank-transfer::before { + content: "\FA26"; +} +.mdi-bank-transfer-in::before { + content: "\FA27"; +} +.mdi-bank-transfer-out::before { + content: "\FA28"; +} +.mdi-barcode::before { + content: "\F071"; +} +.mdi-barcode-off::before { + content: "\F0261"; +} +.mdi-barcode-scan::before { + content: "\F072"; +} +.mdi-barley::before { + content: "\F073"; +} +.mdi-barley-off::before { + content: "\FB39"; +} +.mdi-barn::before { + content: "\FB3A"; +} +.mdi-barrel::before { + content: "\F074"; +} +.mdi-baseball::before { + content: "\F851"; +} +.mdi-baseball-bat::before { + content: "\F852"; +} +.mdi-basecamp::before { + content: "\F075"; +} +.mdi-bash::before { + content: "\F01AE"; +} +.mdi-basket::before { + content: "\F076"; +} +.mdi-basket-fill::before { + content: "\F077"; +} +.mdi-basket-outline::before { + content: "\F01AC"; +} +.mdi-basket-unfill::before { + content: "\F078"; +} +.mdi-basketball::before { + content: "\F805"; +} +.mdi-basketball-hoop::before { + content: "\FC17"; +} +.mdi-basketball-hoop-outline::before { + content: "\FC18"; +} +.mdi-bat::before { + content: "\FB3B"; +} +.mdi-battery::before { + content: "\F079"; +} +.mdi-battery-10::before { + content: "\F07A"; +} +.mdi-battery-10-bluetooth::before { + content: "\F93D"; +} +.mdi-battery-20::before { + content: "\F07B"; +} +.mdi-battery-20-bluetooth::before { + content: "\F93E"; +} +.mdi-battery-30::before { + content: "\F07C"; +} +.mdi-battery-30-bluetooth::before { + content: "\F93F"; +} +.mdi-battery-40::before { + content: "\F07D"; +} +.mdi-battery-40-bluetooth::before { + content: "\F940"; +} +.mdi-battery-50::before { + content: "\F07E"; +} +.mdi-battery-50-bluetooth::before { + content: "\F941"; +} +.mdi-battery-60::before { + content: "\F07F"; +} +.mdi-battery-60-bluetooth::before { + content: "\F942"; +} +.mdi-battery-70::before { + content: "\F080"; +} +.mdi-battery-70-bluetooth::before { + content: "\F943"; +} +.mdi-battery-80::before { + content: "\F081"; +} +.mdi-battery-80-bluetooth::before { + content: "\F944"; +} +.mdi-battery-90::before { + content: "\F082"; +} +.mdi-battery-90-bluetooth::before { + content: "\F945"; +} +.mdi-battery-alert::before { + content: "\F083"; +} +.mdi-battery-alert-bluetooth::before { + content: "\F946"; +} +.mdi-battery-alert-variant::before { + content: "\F00F7"; +} +.mdi-battery-alert-variant-outline::before { + content: "\F00F8"; +} +.mdi-battery-bluetooth::before { + content: "\F947"; +} +.mdi-battery-bluetooth-variant::before { + content: "\F948"; +} +.mdi-battery-charging::before { + content: "\F084"; +} +.mdi-battery-charging-10::before { + content: "\F89B"; +} +.mdi-battery-charging-100::before { + content: "\F085"; +} +.mdi-battery-charging-20::before { + content: "\F086"; +} +.mdi-battery-charging-30::before { + content: "\F087"; +} +.mdi-battery-charging-40::before { + content: "\F088"; +} +.mdi-battery-charging-50::before { + content: "\F89C"; +} +.mdi-battery-charging-60::before { + content: "\F089"; +} +.mdi-battery-charging-70::before { + content: "\F89D"; +} +.mdi-battery-charging-80::before { + content: "\F08A"; +} +.mdi-battery-charging-90::before { + content: "\F08B"; +} +.mdi-battery-charging-high::before { + content: "\F02D1"; +} +.mdi-battery-charging-low::before { + content: "\F02CF"; +} +.mdi-battery-charging-medium::before { + content: "\F02D0"; +} +.mdi-battery-charging-outline::before { + content: "\F89E"; +} +.mdi-battery-charging-wireless::before { + content: "\F806"; +} +.mdi-battery-charging-wireless-10::before { + content: "\F807"; +} +.mdi-battery-charging-wireless-20::before { + content: "\F808"; +} +.mdi-battery-charging-wireless-30::before { + content: "\F809"; +} +.mdi-battery-charging-wireless-40::before { + content: "\F80A"; +} +.mdi-battery-charging-wireless-50::before { + content: "\F80B"; +} +.mdi-battery-charging-wireless-60::before { + content: "\F80C"; +} +.mdi-battery-charging-wireless-70::before { + content: "\F80D"; +} +.mdi-battery-charging-wireless-80::before { + content: "\F80E"; +} +.mdi-battery-charging-wireless-90::before { + content: "\F80F"; +} +.mdi-battery-charging-wireless-alert::before { + content: "\F810"; +} +.mdi-battery-charging-wireless-outline::before { + content: "\F811"; +} +.mdi-battery-heart::before { + content: "\F023A"; +} +.mdi-battery-heart-outline::before { + content: "\F023B"; +} +.mdi-battery-heart-variant::before { + content: "\F023C"; +} +.mdi-battery-high::before { + content: "\F02CE"; +} +.mdi-battery-low::before { + content: "\F02CC"; +} +.mdi-battery-medium::before { + content: "\F02CD"; +} +.mdi-battery-minus::before { + content: "\F08C"; +} +.mdi-battery-negative::before { + content: "\F08D"; +} +.mdi-battery-off::before { + content: "\F0288"; +} +.mdi-battery-off-outline::before { + content: "\F0289"; +} +.mdi-battery-outline::before { + content: "\F08E"; +} +.mdi-battery-plus::before { + content: "\F08F"; +} +.mdi-battery-positive::before { + content: "\F090"; +} +.mdi-battery-unknown::before { + content: "\F091"; +} +.mdi-battery-unknown-bluetooth::before { + content: "\F949"; +} +.mdi-battlenet::before { + content: "\FB3C"; +} +.mdi-beach::before { + content: "\F092"; +} +.mdi-beaker::before { + content: "\FCC6"; +} +.mdi-beaker-alert::before { + content: "\F0254"; +} +.mdi-beaker-alert-outline::before { + content: "\F0255"; +} +.mdi-beaker-check::before { + content: "\F0256"; +} +.mdi-beaker-check-outline::before { + content: "\F0257"; +} +.mdi-beaker-minus::before { + content: "\F0258"; +} +.mdi-beaker-minus-outline::before { + content: "\F0259"; +} +.mdi-beaker-outline::before { + content: "\F68F"; +} +.mdi-beaker-plus::before { + content: "\F025A"; +} +.mdi-beaker-plus-outline::before { + content: "\F025B"; +} +.mdi-beaker-question::before { + content: "\F025C"; +} +.mdi-beaker-question-outline::before { + content: "\F025D"; +} +.mdi-beaker-remove::before { + content: "\F025E"; +} +.mdi-beaker-remove-outline::before { + content: "\F025F"; +} +.mdi-beats::before { + content: "\F097"; +} +.mdi-bed-double::before { + content: "\F0092"; +} +.mdi-bed-double-outline::before { + content: "\F0093"; +} +.mdi-bed-empty::before { + content: "\F89F"; +} +.mdi-bed-king::before { + content: "\F0094"; +} +.mdi-bed-king-outline::before { + content: "\F0095"; +} +.mdi-bed-queen::before { + content: "\F0096"; +} +.mdi-bed-queen-outline::before { + content: "\F0097"; +} +.mdi-bed-single::before { + content: "\F0098"; +} +.mdi-bed-single-outline::before { + content: "\F0099"; +} +.mdi-bee::before { + content: "\FFC1"; +} +.mdi-bee-flower::before { + content: "\FFC2"; +} +.mdi-beehive-outline::before { + content: "\F00F9"; +} +.mdi-beer::before { + content: "\F098"; +} +.mdi-beer-outline::before { + content: "\F0337"; +} +.mdi-behance::before { + content: "\F099"; +} +.mdi-bell::before { + content: "\F09A"; +} +.mdi-bell-alert::before { + content: "\FD35"; +} +.mdi-bell-alert-outline::before { + content: "\FE9E"; +} +.mdi-bell-check::before { + content: "\F0210"; +} +.mdi-bell-check-outline::before { + content: "\F0211"; +} +.mdi-bell-circle::before { + content: "\FD36"; +} +.mdi-bell-circle-outline::before { + content: "\FD37"; +} +.mdi-bell-off::before { + content: "\F09B"; +} +.mdi-bell-off-outline::before { + content: "\FA90"; +} +.mdi-bell-outline::before { + content: "\F09C"; +} +.mdi-bell-plus::before { + content: "\F09D"; +} +.mdi-bell-plus-outline::before { + content: "\FA91"; +} +.mdi-bell-ring::before { + content: "\F09E"; +} +.mdi-bell-ring-outline::before { + content: "\F09F"; +} +.mdi-bell-sleep::before { + content: "\F0A0"; +} +.mdi-bell-sleep-outline::before { + content: "\FA92"; +} +.mdi-beta::before { + content: "\F0A1"; +} +.mdi-betamax::before { + content: "\F9CA"; +} +.mdi-biathlon::before { + content: "\FDF7"; +} +.mdi-bible::before { + content: "\F0A2"; +} +.mdi-bicycle::before { + content: "\F00C7"; +} +.mdi-bicycle-basket::before { + content: "\F0260"; +} +.mdi-bike::before { + content: "\F0A3"; +} +.mdi-bike-fast::before { + content: "\F014A"; +} +.mdi-billboard::before { + content: "\F0032"; +} +.mdi-billiards::before { + content: "\FB3D"; +} +.mdi-billiards-rack::before { + content: "\FB3E"; +} +.mdi-bing::before { + content: "\F0A4"; +} +.mdi-binoculars::before { + content: "\F0A5"; +} +.mdi-bio::before { + content: "\F0A6"; +} +.mdi-biohazard::before { + content: "\F0A7"; +} +.mdi-bitbucket::before { + content: "\F0A8"; +} +.mdi-bitcoin::before { + content: "\F812"; +} +.mdi-black-mesa::before { + content: "\F0A9"; +} +.mdi-blackberry::before { + content: "\F0AA"; +} +.mdi-blender::before { + content: "\FCC7"; +} +.mdi-blender-software::before { + content: "\F0AB"; +} +.mdi-blinds::before { + content: "\F0AC"; +} +.mdi-blinds-open::before { + content: "\F0033"; +} +.mdi-block-helper::before { + content: "\F0AD"; +} +.mdi-blogger::before { + content: "\F0AE"; +} +.mdi-blood-bag::before { + content: "\FCC8"; +} +.mdi-bluetooth::before { + content: "\F0AF"; +} +.mdi-bluetooth-audio::before { + content: "\F0B0"; +} +.mdi-bluetooth-connect::before { + content: "\F0B1"; +} +.mdi-bluetooth-off::before { + content: "\F0B2"; +} +.mdi-bluetooth-settings::before { + content: "\F0B3"; +} +.mdi-bluetooth-transfer::before { + content: "\F0B4"; +} +.mdi-blur::before { + content: "\F0B5"; +} +.mdi-blur-linear::before { + content: "\F0B6"; +} +.mdi-blur-off::before { + content: "\F0B7"; +} +.mdi-blur-radial::before { + content: "\F0B8"; +} +.mdi-bolnisi-cross::before { + content: "\FCC9"; +} +.mdi-bolt::before { + content: "\FD8F"; +} +.mdi-bomb::before { + content: "\F690"; +} +.mdi-bomb-off::before { + content: "\F6C4"; +} +.mdi-bone::before { + content: "\F0B9"; +} +.mdi-book::before { + content: "\F0BA"; +} +.mdi-book-information-variant::before { + content: "\F009A"; +} +.mdi-book-lock::before { + content: "\F799"; +} +.mdi-book-lock-open::before { + content: "\F79A"; +} +.mdi-book-minus::before { + content: "\F5D9"; +} +.mdi-book-minus-multiple::before { + content: "\FA93"; +} +.mdi-book-multiple::before { + content: "\F0BB"; +} +.mdi-book-open::before { + content: "\F0BD"; +} +.mdi-book-open-outline::before { + content: "\FB3F"; +} +.mdi-book-open-page-variant::before { + content: "\F5DA"; +} +.mdi-book-open-variant::before { + content: "\F0BE"; +} +.mdi-book-outline::before { + content: "\FB40"; +} +.mdi-book-play::before { + content: "\FE9F"; +} +.mdi-book-play-outline::before { + content: "\FEA0"; +} +.mdi-book-plus::before { + content: "\F5DB"; +} +.mdi-book-plus-multiple::before { + content: "\FA94"; +} +.mdi-book-remove::before { + content: "\FA96"; +} +.mdi-book-remove-multiple::before { + content: "\FA95"; +} +.mdi-book-search::before { + content: "\FEA1"; +} +.mdi-book-search-outline::before { + content: "\FEA2"; +} +.mdi-book-variant::before { + content: "\F0BF"; +} +.mdi-book-variant-multiple::before { + content: "\F0BC"; +} +.mdi-bookmark::before { + content: "\F0C0"; +} +.mdi-bookmark-check::before { + content: "\F0C1"; +} +.mdi-bookmark-check-outline::before { + content: "\F03A6"; +} +.mdi-bookmark-minus::before { + content: "\F9CB"; +} +.mdi-bookmark-minus-outline::before { + content: "\F9CC"; +} +.mdi-bookmark-multiple::before { + content: "\FDF8"; +} +.mdi-bookmark-multiple-outline::before { + content: "\FDF9"; +} +.mdi-bookmark-music::before { + content: "\F0C2"; +} +.mdi-bookmark-music-outline::before { + content: "\F03A4"; +} +.mdi-bookmark-off::before { + content: "\F9CD"; +} +.mdi-bookmark-off-outline::before { + content: "\F9CE"; +} +.mdi-bookmark-outline::before { + content: "\F0C3"; +} +.mdi-bookmark-plus::before { + content: "\F0C5"; +} +.mdi-bookmark-plus-outline::before { + content: "\F0C4"; +} +.mdi-bookmark-remove::before { + content: "\F0C6"; +} +.mdi-bookmark-remove-outline::before { + content: "\F03A5"; +} +.mdi-bookshelf::before { + content: "\F028A"; +} +.mdi-boom-gate::before { + content: "\FEA3"; +} +.mdi-boom-gate-alert::before { + content: "\FEA4"; +} +.mdi-boom-gate-alert-outline::before { + content: "\FEA5"; +} +.mdi-boom-gate-down::before { + content: "\FEA6"; +} +.mdi-boom-gate-down-outline::before { + content: "\FEA7"; +} +.mdi-boom-gate-outline::before { + content: "\FEA8"; +} +.mdi-boom-gate-up::before { + content: "\FEA9"; +} +.mdi-boom-gate-up-outline::before { + content: "\FEAA"; +} +.mdi-boombox::before { + content: "\F5DC"; +} +.mdi-boomerang::before { + content: "\F00FA"; +} +.mdi-bootstrap::before { + content: "\F6C5"; +} +.mdi-border-all::before { + content: "\F0C7"; +} +.mdi-border-all-variant::before { + content: "\F8A0"; +} +.mdi-border-bottom::before { + content: "\F0C8"; +} +.mdi-border-bottom-variant::before { + content: "\F8A1"; +} +.mdi-border-color::before { + content: "\F0C9"; +} +.mdi-border-horizontal::before { + content: "\F0CA"; +} +.mdi-border-inside::before { + content: "\F0CB"; +} +.mdi-border-left::before { + content: "\F0CC"; +} +.mdi-border-left-variant::before { + content: "\F8A2"; +} +.mdi-border-none::before { + content: "\F0CD"; +} +.mdi-border-none-variant::before { + content: "\F8A3"; +} +.mdi-border-outside::before { + content: "\F0CE"; +} +.mdi-border-right::before { + content: "\F0CF"; +} +.mdi-border-right-variant::before { + content: "\F8A4"; +} +.mdi-border-style::before { + content: "\F0D0"; +} +.mdi-border-top::before { + content: "\F0D1"; +} +.mdi-border-top-variant::before { + content: "\F8A5"; +} +.mdi-border-vertical::before { + content: "\F0D2"; +} +.mdi-bottle-soda::before { + content: "\F009B"; +} +.mdi-bottle-soda-classic::before { + content: "\F009C"; +} +.mdi-bottle-soda-classic-outline::before { + content: "\F038E"; +} +.mdi-bottle-soda-outline::before { + content: "\F009D"; +} +.mdi-bottle-tonic::before { + content: "\F0159"; +} +.mdi-bottle-tonic-outline::before { + content: "\F015A"; +} +.mdi-bottle-tonic-plus::before { + content: "\F015B"; +} +.mdi-bottle-tonic-plus-outline::before { + content: "\F015C"; +} +.mdi-bottle-tonic-skull::before { + content: "\F015D"; +} +.mdi-bottle-tonic-skull-outline::before { + content: "\F015E"; +} +.mdi-bottle-wine::before { + content: "\F853"; +} +.mdi-bottle-wine-outline::before { + content: "\F033B"; +} +.mdi-bow-tie::before { + content: "\F677"; +} +.mdi-bowl::before { + content: "\F617"; +} +.mdi-bowling::before { + content: "\F0D3"; +} +.mdi-box::before { + content: "\F0D4"; +} +.mdi-box-cutter::before { + content: "\F0D5"; +} +.mdi-box-shadow::before { + content: "\F637"; +} +.mdi-boxing-glove::before { + content: "\FB41"; +} +.mdi-braille::before { + content: "\F9CF"; +} +.mdi-brain::before { + content: "\F9D0"; +} +.mdi-bread-slice::before { + content: "\FCCA"; +} +.mdi-bread-slice-outline::before { + content: "\FCCB"; +} +.mdi-bridge::before { + content: "\F618"; +} +.mdi-briefcase::before { + content: "\F0D6"; +} +.mdi-briefcase-account::before { + content: "\FCCC"; +} +.mdi-briefcase-account-outline::before { + content: "\FCCD"; +} +.mdi-briefcase-check::before { + content: "\F0D7"; +} +.mdi-briefcase-check-outline::before { + content: "\F0349"; +} +.mdi-briefcase-clock::before { + content: "\F00FB"; +} +.mdi-briefcase-clock-outline::before { + content: "\F00FC"; +} +.mdi-briefcase-download::before { + content: "\F0D8"; +} +.mdi-briefcase-download-outline::before { + content: "\FC19"; +} +.mdi-briefcase-edit::before { + content: "\FA97"; +} +.mdi-briefcase-edit-outline::before { + content: "\FC1A"; +} +.mdi-briefcase-minus::before { + content: "\FA29"; +} +.mdi-briefcase-minus-outline::before { + content: "\FC1B"; +} +.mdi-briefcase-outline::before { + content: "\F813"; +} +.mdi-briefcase-plus::before { + content: "\FA2A"; +} +.mdi-briefcase-plus-outline::before { + content: "\FC1C"; +} +.mdi-briefcase-remove::before { + content: "\FA2B"; +} +.mdi-briefcase-remove-outline::before { + content: "\FC1D"; +} +.mdi-briefcase-search::before { + content: "\FA2C"; +} +.mdi-briefcase-search-outline::before { + content: "\FC1E"; +} +.mdi-briefcase-upload::before { + content: "\F0D9"; +} +.mdi-briefcase-upload-outline::before { + content: "\FC1F"; +} +.mdi-brightness-1::before { + content: "\F0DA"; +} +.mdi-brightness-2::before { + content: "\F0DB"; +} +.mdi-brightness-3::before { + content: "\F0DC"; +} +.mdi-brightness-4::before { + content: "\F0DD"; +} +.mdi-brightness-5::before { + content: "\F0DE"; +} +.mdi-brightness-6::before { + content: "\F0DF"; +} +.mdi-brightness-7::before { + content: "\F0E0"; +} +.mdi-brightness-auto::before { + content: "\F0E1"; +} +.mdi-brightness-percent::before { + content: "\FCCE"; +} +.mdi-broom::before { + content: "\F0E2"; +} +.mdi-brush::before { + content: "\F0E3"; +} +.mdi-buddhism::before { + content: "\F94A"; +} +.mdi-buffer::before { + content: "\F619"; +} +.mdi-bug::before { + content: "\F0E4"; +} +.mdi-bug-check::before { + content: "\FA2D"; +} +.mdi-bug-check-outline::before { + content: "\FA2E"; +} +.mdi-bug-outline::before { + content: "\FA2F"; +} +.mdi-bugle::before { + content: "\FD90"; +} +.mdi-bulldozer::before { + content: "\FB07"; +} +.mdi-bullet::before { + content: "\FCCF"; +} +.mdi-bulletin-board::before { + content: "\F0E5"; +} +.mdi-bullhorn::before { + content: "\F0E6"; +} +.mdi-bullhorn-outline::before { + content: "\FB08"; +} +.mdi-bullseye::before { + content: "\F5DD"; +} +.mdi-bullseye-arrow::before { + content: "\F8C8"; +} +.mdi-bulma::before { + content: "\F0312"; +} +.mdi-bunk-bed::before { + content: "\F032D"; +} +.mdi-bus::before { + content: "\F0E7"; +} +.mdi-bus-alert::before { + content: "\FA98"; +} +.mdi-bus-articulated-end::before { + content: "\F79B"; +} +.mdi-bus-articulated-front::before { + content: "\F79C"; +} +.mdi-bus-clock::before { + content: "\F8C9"; +} +.mdi-bus-double-decker::before { + content: "\F79D"; +} +.mdi-bus-marker::before { + content: "\F023D"; +} +.mdi-bus-multiple::before { + content: "\FF5C"; +} +.mdi-bus-school::before { + content: "\F79E"; +} +.mdi-bus-side::before { + content: "\F79F"; +} +.mdi-bus-stop::before { + content: "\F0034"; +} +.mdi-bus-stop-covered::before { + content: "\F0035"; +} +.mdi-bus-stop-uncovered::before { + content: "\F0036"; +} +.mdi-cached::before { + content: "\F0E8"; +} +.mdi-cactus::before { + content: "\FD91"; +} +.mdi-cake::before { + content: "\F0E9"; +} +.mdi-cake-layered::before { + content: "\F0EA"; +} +.mdi-cake-variant::before { + content: "\F0EB"; +} +.mdi-calculator::before { + content: "\F0EC"; +} +.mdi-calculator-variant::before { + content: "\FA99"; +} +.mdi-calendar::before { + content: "\F0ED"; +} +.mdi-calendar-account::before { + content: "\FEF4"; +} +.mdi-calendar-account-outline::before { + content: "\FEF5"; +} +.mdi-calendar-alert::before { + content: "\FA30"; +} +.mdi-calendar-arrow-left::before { + content: "\F015F"; +} +.mdi-calendar-arrow-right::before { + content: "\F0160"; +} +.mdi-calendar-blank::before { + content: "\F0EE"; +} +.mdi-calendar-blank-multiple::before { + content: "\F009E"; +} +.mdi-calendar-blank-outline::before { + content: "\FB42"; +} +.mdi-calendar-check::before { + content: "\F0EF"; +} +.mdi-calendar-check-outline::before { + content: "\FC20"; +} +.mdi-calendar-clock::before { + content: "\F0F0"; +} +.mdi-calendar-edit::before { + content: "\F8A6"; +} +.mdi-calendar-export::before { + content: "\FB09"; +} +.mdi-calendar-heart::before { + content: "\F9D1"; +} +.mdi-calendar-import::before { + content: "\FB0A"; +} +.mdi-calendar-minus::before { + content: "\FD38"; +} +.mdi-calendar-month::before { + content: "\FDFA"; +} +.mdi-calendar-month-outline::before { + content: "\FDFB"; +} +.mdi-calendar-multiple::before { + content: "\F0F1"; +} +.mdi-calendar-multiple-check::before { + content: "\F0F2"; +} +.mdi-calendar-multiselect::before { + content: "\FA31"; +} +.mdi-calendar-outline::before { + content: "\FB43"; +} +.mdi-calendar-plus::before { + content: "\F0F3"; +} +.mdi-calendar-question::before { + content: "\F691"; +} +.mdi-calendar-range::before { + content: "\F678"; +} +.mdi-calendar-range-outline::before { + content: "\FB44"; +} +.mdi-calendar-remove::before { + content: "\F0F4"; +} +.mdi-calendar-remove-outline::before { + content: "\FC21"; +} +.mdi-calendar-repeat::before { + content: "\FEAB"; +} +.mdi-calendar-repeat-outline::before { + content: "\FEAC"; +} +.mdi-calendar-search::before { + content: "\F94B"; +} +.mdi-calendar-star::before { + content: "\F9D2"; +} +.mdi-calendar-text::before { + content: "\F0F5"; +} +.mdi-calendar-text-outline::before { + content: "\FC22"; +} +.mdi-calendar-today::before { + content: "\F0F6"; +} +.mdi-calendar-week::before { + content: "\FA32"; +} +.mdi-calendar-week-begin::before { + content: "\FA33"; +} +.mdi-calendar-weekend::before { + content: "\FEF6"; +} +.mdi-calendar-weekend-outline::before { + content: "\FEF7"; +} +.mdi-call-made::before { + content: "\F0F7"; +} +.mdi-call-merge::before { + content: "\F0F8"; +} +.mdi-call-missed::before { + content: "\F0F9"; +} +.mdi-call-received::before { + content: "\F0FA"; +} +.mdi-call-split::before { + content: "\F0FB"; +} +.mdi-camcorder::before { + content: "\F0FC"; +} +.mdi-camcorder-box::before { + content: "\F0FD"; +} +.mdi-camcorder-box-off::before { + content: "\F0FE"; +} +.mdi-camcorder-off::before { + content: "\F0FF"; +} +.mdi-camera::before { + content: "\F100"; +} +.mdi-camera-account::before { + content: "\F8CA"; +} +.mdi-camera-burst::before { + content: "\F692"; +} +.mdi-camera-control::before { + content: "\FB45"; +} +.mdi-camera-enhance::before { + content: "\F101"; +} +.mdi-camera-enhance-outline::before { + content: "\FB46"; +} +.mdi-camera-front::before { + content: "\F102"; +} +.mdi-camera-front-variant::before { + content: "\F103"; +} +.mdi-camera-gopro::before { + content: "\F7A0"; +} +.mdi-camera-image::before { + content: "\F8CB"; +} +.mdi-camera-iris::before { + content: "\F104"; +} +.mdi-camera-metering-center::before { + content: "\F7A1"; +} +.mdi-camera-metering-matrix::before { + content: "\F7A2"; +} +.mdi-camera-metering-partial::before { + content: "\F7A3"; +} +.mdi-camera-metering-spot::before { + content: "\F7A4"; +} +.mdi-camera-off::before { + content: "\F5DF"; +} +.mdi-camera-outline::before { + content: "\FD39"; +} +.mdi-camera-party-mode::before { + content: "\F105"; +} +.mdi-camera-plus::before { + content: "\FEF8"; +} +.mdi-camera-plus-outline::before { + content: "\FEF9"; +} +.mdi-camera-rear::before { + content: "\F106"; +} +.mdi-camera-rear-variant::before { + content: "\F107"; +} +.mdi-camera-retake::before { + content: "\FDFC"; +} +.mdi-camera-retake-outline::before { + content: "\FDFD"; +} +.mdi-camera-switch::before { + content: "\F108"; +} +.mdi-camera-timer::before { + content: "\F109"; +} +.mdi-camera-wireless::before { + content: "\FD92"; +} +.mdi-camera-wireless-outline::before { + content: "\FD93"; +} +.mdi-campfire::before { + content: "\FEFA"; +} +.mdi-cancel::before { + content: "\F739"; +} +.mdi-candle::before { + content: "\F5E2"; +} +.mdi-candycane::before { + content: "\F10A"; +} +.mdi-cannabis::before { + content: "\F7A5"; +} +.mdi-caps-lock::before { + content: "\FA9A"; +} +.mdi-car::before { + content: "\F10B"; +} +.mdi-car-2-plus::before { + content: "\F0037"; +} +.mdi-car-3-plus::before { + content: "\F0038"; +} +.mdi-car-back::before { + content: "\FDFE"; +} +.mdi-car-battery::before { + content: "\F10C"; +} +.mdi-car-brake-abs::before { + content: "\FC23"; +} +.mdi-car-brake-alert::before { + content: "\FC24"; +} +.mdi-car-brake-hold::before { + content: "\FD3A"; +} +.mdi-car-brake-parking::before { + content: "\FD3B"; +} +.mdi-car-brake-retarder::before { + content: "\F0039"; +} +.mdi-car-child-seat::before { + content: "\FFC3"; +} +.mdi-car-clutch::before { + content: "\F003A"; +} +.mdi-car-connected::before { + content: "\F10D"; +} +.mdi-car-convertible::before { + content: "\F7A6"; +} +.mdi-car-coolant-level::before { + content: "\F003B"; +} +.mdi-car-cruise-control::before { + content: "\FD3C"; +} +.mdi-car-defrost-front::before { + content: "\FD3D"; +} +.mdi-car-defrost-rear::before { + content: "\FD3E"; +} +.mdi-car-door::before { + content: "\FB47"; +} +.mdi-car-door-lock::before { + content: "\F00C8"; +} +.mdi-car-electric::before { + content: "\FB48"; +} +.mdi-car-esp::before { + content: "\FC25"; +} +.mdi-car-estate::before { + content: "\F7A7"; +} +.mdi-car-hatchback::before { + content: "\F7A8"; +} +.mdi-car-info::before { + content: "\F01E9"; +} +.mdi-car-key::before { + content: "\FB49"; +} +.mdi-car-light-dimmed::before { + content: "\FC26"; +} +.mdi-car-light-fog::before { + content: "\FC27"; +} +.mdi-car-light-high::before { + content: "\FC28"; +} +.mdi-car-limousine::before { + content: "\F8CC"; +} +.mdi-car-multiple::before { + content: "\FB4A"; +} +.mdi-car-off::before { + content: "\FDFF"; +} +.mdi-car-parking-lights::before { + content: "\FD3F"; +} +.mdi-car-pickup::before { + content: "\F7A9"; +} +.mdi-car-seat::before { + content: "\FFC4"; +} +.mdi-car-seat-cooler::before { + content: "\FFC5"; +} +.mdi-car-seat-heater::before { + content: "\FFC6"; +} +.mdi-car-shift-pattern::before { + content: "\FF5D"; +} +.mdi-car-side::before { + content: "\F7AA"; +} +.mdi-car-sports::before { + content: "\F7AB"; +} +.mdi-car-tire-alert::before { + content: "\FC29"; +} +.mdi-car-traction-control::before { + content: "\FD40"; +} +.mdi-car-turbocharger::before { + content: "\F003C"; +} +.mdi-car-wash::before { + content: "\F10E"; +} +.mdi-car-windshield::before { + content: "\F003D"; +} +.mdi-car-windshield-outline::before { + content: "\F003E"; +} +.mdi-caravan::before { + content: "\F7AC"; +} +.mdi-card::before { + content: "\FB4B"; +} +.mdi-card-bulleted::before { + content: "\FB4C"; +} +.mdi-card-bulleted-off::before { + content: "\FB4D"; +} +.mdi-card-bulleted-off-outline::before { + content: "\FB4E"; +} +.mdi-card-bulleted-outline::before { + content: "\FB4F"; +} +.mdi-card-bulleted-settings::before { + content: "\FB50"; +} +.mdi-card-bulleted-settings-outline::before { + content: "\FB51"; +} +.mdi-card-outline::before { + content: "\FB52"; +} +.mdi-card-plus::before { + content: "\F022A"; +} +.mdi-card-plus-outline::before { + content: "\F022B"; +} +.mdi-card-search::before { + content: "\F009F"; +} +.mdi-card-search-outline::before { + content: "\F00A0"; +} +.mdi-card-text::before { + content: "\FB53"; +} +.mdi-card-text-outline::before { + content: "\FB54"; +} +.mdi-cards::before { + content: "\F638"; +} +.mdi-cards-club::before { + content: "\F8CD"; +} +.mdi-cards-diamond::before { + content: "\F8CE"; +} +.mdi-cards-diamond-outline::before { + content: "\F003F"; +} +.mdi-cards-heart::before { + content: "\F8CF"; +} +.mdi-cards-outline::before { + content: "\F639"; +} +.mdi-cards-playing-outline::before { + content: "\F63A"; +} +.mdi-cards-spade::before { + content: "\F8D0"; +} +.mdi-cards-variant::before { + content: "\F6C6"; +} +.mdi-carrot::before { + content: "\F10F"; +} +.mdi-cart::before { + content: "\F110"; +} +.mdi-cart-arrow-down::before { + content: "\FD42"; +} +.mdi-cart-arrow-right::before { + content: "\FC2A"; +} +.mdi-cart-arrow-up::before { + content: "\FD43"; +} +.mdi-cart-minus::before { + content: "\FD44"; +} +.mdi-cart-off::before { + content: "\F66B"; +} +.mdi-cart-outline::before { + content: "\F111"; +} +.mdi-cart-plus::before { + content: "\F112"; +} +.mdi-cart-remove::before { + content: "\FD45"; +} +.mdi-case-sensitive-alt::before { + content: "\F113"; +} +.mdi-cash::before { + content: "\F114"; +} +.mdi-cash-100::before { + content: "\F115"; +} +.mdi-cash-marker::before { + content: "\FD94"; +} +.mdi-cash-minus::before { + content: "\F028B"; +} +.mdi-cash-multiple::before { + content: "\F116"; +} +.mdi-cash-plus::before { + content: "\F028C"; +} +.mdi-cash-refund::before { + content: "\FA9B"; +} +.mdi-cash-register::before { + content: "\FCD0"; +} +.mdi-cash-remove::before { + content: "\F028D"; +} +.mdi-cash-usd::before { + content: "\F01A1"; +} +.mdi-cash-usd-outline::before { + content: "\F117"; +} +.mdi-cassette::before { + content: "\F9D3"; +} +.mdi-cast::before { + content: "\F118"; +} +.mdi-cast-audio::before { + content: "\F0040"; +} +.mdi-cast-connected::before { + content: "\F119"; +} +.mdi-cast-education::before { + content: "\FE6D"; +} +.mdi-cast-off::before { + content: "\F789"; +} +.mdi-castle::before { + content: "\F11A"; +} +.mdi-cat::before { + content: "\F11B"; +} +.mdi-cctv::before { + content: "\F7AD"; +} +.mdi-ceiling-light::before { + content: "\F768"; +} +.mdi-cellphone::before { + content: "\F11C"; +} +.mdi-cellphone-android::before { + content: "\F11D"; +} +.mdi-cellphone-arrow-down::before { + content: "\F9D4"; +} +.mdi-cellphone-basic::before { + content: "\F11E"; +} +.mdi-cellphone-dock::before { + content: "\F11F"; +} +.mdi-cellphone-erase::before { + content: "\F94C"; +} +.mdi-cellphone-information::before { + content: "\FF5E"; +} +.mdi-cellphone-iphone::before { + content: "\F120"; +} +.mdi-cellphone-key::before { + content: "\F94D"; +} +.mdi-cellphone-link::before { + content: "\F121"; +} +.mdi-cellphone-link-off::before { + content: "\F122"; +} +.mdi-cellphone-lock::before { + content: "\F94E"; +} +.mdi-cellphone-message::before { + content: "\F8D2"; +} +.mdi-cellphone-message-off::before { + content: "\F00FD"; +} +.mdi-cellphone-nfc::before { + content: "\FEAD"; +} +.mdi-cellphone-nfc-off::before { + content: "\F0303"; +} +.mdi-cellphone-off::before { + content: "\F94F"; +} +.mdi-cellphone-play::before { + content: "\F0041"; +} +.mdi-cellphone-screenshot::before { + content: "\FA34"; +} +.mdi-cellphone-settings::before { + content: "\F123"; +} +.mdi-cellphone-settings-variant::before { + content: "\F950"; +} +.mdi-cellphone-sound::before { + content: "\F951"; +} +.mdi-cellphone-text::before { + content: "\F8D1"; +} +.mdi-cellphone-wireless::before { + content: "\F814"; +} +.mdi-celtic-cross::before { + content: "\FCD1"; +} +.mdi-centos::before { + content: "\F0145"; +} +.mdi-certificate::before { + content: "\F124"; +} +.mdi-certificate-outline::before { + content: "\F01B3"; +} +.mdi-chair-rolling::before { + content: "\FFBA"; +} +.mdi-chair-school::before { + content: "\F125"; +} +.mdi-charity::before { + content: "\FC2B"; +} +.mdi-chart-arc::before { + content: "\F126"; +} +.mdi-chart-areaspline::before { + content: "\F127"; +} +.mdi-chart-areaspline-variant::before { + content: "\FEAE"; +} +.mdi-chart-bar::before { + content: "\F128"; +} +.mdi-chart-bar-stacked::before { + content: "\F769"; +} +.mdi-chart-bell-curve::before { + content: "\FC2C"; +} +.mdi-chart-bell-curve-cumulative::before { + content: "\FFC7"; +} +.mdi-chart-bubble::before { + content: "\F5E3"; +} +.mdi-chart-donut::before { + content: "\F7AE"; +} +.mdi-chart-donut-variant::before { + content: "\F7AF"; +} +.mdi-chart-gantt::before { + content: "\F66C"; +} +.mdi-chart-histogram::before { + content: "\F129"; +} +.mdi-chart-line::before { + content: "\F12A"; +} +.mdi-chart-line-stacked::before { + content: "\F76A"; +} +.mdi-chart-line-variant::before { + content: "\F7B0"; +} +.mdi-chart-multiline::before { + content: "\F8D3"; +} +.mdi-chart-multiple::before { + content: "\F023E"; +} +.mdi-chart-pie::before { + content: "\F12B"; +} +.mdi-chart-ppf::before { + content: "\F03AB"; +} +.mdi-chart-scatter-plot::before { + content: "\FEAF"; +} +.mdi-chart-scatter-plot-hexbin::before { + content: "\F66D"; +} +.mdi-chart-snakey::before { + content: "\F020A"; +} +.mdi-chart-snakey-variant::before { + content: "\F020B"; +} +.mdi-chart-timeline::before { + content: "\F66E"; +} +.mdi-chart-timeline-variant::before { + content: "\FEB0"; +} +.mdi-chart-tree::before { + content: "\FEB1"; +} +.mdi-chat::before { + content: "\FB55"; +} +.mdi-chat-alert::before { + content: "\FB56"; +} +.mdi-chat-alert-outline::before { + content: "\F02F4"; +} +.mdi-chat-outline::before { + content: "\FEFB"; +} +.mdi-chat-processing::before { + content: "\FB57"; +} +.mdi-chat-processing-outline::before { + content: "\F02F5"; +} +.mdi-chat-sleep::before { + content: "\F02FC"; +} +.mdi-chat-sleep-outline::before { + content: "\F02FD"; +} +.mdi-check::before { + content: "\F12C"; +} +.mdi-check-all::before { + content: "\F12D"; +} +.mdi-check-bold::before { + content: "\FE6E"; +} +.mdi-check-box-multiple-outline::before { + content: "\FC2D"; +} +.mdi-check-box-outline::before { + content: "\FC2E"; +} +.mdi-check-circle::before { + content: "\F5E0"; +} +.mdi-check-circle-outline::before { + content: "\F5E1"; +} +.mdi-check-decagram::before { + content: "\F790"; +} +.mdi-check-network::before { + content: "\FC2F"; +} +.mdi-check-network-outline::before { + content: "\FC30"; +} +.mdi-check-outline::before { + content: "\F854"; +} +.mdi-check-underline::before { + content: "\FE70"; +} +.mdi-check-underline-circle::before { + content: "\FE71"; +} +.mdi-check-underline-circle-outline::before { + content: "\FE72"; +} +.mdi-checkbook::before { + content: "\FA9C"; +} +.mdi-checkbox-blank::before { + content: "\F12E"; +} +.mdi-checkbox-blank-circle::before { + content: "\F12F"; +} +.mdi-checkbox-blank-circle-outline::before { + content: "\F130"; +} +.mdi-checkbox-blank-off::before { + content: "\F0317"; +} +.mdi-checkbox-blank-off-outline::before { + content: "\F0318"; +} +.mdi-checkbox-blank-outline::before { + content: "\F131"; +} +.mdi-checkbox-intermediate::before { + content: "\F855"; +} +.mdi-checkbox-marked::before { + content: "\F132"; +} +.mdi-checkbox-marked-circle::before { + content: "\F133"; +} +.mdi-checkbox-marked-circle-outline::before { + content: "\F134"; +} +.mdi-checkbox-marked-outline::before { + content: "\F135"; +} +.mdi-checkbox-multiple-blank::before { + content: "\F136"; +} +.mdi-checkbox-multiple-blank-circle::before { + content: "\F63B"; +} +.mdi-checkbox-multiple-blank-circle-outline::before { + content: "\F63C"; +} +.mdi-checkbox-multiple-blank-outline::before { + content: "\F137"; +} +.mdi-checkbox-multiple-marked::before { + content: "\F138"; +} +.mdi-checkbox-multiple-marked-circle::before { + content: "\F63D"; +} +.mdi-checkbox-multiple-marked-circle-outline::before { + content: "\F63E"; +} +.mdi-checkbox-multiple-marked-outline::before { + content: "\F139"; +} +.mdi-checkerboard::before { + content: "\F13A"; +} +.mdi-checkerboard-minus::before { + content: "\F022D"; +} +.mdi-checkerboard-plus::before { + content: "\F022C"; +} +.mdi-checkerboard-remove::before { + content: "\F022E"; +} +.mdi-cheese::before { + content: "\F02E4"; +} +.mdi-chef-hat::before { + content: "\FB58"; +} +.mdi-chemical-weapon::before { + content: "\F13B"; +} +.mdi-chess-bishop::before { + content: "\F85B"; +} +.mdi-chess-king::before { + content: "\F856"; +} +.mdi-chess-knight::before { + content: "\F857"; +} +.mdi-chess-pawn::before { + content: "\F858"; +} +.mdi-chess-queen::before { + content: "\F859"; +} +.mdi-chess-rook::before { + content: "\F85A"; +} +.mdi-chevron-double-down::before { + content: "\F13C"; +} +.mdi-chevron-double-left::before { + content: "\F13D"; +} +.mdi-chevron-double-right::before { + content: "\F13E"; +} +.mdi-chevron-double-up::before { + content: "\F13F"; +} +.mdi-chevron-down::before { + content: "\F140"; +} +.mdi-chevron-down-box::before { + content: "\F9D5"; +} +.mdi-chevron-down-box-outline::before { + content: "\F9D6"; +} +.mdi-chevron-down-circle::before { + content: "\FB0B"; +} +.mdi-chevron-down-circle-outline::before { + content: "\FB0C"; +} +.mdi-chevron-left::before { + content: "\F141"; +} +.mdi-chevron-left-box::before { + content: "\F9D7"; +} +.mdi-chevron-left-box-outline::before { + content: "\F9D8"; +} +.mdi-chevron-left-circle::before { + content: "\FB0D"; +} +.mdi-chevron-left-circle-outline::before { + content: "\FB0E"; +} +.mdi-chevron-right::before { + content: "\F142"; +} +.mdi-chevron-right-box::before { + content: "\F9D9"; +} +.mdi-chevron-right-box-outline::before { + content: "\F9DA"; +} +.mdi-chevron-right-circle::before { + content: "\FB0F"; +} +.mdi-chevron-right-circle-outline::before { + content: "\FB10"; +} +.mdi-chevron-triple-down::before { + content: "\FD95"; +} +.mdi-chevron-triple-left::before { + content: "\FD96"; +} +.mdi-chevron-triple-right::before { + content: "\FD97"; +} +.mdi-chevron-triple-up::before { + content: "\FD98"; +} +.mdi-chevron-up::before { + content: "\F143"; +} +.mdi-chevron-up-box::before { + content: "\F9DB"; +} +.mdi-chevron-up-box-outline::before { + content: "\F9DC"; +} +.mdi-chevron-up-circle::before { + content: "\FB11"; +} +.mdi-chevron-up-circle-outline::before { + content: "\FB12"; +} +.mdi-chili-hot::before { + content: "\F7B1"; +} +.mdi-chili-medium::before { + content: "\F7B2"; +} +.mdi-chili-mild::before { + content: "\F7B3"; +} +.mdi-chip::before { + content: "\F61A"; +} +.mdi-christianity::before { + content: "\F952"; +} +.mdi-christianity-outline::before { + content: "\FCD2"; +} +.mdi-church::before { + content: "\F144"; +} +.mdi-cigar::before { + content: "\F01B4"; +} +.mdi-circle::before { + content: "\F764"; +} +.mdi-circle-double::before { + content: "\FEB2"; +} +.mdi-circle-edit-outline::before { + content: "\F8D4"; +} +.mdi-circle-expand::before { + content: "\FEB3"; +} +.mdi-circle-medium::before { + content: "\F9DD"; +} +.mdi-circle-off-outline::before { + content: "\F00FE"; +} +.mdi-circle-outline::before { + content: "\F765"; +} +.mdi-circle-slice-1::before { + content: "\FA9D"; +} +.mdi-circle-slice-2::before { + content: "\FA9E"; +} +.mdi-circle-slice-3::before { + content: "\FA9F"; +} +.mdi-circle-slice-4::before { + content: "\FAA0"; +} +.mdi-circle-slice-5::before { + content: "\FAA1"; +} +.mdi-circle-slice-6::before { + content: "\FAA2"; +} +.mdi-circle-slice-7::before { + content: "\FAA3"; +} +.mdi-circle-slice-8::before { + content: "\FAA4"; +} +.mdi-circle-small::before { + content: "\F9DE"; +} +.mdi-circular-saw::before { + content: "\FE73"; +} +.mdi-cisco-webex::before { + content: "\F145"; +} +.mdi-city::before { + content: "\F146"; +} +.mdi-city-variant::before { + content: "\FA35"; +} +.mdi-city-variant-outline::before { + content: "\FA36"; +} +.mdi-clipboard::before { + content: "\F147"; +} +.mdi-clipboard-account::before { + content: "\F148"; +} +.mdi-clipboard-account-outline::before { + content: "\FC31"; +} +.mdi-clipboard-alert::before { + content: "\F149"; +} +.mdi-clipboard-alert-outline::before { + content: "\FCD3"; +} +.mdi-clipboard-arrow-down::before { + content: "\F14A"; +} +.mdi-clipboard-arrow-down-outline::before { + content: "\FC32"; +} +.mdi-clipboard-arrow-left::before { + content: "\F14B"; +} +.mdi-clipboard-arrow-left-outline::before { + content: "\FCD4"; +} +.mdi-clipboard-arrow-right::before { + content: "\FCD5"; +} +.mdi-clipboard-arrow-right-outline::before { + content: "\FCD6"; +} +.mdi-clipboard-arrow-up::before { + content: "\FC33"; +} +.mdi-clipboard-arrow-up-outline::before { + content: "\FC34"; +} +.mdi-clipboard-check::before { + content: "\F14C"; +} +.mdi-clipboard-check-multiple::before { + content: "\F028E"; +} +.mdi-clipboard-check-multiple-outline::before { + content: "\F028F"; +} +.mdi-clipboard-check-outline::before { + content: "\F8A7"; +} +.mdi-clipboard-file::before { + content: "\F0290"; +} +.mdi-clipboard-file-outline::before { + content: "\F0291"; +} +.mdi-clipboard-flow::before { + content: "\F6C7"; +} +.mdi-clipboard-flow-outline::before { + content: "\F0142"; +} +.mdi-clipboard-list::before { + content: "\F00FF"; +} +.mdi-clipboard-list-outline::before { + content: "\F0100"; +} +.mdi-clipboard-multiple::before { + content: "\F0292"; +} +.mdi-clipboard-multiple-outline::before { + content: "\F0293"; +} +.mdi-clipboard-outline::before { + content: "\F14D"; +} +.mdi-clipboard-play::before { + content: "\FC35"; +} +.mdi-clipboard-play-multiple::before { + content: "\F0294"; +} +.mdi-clipboard-play-multiple-outline::before { + content: "\F0295"; +} +.mdi-clipboard-play-outline::before { + content: "\FC36"; +} +.mdi-clipboard-plus::before { + content: "\F750"; +} +.mdi-clipboard-plus-outline::before { + content: "\F034A"; +} +.mdi-clipboard-pulse::before { + content: "\F85C"; +} +.mdi-clipboard-pulse-outline::before { + content: "\F85D"; +} +.mdi-clipboard-text::before { + content: "\F14E"; +} +.mdi-clipboard-text-multiple::before { + content: "\F0296"; +} +.mdi-clipboard-text-multiple-outline::before { + content: "\F0297"; +} +.mdi-clipboard-text-outline::before { + content: "\FA37"; +} +.mdi-clipboard-text-play::before { + content: "\FC37"; +} +.mdi-clipboard-text-play-outline::before { + content: "\FC38"; +} +.mdi-clippy::before { + content: "\F14F"; +} +.mdi-clock::before { + content: "\F953"; +} +.mdi-clock-alert::before { + content: "\F954"; +} +.mdi-clock-alert-outline::before { + content: "\F5CE"; +} +.mdi-clock-check::before { + content: "\FFC8"; +} +.mdi-clock-check-outline::before { + content: "\FFC9"; +} +.mdi-clock-digital::before { + content: "\FEB4"; +} +.mdi-clock-end::before { + content: "\F151"; +} +.mdi-clock-fast::before { + content: "\F152"; +} +.mdi-clock-in::before { + content: "\F153"; +} +.mdi-clock-out::before { + content: "\F154"; +} +.mdi-clock-outline::before { + content: "\F150"; +} +.mdi-clock-start::before { + content: "\F155"; +} +.mdi-close::before { + content: "\F156"; +} +.mdi-close-box::before { + content: "\F157"; +} +.mdi-close-box-multiple::before { + content: "\FC39"; +} +.mdi-close-box-multiple-outline::before { + content: "\FC3A"; +} +.mdi-close-box-outline::before { + content: "\F158"; +} +.mdi-close-circle::before { + content: "\F159"; +} +.mdi-close-circle-outline::before { + content: "\F15A"; +} +.mdi-close-network::before { + content: "\F15B"; +} +.mdi-close-network-outline::before { + content: "\FC3B"; +} +.mdi-close-octagon::before { + content: "\F15C"; +} +.mdi-close-octagon-outline::before { + content: "\F15D"; +} +.mdi-close-outline::before { + content: "\F6C8"; +} +.mdi-closed-caption::before { + content: "\F15E"; +} +.mdi-closed-caption-outline::before { + content: "\FD99"; +} +.mdi-cloud::before { + content: "\F15F"; +} +.mdi-cloud-alert::before { + content: "\F9DF"; +} +.mdi-cloud-braces::before { + content: "\F7B4"; +} +.mdi-cloud-check::before { + content: "\F160"; +} +.mdi-cloud-check-outline::before { + content: "\F02F7"; +} +.mdi-cloud-circle::before { + content: "\F161"; +} +.mdi-cloud-download::before { + content: "\F162"; +} +.mdi-cloud-download-outline::before { + content: "\FB59"; +} +.mdi-cloud-lock::before { + content: "\F021C"; +} +.mdi-cloud-lock-outline::before { + content: "\F021D"; +} +.mdi-cloud-off-outline::before { + content: "\F164"; +} +.mdi-cloud-outline::before { + content: "\F163"; +} +.mdi-cloud-print::before { + content: "\F165"; +} +.mdi-cloud-print-outline::before { + content: "\F166"; +} +.mdi-cloud-question::before { + content: "\FA38"; +} +.mdi-cloud-search::before { + content: "\F955"; +} +.mdi-cloud-search-outline::before { + content: "\F956"; +} +.mdi-cloud-sync::before { + content: "\F63F"; +} +.mdi-cloud-sync-outline::before { + content: "\F0301"; +} +.mdi-cloud-tags::before { + content: "\F7B5"; +} +.mdi-cloud-upload::before { + content: "\F167"; +} +.mdi-cloud-upload-outline::before { + content: "\FB5A"; +} +.mdi-clover::before { + content: "\F815"; +} +.mdi-coach-lamp::before { + content: "\F0042"; +} +.mdi-coat-rack::before { + content: "\F00C9"; +} +.mdi-code-array::before { + content: "\F168"; +} +.mdi-code-braces::before { + content: "\F169"; +} +.mdi-code-braces-box::before { + content: "\F0101"; +} +.mdi-code-brackets::before { + content: "\F16A"; +} +.mdi-code-equal::before { + content: "\F16B"; +} +.mdi-code-greater-than::before { + content: "\F16C"; +} +.mdi-code-greater-than-or-equal::before { + content: "\F16D"; +} +.mdi-code-less-than::before { + content: "\F16E"; +} +.mdi-code-less-than-or-equal::before { + content: "\F16F"; +} +.mdi-code-not-equal::before { + content: "\F170"; +} +.mdi-code-not-equal-variant::before { + content: "\F171"; +} +.mdi-code-parentheses::before { + content: "\F172"; +} +.mdi-code-parentheses-box::before { + content: "\F0102"; +} +.mdi-code-string::before { + content: "\F173"; +} +.mdi-code-tags::before { + content: "\F174"; +} +.mdi-code-tags-check::before { + content: "\F693"; +} +.mdi-codepen::before { + content: "\F175"; +} +.mdi-coffee::before { + content: "\F176"; +} +.mdi-coffee-maker::before { + content: "\F00CA"; +} +.mdi-coffee-off::before { + content: "\FFCA"; +} +.mdi-coffee-off-outline::before { + content: "\FFCB"; +} +.mdi-coffee-outline::before { + content: "\F6C9"; +} +.mdi-coffee-to-go::before { + content: "\F177"; +} +.mdi-coffee-to-go-outline::before { + content: "\F0339"; +} +.mdi-coffin::before { + content: "\FB5B"; +} +.mdi-cog-clockwise::before { + content: "\F0208"; +} +.mdi-cog-counterclockwise::before { + content: "\F0209"; +} +.mdi-cogs::before { + content: "\F8D5"; +} +.mdi-coin::before { + content: "\F0196"; +} +.mdi-coin-outline::before { + content: "\F178"; +} +.mdi-coins::before { + content: "\F694"; +} +.mdi-collage::before { + content: "\F640"; +} +.mdi-collapse-all::before { + content: "\FAA5"; +} +.mdi-collapse-all-outline::before { + content: "\FAA6"; +} +.mdi-color-helper::before { + content: "\F179"; +} +.mdi-comma::before { + content: "\FE74"; +} +.mdi-comma-box::before { + content: "\FE75"; +} +.mdi-comma-box-outline::before { + content: "\FE76"; +} +.mdi-comma-circle::before { + content: "\FE77"; +} +.mdi-comma-circle-outline::before { + content: "\FE78"; +} +.mdi-comment::before { + content: "\F17A"; +} +.mdi-comment-account::before { + content: "\F17B"; +} +.mdi-comment-account-outline::before { + content: "\F17C"; +} +.mdi-comment-alert::before { + content: "\F17D"; +} +.mdi-comment-alert-outline::before { + content: "\F17E"; +} +.mdi-comment-arrow-left::before { + content: "\F9E0"; +} +.mdi-comment-arrow-left-outline::before { + content: "\F9E1"; +} +.mdi-comment-arrow-right::before { + content: "\F9E2"; +} +.mdi-comment-arrow-right-outline::before { + content: "\F9E3"; +} +.mdi-comment-check::before { + content: "\F17F"; +} +.mdi-comment-check-outline::before { + content: "\F180"; +} +.mdi-comment-edit::before { + content: "\F01EA"; +} +.mdi-comment-edit-outline::before { + content: "\F02EF"; +} +.mdi-comment-eye::before { + content: "\FA39"; +} +.mdi-comment-eye-outline::before { + content: "\FA3A"; +} +.mdi-comment-multiple::before { + content: "\F85E"; +} +.mdi-comment-multiple-outline::before { + content: "\F181"; +} +.mdi-comment-outline::before { + content: "\F182"; +} +.mdi-comment-plus::before { + content: "\F9E4"; +} +.mdi-comment-plus-outline::before { + content: "\F183"; +} +.mdi-comment-processing::before { + content: "\F184"; +} +.mdi-comment-processing-outline::before { + content: "\F185"; +} +.mdi-comment-question::before { + content: "\F816"; +} +.mdi-comment-question-outline::before { + content: "\F186"; +} +.mdi-comment-quote::before { + content: "\F0043"; +} +.mdi-comment-quote-outline::before { + content: "\F0044"; +} +.mdi-comment-remove::before { + content: "\F5DE"; +} +.mdi-comment-remove-outline::before { + content: "\F187"; +} +.mdi-comment-search::before { + content: "\FA3B"; +} +.mdi-comment-search-outline::before { + content: "\FA3C"; +} +.mdi-comment-text::before { + content: "\F188"; +} +.mdi-comment-text-multiple::before { + content: "\F85F"; +} +.mdi-comment-text-multiple-outline::before { + content: "\F860"; +} +.mdi-comment-text-outline::before { + content: "\F189"; +} +.mdi-compare::before { + content: "\F18A"; +} +.mdi-compass::before { + content: "\F18B"; +} +.mdi-compass-off::before { + content: "\FB5C"; +} +.mdi-compass-off-outline::before { + content: "\FB5D"; +} +.mdi-compass-outline::before { + content: "\F18C"; +} +.mdi-compass-rose::before { + content: "\F03AD"; +} +.mdi-concourse-ci::before { + content: "\F00CB"; +} +.mdi-console::before { + content: "\F18D"; +} +.mdi-console-line::before { + content: "\F7B6"; +} +.mdi-console-network::before { + content: "\F8A8"; +} +.mdi-console-network-outline::before { + content: "\FC3C"; +} +.mdi-consolidate::before { + content: "\F0103"; +} +.mdi-contact-mail::before { + content: "\F18E"; +} +.mdi-contact-mail-outline::before { + content: "\FEB5"; +} +.mdi-contact-phone::before { + content: "\FEB6"; +} +.mdi-contact-phone-outline::before { + content: "\FEB7"; +} +.mdi-contactless-payment::before { + content: "\FD46"; +} +.mdi-contacts::before { + content: "\F6CA"; +} +.mdi-contain::before { + content: "\FA3D"; +} +.mdi-contain-end::before { + content: "\FA3E"; +} +.mdi-contain-start::before { + content: "\FA3F"; +} +.mdi-content-copy::before { + content: "\F18F"; +} +.mdi-content-cut::before { + content: "\F190"; +} +.mdi-content-duplicate::before { + content: "\F191"; +} +.mdi-content-paste::before { + content: "\F192"; +} +.mdi-content-save::before { + content: "\F193"; +} +.mdi-content-save-alert::before { + content: "\FF5F"; +} +.mdi-content-save-alert-outline::before { + content: "\FF60"; +} +.mdi-content-save-all::before { + content: "\F194"; +} +.mdi-content-save-all-outline::before { + content: "\FF61"; +} +.mdi-content-save-edit::before { + content: "\FCD7"; +} +.mdi-content-save-edit-outline::before { + content: "\FCD8"; +} +.mdi-content-save-move::before { + content: "\FE79"; +} +.mdi-content-save-move-outline::before { + content: "\FE7A"; +} +.mdi-content-save-outline::before { + content: "\F817"; +} +.mdi-content-save-settings::before { + content: "\F61B"; +} +.mdi-content-save-settings-outline::before { + content: "\FB13"; +} +.mdi-contrast::before { + content: "\F195"; +} +.mdi-contrast-box::before { + content: "\F196"; +} +.mdi-contrast-circle::before { + content: "\F197"; +} +.mdi-controller-classic::before { + content: "\FB5E"; +} +.mdi-controller-classic-outline::before { + content: "\FB5F"; +} +.mdi-cookie::before { + content: "\F198"; +} +.mdi-coolant-temperature::before { + content: "\F3C8"; +} +.mdi-copyright::before { + content: "\F5E6"; +} +.mdi-cordova::before { + content: "\F957"; +} +.mdi-corn::before { + content: "\F7B7"; +} +.mdi-counter::before { + content: "\F199"; +} +.mdi-cow::before { + content: "\F19A"; +} +.mdi-cowboy::before { + content: "\FEB8"; +} +.mdi-cpu-32-bit::before { + content: "\FEFC"; +} +.mdi-cpu-64-bit::before { + content: "\FEFD"; +} +.mdi-crane::before { + content: "\F861"; +} +.mdi-creation::before { + content: "\F1C9"; +} +.mdi-creative-commons::before { + content: "\FD47"; +} +.mdi-credit-card::before { + content: "\F0010"; +} +.mdi-credit-card-clock::before { + content: "\FEFE"; +} +.mdi-credit-card-clock-outline::before { + content: "\FFBC"; +} +.mdi-credit-card-marker::before { + content: "\F6A7"; +} +.mdi-credit-card-marker-outline::before { + content: "\FD9A"; +} +.mdi-credit-card-minus::before { + content: "\FFCC"; +} +.mdi-credit-card-minus-outline::before { + content: "\FFCD"; +} +.mdi-credit-card-multiple::before { + content: "\F0011"; +} +.mdi-credit-card-multiple-outline::before { + content: "\F19C"; +} +.mdi-credit-card-off::before { + content: "\F0012"; +} +.mdi-credit-card-off-outline::before { + content: "\F5E4"; +} +.mdi-credit-card-outline::before { + content: "\F19B"; +} +.mdi-credit-card-plus::before { + content: "\F0013"; +} +.mdi-credit-card-plus-outline::before { + content: "\F675"; +} +.mdi-credit-card-refund::before { + content: "\F0014"; +} +.mdi-credit-card-refund-outline::before { + content: "\FAA7"; +} +.mdi-credit-card-remove::before { + content: "\FFCE"; +} +.mdi-credit-card-remove-outline::before { + content: "\FFCF"; +} +.mdi-credit-card-scan::before { + content: "\F0015"; +} +.mdi-credit-card-scan-outline::before { + content: "\F19D"; +} +.mdi-credit-card-settings::before { + content: "\F0016"; +} +.mdi-credit-card-settings-outline::before { + content: "\F8D6"; +} +.mdi-credit-card-wireless::before { + content: "\F801"; +} +.mdi-credit-card-wireless-outline::before { + content: "\FD48"; +} +.mdi-cricket::before { + content: "\FD49"; +} +.mdi-crop::before { + content: "\F19E"; +} +.mdi-crop-free::before { + content: "\F19F"; +} +.mdi-crop-landscape::before { + content: "\F1A0"; +} +.mdi-crop-portrait::before { + content: "\F1A1"; +} +.mdi-crop-rotate::before { + content: "\F695"; +} +.mdi-crop-square::before { + content: "\F1A2"; +} +.mdi-crosshairs::before { + content: "\F1A3"; +} +.mdi-crosshairs-gps::before { + content: "\F1A4"; +} +.mdi-crosshairs-off::before { + content: "\FF62"; +} +.mdi-crosshairs-question::before { + content: "\F0161"; +} +.mdi-crown::before { + content: "\F1A5"; +} +.mdi-crown-outline::before { + content: "\F01FB"; +} +.mdi-cryengine::before { + content: "\F958"; +} +.mdi-crystal-ball::before { + content: "\FB14"; +} +.mdi-cube::before { + content: "\F1A6"; +} +.mdi-cube-outline::before { + content: "\F1A7"; +} +.mdi-cube-scan::before { + content: "\FB60"; +} +.mdi-cube-send::before { + content: "\F1A8"; +} +.mdi-cube-unfolded::before { + content: "\F1A9"; +} +.mdi-cup::before { + content: "\F1AA"; +} +.mdi-cup-off::before { + content: "\F5E5"; +} +.mdi-cup-off-outline::before { + content: "\F03A8"; +} +.mdi-cup-outline::before { + content: "\F033A"; +} +.mdi-cup-water::before { + content: "\F1AB"; +} +.mdi-cupboard::before { + content: "\FF63"; +} +.mdi-cupboard-outline::before { + content: "\FF64"; +} +.mdi-cupcake::before { + content: "\F959"; +} +.mdi-curling::before { + content: "\F862"; +} +.mdi-currency-bdt::before { + content: "\F863"; +} +.mdi-currency-brl::before { + content: "\FB61"; +} +.mdi-currency-btc::before { + content: "\F1AC"; +} +.mdi-currency-cny::before { + content: "\F7B9"; +} +.mdi-currency-eth::before { + content: "\F7BA"; +} +.mdi-currency-eur::before { + content: "\F1AD"; +} +.mdi-currency-eur-off::before { + content: "\F0340"; +} +.mdi-currency-gbp::before { + content: "\F1AE"; +} +.mdi-currency-ils::before { + content: "\FC3D"; +} +.mdi-currency-inr::before { + content: "\F1AF"; +} +.mdi-currency-jpy::before { + content: "\F7BB"; +} +.mdi-currency-krw::before { + content: "\F7BC"; +} +.mdi-currency-kzt::before { + content: "\F864"; +} +.mdi-currency-ngn::before { + content: "\F1B0"; +} +.mdi-currency-php::before { + content: "\F9E5"; +} +.mdi-currency-rial::before { + content: "\FEB9"; +} +.mdi-currency-rub::before { + content: "\F1B1"; +} +.mdi-currency-sign::before { + content: "\F7BD"; +} +.mdi-currency-try::before { + content: "\F1B2"; +} +.mdi-currency-twd::before { + content: "\F7BE"; +} +.mdi-currency-usd::before { + content: "\F1B3"; +} +.mdi-currency-usd-off::before { + content: "\F679"; +} +.mdi-current-ac::before { + content: "\F95A"; +} +.mdi-current-dc::before { + content: "\F95B"; +} +.mdi-cursor-default::before { + content: "\F1B4"; +} +.mdi-cursor-default-click::before { + content: "\FCD9"; +} +.mdi-cursor-default-click-outline::before { + content: "\FCDA"; +} +.mdi-cursor-default-gesture::before { + content: "\F0152"; +} +.mdi-cursor-default-gesture-outline::before { + content: "\F0153"; +} +.mdi-cursor-default-outline::before { + content: "\F1B5"; +} +.mdi-cursor-move::before { + content: "\F1B6"; +} +.mdi-cursor-pointer::before { + content: "\F1B7"; +} +.mdi-cursor-text::before { + content: "\F5E7"; +} +.mdi-database::before { + content: "\F1B8"; +} +.mdi-database-check::before { + content: "\FAA8"; +} +.mdi-database-edit::before { + content: "\FB62"; +} +.mdi-database-export::before { + content: "\F95D"; +} +.mdi-database-import::before { + content: "\F95C"; +} +.mdi-database-lock::before { + content: "\FAA9"; +} +.mdi-database-marker::before { + content: "\F0321"; +} +.mdi-database-minus::before { + content: "\F1B9"; +} +.mdi-database-plus::before { + content: "\F1BA"; +} +.mdi-database-refresh::before { + content: "\FCDB"; +} +.mdi-database-remove::before { + content: "\FCDC"; +} +.mdi-database-search::before { + content: "\F865"; +} +.mdi-database-settings::before { + content: "\FCDD"; +} +.mdi-death-star::before { + content: "\F8D7"; +} +.mdi-death-star-variant::before { + content: "\F8D8"; +} +.mdi-deathly-hallows::before { + content: "\FB63"; +} +.mdi-debian::before { + content: "\F8D9"; +} +.mdi-debug-step-into::before { + content: "\F1BB"; +} +.mdi-debug-step-out::before { + content: "\F1BC"; +} +.mdi-debug-step-over::before { + content: "\F1BD"; +} +.mdi-decagram::before { + content: "\F76B"; +} +.mdi-decagram-outline::before { + content: "\F76C"; +} +.mdi-decimal::before { + content: "\F00CC"; +} +.mdi-decimal-comma::before { + content: "\F00CD"; +} +.mdi-decimal-comma-decrease::before { + content: "\F00CE"; +} +.mdi-decimal-comma-increase::before { + content: "\F00CF"; +} +.mdi-decimal-decrease::before { + content: "\F1BE"; +} +.mdi-decimal-increase::before { + content: "\F1BF"; +} +.mdi-delete::before { + content: "\F1C0"; +} +.mdi-delete-alert::before { + content: "\F00D0"; +} +.mdi-delete-alert-outline::before { + content: "\F00D1"; +} +.mdi-delete-circle::before { + content: "\F682"; +} +.mdi-delete-circle-outline::before { + content: "\FB64"; +} +.mdi-delete-empty::before { + content: "\F6CB"; +} +.mdi-delete-empty-outline::before { + content: "\FEBA"; +} +.mdi-delete-forever::before { + content: "\F5E8"; +} +.mdi-delete-forever-outline::before { + content: "\FB65"; +} +.mdi-delete-off::before { + content: "\F00D2"; +} +.mdi-delete-off-outline::before { + content: "\F00D3"; +} +.mdi-delete-outline::before { + content: "\F9E6"; +} +.mdi-delete-restore::before { + content: "\F818"; +} +.mdi-delete-sweep::before { + content: "\F5E9"; +} +.mdi-delete-sweep-outline::before { + content: "\FC3E"; +} +.mdi-delete-variant::before { + content: "\F1C1"; +} +.mdi-delta::before { + content: "\F1C2"; +} +.mdi-desk::before { + content: "\F0264"; +} +.mdi-desk-lamp::before { + content: "\F95E"; +} +.mdi-deskphone::before { + content: "\F1C3"; +} +.mdi-desktop-classic::before { + content: "\F7BF"; +} +.mdi-desktop-mac::before { + content: "\F1C4"; +} +.mdi-desktop-mac-dashboard::before { + content: "\F9E7"; +} +.mdi-desktop-tower::before { + content: "\F1C5"; +} +.mdi-desktop-tower-monitor::before { + content: "\FAAA"; +} +.mdi-details::before { + content: "\F1C6"; +} +.mdi-dev-to::before { + content: "\FD4A"; +} +.mdi-developer-board::before { + content: "\F696"; +} +.mdi-deviantart::before { + content: "\F1C7"; +} +.mdi-devices::before { + content: "\FFD0"; +} +.mdi-diabetes::before { + content: "\F0151"; +} +.mdi-dialpad::before { + content: "\F61C"; +} +.mdi-diameter::before { + content: "\FC3F"; +} +.mdi-diameter-outline::before { + content: "\FC40"; +} +.mdi-diameter-variant::before { + content: "\FC41"; +} +.mdi-diamond::before { + content: "\FB66"; +} +.mdi-diamond-outline::before { + content: "\FB67"; +} +.mdi-diamond-stone::before { + content: "\F1C8"; +} +.mdi-dice-1::before { + content: "\F1CA"; +} +.mdi-dice-1-outline::before { + content: "\F0175"; +} +.mdi-dice-2::before { + content: "\F1CB"; +} +.mdi-dice-2-outline::before { + content: "\F0176"; +} +.mdi-dice-3::before { + content: "\F1CC"; +} +.mdi-dice-3-outline::before { + content: "\F0177"; +} +.mdi-dice-4::before { + content: "\F1CD"; +} +.mdi-dice-4-outline::before { + content: "\F0178"; +} +.mdi-dice-5::before { + content: "\F1CE"; +} +.mdi-dice-5-outline::before { + content: "\F0179"; +} +.mdi-dice-6::before { + content: "\F1CF"; +} +.mdi-dice-6-outline::before { + content: "\F017A"; +} +.mdi-dice-d10::before { + content: "\F017E"; +} +.mdi-dice-d10-outline::before { + content: "\F76E"; +} +.mdi-dice-d12::before { + content: "\F017F"; +} +.mdi-dice-d12-outline::before { + content: "\F866"; +} +.mdi-dice-d20::before { + content: "\F0180"; +} +.mdi-dice-d20-outline::before { + content: "\F5EA"; +} +.mdi-dice-d4::before { + content: "\F017B"; +} +.mdi-dice-d4-outline::before { + content: "\F5EB"; +} +.mdi-dice-d6::before { + content: "\F017C"; +} +.mdi-dice-d6-outline::before { + content: "\F5EC"; +} +.mdi-dice-d8::before { + content: "\F017D"; +} +.mdi-dice-d8-outline::before { + content: "\F5ED"; +} +.mdi-dice-multiple::before { + content: "\F76D"; +} +.mdi-dice-multiple-outline::before { + content: "\F0181"; +} +.mdi-dictionary::before { + content: "\F61D"; +} +.mdi-digital-ocean::before { + content: "\F0262"; +} +.mdi-dip-switch::before { + content: "\F7C0"; +} +.mdi-directions::before { + content: "\F1D0"; +} +.mdi-directions-fork::before { + content: "\F641"; +} +.mdi-disc::before { + content: "\F5EE"; +} +.mdi-disc-alert::before { + content: "\F1D1"; +} +.mdi-disc-player::before { + content: "\F95F"; +} +.mdi-discord::before { + content: "\F66F"; +} +.mdi-dishwasher::before { + content: "\FAAB"; +} +.mdi-dishwasher-alert::before { + content: "\F01E3"; +} +.mdi-dishwasher-off::before { + content: "\F01E4"; +} +.mdi-disqus::before { + content: "\F1D2"; +} +.mdi-disqus-outline::before { + content: "\F1D3"; +} +.mdi-distribute-horizontal-center::before { + content: "\F01F4"; +} +.mdi-distribute-horizontal-left::before { + content: "\F01F3"; +} +.mdi-distribute-horizontal-right::before { + content: "\F01F5"; +} +.mdi-distribute-vertical-bottom::before { + content: "\F01F6"; +} +.mdi-distribute-vertical-center::before { + content: "\F01F7"; +} +.mdi-distribute-vertical-top::before { + content: "\F01F8"; +} +.mdi-diving-flippers::before { + content: "\FD9B"; +} +.mdi-diving-helmet::before { + content: "\FD9C"; +} +.mdi-diving-scuba::before { + content: "\FD9D"; +} +.mdi-diving-scuba-flag::before { + content: "\FD9E"; +} +.mdi-diving-scuba-tank::before { + content: "\FD9F"; +} +.mdi-diving-scuba-tank-multiple::before { + content: "\FDA0"; +} +.mdi-diving-snorkel::before { + content: "\FDA1"; +} +.mdi-division::before { + content: "\F1D4"; +} +.mdi-division-box::before { + content: "\F1D5"; +} +.mdi-dlna::before { + content: "\FA40"; +} +.mdi-dna::before { + content: "\F683"; +} +.mdi-dns::before { + content: "\F1D6"; +} +.mdi-dns-outline::before { + content: "\FB68"; +} +.mdi-do-not-disturb::before { + content: "\F697"; +} +.mdi-do-not-disturb-off::before { + content: "\F698"; +} +.mdi-dock-bottom::before { + content: "\F00D4"; +} +.mdi-dock-left::before { + content: "\F00D5"; +} +.mdi-dock-right::before { + content: "\F00D6"; +} +.mdi-dock-window::before { + content: "\F00D7"; +} +.mdi-docker::before { + content: "\F867"; +} +.mdi-doctor::before { + content: "\FA41"; +} +.mdi-dog::before { + content: "\FA42"; +} +.mdi-dog-service::before { + content: "\FAAC"; +} +.mdi-dog-side::before { + content: "\FA43"; +} +.mdi-dolby::before { + content: "\F6B2"; +} +.mdi-dolly::before { + content: "\FEBB"; +} +.mdi-domain::before { + content: "\F1D7"; +} +.mdi-domain-off::before { + content: "\FD4B"; +} +.mdi-domain-plus::before { + content: "\F00D8"; +} +.mdi-domain-remove::before { + content: "\F00D9"; +} +.mdi-domino-mask::before { + content: "\F0045"; +} +.mdi-donkey::before { + content: "\F7C1"; +} +.mdi-door::before { + content: "\F819"; +} +.mdi-door-closed::before { + content: "\F81A"; +} +.mdi-door-closed-lock::before { + content: "\F00DA"; +} +.mdi-door-open::before { + content: "\F81B"; +} +.mdi-doorbell::before { + content: "\F0311"; +} +.mdi-doorbell-video::before { + content: "\F868"; +} +.mdi-dot-net::before { + content: "\FAAD"; +} +.mdi-dots-horizontal::before { + content: "\F1D8"; +} +.mdi-dots-horizontal-circle::before { + content: "\F7C2"; +} +.mdi-dots-horizontal-circle-outline::before { + content: "\FB69"; +} +.mdi-dots-vertical::before { + content: "\F1D9"; +} +.mdi-dots-vertical-circle::before { + content: "\F7C3"; +} +.mdi-dots-vertical-circle-outline::before { + content: "\FB6A"; +} +.mdi-douban::before { + content: "\F699"; +} +.mdi-download::before { + content: "\F1DA"; +} +.mdi-download-lock::before { + content: "\F034B"; +} +.mdi-download-lock-outline::before { + content: "\F034C"; +} +.mdi-download-multiple::before { + content: "\F9E8"; +} +.mdi-download-network::before { + content: "\F6F3"; +} +.mdi-download-network-outline::before { + content: "\FC42"; +} +.mdi-download-off::before { + content: "\F00DB"; +} +.mdi-download-off-outline::before { + content: "\F00DC"; +} +.mdi-download-outline::before { + content: "\FB6B"; +} +.mdi-drag::before { + content: "\F1DB"; +} +.mdi-drag-horizontal::before { + content: "\F1DC"; +} +.mdi-drag-horizontal-variant::before { + content: "\F031B"; +} +.mdi-drag-variant::before { + content: "\FB6C"; +} +.mdi-drag-vertical::before { + content: "\F1DD"; +} +.mdi-drag-vertical-variant::before { + content: "\F031C"; +} +.mdi-drama-masks::before { + content: "\FCDE"; +} +.mdi-draw::before { + content: "\FF66"; +} +.mdi-drawing::before { + content: "\F1DE"; +} +.mdi-drawing-box::before { + content: "\F1DF"; +} +.mdi-dresser::before { + content: "\FF67"; +} +.mdi-dresser-outline::before { + content: "\FF68"; +} +.mdi-dribbble::before { + content: "\F1E0"; +} +.mdi-dribbble-box::before { + content: "\F1E1"; +} +.mdi-drone::before { + content: "\F1E2"; +} +.mdi-dropbox::before { + content: "\F1E3"; +} +.mdi-drupal::before { + content: "\F1E4"; +} +.mdi-duck::before { + content: "\F1E5"; +} +.mdi-dumbbell::before { + content: "\F1E6"; +} +.mdi-dump-truck::before { + content: "\FC43"; +} +.mdi-ear-hearing::before { + content: "\F7C4"; +} +.mdi-ear-hearing-off::before { + content: "\FA44"; +} +.mdi-earth::before { + content: "\F1E7"; +} +.mdi-earth-arrow-right::before { + content: "\F033C"; +} +.mdi-earth-box::before { + content: "\F6CC"; +} +.mdi-earth-box-off::before { + content: "\F6CD"; +} +.mdi-earth-off::before { + content: "\F1E8"; +} +.mdi-edge::before { + content: "\F1E9"; +} +.mdi-edge-legacy::before { + content: "\F027B"; +} +.mdi-egg::before { + content: "\FAAE"; +} +.mdi-egg-easter::before { + content: "\FAAF"; +} +.mdi-eight-track::before { + content: "\F9E9"; +} +.mdi-eject::before { + content: "\F1EA"; +} +.mdi-eject-outline::before { + content: "\FB6D"; +} +.mdi-electric-switch::before { + content: "\FEBC"; +} +.mdi-electric-switch-closed::before { + content: "\F0104"; +} +.mdi-electron-framework::before { + content: "\F0046"; +} +.mdi-elephant::before { + content: "\F7C5"; +} +.mdi-elevation-decline::before { + content: "\F1EB"; +} +.mdi-elevation-rise::before { + content: "\F1EC"; +} +.mdi-elevator::before { + content: "\F1ED"; +} +.mdi-elevator-down::before { + content: "\F02ED"; +} +.mdi-elevator-passenger::before { + content: "\F03AC"; +} +.mdi-elevator-up::before { + content: "\F02EC"; +} +.mdi-ellipse::before { + content: "\FEBD"; +} +.mdi-ellipse-outline::before { + content: "\FEBE"; +} +.mdi-email::before { + content: "\F1EE"; +} +.mdi-email-alert::before { + content: "\F6CE"; +} +.mdi-email-alert-outline::before { + content: "\FD1E"; +} +.mdi-email-box::before { + content: "\FCDF"; +} +.mdi-email-check::before { + content: "\FAB0"; +} +.mdi-email-check-outline::before { + content: "\FAB1"; +} +.mdi-email-edit::before { + content: "\FF00"; +} +.mdi-email-edit-outline::before { + content: "\FF01"; +} +.mdi-email-lock::before { + content: "\F1F1"; +} +.mdi-email-mark-as-unread::before { + content: "\FB6E"; +} +.mdi-email-minus::before { + content: "\FF02"; +} +.mdi-email-minus-outline::before { + content: "\FF03"; +} +.mdi-email-multiple::before { + content: "\FF04"; +} +.mdi-email-multiple-outline::before { + content: "\FF05"; +} +.mdi-email-newsletter::before { + content: "\FFD1"; +} +.mdi-email-open::before { + content: "\F1EF"; +} +.mdi-email-open-multiple::before { + content: "\FF06"; +} +.mdi-email-open-multiple-outline::before { + content: "\FF07"; +} +.mdi-email-open-outline::before { + content: "\F5EF"; +} +.mdi-email-outline::before { + content: "\F1F0"; +} +.mdi-email-plus::before { + content: "\F9EA"; +} +.mdi-email-plus-outline::before { + content: "\F9EB"; +} +.mdi-email-receive::before { + content: "\F0105"; +} +.mdi-email-receive-outline::before { + content: "\F0106"; +} +.mdi-email-search::before { + content: "\F960"; +} +.mdi-email-search-outline::before { + content: "\F961"; +} +.mdi-email-send::before { + content: "\F0107"; +} +.mdi-email-send-outline::before { + content: "\F0108"; +} +.mdi-email-sync::before { + content: "\F02F2"; +} +.mdi-email-sync-outline::before { + content: "\F02F3"; +} +.mdi-email-variant::before { + content: "\F5F0"; +} +.mdi-ember::before { + content: "\FB15"; +} +.mdi-emby::before { + content: "\F6B3"; +} +.mdi-emoticon::before { + content: "\FC44"; +} +.mdi-emoticon-angry::before { + content: "\FC45"; +} +.mdi-emoticon-angry-outline::before { + content: "\FC46"; +} +.mdi-emoticon-confused::before { + content: "\F0109"; +} +.mdi-emoticon-confused-outline::before { + content: "\F010A"; +} +.mdi-emoticon-cool::before { + content: "\FC47"; +} +.mdi-emoticon-cool-outline::before { + content: "\F1F3"; +} +.mdi-emoticon-cry::before { + content: "\FC48"; +} +.mdi-emoticon-cry-outline::before { + content: "\FC49"; +} +.mdi-emoticon-dead::before { + content: "\FC4A"; +} +.mdi-emoticon-dead-outline::before { + content: "\F69A"; +} +.mdi-emoticon-devil::before { + content: "\FC4B"; +} +.mdi-emoticon-devil-outline::before { + content: "\F1F4"; +} +.mdi-emoticon-excited::before { + content: "\FC4C"; +} +.mdi-emoticon-excited-outline::before { + content: "\F69B"; +} +.mdi-emoticon-frown::before { + content: "\FF69"; +} +.mdi-emoticon-frown-outline::before { + content: "\FF6A"; +} +.mdi-emoticon-happy::before { + content: "\FC4D"; +} +.mdi-emoticon-happy-outline::before { + content: "\F1F5"; +} +.mdi-emoticon-kiss::before { + content: "\FC4E"; +} +.mdi-emoticon-kiss-outline::before { + content: "\FC4F"; +} +.mdi-emoticon-lol::before { + content: "\F023F"; +} +.mdi-emoticon-lol-outline::before { + content: "\F0240"; +} +.mdi-emoticon-neutral::before { + content: "\FC50"; +} +.mdi-emoticon-neutral-outline::before { + content: "\F1F6"; +} +.mdi-emoticon-outline::before { + content: "\F1F2"; +} +.mdi-emoticon-poop::before { + content: "\F1F7"; +} +.mdi-emoticon-poop-outline::before { + content: "\FC51"; +} +.mdi-emoticon-sad::before { + content: "\FC52"; +} +.mdi-emoticon-sad-outline::before { + content: "\F1F8"; +} +.mdi-emoticon-tongue::before { + content: "\F1F9"; +} +.mdi-emoticon-tongue-outline::before { + content: "\FC53"; +} +.mdi-emoticon-wink::before { + content: "\FC54"; +} +.mdi-emoticon-wink-outline::before { + content: "\FC55"; +} +.mdi-engine::before { + content: "\F1FA"; +} +.mdi-engine-off::before { + content: "\FA45"; +} +.mdi-engine-off-outline::before { + content: "\FA46"; +} +.mdi-engine-outline::before { + content: "\F1FB"; +} +.mdi-epsilon::before { + content: "\F010B"; +} +.mdi-equal::before { + content: "\F1FC"; +} +.mdi-equal-box::before { + content: "\F1FD"; +} +.mdi-equalizer::before { + content: "\FEBF"; +} +.mdi-equalizer-outline::before { + content: "\FEC0"; +} +.mdi-eraser::before { + content: "\F1FE"; +} +.mdi-eraser-variant::before { + content: "\F642"; +} +.mdi-escalator::before { + content: "\F1FF"; +} +.mdi-escalator-down::before { + content: "\F02EB"; +} +.mdi-escalator-up::before { + content: "\F02EA"; +} +.mdi-eslint::before { + content: "\FC56"; +} +.mdi-et::before { + content: "\FAB2"; +} +.mdi-ethereum::before { + content: "\F869"; +} +.mdi-ethernet::before { + content: "\F200"; +} +.mdi-ethernet-cable::before { + content: "\F201"; +} +.mdi-ethernet-cable-off::before { + content: "\F202"; +} +.mdi-etsy::before { + content: "\F203"; +} +.mdi-ev-station::before { + content: "\F5F1"; +} +.mdi-eventbrite::before { + content: "\F7C6"; +} +.mdi-evernote::before { + content: "\F204"; +} +.mdi-excavator::before { + content: "\F0047"; +} +.mdi-exclamation::before { + content: "\F205"; +} +.mdi-exclamation-thick::before { + content: "\F0263"; +} +.mdi-exit-run::before { + content: "\FA47"; +} +.mdi-exit-to-app::before { + content: "\F206"; +} +.mdi-expand-all::before { + content: "\FAB3"; +} +.mdi-expand-all-outline::before { + content: "\FAB4"; +} +.mdi-expansion-card::before { + content: "\F8AD"; +} +.mdi-expansion-card-variant::before { + content: "\FFD2"; +} +.mdi-exponent::before { + content: "\F962"; +} +.mdi-exponent-box::before { + content: "\F963"; +} +.mdi-export::before { + content: "\F207"; +} +.mdi-export-variant::before { + content: "\FB6F"; +} +.mdi-eye::before { + content: "\F208"; +} +.mdi-eye-check::before { + content: "\FCE0"; +} +.mdi-eye-check-outline::before { + content: "\FCE1"; +} +.mdi-eye-circle::before { + content: "\FB70"; +} +.mdi-eye-circle-outline::before { + content: "\FB71"; +} +.mdi-eye-minus::before { + content: "\F0048"; +} +.mdi-eye-minus-outline::before { + content: "\F0049"; +} +.mdi-eye-off::before { + content: "\F209"; +} +.mdi-eye-off-outline::before { + content: "\F6D0"; +} +.mdi-eye-outline::before { + content: "\F6CF"; +} +.mdi-eye-plus::before { + content: "\F86A"; +} +.mdi-eye-plus-outline::before { + content: "\F86B"; +} +.mdi-eye-settings::before { + content: "\F86C"; +} +.mdi-eye-settings-outline::before { + content: "\F86D"; +} +.mdi-eyedropper::before { + content: "\F20A"; +} +.mdi-eyedropper-variant::before { + content: "\F20B"; +} +.mdi-face::before { + content: "\F643"; +} +.mdi-face-agent::before { + content: "\FD4C"; +} +.mdi-face-outline::before { + content: "\FB72"; +} +.mdi-face-profile::before { + content: "\F644"; +} +.mdi-face-profile-woman::before { + content: "\F00A1"; +} +.mdi-face-recognition::before { + content: "\FC57"; +} +.mdi-face-woman::before { + content: "\F00A2"; +} +.mdi-face-woman-outline::before { + content: "\F00A3"; +} +.mdi-facebook::before { + content: "\F20C"; +} +.mdi-facebook-box::before { + content: "\F20D"; +} +.mdi-facebook-messenger::before { + content: "\F20E"; +} +.mdi-facebook-workplace::before { + content: "\FB16"; +} +.mdi-factory::before { + content: "\F20F"; +} +.mdi-fan::before { + content: "\F210"; +} +.mdi-fan-off::before { + content: "\F81C"; +} +.mdi-fast-forward::before { + content: "\F211"; +} +.mdi-fast-forward-10::before { + content: "\FD4D"; +} +.mdi-fast-forward-30::before { + content: "\FCE2"; +} +.mdi-fast-forward-5::before { + content: "\F0223"; +} +.mdi-fast-forward-outline::before { + content: "\F6D1"; +} +.mdi-fax::before { + content: "\F212"; +} +.mdi-feather::before { + content: "\F6D2"; +} +.mdi-feature-search::before { + content: "\FA48"; +} +.mdi-feature-search-outline::before { + content: "\FA49"; +} +.mdi-fedora::before { + content: "\F8DA"; +} +.mdi-ferris-wheel::before { + content: "\FEC1"; +} +.mdi-ferry::before { + content: "\F213"; +} +.mdi-file::before { + content: "\F214"; +} +.mdi-file-account::before { + content: "\F73A"; +} +.mdi-file-account-outline::before { + content: "\F004A"; +} +.mdi-file-alert::before { + content: "\FA4A"; +} +.mdi-file-alert-outline::before { + content: "\FA4B"; +} +.mdi-file-cabinet::before { + content: "\FAB5"; +} +.mdi-file-cad::before { + content: "\FF08"; +} +.mdi-file-cad-box::before { + content: "\FF09"; +} +.mdi-file-cancel::before { + content: "\FDA2"; +} +.mdi-file-cancel-outline::before { + content: "\FDA3"; +} +.mdi-file-certificate::before { + content: "\F01B1"; +} +.mdi-file-certificate-outline::before { + content: "\F01B2"; +} +.mdi-file-chart::before { + content: "\F215"; +} +.mdi-file-chart-outline::before { + content: "\F004B"; +} +.mdi-file-check::before { + content: "\F216"; +} +.mdi-file-check-outline::before { + content: "\FE7B"; +} +.mdi-file-clock::before { + content: "\F030C"; +} +.mdi-file-clock-outline::before { + content: "\F030D"; +} +.mdi-file-cloud::before { + content: "\F217"; +} +.mdi-file-cloud-outline::before { + content: "\F004C"; +} +.mdi-file-code::before { + content: "\F22E"; +} +.mdi-file-code-outline::before { + content: "\F004D"; +} +.mdi-file-compare::before { + content: "\F8A9"; +} +.mdi-file-delimited::before { + content: "\F218"; +} +.mdi-file-delimited-outline::before { + content: "\FEC2"; +} +.mdi-file-document::before { + content: "\F219"; +} +.mdi-file-document-box::before { + content: "\F21A"; +} +.mdi-file-document-box-check::before { + content: "\FEC3"; +} +.mdi-file-document-box-check-outline::before { + content: "\FEC4"; +} +.mdi-file-document-box-minus::before { + content: "\FEC5"; +} +.mdi-file-document-box-minus-outline::before { + content: "\FEC6"; +} +.mdi-file-document-box-multiple::before { + content: "\FAB6"; +} +.mdi-file-document-box-multiple-outline::before { + content: "\FAB7"; +} +.mdi-file-document-box-outline::before { + content: "\F9EC"; +} +.mdi-file-document-box-plus::before { + content: "\FEC7"; +} +.mdi-file-document-box-plus-outline::before { + content: "\FEC8"; +} +.mdi-file-document-box-remove::before { + content: "\FEC9"; +} +.mdi-file-document-box-remove-outline::before { + content: "\FECA"; +} +.mdi-file-document-box-search::before { + content: "\FECB"; +} +.mdi-file-document-box-search-outline::before { + content: "\FECC"; +} +.mdi-file-document-edit::before { + content: "\FDA4"; +} +.mdi-file-document-edit-outline::before { + content: "\FDA5"; +} +.mdi-file-document-outline::before { + content: "\F9ED"; +} +.mdi-file-download::before { + content: "\F964"; +} +.mdi-file-download-outline::before { + content: "\F965"; +} +.mdi-file-edit::before { + content: "\F0212"; +} +.mdi-file-edit-outline::before { + content: "\F0213"; +} +.mdi-file-excel::before { + content: "\F21B"; +} +.mdi-file-excel-box::before { + content: "\F21C"; +} +.mdi-file-excel-box-outline::before { + content: "\F004E"; +} +.mdi-file-excel-outline::before { + content: "\F004F"; +} +.mdi-file-export::before { + content: "\F21D"; +} +.mdi-file-export-outline::before { + content: "\F0050"; +} +.mdi-file-eye::before { + content: "\FDA6"; +} +.mdi-file-eye-outline::before { + content: "\FDA7"; +} +.mdi-file-find::before { + content: "\F21E"; +} +.mdi-file-find-outline::before { + content: "\FB73"; +} +.mdi-file-hidden::before { + content: "\F613"; +} +.mdi-file-image::before { + content: "\F21F"; +} +.mdi-file-image-outline::before { + content: "\FECD"; +} +.mdi-file-import::before { + content: "\F220"; +} +.mdi-file-import-outline::before { + content: "\F0051"; +} +.mdi-file-key::before { + content: "\F01AF"; +} +.mdi-file-key-outline::before { + content: "\F01B0"; +} +.mdi-file-link::before { + content: "\F01A2"; +} +.mdi-file-link-outline::before { + content: "\F01A3"; +} +.mdi-file-lock::before { + content: "\F221"; +} +.mdi-file-lock-outline::before { + content: "\F0052"; +} +.mdi-file-move::before { + content: "\FAB8"; +} +.mdi-file-move-outline::before { + content: "\F0053"; +} +.mdi-file-multiple::before { + content: "\F222"; +} +.mdi-file-multiple-outline::before { + content: "\F0054"; +} +.mdi-file-music::before { + content: "\F223"; +} +.mdi-file-music-outline::before { + content: "\FE7C"; +} +.mdi-file-outline::before { + content: "\F224"; +} +.mdi-file-pdf::before { + content: "\F225"; +} +.mdi-file-pdf-box::before { + content: "\F226"; +} +.mdi-file-pdf-box-outline::before { + content: "\FFD3"; +} +.mdi-file-pdf-outline::before { + content: "\FE7D"; +} +.mdi-file-percent::before { + content: "\F81D"; +} +.mdi-file-percent-outline::before { + content: "\F0055"; +} +.mdi-file-phone::before { + content: "\F01A4"; +} +.mdi-file-phone-outline::before { + content: "\F01A5"; +} +.mdi-file-plus::before { + content: "\F751"; +} +.mdi-file-plus-outline::before { + content: "\FF0A"; +} +.mdi-file-powerpoint::before { + content: "\F227"; +} +.mdi-file-powerpoint-box::before { + content: "\F228"; +} +.mdi-file-powerpoint-box-outline::before { + content: "\F0056"; +} +.mdi-file-powerpoint-outline::before { + content: "\F0057"; +} +.mdi-file-presentation-box::before { + content: "\F229"; +} +.mdi-file-question::before { + content: "\F86E"; +} +.mdi-file-question-outline::before { + content: "\F0058"; +} +.mdi-file-remove::before { + content: "\FB74"; +} +.mdi-file-remove-outline::before { + content: "\F0059"; +} +.mdi-file-replace::before { + content: "\FB17"; +} +.mdi-file-replace-outline::before { + content: "\FB18"; +} +.mdi-file-restore::before { + content: "\F670"; +} +.mdi-file-restore-outline::before { + content: "\F005A"; +} +.mdi-file-search::before { + content: "\FC58"; +} +.mdi-file-search-outline::before { + content: "\FC59"; +} +.mdi-file-send::before { + content: "\F22A"; +} +.mdi-file-send-outline::before { + content: "\F005B"; +} +.mdi-file-settings::before { + content: "\F00A4"; +} +.mdi-file-settings-outline::before { + content: "\F00A5"; +} +.mdi-file-settings-variant::before { + content: "\F00A6"; +} +.mdi-file-settings-variant-outline::before { + content: "\F00A7"; +} +.mdi-file-star::before { + content: "\F005C"; +} +.mdi-file-star-outline::before { + content: "\F005D"; +} +.mdi-file-swap::before { + content: "\FFD4"; +} +.mdi-file-swap-outline::before { + content: "\FFD5"; +} +.mdi-file-sync::before { + content: "\F0241"; +} +.mdi-file-sync-outline::before { + content: "\F0242"; +} +.mdi-file-table::before { + content: "\FC5A"; +} +.mdi-file-table-box::before { + content: "\F010C"; +} +.mdi-file-table-box-multiple::before { + content: "\F010D"; +} +.mdi-file-table-box-multiple-outline::before { + content: "\F010E"; +} +.mdi-file-table-box-outline::before { + content: "\F010F"; +} +.mdi-file-table-outline::before { + content: "\FC5B"; +} +.mdi-file-tree::before { + content: "\F645"; +} +.mdi-file-undo::before { + content: "\F8DB"; +} +.mdi-file-undo-outline::before { + content: "\F005E"; +} +.mdi-file-upload::before { + content: "\FA4C"; +} +.mdi-file-upload-outline::before { + content: "\FA4D"; +} +.mdi-file-video::before { + content: "\F22B"; +} +.mdi-file-video-outline::before { + content: "\FE10"; +} +.mdi-file-word::before { + content: "\F22C"; +} +.mdi-file-word-box::before { + content: "\F22D"; +} +.mdi-file-word-box-outline::before { + content: "\F005F"; +} +.mdi-file-word-outline::before { + content: "\F0060"; +} +.mdi-film::before { + content: "\F22F"; +} +.mdi-filmstrip::before { + content: "\F230"; +} +.mdi-filmstrip-off::before { + content: "\F231"; +} +.mdi-filter::before { + content: "\F232"; +} +.mdi-filter-menu::before { + content: "\F0110"; +} +.mdi-filter-menu-outline::before { + content: "\F0111"; +} +.mdi-filter-minus::before { + content: "\FF0B"; +} +.mdi-filter-minus-outline::before { + content: "\FF0C"; +} +.mdi-filter-outline::before { + content: "\F233"; +} +.mdi-filter-plus::before { + content: "\FF0D"; +} +.mdi-filter-plus-outline::before { + content: "\FF0E"; +} +.mdi-filter-remove::before { + content: "\F234"; +} +.mdi-filter-remove-outline::before { + content: "\F235"; +} +.mdi-filter-variant::before { + content: "\F236"; +} +.mdi-filter-variant-minus::before { + content: "\F013D"; +} +.mdi-filter-variant-plus::before { + content: "\F013E"; +} +.mdi-filter-variant-remove::before { + content: "\F0061"; +} +.mdi-finance::before { + content: "\F81E"; +} +.mdi-find-replace::before { + content: "\F6D3"; +} +.mdi-fingerprint::before { + content: "\F237"; +} +.mdi-fingerprint-off::before { + content: "\FECE"; +} +.mdi-fire::before { + content: "\F238"; +} +.mdi-fire-extinguisher::before { + content: "\FF0F"; +} +.mdi-fire-hydrant::before { + content: "\F0162"; +} +.mdi-fire-hydrant-alert::before { + content: "\F0163"; +} +.mdi-fire-hydrant-off::before { + content: "\F0164"; +} +.mdi-fire-truck::before { + content: "\F8AA"; +} +.mdi-firebase::before { + content: "\F966"; +} +.mdi-firefox::before { + content: "\F239"; +} +.mdi-fireplace::before { + content: "\FE11"; +} +.mdi-fireplace-off::before { + content: "\FE12"; +} +.mdi-firework::before { + content: "\FE13"; +} +.mdi-fish::before { + content: "\F23A"; +} +.mdi-fishbowl::before { + content: "\FF10"; +} +.mdi-fishbowl-outline::before { + content: "\FF11"; +} +.mdi-fit-to-page::before { + content: "\FF12"; +} +.mdi-fit-to-page-outline::before { + content: "\FF13"; +} +.mdi-flag::before { + content: "\F23B"; +} +.mdi-flag-checkered::before { + content: "\F23C"; +} +.mdi-flag-minus::before { + content: "\FB75"; +} +.mdi-flag-minus-outline::before { + content: "\F00DD"; +} +.mdi-flag-outline::before { + content: "\F23D"; +} +.mdi-flag-plus::before { + content: "\FB76"; +} +.mdi-flag-plus-outline::before { + content: "\F00DE"; +} +.mdi-flag-remove::before { + content: "\FB77"; +} +.mdi-flag-remove-outline::before { + content: "\F00DF"; +} +.mdi-flag-triangle::before { + content: "\F23F"; +} +.mdi-flag-variant::before { + content: "\F240"; +} +.mdi-flag-variant-outline::before { + content: "\F23E"; +} +.mdi-flare::before { + content: "\FD4E"; +} +.mdi-flash::before { + content: "\F241"; +} +.mdi-flash-alert::before { + content: "\FF14"; +} +.mdi-flash-alert-outline::before { + content: "\FF15"; +} +.mdi-flash-auto::before { + content: "\F242"; +} +.mdi-flash-circle::before { + content: "\F81F"; +} +.mdi-flash-off::before { + content: "\F243"; +} +.mdi-flash-outline::before { + content: "\F6D4"; +} +.mdi-flash-red-eye::before { + content: "\F67A"; +} +.mdi-flashlight::before { + content: "\F244"; +} +.mdi-flashlight-off::before { + content: "\F245"; +} +.mdi-flask::before { + content: "\F093"; +} +.mdi-flask-empty::before { + content: "\F094"; +} +.mdi-flask-empty-minus::before { + content: "\F0265"; +} +.mdi-flask-empty-minus-outline::before { + content: "\F0266"; +} +.mdi-flask-empty-outline::before { + content: "\F095"; +} +.mdi-flask-empty-plus::before { + content: "\F0267"; +} +.mdi-flask-empty-plus-outline::before { + content: "\F0268"; +} +.mdi-flask-empty-remove::before { + content: "\F0269"; +} +.mdi-flask-empty-remove-outline::before { + content: "\F026A"; +} +.mdi-flask-minus::before { + content: "\F026B"; +} +.mdi-flask-minus-outline::before { + content: "\F026C"; +} +.mdi-flask-outline::before { + content: "\F096"; +} +.mdi-flask-plus::before { + content: "\F026D"; +} +.mdi-flask-plus-outline::before { + content: "\F026E"; +} +.mdi-flask-remove::before { + content: "\F026F"; +} +.mdi-flask-remove-outline::before { + content: "\F0270"; +} +.mdi-flask-round-bottom::before { + content: "\F0276"; +} +.mdi-flask-round-bottom-empty::before { + content: "\F0277"; +} +.mdi-flask-round-bottom-empty-outline::before { + content: "\F0278"; +} +.mdi-flask-round-bottom-outline::before { + content: "\F0279"; +} +.mdi-flattr::before { + content: "\F246"; +} +.mdi-fleur-de-lis::before { + content: "\F032E"; +} +.mdi-flickr::before { + content: "\FCE3"; +} +.mdi-flip-horizontal::before { + content: "\F0112"; +} +.mdi-flip-to-back::before { + content: "\F247"; +} +.mdi-flip-to-front::before { + content: "\F248"; +} +.mdi-flip-vertical::before { + content: "\F0113"; +} +.mdi-floor-lamp::before { + content: "\F8DC"; +} +.mdi-floor-lamp-dual::before { + content: "\F0062"; +} +.mdi-floor-lamp-variant::before { + content: "\F0063"; +} +.mdi-floor-plan::before { + content: "\F820"; +} +.mdi-floppy::before { + content: "\F249"; +} +.mdi-floppy-variant::before { + content: "\F9EE"; +} +.mdi-flower::before { + content: "\F24A"; +} +.mdi-flower-outline::before { + content: "\F9EF"; +} +.mdi-flower-poppy::before { + content: "\FCE4"; +} +.mdi-flower-tulip::before { + content: "\F9F0"; +} +.mdi-flower-tulip-outline::before { + content: "\F9F1"; +} +.mdi-focus-auto::before { + content: "\FF6B"; +} +.mdi-focus-field::before { + content: "\FF6C"; +} +.mdi-focus-field-horizontal::before { + content: "\FF6D"; +} +.mdi-focus-field-vertical::before { + content: "\FF6E"; +} +.mdi-folder::before { + content: "\F24B"; +} +.mdi-folder-account::before { + content: "\F24C"; +} +.mdi-folder-account-outline::before { + content: "\FB78"; +} +.mdi-folder-alert::before { + content: "\FDA8"; +} +.mdi-folder-alert-outline::before { + content: "\FDA9"; +} +.mdi-folder-clock::before { + content: "\FAB9"; +} +.mdi-folder-clock-outline::before { + content: "\FABA"; +} +.mdi-folder-download::before { + content: "\F24D"; +} +.mdi-folder-download-outline::before { + content: "\F0114"; +} +.mdi-folder-edit::before { + content: "\F8DD"; +} +.mdi-folder-edit-outline::before { + content: "\FDAA"; +} +.mdi-folder-google-drive::before { + content: "\F24E"; +} +.mdi-folder-heart::before { + content: "\F0115"; +} +.mdi-folder-heart-outline::before { + content: "\F0116"; +} +.mdi-folder-home::before { + content: "\F00E0"; +} +.mdi-folder-home-outline::before { + content: "\F00E1"; +} +.mdi-folder-image::before { + content: "\F24F"; +} +.mdi-folder-information::before { + content: "\F00E2"; +} +.mdi-folder-information-outline::before { + content: "\F00E3"; +} +.mdi-folder-key::before { + content: "\F8AB"; +} +.mdi-folder-key-network::before { + content: "\F8AC"; +} +.mdi-folder-key-network-outline::before { + content: "\FC5C"; +} +.mdi-folder-key-outline::before { + content: "\F0117"; +} +.mdi-folder-lock::before { + content: "\F250"; +} +.mdi-folder-lock-open::before { + content: "\F251"; +} +.mdi-folder-marker::before { + content: "\F0298"; +} +.mdi-folder-marker-outline::before { + content: "\F0299"; +} +.mdi-folder-move::before { + content: "\F252"; +} +.mdi-folder-move-outline::before { + content: "\F0271"; +} +.mdi-folder-multiple::before { + content: "\F253"; +} +.mdi-folder-multiple-image::before { + content: "\F254"; +} +.mdi-folder-multiple-outline::before { + content: "\F255"; +} +.mdi-folder-music::before { + content: "\F0384"; +} +.mdi-folder-music-outline::before { + content: "\F0385"; +} +.mdi-folder-network::before { + content: "\F86F"; +} +.mdi-folder-network-outline::before { + content: "\FC5D"; +} +.mdi-folder-open::before { + content: "\F76F"; +} +.mdi-folder-open-outline::before { + content: "\FDAB"; +} +.mdi-folder-outline::before { + content: "\F256"; +} +.mdi-folder-plus::before { + content: "\F257"; +} +.mdi-folder-plus-outline::before { + content: "\FB79"; +} +.mdi-folder-pound::before { + content: "\FCE5"; +} +.mdi-folder-pound-outline::before { + content: "\FCE6"; +} +.mdi-folder-remove::before { + content: "\F258"; +} +.mdi-folder-remove-outline::before { + content: "\FB7A"; +} +.mdi-folder-search::before { + content: "\F967"; +} +.mdi-folder-search-outline::before { + content: "\F968"; +} +.mdi-folder-settings::before { + content: "\F00A8"; +} +.mdi-folder-settings-outline::before { + content: "\F00A9"; +} +.mdi-folder-settings-variant::before { + content: "\F00AA"; +} +.mdi-folder-settings-variant-outline::before { + content: "\F00AB"; +} +.mdi-folder-star::before { + content: "\F69C"; +} +.mdi-folder-star-outline::before { + content: "\FB7B"; +} +.mdi-folder-swap::before { + content: "\FFD6"; +} +.mdi-folder-swap-outline::before { + content: "\FFD7"; +} +.mdi-folder-sync::before { + content: "\FCE7"; +} +.mdi-folder-sync-outline::before { + content: "\FCE8"; +} +.mdi-folder-table::before { + content: "\F030E"; +} +.mdi-folder-table-outline::before { + content: "\F030F"; +} +.mdi-folder-text::before { + content: "\FC5E"; +} +.mdi-folder-text-outline::before { + content: "\FC5F"; +} +.mdi-folder-upload::before { + content: "\F259"; +} +.mdi-folder-upload-outline::before { + content: "\F0118"; +} +.mdi-folder-zip::before { + content: "\F6EA"; +} +.mdi-folder-zip-outline::before { + content: "\F7B8"; +} +.mdi-font-awesome::before { + content: "\F03A"; +} +.mdi-food::before { + content: "\F25A"; +} +.mdi-food-apple::before { + content: "\F25B"; +} +.mdi-food-apple-outline::before { + content: "\FC60"; +} +.mdi-food-croissant::before { + content: "\F7C7"; +} +.mdi-food-fork-drink::before { + content: "\F5F2"; +} +.mdi-food-off::before { + content: "\F5F3"; +} +.mdi-food-variant::before { + content: "\F25C"; +} +.mdi-foot-print::before { + content: "\FF6F"; +} +.mdi-football::before { + content: "\F25D"; +} +.mdi-football-australian::before { + content: "\F25E"; +} +.mdi-football-helmet::before { + content: "\F25F"; +} +.mdi-forklift::before { + content: "\F7C8"; +} +.mdi-format-align-bottom::before { + content: "\F752"; +} +.mdi-format-align-center::before { + content: "\F260"; +} +.mdi-format-align-justify::before { + content: "\F261"; +} +.mdi-format-align-left::before { + content: "\F262"; +} +.mdi-format-align-middle::before { + content: "\F753"; +} +.mdi-format-align-right::before { + content: "\F263"; +} +.mdi-format-align-top::before { + content: "\F754"; +} +.mdi-format-annotation-minus::before { + content: "\FABB"; +} +.mdi-format-annotation-plus::before { + content: "\F646"; +} +.mdi-format-bold::before { + content: "\F264"; +} +.mdi-format-clear::before { + content: "\F265"; +} +.mdi-format-color-fill::before { + content: "\F266"; +} +.mdi-format-color-highlight::before { + content: "\FE14"; +} +.mdi-format-color-marker-cancel::before { + content: "\F033E"; +} +.mdi-format-color-text::before { + content: "\F69D"; +} +.mdi-format-columns::before { + content: "\F8DE"; +} +.mdi-format-float-center::before { + content: "\F267"; +} +.mdi-format-float-left::before { + content: "\F268"; +} +.mdi-format-float-none::before { + content: "\F269"; +} +.mdi-format-float-right::before { + content: "\F26A"; +} +.mdi-format-font::before { + content: "\F6D5"; +} +.mdi-format-font-size-decrease::before { + content: "\F9F2"; +} +.mdi-format-font-size-increase::before { + content: "\F9F3"; +} +.mdi-format-header-1::before { + content: "\F26B"; +} +.mdi-format-header-2::before { + content: "\F26C"; +} +.mdi-format-header-3::before { + content: "\F26D"; +} +.mdi-format-header-4::before { + content: "\F26E"; +} +.mdi-format-header-5::before { + content: "\F26F"; +} +.mdi-format-header-6::before { + content: "\F270"; +} +.mdi-format-header-decrease::before { + content: "\F271"; +} +.mdi-format-header-equal::before { + content: "\F272"; +} +.mdi-format-header-increase::before { + content: "\F273"; +} +.mdi-format-header-pound::before { + content: "\F274"; +} +.mdi-format-horizontal-align-center::before { + content: "\F61E"; +} +.mdi-format-horizontal-align-left::before { + content: "\F61F"; +} +.mdi-format-horizontal-align-right::before { + content: "\F620"; +} +.mdi-format-indent-decrease::before { + content: "\F275"; +} +.mdi-format-indent-increase::before { + content: "\F276"; +} +.mdi-format-italic::before { + content: "\F277"; +} +.mdi-format-letter-case::before { + content: "\FB19"; +} +.mdi-format-letter-case-lower::before { + content: "\FB1A"; +} +.mdi-format-letter-case-upper::before { + content: "\FB1B"; +} +.mdi-format-letter-ends-with::before { + content: "\FFD8"; +} +.mdi-format-letter-matches::before { + content: "\FFD9"; +} +.mdi-format-letter-starts-with::before { + content: "\FFDA"; +} +.mdi-format-line-spacing::before { + content: "\F278"; +} +.mdi-format-line-style::before { + content: "\F5C8"; +} +.mdi-format-line-weight::before { + content: "\F5C9"; +} +.mdi-format-list-bulleted::before { + content: "\F279"; +} +.mdi-format-list-bulleted-square::before { + content: "\FDAC"; +} +.mdi-format-list-bulleted-triangle::before { + content: "\FECF"; +} +.mdi-format-list-bulleted-type::before { + content: "\F27A"; +} +.mdi-format-list-checkbox::before { + content: "\F969"; +} +.mdi-format-list-checks::before { + content: "\F755"; +} +.mdi-format-list-numbered::before { + content: "\F27B"; +} +.mdi-format-list-numbered-rtl::before { + content: "\FCE9"; +} +.mdi-format-list-text::before { + content: "\F029A"; +} +.mdi-format-overline::before { + content: "\FED0"; +} +.mdi-format-page-break::before { + content: "\F6D6"; +} +.mdi-format-paint::before { + content: "\F27C"; +} +.mdi-format-paragraph::before { + content: "\F27D"; +} +.mdi-format-pilcrow::before { + content: "\F6D7"; +} +.mdi-format-quote-close::before { + content: "\F27E"; +} +.mdi-format-quote-close-outline::before { + content: "\F01D3"; +} +.mdi-format-quote-open::before { + content: "\F756"; +} +.mdi-format-quote-open-outline::before { + content: "\F01D2"; +} +.mdi-format-rotate-90::before { + content: "\F6A9"; +} +.mdi-format-section::before { + content: "\F69E"; +} +.mdi-format-size::before { + content: "\F27F"; +} +.mdi-format-strikethrough::before { + content: "\F280"; +} +.mdi-format-strikethrough-variant::before { + content: "\F281"; +} +.mdi-format-subscript::before { + content: "\F282"; +} +.mdi-format-superscript::before { + content: "\F283"; +} +.mdi-format-text::before { + content: "\F284"; +} +.mdi-format-text-rotation-angle-down::before { + content: "\FFDB"; +} +.mdi-format-text-rotation-angle-up::before { + content: "\FFDC"; +} +.mdi-format-text-rotation-down::before { + content: "\FD4F"; +} +.mdi-format-text-rotation-down-vertical::before { + content: "\FFDD"; +} +.mdi-format-text-rotation-none::before { + content: "\FD50"; +} +.mdi-format-text-rotation-up::before { + content: "\FFDE"; +} +.mdi-format-text-rotation-vertical::before { + content: "\FFDF"; +} +.mdi-format-text-variant::before { + content: "\FE15"; +} +.mdi-format-text-wrapping-clip::before { + content: "\FCEA"; +} +.mdi-format-text-wrapping-overflow::before { + content: "\FCEB"; +} +.mdi-format-text-wrapping-wrap::before { + content: "\FCEC"; +} +.mdi-format-textbox::before { + content: "\FCED"; +} +.mdi-format-textdirection-l-to-r::before { + content: "\F285"; +} +.mdi-format-textdirection-r-to-l::before { + content: "\F286"; +} +.mdi-format-title::before { + content: "\F5F4"; +} +.mdi-format-underline::before { + content: "\F287"; +} +.mdi-format-vertical-align-bottom::before { + content: "\F621"; +} +.mdi-format-vertical-align-center::before { + content: "\F622"; +} +.mdi-format-vertical-align-top::before { + content: "\F623"; +} +.mdi-format-wrap-inline::before { + content: "\F288"; +} +.mdi-format-wrap-square::before { + content: "\F289"; +} +.mdi-format-wrap-tight::before { + content: "\F28A"; +} +.mdi-format-wrap-top-bottom::before { + content: "\F28B"; +} +.mdi-forum::before { + content: "\F28C"; +} +.mdi-forum-outline::before { + content: "\F821"; +} +.mdi-forward::before { + content: "\F28D"; +} +.mdi-forwardburger::before { + content: "\FD51"; +} +.mdi-fountain::before { + content: "\F96A"; +} +.mdi-fountain-pen::before { + content: "\FCEE"; +} +.mdi-fountain-pen-tip::before { + content: "\FCEF"; +} +.mdi-foursquare::before { + content: "\F28E"; +} +.mdi-freebsd::before { + content: "\F8DF"; +} +.mdi-frequently-asked-questions::before { + content: "\FED1"; +} +.mdi-fridge::before { + content: "\F290"; +} +.mdi-fridge-alert::before { + content: "\F01DC"; +} +.mdi-fridge-alert-outline::before { + content: "\F01DD"; +} +.mdi-fridge-bottom::before { + content: "\F292"; +} +.mdi-fridge-off::before { + content: "\F01DA"; +} +.mdi-fridge-off-outline::before { + content: "\F01DB"; +} +.mdi-fridge-outline::before { + content: "\F28F"; +} +.mdi-fridge-top::before { + content: "\F291"; +} +.mdi-fruit-cherries::before { + content: "\F0064"; +} +.mdi-fruit-citrus::before { + content: "\F0065"; +} +.mdi-fruit-grapes::before { + content: "\F0066"; +} +.mdi-fruit-grapes-outline::before { + content: "\F0067"; +} +.mdi-fruit-pineapple::before { + content: "\F0068"; +} +.mdi-fruit-watermelon::before { + content: "\F0069"; +} +.mdi-fuel::before { + content: "\F7C9"; +} +.mdi-fullscreen::before { + content: "\F293"; +} +.mdi-fullscreen-exit::before { + content: "\F294"; +} +.mdi-function::before { + content: "\F295"; +} +.mdi-function-variant::before { + content: "\F870"; +} +.mdi-furigana-horizontal::before { + content: "\F00AC"; +} +.mdi-furigana-vertical::before { + content: "\F00AD"; +} +.mdi-fuse::before { + content: "\FC61"; +} +.mdi-fuse-blade::before { + content: "\FC62"; +} +.mdi-gamepad::before { + content: "\F296"; +} +.mdi-gamepad-circle::before { + content: "\FE16"; +} +.mdi-gamepad-circle-down::before { + content: "\FE17"; +} +.mdi-gamepad-circle-left::before { + content: "\FE18"; +} +.mdi-gamepad-circle-outline::before { + content: "\FE19"; +} +.mdi-gamepad-circle-right::before { + content: "\FE1A"; +} +.mdi-gamepad-circle-up::before { + content: "\FE1B"; +} +.mdi-gamepad-down::before { + content: "\FE1C"; +} +.mdi-gamepad-left::before { + content: "\FE1D"; +} +.mdi-gamepad-right::before { + content: "\FE1E"; +} +.mdi-gamepad-round::before { + content: "\FE1F"; +} +.mdi-gamepad-round-down::before { + content: "\FE7E"; +} +.mdi-gamepad-round-left::before { + content: "\FE7F"; +} +.mdi-gamepad-round-outline::before { + content: "\FE80"; +} +.mdi-gamepad-round-right::before { + content: "\FE81"; +} +.mdi-gamepad-round-up::before { + content: "\FE82"; +} +.mdi-gamepad-square::before { + content: "\FED2"; +} +.mdi-gamepad-square-outline::before { + content: "\FED3"; +} +.mdi-gamepad-up::before { + content: "\FE83"; +} +.mdi-gamepad-variant::before { + content: "\F297"; +} +.mdi-gamepad-variant-outline::before { + content: "\FED4"; +} +.mdi-gamma::before { + content: "\F0119"; +} +.mdi-gantry-crane::before { + content: "\FDAD"; +} +.mdi-garage::before { + content: "\F6D8"; +} +.mdi-garage-alert::before { + content: "\F871"; +} +.mdi-garage-alert-variant::before { + content: "\F0300"; +} +.mdi-garage-open::before { + content: "\F6D9"; +} +.mdi-garage-open-variant::before { + content: "\F02FF"; +} +.mdi-garage-variant::before { + content: "\F02FE"; +} +.mdi-gas-cylinder::before { + content: "\F647"; +} +.mdi-gas-station::before { + content: "\F298"; +} +.mdi-gas-station-outline::before { + content: "\FED5"; +} +.mdi-gate::before { + content: "\F299"; +} +.mdi-gate-and::before { + content: "\F8E0"; +} +.mdi-gate-arrow-right::before { + content: "\F0194"; +} +.mdi-gate-nand::before { + content: "\F8E1"; +} +.mdi-gate-nor::before { + content: "\F8E2"; +} +.mdi-gate-not::before { + content: "\F8E3"; +} +.mdi-gate-open::before { + content: "\F0195"; +} +.mdi-gate-or::before { + content: "\F8E4"; +} +.mdi-gate-xnor::before { + content: "\F8E5"; +} +.mdi-gate-xor::before { + content: "\F8E6"; +} +.mdi-gatsby::before { + content: "\FE84"; +} +.mdi-gauge::before { + content: "\F29A"; +} +.mdi-gauge-empty::before { + content: "\F872"; +} +.mdi-gauge-full::before { + content: "\F873"; +} +.mdi-gauge-low::before { + content: "\F874"; +} +.mdi-gavel::before { + content: "\F29B"; +} +.mdi-gender-female::before { + content: "\F29C"; +} +.mdi-gender-male::before { + content: "\F29D"; +} +.mdi-gender-male-female::before { + content: "\F29E"; +} +.mdi-gender-male-female-variant::before { + content: "\F016A"; +} +.mdi-gender-non-binary::before { + content: "\F016B"; +} +.mdi-gender-transgender::before { + content: "\F29F"; +} +.mdi-gentoo::before { + content: "\F8E7"; +} +.mdi-gesture::before { + content: "\F7CA"; +} +.mdi-gesture-double-tap::before { + content: "\F73B"; +} +.mdi-gesture-pinch::before { + content: "\FABC"; +} +.mdi-gesture-spread::before { + content: "\FABD"; +} +.mdi-gesture-swipe::before { + content: "\FD52"; +} +.mdi-gesture-swipe-down::before { + content: "\F73C"; +} +.mdi-gesture-swipe-horizontal::before { + content: "\FABE"; +} +.mdi-gesture-swipe-left::before { + content: "\F73D"; +} +.mdi-gesture-swipe-right::before { + content: "\F73E"; +} +.mdi-gesture-swipe-up::before { + content: "\F73F"; +} +.mdi-gesture-swipe-vertical::before { + content: "\FABF"; +} +.mdi-gesture-tap::before { + content: "\F740"; +} +.mdi-gesture-tap-box::before { + content: "\F02D4"; +} +.mdi-gesture-tap-button::before { + content: "\F02D3"; +} +.mdi-gesture-tap-hold::before { + content: "\FD53"; +} +.mdi-gesture-two-double-tap::before { + content: "\F741"; +} +.mdi-gesture-two-tap::before { + content: "\F742"; +} +.mdi-ghost::before { + content: "\F2A0"; +} +.mdi-ghost-off::before { + content: "\F9F4"; +} +.mdi-gif::before { + content: "\FD54"; +} +.mdi-gift::before { + content: "\FE85"; +} +.mdi-gift-outline::before { + content: "\F2A1"; +} +.mdi-git::before { + content: "\F2A2"; +} +.mdi-github-box::before { + content: "\F2A3"; +} +.mdi-github-circle::before { + content: "\F2A4"; +} +.mdi-github-face::before { + content: "\F6DA"; +} +.mdi-gitlab::before { + content: "\FB7C"; +} +.mdi-glass-cocktail::before { + content: "\F356"; +} +.mdi-glass-flute::before { + content: "\F2A5"; +} +.mdi-glass-mug::before { + content: "\F2A6"; +} +.mdi-glass-mug-variant::before { + content: "\F0141"; +} +.mdi-glass-pint-outline::before { + content: "\F0338"; +} +.mdi-glass-stange::before { + content: "\F2A7"; +} +.mdi-glass-tulip::before { + content: "\F2A8"; +} +.mdi-glass-wine::before { + content: "\F875"; +} +.mdi-glassdoor::before { + content: "\F2A9"; +} +.mdi-glasses::before { + content: "\F2AA"; +} +.mdi-globe-light::before { + content: "\F0302"; +} +.mdi-globe-model::before { + content: "\F8E8"; +} +.mdi-gmail::before { + content: "\F2AB"; +} +.mdi-gnome::before { + content: "\F2AC"; +} +.mdi-go-kart::before { + content: "\FD55"; +} +.mdi-go-kart-track::before { + content: "\FD56"; +} +.mdi-gog::before { + content: "\FB7D"; +} +.mdi-gold::before { + content: "\F027A"; +} +.mdi-golf::before { + content: "\F822"; +} +.mdi-golf-cart::before { + content: "\F01CF"; +} +.mdi-golf-tee::before { + content: "\F00AE"; +} +.mdi-gondola::before { + content: "\F685"; +} +.mdi-goodreads::before { + content: "\FD57"; +} +.mdi-google::before { + content: "\F2AD"; +} +.mdi-google-adwords::before { + content: "\FC63"; +} +.mdi-google-analytics::before { + content: "\F7CB"; +} +.mdi-google-assistant::before { + content: "\F7CC"; +} +.mdi-google-cardboard::before { + content: "\F2AE"; +} +.mdi-google-chrome::before { + content: "\F2AF"; +} +.mdi-google-circles::before { + content: "\F2B0"; +} +.mdi-google-circles-communities::before { + content: "\F2B1"; +} +.mdi-google-circles-extended::before { + content: "\F2B2"; +} +.mdi-google-circles-group::before { + content: "\F2B3"; +} +.mdi-google-classroom::before { + content: "\F2C0"; +} +.mdi-google-cloud::before { + content: "\F0221"; +} +.mdi-google-controller::before { + content: "\F2B4"; +} +.mdi-google-controller-off::before { + content: "\F2B5"; +} +.mdi-google-downasaur::before { + content: "\F038D"; +} +.mdi-google-drive::before { + content: "\F2B6"; +} +.mdi-google-earth::before { + content: "\F2B7"; +} +.mdi-google-fit::before { + content: "\F96B"; +} +.mdi-google-glass::before { + content: "\F2B8"; +} +.mdi-google-hangouts::before { + content: "\F2C9"; +} +.mdi-google-home::before { + content: "\F823"; +} +.mdi-google-keep::before { + content: "\F6DB"; +} +.mdi-google-lens::before { + content: "\F9F5"; +} +.mdi-google-maps::before { + content: "\F5F5"; +} +.mdi-google-my-business::before { + content: "\F006A"; +} +.mdi-google-nearby::before { + content: "\F2B9"; +} +.mdi-google-pages::before { + content: "\F2BA"; +} +.mdi-google-photos::before { + content: "\F6DC"; +} +.mdi-google-physical-web::before { + content: "\F2BB"; +} +.mdi-google-play::before { + content: "\F2BC"; +} +.mdi-google-plus::before { + content: "\F2BD"; +} +.mdi-google-plus-box::before { + content: "\F2BE"; +} +.mdi-google-podcast::before { + content: "\FED6"; +} +.mdi-google-spreadsheet::before { + content: "\F9F6"; +} +.mdi-google-street-view::before { + content: "\FC64"; +} +.mdi-google-translate::before { + content: "\F2BF"; +} +.mdi-gradient::before { + content: "\F69F"; +} +.mdi-grain::before { + content: "\FD58"; +} +.mdi-graph::before { + content: "\F006B"; +} +.mdi-graph-outline::before { + content: "\F006C"; +} +.mdi-graphql::before { + content: "\F876"; +} +.mdi-grave-stone::before { + content: "\FB7E"; +} +.mdi-grease-pencil::before { + content: "\F648"; +} +.mdi-greater-than::before { + content: "\F96C"; +} +.mdi-greater-than-or-equal::before { + content: "\F96D"; +} +.mdi-grid::before { + content: "\F2C1"; +} +.mdi-grid-large::before { + content: "\F757"; +} +.mdi-grid-off::before { + content: "\F2C2"; +} +.mdi-grill::before { + content: "\FE86"; +} +.mdi-grill-outline::before { + content: "\F01B5"; +} +.mdi-group::before { + content: "\F2C3"; +} +.mdi-guitar-acoustic::before { + content: "\F770"; +} +.mdi-guitar-electric::before { + content: "\F2C4"; +} +.mdi-guitar-pick::before { + content: "\F2C5"; +} +.mdi-guitar-pick-outline::before { + content: "\F2C6"; +} +.mdi-guy-fawkes-mask::before { + content: "\F824"; +} +.mdi-hackernews::before { + content: "\F624"; +} +.mdi-hail::before { + content: "\FAC0"; +} +.mdi-hair-dryer::before { + content: "\F011A"; +} +.mdi-hair-dryer-outline::before { + content: "\F011B"; +} +.mdi-halloween::before { + content: "\FB7F"; +} +.mdi-hamburger::before { + content: "\F684"; +} +.mdi-hammer::before { + content: "\F8E9"; +} +.mdi-hammer-screwdriver::before { + content: "\F034D"; +} +.mdi-hammer-wrench::before { + content: "\F034E"; +} +.mdi-hand::before { + content: "\FA4E"; +} +.mdi-hand-heart::before { + content: "\F011C"; +} +.mdi-hand-left::before { + content: "\FE87"; +} +.mdi-hand-okay::before { + content: "\FA4F"; +} +.mdi-hand-peace::before { + content: "\FA50"; +} +.mdi-hand-peace-variant::before { + content: "\FA51"; +} +.mdi-hand-pointing-down::before { + content: "\FA52"; +} +.mdi-hand-pointing-left::before { + content: "\FA53"; +} +.mdi-hand-pointing-right::before { + content: "\F2C7"; +} +.mdi-hand-pointing-up::before { + content: "\FA54"; +} +.mdi-hand-right::before { + content: "\FE88"; +} +.mdi-hand-saw::before { + content: "\FE89"; +} +.mdi-handball::before { + content: "\FF70"; +} +.mdi-handcuffs::before { + content: "\F0169"; +} +.mdi-handshake::before { + content: "\F0243"; +} +.mdi-hanger::before { + content: "\F2C8"; +} +.mdi-hard-hat::before { + content: "\F96E"; +} +.mdi-harddisk::before { + content: "\F2CA"; +} +.mdi-harddisk-plus::before { + content: "\F006D"; +} +.mdi-harddisk-remove::before { + content: "\F006E"; +} +.mdi-hat-fedora::before { + content: "\FB80"; +} +.mdi-hazard-lights::before { + content: "\FC65"; +} +.mdi-hdr::before { + content: "\FD59"; +} +.mdi-hdr-off::before { + content: "\FD5A"; +} +.mdi-head::before { + content: "\F0389"; +} +.mdi-head-alert::before { + content: "\F0363"; +} +.mdi-head-alert-outline::before { + content: "\F0364"; +} +.mdi-head-check::before { + content: "\F0365"; +} +.mdi-head-check-outline::before { + content: "\F0366"; +} +.mdi-head-cog::before { + content: "\F0367"; +} +.mdi-head-cog-outline::before { + content: "\F0368"; +} +.mdi-head-dots-horizontal::before { + content: "\F0369"; +} +.mdi-head-dots-horizontal-outline::before { + content: "\F036A"; +} +.mdi-head-flash::before { + content: "\F036B"; +} +.mdi-head-flash-outline::before { + content: "\F036C"; +} +.mdi-head-heart::before { + content: "\F036D"; +} +.mdi-head-heart-outline::before { + content: "\F036E"; +} +.mdi-head-lightbulb::before { + content: "\F036F"; +} +.mdi-head-lightbulb-outline::before { + content: "\F0370"; +} +.mdi-head-minus::before { + content: "\F0371"; +} +.mdi-head-minus-outline::before { + content: "\F0372"; +} +.mdi-head-outline::before { + content: "\F038A"; +} +.mdi-head-plus::before { + content: "\F0373"; +} +.mdi-head-plus-outline::before { + content: "\F0374"; +} +.mdi-head-question::before { + content: "\F0375"; +} +.mdi-head-question-outline::before { + content: "\F0376"; +} +.mdi-head-remove::before { + content: "\F0377"; +} +.mdi-head-remove-outline::before { + content: "\F0378"; +} +.mdi-head-snowflake::before { + content: "\F0379"; +} +.mdi-head-snowflake-outline::before { + content: "\F037A"; +} +.mdi-head-sync::before { + content: "\F037B"; +} +.mdi-head-sync-outline::before { + content: "\F037C"; +} +.mdi-headphones::before { + content: "\F2CB"; +} +.mdi-headphones-bluetooth::before { + content: "\F96F"; +} +.mdi-headphones-box::before { + content: "\F2CC"; +} +.mdi-headphones-off::before { + content: "\F7CD"; +} +.mdi-headphones-settings::before { + content: "\F2CD"; +} +.mdi-headset::before { + content: "\F2CE"; +} +.mdi-headset-dock::before { + content: "\F2CF"; +} +.mdi-headset-off::before { + content: "\F2D0"; +} +.mdi-heart::before { + content: "\F2D1"; +} +.mdi-heart-box::before { + content: "\F2D2"; +} +.mdi-heart-box-outline::before { + content: "\F2D3"; +} +.mdi-heart-broken::before { + content: "\F2D4"; +} +.mdi-heart-broken-outline::before { + content: "\FCF0"; +} +.mdi-heart-circle::before { + content: "\F970"; +} +.mdi-heart-circle-outline::before { + content: "\F971"; +} +.mdi-heart-flash::before { + content: "\FF16"; +} +.mdi-heart-half::before { + content: "\F6DE"; +} +.mdi-heart-half-full::before { + content: "\F6DD"; +} +.mdi-heart-half-outline::before { + content: "\F6DF"; +} +.mdi-heart-multiple::before { + content: "\FA55"; +} +.mdi-heart-multiple-outline::before { + content: "\FA56"; +} +.mdi-heart-off::before { + content: "\F758"; +} +.mdi-heart-outline::before { + content: "\F2D5"; +} +.mdi-heart-pulse::before { + content: "\F5F6"; +} +.mdi-helicopter::before { + content: "\FAC1"; +} +.mdi-help::before { + content: "\F2D6"; +} +.mdi-help-box::before { + content: "\F78A"; +} +.mdi-help-circle::before { + content: "\F2D7"; +} +.mdi-help-circle-outline::before { + content: "\F625"; +} +.mdi-help-network::before { + content: "\F6F4"; +} +.mdi-help-network-outline::before { + content: "\FC66"; +} +.mdi-help-rhombus::before { + content: "\FB81"; +} +.mdi-help-rhombus-outline::before { + content: "\FB82"; +} +.mdi-hexadecimal::before { + content: "\F02D2"; +} +.mdi-hexagon::before { + content: "\F2D8"; +} +.mdi-hexagon-multiple::before { + content: "\F6E0"; +} +.mdi-hexagon-multiple-outline::before { + content: "\F011D"; +} +.mdi-hexagon-outline::before { + content: "\F2D9"; +} +.mdi-hexagon-slice-1::before { + content: "\FAC2"; +} +.mdi-hexagon-slice-2::before { + content: "\FAC3"; +} +.mdi-hexagon-slice-3::before { + content: "\FAC4"; +} +.mdi-hexagon-slice-4::before { + content: "\FAC5"; +} +.mdi-hexagon-slice-5::before { + content: "\FAC6"; +} +.mdi-hexagon-slice-6::before { + content: "\FAC7"; +} +.mdi-hexagram::before { + content: "\FAC8"; +} +.mdi-hexagram-outline::before { + content: "\FAC9"; +} +.mdi-high-definition::before { + content: "\F7CE"; +} +.mdi-high-definition-box::before { + content: "\F877"; +} +.mdi-highway::before { + content: "\F5F7"; +} +.mdi-hiking::before { + content: "\FD5B"; +} +.mdi-hinduism::before { + content: "\F972"; +} +.mdi-history::before { + content: "\F2DA"; +} +.mdi-hockey-puck::before { + content: "\F878"; +} +.mdi-hockey-sticks::before { + content: "\F879"; +} +.mdi-hololens::before { + content: "\F2DB"; +} +.mdi-home::before { + content: "\F2DC"; +} +.mdi-home-account::before { + content: "\F825"; +} +.mdi-home-alert::before { + content: "\F87A"; +} +.mdi-home-analytics::before { + content: "\FED7"; +} +.mdi-home-assistant::before { + content: "\F7CF"; +} +.mdi-home-automation::before { + content: "\F7D0"; +} +.mdi-home-circle::before { + content: "\F7D1"; +} +.mdi-home-circle-outline::before { + content: "\F006F"; +} +.mdi-home-city::before { + content: "\FCF1"; +} +.mdi-home-city-outline::before { + content: "\FCF2"; +} +.mdi-home-currency-usd::before { + content: "\F8AE"; +} +.mdi-home-edit::before { + content: "\F0184"; +} +.mdi-home-edit-outline::before { + content: "\F0185"; +} +.mdi-home-export-outline::before { + content: "\FFB8"; +} +.mdi-home-flood::before { + content: "\FF17"; +} +.mdi-home-floor-0::before { + content: "\FDAE"; +} +.mdi-home-floor-1::before { + content: "\FD5C"; +} +.mdi-home-floor-2::before { + content: "\FD5D"; +} +.mdi-home-floor-3::before { + content: "\FD5E"; +} +.mdi-home-floor-a::before { + content: "\FD5F"; +} +.mdi-home-floor-b::before { + content: "\FD60"; +} +.mdi-home-floor-g::before { + content: "\FD61"; +} +.mdi-home-floor-l::before { + content: "\FD62"; +} +.mdi-home-floor-negative-1::before { + content: "\FDAF"; +} +.mdi-home-group::before { + content: "\FDB0"; +} +.mdi-home-heart::before { + content: "\F826"; +} +.mdi-home-import-outline::before { + content: "\FFB9"; +} +.mdi-home-lightbulb::before { + content: "\F027C"; +} +.mdi-home-lightbulb-outline::before { + content: "\F027D"; +} +.mdi-home-lock::before { + content: "\F8EA"; +} +.mdi-home-lock-open::before { + content: "\F8EB"; +} +.mdi-home-map-marker::before { + content: "\F5F8"; +} +.mdi-home-minus::before { + content: "\F973"; +} +.mdi-home-modern::before { + content: "\F2DD"; +} +.mdi-home-outline::before { + content: "\F6A0"; +} +.mdi-home-plus::before { + content: "\F974"; +} +.mdi-home-remove::before { + content: "\F0272"; +} +.mdi-home-roof::before { + content: "\F0156"; +} +.mdi-home-thermometer::before { + content: "\FF71"; +} +.mdi-home-thermometer-outline::before { + content: "\FF72"; +} +.mdi-home-variant::before { + content: "\F2DE"; +} +.mdi-home-variant-outline::before { + content: "\FB83"; +} +.mdi-hook::before { + content: "\F6E1"; +} +.mdi-hook-off::before { + content: "\F6E2"; +} +.mdi-hops::before { + content: "\F2DF"; +} +.mdi-horizontal-rotate-clockwise::before { + content: "\F011E"; +} +.mdi-horizontal-rotate-counterclockwise::before { + content: "\F011F"; +} +.mdi-horseshoe::before { + content: "\FA57"; +} +.mdi-hospital::before { + content: "\F0017"; +} +.mdi-hospital-box::before { + content: "\F2E0"; +} +.mdi-hospital-box-outline::before { + content: "\F0018"; +} +.mdi-hospital-building::before { + content: "\F2E1"; +} +.mdi-hospital-marker::before { + content: "\F2E2"; +} +.mdi-hot-tub::before { + content: "\F827"; +} +.mdi-hotel::before { + content: "\F2E3"; +} +.mdi-houzz::before { + content: "\F2E4"; +} +.mdi-houzz-box::before { + content: "\F2E5"; +} +.mdi-hubspot::before { + content: "\FCF3"; +} +.mdi-hulu::before { + content: "\F828"; +} +.mdi-human::before { + content: "\F2E6"; +} +.mdi-human-child::before { + content: "\F2E7"; +} +.mdi-human-female::before { + content: "\F649"; +} +.mdi-human-female-boy::before { + content: "\FA58"; +} +.mdi-human-female-female::before { + content: "\FA59"; +} +.mdi-human-female-girl::before { + content: "\FA5A"; +} +.mdi-human-greeting::before { + content: "\F64A"; +} +.mdi-human-handsdown::before { + content: "\F64B"; +} +.mdi-human-handsup::before { + content: "\F64C"; +} +.mdi-human-male::before { + content: "\F64D"; +} +.mdi-human-male-boy::before { + content: "\FA5B"; +} +.mdi-human-male-female::before { + content: "\F2E8"; +} +.mdi-human-male-girl::before { + content: "\FA5C"; +} +.mdi-human-male-height::before { + content: "\FF18"; +} +.mdi-human-male-height-variant::before { + content: "\FF19"; +} +.mdi-human-male-male::before { + content: "\FA5D"; +} +.mdi-human-pregnant::before { + content: "\F5CF"; +} +.mdi-humble-bundle::before { + content: "\F743"; +} +.mdi-hvac::before { + content: "\F037D"; +} +.mdi-hydraulic-oil-level::before { + content: "\F034F"; +} +.mdi-hydraulic-oil-temperature::before { + content: "\F0350"; +} +.mdi-hydro-power::before { + content: "\F0310"; +} +.mdi-ice-cream::before { + content: "\F829"; +} +.mdi-ice-pop::before { + content: "\FF1A"; +} +.mdi-id-card::before { + content: "\FFE0"; +} +.mdi-identifier::before { + content: "\FF1B"; +} +.mdi-ideogram-cjk::before { + content: "\F035C"; +} +.mdi-ideogram-cjk-variant::before { + content: "\F035D"; +} +.mdi-iframe::before { + content: "\FC67"; +} +.mdi-iframe-array::before { + content: "\F0120"; +} +.mdi-iframe-array-outline::before { + content: "\F0121"; +} +.mdi-iframe-braces::before { + content: "\F0122"; +} +.mdi-iframe-braces-outline::before { + content: "\F0123"; +} +.mdi-iframe-outline::before { + content: "\FC68"; +} +.mdi-iframe-parentheses::before { + content: "\F0124"; +} +.mdi-iframe-parentheses-outline::before { + content: "\F0125"; +} +.mdi-iframe-variable::before { + content: "\F0126"; +} +.mdi-iframe-variable-outline::before { + content: "\F0127"; +} +.mdi-image::before { + content: "\F2E9"; +} +.mdi-image-album::before { + content: "\F2EA"; +} +.mdi-image-area::before { + content: "\F2EB"; +} +.mdi-image-area-close::before { + content: "\F2EC"; +} +.mdi-image-auto-adjust::before { + content: "\FFE1"; +} +.mdi-image-broken::before { + content: "\F2ED"; +} +.mdi-image-broken-variant::before { + content: "\F2EE"; +} +.mdi-image-edit::before { + content: "\F020E"; +} +.mdi-image-edit-outline::before { + content: "\F020F"; +} +.mdi-image-filter::before { + content: "\F2EF"; +} +.mdi-image-filter-black-white::before { + content: "\F2F0"; +} +.mdi-image-filter-center-focus::before { + content: "\F2F1"; +} +.mdi-image-filter-center-focus-strong::before { + content: "\FF1C"; +} +.mdi-image-filter-center-focus-strong-outline::before { + content: "\FF1D"; +} +.mdi-image-filter-center-focus-weak::before { + content: "\F2F2"; +} +.mdi-image-filter-drama::before { + content: "\F2F3"; +} +.mdi-image-filter-frames::before { + content: "\F2F4"; +} +.mdi-image-filter-hdr::before { + content: "\F2F5"; +} +.mdi-image-filter-none::before { + content: "\F2F6"; +} +.mdi-image-filter-tilt-shift::before { + content: "\F2F7"; +} +.mdi-image-filter-vintage::before { + content: "\F2F8"; +} +.mdi-image-frame::before { + content: "\FE8A"; +} +.mdi-image-move::before { + content: "\F9F7"; +} +.mdi-image-multiple::before { + content: "\F2F9"; +} +.mdi-image-off::before { + content: "\F82A"; +} +.mdi-image-off-outline::before { + content: "\F01FC"; +} +.mdi-image-outline::before { + content: "\F975"; +} +.mdi-image-plus::before { + content: "\F87B"; +} +.mdi-image-search::before { + content: "\F976"; +} +.mdi-image-search-outline::before { + content: "\F977"; +} +.mdi-image-size-select-actual::before { + content: "\FC69"; +} +.mdi-image-size-select-large::before { + content: "\FC6A"; +} +.mdi-image-size-select-small::before { + content: "\FC6B"; +} +.mdi-import::before { + content: "\F2FA"; +} +.mdi-inbox::before { + content: "\F686"; +} +.mdi-inbox-arrow-down::before { + content: "\F2FB"; +} +.mdi-inbox-arrow-down-outline::before { + content: "\F029B"; +} +.mdi-inbox-arrow-up::before { + content: "\F3D1"; +} +.mdi-inbox-arrow-up-outline::before { + content: "\F029C"; +} +.mdi-inbox-full::before { + content: "\F029D"; +} +.mdi-inbox-full-outline::before { + content: "\F029E"; +} +.mdi-inbox-multiple::before { + content: "\F8AF"; +} +.mdi-inbox-multiple-outline::before { + content: "\FB84"; +} +.mdi-inbox-outline::before { + content: "\F029F"; +} +.mdi-incognito::before { + content: "\F5F9"; +} +.mdi-infinity::before { + content: "\F6E3"; +} +.mdi-information::before { + content: "\F2FC"; +} +.mdi-information-outline::before { + content: "\F2FD"; +} +.mdi-information-variant::before { + content: "\F64E"; +} +.mdi-instagram::before { + content: "\F2FE"; +} +.mdi-instapaper::before { + content: "\F2FF"; +} +.mdi-instrument-triangle::before { + content: "\F0070"; +} +.mdi-internet-explorer::before { + content: "\F300"; +} +.mdi-invert-colors::before { + content: "\F301"; +} +.mdi-invert-colors-off::before { + content: "\FE8B"; +} +.mdi-iobroker::before { + content: "\F0313"; +} +.mdi-ip::before { + content: "\FA5E"; +} +.mdi-ip-network::before { + content: "\FA5F"; +} +.mdi-ip-network-outline::before { + content: "\FC6C"; +} +.mdi-ipod::before { + content: "\FC6D"; +} +.mdi-islam::before { + content: "\F978"; +} +.mdi-island::before { + content: "\F0071"; +} +.mdi-itunes::before { + content: "\F676"; +} +.mdi-iv-bag::before { + content: "\F00E4"; +} +.mdi-jabber::before { + content: "\FDB1"; +} +.mdi-jeepney::before { + content: "\F302"; +} +.mdi-jellyfish::before { + content: "\FF1E"; +} +.mdi-jellyfish-outline::before { + content: "\FF1F"; +} +.mdi-jira::before { + content: "\F303"; +} +.mdi-jquery::before { + content: "\F87C"; +} +.mdi-jsfiddle::before { + content: "\F304"; +} +.mdi-json::before { + content: "\F626"; +} +.mdi-judaism::before { + content: "\F979"; +} +.mdi-jump-rope::before { + content: "\F032A"; +} +.mdi-kabaddi::before { + content: "\FD63"; +} +.mdi-karate::before { + content: "\F82B"; +} +.mdi-keg::before { + content: "\F305"; +} +.mdi-kettle::before { + content: "\F5FA"; +} +.mdi-kettle-alert::before { + content: "\F0342"; +} +.mdi-kettle-alert-outline::before { + content: "\F0343"; +} +.mdi-kettle-off::before { + content: "\F0346"; +} +.mdi-kettle-off-outline::before { + content: "\F0347"; +} +.mdi-kettle-outline::before { + content: "\FF73"; +} +.mdi-kettle-steam::before { + content: "\F0344"; +} +.mdi-kettle-steam-outline::before { + content: "\F0345"; +} +.mdi-kettlebell::before { + content: "\F032B"; +} +.mdi-key::before { + content: "\F306"; +} +.mdi-key-arrow-right::before { + content: "\F033D"; +} +.mdi-key-change::before { + content: "\F307"; +} +.mdi-key-link::before { + content: "\F01CA"; +} +.mdi-key-minus::before { + content: "\F308"; +} +.mdi-key-outline::before { + content: "\FDB2"; +} +.mdi-key-plus::before { + content: "\F309"; +} +.mdi-key-remove::before { + content: "\F30A"; +} +.mdi-key-star::before { + content: "\F01C9"; +} +.mdi-key-variant::before { + content: "\F30B"; +} +.mdi-key-wireless::before { + content: "\FFE2"; +} +.mdi-keyboard::before { + content: "\F30C"; +} +.mdi-keyboard-backspace::before { + content: "\F30D"; +} +.mdi-keyboard-caps::before { + content: "\F30E"; +} +.mdi-keyboard-close::before { + content: "\F30F"; +} +.mdi-keyboard-esc::before { + content: "\F02E2"; +} +.mdi-keyboard-f1::before { + content: "\F02D6"; +} +.mdi-keyboard-f10::before { + content: "\F02DF"; +} +.mdi-keyboard-f11::before { + content: "\F02E0"; +} +.mdi-keyboard-f12::before { + content: "\F02E1"; +} +.mdi-keyboard-f2::before { + content: "\F02D7"; +} +.mdi-keyboard-f3::before { + content: "\F02D8"; +} +.mdi-keyboard-f4::before { + content: "\F02D9"; +} +.mdi-keyboard-f5::before { + content: "\F02DA"; +} +.mdi-keyboard-f6::before { + content: "\F02DB"; +} +.mdi-keyboard-f7::before { + content: "\F02DC"; +} +.mdi-keyboard-f8::before { + content: "\F02DD"; +} +.mdi-keyboard-f9::before { + content: "\F02DE"; +} +.mdi-keyboard-off::before { + content: "\F310"; +} +.mdi-keyboard-off-outline::before { + content: "\FE8C"; +} +.mdi-keyboard-outline::before { + content: "\F97A"; +} +.mdi-keyboard-return::before { + content: "\F311"; +} +.mdi-keyboard-settings::before { + content: "\F9F8"; +} +.mdi-keyboard-settings-outline::before { + content: "\F9F9"; +} +.mdi-keyboard-space::before { + content: "\F0072"; +} +.mdi-keyboard-tab::before { + content: "\F312"; +} +.mdi-keyboard-variant::before { + content: "\F313"; +} +.mdi-khanda::before { + content: "\F0128"; +} +.mdi-kickstarter::before { + content: "\F744"; +} +.mdi-klingon::before { + content: "\F0386"; +} +.mdi-knife::before { + content: "\F9FA"; +} +.mdi-knife-military::before { + content: "\F9FB"; +} +.mdi-kodi::before { + content: "\F314"; +} +.mdi-kotlin::before { + content: "\F0244"; +} +.mdi-kubernetes::before { + content: "\F0129"; +} +.mdi-label::before { + content: "\F315"; +} +.mdi-label-multiple::before { + content: "\F03A0"; +} +.mdi-label-multiple-outline::before { + content: "\F03A1"; +} +.mdi-label-off::before { + content: "\FACA"; +} +.mdi-label-off-outline::before { + content: "\FACB"; +} +.mdi-label-outline::before { + content: "\F316"; +} +.mdi-label-percent::before { + content: "\F0315"; +} +.mdi-label-percent-outline::before { + content: "\F0316"; +} +.mdi-label-variant::before { + content: "\FACC"; +} +.mdi-label-variant-outline::before { + content: "\FACD"; +} +.mdi-ladybug::before { + content: "\F82C"; +} +.mdi-lambda::before { + content: "\F627"; +} +.mdi-lamp::before { + content: "\F6B4"; +} +.mdi-lan::before { + content: "\F317"; +} +.mdi-lan-check::before { + content: "\F02D5"; +} +.mdi-lan-connect::before { + content: "\F318"; +} +.mdi-lan-disconnect::before { + content: "\F319"; +} +.mdi-lan-pending::before { + content: "\F31A"; +} +.mdi-language-c::before { + content: "\F671"; +} +.mdi-language-cpp::before { + content: "\F672"; +} +.mdi-language-csharp::before { + content: "\F31B"; +} +.mdi-language-css3::before { + content: "\F31C"; +} +.mdi-language-fortran::before { + content: "\F0245"; +} +.mdi-language-go::before { + content: "\F7D2"; +} +.mdi-language-haskell::before { + content: "\FC6E"; +} +.mdi-language-html5::before { + content: "\F31D"; +} +.mdi-language-java::before { + content: "\FB1C"; +} +.mdi-language-javascript::before { + content: "\F31E"; +} +.mdi-language-lua::before { + content: "\F8B0"; +} +.mdi-language-php::before { + content: "\F31F"; +} +.mdi-language-python::before { + content: "\F320"; +} +.mdi-language-python-text::before { + content: "\F321"; +} +.mdi-language-r::before { + content: "\F7D3"; +} +.mdi-language-ruby-on-rails::before { + content: "\FACE"; +} +.mdi-language-swift::before { + content: "\F6E4"; +} +.mdi-language-typescript::before { + content: "\F6E5"; +} +.mdi-laptop::before { + content: "\F322"; +} +.mdi-laptop-chromebook::before { + content: "\F323"; +} +.mdi-laptop-mac::before { + content: "\F324"; +} +.mdi-laptop-off::before { + content: "\F6E6"; +} +.mdi-laptop-windows::before { + content: "\F325"; +} +.mdi-laravel::before { + content: "\FACF"; +} +.mdi-lasso::before { + content: "\FF20"; +} +.mdi-lastfm::before { + content: "\F326"; +} +.mdi-lastpass::before { + content: "\F446"; +} +.mdi-latitude::before { + content: "\FF74"; +} +.mdi-launch::before { + content: "\F327"; +} +.mdi-lava-lamp::before { + content: "\F7D4"; +} +.mdi-layers::before { + content: "\F328"; +} +.mdi-layers-minus::before { + content: "\FE8D"; +} +.mdi-layers-off::before { + content: "\F329"; +} +.mdi-layers-off-outline::before { + content: "\F9FC"; +} +.mdi-layers-outline::before { + content: "\F9FD"; +} +.mdi-layers-plus::before { + content: "\FE30"; +} +.mdi-layers-remove::before { + content: "\FE31"; +} +.mdi-layers-search::before { + content: "\F0231"; +} +.mdi-layers-search-outline::before { + content: "\F0232"; +} +.mdi-layers-triple::before { + content: "\FF75"; +} +.mdi-layers-triple-outline::before { + content: "\FF76"; +} +.mdi-lead-pencil::before { + content: "\F64F"; +} +.mdi-leaf::before { + content: "\F32A"; +} +.mdi-leaf-maple::before { + content: "\FC6F"; +} +.mdi-leaf-maple-off::before { + content: "\F0305"; +} +.mdi-leaf-off::before { + content: "\F0304"; +} +.mdi-leak::before { + content: "\FDB3"; +} +.mdi-leak-off::before { + content: "\FDB4"; +} +.mdi-led-off::before { + content: "\F32B"; +} +.mdi-led-on::before { + content: "\F32C"; +} +.mdi-led-outline::before { + content: "\F32D"; +} +.mdi-led-strip::before { + content: "\F7D5"; +} +.mdi-led-strip-variant::before { + content: "\F0073"; +} +.mdi-led-variant-off::before { + content: "\F32E"; +} +.mdi-led-variant-on::before { + content: "\F32F"; +} +.mdi-led-variant-outline::before { + content: "\F330"; +} +.mdi-leek::before { + content: "\F01A8"; +} +.mdi-less-than::before { + content: "\F97B"; +} +.mdi-less-than-or-equal::before { + content: "\F97C"; +} +.mdi-library::before { + content: "\F331"; +} +.mdi-library-books::before { + content: "\F332"; +} +.mdi-library-movie::before { + content: "\FCF4"; +} +.mdi-library-music::before { + content: "\F333"; +} +.mdi-library-music-outline::before { + content: "\FF21"; +} +.mdi-library-shelves::before { + content: "\FB85"; +} +.mdi-library-video::before { + content: "\FCF5"; +} +.mdi-license::before { + content: "\FFE3"; +} +.mdi-lifebuoy::before { + content: "\F87D"; +} +.mdi-light-switch::before { + content: "\F97D"; +} +.mdi-lightbulb::before { + content: "\F335"; +} +.mdi-lightbulb-cfl::before { + content: "\F0233"; +} +.mdi-lightbulb-cfl-off::before { + content: "\F0234"; +} +.mdi-lightbulb-cfl-spiral::before { + content: "\F02A0"; +} +.mdi-lightbulb-cfl-spiral-off::before { + content: "\F02EE"; +} +.mdi-lightbulb-group::before { + content: "\F027E"; +} +.mdi-lightbulb-group-off::before { + content: "\F02F8"; +} +.mdi-lightbulb-group-off-outline::before { + content: "\F02F9"; +} +.mdi-lightbulb-group-outline::before { + content: "\F027F"; +} +.mdi-lightbulb-multiple::before { + content: "\F0280"; +} +.mdi-lightbulb-multiple-off::before { + content: "\F02FA"; +} +.mdi-lightbulb-multiple-off-outline::before { + content: "\F02FB"; +} +.mdi-lightbulb-multiple-outline::before { + content: "\F0281"; +} +.mdi-lightbulb-off::before { + content: "\FE32"; +} +.mdi-lightbulb-off-outline::before { + content: "\FE33"; +} +.mdi-lightbulb-on::before { + content: "\F6E7"; +} +.mdi-lightbulb-on-outline::before { + content: "\F6E8"; +} +.mdi-lightbulb-outline::before { + content: "\F336"; +} +.mdi-lighthouse::before { + content: "\F9FE"; +} +.mdi-lighthouse-on::before { + content: "\F9FF"; +} +.mdi-link::before { + content: "\F337"; +} +.mdi-link-box::before { + content: "\FCF6"; +} +.mdi-link-box-outline::before { + content: "\FCF7"; +} +.mdi-link-box-variant::before { + content: "\FCF8"; +} +.mdi-link-box-variant-outline::before { + content: "\FCF9"; +} +.mdi-link-lock::before { + content: "\F00E5"; +} +.mdi-link-off::before { + content: "\F338"; +} +.mdi-link-plus::before { + content: "\FC70"; +} +.mdi-link-variant::before { + content: "\F339"; +} +.mdi-link-variant-minus::before { + content: "\F012A"; +} +.mdi-link-variant-off::before { + content: "\F33A"; +} +.mdi-link-variant-plus::before { + content: "\F012B"; +} +.mdi-link-variant-remove::before { + content: "\F012C"; +} +.mdi-linkedin::before { + content: "\F33B"; +} +.mdi-linkedin-box::before { + content: "\F33C"; +} +.mdi-linux::before { + content: "\F33D"; +} +.mdi-linux-mint::before { + content: "\F8EC"; +} +.mdi-litecoin::before { + content: "\FA60"; +} +.mdi-loading::before { + content: "\F771"; +} +.mdi-location-enter::before { + content: "\FFE4"; +} +.mdi-location-exit::before { + content: "\FFE5"; +} +.mdi-lock::before { + content: "\F33E"; +} +.mdi-lock-alert::before { + content: "\F8ED"; +} +.mdi-lock-clock::before { + content: "\F97E"; +} +.mdi-lock-open::before { + content: "\F33F"; +} +.mdi-lock-open-outline::before { + content: "\F340"; +} +.mdi-lock-open-variant::before { + content: "\FFE6"; +} +.mdi-lock-open-variant-outline::before { + content: "\FFE7"; +} +.mdi-lock-outline::before { + content: "\F341"; +} +.mdi-lock-pattern::before { + content: "\F6E9"; +} +.mdi-lock-plus::before { + content: "\F5FB"; +} +.mdi-lock-question::before { + content: "\F8EE"; +} +.mdi-lock-reset::before { + content: "\F772"; +} +.mdi-lock-smart::before { + content: "\F8B1"; +} +.mdi-locker::before { + content: "\F7D6"; +} +.mdi-locker-multiple::before { + content: "\F7D7"; +} +.mdi-login::before { + content: "\F342"; +} +.mdi-login-variant::before { + content: "\F5FC"; +} +.mdi-logout::before { + content: "\F343"; +} +.mdi-logout-variant::before { + content: "\F5FD"; +} +.mdi-longitude::before { + content: "\FF77"; +} +.mdi-looks::before { + content: "\F344"; +} +.mdi-loupe::before { + content: "\F345"; +} +.mdi-lumx::before { + content: "\F346"; +} +.mdi-lungs::before { + content: "\F00AF"; +} +.mdi-lyft::before { + content: "\FB1D"; +} +.mdi-magnet::before { + content: "\F347"; +} +.mdi-magnet-on::before { + content: "\F348"; +} +.mdi-magnify::before { + content: "\F349"; +} +.mdi-magnify-close::before { + content: "\F97F"; +} +.mdi-magnify-minus::before { + content: "\F34A"; +} +.mdi-magnify-minus-cursor::before { + content: "\FA61"; +} +.mdi-magnify-minus-outline::before { + content: "\F6EB"; +} +.mdi-magnify-plus::before { + content: "\F34B"; +} +.mdi-magnify-plus-cursor::before { + content: "\FA62"; +} +.mdi-magnify-plus-outline::before { + content: "\F6EC"; +} +.mdi-magnify-remove-cursor::before { + content: "\F0237"; +} +.mdi-magnify-remove-outline::before { + content: "\F0238"; +} +.mdi-magnify-scan::before { + content: "\F02A1"; +} +.mdi-mail::before { + content: "\FED8"; +} +.mdi-mail-ru::before { + content: "\F34C"; +} +.mdi-mailbox::before { + content: "\F6ED"; +} +.mdi-mailbox-open::before { + content: "\FD64"; +} +.mdi-mailbox-open-outline::before { + content: "\FD65"; +} +.mdi-mailbox-open-up::before { + content: "\FD66"; +} +.mdi-mailbox-open-up-outline::before { + content: "\FD67"; +} +.mdi-mailbox-outline::before { + content: "\FD68"; +} +.mdi-mailbox-up::before { + content: "\FD69"; +} +.mdi-mailbox-up-outline::before { + content: "\FD6A"; +} +.mdi-map::before { + content: "\F34D"; +} +.mdi-map-check::before { + content: "\FED9"; +} +.mdi-map-check-outline::before { + content: "\FEDA"; +} +.mdi-map-clock::before { + content: "\FCFA"; +} +.mdi-map-clock-outline::before { + content: "\FCFB"; +} +.mdi-map-legend::before { + content: "\FA00"; +} +.mdi-map-marker::before { + content: "\F34E"; +} +.mdi-map-marker-alert::before { + content: "\FF22"; +} +.mdi-map-marker-alert-outline::before { + content: "\FF23"; +} +.mdi-map-marker-check::before { + content: "\FC71"; +} +.mdi-map-marker-check-outline::before { + content: "\F0326"; +} +.mdi-map-marker-circle::before { + content: "\F34F"; +} +.mdi-map-marker-distance::before { + content: "\F8EF"; +} +.mdi-map-marker-down::before { + content: "\F012D"; +} +.mdi-map-marker-left::before { + content: "\F0306"; +} +.mdi-map-marker-left-outline::before { + content: "\F0308"; +} +.mdi-map-marker-minus::before { + content: "\F650"; +} +.mdi-map-marker-minus-outline::before { + content: "\F0324"; +} +.mdi-map-marker-multiple::before { + content: "\F350"; +} +.mdi-map-marker-multiple-outline::before { + content: "\F02A2"; +} +.mdi-map-marker-off::before { + content: "\F351"; +} +.mdi-map-marker-off-outline::before { + content: "\F0328"; +} +.mdi-map-marker-outline::before { + content: "\F7D8"; +} +.mdi-map-marker-path::before { + content: "\FCFC"; +} +.mdi-map-marker-plus::before { + content: "\F651"; +} +.mdi-map-marker-plus-outline::before { + content: "\F0323"; +} +.mdi-map-marker-question::before { + content: "\FF24"; +} +.mdi-map-marker-question-outline::before { + content: "\FF25"; +} +.mdi-map-marker-radius::before { + content: "\F352"; +} +.mdi-map-marker-radius-outline::before { + content: "\F0327"; +} +.mdi-map-marker-remove::before { + content: "\FF26"; +} +.mdi-map-marker-remove-outline::before { + content: "\F0325"; +} +.mdi-map-marker-remove-variant::before { + content: "\FF27"; +} +.mdi-map-marker-right::before { + content: "\F0307"; +} +.mdi-map-marker-right-outline::before { + content: "\F0309"; +} +.mdi-map-marker-up::before { + content: "\F012E"; +} +.mdi-map-minus::before { + content: "\F980"; +} +.mdi-map-outline::before { + content: "\F981"; +} +.mdi-map-plus::before { + content: "\F982"; +} +.mdi-map-search::before { + content: "\F983"; +} +.mdi-map-search-outline::before { + content: "\F984"; +} +.mdi-mapbox::before { + content: "\FB86"; +} +.mdi-margin::before { + content: "\F353"; +} +.mdi-markdown::before { + content: "\F354"; +} +.mdi-markdown-outline::before { + content: "\FF78"; +} +.mdi-marker::before { + content: "\F652"; +} +.mdi-marker-cancel::before { + content: "\FDB5"; +} +.mdi-marker-check::before { + content: "\F355"; +} +.mdi-mastodon::before { + content: "\FAD0"; +} +.mdi-mastodon-variant::before { + content: "\FAD1"; +} +.mdi-material-design::before { + content: "\F985"; +} +.mdi-material-ui::before { + content: "\F357"; +} +.mdi-math-compass::before { + content: "\F358"; +} +.mdi-math-cos::before { + content: "\FC72"; +} +.mdi-math-integral::before { + content: "\FFE8"; +} +.mdi-math-integral-box::before { + content: "\FFE9"; +} +.mdi-math-log::before { + content: "\F00B0"; +} +.mdi-math-norm::before { + content: "\FFEA"; +} +.mdi-math-norm-box::before { + content: "\FFEB"; +} +.mdi-math-sin::before { + content: "\FC73"; +} +.mdi-math-tan::before { + content: "\FC74"; +} +.mdi-matrix::before { + content: "\F628"; +} +.mdi-medal::before { + content: "\F986"; +} +.mdi-medal-outline::before { + content: "\F0351"; +} +.mdi-medical-bag::before { + content: "\F6EE"; +} +.mdi-meditation::before { + content: "\F01A6"; +} +.mdi-medium::before { + content: "\F35A"; +} +.mdi-meetup::before { + content: "\FAD2"; +} +.mdi-memory::before { + content: "\F35B"; +} +.mdi-menu::before { + content: "\F35C"; +} +.mdi-menu-down::before { + content: "\F35D"; +} +.mdi-menu-down-outline::before { + content: "\F6B5"; +} +.mdi-menu-left::before { + content: "\F35E"; +} +.mdi-menu-left-outline::before { + content: "\FA01"; +} +.mdi-menu-open::before { + content: "\FB87"; +} +.mdi-menu-right::before { + content: "\F35F"; +} +.mdi-menu-right-outline::before { + content: "\FA02"; +} +.mdi-menu-swap::before { + content: "\FA63"; +} +.mdi-menu-swap-outline::before { + content: "\FA64"; +} +.mdi-menu-up::before { + content: "\F360"; +} +.mdi-menu-up-outline::before { + content: "\F6B6"; +} +.mdi-merge::before { + content: "\FF79"; +} +.mdi-message::before { + content: "\F361"; +} +.mdi-message-alert::before { + content: "\F362"; +} +.mdi-message-alert-outline::before { + content: "\FA03"; +} +.mdi-message-arrow-left::before { + content: "\F031D"; +} +.mdi-message-arrow-left-outline::before { + content: "\F031E"; +} +.mdi-message-arrow-right::before { + content: "\F031F"; +} +.mdi-message-arrow-right-outline::before { + content: "\F0320"; +} +.mdi-message-bulleted::before { + content: "\F6A1"; +} +.mdi-message-bulleted-off::before { + content: "\F6A2"; +} +.mdi-message-draw::before { + content: "\F363"; +} +.mdi-message-image::before { + content: "\F364"; +} +.mdi-message-image-outline::before { + content: "\F0197"; +} +.mdi-message-lock::before { + content: "\FFEC"; +} +.mdi-message-lock-outline::before { + content: "\F0198"; +} +.mdi-message-minus::before { + content: "\F0199"; +} +.mdi-message-minus-outline::before { + content: "\F019A"; +} +.mdi-message-outline::before { + content: "\F365"; +} +.mdi-message-plus::before { + content: "\F653"; +} +.mdi-message-plus-outline::before { + content: "\F00E6"; +} +.mdi-message-processing::before { + content: "\F366"; +} +.mdi-message-processing-outline::before { + content: "\F019B"; +} +.mdi-message-reply::before { + content: "\F367"; +} +.mdi-message-reply-text::before { + content: "\F368"; +} +.mdi-message-settings::before { + content: "\F6EF"; +} +.mdi-message-settings-outline::before { + content: "\F019C"; +} +.mdi-message-settings-variant::before { + content: "\F6F0"; +} +.mdi-message-settings-variant-outline::before { + content: "\F019D"; +} +.mdi-message-text::before { + content: "\F369"; +} +.mdi-message-text-clock::before { + content: "\F019E"; +} +.mdi-message-text-clock-outline::before { + content: "\F019F"; +} +.mdi-message-text-lock::before { + content: "\FFED"; +} +.mdi-message-text-lock-outline::before { + content: "\F01A0"; +} +.mdi-message-text-outline::before { + content: "\F36A"; +} +.mdi-message-video::before { + content: "\F36B"; +} +.mdi-meteor::before { + content: "\F629"; +} +.mdi-metronome::before { + content: "\F7D9"; +} +.mdi-metronome-tick::before { + content: "\F7DA"; +} +.mdi-micro-sd::before { + content: "\F7DB"; +} +.mdi-microphone::before { + content: "\F36C"; +} +.mdi-microphone-minus::before { + content: "\F8B2"; +} +.mdi-microphone-off::before { + content: "\F36D"; +} +.mdi-microphone-outline::before { + content: "\F36E"; +} +.mdi-microphone-plus::before { + content: "\F8B3"; +} +.mdi-microphone-settings::before { + content: "\F36F"; +} +.mdi-microphone-variant::before { + content: "\F370"; +} +.mdi-microphone-variant-off::before { + content: "\F371"; +} +.mdi-microscope::before { + content: "\F654"; +} +.mdi-microsoft::before { + content: "\F372"; +} +.mdi-microsoft-dynamics::before { + content: "\F987"; +} +.mdi-microwave::before { + content: "\FC75"; +} +.mdi-middleware::before { + content: "\FF7A"; +} +.mdi-middleware-outline::before { + content: "\FF7B"; +} +.mdi-midi::before { + content: "\F8F0"; +} +.mdi-midi-port::before { + content: "\F8F1"; +} +.mdi-mine::before { + content: "\FDB6"; +} +.mdi-minecraft::before { + content: "\F373"; +} +.mdi-mini-sd::before { + content: "\FA04"; +} +.mdi-minidisc::before { + content: "\FA05"; +} +.mdi-minus::before { + content: "\F374"; +} +.mdi-minus-box::before { + content: "\F375"; +} +.mdi-minus-box-multiple::before { + content: "\F016C"; +} +.mdi-minus-box-multiple-outline::before { + content: "\F016D"; +} +.mdi-minus-box-outline::before { + content: "\F6F1"; +} +.mdi-minus-circle::before { + content: "\F376"; +} +.mdi-minus-circle-outline::before { + content: "\F377"; +} +.mdi-minus-network::before { + content: "\F378"; +} +.mdi-minus-network-outline::before { + content: "\FC76"; +} +.mdi-mirror::before { + content: "\F0228"; +} +.mdi-mixcloud::before { + content: "\F62A"; +} +.mdi-mixed-martial-arts::before { + content: "\FD6B"; +} +.mdi-mixed-reality::before { + content: "\F87E"; +} +.mdi-mixer::before { + content: "\F7DC"; +} +.mdi-molecule::before { + content: "\FB88"; +} +.mdi-monitor::before { + content: "\F379"; +} +.mdi-monitor-cellphone::before { + content: "\F988"; +} +.mdi-monitor-cellphone-star::before { + content: "\F989"; +} +.mdi-monitor-clean::before { + content: "\F012F"; +} +.mdi-monitor-dashboard::before { + content: "\FA06"; +} +.mdi-monitor-edit::before { + content: "\F02F1"; +} +.mdi-monitor-lock::before { + content: "\FDB7"; +} +.mdi-monitor-multiple::before { + content: "\F37A"; +} +.mdi-monitor-off::before { + content: "\FD6C"; +} +.mdi-monitor-screenshot::before { + content: "\FE34"; +} +.mdi-monitor-speaker::before { + content: "\FF7C"; +} +.mdi-monitor-speaker-off::before { + content: "\FF7D"; +} +.mdi-monitor-star::before { + content: "\FDB8"; +} +.mdi-moon-first-quarter::before { + content: "\FF7E"; +} +.mdi-moon-full::before { + content: "\FF7F"; +} +.mdi-moon-last-quarter::before { + content: "\FF80"; +} +.mdi-moon-new::before { + content: "\FF81"; +} +.mdi-moon-waning-crescent::before { + content: "\FF82"; +} +.mdi-moon-waning-gibbous::before { + content: "\FF83"; +} +.mdi-moon-waxing-crescent::before { + content: "\FF84"; +} +.mdi-moon-waxing-gibbous::before { + content: "\FF85"; +} +.mdi-moped::before { + content: "\F00B1"; +} +.mdi-more::before { + content: "\F37B"; +} +.mdi-mother-heart::before { + content: "\F033F"; +} +.mdi-mother-nurse::before { + content: "\FCFD"; +} +.mdi-motion-sensor::before { + content: "\FD6D"; +} +.mdi-motorbike::before { + content: "\F37C"; +} +.mdi-mouse::before { + content: "\F37D"; +} +.mdi-mouse-bluetooth::before { + content: "\F98A"; +} +.mdi-mouse-off::before { + content: "\F37E"; +} +.mdi-mouse-variant::before { + content: "\F37F"; +} +.mdi-mouse-variant-off::before { + content: "\F380"; +} +.mdi-move-resize::before { + content: "\F655"; +} +.mdi-move-resize-variant::before { + content: "\F656"; +} +.mdi-movie::before { + content: "\F381"; +} +.mdi-movie-edit::before { + content: "\F014D"; +} +.mdi-movie-edit-outline::before { + content: "\F014E"; +} +.mdi-movie-filter::before { + content: "\F014F"; +} +.mdi-movie-filter-outline::before { + content: "\F0150"; +} +.mdi-movie-open::before { + content: "\FFEE"; +} +.mdi-movie-open-outline::before { + content: "\FFEF"; +} +.mdi-movie-outline::before { + content: "\FDB9"; +} +.mdi-movie-roll::before { + content: "\F7DD"; +} +.mdi-movie-search::before { + content: "\F01FD"; +} +.mdi-movie-search-outline::before { + content: "\F01FE"; +} +.mdi-muffin::before { + content: "\F98B"; +} +.mdi-multiplication::before { + content: "\F382"; +} +.mdi-multiplication-box::before { + content: "\F383"; +} +.mdi-mushroom::before { + content: "\F7DE"; +} +.mdi-mushroom-outline::before { + content: "\F7DF"; +} +.mdi-music::before { + content: "\F759"; +} +.mdi-music-accidental-double-flat::before { + content: "\FF86"; +} +.mdi-music-accidental-double-sharp::before { + content: "\FF87"; +} +.mdi-music-accidental-flat::before { + content: "\FF88"; +} +.mdi-music-accidental-natural::before { + content: "\FF89"; +} +.mdi-music-accidental-sharp::before { + content: "\FF8A"; +} +.mdi-music-box::before { + content: "\F384"; +} +.mdi-music-box-outline::before { + content: "\F385"; +} +.mdi-music-circle::before { + content: "\F386"; +} +.mdi-music-circle-outline::before { + content: "\FAD3"; +} +.mdi-music-clef-alto::before { + content: "\FF8B"; +} +.mdi-music-clef-bass::before { + content: "\FF8C"; +} +.mdi-music-clef-treble::before { + content: "\FF8D"; +} +.mdi-music-note::before { + content: "\F387"; +} +.mdi-music-note-bluetooth::before { + content: "\F5FE"; +} +.mdi-music-note-bluetooth-off::before { + content: "\F5FF"; +} +.mdi-music-note-eighth::before { + content: "\F388"; +} +.mdi-music-note-eighth-dotted::before { + content: "\FF8E"; +} +.mdi-music-note-half::before { + content: "\F389"; +} +.mdi-music-note-half-dotted::before { + content: "\FF8F"; +} +.mdi-music-note-off::before { + content: "\F38A"; +} +.mdi-music-note-off-outline::before { + content: "\FF90"; +} +.mdi-music-note-outline::before { + content: "\FF91"; +} +.mdi-music-note-plus::before { + content: "\FDBA"; +} +.mdi-music-note-quarter::before { + content: "\F38B"; +} +.mdi-music-note-quarter-dotted::before { + content: "\FF92"; +} +.mdi-music-note-sixteenth::before { + content: "\F38C"; +} +.mdi-music-note-sixteenth-dotted::before { + content: "\FF93"; +} +.mdi-music-note-whole::before { + content: "\F38D"; +} +.mdi-music-note-whole-dotted::before { + content: "\FF94"; +} +.mdi-music-off::before { + content: "\F75A"; +} +.mdi-music-rest-eighth::before { + content: "\FF95"; +} +.mdi-music-rest-half::before { + content: "\FF96"; +} +.mdi-music-rest-quarter::before { + content: "\FF97"; +} +.mdi-music-rest-sixteenth::before { + content: "\FF98"; +} +.mdi-music-rest-whole::before { + content: "\FF99"; +} +.mdi-nail::before { + content: "\FDBB"; +} +.mdi-nas::before { + content: "\F8F2"; +} +.mdi-nativescript::before { + content: "\F87F"; +} +.mdi-nature::before { + content: "\F38E"; +} +.mdi-nature-people::before { + content: "\F38F"; +} +.mdi-navigation::before { + content: "\F390"; +} +.mdi-near-me::before { + content: "\F5CD"; +} +.mdi-necklace::before { + content: "\FF28"; +} +.mdi-needle::before { + content: "\F391"; +} +.mdi-netflix::before { + content: "\F745"; +} +.mdi-network::before { + content: "\F6F2"; +} +.mdi-network-off::before { + content: "\FC77"; +} +.mdi-network-off-outline::before { + content: "\FC78"; +} +.mdi-network-outline::before { + content: "\FC79"; +} +.mdi-network-router::before { + content: "\F00B2"; +} +.mdi-network-strength-1::before { + content: "\F8F3"; +} +.mdi-network-strength-1-alert::before { + content: "\F8F4"; +} +.mdi-network-strength-2::before { + content: "\F8F5"; +} +.mdi-network-strength-2-alert::before { + content: "\F8F6"; +} +.mdi-network-strength-3::before { + content: "\F8F7"; +} +.mdi-network-strength-3-alert::before { + content: "\F8F8"; +} +.mdi-network-strength-4::before { + content: "\F8F9"; +} +.mdi-network-strength-4-alert::before { + content: "\F8FA"; +} +.mdi-network-strength-off::before { + content: "\F8FB"; +} +.mdi-network-strength-off-outline::before { + content: "\F8FC"; +} +.mdi-network-strength-outline::before { + content: "\F8FD"; +} +.mdi-new-box::before { + content: "\F394"; +} +.mdi-newspaper::before { + content: "\F395"; +} +.mdi-newspaper-minus::before { + content: "\FF29"; +} +.mdi-newspaper-plus::before { + content: "\FF2A"; +} +.mdi-newspaper-variant::before { + content: "\F0023"; +} +.mdi-newspaper-variant-multiple::before { + content: "\F0024"; +} +.mdi-newspaper-variant-multiple-outline::before { + content: "\F0025"; +} +.mdi-newspaper-variant-outline::before { + content: "\F0026"; +} +.mdi-nfc::before { + content: "\F396"; +} +.mdi-nfc-off::before { + content: "\FE35"; +} +.mdi-nfc-search-variant::before { + content: "\FE36"; +} +.mdi-nfc-tap::before { + content: "\F397"; +} +.mdi-nfc-variant::before { + content: "\F398"; +} +.mdi-nfc-variant-off::before { + content: "\FE37"; +} +.mdi-ninja::before { + content: "\F773"; +} +.mdi-nintendo-switch::before { + content: "\F7E0"; +} +.mdi-nix::before { + content: "\F0130"; +} +.mdi-nodejs::before { + content: "\F399"; +} +.mdi-noodles::before { + content: "\F01A9"; +} +.mdi-not-equal::before { + content: "\F98C"; +} +.mdi-not-equal-variant::before { + content: "\F98D"; +} +.mdi-note::before { + content: "\F39A"; +} +.mdi-note-multiple::before { + content: "\F6B7"; +} +.mdi-note-multiple-outline::before { + content: "\F6B8"; +} +.mdi-note-outline::before { + content: "\F39B"; +} +.mdi-note-plus::before { + content: "\F39C"; +} +.mdi-note-plus-outline::before { + content: "\F39D"; +} +.mdi-note-text::before { + content: "\F39E"; +} +.mdi-note-text-outline::before { + content: "\F0202"; +} +.mdi-notebook::before { + content: "\F82D"; +} +.mdi-notebook-multiple::before { + content: "\FE38"; +} +.mdi-notebook-outline::before { + content: "\FEDC"; +} +.mdi-notification-clear-all::before { + content: "\F39F"; +} +.mdi-npm::before { + content: "\F6F6"; +} +.mdi-npm-variant::before { + content: "\F98E"; +} +.mdi-npm-variant-outline::before { + content: "\F98F"; +} +.mdi-nuke::before { + content: "\F6A3"; +} +.mdi-null::before { + content: "\F7E1"; +} +.mdi-numeric::before { + content: "\F3A0"; +} +.mdi-numeric-0::before { + content: "\30"; +} +.mdi-numeric-0-box::before { + content: "\F3A1"; +} +.mdi-numeric-0-box-multiple::before { + content: "\FF2B"; +} +.mdi-numeric-0-box-multiple-outline::before { + content: "\F3A2"; +} +.mdi-numeric-0-box-outline::before { + content: "\F3A3"; +} +.mdi-numeric-0-circle::before { + content: "\FC7A"; +} +.mdi-numeric-0-circle-outline::before { + content: "\FC7B"; +} +.mdi-numeric-1::before { + content: "\31"; +} +.mdi-numeric-1-box::before { + content: "\F3A4"; +} +.mdi-numeric-1-box-multiple::before { + content: "\FF2C"; +} +.mdi-numeric-1-box-multiple-outline::before { + content: "\F3A5"; +} +.mdi-numeric-1-box-outline::before { + content: "\F3A6"; +} +.mdi-numeric-1-circle::before { + content: "\FC7C"; +} +.mdi-numeric-1-circle-outline::before { + content: "\FC7D"; +} +.mdi-numeric-10::before { + content: "\F000A"; +} +.mdi-numeric-10-box::before { + content: "\FF9A"; +} +.mdi-numeric-10-box-multiple::before { + content: "\F000B"; +} +.mdi-numeric-10-box-multiple-outline::before { + content: "\F000C"; +} +.mdi-numeric-10-box-outline::before { + content: "\FF9B"; +} +.mdi-numeric-10-circle::before { + content: "\F000D"; +} +.mdi-numeric-10-circle-outline::before { + content: "\F000E"; +} +.mdi-numeric-2::before { + content: "\32"; +} +.mdi-numeric-2-box::before { + content: "\F3A7"; +} +.mdi-numeric-2-box-multiple::before { + content: "\FF2D"; +} +.mdi-numeric-2-box-multiple-outline::before { + content: "\F3A8"; +} +.mdi-numeric-2-box-outline::before { + content: "\F3A9"; +} +.mdi-numeric-2-circle::before { + content: "\FC7E"; +} +.mdi-numeric-2-circle-outline::before { + content: "\FC7F"; +} +.mdi-numeric-3::before { + content: "\33"; +} +.mdi-numeric-3-box::before { + content: "\F3AA"; +} +.mdi-numeric-3-box-multiple::before { + content: "\FF2E"; +} +.mdi-numeric-3-box-multiple-outline::before { + content: "\F3AB"; +} +.mdi-numeric-3-box-outline::before { + content: "\F3AC"; +} +.mdi-numeric-3-circle::before { + content: "\FC80"; +} +.mdi-numeric-3-circle-outline::before { + content: "\FC81"; +} +.mdi-numeric-4::before { + content: "\34"; +} +.mdi-numeric-4-box::before { + content: "\F3AD"; +} +.mdi-numeric-4-box-multiple::before { + content: "\FF2F"; +} +.mdi-numeric-4-box-multiple-outline::before { + content: "\F3AE"; +} +.mdi-numeric-4-box-outline::before { + content: "\F3AF"; +} +.mdi-numeric-4-circle::before { + content: "\FC82"; +} +.mdi-numeric-4-circle-outline::before { + content: "\FC83"; +} +.mdi-numeric-5::before { + content: "\35"; +} +.mdi-numeric-5-box::before { + content: "\F3B0"; +} +.mdi-numeric-5-box-multiple::before { + content: "\FF30"; +} +.mdi-numeric-5-box-multiple-outline::before { + content: "\F3B1"; +} +.mdi-numeric-5-box-outline::before { + content: "\F3B2"; +} +.mdi-numeric-5-circle::before { + content: "\FC84"; +} +.mdi-numeric-5-circle-outline::before { + content: "\FC85"; +} +.mdi-numeric-6::before { + content: "\36"; +} +.mdi-numeric-6-box::before { + content: "\F3B3"; +} +.mdi-numeric-6-box-multiple::before { + content: "\FF31"; +} +.mdi-numeric-6-box-multiple-outline::before { + content: "\F3B4"; +} +.mdi-numeric-6-box-outline::before { + content: "\F3B5"; +} +.mdi-numeric-6-circle::before { + content: "\FC86"; +} +.mdi-numeric-6-circle-outline::before { + content: "\FC87"; +} +.mdi-numeric-7::before { + content: "\37"; +} +.mdi-numeric-7-box::before { + content: "\F3B6"; +} +.mdi-numeric-7-box-multiple::before { + content: "\FF32"; +} +.mdi-numeric-7-box-multiple-outline::before { + content: "\F3B7"; +} +.mdi-numeric-7-box-outline::before { + content: "\F3B8"; +} +.mdi-numeric-7-circle::before { + content: "\FC88"; +} +.mdi-numeric-7-circle-outline::before { + content: "\FC89"; +} +.mdi-numeric-8::before { + content: "\38"; +} +.mdi-numeric-8-box::before { + content: "\F3B9"; +} +.mdi-numeric-8-box-multiple::before { + content: "\FF33"; +} +.mdi-numeric-8-box-multiple-outline::before { + content: "\F3BA"; +} +.mdi-numeric-8-box-outline::before { + content: "\F3BB"; +} +.mdi-numeric-8-circle::before { + content: "\FC8A"; +} +.mdi-numeric-8-circle-outline::before { + content: "\FC8B"; +} +.mdi-numeric-9::before { + content: "\39"; +} +.mdi-numeric-9-box::before { + content: "\F3BC"; +} +.mdi-numeric-9-box-multiple::before { + content: "\FF34"; +} +.mdi-numeric-9-box-multiple-outline::before { + content: "\F3BD"; +} +.mdi-numeric-9-box-outline::before { + content: "\F3BE"; +} +.mdi-numeric-9-circle::before { + content: "\FC8C"; +} +.mdi-numeric-9-circle-outline::before { + content: "\FC8D"; +} +.mdi-numeric-9-plus::before { + content: "\F000F"; +} +.mdi-numeric-9-plus-box::before { + content: "\F3BF"; +} +.mdi-numeric-9-plus-box-multiple::before { + content: "\FF35"; +} +.mdi-numeric-9-plus-box-multiple-outline::before { + content: "\F3C0"; +} +.mdi-numeric-9-plus-box-outline::before { + content: "\F3C1"; +} +.mdi-numeric-9-plus-circle::before { + content: "\FC8E"; +} +.mdi-numeric-9-plus-circle-outline::before { + content: "\FC8F"; +} +.mdi-numeric-negative-1::before { + content: "\F0074"; +} +.mdi-nut::before { + content: "\F6F7"; +} +.mdi-nutrition::before { + content: "\F3C2"; +} +.mdi-nuxt::before { + content: "\F0131"; +} +.mdi-oar::before { + content: "\F67B"; +} +.mdi-ocarina::before { + content: "\FDBC"; +} +.mdi-oci::before { + content: "\F0314"; +} +.mdi-ocr::before { + content: "\F0165"; +} +.mdi-octagon::before { + content: "\F3C3"; +} +.mdi-octagon-outline::before { + content: "\F3C4"; +} +.mdi-octagram::before { + content: "\F6F8"; +} +.mdi-octagram-outline::before { + content: "\F774"; +} +.mdi-odnoklassniki::before { + content: "\F3C5"; +} +.mdi-offer::before { + content: "\F0246"; +} +.mdi-office::before { + content: "\F3C6"; +} +.mdi-office-building::before { + content: "\F990"; +} +.mdi-oil::before { + content: "\F3C7"; +} +.mdi-oil-lamp::before { + content: "\FF36"; +} +.mdi-oil-level::before { + content: "\F0075"; +} +.mdi-oil-temperature::before { + content: "\F0019"; +} +.mdi-omega::before { + content: "\F3C9"; +} +.mdi-one-up::before { + content: "\FB89"; +} +.mdi-onedrive::before { + content: "\F3CA"; +} +.mdi-onenote::before { + content: "\F746"; +} +.mdi-onepassword::before { + content: "\F880"; +} +.mdi-opacity::before { + content: "\F5CC"; +} +.mdi-open-in-app::before { + content: "\F3CB"; +} +.mdi-open-in-new::before { + content: "\F3CC"; +} +.mdi-open-source-initiative::before { + content: "\FB8A"; +} +.mdi-openid::before { + content: "\F3CD"; +} +.mdi-opera::before { + content: "\F3CE"; +} +.mdi-orbit::before { + content: "\F018"; +} +.mdi-origin::before { + content: "\FB2B"; +} +.mdi-ornament::before { + content: "\F3CF"; +} +.mdi-ornament-variant::before { + content: "\F3D0"; +} +.mdi-outdoor-lamp::before { + content: "\F0076"; +} +.mdi-outlook::before { + content: "\FCFE"; +} +.mdi-overscan::before { + content: "\F0027"; +} +.mdi-owl::before { + content: "\F3D2"; +} +.mdi-pac-man::before { + content: "\FB8B"; +} +.mdi-package::before { + content: "\F3D3"; +} +.mdi-package-down::before { + content: "\F3D4"; +} +.mdi-package-up::before { + content: "\F3D5"; +} +.mdi-package-variant::before { + content: "\F3D6"; +} +.mdi-package-variant-closed::before { + content: "\F3D7"; +} +.mdi-page-first::before { + content: "\F600"; +} +.mdi-page-last::before { + content: "\F601"; +} +.mdi-page-layout-body::before { + content: "\F6F9"; +} +.mdi-page-layout-footer::before { + content: "\F6FA"; +} +.mdi-page-layout-header::before { + content: "\F6FB"; +} +.mdi-page-layout-header-footer::before { + content: "\FF9C"; +} +.mdi-page-layout-sidebar-left::before { + content: "\F6FC"; +} +.mdi-page-layout-sidebar-right::before { + content: "\F6FD"; +} +.mdi-page-next::before { + content: "\FB8C"; +} +.mdi-page-next-outline::before { + content: "\FB8D"; +} +.mdi-page-previous::before { + content: "\FB8E"; +} +.mdi-page-previous-outline::before { + content: "\FB8F"; +} +.mdi-palette::before { + content: "\F3D8"; +} +.mdi-palette-advanced::before { + content: "\F3D9"; +} +.mdi-palette-outline::before { + content: "\FE6C"; +} +.mdi-palette-swatch::before { + content: "\F8B4"; +} +.mdi-palette-swatch-outline::before { + content: "\F0387"; +} +.mdi-palm-tree::before { + content: "\F0077"; +} +.mdi-pan::before { + content: "\FB90"; +} +.mdi-pan-bottom-left::before { + content: "\FB91"; +} +.mdi-pan-bottom-right::before { + content: "\FB92"; +} +.mdi-pan-down::before { + content: "\FB93"; +} +.mdi-pan-horizontal::before { + content: "\FB94"; +} +.mdi-pan-left::before { + content: "\FB95"; +} +.mdi-pan-right::before { + content: "\FB96"; +} +.mdi-pan-top-left::before { + content: "\FB97"; +} +.mdi-pan-top-right::before { + content: "\FB98"; +} +.mdi-pan-up::before { + content: "\FB99"; +} +.mdi-pan-vertical::before { + content: "\FB9A"; +} +.mdi-panda::before { + content: "\F3DA"; +} +.mdi-pandora::before { + content: "\F3DB"; +} +.mdi-panorama::before { + content: "\F3DC"; +} +.mdi-panorama-fisheye::before { + content: "\F3DD"; +} +.mdi-panorama-horizontal::before { + content: "\F3DE"; +} +.mdi-panorama-vertical::before { + content: "\F3DF"; +} +.mdi-panorama-wide-angle::before { + content: "\F3E0"; +} +.mdi-paper-cut-vertical::before { + content: "\F3E1"; +} +.mdi-paper-roll::before { + content: "\F0182"; +} +.mdi-paper-roll-outline::before { + content: "\F0183"; +} +.mdi-paperclip::before { + content: "\F3E2"; +} +.mdi-parachute::before { + content: "\FC90"; +} +.mdi-parachute-outline::before { + content: "\FC91"; +} +.mdi-parking::before { + content: "\F3E3"; +} +.mdi-party-popper::before { + content: "\F0078"; +} +.mdi-passport::before { + content: "\F7E2"; +} +.mdi-passport-biometric::before { + content: "\FDBD"; +} +.mdi-pasta::before { + content: "\F018B"; +} +.mdi-patio-heater::before { + content: "\FF9D"; +} +.mdi-patreon::before { + content: "\F881"; +} +.mdi-pause::before { + content: "\F3E4"; +} +.mdi-pause-circle::before { + content: "\F3E5"; +} +.mdi-pause-circle-outline::before { + content: "\F3E6"; +} +.mdi-pause-octagon::before { + content: "\F3E7"; +} +.mdi-pause-octagon-outline::before { + content: "\F3E8"; +} +.mdi-paw::before { + content: "\F3E9"; +} +.mdi-paw-off::before { + content: "\F657"; +} +.mdi-paypal::before { + content: "\F882"; +} +.mdi-pdf-box::before { + content: "\FE39"; +} +.mdi-peace::before { + content: "\F883"; +} +.mdi-peanut::before { + content: "\F001E"; +} +.mdi-peanut-off::before { + content: "\F001F"; +} +.mdi-peanut-off-outline::before { + content: "\F0021"; +} +.mdi-peanut-outline::before { + content: "\F0020"; +} +.mdi-pen::before { + content: "\F3EA"; +} +.mdi-pen-lock::before { + content: "\FDBE"; +} +.mdi-pen-minus::before { + content: "\FDBF"; +} +.mdi-pen-off::before { + content: "\FDC0"; +} +.mdi-pen-plus::before { + content: "\FDC1"; +} +.mdi-pen-remove::before { + content: "\FDC2"; +} +.mdi-pencil::before { + content: "\F3EB"; +} +.mdi-pencil-box::before { + content: "\F3EC"; +} +.mdi-pencil-box-multiple::before { + content: "\F016F"; +} +.mdi-pencil-box-multiple-outline::before { + content: "\F0170"; +} +.mdi-pencil-box-outline::before { + content: "\F3ED"; +} +.mdi-pencil-circle::before { + content: "\F6FE"; +} +.mdi-pencil-circle-outline::before { + content: "\F775"; +} +.mdi-pencil-lock::before { + content: "\F3EE"; +} +.mdi-pencil-lock-outline::before { + content: "\FDC3"; +} +.mdi-pencil-minus::before { + content: "\FDC4"; +} +.mdi-pencil-minus-outline::before { + content: "\FDC5"; +} +.mdi-pencil-off::before { + content: "\F3EF"; +} +.mdi-pencil-off-outline::before { + content: "\FDC6"; +} +.mdi-pencil-outline::before { + content: "\FC92"; +} +.mdi-pencil-plus::before { + content: "\FDC7"; +} +.mdi-pencil-plus-outline::before { + content: "\FDC8"; +} +.mdi-pencil-remove::before { + content: "\FDC9"; +} +.mdi-pencil-remove-outline::before { + content: "\FDCA"; +} +.mdi-pencil-ruler::before { + content: "\F037E"; +} +.mdi-penguin::before { + content: "\FEDD"; +} +.mdi-pentagon::before { + content: "\F6FF"; +} +.mdi-pentagon-outline::before { + content: "\F700"; +} +.mdi-percent::before { + content: "\F3F0"; +} +.mdi-percent-outline::before { + content: "\F02A3"; +} +.mdi-periodic-table::before { + content: "\F8B5"; +} +.mdi-periodic-table-co::before { + content: "\F0329"; +} +.mdi-periodic-table-co2::before { + content: "\F7E3"; +} +.mdi-periscope::before { + content: "\F747"; +} +.mdi-perspective-less::before { + content: "\FCFF"; +} +.mdi-perspective-more::before { + content: "\FD00"; +} +.mdi-pharmacy::before { + content: "\F3F1"; +} +.mdi-phone::before { + content: "\F3F2"; +} +.mdi-phone-alert::before { + content: "\FF37"; +} +.mdi-phone-alert-outline::before { + content: "\F01B9"; +} +.mdi-phone-bluetooth::before { + content: "\F3F3"; +} +.mdi-phone-bluetooth-outline::before { + content: "\F01BA"; +} +.mdi-phone-cancel::before { + content: "\F00E7"; +} +.mdi-phone-cancel-outline::before { + content: "\F01BB"; +} +.mdi-phone-check::before { + content: "\F01D4"; +} +.mdi-phone-check-outline::before { + content: "\F01D5"; +} +.mdi-phone-classic::before { + content: "\F602"; +} +.mdi-phone-classic-off::before { + content: "\F02A4"; +} +.mdi-phone-forward::before { + content: "\F3F4"; +} +.mdi-phone-forward-outline::before { + content: "\F01BC"; +} +.mdi-phone-hangup::before { + content: "\F3F5"; +} +.mdi-phone-hangup-outline::before { + content: "\F01BD"; +} +.mdi-phone-in-talk::before { + content: "\F3F6"; +} +.mdi-phone-in-talk-outline::before { + content: "\F01AD"; +} +.mdi-phone-incoming::before { + content: "\F3F7"; +} +.mdi-phone-incoming-outline::before { + content: "\F01BE"; +} +.mdi-phone-lock::before { + content: "\F3F8"; +} +.mdi-phone-lock-outline::before { + content: "\F01BF"; +} +.mdi-phone-log::before { + content: "\F3F9"; +} +.mdi-phone-log-outline::before { + content: "\F01C0"; +} +.mdi-phone-message::before { + content: "\F01C1"; +} +.mdi-phone-message-outline::before { + content: "\F01C2"; +} +.mdi-phone-minus::before { + content: "\F658"; +} +.mdi-phone-minus-outline::before { + content: "\F01C3"; +} +.mdi-phone-missed::before { + content: "\F3FA"; +} +.mdi-phone-missed-outline::before { + content: "\F01D0"; +} +.mdi-phone-off::before { + content: "\FDCB"; +} +.mdi-phone-off-outline::before { + content: "\F01D1"; +} +.mdi-phone-outgoing::before { + content: "\F3FB"; +} +.mdi-phone-outgoing-outline::before { + content: "\F01C4"; +} +.mdi-phone-outline::before { + content: "\FDCC"; +} +.mdi-phone-paused::before { + content: "\F3FC"; +} +.mdi-phone-paused-outline::before { + content: "\F01C5"; +} +.mdi-phone-plus::before { + content: "\F659"; +} +.mdi-phone-plus-outline::before { + content: "\F01C6"; +} +.mdi-phone-return::before { + content: "\F82E"; +} +.mdi-phone-return-outline::before { + content: "\F01C7"; +} +.mdi-phone-ring::before { + content: "\F01D6"; +} +.mdi-phone-ring-outline::before { + content: "\F01D7"; +} +.mdi-phone-rotate-landscape::before { + content: "\F884"; +} +.mdi-phone-rotate-portrait::before { + content: "\F885"; +} +.mdi-phone-settings::before { + content: "\F3FD"; +} +.mdi-phone-settings-outline::before { + content: "\F01C8"; +} +.mdi-phone-voip::before { + content: "\F3FE"; +} +.mdi-pi::before { + content: "\F3FF"; +} +.mdi-pi-box::before { + content: "\F400"; +} +.mdi-pi-hole::before { + content: "\FDCD"; +} +.mdi-piano::before { + content: "\F67C"; +} +.mdi-pickaxe::before { + content: "\F8B6"; +} +.mdi-picture-in-picture-bottom-right::before { + content: "\FE3A"; +} +.mdi-picture-in-picture-bottom-right-outline::before { + content: "\FE3B"; +} +.mdi-picture-in-picture-top-right::before { + content: "\FE3C"; +} +.mdi-picture-in-picture-top-right-outline::before { + content: "\FE3D"; +} +.mdi-pier::before { + content: "\F886"; +} +.mdi-pier-crane::before { + content: "\F887"; +} +.mdi-pig::before { + content: "\F401"; +} +.mdi-pig-variant::before { + content: "\F0028"; +} +.mdi-piggy-bank::before { + content: "\F0029"; +} +.mdi-pill::before { + content: "\F402"; +} +.mdi-pillar::before { + content: "\F701"; +} +.mdi-pin::before { + content: "\F403"; +} +.mdi-pin-off::before { + content: "\F404"; +} +.mdi-pin-off-outline::before { + content: "\F92F"; +} +.mdi-pin-outline::before { + content: "\F930"; +} +.mdi-pine-tree::before { + content: "\F405"; +} +.mdi-pine-tree-box::before { + content: "\F406"; +} +.mdi-pinterest::before { + content: "\F407"; +} +.mdi-pinterest-box::before { + content: "\F408"; +} +.mdi-pinwheel::before { + content: "\FAD4"; +} +.mdi-pinwheel-outline::before { + content: "\FAD5"; +} +.mdi-pipe::before { + content: "\F7E4"; +} +.mdi-pipe-disconnected::before { + content: "\F7E5"; +} +.mdi-pipe-leak::before { + content: "\F888"; +} +.mdi-pipe-wrench::before { + content: "\F037F"; +} +.mdi-pirate::before { + content: "\FA07"; +} +.mdi-pistol::before { + content: "\F702"; +} +.mdi-piston::before { + content: "\F889"; +} +.mdi-pizza::before { + content: "\F409"; +} +.mdi-play::before { + content: "\F40A"; +} +.mdi-play-box::before { + content: "\F02A5"; +} +.mdi-play-box-outline::before { + content: "\F40B"; +} +.mdi-play-circle::before { + content: "\F40C"; +} +.mdi-play-circle-outline::before { + content: "\F40D"; +} +.mdi-play-network::before { + content: "\F88A"; +} +.mdi-play-network-outline::before { + content: "\FC93"; +} +.mdi-play-outline::before { + content: "\FF38"; +} +.mdi-play-pause::before { + content: "\F40E"; +} +.mdi-play-protected-content::before { + content: "\F40F"; +} +.mdi-play-speed::before { + content: "\F8FE"; +} +.mdi-playlist-check::before { + content: "\F5C7"; +} +.mdi-playlist-edit::before { + content: "\F8FF"; +} +.mdi-playlist-minus::before { + content: "\F410"; +} +.mdi-playlist-music::before { + content: "\FC94"; +} +.mdi-playlist-music-outline::before { + content: "\FC95"; +} +.mdi-playlist-play::before { + content: "\F411"; +} +.mdi-playlist-plus::before { + content: "\F412"; +} +.mdi-playlist-remove::before { + content: "\F413"; +} +.mdi-playlist-star::before { + content: "\FDCE"; +} +.mdi-playstation::before { + content: "\F414"; +} +.mdi-plex::before { + content: "\F6B9"; +} +.mdi-plus::before { + content: "\F415"; +} +.mdi-plus-box::before { + content: "\F416"; +} +.mdi-plus-box-multiple::before { + content: "\F334"; +} +.mdi-plus-box-multiple-outline::before { + content: "\F016E"; +} +.mdi-plus-box-outline::before { + content: "\F703"; +} +.mdi-plus-circle::before { + content: "\F417"; +} +.mdi-plus-circle-multiple-outline::before { + content: "\F418"; +} +.mdi-plus-circle-outline::before { + content: "\F419"; +} +.mdi-plus-minus::before { + content: "\F991"; +} +.mdi-plus-minus-box::before { + content: "\F992"; +} +.mdi-plus-network::before { + content: "\F41A"; +} +.mdi-plus-network-outline::before { + content: "\FC96"; +} +.mdi-plus-one::before { + content: "\F41B"; +} +.mdi-plus-outline::before { + content: "\F704"; +} +.mdi-plus-thick::before { + content: "\F0217"; +} +.mdi-pocket::before { + content: "\F41C"; +} +.mdi-podcast::before { + content: "\F993"; +} +.mdi-podium::before { + content: "\FD01"; +} +.mdi-podium-bronze::before { + content: "\FD02"; +} +.mdi-podium-gold::before { + content: "\FD03"; +} +.mdi-podium-silver::before { + content: "\FD04"; +} +.mdi-point-of-sale::before { + content: "\FD6E"; +} +.mdi-pokeball::before { + content: "\F41D"; +} +.mdi-pokemon-go::before { + content: "\FA08"; +} +.mdi-poker-chip::before { + content: "\F82F"; +} +.mdi-polaroid::before { + content: "\F41E"; +} +.mdi-police-badge::before { + content: "\F0192"; +} +.mdi-police-badge-outline::before { + content: "\F0193"; +} +.mdi-poll::before { + content: "\F41F"; +} +.mdi-poll-box::before { + content: "\F420"; +} +.mdi-poll-box-outline::before { + content: "\F02A6"; +} +.mdi-polymer::before { + content: "\F421"; +} +.mdi-pool::before { + content: "\F606"; +} +.mdi-popcorn::before { + content: "\F422"; +} +.mdi-post::before { + content: "\F002A"; +} +.mdi-post-outline::before { + content: "\F002B"; +} +.mdi-postage-stamp::before { + content: "\FC97"; +} +.mdi-pot::before { + content: "\F65A"; +} +.mdi-pot-mix::before { + content: "\F65B"; +} +.mdi-pound::before { + content: "\F423"; +} +.mdi-pound-box::before { + content: "\F424"; +} +.mdi-pound-box-outline::before { + content: "\F01AA"; +} +.mdi-power::before { + content: "\F425"; +} +.mdi-power-cycle::before { + content: "\F900"; +} +.mdi-power-off::before { + content: "\F901"; +} +.mdi-power-on::before { + content: "\F902"; +} +.mdi-power-plug::before { + content: "\F6A4"; +} +.mdi-power-plug-off::before { + content: "\F6A5"; +} +.mdi-power-settings::before { + content: "\F426"; +} +.mdi-power-sleep::before { + content: "\F903"; +} +.mdi-power-socket::before { + content: "\F427"; +} +.mdi-power-socket-au::before { + content: "\F904"; +} +.mdi-power-socket-de::before { + content: "\F0132"; +} +.mdi-power-socket-eu::before { + content: "\F7E6"; +} +.mdi-power-socket-fr::before { + content: "\F0133"; +} +.mdi-power-socket-jp::before { + content: "\F0134"; +} +.mdi-power-socket-uk::before { + content: "\F7E7"; +} +.mdi-power-socket-us::before { + content: "\F7E8"; +} +.mdi-power-standby::before { + content: "\F905"; +} +.mdi-powershell::before { + content: "\FA09"; +} +.mdi-prescription::before { + content: "\F705"; +} +.mdi-presentation::before { + content: "\F428"; +} +.mdi-presentation-play::before { + content: "\F429"; +} +.mdi-printer::before { + content: "\F42A"; +} +.mdi-printer-3d::before { + content: "\F42B"; +} +.mdi-printer-3d-nozzle::before { + content: "\FE3E"; +} +.mdi-printer-3d-nozzle-alert::before { + content: "\F01EB"; +} +.mdi-printer-3d-nozzle-alert-outline::before { + content: "\F01EC"; +} +.mdi-printer-3d-nozzle-outline::before { + content: "\FE3F"; +} +.mdi-printer-alert::before { + content: "\F42C"; +} +.mdi-printer-check::before { + content: "\F0171"; +} +.mdi-printer-off::before { + content: "\FE40"; +} +.mdi-printer-pos::before { + content: "\F0079"; +} +.mdi-printer-settings::before { + content: "\F706"; +} +.mdi-printer-wireless::before { + content: "\FA0A"; +} +.mdi-priority-high::before { + content: "\F603"; +} +.mdi-priority-low::before { + content: "\F604"; +} +.mdi-professional-hexagon::before { + content: "\F42D"; +} +.mdi-progress-alert::before { + content: "\FC98"; +} +.mdi-progress-check::before { + content: "\F994"; +} +.mdi-progress-clock::before { + content: "\F995"; +} +.mdi-progress-close::before { + content: "\F0135"; +} +.mdi-progress-download::before { + content: "\F996"; +} +.mdi-progress-upload::before { + content: "\F997"; +} +.mdi-progress-wrench::before { + content: "\FC99"; +} +.mdi-projector::before { + content: "\F42E"; +} +.mdi-projector-screen::before { + content: "\F42F"; +} +.mdi-propane-tank::before { + content: "\F0382"; +} +.mdi-propane-tank-outline::before { + content: "\F0383"; +} +.mdi-protocol::before { + content: "\FFF9"; +} +.mdi-publish::before { + content: "\F6A6"; +} +.mdi-pulse::before { + content: "\F430"; +} +.mdi-pumpkin::before { + content: "\FB9B"; +} +.mdi-purse::before { + content: "\FF39"; +} +.mdi-purse-outline::before { + content: "\FF3A"; +} +.mdi-puzzle::before { + content: "\F431"; +} +.mdi-puzzle-outline::before { + content: "\FA65"; +} +.mdi-qi::before { + content: "\F998"; +} +.mdi-qqchat::before { + content: "\F605"; +} +.mdi-qrcode::before { + content: "\F432"; +} +.mdi-qrcode-edit::before { + content: "\F8B7"; +} +.mdi-qrcode-minus::before { + content: "\F01B7"; +} +.mdi-qrcode-plus::before { + content: "\F01B6"; +} +.mdi-qrcode-remove::before { + content: "\F01B8"; +} +.mdi-qrcode-scan::before { + content: "\F433"; +} +.mdi-quadcopter::before { + content: "\F434"; +} +.mdi-quality-high::before { + content: "\F435"; +} +.mdi-quality-low::before { + content: "\FA0B"; +} +.mdi-quality-medium::before { + content: "\FA0C"; +} +.mdi-quicktime::before { + content: "\F436"; +} +.mdi-quora::before { + content: "\FD05"; +} +.mdi-rabbit::before { + content: "\F906"; +} +.mdi-racing-helmet::before { + content: "\FD6F"; +} +.mdi-racquetball::before { + content: "\FD70"; +} +.mdi-radar::before { + content: "\F437"; +} +.mdi-radiator::before { + content: "\F438"; +} +.mdi-radiator-disabled::before { + content: "\FAD6"; +} +.mdi-radiator-off::before { + content: "\FAD7"; +} +.mdi-radio::before { + content: "\F439"; +} +.mdi-radio-am::before { + content: "\FC9A"; +} +.mdi-radio-fm::before { + content: "\FC9B"; +} +.mdi-radio-handheld::before { + content: "\F43A"; +} +.mdi-radio-off::before { + content: "\F0247"; +} +.mdi-radio-tower::before { + content: "\F43B"; +} +.mdi-radioactive::before { + content: "\F43C"; +} +.mdi-radioactive-off::before { + content: "\FEDE"; +} +.mdi-radiobox-blank::before { + content: "\F43D"; +} +.mdi-radiobox-marked::before { + content: "\F43E"; +} +.mdi-radius::before { + content: "\FC9C"; +} +.mdi-radius-outline::before { + content: "\FC9D"; +} +.mdi-railroad-light::before { + content: "\FF3B"; +} +.mdi-raspberry-pi::before { + content: "\F43F"; +} +.mdi-ray-end::before { + content: "\F440"; +} +.mdi-ray-end-arrow::before { + content: "\F441"; +} +.mdi-ray-start::before { + content: "\F442"; +} +.mdi-ray-start-arrow::before { + content: "\F443"; +} +.mdi-ray-start-end::before { + content: "\F444"; +} +.mdi-ray-vertex::before { + content: "\F445"; +} +.mdi-react::before { + content: "\F707"; +} +.mdi-read::before { + content: "\F447"; +} +.mdi-receipt::before { + content: "\F449"; +} +.mdi-record::before { + content: "\F44A"; +} +.mdi-record-circle::before { + content: "\FEDF"; +} +.mdi-record-circle-outline::before { + content: "\FEE0"; +} +.mdi-record-player::before { + content: "\F999"; +} +.mdi-record-rec::before { + content: "\F44B"; +} +.mdi-rectangle::before { + content: "\FE41"; +} +.mdi-rectangle-outline::before { + content: "\FE42"; +} +.mdi-recycle::before { + content: "\F44C"; +} +.mdi-reddit::before { + content: "\F44D"; +} +.mdi-redhat::before { + content: "\F0146"; +} +.mdi-redo::before { + content: "\F44E"; +} +.mdi-redo-variant::before { + content: "\F44F"; +} +.mdi-reflect-horizontal::before { + content: "\FA0D"; +} +.mdi-reflect-vertical::before { + content: "\FA0E"; +} +.mdi-refresh::before { + content: "\F450"; +} +.mdi-refresh-circle::before { + content: "\F03A2"; +} +.mdi-regex::before { + content: "\F451"; +} +.mdi-registered-trademark::before { + content: "\FA66"; +} +.mdi-relative-scale::before { + content: "\F452"; +} +.mdi-reload::before { + content: "\F453"; +} +.mdi-reload-alert::before { + content: "\F0136"; +} +.mdi-reminder::before { + content: "\F88B"; +} +.mdi-remote::before { + content: "\F454"; +} +.mdi-remote-desktop::before { + content: "\F8B8"; +} +.mdi-remote-off::before { + content: "\FEE1"; +} +.mdi-remote-tv::before { + content: "\FEE2"; +} +.mdi-remote-tv-off::before { + content: "\FEE3"; +} +.mdi-rename-box::before { + content: "\F455"; +} +.mdi-reorder-horizontal::before { + content: "\F687"; +} +.mdi-reorder-vertical::before { + content: "\F688"; +} +.mdi-repeat::before { + content: "\F456"; +} +.mdi-repeat-off::before { + content: "\F457"; +} +.mdi-repeat-once::before { + content: "\F458"; +} +.mdi-replay::before { + content: "\F459"; +} +.mdi-reply::before { + content: "\F45A"; +} +.mdi-reply-all::before { + content: "\F45B"; +} +.mdi-reply-all-outline::before { + content: "\FF3C"; +} +.mdi-reply-circle::before { + content: "\F01D9"; +} +.mdi-reply-outline::before { + content: "\FF3D"; +} +.mdi-reproduction::before { + content: "\F45C"; +} +.mdi-resistor::before { + content: "\FB1F"; +} +.mdi-resistor-nodes::before { + content: "\FB20"; +} +.mdi-resize::before { + content: "\FA67"; +} +.mdi-resize-bottom-right::before { + content: "\F45D"; +} +.mdi-responsive::before { + content: "\F45E"; +} +.mdi-restart::before { + content: "\F708"; +} +.mdi-restart-alert::before { + content: "\F0137"; +} +.mdi-restart-off::before { + content: "\FD71"; +} +.mdi-restore::before { + content: "\F99A"; +} +.mdi-restore-alert::before { + content: "\F0138"; +} +.mdi-rewind::before { + content: "\F45F"; +} +.mdi-rewind-10::before { + content: "\FD06"; +} +.mdi-rewind-30::before { + content: "\FD72"; +} +.mdi-rewind-5::before { + content: "\F0224"; +} +.mdi-rewind-outline::before { + content: "\F709"; +} +.mdi-rhombus::before { + content: "\F70A"; +} +.mdi-rhombus-medium::before { + content: "\FA0F"; +} +.mdi-rhombus-outline::before { + content: "\F70B"; +} +.mdi-rhombus-split::before { + content: "\FA10"; +} +.mdi-ribbon::before { + content: "\F460"; +} +.mdi-rice::before { + content: "\F7E9"; +} +.mdi-ring::before { + content: "\F7EA"; +} +.mdi-rivet::before { + content: "\FE43"; +} +.mdi-road::before { + content: "\F461"; +} +.mdi-road-variant::before { + content: "\F462"; +} +.mdi-robber::before { + content: "\F007A"; +} +.mdi-robot::before { + content: "\F6A8"; +} +.mdi-robot-industrial::before { + content: "\FB21"; +} +.mdi-robot-mower::before { + content: "\F0222"; +} +.mdi-robot-mower-outline::before { + content: "\F021E"; +} +.mdi-robot-vacuum::before { + content: "\F70C"; +} +.mdi-robot-vacuum-variant::before { + content: "\F907"; +} +.mdi-rocket::before { + content: "\F463"; +} +.mdi-rodent::before { + content: "\F0352"; +} +.mdi-roller-skate::before { + content: "\FD07"; +} +.mdi-rollerblade::before { + content: "\FD08"; +} +.mdi-rollupjs::before { + content: "\FB9C"; +} +.mdi-roman-numeral-1::before { + content: "\F00B3"; +} +.mdi-roman-numeral-10::before { + content: "\F00BC"; +} +.mdi-roman-numeral-2::before { + content: "\F00B4"; +} +.mdi-roman-numeral-3::before { + content: "\F00B5"; +} +.mdi-roman-numeral-4::before { + content: "\F00B6"; +} +.mdi-roman-numeral-5::before { + content: "\F00B7"; +} +.mdi-roman-numeral-6::before { + content: "\F00B8"; +} +.mdi-roman-numeral-7::before { + content: "\F00B9"; +} +.mdi-roman-numeral-8::before { + content: "\F00BA"; +} +.mdi-roman-numeral-9::before { + content: "\F00BB"; +} +.mdi-room-service::before { + content: "\F88C"; +} +.mdi-room-service-outline::before { + content: "\FD73"; +} +.mdi-rotate-3d::before { + content: "\FEE4"; +} +.mdi-rotate-3d-variant::before { + content: "\F464"; +} +.mdi-rotate-left::before { + content: "\F465"; +} +.mdi-rotate-left-variant::before { + content: "\F466"; +} +.mdi-rotate-orbit::before { + content: "\FD74"; +} +.mdi-rotate-right::before { + content: "\F467"; +} +.mdi-rotate-right-variant::before { + content: "\F468"; +} +.mdi-rounded-corner::before { + content: "\F607"; +} +.mdi-router::before { + content: "\F020D"; +} +.mdi-router-wireless::before { + content: "\F469"; +} +.mdi-router-wireless-settings::before { + content: "\FA68"; +} +.mdi-routes::before { + content: "\F46A"; +} +.mdi-routes-clock::before { + content: "\F007B"; +} +.mdi-rowing::before { + content: "\F608"; +} +.mdi-rss::before { + content: "\F46B"; +} +.mdi-rss-box::before { + content: "\F46C"; +} +.mdi-rss-off::before { + content: "\FF3E"; +} +.mdi-ruby::before { + content: "\FD09"; +} +.mdi-rugby::before { + content: "\FD75"; +} +.mdi-ruler::before { + content: "\F46D"; +} +.mdi-ruler-square::before { + content: "\FC9E"; +} +.mdi-ruler-square-compass::before { + content: "\FEDB"; +} +.mdi-run::before { + content: "\F70D"; +} +.mdi-run-fast::before { + content: "\F46E"; +} +.mdi-rv-truck::before { + content: "\F01FF"; +} +.mdi-sack::before { + content: "\FD0A"; +} +.mdi-sack-percent::before { + content: "\FD0B"; +} +.mdi-safe::before { + content: "\FA69"; +} +.mdi-safe-square::before { + content: "\F02A7"; +} +.mdi-safe-square-outline::before { + content: "\F02A8"; +} +.mdi-safety-goggles::before { + content: "\FD0C"; +} +.mdi-sailing::before { + content: "\FEE5"; +} +.mdi-sale::before { + content: "\F46F"; +} +.mdi-salesforce::before { + content: "\F88D"; +} +.mdi-sass::before { + content: "\F7EB"; +} +.mdi-satellite::before { + content: "\F470"; +} +.mdi-satellite-uplink::before { + content: "\F908"; +} +.mdi-satellite-variant::before { + content: "\F471"; +} +.mdi-sausage::before { + content: "\F8B9"; +} +.mdi-saw-blade::before { + content: "\FE44"; +} +.mdi-saxophone::before { + content: "\F609"; +} +.mdi-scale::before { + content: "\F472"; +} +.mdi-scale-balance::before { + content: "\F5D1"; +} +.mdi-scale-bathroom::before { + content: "\F473"; +} +.mdi-scale-off::before { + content: "\F007C"; +} +.mdi-scanner::before { + content: "\F6AA"; +} +.mdi-scanner-off::before { + content: "\F909"; +} +.mdi-scatter-plot::before { + content: "\FEE6"; +} +.mdi-scatter-plot-outline::before { + content: "\FEE7"; +} +.mdi-school::before { + content: "\F474"; +} +.mdi-school-outline::before { + content: "\F01AB"; +} +.mdi-scissors-cutting::before { + content: "\FA6A"; +} +.mdi-scooter::before { + content: "\F0214"; +} +.mdi-scoreboard::before { + content: "\F02A9"; +} +.mdi-scoreboard-outline::before { + content: "\F02AA"; +} +.mdi-screen-rotation::before { + content: "\F475"; +} +.mdi-screen-rotation-lock::before { + content: "\F476"; +} +.mdi-screw-flat-top::before { + content: "\FDCF"; +} +.mdi-screw-lag::before { + content: "\FE54"; +} +.mdi-screw-machine-flat-top::before { + content: "\FE55"; +} +.mdi-screw-machine-round-top::before { + content: "\FE56"; +} +.mdi-screw-round-top::before { + content: "\FE57"; +} +.mdi-screwdriver::before { + content: "\F477"; +} +.mdi-script::before { + content: "\FB9D"; +} +.mdi-script-outline::before { + content: "\F478"; +} +.mdi-script-text::before { + content: "\FB9E"; +} +.mdi-script-text-outline::before { + content: "\FB9F"; +} +.mdi-sd::before { + content: "\F479"; +} +.mdi-seal::before { + content: "\F47A"; +} +.mdi-seal-variant::before { + content: "\FFFA"; +} +.mdi-search-web::before { + content: "\F70E"; +} +.mdi-seat::before { + content: "\FC9F"; +} +.mdi-seat-flat::before { + content: "\F47B"; +} +.mdi-seat-flat-angled::before { + content: "\F47C"; +} +.mdi-seat-individual-suite::before { + content: "\F47D"; +} +.mdi-seat-legroom-extra::before { + content: "\F47E"; +} +.mdi-seat-legroom-normal::before { + content: "\F47F"; +} +.mdi-seat-legroom-reduced::before { + content: "\F480"; +} +.mdi-seat-outline::before { + content: "\FCA0"; +} +.mdi-seat-passenger::before { + content: "\F0274"; +} +.mdi-seat-recline-extra::before { + content: "\F481"; +} +.mdi-seat-recline-normal::before { + content: "\F482"; +} +.mdi-seatbelt::before { + content: "\FCA1"; +} +.mdi-security::before { + content: "\F483"; +} +.mdi-security-network::before { + content: "\F484"; +} +.mdi-seed::before { + content: "\FE45"; +} +.mdi-seed-outline::before { + content: "\FE46"; +} +.mdi-segment::before { + content: "\FEE8"; +} +.mdi-select::before { + content: "\F485"; +} +.mdi-select-all::before { + content: "\F486"; +} +.mdi-select-color::before { + content: "\FD0D"; +} +.mdi-select-compare::before { + content: "\FAD8"; +} +.mdi-select-drag::before { + content: "\FA6B"; +} +.mdi-select-group::before { + content: "\FF9F"; +} +.mdi-select-inverse::before { + content: "\F487"; +} +.mdi-select-marker::before { + content: "\F02AB"; +} +.mdi-select-multiple::before { + content: "\F02AC"; +} +.mdi-select-multiple-marker::before { + content: "\F02AD"; +} +.mdi-select-off::before { + content: "\F488"; +} +.mdi-select-place::before { + content: "\FFFB"; +} +.mdi-select-search::before { + content: "\F022F"; +} +.mdi-selection::before { + content: "\F489"; +} +.mdi-selection-drag::before { + content: "\FA6C"; +} +.mdi-selection-ellipse::before { + content: "\FD0E"; +} +.mdi-selection-ellipse-arrow-inside::before { + content: "\FF3F"; +} +.mdi-selection-marker::before { + content: "\F02AE"; +} +.mdi-selection-multiple-marker::before { + content: "\F02AF"; +} +.mdi-selection-mutliple::before { + content: "\F02B0"; +} +.mdi-selection-off::before { + content: "\F776"; +} +.mdi-selection-search::before { + content: "\F0230"; +} +.mdi-semantic-web::before { + content: "\F0341"; +} +.mdi-send::before { + content: "\F48A"; +} +.mdi-send-check::before { + content: "\F018C"; +} +.mdi-send-check-outline::before { + content: "\F018D"; +} +.mdi-send-circle::before { + content: "\FE58"; +} +.mdi-send-circle-outline::before { + content: "\FE59"; +} +.mdi-send-clock::before { + content: "\F018E"; +} +.mdi-send-clock-outline::before { + content: "\F018F"; +} +.mdi-send-lock::before { + content: "\F7EC"; +} +.mdi-send-lock-outline::before { + content: "\F0191"; +} +.mdi-send-outline::before { + content: "\F0190"; +} +.mdi-serial-port::before { + content: "\F65C"; +} +.mdi-server::before { + content: "\F48B"; +} +.mdi-server-minus::before { + content: "\F48C"; +} +.mdi-server-network::before { + content: "\F48D"; +} +.mdi-server-network-off::before { + content: "\F48E"; +} +.mdi-server-off::before { + content: "\F48F"; +} +.mdi-server-plus::before { + content: "\F490"; +} +.mdi-server-remove::before { + content: "\F491"; +} +.mdi-server-security::before { + content: "\F492"; +} +.mdi-set-all::before { + content: "\F777"; +} +.mdi-set-center::before { + content: "\F778"; +} +.mdi-set-center-right::before { + content: "\F779"; +} +.mdi-set-left::before { + content: "\F77A"; +} +.mdi-set-left-center::before { + content: "\F77B"; +} +.mdi-set-left-right::before { + content: "\F77C"; +} +.mdi-set-none::before { + content: "\F77D"; +} +.mdi-set-right::before { + content: "\F77E"; +} +.mdi-set-top-box::before { + content: "\F99E"; +} +.mdi-settings::before { + content: "\F493"; +} +.mdi-settings-box::before { + content: "\F494"; +} +.mdi-settings-helper::before { + content: "\FA6D"; +} +.mdi-settings-outline::before { + content: "\F8BA"; +} +.mdi-settings-transfer::before { + content: "\F007D"; +} +.mdi-settings-transfer-outline::before { + content: "\F007E"; +} +.mdi-shaker::before { + content: "\F0139"; +} +.mdi-shaker-outline::before { + content: "\F013A"; +} +.mdi-shape::before { + content: "\F830"; +} +.mdi-shape-circle-plus::before { + content: "\F65D"; +} +.mdi-shape-outline::before { + content: "\F831"; +} +.mdi-shape-oval-plus::before { + content: "\F0225"; +} +.mdi-shape-plus::before { + content: "\F495"; +} +.mdi-shape-polygon-plus::before { + content: "\F65E"; +} +.mdi-shape-rectangle-plus::before { + content: "\F65F"; +} +.mdi-shape-square-plus::before { + content: "\F660"; +} +.mdi-share::before { + content: "\F496"; +} +.mdi-share-all::before { + content: "\F021F"; +} +.mdi-share-all-outline::before { + content: "\F0220"; +} +.mdi-share-circle::before { + content: "\F01D8"; +} +.mdi-share-off::before { + content: "\FF40"; +} +.mdi-share-off-outline::before { + content: "\FF41"; +} +.mdi-share-outline::before { + content: "\F931"; +} +.mdi-share-variant::before { + content: "\F497"; +} +.mdi-sheep::before { + content: "\FCA2"; +} +.mdi-shield::before { + content: "\F498"; +} +.mdi-shield-account::before { + content: "\F88E"; +} +.mdi-shield-account-outline::before { + content: "\FA11"; +} +.mdi-shield-airplane::before { + content: "\F6BA"; +} +.mdi-shield-airplane-outline::before { + content: "\FCA3"; +} +.mdi-shield-alert::before { + content: "\FEE9"; +} +.mdi-shield-alert-outline::before { + content: "\FEEA"; +} +.mdi-shield-car::before { + content: "\FFA0"; +} +.mdi-shield-check::before { + content: "\F565"; +} +.mdi-shield-check-outline::before { + content: "\FCA4"; +} +.mdi-shield-cross::before { + content: "\FCA5"; +} +.mdi-shield-cross-outline::before { + content: "\FCA6"; +} +.mdi-shield-edit::before { + content: "\F01CB"; +} +.mdi-shield-edit-outline::before { + content: "\F01CC"; +} +.mdi-shield-half::before { + content: "\F038B"; +} +.mdi-shield-half-full::before { + content: "\F77F"; +} +.mdi-shield-home::before { + content: "\F689"; +} +.mdi-shield-home-outline::before { + content: "\FCA7"; +} +.mdi-shield-key::before { + content: "\FBA0"; +} +.mdi-shield-key-outline::before { + content: "\FBA1"; +} +.mdi-shield-link-variant::before { + content: "\FD0F"; +} +.mdi-shield-link-variant-outline::before { + content: "\FD10"; +} +.mdi-shield-lock::before { + content: "\F99C"; +} +.mdi-shield-lock-outline::before { + content: "\FCA8"; +} +.mdi-shield-off::before { + content: "\F99D"; +} +.mdi-shield-off-outline::before { + content: "\F99B"; +} +.mdi-shield-outline::before { + content: "\F499"; +} +.mdi-shield-plus::before { + content: "\FAD9"; +} +.mdi-shield-plus-outline::before { + content: "\FADA"; +} +.mdi-shield-refresh::before { + content: "\F01CD"; +} +.mdi-shield-refresh-outline::before { + content: "\F01CE"; +} +.mdi-shield-remove::before { + content: "\FADB"; +} +.mdi-shield-remove-outline::before { + content: "\FADC"; +} +.mdi-shield-search::before { + content: "\FD76"; +} +.mdi-shield-star::before { + content: "\F0166"; +} +.mdi-shield-star-outline::before { + content: "\F0167"; +} +.mdi-shield-sun::before { + content: "\F007F"; +} +.mdi-shield-sun-outline::before { + content: "\F0080"; +} +.mdi-ship-wheel::before { + content: "\F832"; +} +.mdi-shoe-formal::before { + content: "\FB22"; +} +.mdi-shoe-heel::before { + content: "\FB23"; +} +.mdi-shoe-print::before { + content: "\FE5A"; +} +.mdi-shopify::before { + content: "\FADD"; +} +.mdi-shopping::before { + content: "\F49A"; +} +.mdi-shopping-music::before { + content: "\F49B"; +} +.mdi-shopping-outline::before { + content: "\F0200"; +} +.mdi-shopping-search::before { + content: "\FFA1"; +} +.mdi-shovel::before { + content: "\F70F"; +} +.mdi-shovel-off::before { + content: "\F710"; +} +.mdi-shower::before { + content: "\F99F"; +} +.mdi-shower-head::before { + content: "\F9A0"; +} +.mdi-shredder::before { + content: "\F49C"; +} +.mdi-shuffle::before { + content: "\F49D"; +} +.mdi-shuffle-disabled::before { + content: "\F49E"; +} +.mdi-shuffle-variant::before { + content: "\F49F"; +} +.mdi-shuriken::before { + content: "\F03AA"; +} +.mdi-sigma::before { + content: "\F4A0"; +} +.mdi-sigma-lower::before { + content: "\F62B"; +} +.mdi-sign-caution::before { + content: "\F4A1"; +} +.mdi-sign-direction::before { + content: "\F780"; +} +.mdi-sign-direction-minus::before { + content: "\F0022"; +} +.mdi-sign-direction-plus::before { + content: "\FFFD"; +} +.mdi-sign-direction-remove::before { + content: "\FFFE"; +} +.mdi-sign-real-estate::before { + content: "\F0143"; +} +.mdi-sign-text::before { + content: "\F781"; +} +.mdi-signal::before { + content: "\F4A2"; +} +.mdi-signal-2g::before { + content: "\F711"; +} +.mdi-signal-3g::before { + content: "\F712"; +} +.mdi-signal-4g::before { + content: "\F713"; +} +.mdi-signal-5g::before { + content: "\FA6E"; +} +.mdi-signal-cellular-1::before { + content: "\F8BB"; +} +.mdi-signal-cellular-2::before { + content: "\F8BC"; +} +.mdi-signal-cellular-3::before { + content: "\F8BD"; +} +.mdi-signal-cellular-outline::before { + content: "\F8BE"; +} +.mdi-signal-distance-variant::before { + content: "\FE47"; +} +.mdi-signal-hspa::before { + content: "\F714"; +} +.mdi-signal-hspa-plus::before { + content: "\F715"; +} +.mdi-signal-off::before { + content: "\F782"; +} +.mdi-signal-variant::before { + content: "\F60A"; +} +.mdi-signature::before { + content: "\FE5B"; +} +.mdi-signature-freehand::before { + content: "\FE5C"; +} +.mdi-signature-image::before { + content: "\FE5D"; +} +.mdi-signature-text::before { + content: "\FE5E"; +} +.mdi-silo::before { + content: "\FB24"; +} +.mdi-silverware::before { + content: "\F4A3"; +} +.mdi-silverware-clean::before { + content: "\FFFF"; +} +.mdi-silverware-fork::before { + content: "\F4A4"; +} +.mdi-silverware-fork-knife::before { + content: "\FA6F"; +} +.mdi-silverware-spoon::before { + content: "\F4A5"; +} +.mdi-silverware-variant::before { + content: "\F4A6"; +} +.mdi-sim::before { + content: "\F4A7"; +} +.mdi-sim-alert::before { + content: "\F4A8"; +} +.mdi-sim-off::before { + content: "\F4A9"; +} +.mdi-simple-icons::before { + content: "\F0348"; +} +.mdi-sina-weibo::before { + content: "\FADE"; +} +.mdi-sitemap::before { + content: "\F4AA"; +} +.mdi-skate::before { + content: "\FD11"; +} +.mdi-skew-less::before { + content: "\FD12"; +} +.mdi-skew-more::before { + content: "\FD13"; +} +.mdi-ski::before { + content: "\F032F"; +} +.mdi-ski-cross-country::before { + content: "\F0330"; +} +.mdi-ski-water::before { + content: "\F0331"; +} +.mdi-skip-backward::before { + content: "\F4AB"; +} +.mdi-skip-backward-outline::before { + content: "\FF42"; +} +.mdi-skip-forward::before { + content: "\F4AC"; +} +.mdi-skip-forward-outline::before { + content: "\FF43"; +} +.mdi-skip-next::before { + content: "\F4AD"; +} +.mdi-skip-next-circle::before { + content: "\F661"; +} +.mdi-skip-next-circle-outline::before { + content: "\F662"; +} +.mdi-skip-next-outline::before { + content: "\FF44"; +} +.mdi-skip-previous::before { + content: "\F4AE"; +} +.mdi-skip-previous-circle::before { + content: "\F663"; +} +.mdi-skip-previous-circle-outline::before { + content: "\F664"; +} +.mdi-skip-previous-outline::before { + content: "\FF45"; +} +.mdi-skull::before { + content: "\F68B"; +} +.mdi-skull-crossbones::before { + content: "\FBA2"; +} +.mdi-skull-crossbones-outline::before { + content: "\FBA3"; +} +.mdi-skull-outline::before { + content: "\FBA4"; +} +.mdi-skype::before { + content: "\F4AF"; +} +.mdi-skype-business::before { + content: "\F4B0"; +} +.mdi-slack::before { + content: "\F4B1"; +} +.mdi-slackware::before { + content: "\F90A"; +} +.mdi-slash-forward::before { + content: "\F0000"; +} +.mdi-slash-forward-box::before { + content: "\F0001"; +} +.mdi-sleep::before { + content: "\F4B2"; +} +.mdi-sleep-off::before { + content: "\F4B3"; +} +.mdi-slope-downhill::before { + content: "\FE5F"; +} +.mdi-slope-uphill::before { + content: "\FE60"; +} +.mdi-slot-machine::before { + content: "\F013F"; +} +.mdi-slot-machine-outline::before { + content: "\F0140"; +} +.mdi-smart-card::before { + content: "\F00E8"; +} +.mdi-smart-card-outline::before { + content: "\F00E9"; +} +.mdi-smart-card-reader::before { + content: "\F00EA"; +} +.mdi-smart-card-reader-outline::before { + content: "\F00EB"; +} +.mdi-smog::before { + content: "\FA70"; +} +.mdi-smoke-detector::before { + content: "\F392"; +} +.mdi-smoking::before { + content: "\F4B4"; +} +.mdi-smoking-off::before { + content: "\F4B5"; +} +.mdi-snapchat::before { + content: "\F4B6"; +} +.mdi-snowboard::before { + content: "\F0332"; +} +.mdi-snowflake::before { + content: "\F716"; +} +.mdi-snowflake-alert::before { + content: "\FF46"; +} +.mdi-snowflake-melt::before { + content: "\F02F6"; +} +.mdi-snowflake-variant::before { + content: "\FF47"; +} +.mdi-snowman::before { + content: "\F4B7"; +} +.mdi-soccer::before { + content: "\F4B8"; +} +.mdi-soccer-field::before { + content: "\F833"; +} +.mdi-sofa::before { + content: "\F4B9"; +} +.mdi-solar-panel::before { + content: "\FD77"; +} +.mdi-solar-panel-large::before { + content: "\FD78"; +} +.mdi-solar-power::before { + content: "\FA71"; +} +.mdi-soldering-iron::before { + content: "\F00BD"; +} +.mdi-solid::before { + content: "\F68C"; +} +.mdi-sort::before { + content: "\F4BA"; +} +.mdi-sort-alphabetical::before { + content: "\F4BB"; +} +.mdi-sort-alphabetical-ascending::before { + content: "\F0173"; +} +.mdi-sort-alphabetical-descending::before { + content: "\F0174"; +} +.mdi-sort-ascending::before { + content: "\F4BC"; +} +.mdi-sort-descending::before { + content: "\F4BD"; +} +.mdi-sort-numeric::before { + content: "\F4BE"; +} +.mdi-sort-variant::before { + content: "\F4BF"; +} +.mdi-sort-variant-lock::before { + content: "\FCA9"; +} +.mdi-sort-variant-lock-open::before { + content: "\FCAA"; +} +.mdi-sort-variant-remove::before { + content: "\F0172"; +} +.mdi-soundcloud::before { + content: "\F4C0"; +} +.mdi-source-branch::before { + content: "\F62C"; +} +.mdi-source-commit::before { + content: "\F717"; +} +.mdi-source-commit-end::before { + content: "\F718"; +} +.mdi-source-commit-end-local::before { + content: "\F719"; +} +.mdi-source-commit-local::before { + content: "\F71A"; +} +.mdi-source-commit-next-local::before { + content: "\F71B"; +} +.mdi-source-commit-start::before { + content: "\F71C"; +} +.mdi-source-commit-start-next-local::before { + content: "\F71D"; +} +.mdi-source-fork::before { + content: "\F4C1"; +} +.mdi-source-merge::before { + content: "\F62D"; +} +.mdi-source-pull::before { + content: "\F4C2"; +} +.mdi-source-repository::before { + content: "\FCAB"; +} +.mdi-source-repository-multiple::before { + content: "\FCAC"; +} +.mdi-soy-sauce::before { + content: "\F7ED"; +} +.mdi-spa::before { + content: "\FCAD"; +} +.mdi-spa-outline::before { + content: "\FCAE"; +} +.mdi-space-invaders::before { + content: "\FBA5"; +} +.mdi-space-station::before { + content: "\F03AE"; +} +.mdi-spade::before { + content: "\FE48"; +} +.mdi-speaker::before { + content: "\F4C3"; +} +.mdi-speaker-bluetooth::before { + content: "\F9A1"; +} +.mdi-speaker-multiple::before { + content: "\FD14"; +} +.mdi-speaker-off::before { + content: "\F4C4"; +} +.mdi-speaker-wireless::before { + content: "\F71E"; +} +.mdi-speedometer::before { + content: "\F4C5"; +} +.mdi-speedometer-medium::before { + content: "\FFA2"; +} +.mdi-speedometer-slow::before { + content: "\FFA3"; +} +.mdi-spellcheck::before { + content: "\F4C6"; +} +.mdi-spider::before { + content: "\F0215"; +} +.mdi-spider-thread::before { + content: "\F0216"; +} +.mdi-spider-web::before { + content: "\FBA6"; +} +.mdi-spotify::before { + content: "\F4C7"; +} +.mdi-spotlight::before { + content: "\F4C8"; +} +.mdi-spotlight-beam::before { + content: "\F4C9"; +} +.mdi-spray::before { + content: "\F665"; +} +.mdi-spray-bottle::before { + content: "\FADF"; +} +.mdi-sprinkler::before { + content: "\F0081"; +} +.mdi-sprinkler-variant::before { + content: "\F0082"; +} +.mdi-sprout::before { + content: "\FE49"; +} +.mdi-sprout-outline::before { + content: "\FE4A"; +} +.mdi-square::before { + content: "\F763"; +} +.mdi-square-edit-outline::before { + content: "\F90B"; +} +.mdi-square-inc::before { + content: "\F4CA"; +} +.mdi-square-inc-cash::before { + content: "\F4CB"; +} +.mdi-square-medium::before { + content: "\FA12"; +} +.mdi-square-medium-outline::before { + content: "\FA13"; +} +.mdi-square-off::before { + content: "\F0319"; +} +.mdi-square-off-outline::before { + content: "\F031A"; +} +.mdi-square-outline::before { + content: "\F762"; +} +.mdi-square-root::before { + content: "\F783"; +} +.mdi-square-root-box::before { + content: "\F9A2"; +} +.mdi-square-small::before { + content: "\FA14"; +} +.mdi-squeegee::before { + content: "\FAE0"; +} +.mdi-ssh::before { + content: "\F8BF"; +} +.mdi-stack-exchange::before { + content: "\F60B"; +} +.mdi-stack-overflow::before { + content: "\F4CC"; +} +.mdi-stackpath::before { + content: "\F359"; +} +.mdi-stadium::before { + content: "\F001A"; +} +.mdi-stadium-variant::before { + content: "\F71F"; +} +.mdi-stairs::before { + content: "\F4CD"; +} +.mdi-stairs-down::before { + content: "\F02E9"; +} +.mdi-stairs-up::before { + content: "\F02E8"; +} +.mdi-stamper::before { + content: "\FD15"; +} +.mdi-standard-definition::before { + content: "\F7EE"; +} +.mdi-star::before { + content: "\F4CE"; +} +.mdi-star-box::before { + content: "\FA72"; +} +.mdi-star-box-multiple::before { + content: "\F02B1"; +} +.mdi-star-box-multiple-outline::before { + content: "\F02B2"; +} +.mdi-star-box-outline::before { + content: "\FA73"; +} +.mdi-star-circle::before { + content: "\F4CF"; +} +.mdi-star-circle-outline::before { + content: "\F9A3"; +} +.mdi-star-face::before { + content: "\F9A4"; +} +.mdi-star-four-points::before { + content: "\FAE1"; +} +.mdi-star-four-points-outline::before { + content: "\FAE2"; +} +.mdi-star-half::before { + content: "\F4D0"; +} +.mdi-star-off::before { + content: "\F4D1"; +} +.mdi-star-outline::before { + content: "\F4D2"; +} +.mdi-star-three-points::before { + content: "\FAE3"; +} +.mdi-star-three-points-outline::before { + content: "\FAE4"; +} +.mdi-state-machine::before { + content: "\F021A"; +} +.mdi-steam::before { + content: "\F4D3"; +} +.mdi-steam-box::before { + content: "\F90C"; +} +.mdi-steering::before { + content: "\F4D4"; +} +.mdi-steering-off::before { + content: "\F90D"; +} +.mdi-step-backward::before { + content: "\F4D5"; +} +.mdi-step-backward-2::before { + content: "\F4D6"; +} +.mdi-step-forward::before { + content: "\F4D7"; +} +.mdi-step-forward-2::before { + content: "\F4D8"; +} +.mdi-stethoscope::before { + content: "\F4D9"; +} +.mdi-sticker::before { + content: "\F038F"; +} +.mdi-sticker-alert::before { + content: "\F0390"; +} +.mdi-sticker-alert-outline::before { + content: "\F0391"; +} +.mdi-sticker-check::before { + content: "\F0392"; +} +.mdi-sticker-check-outline::before { + content: "\F0393"; +} +.mdi-sticker-circle-outline::before { + content: "\F5D0"; +} +.mdi-sticker-emoji::before { + content: "\F784"; +} +.mdi-sticker-minus::before { + content: "\F0394"; +} +.mdi-sticker-minus-outline::before { + content: "\F0395"; +} +.mdi-sticker-outline::before { + content: "\F0396"; +} +.mdi-sticker-plus::before { + content: "\F0397"; +} +.mdi-sticker-plus-outline::before { + content: "\F0398"; +} +.mdi-sticker-remove::before { + content: "\F0399"; +} +.mdi-sticker-remove-outline::before { + content: "\F039A"; +} +.mdi-stocking::before { + content: "\F4DA"; +} +.mdi-stomach::before { + content: "\F00BE"; +} +.mdi-stop::before { + content: "\F4DB"; +} +.mdi-stop-circle::before { + content: "\F666"; +} +.mdi-stop-circle-outline::before { + content: "\F667"; +} +.mdi-store::before { + content: "\F4DC"; +} +.mdi-store-24-hour::before { + content: "\F4DD"; +} +.mdi-store-outline::before { + content: "\F038C"; +} +.mdi-storefront::before { + content: "\F00EC"; +} +.mdi-stove::before { + content: "\F4DE"; +} +.mdi-strategy::before { + content: "\F0201"; +} +.mdi-strava::before { + content: "\FB25"; +} +.mdi-stretch-to-page::before { + content: "\FF48"; +} +.mdi-stretch-to-page-outline::before { + content: "\FF49"; +} +.mdi-string-lights::before { + content: "\F02E5"; +} +.mdi-string-lights-off::before { + content: "\F02E6"; +} +.mdi-subdirectory-arrow-left::before { + content: "\F60C"; +} +.mdi-subdirectory-arrow-right::before { + content: "\F60D"; +} +.mdi-subtitles::before { + content: "\FA15"; +} +.mdi-subtitles-outline::before { + content: "\FA16"; +} +.mdi-subway::before { + content: "\F6AB"; +} +.mdi-subway-alert-variant::before { + content: "\FD79"; +} +.mdi-subway-variant::before { + content: "\F4DF"; +} +.mdi-summit::before { + content: "\F785"; +} +.mdi-sunglasses::before { + content: "\F4E0"; +} +.mdi-surround-sound::before { + content: "\F5C5"; +} +.mdi-surround-sound-2-0::before { + content: "\F7EF"; +} +.mdi-surround-sound-3-1::before { + content: "\F7F0"; +} +.mdi-surround-sound-5-1::before { + content: "\F7F1"; +} +.mdi-surround-sound-7-1::before { + content: "\F7F2"; +} +.mdi-svg::before { + content: "\F720"; +} +.mdi-swap-horizontal::before { + content: "\F4E1"; +} +.mdi-swap-horizontal-bold::before { + content: "\FBA9"; +} +.mdi-swap-horizontal-circle::before { + content: "\F0002"; +} +.mdi-swap-horizontal-circle-outline::before { + content: "\F0003"; +} +.mdi-swap-horizontal-variant::before { + content: "\F8C0"; +} +.mdi-swap-vertical::before { + content: "\F4E2"; +} +.mdi-swap-vertical-bold::before { + content: "\FBAA"; +} +.mdi-swap-vertical-circle::before { + content: "\F0004"; +} +.mdi-swap-vertical-circle-outline::before { + content: "\F0005"; +} +.mdi-swap-vertical-variant::before { + content: "\F8C1"; +} +.mdi-swim::before { + content: "\F4E3"; +} +.mdi-switch::before { + content: "\F4E4"; +} +.mdi-sword::before { + content: "\F4E5"; +} +.mdi-sword-cross::before { + content: "\F786"; +} +.mdi-syllabary-hangul::before { + content: "\F035E"; +} +.mdi-syllabary-hiragana::before { + content: "\F035F"; +} +.mdi-syllabary-katakana::before { + content: "\F0360"; +} +.mdi-syllabary-katakana-half-width::before { + content: "\F0361"; +} +.mdi-symfony::before { + content: "\FAE5"; +} +.mdi-sync::before { + content: "\F4E6"; +} +.mdi-sync-alert::before { + content: "\F4E7"; +} +.mdi-sync-circle::before { + content: "\F03A3"; +} +.mdi-sync-off::before { + content: "\F4E8"; +} +.mdi-tab::before { + content: "\F4E9"; +} +.mdi-tab-minus::before { + content: "\FB26"; +} +.mdi-tab-plus::before { + content: "\F75B"; +} +.mdi-tab-remove::before { + content: "\FB27"; +} +.mdi-tab-unselected::before { + content: "\F4EA"; +} +.mdi-table::before { + content: "\F4EB"; +} +.mdi-table-border::before { + content: "\FA17"; +} +.mdi-table-chair::before { + content: "\F0083"; +} +.mdi-table-column::before { + content: "\F834"; +} +.mdi-table-column-plus-after::before { + content: "\F4EC"; +} +.mdi-table-column-plus-before::before { + content: "\F4ED"; +} +.mdi-table-column-remove::before { + content: "\F4EE"; +} +.mdi-table-column-width::before { + content: "\F4EF"; +} +.mdi-table-edit::before { + content: "\F4F0"; +} +.mdi-table-eye::before { + content: "\F00BF"; +} +.mdi-table-headers-eye::before { + content: "\F0248"; +} +.mdi-table-headers-eye-off::before { + content: "\F0249"; +} +.mdi-table-large::before { + content: "\F4F1"; +} +.mdi-table-large-plus::before { + content: "\FFA4"; +} +.mdi-table-large-remove::before { + content: "\FFA5"; +} +.mdi-table-merge-cells::before { + content: "\F9A5"; +} +.mdi-table-of-contents::before { + content: "\F835"; +} +.mdi-table-plus::before { + content: "\FA74"; +} +.mdi-table-remove::before { + content: "\FA75"; +} +.mdi-table-row::before { + content: "\F836"; +} +.mdi-table-row-height::before { + content: "\F4F2"; +} +.mdi-table-row-plus-after::before { + content: "\F4F3"; +} +.mdi-table-row-plus-before::before { + content: "\F4F4"; +} +.mdi-table-row-remove::before { + content: "\F4F5"; +} +.mdi-table-search::before { + content: "\F90E"; +} +.mdi-table-settings::before { + content: "\F837"; +} +.mdi-table-tennis::before { + content: "\FE4B"; +} +.mdi-tablet::before { + content: "\F4F6"; +} +.mdi-tablet-android::before { + content: "\F4F7"; +} +.mdi-tablet-cellphone::before { + content: "\F9A6"; +} +.mdi-tablet-dashboard::before { + content: "\FEEB"; +} +.mdi-tablet-ipad::before { + content: "\F4F8"; +} +.mdi-taco::before { + content: "\F761"; +} +.mdi-tag::before { + content: "\F4F9"; +} +.mdi-tag-faces::before { + content: "\F4FA"; +} +.mdi-tag-heart::before { + content: "\F68A"; +} +.mdi-tag-heart-outline::before { + content: "\FBAB"; +} +.mdi-tag-minus::before { + content: "\F90F"; +} +.mdi-tag-minus-outline::before { + content: "\F024A"; +} +.mdi-tag-multiple::before { + content: "\F4FB"; +} +.mdi-tag-multiple-outline::before { + content: "\F0322"; +} +.mdi-tag-off::before { + content: "\F024B"; +} +.mdi-tag-off-outline::before { + content: "\F024C"; +} +.mdi-tag-outline::before { + content: "\F4FC"; +} +.mdi-tag-plus::before { + content: "\F721"; +} +.mdi-tag-plus-outline::before { + content: "\F024D"; +} +.mdi-tag-remove::before { + content: "\F722"; +} +.mdi-tag-remove-outline::before { + content: "\F024E"; +} +.mdi-tag-text::before { + content: "\F024F"; +} +.mdi-tag-text-outline::before { + content: "\F4FD"; +} +.mdi-tank::before { + content: "\FD16"; +} +.mdi-tanker-truck::before { + content: "\F0006"; +} +.mdi-tape-measure::before { + content: "\FB28"; +} +.mdi-target::before { + content: "\F4FE"; +} +.mdi-target-account::before { + content: "\FBAC"; +} +.mdi-target-variant::before { + content: "\FA76"; +} +.mdi-taxi::before { + content: "\F4FF"; +} +.mdi-tea::before { + content: "\FD7A"; +} +.mdi-tea-outline::before { + content: "\FD7B"; +} +.mdi-teach::before { + content: "\F88F"; +} +.mdi-teamviewer::before { + content: "\F500"; +} +.mdi-telegram::before { + content: "\F501"; +} +.mdi-telescope::before { + content: "\FB29"; +} +.mdi-television::before { + content: "\F502"; +} +.mdi-television-ambient-light::before { + content: "\F0381"; +} +.mdi-television-box::before { + content: "\F838"; +} +.mdi-television-classic::before { + content: "\F7F3"; +} +.mdi-television-classic-off::before { + content: "\F839"; +} +.mdi-television-clean::before { + content: "\F013B"; +} +.mdi-television-guide::before { + content: "\F503"; +} +.mdi-television-off::before { + content: "\F83A"; +} +.mdi-television-pause::before { + content: "\FFA6"; +} +.mdi-television-play::before { + content: "\FEEC"; +} +.mdi-television-stop::before { + content: "\FFA7"; +} +.mdi-temperature-celsius::before { + content: "\F504"; +} +.mdi-temperature-fahrenheit::before { + content: "\F505"; +} +.mdi-temperature-kelvin::before { + content: "\F506"; +} +.mdi-tennis::before { + content: "\FD7C"; +} +.mdi-tennis-ball::before { + content: "\F507"; +} +.mdi-tent::before { + content: "\F508"; +} +.mdi-terraform::before { + content: "\F0084"; +} +.mdi-terrain::before { + content: "\F509"; +} +.mdi-test-tube::before { + content: "\F668"; +} +.mdi-test-tube-empty::before { + content: "\F910"; +} +.mdi-test-tube-off::before { + content: "\F911"; +} +.mdi-text::before { + content: "\F9A7"; +} +.mdi-text-recognition::before { + content: "\F0168"; +} +.mdi-text-shadow::before { + content: "\F669"; +} +.mdi-text-short::before { + content: "\F9A8"; +} +.mdi-text-subject::before { + content: "\F9A9"; +} +.mdi-text-to-speech::before { + content: "\F50A"; +} +.mdi-text-to-speech-off::before { + content: "\F50B"; +} +.mdi-textarea::before { + content: "\F00C0"; +} +.mdi-textbox::before { + content: "\F60E"; +} +.mdi-textbox-lock::before { + content: "\F0388"; +} +.mdi-textbox-password::before { + content: "\F7F4"; +} +.mdi-texture::before { + content: "\F50C"; +} +.mdi-texture-box::before { + content: "\F0007"; +} +.mdi-theater::before { + content: "\F50D"; +} +.mdi-theme-light-dark::before { + content: "\F50E"; +} +.mdi-thermometer::before { + content: "\F50F"; +} +.mdi-thermometer-alert::before { + content: "\FE61"; +} +.mdi-thermometer-chevron-down::before { + content: "\FE62"; +} +.mdi-thermometer-chevron-up::before { + content: "\FE63"; +} +.mdi-thermometer-high::before { + content: "\F00ED"; +} +.mdi-thermometer-lines::before { + content: "\F510"; +} +.mdi-thermometer-low::before { + content: "\F00EE"; +} +.mdi-thermometer-minus::before { + content: "\FE64"; +} +.mdi-thermometer-plus::before { + content: "\FE65"; +} +.mdi-thermostat::before { + content: "\F393"; +} +.mdi-thermostat-box::before { + content: "\F890"; +} +.mdi-thought-bubble::before { + content: "\F7F5"; +} +.mdi-thought-bubble-outline::before { + content: "\F7F6"; +} +.mdi-thumb-down::before { + content: "\F511"; +} +.mdi-thumb-down-outline::before { + content: "\F512"; +} +.mdi-thumb-up::before { + content: "\F513"; +} +.mdi-thumb-up-outline::before { + content: "\F514"; +} +.mdi-thumbs-up-down::before { + content: "\F515"; +} +.mdi-ticket::before { + content: "\F516"; +} +.mdi-ticket-account::before { + content: "\F517"; +} +.mdi-ticket-confirmation::before { + content: "\F518"; +} +.mdi-ticket-outline::before { + content: "\F912"; +} +.mdi-ticket-percent::before { + content: "\F723"; +} +.mdi-tie::before { + content: "\F519"; +} +.mdi-tilde::before { + content: "\F724"; +} +.mdi-timelapse::before { + content: "\F51A"; +} +.mdi-timeline::before { + content: "\FBAD"; +} +.mdi-timeline-alert::before { + content: "\FFB2"; +} +.mdi-timeline-alert-outline::before { + content: "\FFB5"; +} +.mdi-timeline-clock::before { + content: "\F0226"; +} +.mdi-timeline-clock-outline::before { + content: "\F0227"; +} +.mdi-timeline-help::before { + content: "\FFB6"; +} +.mdi-timeline-help-outline::before { + content: "\FFB7"; +} +.mdi-timeline-outline::before { + content: "\FBAE"; +} +.mdi-timeline-plus::before { + content: "\FFB3"; +} +.mdi-timeline-plus-outline::before { + content: "\FFB4"; +} +.mdi-timeline-text::before { + content: "\FBAF"; +} +.mdi-timeline-text-outline::before { + content: "\FBB0"; +} +.mdi-timer::before { + content: "\F51B"; +} +.mdi-timer-10::before { + content: "\F51C"; +} +.mdi-timer-3::before { + content: "\F51D"; +} +.mdi-timer-off::before { + content: "\F51E"; +} +.mdi-timer-sand::before { + content: "\F51F"; +} +.mdi-timer-sand-empty::before { + content: "\F6AC"; +} +.mdi-timer-sand-full::before { + content: "\F78B"; +} +.mdi-timetable::before { + content: "\F520"; +} +.mdi-toaster::before { + content: "\F0085"; +} +.mdi-toaster-off::before { + content: "\F01E2"; +} +.mdi-toaster-oven::before { + content: "\FCAF"; +} +.mdi-toggle-switch::before { + content: "\F521"; +} +.mdi-toggle-switch-off::before { + content: "\F522"; +} +.mdi-toggle-switch-off-outline::before { + content: "\FA18"; +} +.mdi-toggle-switch-outline::before { + content: "\FA19"; +} +.mdi-toilet::before { + content: "\F9AA"; +} +.mdi-toolbox::before { + content: "\F9AB"; +} +.mdi-toolbox-outline::before { + content: "\F9AC"; +} +.mdi-tools::before { + content: "\F0086"; +} +.mdi-tooltip::before { + content: "\F523"; +} +.mdi-tooltip-account::before { + content: "\F00C"; +} +.mdi-tooltip-edit::before { + content: "\F524"; +} +.mdi-tooltip-edit-outline::before { + content: "\F02F0"; +} +.mdi-tooltip-image::before { + content: "\F525"; +} +.mdi-tooltip-image-outline::before { + content: "\FBB1"; +} +.mdi-tooltip-outline::before { + content: "\F526"; +} +.mdi-tooltip-plus::before { + content: "\FBB2"; +} +.mdi-tooltip-plus-outline::before { + content: "\F527"; +} +.mdi-tooltip-text::before { + content: "\F528"; +} +.mdi-tooltip-text-outline::before { + content: "\FBB3"; +} +.mdi-tooth::before { + content: "\F8C2"; +} +.mdi-tooth-outline::before { + content: "\F529"; +} +.mdi-toothbrush::before { + content: "\F0154"; +} +.mdi-toothbrush-electric::before { + content: "\F0157"; +} +.mdi-toothbrush-paste::before { + content: "\F0155"; +} +.mdi-tor::before { + content: "\F52A"; +} +.mdi-tortoise::before { + content: "\FD17"; +} +.mdi-toslink::before { + content: "\F02E3"; +} +.mdi-tournament::before { + content: "\F9AD"; +} +.mdi-tower-beach::before { + content: "\F680"; +} +.mdi-tower-fire::before { + content: "\F681"; +} +.mdi-towing::before { + content: "\F83B"; +} +.mdi-toy-brick::before { + content: "\F02B3"; +} +.mdi-toy-brick-marker::before { + content: "\F02B4"; +} +.mdi-toy-brick-marker-outline::before { + content: "\F02B5"; +} +.mdi-toy-brick-minus::before { + content: "\F02B6"; +} +.mdi-toy-brick-minus-outline::before { + content: "\F02B7"; +} +.mdi-toy-brick-outline::before { + content: "\F02B8"; +} +.mdi-toy-brick-plus::before { + content: "\F02B9"; +} +.mdi-toy-brick-plus-outline::before { + content: "\F02BA"; +} +.mdi-toy-brick-remove::before { + content: "\F02BB"; +} +.mdi-toy-brick-remove-outline::before { + content: "\F02BC"; +} +.mdi-toy-brick-search::before { + content: "\F02BD"; +} +.mdi-toy-brick-search-outline::before { + content: "\F02BE"; +} +.mdi-track-light::before { + content: "\F913"; +} +.mdi-trackpad::before { + content: "\F7F7"; +} +.mdi-trackpad-lock::before { + content: "\F932"; +} +.mdi-tractor::before { + content: "\F891"; +} +.mdi-trademark::before { + content: "\FA77"; +} +.mdi-traffic-cone::before { + content: "\F03A7"; +} +.mdi-traffic-light::before { + content: "\F52B"; +} +.mdi-train::before { + content: "\F52C"; +} +.mdi-train-car::before { + content: "\FBB4"; +} +.mdi-train-variant::before { + content: "\F8C3"; +} +.mdi-tram::before { + content: "\F52D"; +} +.mdi-tram-side::before { + content: "\F0008"; +} +.mdi-transcribe::before { + content: "\F52E"; +} +.mdi-transcribe-close::before { + content: "\F52F"; +} +.mdi-transfer::before { + content: "\F0087"; +} +.mdi-transfer-down::before { + content: "\FD7D"; +} +.mdi-transfer-left::before { + content: "\FD7E"; +} +.mdi-transfer-right::before { + content: "\F530"; +} +.mdi-transfer-up::before { + content: "\FD7F"; +} +.mdi-transit-connection::before { + content: "\FD18"; +} +.mdi-transit-connection-variant::before { + content: "\FD19"; +} +.mdi-transit-detour::before { + content: "\FFA8"; +} +.mdi-transit-transfer::before { + content: "\F6AD"; +} +.mdi-transition::before { + content: "\F914"; +} +.mdi-transition-masked::before { + content: "\F915"; +} +.mdi-translate::before { + content: "\F5CA"; +} +.mdi-translate-off::before { + content: "\FE66"; +} +.mdi-transmission-tower::before { + content: "\FD1A"; +} +.mdi-trash-can::before { + content: "\FA78"; +} +.mdi-trash-can-outline::before { + content: "\FA79"; +} +.mdi-tray::before { + content: "\F02BF"; +} +.mdi-tray-alert::before { + content: "\F02C0"; +} +.mdi-tray-full::before { + content: "\F02C1"; +} +.mdi-tray-minus::before { + content: "\F02C2"; +} +.mdi-tray-plus::before { + content: "\F02C3"; +} +.mdi-tray-remove::before { + content: "\F02C4"; +} +.mdi-treasure-chest::before { + content: "\F725"; +} +.mdi-tree::before { + content: "\F531"; +} +.mdi-tree-outline::before { + content: "\FE4C"; +} +.mdi-trello::before { + content: "\F532"; +} +.mdi-trending-down::before { + content: "\F533"; +} +.mdi-trending-neutral::before { + content: "\F534"; +} +.mdi-trending-up::before { + content: "\F535"; +} +.mdi-triangle::before { + content: "\F536"; +} +.mdi-triangle-outline::before { + content: "\F537"; +} +.mdi-triforce::before { + content: "\FBB5"; +} +.mdi-trophy::before { + content: "\F538"; +} +.mdi-trophy-award::before { + content: "\F539"; +} +.mdi-trophy-broken::before { + content: "\FD80"; +} +.mdi-trophy-outline::before { + content: "\F53A"; +} +.mdi-trophy-variant::before { + content: "\F53B"; +} +.mdi-trophy-variant-outline::before { + content: "\F53C"; +} +.mdi-truck::before { + content: "\F53D"; +} +.mdi-truck-check::before { + content: "\FCB0"; +} +.mdi-truck-check-outline::before { + content: "\F02C5"; +} +.mdi-truck-delivery::before { + content: "\F53E"; +} +.mdi-truck-delivery-outline::before { + content: "\F02C6"; +} +.mdi-truck-fast::before { + content: "\F787"; +} +.mdi-truck-fast-outline::before { + content: "\F02C7"; +} +.mdi-truck-outline::before { + content: "\F02C8"; +} +.mdi-truck-trailer::before { + content: "\F726"; +} +.mdi-trumpet::before { + content: "\F00C1"; +} +.mdi-tshirt-crew::before { + content: "\FA7A"; +} +.mdi-tshirt-crew-outline::before { + content: "\F53F"; +} +.mdi-tshirt-v::before { + content: "\FA7B"; +} +.mdi-tshirt-v-outline::before { + content: "\F540"; +} +.mdi-tumble-dryer::before { + content: "\F916"; +} +.mdi-tumble-dryer-alert::before { + content: "\F01E5"; +} +.mdi-tumble-dryer-off::before { + content: "\F01E6"; +} +.mdi-tumblr::before { + content: "\F541"; +} +.mdi-tumblr-box::before { + content: "\F917"; +} +.mdi-tumblr-reblog::before { + content: "\F542"; +} +.mdi-tune::before { + content: "\F62E"; +} +.mdi-tune-vertical::before { + content: "\F66A"; +} +.mdi-turnstile::before { + content: "\FCB1"; +} +.mdi-turnstile-outline::before { + content: "\FCB2"; +} +.mdi-turtle::before { + content: "\FCB3"; +} +.mdi-twitch::before { + content: "\F543"; +} +.mdi-twitter::before { + content: "\F544"; +} +.mdi-twitter-box::before { + content: "\F545"; +} +.mdi-twitter-circle::before { + content: "\F546"; +} +.mdi-twitter-retweet::before { + content: "\F547"; +} +.mdi-two-factor-authentication::before { + content: "\F9AE"; +} +.mdi-typewriter::before { + content: "\FF4A"; +} +.mdi-uber::before { + content: "\F748"; +} +.mdi-ubisoft::before { + content: "\FBB6"; +} +.mdi-ubuntu::before { + content: "\F548"; +} +.mdi-ufo::before { + content: "\F00EF"; +} +.mdi-ufo-outline::before { + content: "\F00F0"; +} +.mdi-ultra-high-definition::before { + content: "\F7F8"; +} +.mdi-umbraco::before { + content: "\F549"; +} +.mdi-umbrella::before { + content: "\F54A"; +} +.mdi-umbrella-closed::before { + content: "\F9AF"; +} +.mdi-umbrella-outline::before { + content: "\F54B"; +} +.mdi-undo::before { + content: "\F54C"; +} +.mdi-undo-variant::before { + content: "\F54D"; +} +.mdi-unfold-less-horizontal::before { + content: "\F54E"; +} +.mdi-unfold-less-vertical::before { + content: "\F75F"; +} +.mdi-unfold-more-horizontal::before { + content: "\F54F"; +} +.mdi-unfold-more-vertical::before { + content: "\F760"; +} +.mdi-ungroup::before { + content: "\F550"; +} +.mdi-unicode::before { + content: "\FEED"; +} +.mdi-unity::before { + content: "\F6AE"; +} +.mdi-unreal::before { + content: "\F9B0"; +} +.mdi-untappd::before { + content: "\F551"; +} +.mdi-update::before { + content: "\F6AF"; +} +.mdi-upload::before { + content: "\F552"; +} +.mdi-upload-lock::before { + content: "\F039E"; +} +.mdi-upload-lock-outline::before { + content: "\F039F"; +} +.mdi-upload-multiple::before { + content: "\F83C"; +} +.mdi-upload-network::before { + content: "\F6F5"; +} +.mdi-upload-network-outline::before { + content: "\FCB4"; +} +.mdi-upload-off::before { + content: "\F00F1"; +} +.mdi-upload-off-outline::before { + content: "\F00F2"; +} +.mdi-upload-outline::before { + content: "\FE67"; +} +.mdi-usb::before { + content: "\F553"; +} +.mdi-usb-flash-drive::before { + content: "\F02C9"; +} +.mdi-usb-flash-drive-outline::before { + content: "\F02CA"; +} +.mdi-usb-port::before { + content: "\F021B"; +} +.mdi-valve::before { + content: "\F0088"; +} +.mdi-valve-closed::before { + content: "\F0089"; +} +.mdi-valve-open::before { + content: "\F008A"; +} +.mdi-van-passenger::before { + content: "\F7F9"; +} +.mdi-van-utility::before { + content: "\F7FA"; +} +.mdi-vanish::before { + content: "\F7FB"; +} +.mdi-vanity-light::before { + content: "\F020C"; +} +.mdi-variable::before { + content: "\FAE6"; +} +.mdi-variable-box::before { + content: "\F013C"; +} +.mdi-vector-arrange-above::before { + content: "\F554"; +} +.mdi-vector-arrange-below::before { + content: "\F555"; +} +.mdi-vector-bezier::before { + content: "\FAE7"; +} +.mdi-vector-circle::before { + content: "\F556"; +} +.mdi-vector-circle-variant::before { + content: "\F557"; +} +.mdi-vector-combine::before { + content: "\F558"; +} +.mdi-vector-curve::before { + content: "\F559"; +} +.mdi-vector-difference::before { + content: "\F55A"; +} +.mdi-vector-difference-ab::before { + content: "\F55B"; +} +.mdi-vector-difference-ba::before { + content: "\F55C"; +} +.mdi-vector-ellipse::before { + content: "\F892"; +} +.mdi-vector-intersection::before { + content: "\F55D"; +} +.mdi-vector-line::before { + content: "\F55E"; +} +.mdi-vector-link::before { + content: "\F0009"; +} +.mdi-vector-point::before { + content: "\F55F"; +} +.mdi-vector-polygon::before { + content: "\F560"; +} +.mdi-vector-polyline::before { + content: "\F561"; +} +.mdi-vector-polyline-edit::before { + content: "\F0250"; +} +.mdi-vector-polyline-minus::before { + content: "\F0251"; +} +.mdi-vector-polyline-plus::before { + content: "\F0252"; +} +.mdi-vector-polyline-remove::before { + content: "\F0253"; +} +.mdi-vector-radius::before { + content: "\F749"; +} +.mdi-vector-rectangle::before { + content: "\F5C6"; +} +.mdi-vector-selection::before { + content: "\F562"; +} +.mdi-vector-square::before { + content: "\F001"; +} +.mdi-vector-triangle::before { + content: "\F563"; +} +.mdi-vector-union::before { + content: "\F564"; +} +.mdi-venmo::before { + content: "\F578"; +} +.mdi-vhs::before { + content: "\FA1A"; +} +.mdi-vibrate::before { + content: "\F566"; +} +.mdi-vibrate-off::before { + content: "\FCB5"; +} +.mdi-video::before { + content: "\F567"; +} +.mdi-video-3d::before { + content: "\F7FC"; +} +.mdi-video-3d-variant::before { + content: "\FEEE"; +} +.mdi-video-4k-box::before { + content: "\F83D"; +} +.mdi-video-account::before { + content: "\F918"; +} +.mdi-video-check::before { + content: "\F008B"; +} +.mdi-video-check-outline::before { + content: "\F008C"; +} +.mdi-video-image::before { + content: "\F919"; +} +.mdi-video-input-antenna::before { + content: "\F83E"; +} +.mdi-video-input-component::before { + content: "\F83F"; +} +.mdi-video-input-hdmi::before { + content: "\F840"; +} +.mdi-video-input-scart::before { + content: "\FFA9"; +} +.mdi-video-input-svideo::before { + content: "\F841"; +} +.mdi-video-minus::before { + content: "\F9B1"; +} +.mdi-video-off::before { + content: "\F568"; +} +.mdi-video-off-outline::before { + content: "\FBB7"; +} +.mdi-video-outline::before { + content: "\FBB8"; +} +.mdi-video-plus::before { + content: "\F9B2"; +} +.mdi-video-stabilization::before { + content: "\F91A"; +} +.mdi-video-switch::before { + content: "\F569"; +} +.mdi-video-vintage::before { + content: "\FA1B"; +} +.mdi-video-wireless::before { + content: "\FEEF"; +} +.mdi-video-wireless-outline::before { + content: "\FEF0"; +} +.mdi-view-agenda::before { + content: "\F56A"; +} +.mdi-view-agenda-outline::before { + content: "\F0203"; +} +.mdi-view-array::before { + content: "\F56B"; +} +.mdi-view-carousel::before { + content: "\F56C"; +} +.mdi-view-column::before { + content: "\F56D"; +} +.mdi-view-comfy::before { + content: "\FE4D"; +} +.mdi-view-compact::before { + content: "\FE4E"; +} +.mdi-view-compact-outline::before { + content: "\FE4F"; +} +.mdi-view-dashboard::before { + content: "\F56E"; +} +.mdi-view-dashboard-outline::before { + content: "\FA1C"; +} +.mdi-view-dashboard-variant::before { + content: "\F842"; +} +.mdi-view-day::before { + content: "\F56F"; +} +.mdi-view-grid::before { + content: "\F570"; +} +.mdi-view-grid-outline::before { + content: "\F0204"; +} +.mdi-view-grid-plus::before { + content: "\FFAA"; +} +.mdi-view-grid-plus-outline::before { + content: "\F0205"; +} +.mdi-view-headline::before { + content: "\F571"; +} +.mdi-view-list::before { + content: "\F572"; +} +.mdi-view-module::before { + content: "\F573"; +} +.mdi-view-parallel::before { + content: "\F727"; +} +.mdi-view-quilt::before { + content: "\F574"; +} +.mdi-view-sequential::before { + content: "\F728"; +} +.mdi-view-split-horizontal::before { + content: "\FBA7"; +} +.mdi-view-split-vertical::before { + content: "\FBA8"; +} +.mdi-view-stream::before { + content: "\F575"; +} +.mdi-view-week::before { + content: "\F576"; +} +.mdi-vimeo::before { + content: "\F577"; +} +.mdi-violin::before { + content: "\F60F"; +} +.mdi-virtual-reality::before { + content: "\F893"; +} +.mdi-visual-studio::before { + content: "\F610"; +} +.mdi-visual-studio-code::before { + content: "\FA1D"; +} +.mdi-vk::before { + content: "\F579"; +} +.mdi-vk-box::before { + content: "\F57A"; +} +.mdi-vk-circle::before { + content: "\F57B"; +} +.mdi-vlc::before { + content: "\F57C"; +} +.mdi-voice::before { + content: "\F5CB"; +} +.mdi-voice-off::before { + content: "\FEF1"; +} +.mdi-voicemail::before { + content: "\F57D"; +} +.mdi-volleyball::before { + content: "\F9B3"; +} +.mdi-volume-high::before { + content: "\F57E"; +} +.mdi-volume-low::before { + content: "\F57F"; +} +.mdi-volume-medium::before { + content: "\F580"; +} +.mdi-volume-minus::before { + content: "\F75D"; +} +.mdi-volume-mute::before { + content: "\F75E"; +} +.mdi-volume-off::before { + content: "\F581"; +} +.mdi-volume-plus::before { + content: "\F75C"; +} +.mdi-volume-source::before { + content: "\F014B"; +} +.mdi-volume-variant-off::before { + content: "\FE68"; +} +.mdi-volume-vibrate::before { + content: "\F014C"; +} +.mdi-vote::before { + content: "\FA1E"; +} +.mdi-vote-outline::before { + content: "\FA1F"; +} +.mdi-vpn::before { + content: "\F582"; +} +.mdi-vuejs::before { + content: "\F843"; +} +.mdi-vuetify::before { + content: "\FE50"; +} +.mdi-walk::before { + content: "\F583"; +} +.mdi-wall::before { + content: "\F7FD"; +} +.mdi-wall-sconce::before { + content: "\F91B"; +} +.mdi-wall-sconce-flat::before { + content: "\F91C"; +} +.mdi-wall-sconce-variant::before { + content: "\F91D"; +} +.mdi-wallet::before { + content: "\F584"; +} +.mdi-wallet-giftcard::before { + content: "\F585"; +} +.mdi-wallet-membership::before { + content: "\F586"; +} +.mdi-wallet-outline::before { + content: "\FBB9"; +} +.mdi-wallet-plus::before { + content: "\FFAB"; +} +.mdi-wallet-plus-outline::before { + content: "\FFAC"; +} +.mdi-wallet-travel::before { + content: "\F587"; +} +.mdi-wallpaper::before { + content: "\FE69"; +} +.mdi-wan::before { + content: "\F588"; +} +.mdi-wardrobe::before { + content: "\FFAD"; +} +.mdi-wardrobe-outline::before { + content: "\FFAE"; +} +.mdi-warehouse::before { + content: "\FFBB"; +} +.mdi-washing-machine::before { + content: "\F729"; +} +.mdi-washing-machine-alert::before { + content: "\F01E7"; +} +.mdi-washing-machine-off::before { + content: "\F01E8"; +} +.mdi-watch::before { + content: "\F589"; +} +.mdi-watch-export::before { + content: "\F58A"; +} +.mdi-watch-export-variant::before { + content: "\F894"; +} +.mdi-watch-import::before { + content: "\F58B"; +} +.mdi-watch-import-variant::before { + content: "\F895"; +} +.mdi-watch-variant::before { + content: "\F896"; +} +.mdi-watch-vibrate::before { + content: "\F6B0"; +} +.mdi-watch-vibrate-off::before { + content: "\FCB6"; +} +.mdi-water::before { + content: "\F58C"; +} +.mdi-water-boiler::before { + content: "\FFAF"; +} +.mdi-water-boiler-alert::before { + content: "\F01DE"; +} +.mdi-water-boiler-off::before { + content: "\F01DF"; +} +.mdi-water-off::before { + content: "\F58D"; +} +.mdi-water-outline::before { + content: "\FE6A"; +} +.mdi-water-percent::before { + content: "\F58E"; +} +.mdi-water-polo::before { + content: "\F02CB"; +} +.mdi-water-pump::before { + content: "\F58F"; +} +.mdi-water-pump-off::before { + content: "\FFB0"; +} +.mdi-water-well::before { + content: "\F008D"; +} +.mdi-water-well-outline::before { + content: "\F008E"; +} +.mdi-watermark::before { + content: "\F612"; +} +.mdi-wave::before { + content: "\FF4B"; +} +.mdi-waves::before { + content: "\F78C"; +} +.mdi-waze::before { + content: "\FBBA"; +} +.mdi-weather-cloudy::before { + content: "\F590"; +} +.mdi-weather-cloudy-alert::before { + content: "\FF4C"; +} +.mdi-weather-cloudy-arrow-right::before { + content: "\FE51"; +} +.mdi-weather-fog::before { + content: "\F591"; +} +.mdi-weather-hail::before { + content: "\F592"; +} +.mdi-weather-hazy::before { + content: "\FF4D"; +} +.mdi-weather-hurricane::before { + content: "\F897"; +} +.mdi-weather-lightning::before { + content: "\F593"; +} +.mdi-weather-lightning-rainy::before { + content: "\F67D"; +} +.mdi-weather-night::before { + content: "\F594"; +} +.mdi-weather-night-partly-cloudy::before { + content: "\FF4E"; +} +.mdi-weather-partly-cloudy::before { + content: "\F595"; +} +.mdi-weather-partly-lightning::before { + content: "\FF4F"; +} +.mdi-weather-partly-rainy::before { + content: "\FF50"; +} +.mdi-weather-partly-snowy::before { + content: "\FF51"; +} +.mdi-weather-partly-snowy-rainy::before { + content: "\FF52"; +} +.mdi-weather-pouring::before { + content: "\F596"; +} +.mdi-weather-rainy::before { + content: "\F597"; +} +.mdi-weather-snowy::before { + content: "\F598"; +} +.mdi-weather-snowy-heavy::before { + content: "\FF53"; +} +.mdi-weather-snowy-rainy::before { + content: "\F67E"; +} +.mdi-weather-sunny::before { + content: "\F599"; +} +.mdi-weather-sunny-alert::before { + content: "\FF54"; +} +.mdi-weather-sunset::before { + content: "\F59A"; +} +.mdi-weather-sunset-down::before { + content: "\F59B"; +} +.mdi-weather-sunset-up::before { + content: "\F59C"; +} +.mdi-weather-tornado::before { + content: "\FF55"; +} +.mdi-weather-windy::before { + content: "\F59D"; +} +.mdi-weather-windy-variant::before { + content: "\F59E"; +} +.mdi-web::before { + content: "\F59F"; +} +.mdi-web-box::before { + content: "\FFB1"; +} +.mdi-web-clock::before { + content: "\F0275"; +} +.mdi-webcam::before { + content: "\F5A0"; +} +.mdi-webhook::before { + content: "\F62F"; +} +.mdi-webpack::before { + content: "\F72A"; +} +.mdi-webrtc::before { + content: "\F0273"; +} +.mdi-wechat::before { + content: "\F611"; +} +.mdi-weight::before { + content: "\F5A1"; +} +.mdi-weight-gram::before { + content: "\FD1B"; +} +.mdi-weight-kilogram::before { + content: "\F5A2"; +} +.mdi-weight-lifter::before { + content: "\F0188"; +} +.mdi-weight-pound::before { + content: "\F9B4"; +} +.mdi-whatsapp::before { + content: "\F5A3"; +} +.mdi-wheelchair-accessibility::before { + content: "\F5A4"; +} +.mdi-whistle::before { + content: "\F9B5"; +} +.mdi-whistle-outline::before { + content: "\F02E7"; +} +.mdi-white-balance-auto::before { + content: "\F5A5"; +} +.mdi-white-balance-incandescent::before { + content: "\F5A6"; +} +.mdi-white-balance-iridescent::before { + content: "\F5A7"; +} +.mdi-white-balance-sunny::before { + content: "\F5A8"; +} +.mdi-widgets::before { + content: "\F72B"; +} +.mdi-widgets-outline::before { + content: "\F0380"; +} +.mdi-wifi::before { + content: "\F5A9"; +} +.mdi-wifi-off::before { + content: "\F5AA"; +} +.mdi-wifi-star::before { + content: "\FE6B"; +} +.mdi-wifi-strength-1::before { + content: "\F91E"; +} +.mdi-wifi-strength-1-alert::before { + content: "\F91F"; +} +.mdi-wifi-strength-1-lock::before { + content: "\F920"; +} +.mdi-wifi-strength-2::before { + content: "\F921"; +} +.mdi-wifi-strength-2-alert::before { + content: "\F922"; +} +.mdi-wifi-strength-2-lock::before { + content: "\F923"; +} +.mdi-wifi-strength-3::before { + content: "\F924"; +} +.mdi-wifi-strength-3-alert::before { + content: "\F925"; +} +.mdi-wifi-strength-3-lock::before { + content: "\F926"; +} +.mdi-wifi-strength-4::before { + content: "\F927"; +} +.mdi-wifi-strength-4-alert::before { + content: "\F928"; +} +.mdi-wifi-strength-4-lock::before { + content: "\F929"; +} +.mdi-wifi-strength-alert-outline::before { + content: "\F92A"; +} +.mdi-wifi-strength-lock-outline::before { + content: "\F92B"; +} +.mdi-wifi-strength-off::before { + content: "\F92C"; +} +.mdi-wifi-strength-off-outline::before { + content: "\F92D"; +} +.mdi-wifi-strength-outline::before { + content: "\F92E"; +} +.mdi-wii::before { + content: "\F5AB"; +} +.mdi-wiiu::before { + content: "\F72C"; +} +.mdi-wikipedia::before { + content: "\F5AC"; +} +.mdi-wind-turbine::before { + content: "\FD81"; +} +.mdi-window-close::before { + content: "\F5AD"; +} +.mdi-window-closed::before { + content: "\F5AE"; +} +.mdi-window-closed-variant::before { + content: "\F0206"; +} +.mdi-window-maximize::before { + content: "\F5AF"; +} +.mdi-window-minimize::before { + content: "\F5B0"; +} +.mdi-window-open::before { + content: "\F5B1"; +} +.mdi-window-open-variant::before { + content: "\F0207"; +} +.mdi-window-restore::before { + content: "\F5B2"; +} +.mdi-window-shutter::before { + content: "\F0147"; +} +.mdi-window-shutter-alert::before { + content: "\F0148"; +} +.mdi-window-shutter-open::before { + content: "\F0149"; +} +.mdi-windows::before { + content: "\F5B3"; +} +.mdi-windows-classic::before { + content: "\FA20"; +} +.mdi-wiper::before { + content: "\FAE8"; +} +.mdi-wiper-wash::before { + content: "\FD82"; +} +.mdi-wordpress::before { + content: "\F5B4"; +} +.mdi-worker::before { + content: "\F5B5"; +} +.mdi-wrap::before { + content: "\F5B6"; +} +.mdi-wrap-disabled::before { + content: "\FBBB"; +} +.mdi-wrench::before { + content: "\F5B7"; +} +.mdi-wrench-outline::before { + content: "\FBBC"; +} +.mdi-wunderlist::before { + content: "\F5B8"; +} +.mdi-xamarin::before { + content: "\F844"; +} +.mdi-xamarin-outline::before { + content: "\F845"; +} +.mdi-xaml::before { + content: "\F673"; +} +.mdi-xbox::before { + content: "\F5B9"; +} +.mdi-xbox-controller::before { + content: "\F5BA"; +} +.mdi-xbox-controller-battery-alert::before { + content: "\F74A"; +} +.mdi-xbox-controller-battery-charging::before { + content: "\FA21"; +} +.mdi-xbox-controller-battery-empty::before { + content: "\F74B"; +} +.mdi-xbox-controller-battery-full::before { + content: "\F74C"; +} +.mdi-xbox-controller-battery-low::before { + content: "\F74D"; +} +.mdi-xbox-controller-battery-medium::before { + content: "\F74E"; +} +.mdi-xbox-controller-battery-unknown::before { + content: "\F74F"; +} +.mdi-xbox-controller-menu::before { + content: "\FE52"; +} +.mdi-xbox-controller-off::before { + content: "\F5BB"; +} +.mdi-xbox-controller-view::before { + content: "\FE53"; +} +.mdi-xda::before { + content: "\F5BC"; +} +.mdi-xing::before { + content: "\F5BD"; +} +.mdi-xing-box::before { + content: "\F5BE"; +} +.mdi-xing-circle::before { + content: "\F5BF"; +} +.mdi-xml::before { + content: "\F5C0"; +} +.mdi-xmpp::before { + content: "\F7FE"; +} +.mdi-yahoo::before { + content: "\FB2A"; +} +.mdi-yammer::before { + content: "\F788"; +} +.mdi-yeast::before { + content: "\F5C1"; +} +.mdi-yelp::before { + content: "\F5C2"; +} +.mdi-yin-yang::before { + content: "\F67F"; +} +.mdi-yoga::before { + content: "\F01A7"; +} +.mdi-youtube::before { + content: "\F5C3"; +} +.mdi-youtube-creator-studio::before { + content: "\F846"; +} +.mdi-youtube-gaming::before { + content: "\F847"; +} +.mdi-youtube-subscription::before { + content: "\FD1C"; +} +.mdi-youtube-tv::before { + content: "\F448"; +} +.mdi-z-wave::before { + content: "\FAE9"; +} +.mdi-zend::before { + content: "\FAEA"; +} +.mdi-zigbee::before { + content: "\FD1D"; +} +.mdi-zip-box::before { + content: "\F5C4"; +} +.mdi-zip-box-outline::before { + content: "\F001B"; +} +.mdi-zip-disk::before { + content: "\FA22"; +} +.mdi-zodiac-aquarius::before { + content: "\FA7C"; +} +.mdi-zodiac-aries::before { + content: "\FA7D"; +} +.mdi-zodiac-cancer::before { + content: "\FA7E"; +} +.mdi-zodiac-capricorn::before { + content: "\FA7F"; +} +.mdi-zodiac-gemini::before { + content: "\FA80"; +} +.mdi-zodiac-leo::before { + content: "\FA81"; +} +.mdi-zodiac-libra::before { + content: "\FA82"; +} +.mdi-zodiac-pisces::before { + content: "\FA83"; +} +.mdi-zodiac-sagittarius::before { + content: "\FA84"; +} +.mdi-zodiac-scorpio::before { + content: "\FA85"; +} +.mdi-zodiac-taurus::before { + content: "\FA86"; +} +.mdi-zodiac-virgo::before { + content: "\FA87"; +} +.mdi-blank::before { + content: "\F68C"; + visibility: hidden; +} +.mdi-18px.mdi-set, +.mdi-18px.mdi:before { + font-size: 18px; +} +.mdi-24px.mdi-set, +.mdi-24px.mdi:before { + font-size: 24px; +} +.mdi-36px.mdi-set, +.mdi-36px.mdi:before { + font-size: 36px; +} +.mdi-48px.mdi-set, +.mdi-48px.mdi:before { + font-size: 48px; +} +.mdi-dark:before { + color: rgba(0, 0, 0, 0.54); +} +.mdi-dark.mdi-inactive:before { + color: rgba(0, 0, 0, 0.26); +} +.mdi-light:before { + color: #fff; +} +.mdi-light.mdi-inactive:before { + color: rgba(255, 255, 255, 0.3); +} +.mdi-rotate-45:before { + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} +.mdi-rotate-90:before { + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.mdi-rotate-135:before { + -webkit-transform: rotate(135deg); + -ms-transform: rotate(135deg); + transform: rotate(135deg); +} +.mdi-rotate-180:before { + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.mdi-rotate-225:before { + -webkit-transform: rotate(225deg); + -ms-transform: rotate(225deg); + transform: rotate(225deg); +} +.mdi-rotate-270:before { + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.mdi-rotate-315:before { + -webkit-transform: rotate(315deg); + -ms-transform: rotate(315deg); + transform: rotate(315deg); +} +.mdi-flip-h:before { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: "FlipH"; +} +.mdi-flip-v:before { + -webkit-transform: scaleY(-1); + transform: scaleY(-1); + filter: FlipV; + -ms-filter: "FlipV"; +} +.mdi-spin:before { + -webkit-animation: mdi-spin 2s infinite linear; + animation: mdi-spin 2s infinite linear; +} +@-webkit-keyframes mdi-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes mdi-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +/*# sourceMappingURL=materialdesignicons.css.map */ diff --git a/packages/auditor-backoffice-ui/src/scss/libs/_all.scss b/packages/auditor-backoffice-ui/src/scss/libs/_all.scss new file mode 100644 index 000000000..cba6f26eb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/libs/_all.scss @@ -0,0 +1,29 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +@import "node_modules/bulma-radio/bulma-radio"; +// @import "node_modules/bulma-responsive-tables/bulma-responsive-tables"; +@import "node_modules/bulma-checkbox/bulma-checkbox"; +@import "node_modules/bulma-switch-control/bulma-switch-control"; +@import "node_modules/bulma-upload-control/bulma-upload-control"; + +/* Bulma */ +@import "node_modules/bulma/bulma"; diff --git a/packages/auditor-backoffice-ui/src/scss/main.scss b/packages/auditor-backoffice-ui/src/scss/main.scss new file mode 100644 index 000000000..c4be8aa73 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/main.scss @@ -0,0 +1,195 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +/* Theme style (colors & sizes) */ +@import "theme-default"; + +/* Core Libs & Lib configs */ +@import "libs/all"; + +/* Mixins */ +@import "mixins"; + +/* Theme components */ +@import "nav-bar"; +@import "aside"; +@import "title-bar"; +@import "hero-bar"; +@import "card"; +@import "table"; +@import "tiles"; +@import "form"; +@import "main-section"; +@import "modal"; +@import "footer"; +@import "misc"; +@import "custom-calendar"; +@import "loading"; + +@import "fonts/nunito.css"; +@import "icons/materialdesignicons-4.9.95.min.css"; + +$tooltip-color: red; + +@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css"; +@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css"; + +@import "toggle"; + +.notification { + background-color: transparent; +} + +.timeline .timeline-item .timeline-content { + padding-top: 0; +} + +.timeline .timeline-item:last-child::before { + display: none; +} + +.timeline .timeline-item .timeline-marker { + top: 0; +} + +.toast { + position: absolute; + width: 60%; + margin-left: 10%; + margin-right: 10%; + z-index: 999; + + display: flex; + flex-direction: column; + padding: 15px; + text-align: center; + pointer-events: none; +} + +.toast>.message { + white-space: pre-wrap; + opacity: 80%; +} + +div { + &.is-loading { + position: relative; + pointer-events: none; + opacity: 0.5; + + &:after { + // @include loader; + position: absolute; + top: calc(50% - 2.5em); + left: calc(50% - 2.5em); + width: 5em; + height: 5em; + border-width: 0.25em; + } + } +} + +input[type="checkbox"]:indeterminate+.check { + background: red !important; +} + +.right-sticky { + position: sticky; + right: 0px; + background-color: $white; +} + +.right-sticky .buttons { + flex-wrap: nowrap; +} + +.table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky { + background-color: #fafafa; +} + +tr:hover .right-sticky { + background-color: hsl(0, 0%, 80%); +} + +.table.is-striped tbody tr:nth-child(even):hover .right-sticky { + background-color: hsl(0, 0%, 95%); +} + +.content-full-size { + height: calc(100% - 3rem); + position: absolute; + width: calc(100% - 14rem); + display: flex; +} + +.content-full-size .column .card { + min-width: 200px; +} + +@include touch { + .content-full-size { + height: 100%; + position: absolute; + width: 100%; + } +} + +.column.is-half { + flex: none; + width: 50%; +} + +input:read-only { + cursor: initial; +} + +[data-tooltip]:before { + max-width: 15rem; + width: max-content; + text-align: left; + transition: opacity 0.1s linear 1s; + // transform: inherit !important; + white-space: pre-wrap !important; + font-weight: normal; + // position: relative; +} + +.icon[data-tooltip]:before { + transition: none; + z-index: 5; +} + +span[data-tooltip] { + border-bottom: none; +} + +div[data-tooltip]::before { + position: absolute; +} + +.modal-card-body>p { + padding: 1em; +} + +.modal-card-body>p.warning { + background-color: #fffbdd; + border: solid 1px #f2e9bf; +}
\ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/scss/toggle.scss b/packages/auditor-backoffice-ui/src/scss/toggle.scss new file mode 100644 index 000000000..24636da2f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/scss/toggle.scss @@ -0,0 +1,51 @@ +$green: #56c080; + +.toggle { + cursor: pointer; + display: inline-block; +} +.toggle-switch { + display: inline-block; + background: #ccc; + border-radius: 16px; + width: 58px; + height: 32px; + position: relative; + vertical-align: middle; + transition: background 0.25s; + &:before, + &:after { + content: ""; + } + &:before { + display: block; + background: linear-gradient(to bottom, #fff 0%, #eee 100%); + border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25); + width: 24px; + height: 24px; + position: absolute; + top: 4px; + left: 4px; + transition: left 0.25s; + } + .toggle:hover &:before { + background: linear-gradient(to bottom, #fff 0%, #fff 100%); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5); + } + .toggle-checkbox:checked + & { + background: $green; + &:before { + left: 30px; + } + } +} +.toggle-checkbox { + position: absolute; + visibility: hidden; +} +.toggle-label { + margin-left: 5px; + position: relative; + top: 2px; +} diff --git a/packages/auditor-backoffice-ui/src/stories.test.ts b/packages/auditor-backoffice-ui/src/stories.test.ts new file mode 100644 index 000000000..abd993550 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/stories.test.ts @@ -0,0 +1,44 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { setupI18n } from "@gnu-taler/taler-util"; +import * as tests from "@gnu-taler/web-util/testing"; +import { parseGroupImport } from "@gnu-taler/web-util/browser"; +import * as admin from "./paths/admin/index.stories.js"; +import * as instance from "./paths/instance/index.stories.js"; + +setupI18n("en", { en: {} }); + +describe("All the examples:", () => { + const cms = parseGroupImport({ admin, instance }); + cms.forEach((group) => { + describe(`Example for group: ${group.title}`, () => { + group.list.forEach((component) => { + describe(`Component: ${component.name}`, () => { + component.examples.forEach((example) => { + it(`should render example: ${example.name}`, () => { + tests.renderUI(example.render); + }); + }); + }); + }); + }); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/stories.tsx b/packages/auditor-backoffice-ui/src/stories.tsx new file mode 100644 index 000000000..8bb06b8cb --- /dev/null +++ b/packages/auditor-backoffice-ui/src/stories.tsx @@ -0,0 +1,48 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { strings } from "./i18n/strings.js"; + +import * as admin from "./paths/admin/index.stories.js"; +import * as instance from "./paths/instance/index.stories.js"; +import * as components from "./components/index.stories.js"; + +import { renderStories } from "@gnu-taler/web-util/browser"; + +import "./scss/main.scss"; + +function SortStories(a: any, b: any): number { + return (a?.order ?? 0) - (b?.order ?? 0); +} + +function main(): void { + renderStories( + { admin, instance, components }, + { + strings, + }, + ); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", main); +} else { + main(); +} diff --git a/packages/auditor-backoffice-ui/src/sw.js b/packages/auditor-backoffice-ui/src/sw.js new file mode 100644 index 000000000..bf52db6fa --- /dev/null +++ b/packages/auditor-backoffice-ui/src/sw.js @@ -0,0 +1,25 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +// import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/'; + +// setupRouting(); +// setupPrecaching(getFiles()); diff --git a/packages/auditor-backoffice-ui/src/utils/amount.ts b/packages/auditor-backoffice-ui/src/utils/amount.ts new file mode 100644 index 000000000..475489d3e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/utils/amount.ts @@ -0,0 +1,71 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + amountFractionalBase, + AmountJson, + Amounts, +} from "@gnu-taler/taler-util"; +import { MerchantBackend } from "../declaration.js"; + +/** + * merge refund with the same description and a difference less than one minute + * @param prev list of refunds that will hold the merged refunds + * @param cur new refund to add to the list + * @returns list with the new refund, may be merged with the last + */ +export function mergeRefunds( + prev: MerchantBackend.Orders.RefundDetails[], + cur: MerchantBackend.Orders.RefundDetails, +): MerchantBackend.Orders.RefundDetails[] { + let tail; + + if ( + prev.length === 0 || //empty list + cur.timestamp.t_s === "never" || //current does not have timestamp + (tail = prev[prev.length - 1]).timestamp.t_s === "never" || // last does not have timestamp + cur.reason !== tail.reason || //different reason + cur.pending !== tail.pending || //different pending state + Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60 + ) { + //more than 1 minute difference + + //can't merge refunds, they are different or to distant in time + prev.push(cur); + return prev; + } + + const a = Amounts.parseOrThrow(tail.amount); + const b = Amounts.parseOrThrow(cur.amount); + const r = Amounts.add(a, b).amount; + + prev[prev.length - 1] = { + ...tail, + amount: Amounts.stringify(r), + }; + + return prev; +} + +export function rate(a: AmountJson, b: AmountJson): number { + const af = toFloat(a); + const bf = toFloat(b); + if (bf === 0) return 0; + return af / bf; +} + +function toFloat(amount: AmountJson): number { + return amount.value + amount.fraction / amountFractionalBase; +} diff --git a/packages/auditor-backoffice-ui/src/utils/constants.ts b/packages/auditor-backoffice-ui/src/utils/constants.ts new file mode 100644 index 000000000..7c4e288b3 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/utils/constants.ts @@ -0,0 +1,197 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +//https://tools.ietf.org/html/rfc8905 +export const PAYTO_REGEX = + /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/; +export const PAYTO_WIRE_METHOD_LOOKUP = + /payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/; + +export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]{1,11}:[0-9][0-9,]*\.?[0-9,]*$/; + +export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/; + +export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/; + +export const CROCKFORD_BASE32_REGEX = + /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+[*~$=U]*$/; + +export const URL_REGEX = + /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/; + +// how much rows we add every time user hit load more +export const PAGE_SIZE = 20; +// how bigger can be the result set +// after this threshold, load more with move the cursor +export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; + +// how much we will wait for all request, in seconds +export const DEFAULT_REQUEST_TIMEOUT = 10; + +export const MAX_IMAGE_SIZE = 1024 * 1024; + +export const INSTANCE_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.@-]+$/; + +export const COUNTRY_TABLE = { + AE: "U.A.E.", + AF: "Afghanistan", + AL: "Albania", + AM: "Armenia", + AN: "Netherlands Antilles", + AR: "Argentina", + AT: "Austria", + AU: "Australia", + AZ: "Azerbaijan", + BA: "Bosnia and Herzegovina", + BD: "Bangladesh", + BE: "Belgium", + BG: "Bulgaria", + BH: "Bahrain", + BN: "Brunei Darussalam", + BO: "Bolivia", + BR: "Brazil", + BT: "Bhutan", + BY: "Belarus", + BZ: "Belize", + CA: "Canada", + CG: "Congo", + CH: "Switzerland", + CI: "Cote d'Ivoire", + CL: "Chile", + CM: "Cameroon", + CN: "People's Republic of China", + CO: "Colombia", + CR: "Costa Rica", + CS: "Serbia and Montenegro", + CZ: "Czech Republic", + DE: "Germany", + DK: "Denmark", + DO: "Dominican Republic", + DZ: "Algeria", + EC: "Ecuador", + EE: "Estonia", + EG: "Egypt", + ER: "Eritrea", + ES: "Spain", + ET: "Ethiopia", + FI: "Finland", + FO: "Faroe Islands", + FR: "France", + GB: "United Kingdom", + GD: "Caribbean", + GE: "Georgia", + GL: "Greenland", + GR: "Greece", + GT: "Guatemala", + HK: "Hong Kong", + // HK: "Hong Kong S.A.R.", + HN: "Honduras", + HR: "Croatia", + HT: "Haiti", + HU: "Hungary", + ID: "Indonesia", + IE: "Ireland", + IL: "Israel", + IN: "India", + IQ: "Iraq", + IR: "Iran", + IS: "Iceland", + IT: "Italy", + JM: "Jamaica", + JO: "Jordan", + JP: "Japan", + KE: "Kenya", + KG: "Kyrgyzstan", + KH: "Cambodia", + KR: "South Korea", + KW: "Kuwait", + KZ: "Kazakhstan", + LA: "Laos", + LB: "Lebanon", + LI: "Liechtenstein", + LK: "Sri Lanka", + LT: "Lithuania", + LU: "Luxembourg", + LV: "Latvia", + LY: "Libya", + MA: "Morocco", + MC: "Principality of Monaco", + MD: "Moldava", + // MD: "Moldova", + ME: "Montenegro", + MK: "Former Yugoslav Republic of Macedonia", + ML: "Mali", + MM: "Myanmar", + MN: "Mongolia", + MO: "Macau S.A.R.", + MT: "Malta", + MV: "Maldives", + MX: "Mexico", + MY: "Malaysia", + NG: "Nigeria", + NI: "Nicaragua", + NL: "Netherlands", + NO: "Norway", + NP: "Nepal", + NZ: "New Zealand", + OM: "Oman", + PA: "Panama", + PE: "Peru", + PH: "Philippines", + PK: "Islamic Republic of Pakistan", + PL: "Poland", + PR: "Puerto Rico", + PT: "Portugal", + PY: "Paraguay", + QA: "Qatar", + RE: "Reunion", + RO: "Romania", + RS: "Serbia", + RU: "Russia", + RW: "Rwanda", + SA: "Saudi Arabia", + SE: "Sweden", + SG: "Singapore", + SI: "Slovenia", + SK: "Slovak", + SN: "Senegal", + SO: "Somalia", + SR: "Suriname", + SV: "El Salvador", + SY: "Syria", + TH: "Thailand", + TJ: "Tajikistan", + TM: "Turkmenistan", + TN: "Tunisia", + TR: "Turkey", + TT: "Trinidad and Tobago", + TW: "Taiwan", + TZ: "Tanzania", + UA: "Ukraine", + US: "United States", + UY: "Uruguay", + VA: "Vatican", + VE: "Venezuela", + VN: "Viet Nam", + YE: "Yemen", + ZA: "South Africa", + ZW: "Zimbabwe", +}; diff --git a/packages/auditor-backoffice-ui/src/utils/crypto.ts b/packages/auditor-backoffice-ui/src/utils/crypto.ts new file mode 100644 index 000000000..27e6ade02 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/utils/crypto.ts @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +const encTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +// base32 RFC 3548 +function encodeBase32(data: ArrayBuffer) { + const dataBytes = new Uint8Array(data); + let sb = ""; + const size = data.byteLength; + let bitBuf = 0; + let numBits = 0; + let pos = 0; + while (pos < size || numBits > 0) { + if (pos < size && numBits < 5) { + const d = dataBytes[pos++]; + bitBuf = (bitBuf << 8) | d; + numBits += 8; + } + if (numBits < 5) { + // zero-padding + bitBuf = bitBuf << (5 - numBits); + numBits = 5; + } + const v = (bitBuf >>> (numBits - 5)) & 31; + sb += encTable[v]; + numBits -= 5; + } + return sb; +} + +export function isBase32RFC3548Charset(s: string): boolean { + for (let idx = 0; idx < s.length; idx++) { + const c = s.charAt(idx); + if (encTable.indexOf(c) === -1) return false; + } + return true; +} + +export function randomBase32Key(): string { + var buf = new Uint8Array(20); + window.crypto.getRandomValues(buf); + return encodeBase32(buf); +} diff --git a/packages/auditor-backoffice-ui/src/utils/regex.test.ts b/packages/auditor-backoffice-ui/src/utils/regex.test.ts new file mode 100644 index 000000000..984f1a472 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/utils/regex.test.ts @@ -0,0 +1,88 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { expect } from "chai"; +import { AMOUNT_REGEX, PAYTO_REGEX } from "./constants.js"; + +describe("payto uri format", () => { + const valids = [ + "payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello", + "payto://ach/122000661/1234", + "payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200", + "payto://void/?amount=EUR:10.5", + "payto://ilp/g.acme.bob", + ]; + + it("should be valid", () => { + valids.forEach((v) => expect(v).match(PAYTO_REGEX)); + }); + + const invalids = [ + // has two question marks + "payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello", + // has a space + "payto://ach /122000661/1234", + // has a space + "payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200", + // invalid field name (mount instead of amount) + "payto://void/?mount=EUR:10.5", + // payto:// is incomplete + "payto: //ilp/g.acme.bob", + ]; + + it("should not be valid", () => { + invalids.forEach((v) => expect(v).not.match(PAYTO_REGEX)); + }); +}); + +describe("amount format", () => { + const valids = [ + "ARS:10", + "COL:10.2", + "UY:1,000.2", + "ARS:10.123,123", + "ARS:1,000,000", + "ARSCOL:10", + "LONGESTCURR:1,000,000.123,123", + ]; + + + it("should be valid", () => { + valids.forEach((v) => expect(v).match(AMOUNT_REGEX)); + }); + + const invalids = [ + //no currency name + ":10", + //use . instead of , + "ARS:1.000.000", + //currency name with numbers + "1ARS:10", + //currency name with numbers + "AR5:10", + //missing value + "USD:", + ]; + + it("should not be valid", () => { + invalids.forEach((v) => expect(v).not.match(AMOUNT_REGEX)); + }); +}); diff --git a/packages/auditor-backoffice-ui/src/utils/table.ts b/packages/auditor-backoffice-ui/src/utils/table.ts new file mode 100644 index 000000000..db2b2021c --- /dev/null +++ b/packages/auditor-backoffice-ui/src/utils/table.ts @@ -0,0 +1,57 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { WithId } from "../declaration.js"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export interface Actions<T extends WithId> { + element: T; + type: "DELETE" | "UPDATE"; +} + +function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +export function buildActions<T extends WithId>( + instances: T[], + selected: string[], + action: "DELETE", +): Actions<T>[] { + return selected + .map((id) => instances.find((i) => i.id === id)) + .filter(notEmpty) + .map((id) => ({ element: id, type: action })); +} + +/** + * For any object or array, return the same object if is not empty. + * not empty: + * - for arrays: at least one element not undefined + * - for objects: at least one property not undefined + * @param obj + * @returns + */ +export function undefinedIfEmpty< + T extends Record<string, unknown> | Array<unknown>, +>(obj: T | undefined): T | undefined { + if (obj === undefined) return undefined; + return Object.values(obj).some((v) => v !== undefined) ? obj : undefined; +} diff --git a/packages/auditor-backoffice-ui/src/utils/types.ts b/packages/auditor-backoffice-ui/src/utils/types.ts new file mode 100644 index 000000000..0d249f3c4 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/utils/types.ts @@ -0,0 +1,31 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { VNode } from "preact"; + +export interface KeyValue { + [key: string]: string; +} + +export interface Notification { + message: string; + description?: string | VNode; + details?: string | VNode; + type: MessageType; +} + +export type ValueOrFunction<T> = T | ((p: T) => T); +export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS"; diff --git a/packages/auditor-backoffice-ui/test.mjs b/packages/auditor-backoffice-ui/test.mjs new file mode 100755 index 000000000..be76348e5 --- /dev/null +++ b/packages/auditor-backoffice-ui/test.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { build } from "@gnu-taler/web-util/build"; +import { getFilesInDirectory } from "@gnu-taler/web-util/build"; + +const allTestFiles = getFilesInDirectory("src", /.test.tsx?$/); + +await build({ + type: "test", + source: { + js: allTestFiles.files, + assets: [{base:"src",files:["src/index.html"]}], + }, + destination: "./dist/test", + css: "sass", +}); diff --git a/packages/auditor-backoffice-ui/tsconfig.json b/packages/auditor-backoffice-ui/tsconfig.json new file mode 100644 index 000000000..396f1e9e7 --- /dev/null +++ b/packages/auditor-backoffice-ui/tsconfig.json @@ -0,0 +1,58 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, + "module": "Node16" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "lib": [ + "es2020", + "dom" + ] /* Specify library files to be included in the compilation: */, + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */, + "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true /* Do not emit outputs. */, + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + "moduleResolution": "node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "esModuleInterop": true /* */, + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */ + }, + "include": ["src/**/*", "tests/**/*"] +} |