mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
feat(legal): wire recurrent-payments document + restore footer link
Backend now serves a dedicated recurring-payments legal document, so: - Add adminLegalPagesApi.get/updateRecurrentPayments and infoApi.getRecurrentPayments (+ InfoVisibility.recurrent). - AdminLegalPages: new 'Рекуррентные платежи' editor tab (DocumentEditor now driven by a DOCUMENT_API dispatch map instead of privacy/offer ternaries). - PublicLegal: add 'recurrent' doc; add public /recurrent-payments route. - Re-add the recurrent link to the login LegalFooter (was removed pending backend). - i18n: admin.legalPages.tabs.recurrent for ru/en/fa/zh (footer.recurrent already existed). type-check, eslint, prettier and build all pass.
This commit is contained in:
@@ -269,6 +269,7 @@ function App() {
|
|||||||
<Route path="/reset-password" element={<ResetPassword />} />
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
<Route path="/offer" element={<PublicLegal doc="offer" />} />
|
<Route path="/offer" element={<PublicLegal doc="offer" />} />
|
||||||
<Route path="/privacy" element={<PublicLegal doc="privacy" />} />
|
<Route path="/privacy" element={<PublicLegal doc="privacy" />} />
|
||||||
|
<Route path="/recurrent-payments" element={<PublicLegal doc="recurrent" />} />
|
||||||
<Route
|
<Route
|
||||||
path="/merge/:mergeToken"
|
path="/merge/:mergeToken"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -111,6 +111,23 @@ export const adminLegalPagesApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getRecurrentPayments: async (): Promise<LegalDocumentResponse> => {
|
||||||
|
const response = await apiClient.get<LegalDocumentResponse>(
|
||||||
|
'/cabinet/admin/legal-pages/recurrent-payments',
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRecurrentPayments: async (
|
||||||
|
data: LegalDocumentUpdateRequest,
|
||||||
|
): Promise<LegalDocumentResponse> => {
|
||||||
|
const response = await apiClient.put<LegalDocumentResponse>(
|
||||||
|
'/cabinet/admin/legal-pages/recurrent-payments',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
getRules: async (): Promise<AdminRulesResponse> => {
|
getRules: async (): Promise<AdminRulesResponse> => {
|
||||||
const response = await apiClient.get<AdminRulesResponse>('/cabinet/admin/legal-pages/rules');
|
const response = await apiClient.get<AdminRulesResponse>('/cabinet/admin/legal-pages/rules');
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export interface PublicOfferResponse {
|
|||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RecurrentPaymentsResponse {
|
||||||
|
content: string;
|
||||||
|
updated_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServiceInfo {
|
export interface ServiceInfo {
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -42,6 +47,7 @@ export interface InfoVisibility {
|
|||||||
rules: boolean;
|
rules: boolean;
|
||||||
privacy: boolean;
|
privacy: boolean;
|
||||||
offer: boolean;
|
offer: boolean;
|
||||||
|
recurrent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const infoApi = {
|
export const infoApi = {
|
||||||
@@ -75,6 +81,14 @@ export const infoApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get recurring-payments document
|
||||||
|
getRecurrentPayments: async (): Promise<RecurrentPaymentsResponse> => {
|
||||||
|
const response = await apiClient.get<RecurrentPaymentsResponse>(
|
||||||
|
'/cabinet/info/recurrent-payments',
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
// Get service info
|
// Get service info
|
||||||
getServiceInfo: async (): Promise<ServiceInfo> => {
|
getServiceInfo: async (): Promise<ServiceInfo> => {
|
||||||
const response = await apiClient.get<ServiceInfo>('/cabinet/info/service');
|
const response = await apiClient.get<ServiceInfo>('/cabinet/info/service');
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ interface LegalLink {
|
|||||||
fallback: string;
|
fallback: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurring-payments has no dedicated legal document in the backend yet, so it is
|
|
||||||
// intentionally omitted rather than pointed at unrelated content. Re-add once a
|
|
||||||
// `recurrent-payments` document + public endpoint exist.
|
|
||||||
const LINKS: LegalLink[] = [
|
const LINKS: LegalLink[] = [
|
||||||
{ href: '/offer', labelKey: 'footer.offer', fallback: 'Публичная оферта' },
|
{ href: '/offer', labelKey: 'footer.offer', fallback: 'Публичная оферта' },
|
||||||
{ href: '/privacy', labelKey: 'footer.privacy', fallback: 'Политика конфиденциальности' },
|
{ href: '/privacy', labelKey: 'footer.privacy', fallback: 'Политика конфиденциальности' },
|
||||||
|
{ href: '/recurrent-payments', labelKey: 'footer.recurrent', fallback: 'Рекуррентные платежи' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface LegalFooterProps {
|
interface LegalFooterProps {
|
||||||
|
|||||||
@@ -4181,6 +4181,7 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
"offer": "Offer",
|
"offer": "Offer",
|
||||||
|
"recurrent": "Recurring Payments",
|
||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"faq": "FAQ"
|
"faq": "FAQ"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4006,6 +4006,7 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"privacy": "حریم خصوصی",
|
"privacy": "حریم خصوصی",
|
||||||
"offer": "پیشنهاد",
|
"offer": "پیشنهاد",
|
||||||
|
"recurrent": "پرداختهای تکراری",
|
||||||
"rules": "قوانین",
|
"rules": "قوانین",
|
||||||
"faq": "FAQ"
|
"faq": "FAQ"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4726,6 +4726,7 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"privacy": "Политика конф.",
|
"privacy": "Политика конф.",
|
||||||
"offer": "Оферта",
|
"offer": "Оферта",
|
||||||
|
"recurrent": "Рекуррентные платежи",
|
||||||
"rules": "Правила",
|
"rules": "Правила",
|
||||||
"faq": "FAQ"
|
"faq": "FAQ"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4005,6 +4005,7 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"privacy": "隐私",
|
"privacy": "隐私",
|
||||||
"offer": "条款",
|
"offer": "条款",
|
||||||
|
"recurrent": "定期付款",
|
||||||
"rules": "规则",
|
"rules": "规则",
|
||||||
"faq": "FAQ"
|
"faq": "FAQ"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,10 +14,35 @@ import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
|||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { ChevronDownIcon, ChevronUpIcon, PlusIcon, TrashIcon } from '@/components/icons';
|
import { ChevronDownIcon, ChevronUpIcon, PlusIcon, TrashIcon } from '@/components/icons';
|
||||||
|
|
||||||
type LegalTab = 'privacy' | 'offer' | 'rules' | 'faq';
|
type LegalTab = 'privacy' | 'offer' | 'recurrent' | 'rules' | 'faq';
|
||||||
|
|
||||||
const DISPLAY_MODES: LegalDisplayMode[] = ['bot', 'web', 'both'];
|
const DISPLAY_MODES: LegalDisplayMode[] = ['bot', 'web', 'both'];
|
||||||
|
|
||||||
|
type DocumentKind = 'privacy-policy' | 'public-offer' | 'recurrent-payments';
|
||||||
|
|
||||||
|
const DOCUMENT_API: Record<
|
||||||
|
DocumentKind,
|
||||||
|
{
|
||||||
|
get: () => ReturnType<typeof adminLegalPagesApi.getPrivacyPolicy>;
|
||||||
|
update: (
|
||||||
|
data: Parameters<typeof adminLegalPagesApi.updatePrivacyPolicy>[0],
|
||||||
|
) => ReturnType<typeof adminLegalPagesApi.updatePrivacyPolicy>;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
'privacy-policy': {
|
||||||
|
get: adminLegalPagesApi.getPrivacyPolicy,
|
||||||
|
update: adminLegalPagesApi.updatePrivacyPolicy,
|
||||||
|
},
|
||||||
|
'public-offer': {
|
||||||
|
get: adminLegalPagesApi.getPublicOffer,
|
||||||
|
update: adminLegalPagesApi.updatePublicOffer,
|
||||||
|
},
|
||||||
|
'recurrent-payments': {
|
||||||
|
get: adminLegalPagesApi.getRecurrentPayments,
|
||||||
|
update: adminLegalPagesApi.updateRecurrentPayments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function extractErrorDetail(err: unknown): string | null {
|
function extractErrorDetail(err: unknown): string | null {
|
||||||
const error = err as { response?: { data?: { detail?: unknown } } };
|
const error = err as { response?: { data?: { detail?: unknown } } };
|
||||||
const detail = error.response?.data?.detail;
|
const detail = error.response?.data?.detail;
|
||||||
@@ -103,7 +128,7 @@ function DocumentEditor({
|
|||||||
kind,
|
kind,
|
||||||
onDirtyChange,
|
onDirtyChange,
|
||||||
}: {
|
}: {
|
||||||
kind: 'privacy-policy' | 'public-offer';
|
kind: DocumentKind;
|
||||||
onDirtyChange: (dirty: boolean) => void;
|
onDirtyChange: (dirty: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -118,10 +143,7 @@ function DocumentEditor({
|
|||||||
|
|
||||||
const { data, isLoading, isFetching } = useQuery({
|
const { data, isLoading, isFetching } = useQuery({
|
||||||
queryKey: ['admin', 'legal-pages', kind],
|
queryKey: ['admin', 'legal-pages', kind],
|
||||||
queryFn: () =>
|
queryFn: () => DOCUMENT_API[kind].get(),
|
||||||
kind === 'privacy-policy'
|
|
||||||
? adminLegalPagesApi.getPrivacyPolicy()
|
|
||||||
: adminLegalPagesApi.getPublicOffer(),
|
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
});
|
});
|
||||||
@@ -167,9 +189,7 @@ function DocumentEditor({
|
|||||||
is_enabled: enabled[language] ?? false,
|
is_enabled: enabled[language] ?? false,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
return kind === 'privacy-policy'
|
return DOCUMENT_API[kind].update(payload);
|
||||||
? adminLegalPagesApi.updatePrivacyPolicy(payload)
|
|
||||||
: adminLegalPagesApi.updatePublicOffer(payload);
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
haptic.success();
|
haptic.success();
|
||||||
@@ -661,7 +681,7 @@ export default function AdminLegalPages() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{(['privacy', 'offer', 'rules', 'faq'] as const).map((tab) => (
|
{(['privacy', 'offer', 'recurrent', 'rules', 'faq'] as const).map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -684,6 +704,9 @@ export default function AdminLegalPages() {
|
|||||||
{activeTab === 'offer' && (
|
{activeTab === 'offer' && (
|
||||||
<DocumentEditor key="offer" kind="public-offer" onDirtyChange={setDirty} />
|
<DocumentEditor key="offer" kind="public-offer" onDirtyChange={setDirty} />
|
||||||
)}
|
)}
|
||||||
|
{activeTab === 'recurrent' && (
|
||||||
|
<DocumentEditor key="recurrent" kind="recurrent-payments" onDirtyChange={setDirty} />
|
||||||
|
)}
|
||||||
{activeTab === 'rules' && <RulesEditor onDirtyChange={setDirty} />}
|
{activeTab === 'rules' && <RulesEditor onDirtyChange={setDirty} />}
|
||||||
{activeTab === 'faq' && <FaqEditor onDirtyChange={setDirty} />}
|
{activeTab === 'faq' && <FaqEditor onDirtyChange={setDirty} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { infoApi } from '../api/info';
|
|||||||
import { formatContent } from '../utils/legalContent';
|
import { formatContent } from '../utils/legalContent';
|
||||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||||
|
|
||||||
export type PublicLegalDoc = 'offer' | 'privacy';
|
export type PublicLegalDoc = 'offer' | 'privacy' | 'recurrent';
|
||||||
|
|
||||||
interface PublicLegalProps {
|
interface PublicLegalProps {
|
||||||
doc: PublicLegalDoc;
|
doc: PublicLegalDoc;
|
||||||
@@ -32,6 +32,12 @@ const DOC_CONFIG: Record<
|
|||||||
titleFallback: 'Политика конфиденциальности',
|
titleFallback: 'Политика конфиденциальности',
|
||||||
fetch: infoApi.getPrivacyPolicy,
|
fetch: infoApi.getPrivacyPolicy,
|
||||||
},
|
},
|
||||||
|
recurrent: {
|
||||||
|
queryKey: 'recurrent-payments',
|
||||||
|
titleKey: 'footer.recurrent',
|
||||||
|
titleFallback: 'Рекуррентные платежи',
|
||||||
|
fetch: infoApi.getRecurrentPayments,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public, unauthenticated viewer for the legal documents linked from the login
|
// Public, unauthenticated viewer for the legal documents linked from the login
|
||||||
|
|||||||
Reference in New Issue
Block a user