templates
This commit is contained in:
parent
45691dc991
commit
cf7f819685
@ -43,6 +43,9 @@ import ProductListPage from "./paths/instance/products/list/index.js";
|
|||||||
import ProductUpdatePage from "./paths/instance/products/update/index.js";
|
import ProductUpdatePage from "./paths/instance/products/update/index.js";
|
||||||
import TransferListPage from "./paths/instance/transfers/list/index.js";
|
import TransferListPage from "./paths/instance/transfers/list/index.js";
|
||||||
import TransferCreatePage from "./paths/instance/transfers/create/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 ReservesCreatePage from "./paths/instance/reserves/create/index.js";
|
||||||
import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
|
import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
|
||||||
import ReservesListPage from "./paths/instance/reserves/list/index.js";
|
import ReservesListPage from "./paths/instance/reserves/list/index.js";
|
||||||
@ -78,6 +81,10 @@ export enum InstancePaths {
|
|||||||
|
|
||||||
transfers_list = "/transfers",
|
transfers_list = "/transfers",
|
||||||
transfers_new = "/transfer/new",
|
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
|
// 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} />
|
<Route path="/" component={Redirect} to={InstancePaths.order_list} />
|
||||||
|
|
||||||
{/**
|
{/**
|
||||||
* Admin pages
|
* Admin pages
|
||||||
*/}
|
*/}
|
||||||
@ -236,7 +242,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{admin && (
|
{admin && (
|
||||||
<Route
|
<Route
|
||||||
path={AdminPaths.new_instance}
|
path={AdminPaths.new_instance}
|
||||||
@ -247,7 +252,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{admin && (
|
{admin && (
|
||||||
<Route
|
<Route
|
||||||
path={AdminPaths.update_instance}
|
path={AdminPaths.update_instance}
|
||||||
@ -261,7 +265,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
onNotFound={NotFoundPage}
|
onNotFound={NotFoundPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/**
|
{/**
|
||||||
* Update instance page
|
* Update instance page
|
||||||
*/}
|
*/}
|
||||||
@ -279,7 +282,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/**
|
{/**
|
||||||
* Product pages
|
* Product pages
|
||||||
*/}
|
*/}
|
||||||
@ -319,7 +321,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
route(InstancePaths.product_list);
|
route(InstancePaths.product_list);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/**
|
{/**
|
||||||
* Order pages
|
* Order pages
|
||||||
*/}
|
*/}
|
||||||
@ -356,7 +357,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
route(InstancePaths.order_list);
|
route(InstancePaths.order_list);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/**
|
{/**
|
||||||
* Transfer pages
|
* Transfer pages
|
||||||
*/}
|
*/}
|
||||||
@ -370,7 +370,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
route(InstancePaths.transfers_new);
|
route(InstancePaths.transfers_new);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={InstancePaths.transfers_new}
|
path={InstancePaths.transfers_new}
|
||||||
component={TransferCreatePage}
|
component={TransferCreatePage}
|
||||||
@ -381,6 +380,45 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
route(InstancePaths.transfers_list);
|
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
|
* reserves pages
|
||||||
@ -398,7 +436,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
route(InstancePaths.reserves_new);
|
route(InstancePaths.reserves_new);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={InstancePaths.reserves_details}
|
path={InstancePaths.reserves_details}
|
||||||
component={ReservesDetailsPage}
|
component={ReservesDetailsPage}
|
||||||
@ -409,7 +446,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
route(InstancePaths.reserves_list);
|
route(InstancePaths.reserves_list);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={InstancePaths.reserves_new}
|
path={InstancePaths.reserves_new}
|
||||||
component={ReservesCreatePage}
|
component={ReservesCreatePage}
|
||||||
@ -420,7 +456,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
|
|||||||
route(InstancePaths.reserves_list);
|
route(InstancePaths.reserves_list);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path={InstancePaths.kyc} component={ListKYCPage} />
|
<Route path={InstancePaths.kyc} component={ListKYCPage} />
|
||||||
{/**
|
{/**
|
||||||
* Example pages
|
* Example pages
|
||||||
|
@ -51,12 +51,6 @@ export function Sidebar({
|
|||||||
|
|
||||||
const kycStatus = useInstanceKYCDetails();
|
const kycStatus = useInstanceKYCDetails();
|
||||||
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
|
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
|
||||||
// const withInstanceIdIfNeeded = useCallback(function (path: string) {
|
|
||||||
// if (mimic) {
|
|
||||||
// return path + '?instance=' + instance
|
|
||||||
// }
|
|
||||||
// return path
|
|
||||||
// },[instance])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside class="aside is-placed-left is-expanded">
|
<aside class="aside is-placed-left is-expanded">
|
||||||
@ -130,6 +124,16 @@ export function Sidebar({
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<a href={"/reserves"} class="has-icon">
|
<a href={"/reserves"} class="has-icon">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
|
@ -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 {
|
interface ContractTerms {
|
||||||
// Human-readable description of the whole purchase
|
// Human-readable description of the whole purchase
|
||||||
summary: string;
|
summary: string;
|
||||||
|
280
packages/merchant-backoffice-ui/src/hooks/templates.ts
Normal file
280
packages/merchant-backoffice-ui/src/hooks/templates.ts
Normal file
@ -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 };
|
||||||
|
}
|
@ -23,7 +23,7 @@ import { h, VNode, Fragment } from "preact";
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Loading } from "../../../../components/exception/loading.js";
|
import { Loading } from "../../../../components/exception/loading.js";
|
||||||
import { NotificationCard } from "../../../../components/menu/index.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 { HttpError } from "../../../../hooks/backend.js";
|
||||||
import {
|
import {
|
||||||
InstanceOrderFilter,
|
InstanceOrderFilter,
|
||||||
@ -44,7 +44,7 @@ interface Props {
|
|||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ({
|
export default function OrderList({
|
||||||
onUnauthorized,
|
onUnauthorized,
|
||||||
onLoadError,
|
onLoadError,
|
||||||
onCreate,
|
onCreate,
|
||||||
@ -56,13 +56,19 @@ export default function ({
|
|||||||
MerchantBackend.Orders.OrderHistoryEntry | undefined
|
MerchantBackend.Orders.OrderHistoryEntry | undefined
|
||||||
>(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 result = useInstanceOrders(filter, setNewDate);
|
||||||
const { refundOrder, getPaymentURL } = useOrderAPI();
|
const { refundOrder, getPaymentURL } = useOrderAPI();
|
||||||
|
|
||||||
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
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.isUnauthorized) return onUnauthorized();
|
||||||
if (result.clientError && result.isNotfound) return onNotFound();
|
if (result.clientError && result.isNotfound) return onNotFound();
|
||||||
if (result.loading) return <Loading />;
|
if (result.loading) return <Loading />;
|
||||||
@ -78,12 +84,7 @@ export default function ({
|
|||||||
? "is-active"
|
? "is-active"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const i18n = useTranslator();
|
async function testIfOrderExistAndSelect(orderId: string): Promise<void> {
|
||||||
const [errorOrderId, setErrorOrderId] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function testIfOrderExistAndSelect(orderId: string) {
|
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
setErrorOrderId(i18n`Enter an order id`);
|
setErrorOrderId(i18n`Enter an order id`);
|
||||||
return;
|
return;
|
||||||
@ -189,7 +190,7 @@ function RefundModalForTable({
|
|||||||
onNotFound,
|
onNotFound,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: RefundProps) {
|
}: RefundProps): VNode {
|
||||||
const result = useOrderDetails(id);
|
const result = useOrderDetails(id);
|
||||||
|
|
||||||
if (result.clientError && result.isUnauthorized) return onUnauthorized();
|
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);
|
return navigator.clipboard.writeText(text);
|
||||||
}
|
}
|
||||||
|
@ -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"],
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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,
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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" },
|
||||||
|
},
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -131,9 +131,9 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
// there's probably a better way to do this, but wanted to try out CSS grid
|
// 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(
|
grid-template-columns:
|
||||||
100% / 7
|
calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
|
||||||
) calc(100% / 7) calc(100% / 7) calc(100% / 7);
|
calc(100% / 7) calc(100% / 7) calc(100% / 7);
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: var(--secondary-text-color-dark);
|
color: var(--secondary-text-color-dark);
|
||||||
@ -147,9 +147,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(
|
grid-template-columns:
|
||||||
100% / 7
|
calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
|
||||||
) calc(100% / 7) calc(100% / 7) calc(100% / 7);
|
calc(100% / 7) calc(100% / 7) calc(100% / 7);
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: var(--primary-text-color-dark);
|
color: var(--primary-text-color-dark);
|
||||||
|
@ -20,18 +20,6 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { MerchantBackend } from "../declaration.js";
|
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
|
* 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 prev list of refunds that will hold the merged refunds
|
||||||
@ -41,7 +29,7 @@ const sumPrices = (one: string, two: string) => {
|
|||||||
export function mergeRefunds(
|
export function mergeRefunds(
|
||||||
prev: MerchantBackend.Orders.RefundDetails[],
|
prev: MerchantBackend.Orders.RefundDetails[],
|
||||||
cur: MerchantBackend.Orders.RefundDetails,
|
cur: MerchantBackend.Orders.RefundDetails,
|
||||||
) {
|
): MerchantBackend.Orders.RefundDetails[] {
|
||||||
let tail;
|
let tail;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -54,19 +42,24 @@ export function mergeRefunds(
|
|||||||
) {
|
) {
|
||||||
//more than 1 minute difference
|
//more than 1 minute difference
|
||||||
|
|
||||||
|
//can't merge refunds, they are different or to distant in time
|
||||||
prev.push(cur);
|
prev.push(cur);
|
||||||
return prev;
|
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] = {
|
prev[prev.length - 1] = {
|
||||||
...tail,
|
...tail,
|
||||||
amount: sumPrices(tail.amount, cur.amount),
|
amount: Amounts.stringify(r),
|
||||||
};
|
};
|
||||||
|
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rate = (one: string, two: string) => {
|
export const rate = (one: string, two: string): number => {
|
||||||
const a = Amounts.parseOrThrow(one);
|
const a = Amounts.parseOrThrow(one);
|
||||||
const b = Amounts.parseOrThrow(two);
|
const b = Amounts.parseOrThrow(two);
|
||||||
const af = toFloat(a);
|
const af = toFloat(a);
|
||||||
@ -75,6 +68,6 @@ export const rate = (one: string, two: string) => {
|
|||||||
return af / bf;
|
return af / bf;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toFloat(amount: AmountJson) {
|
function toFloat(amount: AmountJson): number {
|
||||||
return amount.value + amount.fraction / amountFractionalBase;
|
return amount.value + amount.fraction / amountFractionalBase;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user