commit 9b415001ff6332d97586bc3c8d64900f74c8587b
parent d9e4d8ec33936c27c6f101760b29de6ecfd1075e
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 21 Apr 2025 16:21:50 -0300
fix #9768 partially
Diffstat:
5 files changed, 178 insertions(+), 114 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/pages/RulesInfo.tsx b/packages/aml-backoffice-ui/src/pages/RulesInfo.tsx
@@ -15,12 +15,18 @@ import {
import { formatDuration, intervalToDuration } from "date-fns";
import { Fragment, h, VNode } from "preact";
+type KycRuleWithIdx = KycRule & {
+ idx: number;
+};
+
export function RulesInfo({
rules,
onEdit,
onRemove,
+ onNew,
}: {
rules: KycRule[];
+ onNew?: () => void;
onEdit?: (k: KycRule, idx: number) => void;
onRemove?: (k: KycRule, idx: number) => void;
}): VNode {
@@ -29,14 +35,26 @@ export function RulesInfo({
if (!rules.length) {
return (
- <Attention
- title={i18n.str`There are no rules for operations`}
- type="warning"
- >
- <i18n.Translate>
- This mean that all operation have no limit.
- </i18n.Translate>
- </Attention>
+ <div>
+ <Attention
+ title={i18n.str`There are no rules for operations`}
+ type="warning"
+ >
+ <i18n.Translate>
+ This mean that all operation have no limit.
+ </i18n.Translate>
+ </Attention>
+ {!onNew ? undefined : (
+ <button
+ onClick={() => {
+ onNew();
+ }}
+ class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
+ >
+ <i18n.Translate>Add custom rule</i18n.Translate>
+ </button>
+ )}
+ </div>
);
}
@@ -51,7 +69,9 @@ export function RulesInfo({
[LimitOperationType.merge]: true,
};
- const sorted = [...rules].sort((a, b) => {
+ const theRules = rules.map((r, idx): KycRuleWithIdx => ({ ...r, idx }));
+
+ const sorted = theRules.sort((a, b) => {
// to prevent iterate again we are using this sort function
// to save present operation type
OPERATION_TYPE_MISSING[a.operation_type] = false;
@@ -98,14 +118,38 @@ export function RulesInfo({
scope="col"
class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
>
- {/* <i18n.Translate>Actions</i18n.Translate> */}
+ {!onNew ? undefined : (
+ <button
+ onClick={() => {
+ onNew();
+ }}
+ class="rounded-md w-fit border-0 p-1 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
+ >
+ <i18n.Translate>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 4.5v15m7.5-7.5h-15"
+ />
+ </svg>
+ </i18n.Translate>
+ </button>
+ )}
</th>
)}
</tr>
</thead>
<tbody id="thetable" class="divide-y divide-gray-200 bg-white ">
- {sorted.map((r, idx) => {
+ {sorted.map((r) => {
return (
<tr class="even:bg-gray-200 ">
<td class="flex whitespace-nowrap py-2 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 text-left">
@@ -184,7 +228,7 @@ export function RulesInfo({
{!hasActions ? undefined : (
<td class="relative flex justify-end whitespace-nowrap py-2 pl-3 pr-4 text-sm font-medium sm:pr-6">
{!onEdit ? undefined : (
- <button onClick={() => onEdit(r, idx)}>
+ <button onClick={() => onEdit(r, r.idx)}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -202,7 +246,7 @@ export function RulesInfo({
</button>
)}
{!onRemove ? undefined : (
- <button onClick={() => onRemove(r, idx)}>
+ <button onClick={() => onRemove(r, r.idx)}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -35,9 +35,8 @@ const DEFAULT_MEASURE_IF_NONE = ["VERBOTEN"];
export const DEFAULT_LIMITS_WHEN_NEW_ACCOUNT: LegitimizationRuleSet = {
custom_measures: {},
expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()),
- rules:[],
-}
-
+ rules: [],
+};
/**
* Defined new limits for the account
@@ -114,6 +113,56 @@ export function Rules({
);
}
+function AddNewRuleForm({
+ onAdd,
+ onClose,
+ config,
+ measureList,
+}: {
+ onAdd: (nr: RuleFormType) => void;
+ config: ExchangeVersionResponse;
+ measureList: MeasureListWithId;
+ onClose: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const ruleFormDesign = ruleFormDesignTemplate(
+ i18n,
+ config.currency,
+ measureList,
+ );
+
+ const ruleForm = useForm<RuleFormType>(ruleFormDesign, {});
+ return (
+ <Fragment>
+ <h2 class="mt-4 mb-2">
+ <i18n.Translate>New rule form</i18n.Translate>
+ </h2>
+ <FormUI design={ruleFormDesign} model={ruleForm.model} />
+
+ <button
+ disabled={ruleForm.status.status === "fail"}
+ onClick={() => {
+ onAdd(ruleForm.status.result as RuleFormType);
+ onClose();
+ }}
+ class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
+ >
+ <i18n.Translate>Add</i18n.Translate>
+ </button>
+ <button
+ onClick={() => {
+ onClose();
+ }}
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </Fragment>
+ );
+}
+
+type MeasureListWithId = (MeasureInformation & { id: string })[];
+
function UpdateRulesForm({
config,
limits,
@@ -127,18 +176,10 @@ function UpdateRulesForm({
const [request, updateRequestField, updateRequest] =
useCurrentDecisionRequest();
const [showAddRuleForm, setShowAddRuleForm] = useState(false);
- const measureList = !rootMeasures
+ const measureList: MeasureListWithId = !rootMeasures
? []
: Object.entries(rootMeasures).map(([id, mi]) => ({ id, ...mi }));
- const ruleFormDesign = ruleFormDesignTemplate(
- i18n,
- config.currency,
- measureList,
- );
-
- const ruleForm = useForm<RuleFormType>(ruleFormDesign, {});
-
const expirationFormDesign = expirationFormDesignTemplate(i18n, measureList);
const expirationForm = useForm<ExpirationFormType>(expirationFormDesign, {
@@ -184,8 +225,20 @@ function UpdateRulesForm({
});
updateRequestField("rules", result);
}
+
return (
<div>
+ {!showAddRuleForm ? undefined : (
+ <AddNewRuleForm
+ measureList={measureList}
+ config={config}
+ onAdd={addNewRule}
+ onClose={() => {
+ setShowAddRuleForm(false);
+ }}
+ />
+ )}
+
<h2 class="mt-4 mb-2">
<i18n.Translate>New rules</i18n.Translate>
</h2>
@@ -197,6 +250,9 @@ function UpdateRulesForm({
nr.splice(idx, 1);
updateRequestField("rules", nr);
}}
+ onNew={() => {
+ setShowAddRuleForm(true);
+ }}
/>
<button
@@ -231,69 +287,32 @@ function UpdateRulesForm({
>
<i18n.Translate>Premium</i18n.Translate>
</button>
+ <h2 class="mt-4 mb-2">
+ <i18n.Translate>On expiration behavior</i18n.Translate>
+ </h2>
+ <FormUI design={expirationFormDesign} model={expirationForm.model} />
<button
onClick={() => {
- setShowAddRuleForm(true);
+ expirationForm.model
+ .getHandlerForAttributeKey("measure")
+ .onChange(limits.successor_measure ?? "");
}}
class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
>
- <i18n.Translate>Add new rule</i18n.Translate>
+ <i18n.Translate>Reset measure</i18n.Translate>
+ </button>
+ <button
+ onClick={() => {
+ const c =
+ expirationForm.model.getHandlerForAttributeKey("expiration");
+ c.onChange(
+ AbsoluteTime.fromProtocolTimestamp(limits.expiration_time),
+ );
+ }}
+ class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
+ >
+ <i18n.Translate>Reset expiration</i18n.Translate>
</button>
- {!showAddRuleForm ? (
- <Fragment>
- <h2 class="mt-4 mb-2">
- <i18n.Translate>On expiration behavior</i18n.Translate>
- </h2>
- <FormUI design={expirationFormDesign} model={expirationForm.model} />
- <button
- onClick={() => {
- expirationForm.model
- .getHandlerForAttributeKey("measure")
- .onChange(limits.successor_measure ?? "");
- }}
- class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
- >
- <i18n.Translate>Reset measure</i18n.Translate>
- </button>
- <button
- onClick={() => {
- const c =
- expirationForm.model.getHandlerForAttributeKey("expiration");
- c.onChange(
- AbsoluteTime.fromProtocolTimestamp(limits.expiration_time),
- );
- }}
- class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
- >
- <i18n.Translate>Reset expiration</i18n.Translate>
- </button>
- </Fragment>
- ) : (
- <Fragment>
- <h2 class="mt-4 mb-2">
- <i18n.Translate>New rule form</i18n.Translate>
- </h2>
- <FormUI design={ruleFormDesign} model={ruleForm.model} />
-
- <button
- disabled={ruleForm.status.status === "fail"}
- onClick={() => {
- addNewRule(ruleForm.status.result as RuleFormType);
- }}
- class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
- >
- <i18n.Translate>Add</i18n.Translate>
- </button>
- <button
- onClick={() => {
- setShowAddRuleForm(false);
- }}
- class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- </Fragment>
- )}
</div>
);
}
@@ -364,7 +383,7 @@ const ruleFormDesignTemplate = (
{
id: "timeframe",
type: "durationText",
- // required: true,
+ required: true,
placeholder: "1Y 2M 3D 4h 5m 6s",
label: i18n.str`Timeframe`,
help: `Use YMDhms next to a number as a unit for Year, Month, Day, hour, minute and seconds.`,
@@ -394,35 +413,6 @@ const ruleFormDesignTemplate = (
label: i18n.str`All measures`,
help: i18n.str`Hint the customer that all measure should be completed`,
},
- {
- type: "choiceHorizontal",
- label: i18n.str`Expiration`,
- help: i18n.str`Predefined shortcuts`,
- id: "expiration",
- choices: [
- {
- label: i18n.str`In a week`,
- value: AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ days: 7 }),
- ) as any,
- },
- {
- label: i18n.str`In a month`,
- value: AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ months: 1 }),
- ) as any,
- },
- {
- label: i18n.str`In a year`,
- value: AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ years: 1 }),
- ) as any,
- },
- ],
- },
],
});
const expirationFormDesignTemplate = (
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
@@ -280,6 +280,13 @@ export namespace Duration {
return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365);
}
+ /**
+ * FIXME: it should return undefined when the spec has no values
+ * @deprecated check fromSpecOrUndefined
+ *
+ * @param spec
+ * @returns
+ */
export function fromSpec(spec: {
seconds?: number;
minutes?: number;
@@ -298,6 +305,28 @@ export namespace Duration {
return { d_ms };
}
+ export function fromSpecOrUndefined(spec: {
+ seconds?: number;
+ minutes?: number;
+ hours?: number;
+ days?: number;
+ months?: number;
+ years?: number;
+ }): Duration | undefined {
+ if (
+ spec.seconds == undefined &&
+ spec.minutes == undefined &&
+ spec.hours == undefined &&
+ spec.days == undefined &&
+ spec.months == undefined &&
+ spec.years == undefined
+ ) {
+ return undefined;
+ }
+
+ return Duration.fromSpec(spec)
+ }
+
export function toSpec({ d_ms }: Duration):
| {
seconds: number;
diff --git a/packages/web-util/src/forms/fields/InputDurationText.stories.tsx b/packages/web-util/src/forms/fields/InputDurationText.stories.tsx
@@ -45,6 +45,7 @@ const design: FormDesign = {
type: "durationText",
label: "Age" as TranslatedString,
id: "age",
+ required: true,
tooltip: "just numbers" as TranslatedString,
},
],
diff --git a/packages/web-util/src/forms/fields/InputDurationText.tsx b/packages/web-util/src/forms/fields/InputDurationText.tsx
@@ -59,7 +59,7 @@ export function InputDurationText(props: UIFormProps<string>): VNode {
{...props}
converter={{
//@ts-ignore
- fromStringUI: (v): Duration => {
+ fromStringUI: (v): Duration | undefined => {
if (!v) return Duration.getForever();
const spec = v.split(" ").reduce((prev, cur) => {
const v = parseDurationValue(cur);
@@ -68,7 +68,7 @@ export function InputDurationText(props: UIFormProps<string>): VNode {
}
return prev;
}, {} as DurationSpec);
- return Duration.fromSpec(spec);
+ return Duration.fromSpecOrUndefined(spec);
},
//@ts-ignore
toStringUI: (v?: Duration): string => {