Step 4: Add admin UI page for pricing configuration
- Add pricing API functions to admin.ts - Create admin pricing page with form and validation - Add MANAGE_PRICING permission to auth context - Add pricing to admin navigation - Add translations for pricing page (en, ca, es) - Update PageLayout and Header types for new page
This commit is contained in:
parent
4d0dad8e2b
commit
d838d1be96
11 changed files with 509 additions and 5 deletions
403
frontend/app/admin/pricing/page.tsx
Normal file
403
frontend/app/admin/pricing/page.tsx
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { Permission } from "../../auth-context";
|
||||||
|
import { adminApi } from "../../api";
|
||||||
|
import { PageLayout } from "../../components/PageLayout";
|
||||||
|
import { ConfirmationButton } from "../../components/ConfirmationButton";
|
||||||
|
import { useRequireAuth } from "../../hooks/useRequireAuth";
|
||||||
|
import { useMutation } from "../../hooks/useMutation";
|
||||||
|
import { useTranslation } from "../../hooks/useTranslation";
|
||||||
|
import { components } from "../../generated/api";
|
||||||
|
import { cardStyles, formStyles, buttonStyles, bannerStyles } from "../../styles/shared";
|
||||||
|
|
||||||
|
type PricingConfigUpdate = components["schemas"]["PricingConfigUpdate"];
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
premium_buy: number;
|
||||||
|
premium_sell: number;
|
||||||
|
small_trade_threshold_eur: number;
|
||||||
|
small_trade_extra_premium: number;
|
||||||
|
eur_min_buy: number;
|
||||||
|
eur_max_buy: number;
|
||||||
|
eur_min_sell: number;
|
||||||
|
eur_max_sell: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidationErrors {
|
||||||
|
premium_buy?: string;
|
||||||
|
premium_sell?: string;
|
||||||
|
small_trade_threshold_eur?: string;
|
||||||
|
small_trade_extra_premium?: string;
|
||||||
|
eur_min_buy?: string;
|
||||||
|
eur_max_buy?: string;
|
||||||
|
eur_min_sell?: string;
|
||||||
|
eur_max_sell?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminPricingPage() {
|
||||||
|
const t = useTranslation("admin");
|
||||||
|
const tCommon = useTranslation("common");
|
||||||
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||||
|
requiredPermission: Permission.MANAGE_PRICING,
|
||||||
|
fallbackRedirect: "/",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormData | null>(null);
|
||||||
|
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const fetchConfig = useCallback(async () => {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await adminApi.getPricingConfig();
|
||||||
|
setFormData({
|
||||||
|
premium_buy: data.premium_buy,
|
||||||
|
premium_sell: data.premium_sell,
|
||||||
|
small_trade_threshold_eur: data.small_trade_threshold_eur,
|
||||||
|
small_trade_extra_premium: data.small_trade_extra_premium,
|
||||||
|
eur_min_buy: data.eur_min_buy,
|
||||||
|
eur_max_buy: data.eur_max_buy,
|
||||||
|
eur_min_sell: data.eur_min_sell,
|
||||||
|
eur_max_sell: data.eur_max_sell,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : t("pricing.errors.loadFailed"));
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && isAuthorized) {
|
||||||
|
fetchConfig();
|
||||||
|
}
|
||||||
|
}, [user, isAuthorized, fetchConfig]);
|
||||||
|
|
||||||
|
const validateField = (field: keyof FormData, value: number): string | undefined => {
|
||||||
|
switch (field) {
|
||||||
|
case "premium_buy":
|
||||||
|
case "premium_sell":
|
||||||
|
case "small_trade_extra_premium":
|
||||||
|
if (value < -100 || value > 100) {
|
||||||
|
return t("pricing.validation.premiumRange");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "eur_min_buy":
|
||||||
|
case "eur_min_sell":
|
||||||
|
case "eur_max_buy":
|
||||||
|
case "eur_max_sell":
|
||||||
|
case "small_trade_threshold_eur":
|
||||||
|
if (value <= 0) {
|
||||||
|
return t("pricing.validation.positive");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = (data: FormData): ValidationErrors => {
|
||||||
|
const newErrors: ValidationErrors = {};
|
||||||
|
|
||||||
|
// Validate individual fields
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
const error = validateField(key as keyof FormData, value);
|
||||||
|
if (error) {
|
||||||
|
newErrors[key as keyof ValidationErrors] = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate min < max
|
||||||
|
if (data.eur_min_buy >= data.eur_max_buy) {
|
||||||
|
newErrors.eur_min_buy = t("pricing.validation.minMaxBuy");
|
||||||
|
newErrors.eur_max_buy = t("pricing.validation.minMaxBuy");
|
||||||
|
}
|
||||||
|
if (data.eur_min_sell >= data.eur_max_sell) {
|
||||||
|
newErrors.eur_min_sell = t("pricing.validation.minMaxSell");
|
||||||
|
newErrors.eur_max_sell = t("pricing.validation.minMaxSell");
|
||||||
|
}
|
||||||
|
|
||||||
|
return newErrors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof FormData, value: string) => {
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
const numValue = parseInt(value, 10);
|
||||||
|
if (isNaN(numValue)) return;
|
||||||
|
|
||||||
|
const newData = { ...formData, [field]: numValue };
|
||||||
|
setFormData(newData);
|
||||||
|
|
||||||
|
// Clear error for this field
|
||||||
|
const newErrors = { ...errors };
|
||||||
|
delete newErrors[field];
|
||||||
|
setErrors(newErrors);
|
||||||
|
|
||||||
|
// Validate field
|
||||||
|
const error = validateField(field, numValue);
|
||||||
|
if (error) {
|
||||||
|
newErrors[field] = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate min < max if relevant
|
||||||
|
if (field === "eur_min_buy" || field === "eur_max_buy") {
|
||||||
|
if (newData.eur_min_buy >= newData.eur_max_buy) {
|
||||||
|
newErrors.eur_min_buy = t("pricing.validation.minMaxBuy");
|
||||||
|
newErrors.eur_max_buy = t("pricing.validation.minMaxBuy");
|
||||||
|
} else {
|
||||||
|
delete newErrors.eur_min_buy;
|
||||||
|
delete newErrors.eur_max_buy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field === "eur_min_sell" || field === "eur_max_sell") {
|
||||||
|
if (newData.eur_min_sell >= newData.eur_max_sell) {
|
||||||
|
newErrors.eur_min_sell = t("pricing.validation.minMaxSell");
|
||||||
|
newErrors.eur_max_sell = t("pricing.validation.minMaxSell");
|
||||||
|
} else {
|
||||||
|
delete newErrors.eur_min_sell;
|
||||||
|
delete newErrors.eur_max_sell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: updateConfig,
|
||||||
|
isLoading: isSaving,
|
||||||
|
error: saveError,
|
||||||
|
} = useMutation((data: PricingConfigUpdate) => adminApi.updatePricingConfig(data), {
|
||||||
|
onSuccess: () => {
|
||||||
|
setSuccess(true);
|
||||||
|
setError(null);
|
||||||
|
fetchConfig();
|
||||||
|
setTimeout(() => setSuccess(false), 3000);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err instanceof Error ? err.message : t("pricing.errors.saveFailed"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
const validationErrors = validateForm(formData);
|
||||||
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
|
setErrors(validationErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig({
|
||||||
|
premium_buy: formData.premium_buy,
|
||||||
|
premium_sell: formData.premium_sell,
|
||||||
|
small_trade_threshold_eur: formData.small_trade_threshold_eur,
|
||||||
|
small_trade_extra_premium: formData.small_trade_extra_premium,
|
||||||
|
eur_min_buy: formData.eur_min_buy,
|
||||||
|
eur_max_buy: formData.eur_max_buy,
|
||||||
|
eur_min_sell: formData.eur_min_sell,
|
||||||
|
eur_max_sell: formData.eur_max_sell,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading || !formData) {
|
||||||
|
return (
|
||||||
|
<PageLayout currentPage="admin-pricing" isLoading={isLoading} isAuthorized={isAuthorized}>
|
||||||
|
<div />
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasErrors = Object.keys(errors).length > 0;
|
||||||
|
const displayError = error || saveError;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout
|
||||||
|
currentPage="admin-pricing"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isAuthorized={isAuthorized}
|
||||||
|
error={displayError}
|
||||||
|
>
|
||||||
|
<div style={cardStyles.card}>
|
||||||
|
<h1 style={cardStyles.title}>{t("pricing.title")}</h1>
|
||||||
|
<p style={cardStyles.subtitle}>{t("pricing.subtitle")}</p>
|
||||||
|
|
||||||
|
{success && <div style={bannerStyles.success}>{t("pricing.success")}</div>}
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={formStyles.section}>
|
||||||
|
<h2 style={formStyles.sectionTitle}>{t("pricing.premiumSettings")}</h2>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.premiumBuy")} (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.premium_buy}
|
||||||
|
onChange={(e) => handleFieldChange("premium_buy", e.target.value)}
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.premium_buy ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.premium_buy && <div style={formStyles.error}>{errors.premium_buy}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.premiumSell")} (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.premium_sell}
|
||||||
|
onChange={(e) => handleFieldChange("premium_sell", e.target.value)}
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.premium_sell ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.premium_sell && <div style={formStyles.error}>{errors.premium_sell}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.smallTradeThreshold")} (EUR)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.small_trade_threshold_eur / 100}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFieldChange(
|
||||||
|
"small_trade_threshold_eur",
|
||||||
|
(parseFloat(e.target.value) * 100).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.small_trade_threshold_eur ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.small_trade_threshold_eur && (
|
||||||
|
<div style={formStyles.error}>{errors.small_trade_threshold_eur}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.smallTradeExtraPremium")} (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.small_trade_extra_premium}
|
||||||
|
onChange={(e) => handleFieldChange("small_trade_extra_premium", e.target.value)}
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.small_trade_extra_premium ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.small_trade_extra_premium && (
|
||||||
|
<div style={formStyles.error}>{errors.small_trade_extra_premium}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.section}>
|
||||||
|
<h2 style={formStyles.sectionTitle}>{t("pricing.tradeLimitsBuy")}</h2>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.minAmount")} (EUR)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.eur_min_buy / 100}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFieldChange("eur_min_buy", (parseFloat(e.target.value) * 100).toString())
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.eur_min_buy ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.eur_min_buy && <div style={formStyles.error}>{errors.eur_min_buy}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.maxAmount")} (EUR)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.eur_max_buy / 100}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFieldChange("eur_max_buy", (parseFloat(e.target.value) * 100).toString())
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.eur_max_buy ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.eur_max_buy && <div style={formStyles.error}>{errors.eur_max_buy}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.section}>
|
||||||
|
<h2 style={formStyles.sectionTitle}>{t("pricing.tradeLimitsSell")}</h2>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.minAmount")} (EUR)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.eur_min_sell / 100}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFieldChange("eur_min_sell", (parseFloat(e.target.value) * 100).toString())
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.eur_min_sell ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.eur_min_sell && <div style={formStyles.error}>{errors.eur_min_sell}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.field}>
|
||||||
|
<label style={formStyles.label}>{t("pricing.maxAmount")} (EUR)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.eur_max_sell / 100}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFieldChange("eur_max_sell", (parseFloat(e.target.value) * 100).toString())
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
style={{
|
||||||
|
...formStyles.input,
|
||||||
|
...(errors.eur_max_sell ? formStyles.inputError : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.eur_max_sell && <div style={formStyles.error}>{errors.eur_max_sell}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={formStyles.actions}>
|
||||||
|
<ConfirmationButton
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={hasErrors || isSaving}
|
||||||
|
confirmMessage={t("pricing.confirmSave")}
|
||||||
|
style={{
|
||||||
|
...buttonStyles.primary,
|
||||||
|
...(hasErrors || isSaving ? buttonStyles.disabled : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSaving ? tCommon("saving") : t("pricing.save")}
|
||||||
|
</ConfirmationButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,8 @@ type PaginatedInvites = components["schemas"]["PaginatedResponse_InviteResponse_
|
||||||
type AdminExchangeResponse = components["schemas"]["AdminExchangeResponse"];
|
type AdminExchangeResponse = components["schemas"]["AdminExchangeResponse"];
|
||||||
type AvailabilityResponse = components["schemas"]["AvailabilityResponse"];
|
type AvailabilityResponse = components["schemas"]["AvailabilityResponse"];
|
||||||
type PriceHistoryRecord = components["schemas"]["PriceHistoryResponse"];
|
type PriceHistoryRecord = components["schemas"]["PriceHistoryResponse"];
|
||||||
|
type PricingConfigResponse = components["schemas"]["PricingConfigResponse"];
|
||||||
|
type PricingConfigUpdate = components["schemas"]["PricingConfigUpdate"];
|
||||||
|
|
||||||
interface CreateInviteRequest {
|
interface CreateInviteRequest {
|
||||||
godfather_id: number;
|
godfather_id: number;
|
||||||
|
|
@ -153,4 +155,18 @@ export const adminApi = {
|
||||||
fetchPrice(): Promise<PriceHistoryRecord> {
|
fetchPrice(): Promise<PriceHistoryRecord> {
|
||||||
return client.post<PriceHistoryRecord>("/api/audit/price-history/fetch", {});
|
return client.post<PriceHistoryRecord>("/api/audit/price-history/fetch", {});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current pricing configuration
|
||||||
|
*/
|
||||||
|
getPricingConfig(): Promise<PricingConfigResponse> {
|
||||||
|
return client.get<PricingConfigResponse>("/api/admin/pricing");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update pricing configuration
|
||||||
|
*/
|
||||||
|
updatePricingConfig(request: PricingConfigUpdate): Promise<PricingConfigResponse> {
|
||||||
|
return client.put<PricingConfigResponse>("/api/admin/pricing", request);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export const Permission: Record<string, PermissionType> = {
|
||||||
CANCEL_OWN_EXCHANGE: "cancel_own_exchange",
|
CANCEL_OWN_EXCHANGE: "cancel_own_exchange",
|
||||||
// Availability/Exchange permissions (admin)
|
// Availability/Exchange permissions (admin)
|
||||||
MANAGE_AVAILABILITY: "manage_availability",
|
MANAGE_AVAILABILITY: "manage_availability",
|
||||||
|
MANAGE_PRICING: "manage_pricing",
|
||||||
VIEW_ALL_EXCHANGES: "view_all_exchanges",
|
VIEW_ALL_EXCHANGES: "view_all_exchanges",
|
||||||
CANCEL_ANY_EXCHANGE: "cancel_any_exchange",
|
CANCEL_ANY_EXCHANGE: "cancel_any_exchange",
|
||||||
COMPLETE_EXCHANGE: "complete_exchange",
|
COMPLETE_EXCHANGE: "complete_exchange",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ type PageId =
|
||||||
| "admin-invites"
|
| "admin-invites"
|
||||||
| "admin-availability"
|
| "admin-availability"
|
||||||
| "admin-trades"
|
| "admin-trades"
|
||||||
| "admin-price-history";
|
| "admin-price-history"
|
||||||
|
| "admin-pricing";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
currentPage: PageId;
|
currentPage: PageId;
|
||||||
|
|
@ -48,6 +49,7 @@ const ADMIN_NAV_ITEMS: NavItem[] = [
|
||||||
},
|
},
|
||||||
{ id: "admin-invites", labelKey: "invites", href: "/admin/invites", adminOnly: true },
|
{ id: "admin-invites", labelKey: "invites", href: "/admin/invites", adminOnly: true },
|
||||||
{ id: "admin-price-history", labelKey: "prices", href: "/admin/price-history", adminOnly: true },
|
{ id: "admin-price-history", labelKey: "prices", href: "/admin/price-history", adminOnly: true },
|
||||||
|
{ id: "admin-pricing", labelKey: "pricing", href: "/admin/pricing", adminOnly: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Header({ currentPage }: HeaderProps) {
|
export function Header({ currentPage }: HeaderProps) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ type PageId =
|
||||||
| "admin-invites"
|
| "admin-invites"
|
||||||
| "admin-availability"
|
| "admin-availability"
|
||||||
| "admin-trades"
|
| "admin-trades"
|
||||||
| "admin-price-history";
|
| "admin-price-history"
|
||||||
|
| "admin-pricing";
|
||||||
|
|
||||||
interface PageLayoutProps {
|
interface PageLayoutProps {
|
||||||
/** Current page ID for navigation highlighting */
|
/** Current page ID for navigation highlighting */
|
||||||
|
|
|
||||||
|
|
@ -109,5 +109,31 @@
|
||||||
"clearFailed": "Error en netejar",
|
"clearFailed": "Error en netejar",
|
||||||
"copyFailed": "Error en copiar"
|
"copyFailed": "Error en copiar"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"title": "Configuració de Preus",
|
||||||
|
"subtitle": "Configura els preus de prima i els límits d'import de les operacions",
|
||||||
|
"premiumSettings": "Configuració de Prima",
|
||||||
|
"premiumBuy": "Prima per COMPRAR",
|
||||||
|
"premiumSell": "Prima per VENDRE",
|
||||||
|
"smallTradeThreshold": "Umbral d'Operacions Petites",
|
||||||
|
"smallTradeExtraPremium": "Prima Extra per Operacions Petites",
|
||||||
|
"tradeLimitsBuy": "Límits d'Import d'Operacions (COMPRAR)",
|
||||||
|
"tradeLimitsSell": "Límits d'Import d'Operacions (VENDRE)",
|
||||||
|
"minAmount": "Import Mínim",
|
||||||
|
"maxAmount": "Import Màxim",
|
||||||
|
"save": "Guardar Canvis",
|
||||||
|
"success": "Configuració de preus guardada correctament",
|
||||||
|
"confirmSave": "Estàs segur que vols guardar aquests canvis de preus? Això afectarà totes les noves operacions immediatament.",
|
||||||
|
"validation": {
|
||||||
|
"premiumRange": "La prima ha de ser entre -100% i 100%",
|
||||||
|
"positive": "L'import ha de ser positiu",
|
||||||
|
"minMaxBuy": "El mínim ha de ser menor que el màxim per COMPRAR",
|
||||||
|
"minMaxSell": "El mínim ha de ser menor que el màxim per VENDRE"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"loadFailed": "Error en carregar la configuració de preus",
|
||||||
|
"saveFailed": "Error en guardar la configuració de preus"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@
|
||||||
"trades": "Operacions",
|
"trades": "Operacions",
|
||||||
"availability": "Disponibilitat",
|
"availability": "Disponibilitat",
|
||||||
"invites": "Invitacions",
|
"invites": "Invitacions",
|
||||||
"prices": "Preus"
|
"prices": "Preus",
|
||||||
|
"pricing": "Preus"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,5 +109,31 @@
|
||||||
"clearFailed": "Failed to clear",
|
"clearFailed": "Failed to clear",
|
||||||
"copyFailed": "Failed to copy"
|
"copyFailed": "Failed to copy"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"title": "Pricing Configuration",
|
||||||
|
"subtitle": "Configure premium pricing and trade amount limits",
|
||||||
|
"premiumSettings": "Premium Settings",
|
||||||
|
"premiumBuy": "Premium for BUY",
|
||||||
|
"premiumSell": "Premium for SELL",
|
||||||
|
"smallTradeThreshold": "Small Trade Threshold",
|
||||||
|
"smallTradeExtraPremium": "Extra Premium for Small Trades",
|
||||||
|
"tradeLimitsBuy": "Trade Amount Limits (BUY)",
|
||||||
|
"tradeLimitsSell": "Trade Amount Limits (SELL)",
|
||||||
|
"minAmount": "Minimum Amount",
|
||||||
|
"maxAmount": "Maximum Amount",
|
||||||
|
"save": "Save Changes",
|
||||||
|
"success": "Pricing configuration saved successfully",
|
||||||
|
"confirmSave": "Are you sure you want to save these pricing changes? This will affect all new trades immediately.",
|
||||||
|
"validation": {
|
||||||
|
"premiumRange": "Premium must be between -100% and 100%",
|
||||||
|
"positive": "Amount must be positive",
|
||||||
|
"minMaxBuy": "Minimum must be less than maximum for BUY",
|
||||||
|
"minMaxSell": "Minimum must be less than maximum for SELL"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"loadFailed": "Failed to load pricing configuration",
|
||||||
|
"saveFailed": "Failed to save pricing configuration"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@
|
||||||
"trades": "Trades",
|
"trades": "Trades",
|
||||||
"availability": "Availability",
|
"availability": "Availability",
|
||||||
"invites": "Invites",
|
"invites": "Invites",
|
||||||
"prices": "Prices"
|
"prices": "Prices",
|
||||||
|
"pricing": "Pricing"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,5 +109,31 @@
|
||||||
"clearFailed": "Error al limpiar",
|
"clearFailed": "Error al limpiar",
|
||||||
"copyFailed": "Error al copiar"
|
"copyFailed": "Error al copiar"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"title": "Configuración de Precios",
|
||||||
|
"subtitle": "Configura los precios de prima y los límites de importe de las operaciones",
|
||||||
|
"premiumSettings": "Configuración de Prima",
|
||||||
|
"premiumBuy": "Prima para COMPRAR",
|
||||||
|
"premiumSell": "Prima para VENDER",
|
||||||
|
"smallTradeThreshold": "Umbral de Operaciones Pequeñas",
|
||||||
|
"smallTradeExtraPremium": "Prima Extra para Operaciones Pequeñas",
|
||||||
|
"tradeLimitsBuy": "Límites de Importe de Operaciones (COMPRAR)",
|
||||||
|
"tradeLimitsSell": "Límites de Importe de Operaciones (VENDER)",
|
||||||
|
"minAmount": "Importe Mínimo",
|
||||||
|
"maxAmount": "Importe Máximo",
|
||||||
|
"save": "Guardar Cambios",
|
||||||
|
"success": "Configuración de precios guardada correctamente",
|
||||||
|
"confirmSave": "¿Estás seguro de que quieres guardar estos cambios de precios? Esto afectará todas las nuevas operaciones inmediatamente.",
|
||||||
|
"validation": {
|
||||||
|
"premiumRange": "La prima debe estar entre -100% y 100%",
|
||||||
|
"positive": "El importe debe ser positivo",
|
||||||
|
"minMaxBuy": "El mínimo debe ser menor que el máximo para COMPRAR",
|
||||||
|
"minMaxSell": "El mínimo debe ser menor que el máximo para VENDER"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"loadFailed": "Error al cargar la configuración de precios",
|
||||||
|
"saveFailed": "Error al guardar la configuración de precios"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@
|
||||||
"trades": "Operaciones",
|
"trades": "Operaciones",
|
||||||
"availability": "Disponibilidad",
|
"availability": "Disponibilidad",
|
||||||
"invites": "Invitaciones",
|
"invites": "Invitaciones",
|
||||||
"prices": "Precios"
|
"prices": "Precios",
|
||||||
|
"pricing": "Precios"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue