gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: templates


From: gnunet
Subject: [taler-wallet-core] branch master updated: templates
Date: Mon, 19 Dec 2022 20:25:20 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new cf7f81968 templates
cf7f81968 is described below

commit cf7f819685c19adfe6443a6bdd10f3afe10b247d
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Dec 19 16:25:09 2022 -0300

    templates
---
 .../merchant-backoffice-ui/src/InstanceRoutes.tsx  |  57 ++++-
 .../src/components/menu/SideBar.tsx                |  16 +-
 .../merchant-backoffice-ui/src/declaration.d.ts    |  84 +++++++
 .../merchant-backoffice-ui/src/hooks/templates.ts  | 280 +++++++++++++++++++++
 .../src/paths/instance/orders/list/index.tsx       |  23 +-
 .../instance/templates/create/Create.stories.tsx   |  41 +++
 .../paths/instance/templates/create/CreatePage.tsx | 163 ++++++++++++
 .../src/paths/instance/templates/create/index.tsx  |  63 +++++
 .../paths/instance/templates/list/List.stories.tsx |  28 +++
 .../src/paths/instance/templates/list/ListPage.tsx |  64 +++++
 .../src/paths/instance/templates/list/Table.tsx    | 207 +++++++++++++++
 .../src/paths/instance/templates/list/index.tsx    |  95 +++++++
 .../instance/templates/update/Update.stories.tsx   |  32 +++
 .../paths/instance/templates/update/UpdatePage.tsx | 174 +++++++++++++
 .../src/paths/instance/templates/update/index.tsx  |  86 +++++++
 .../src/scss/_custom-calendar.scss                 |  12 +-
 .../merchant-backoffice-ui/src/utils/amount.ts     |  25 +-
 17 files changed, 1400 insertions(+), 50 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx 
b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
index adfc73e20..817de5f7b 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -43,6 +43,9 @@ import ProductListPage from 
"./paths/instance/products/list/index.js";
 import ProductUpdatePage from "./paths/instance/products/update/index.js";
 import TransferListPage from "./paths/instance/transfers/list/index.js";
 import TransferCreatePage from "./paths/instance/transfers/create/index.js";
+import TemplateListPage from "./paths/instance/templates/list/index.js";
+import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
+import TemplateCreatePage from "./paths/instance/templates/create/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";
@@ -78,6 +81,10 @@ export enum InstancePaths {
 
   transfers_list = "/transfers",
   transfers_new = "/transfer/new",
+
+  templates_list = "/templates",
+  templates_update = "/templates/:tid/update",
+  templates_new = "/templates/new",
 }
 
 // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -217,7 +224,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
         }}
       >
         <Route path="/" component={Redirect} to={InstancePaths.order_list} />
-
         {/**
          * Admin pages
          */}
@@ -236,7 +242,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
           />
         )}
-
         {admin && (
           <Route
             path={AdminPaths.new_instance}
@@ -247,7 +252,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             }}
           />
         )}
-
         {admin && (
           <Route
             path={AdminPaths.update_instance}
@@ -261,7 +265,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             onNotFound={NotFoundPage}
           />
         )}
-
         {/**
          * Update instance page
          */}
@@ -279,7 +282,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
           onUnauthorized={LoginPageAccessDenied}
           onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
         />
-
         {/**
          * Product pages
          */}
@@ -319,7 +321,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             route(InstancePaths.product_list);
           }}
         />
-
         {/**
          * Order pages
          */}
@@ -356,7 +357,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             route(InstancePaths.order_list);
           }}
         />
-
         {/**
          * Transfer pages
          */}
@@ -370,7 +370,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             route(InstancePaths.transfers_new);
           }}
         />
-
         <Route
           path={InstancePaths.transfers_new}
           component={TransferCreatePage}
@@ -381,6 +380,45 @@ export function InstanceRoutes({ id, admin, 
setInstanceName }: Props): VNode {
             route(InstancePaths.transfers_list);
           }}
         />
+        {/**
+         * Templates pages
+         */}
+        <Route
+          path={InstancePaths.templates_list}
+          component={TemplateListPage}
+          onUnauthorized={LoginPageAccessDenied}
+          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+          onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+          onCreate={() => {
+            route(InstancePaths.templates_new);
+          }}
+          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);
+          }}
+        />
 
         {/**
          * reserves pages
@@ -398,7 +436,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             route(InstancePaths.reserves_new);
           }}
         />
-
         <Route
           path={InstancePaths.reserves_details}
           component={ReservesDetailsPage}
@@ -409,7 +446,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             route(InstancePaths.reserves_list);
           }}
         />
-
         <Route
           path={InstancePaths.reserves_new}
           component={ReservesCreatePage}
@@ -420,7 +456,6 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             route(InstancePaths.reserves_list);
           }}
         />
-
         <Route path={InstancePaths.kyc} component={ListKYCPage} />
         {/**
          * Example pages
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx 
b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 1d15bb094..ea49be99a 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -51,12 +51,6 @@ export function Sidebar({
 
   const kycStatus = useInstanceKYCDetails();
   const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
-  // const withInstanceIdIfNeeded = useCallback(function (path: string) {
-  //   if (mimic) {
-  //     return path + '?instance=' + instance
-  //   }
-  //   return path
-  // },[instance])
 
   return (
     <aside class="aside is-placed-left is-expanded">
@@ -130,6 +124,16 @@ export function Sidebar({
                   </span>
                 </a>
               </li>
+              <li>
+                <a href={"/templates"} class="has-icon">
+                  <span class="icon">
+                    <i class="mdi mdi-newspaper" />
+                  </span>
+                  <span class="menu-item-label">
+                    <Translate>Templates</Translate>
+                  </span>
+                </a>
+              </li>
               <li>
                 <a href={"/reserves"} class="has-icon">
                   <span class="icon">
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts 
b/packages/merchant-backoffice-ui/src/declaration.d.ts
index 75bc66201..25b66bdea 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -1275,6 +1275,90 @@ export namespace MerchantBackend {
     // }
   }
 
+  namespace Template {
+    interface TemplateAddDetails {
+      // Template ID to use.
+      template_id: string;
+
+      // Human-readable description for the template.
+      template_description: string;
+
+      // A base64-encoded image selected by the merchant.
+      // This parameter is optional.
+      // We are not sure about it.
+      image?: ImageDataUrl;
+
+      // 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;
+
+      // A base64-encoded image selected by the merchant.
+      // This parameter is optional.
+      // We are not sure about it.
+      image?: ImageDataUrl;
+
+      // 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;
+
+      // A base64-encoded image selected by the merchant.
+      // This parameter is optional.
+      // We are not sure about it.
+      image?: ImageDataUrl;
+
+      // Additional information in a separate template.
+      template_contract: TemplateContractDetails;
+    }
+
+    interface UsingTemplateDetails {
+      // Subject of the template
+      subject?: string;
+
+      // The amount entered by the customer.
+      amount?: Amount;
+    }
+
+    interface UsingTemplateResponse {
+      // After enter the request. The user will be pay with a taler URL.
+      taler_url: string;
+    }
+  }
+
   interface ContractTerms {
     // Human-readable description of the whole purchase
     summary: string;
diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts 
b/packages/merchant-backoffice-ui/src/hooks/templates.ts
new file mode 100644
index 000000000..3e69d78d0
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts
@@ -0,0 +1,280 @@
+/*
+ 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 { MerchantBackend } from "../declaration.js";
+import { useBackendContext } from "../context/backend.js";
+import {
+  request,
+  HttpResponse,
+  HttpError,
+  HttpResponseOk,
+  HttpResponsePaginated,
+  useMatchMutate,
+} from "./backend.js";
+import useSWR from "swr";
+import { useInstanceContext } from "../context/instance.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useEffect, useState } from "preact/hooks";
+
+async function templateFetcher<T>(
+  url: string,
+  token: string,
+  backend: string,
+  position?: string,
+  delta?: number,
+): Promise<HttpResponseOk<T>> {
+  const params: any = {};
+  if (delta !== undefined) {
+    params.limit = delta;
+  }
+  if (position !== undefined) params.offset = position;
+
+  return request<T>(`${backend}${url}`, { token, params });
+}
+
+export function useTemplateAPI(): TemplateAPI {
+  const mutateAll = useMatchMutate();
+  const { url: baseUrl, token: adminToken } = useBackendContext();
+  const { token: instanceToken, id, admin } = useInstanceContext();
+
+  const { url, token } = !admin
+    ? {
+        url: baseUrl,
+        token: adminToken,
+      }
+    : {
+        url: `${baseUrl}/instances/${id}`,
+        token: instanceToken,
+      };
+
+  const createTemplate = async (
+    data: MerchantBackend.Template.TemplateAddDetails,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`${url}/private/templates`, {
+      method: "post",
+      token,
+      data,
+    });
+    await mutateAll(/.*private\/templates.*/);
+    return res;
+  };
+
+  const updateTemplate = async (
+    templateId: string,
+    data: MerchantBackend.Template.TemplatePatchDetails,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`${url}/private/templates/${templateId}`, {
+      method: "patch",
+      token,
+      data,
+    });
+    await mutateAll(/.*private\/templates.*/);
+    return res;
+  };
+
+  const deleteTemplate = async (
+    templateId: string,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`${url}/private/templates/${templateId}`, {
+      method: "delete",
+      token,
+    });
+    await mutateAll(/.*private\/templates.*/);
+    return res;
+  };
+
+  const createOrder = async (
+    templateId: string,
+    data: MerchantBackend.Template.UsingTemplateDetails,
+  ): Promise<
+    HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>
+  > => {
+    const res = await request<MerchantBackend.Template.UsingTemplateResponse>(
+      `${url}/private/templates/${templateId}`,
+      {
+        method: "post",
+        token,
+        data,
+      },
+    );
+    await mutateAll(/.*private\/templates.*/);
+    return res;
+  };
+
+  return { createTemplate, updateTemplate, deleteTemplate, createOrder };
+}
+
+export interface TemplateAPI {
+  createTemplate: (
+    data: MerchantBackend.Template.TemplateAddDetails,
+  ) => Promise<HttpResponseOk<void>>;
+  updateTemplate: (
+    id: string,
+    data: MerchantBackend.Template.TemplatePatchDetails,
+  ) => Promise<HttpResponseOk<void>>;
+  deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>;
+  createOrder: (
+    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> {
+  const { url: baseUrl, token: baseToken } = useBackendContext();
+  const { token: instanceToken, id, admin } = useInstanceContext();
+
+  const { url, token } = !admin
+    ? { url: baseUrl, token: baseToken }
+    : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
+
+  // 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.Template.TemplateSummaryResponse>, 
HttpError>(
+  //   [
+  //     `/private/templates`,
+  //     token,
+  //     url,
+  //     args?.position,
+  //     totalBefore,
+  //   ],
+  //   templateFetcher,
+  // );
+  const {
+    data: afterData,
+    error: afterError,
+    isValidating: loadingAfter,
+  } = useSWR<
+    HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
+    HttpError
+  >(
+    [`/private/templates`, token, url, args?.position, -totalAfter],
+    templateFetcher,
+  );
+
+  //this will save last result
+  // const [lastBefore, setLastBefore] = useState<
+  //   HttpResponse<MerchantBackend.Template.TemplateSummaryResponse>
+  // >({ loading: true });
+  const [lastAfter, setLastAfter] = useState<
+    HttpResponse<MerchantBackend.Template.TemplateSummaryResponse>
+  >({ loading: true });
+  useEffect(() => {
+    if (afterData) setLastAfter(afterData);
+    // if (beforeData) setLastBefore(beforeData);
+  }, [afterData /*, beforeData*/]);
+
+  // if (beforeError) return beforeError;
+  if (afterError) return afterError;
+
+  // 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 = false;
+  // 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> {
+  const { url: baseUrl, token: baseToken } = useBackendContext();
+  const { token: instanceToken, id, admin } = useInstanceContext();
+
+  const { url, token } = !admin
+    ? { url: baseUrl, token: baseToken }
+    : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
+
+  const { data, error, isValidating } = useSWR<
+    HttpResponseOk<MerchantBackend.Template.TemplateDetails>,
+    HttpError
+  >([`/private/templates/${templateId}`, token, url], 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;
+  return { loading: true };
+}
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
index b5fe7611c..315d78c63 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -23,7 +23,7 @@ import { h, VNode, Fragment } 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 { MerchantBackend } from "../../../../declaration.js";
 import { HttpError } from "../../../../hooks/backend.js";
 import {
   InstanceOrderFilter,
@@ -44,7 +44,7 @@ interface Props {
   onCreate: () => void;
 }
 
-export default function ({
+export default function OrderList({
   onUnauthorized,
   onLoadError,
   onCreate,
@@ -56,13 +56,19 @@ export default function ({
     MerchantBackend.Orders.OrderHistoryEntry | undefined
   >(undefined);
 
-  const setNewDate = (date?: Date) => setFilter((prev) => ({ ...prev, date }));
+  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 = useTranslator();
+  const [errorOrderId, setErrorOrderId] = useState<string | undefined>(
+    undefined,
+  );
+
   if (result.clientError && result.isUnauthorized) return onUnauthorized();
   if (result.clientError && result.isNotfound) return onNotFound();
   if (result.loading) return <Loading />;
@@ -78,12 +84,7 @@ export default function ({
       ? "is-active"
       : "";
 
-  const i18n = useTranslator();
-  const [errorOrderId, setErrorOrderId] = useState<string | undefined>(
-    undefined,
-  );
-
-  async function testIfOrderExistAndSelect(orderId: string) {
+  async function testIfOrderExistAndSelect(orderId: string): Promise<void> {
     if (!orderId) {
       setErrorOrderId(i18n`Enter an order id`);
       return;
@@ -189,7 +190,7 @@ function RefundModalForTable({
   onNotFound,
   onConfirm,
   onCancel,
-}: RefundProps) {
+}: RefundProps): VNode {
   const result = useOrderDetails(id);
 
   if (result.clientError && result.isUnauthorized) return onUnauthorized();
@@ -206,6 +207,6 @@ function RefundModalForTable({
   );
 }
 
-async function copyToClipboard(text: string) {
+async function copyToClipboard(text: string): Promise<void> {
   return navigator.clipboard.writeText(text);
 }
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
new file mode 100644
index 000000000..b81130146
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.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 { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+  title: "Pages/Templates/Create",
+  component: TestedComponent,
+};
+
+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/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
new file mode 100644
index 000000000..dba4b5d14
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -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 { 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 { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { Translate, useTranslator } from "../../../../i18n/index.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = MerchantBackend.Template.TemplateAddDetails;
+
+interface Props {
+  onCreate: (d: Entity) => Promise<void>;
+  onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+  const i18n = useTranslator();
+  const backend = useBackendContext();
+
+  const [state, setState] = useState<Partial<Entity>>({
+    template_contract: {
+      minimum_age: 0,
+      pay_duration: {
+        d_us: 1000 * 1000 * 60 * 30, //30 min
+      },
+    },
+  });
+
+  const errors: FormErrors<Entity> = {
+    template_id: !state.template_id ? i18n`should not be empty` : undefined,
+    template_description: !state.template_description
+      ? i18n`should not be empty`
+      : undefined,
+    template_contract: !state.template_contract
+      ? undefined
+      : undefinedIfEmpty({
+          minimum_age:
+            state.template_contract.minimum_age < 0
+              ? i18n`should be greater that 0`
+              : undefined,
+          pay_duration: !state.template_contract.pay_duration
+            ? i18n`can't be empty`
+            : state.template_contract.pay_duration.d_us === "forever"
+            ? undefined
+            : state.template_contract.pay_duration.d_us < 1000
+            ? i18n`to short`
+            : 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}
+            >
+              <InputWithAddon<Entity>
+                name="template_id"
+                addonBefore={`${backend.url}/instances/templates/`}
+                label={i18n`Identifier`}
+                tooltip={i18n`Name of the template in URLs.`}
+              />
+
+              <Input<Entity>
+                name="template_description"
+                label={i18n`Description`}
+                help=""
+                tooltip={i18n`Describe what this template stands for`}
+              />
+              <Input
+                name="template_contract.summary"
+                inputType="multiline"
+                label={i18n`Order summary`}
+                tooltip={i18n`Title of the order to be shown to the customer`}
+              />
+              <InputCurrency
+                name="template_contract.amount"
+                label={i18n`Order price`}
+                tooltip={i18n`Order price`}
+              />
+              <InputNumber
+                name="template_contract.minimum_age"
+                label={i18n`Minimum age`}
+                help=""
+                tooltip={i18n`Is this contract restricted to some age?`}
+              />
+              <InputDuration
+                name="template_contract.pay_duration"
+                label={i18n`Payment timeout`}
+                help=""
+                tooltip={i18n`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}>
+                  <Translate>Cancel</Translate>
+                </button>
+              )}
+              <AsyncButton
+                disabled={hasErrors}
+                data-tooltip={
+                  hasErrors
+                    ? i18n`Need to complete marked fields`
+                    : "confirm operation"
+                }
+                onClick={submitForm}
+              >
+                <Translate>Confirm</Translate>
+              </AsyncButton>
+            </div>
+          </div>
+          <div class="column" />
+        </div>
+      </section>
+    </div>
+  );
+}
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
new file mode 100644
index 000000000..bfedb7369
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/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 { 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 { useTemplateAPI } from "../../../../hooks/templates.js";
+import { useTransferAPI } from "../../../../hooks/transfer.js";
+import { useTranslator } from "../../../../i18n/index.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 = useTranslator();
+
+  return (
+    <>
+      <NotificationCard notification={notif} />
+      <CreatePage
+        onBack={onBack}
+        onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => {
+          return createTemplate(request)
+            .then(() => onConfirm())
+            .catch((error) => {
+              setNotif({
+                message: i18n`could not inform template`,
+                type: "ERROR",
+                description: error.message,
+              });
+            });
+        }}
+      />
+    </>
+  );
+}
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
new file mode 100644
index 000000000..702e9ba4a
--- /dev/null
+++ 
b/packages/merchant-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/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
new file mode 100644
index 000000000..dd983918f
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/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 { useTranslator } from "../../../../i18n/index.js";
+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;
+}
+
+export function ListPage({
+  templates,
+  onCreate,
+  onDelete,
+  onSelect,
+  onLoadMoreBefore,
+  onLoadMoreAfter,
+}: Props): VNode {
+  const form = { payto_uri: "" };
+
+  const i18n = useTranslator();
+  return (
+    <section class="section is-main-section">
+      <CardTable
+        templates={templates.map((o) => ({
+          ...o,
+          id: String(o.template_id),
+        }))}
+        onCreate={onCreate}
+        onDelete={onDelete}
+        onSelect={onSelect}
+        onLoadMoreBefore={onLoadMoreBefore}
+        hasMoreBefore={!onLoadMoreBefore}
+        onLoadMoreAfter={onLoadMoreAfter}
+        hasMoreAfter={!onLoadMoreAfter}
+      />
+    </section>
+  );
+}
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
new file mode 100644
index 000000000..bc8477039
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -0,0 +1,207 @@
+/*
+ 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 { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+import { Translate, useTranslator } from "../../../../i18n/index.js";
+
+type Entity = MerchantBackend.Template.TemplateEntry;
+
+interface Props {
+  templates: Entity[];
+  onDelete: (e: Entity) => void;
+  onSelect: (e: Entity) => void;
+  onCreate: () => void;
+  onLoadMoreBefore?: () => void;
+  hasMoreBefore?: boolean;
+  hasMoreAfter?: boolean;
+  onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+  templates,
+  onCreate,
+  onDelete,
+  onSelect,
+  onLoadMoreAfter,
+  onLoadMoreBefore,
+  hasMoreAfter,
+  hasMoreBefore,
+}: Props): VNode {
+  const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+  const i18n = useTranslator();
+
+  return (
+    <div class="card has-table">
+      <header class="card-header">
+        <p class="card-header-title">
+          <span class="icon">
+            <i class="mdi mdi-newspaper" />
+          </span>
+          <Translate>Templates</Translate>
+        </p>
+        <div class="card-header-icon" aria-label="more options">
+          <span class="has-tooltip-left" data-tooltip={i18n`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}
+                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 = useTranslator();
+  return (
+    <div class="table-container">
+      {onLoadMoreBefore && (
+        <button
+          class="button is-fullwidth"
+          data-tooltip={i18n`load more templates before the first one`}
+          disabled={!hasMoreBefore}
+          onClick={onLoadMoreBefore}
+        >
+          <Translate>load newer templates</Translate>
+        </button>
+      )}
+      <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+        <thead>
+          <tr>
+            <th>
+              <Translate>ID</Translate>
+            </th>
+            <th>
+              <Translate>Description</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>
+                  <button
+                    class="button is-danger is-small has-tooltip-left"
+                    data-tooltip={i18n`delete selected templates from the 
database`}
+                    onClick={() => onDelete(i)}
+                  >
+                    Delete
+                  </button>
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+      {onLoadMoreAfter && (
+        <button
+          class="button is-fullwidth"
+          data-tooltip={i18n`load more templates after the last one`}
+          disabled={!hasMoreAfter}
+          onClick={onLoadMoreAfter}
+        >
+          <Translate>load older templates</Translate>
+        </button>
+      )}
+    </div>
+  );
+}
+
+function EmptyTable(): VNode {
+  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>
+        <Translate>
+          There is no templates yet, add more pressing the + sign
+        </Translate>
+      </p>
+    </div>
+  );
+}
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
new file mode 100644
index 000000000..0d58093d3
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/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 { 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 { HttpError } from "../../../../hooks/backend.js";
+import {
+  useInstanceTemplates,
+  useTemplateAPI,
+} from "../../../../hooks/templates.js";
+import { useTranslator } from "../../../../i18n/index.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+  onUnauthorized: () => VNode;
+  onLoadError: (error: HttpError) => VNode;
+  onNotFound: () => VNode;
+  onCreate: () => void;
+  onSelect: (id: string) => void;
+}
+
+export default function ListTemplates({
+  onUnauthorized,
+  onLoadError,
+  onCreate,
+  onSelect,
+  onNotFound,
+}: Props): VNode {
+  const [position, setPosition] = useState<string | undefined>(undefined);
+  const i18n = useTranslator();
+  const [notif, setNotif] = useState<Notification | undefined>(undefined);
+  const { deleteTemplate } = useTemplateAPI();
+  const result = useInstanceTemplates({ position }, (id) => setPosition(id));
+
+  if (result.clientError && result.isUnauthorized) return onUnauthorized();
+  if (result.clientError && result.isNotfound) return onNotFound();
+  if (result.loading) return <Loading />;
+  if (!result.ok) return onLoadError(result);
+
+  return (
+    <Fragment>
+      <NotificationCard notification={notif} />
+
+      <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);
+        }}
+        onDelete={(e: MerchantBackend.Template.TemplateEntry) =>
+          deleteTemplate(e.template_id)
+            .then(() =>
+              setNotif({
+                message: i18n`template delete successfully`,
+                type: "SUCCESS",
+              }),
+            )
+            .catch((error) =>
+              setNotif({
+                message: i18n`could not delete the template`,
+                type: "ERROR",
+                description: error.message,
+              }),
+            )
+        }
+      />
+    </Fragment>
+  );
+}
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
new file mode 100644
index 000000000..8d07cb31f
--- /dev/null
+++ 
b/packages/merchant-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/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
new file mode 100644
index 000000000..42d9e5825
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -0,0 +1,174 @@
+/*
+ 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 { 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 { ProductForm } from "../../../../components/product/ProductForm.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+import { Translate, useTranslator } from "../../../../i18n/index.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+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 = useTranslator();
+  const backend = useBackendContext();
+
+  const [state, setState] = useState<Partial<Entity>>(template);
+
+  const errors: FormErrors<Entity> = {
+    template_description: !state.template_description
+      ? i18n`should not be empty`
+      : undefined,
+    template_contract: !state.template_contract
+      ? undefined
+      : undefinedIfEmpty({
+          minimum_age:
+            state.template_contract.minimum_age < 0
+              ? i18n`should be greater that 0`
+              : undefined,
+          pay_duration: !state.template_contract.pay_duration
+            ? i18n`can't be empty`
+            : state.template_contract.pay_duration.d_us === "forever"
+            ? undefined
+            : state.template_contract.pay_duration.d_us < 1000
+            ? i18n`to short`
+            : 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">
+                    {backend.url}/instances/template/{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`Identifier`}
+                  tooltip={i18n`Name of the template in URLs.`}
+                />
+
+                <Input<Entity>
+                  name="template_description"
+                  label={i18n`Description`}
+                  help=""
+                  tooltip={i18n`Describe what this template stands for`}
+                />
+                <Input
+                  name="template_contract.summary"
+                  inputType="multiline"
+                  label={i18n`Order summary`}
+                  tooltip={i18n`Title of the order to be shown to the 
customer`}
+                />
+                <InputCurrency
+                  name="template_contract.amount"
+                  label={i18n`Order price`}
+                  tooltip={i18n`total product price added up`}
+                />
+                <InputNumber
+                  name="template_contract.minimum_age"
+                  label={i18n`Minimum age`}
+                  help=""
+                  tooltip={i18n`Is this contract restricted to some age?`}
+                />
+                <InputDuration
+                  name="template_contract.pay_duration"
+                  label={i18n`Payment timeout`}
+                  help=""
+                  tooltip={i18n`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}>
+                    <Translate>Cancel</Translate>
+                  </button>
+                )}
+                <AsyncButton
+                  disabled={hasErrors}
+                  data-tooltip={
+                    hasErrors
+                      ? i18n`Need to complete marked fields`
+                      : "confirm operation"
+                  }
+                  onClick={submitForm}
+                >
+                  <Translate>Confirm</Translate>
+                </AsyncButton>
+              </div>
+            </div>
+          </div>
+        </section>
+      </section>
+    </div>
+  );
+}
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
new file mode 100644
index 000000000..25dc9abdc
--- /dev/null
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.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 { 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 { HttpError } from "../../../../hooks/backend.js";
+import { useProductAPI, useProductDetails } from 
"../../../../hooks/product.js";
+import {
+  useTemplateAPI,
+  useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { useTranslator } from "../../../../i18n/index.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
+
+interface Props {
+  onBack?: () => void;
+  onConfirm: () => void;
+  onUnauthorized: () => VNode;
+  onNotFound: () => VNode;
+  onLoadError: (e: HttpError) => 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 = useTranslator();
+
+  if (result.clientError && result.isUnauthorized) return onUnauthorized();
+  if (result.clientError && result.isNotfound) return onNotFound();
+  if (result.loading) return <Loading />;
+  if (!result.ok) 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`could not update template`,
+                type: "ERROR",
+                description: error.message,
+              });
+            });
+        }}
+      />
+    </Fragment>
+  );
+}
diff --git a/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss 
b/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss
index 5fec21bf6..34c40092b 100644
--- a/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss
@@ -131,9 +131,9 @@
       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);
+      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);
@@ -147,9 +147,9 @@
       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);
+      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);
diff --git a/packages/merchant-backoffice-ui/src/utils/amount.ts 
b/packages/merchant-backoffice-ui/src/utils/amount.ts
index bdc37952f..93d6a3a4a 100644
--- a/packages/merchant-backoffice-ui/src/utils/amount.ts
+++ b/packages/merchant-backoffice-ui/src/utils/amount.ts
@@ -20,18 +20,6 @@ import {
 } from "@gnu-taler/taler-util";
 import { MerchantBackend } from "../declaration.js";
 
-/**
- * sums two prices,
- * @param one
- * @param two
- * @returns
- */
-const sumPrices = (one: string, two: string) => {
-  const [currency, valueOne] = one.split(":");
-  const [, valueTwo] = two.split(":");
-  return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`;
-};
-
 /**
  * merge refund with the same description and a difference less than one minute
  * @param prev list of refunds that will hold the merged refunds
@@ -41,7 +29,7 @@ const sumPrices = (one: string, two: string) => {
 export function mergeRefunds(
   prev: MerchantBackend.Orders.RefundDetails[],
   cur: MerchantBackend.Orders.RefundDetails,
-) {
+): MerchantBackend.Orders.RefundDetails[] {
   let tail;
 
   if (
@@ -54,19 +42,24 @@ export function mergeRefunds(
   ) {
     //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: sumPrices(tail.amount, cur.amount),
+    amount: Amounts.stringify(r),
   };
 
   return prev;
 }
 
-export const rate = (one: string, two: string) => {
+export const rate = (one: string, two: string): number => {
   const a = Amounts.parseOrThrow(one);
   const b = Amounts.parseOrThrow(two);
   const af = toFloat(a);
@@ -75,6 +68,6 @@ export const rate = (one: string, two: string) => {
   return af / bf;
 };
 
-function toFloat(amount: AmountJson) {
+function toFloat(amount: AmountJson): number {
   return amount.value + amount.fraction / amountFractionalBase;
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]