edit referrals actions

This commit is contained in:
smanylov
2026-02-25 18:17:08 +07:00
parent bc1d5d5337
commit d18071e533
10 changed files with 397 additions and 48 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.44.0", "version": "0.45.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
+117 -11
View File
@@ -2,7 +2,6 @@
import { getSessionData } from '@/app/actions/session'; import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL, bankCardFormSchema } from '@/app/actions/definitions'; import { API_BASE_URL, bankCardFormSchema } from '@/app/actions/definitions';
import { FormState } from '@/app/actions/auth';
export async function fetchReferralsLevels() { export async function fetchReferralsLevels() {
const token = await getSessionData('token'); const token = await getSessionData('token');
@@ -174,9 +173,52 @@ export async function referralRefill() {
} }
} }
export interface PayoutMethods {
cardNumber: string,
id: number,
type: string,
displayName: string,
maskedDetails: string,
default: boolean,
verified: boolean
}
export async function getPayoutMethods(): Promise<{
methods: PayoutMethods[],
error: string | null
}> {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/payouts/user/payout-methods`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
const parsed = await response.json();
return {
methods: parsed,
error: null
};
} else {
throw 'error';
}
} catch (error) {
return {
methods: [],
error: 'error'
};
}
}
export async function getUserPayments() { export async function getUserPayments() {
const token = await getSessionData('token'); const token = await getSessionData('token');
console.log('getUserPayments');
try { try {
const response = await fetch(`${API_BASE_URL}/api/payouts/requests`, { const response = await fetch(`${API_BASE_URL}/api/payouts/requests`, {
@@ -190,7 +232,6 @@ export async function getUserPayments() {
if (response.ok) { if (response.ok) {
const parsed = await response.json(); const parsed = await response.json();
console.log(parsed);
return { return {
payments: parsed, payments: parsed,
@@ -220,6 +261,7 @@ export async function addPayoutMethod(
formData: FormData, formData: FormData,
): Promise<PayoutMethodFormState> { ): Promise<PayoutMethodFormState> {
const token = await getSessionData('token'); const token = await getSessionData('token');
console.log('addPayoutMethod');
const bankCard = formData.get('bankCard') as string || ''; const bankCard = formData.get('bankCard') as string || '';
const paymentsType = formData.get('paymentsType') as 'bank-card' | 'bank-transfer' | 'on-account-balance'; const paymentsType = formData.get('paymentsType') as 'bank-card' | 'bank-transfer' | 'on-account-balance';
@@ -250,14 +292,6 @@ export async function addPayoutMethod(
}; };
} }
/* return {
previousState: {
bankCard,
paymentsType
},
error: null
}; */
try { try {
const { bankCard } = validatedFields.data; const { bankCard } = validatedFields.data;
@@ -306,3 +340,75 @@ export async function addPayoutMethod(
}; };
} }
} }
export type PayoutRequest = {
previousState: {
amount?: string | undefined;
} | undefined,
error: string | null
};
export async function createPayoutRequest(
state: PayoutRequest | undefined,
formData: FormData,
): Promise<PayoutRequest> {
const token = await getSessionData('token');
const amount = formData.get('amount') as string || '';
const payoutMethodId = formData.get('payoutMethodId') as string || '';
console.log('createPayoutRequest');
console.log(token);
try {
const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, {
method: 'POST',
body: JSON.stringify({
"amount": amount,
"payoutMethodId": payoutMethodId
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
console.log(response);
if (response.status === 400) {
const responseText = await response.text();
throw JSON.parse(responseText)?.message;
}
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
return {
previousState: {
amount
},
error: null
};
} else {
throw 'error-request';
}
} catch (error: unknown) {
if (error) {
const typedError = error as string;
return {
previousState: {
amount
},
error: typedError
};
}
return {
previousState: {
amount
},
error: 'error-request'
};
}
}
+13
View File
@@ -1394,6 +1394,15 @@
transition: all 0.3s ease; transition: all 0.3s ease;
white-space: nowrap; white-space: nowrap;
height: 48px; height: 48px;
&:disabled {
background: linear-gradient(135deg, v.$p-color-disabled, v.$s-color-disabled);
cursor: default;
}
}
.dropdown-button {
padding: 12px 16px;
} }
} }
} }
@@ -3963,6 +3972,10 @@
.loading-animation { .loading-animation {
display: block; display: block;
text-align: center; text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.global-spinner { .global-spinner {
display: block; display: block;
+2 -3
View File
@@ -106,12 +106,11 @@ export default function LoginForm() {
className={`${styles['btn']}`} className={`${styles['btn']}`}
disabled={isPending} disabled={isPending}
> >
{isPending ? ( {t("sign-in")}
{isPending && (
<div className="loading-animation"> <div className="loading-animation">
<div className="global-spinner"></div> <div className="global-spinner"></div>
</div> </div>
) : (
t("sign-in")
)} )}
</button> </button>
</form> </form>
+2 -3
View File
@@ -581,12 +581,11 @@ export default function RegisterForm() {
className={`${styles['btn']}`} className={`${styles['btn']}`}
disabled={isPending} disabled={isPending}
> >
{isPending ? ( {t("create-an-account")}
{isPending && (
<div className="loading-animation"> <div className="loading-animation">
<div className="global-spinner"></div> <div className="global-spinner"></div>
</div> </div>
) : (
t("create-an-account")
)} )}
</button> </button>
</form > </form >
+24 -4
View File
@@ -1,17 +1,36 @@
'use client' 'use client'
import { useState, ChangeEvent, useEffect } from 'react'; import { useState, ChangeEvent, useEffect } from 'react';
import { InputMask } from '@react-input/mask'; import { InputMask, format } from '@react-input/mask';
import styles from '@/app/styles/module/login.module.scss'; import styles from '@/app/styles/module/login.module.scss';
import { PayoutMethods } from '@/app/actions/referralsActions';
interface BankCardProps { interface BankCardProps {
bankCardState: string | undefined; bankCardState: string | undefined;
validateHandler: (e: ChangeEvent<HTMLInputElement>) => void; validateHandler: (e: ChangeEvent<HTMLInputElement>) => void;
clearCondition?: string | undefined | null clearCondition?: string | undefined | null;
disabled: boolean;
} }
export default function BankCardInput({ bankCardState, validateHandler }: BankCardProps) { export default function BankCardInput({ bankCardState, validateHandler, disabled }: BankCardProps) {
const [bankCard, setBankCard] = useState<string | undefined>(bankCardState ? bankCardState : ''); const [bankCard, setBankCard] = useState<string | undefined>('');
useEffect(() => {
if (bankCardState) {
if (disabled) {
setBankCard(format(bankCardState, {
mask: "**** **** **** ____",
replacement: { _: /\d/ }
}));
} else {
setBankCard(format(bankCardState, {
mask: "____ ____ ____ ____",
replacement: { _: /\d/ }
}));
}
}
}, [bankCardState, disabled]);
return ( return (
<InputMask <InputMask
@@ -22,6 +41,7 @@ export default function BankCardInput({ bankCardState, validateHandler }: BankCa
mask="____ ____ ____ ____" mask="____ ____ ____ ____"
replacement={{ _: /\d/ }} replacement={{ _: /\d/ }}
value={bankCard} value={bankCard}
disabled={disabled}
onChange={(e) => { onChange={(e) => {
setBankCard(e.target.value); setBankCard(e.target.value);
validateHandler(e); validateHandler(e);
@@ -3,6 +3,7 @@
import PaymentsHistory from '@/app/ui/referral-page/table/payments-history'; import PaymentsHistory from '@/app/ui/referral-page/table/payments-history';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetchReferralUserStats } from '@/app/actions/referralsActions'; import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import RequestPayment from '@/app/ui/referral-page/request-payment';
export default function IncomeAndPayments() { export default function IncomeAndPayments() {
const { const {
@@ -55,25 +56,8 @@ export default function IncomeAndPayments() {
</div> </div>
</div> </div>
<div className="payout-form"> <RequestPayment />
<h4>💸 Запросить выплату</h4>
<div className="payout-wrapper">
<div className="form-group">
<label>Сумма к выводу (Б)</label>
<input type="number" id="payout-amount" min="100.00" max="135.00" step="0.01" placeholder="Введите сумму"></input>
</div>
<div className="form-group" >
<label>Способ выплаты</label>
<select id="payout-method">
<option defaultValue="BALANCE">💳 На баланс аккаунта</option>
<option defaultValue="BANK_TRANSFER">🏦 Банковский перевод</option>
<option defaultValue="PAYPAL">💰 PayPal</option>
<option defaultValue="CARD">💳 Банковская карта</option>
</select>
</div>
<button className="btn-success">💸 Заказать выплату</button>
</div>
</div>
<PaymentsHistory /> <PaymentsHistory />
</div> </div>
) )
@@ -0,0 +1,155 @@
'use client'
import DropDownList from '@/app/components/DropDownList';
import { useTranslations } from 'next-intl';
import { useState, useRef, useEffect, useActionState } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import { getPayoutMethods, PayoutMethods, createPayoutRequest } from '@/app/actions/referralsActions';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
export default function RequestPayment() {
const {
data: payoutMethods,
error
} = useQuery({
queryKey: ['payoutMethods'],
queryFn: () => {
return getPayoutMethods();
},
select: (data) => {
if (data?.methods) {
return data?.methods
} else {
return []
};
}
});
const [state, formAction, isPending] = useActionState(
createPayoutRequest,
undefined,
);
useEffect(() => {
if (state && !state.error) {
toast.success('payment request success');
queryClient.invalidateQueries({ queryKey: ['referralUserStats'] });
queryClient.invalidateQueries({ queryKey: ['referralInvitees'] });
} else if (state?.error) {
toast.error(state.error);
}
}, [state]);
const queryClient = useQueryClient();
const [paymentsType, setPaymentsType] = useState<PayoutMethods | null>(null);
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (payoutMethods) {
setPaymentsType(payoutMethods[0]);
}
}, [payoutMethods])
useClickOutside(dropdownRef,
() => {
setIsOpen(false);
},
isOpen
);
const t = useTranslations('Global');
return (
<form
className="payout-form"
action={(e) => {
formAction(e);
}}
>
<h4>Запросить выплату</h4>
<div className="payout-wrapper">
<div className="form-group">
<label>Сумма к выводу (Б)</label>
<input
type="number"
name="amount"
placeholder="Введите сумму">
</input>
</div>
<div className="form-group" >
<label>Способ выплаты</label>
<input
type="hidden"
name="payoutMethodId"
value={paymentsType?.id ?? 0}
/>
<div className="dropdown-wrapper" ref={dropdownRef}>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
className="dropdown-button"
type="button"
>
<span className="flex items-center">
<span className="mr-2">
{paymentsType?.displayName}
</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && (
<div className="dropdown-list">
<ul
className="py-1"
role="listbox"
aria-labelledby="language-selector"
>
{payoutMethods?.map(method => {
return (
<li
key={method.id}
value="on-account-balance"
className={`dropdown-item ${paymentsType?.id === method.id ? "current" : ""}`}
onClick={() => {
setPaymentsType(method)
setIsOpen(false);
}}
>
{method.displayName}
</li>
)
})}
</ul>
</div>
)}
</div>
</div>
<button
className="btn-success btn-primary"
disabled={isPending}
>
Заказать выплату
{isPending && (
<div className="loading-animation">
<div className="global-spinner"></div>
</div>
)}
</button>
</div>
</form>
)
}
@@ -4,8 +4,9 @@ import DropDownList from '@/app/components/DropDownList';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { ChangeEvent, useState, useActionState, useEffect } from 'react'; import { ChangeEvent, useState, useActionState, useEffect } from 'react';
import BankCardInput from '@/app/ui/inputs/bank-card-inpit'; import BankCardInput from '@/app/ui/inputs/bank-card-inpit';
import { addPayoutMethod, PayoutMethodFormState } from '@/app/actions/referralsActions'; import { addPayoutMethod, PayoutMethodFormState, getPayoutMethods, PayoutMethods } from '@/app/actions/referralsActions';
import { bankCardFormSchema } from '@/app/actions/definitions'; import { bankCardFormSchema } from '@/app/actions/definitions';
import { useQuery } from '@tanstack/react-query';
export default function SettingReferralProgram() { export default function SettingReferralProgram() {
const [state, formAction, isPending] = useActionState( const [state, formAction, isPending] = useActionState(
@@ -13,14 +14,43 @@ export default function SettingReferralProgram() {
undefined, undefined,
); );
const {
data: payoutMethods,
error
} = useQuery({
queryKey: ['payoutMethods'],
queryFn: () => {
return getPayoutMethods();
},
select: (data) => {
if (data?.methods) {
return data?.methods
} else {
return []
};
}
});
const [formState, setFormState] = useState<PayoutMethodFormState | undefined>(undefined); const [formState, setFormState] = useState<PayoutMethodFormState | undefined>(undefined);
const [paymentsType, setPaymentsType] = useState('bank-card'); const [paymentsType, setPaymentsType] = useState('bank-card');
const [defaultCardNumber, setDefaultCardNumber] = useState<PayoutMethods | undefined>(undefined);
const [isEditing, setIsEditing] = useState(false);
const t = useTranslations('Global'); const t = useTranslations('Global');
useEffect(() => { useEffect(() => {
setFormState(state); setFormState(state);
}, [state]); }, [state]);
useEffect(() => {
if (payoutMethods) {
payoutMethods.forEach(e => {
if (e.default) {
setDefaultCardNumber(e);
}
})
}
}, [payoutMethods])
async function validateField( async function validateField(
fieldName: string, fieldName: string,
value: string, value: string,
@@ -125,7 +155,11 @@ export default function SettingReferralProgram() {
<h4> <h4>
{t('bank-card-number')} {t('bank-card-number')}
</h4> </h4>
<BankCardInput bankCardState={formState?.previousState?.bankCard} validateHandler={onChangeHandler} /> <BankCardInput
bankCardState={defaultCardNumber?.cardNumber}
validateHandler={onChangeHandler}
disabled={!isEditing}
/>
{formState?.error?.bankCard && ( {formState?.error?.bankCard && (
<p className="text-sm text-red-500"> <p className="text-sm text-red-500">
{ {
@@ -147,9 +181,50 @@ export default function SettingReferralProgram() {
{t(formState?.error?.server)} {t(formState?.error?.server)}
</p> </p>
)} )}
<button type="submit" className="btn-primary">
{t('save-changes')} {
isEditing ? (
<div
className='flex gap-4'
>
<button
type="button"
className="btn-primary"
disabled={isPending}
onClick={() => {
setIsEditing(false)
}}
>
{t('cancel')}
</button> </button>
<button
type="submit"
className="btn-primary"
disabled={isPending}
>
{t("save-changes")}
{isPending && (
<div className="loading-animation">
<div className="global-spinner"></div>
</div>
)}
</button>
</div>
) : (
<button
type="button"
className="btn-primary"
disabled={isPending}
onClick={() => {
setIsEditing(true)
}}
>
Изменить
</button>
)
}
</div> </div>
<div className="settings-form-wrapper"> <div className="settings-form-wrapper">
@@ -57,8 +57,6 @@ export default function InvitationsTable() {
}, },
}); });
const queryClient = useQueryClient();
// Состояния // Состояния
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);