feat: billing guest access

This commit is contained in:
2024-08-08 07:54:43 +03:00
parent 925f1b4407
commit 1ad96e3a69
36 changed files with 644 additions and 53 deletions

View File

@@ -33,6 +33,7 @@
"dot-object": "^2.1.5", "dot-object": "^2.1.5",
"framer-motion": "^11.3.8", "framer-motion": "^11.3.8",
"globals": "^15.8.0", "globals": "^15.8.0",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mantine-form-zod-resolver": "^1.1.0", "mantine-form-zod-resolver": "^1.1.0",
"mantine-react-table": "^2.0.0-beta.5", "mantine-react-table": "^2.0.0-beta.5",

View File

@@ -24,6 +24,8 @@ export type { BarcodeTemplateUpdateResponse } from './models/BarcodeTemplateUpda
export type { BaseEnumListSchema } from './models/BaseEnumListSchema'; export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
export type { BaseEnumSchema } from './models/BaseEnumSchema'; export type { BaseEnumSchema } from './models/BaseEnumSchema';
export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema'; export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
export type { BillPaymentStatus } from './models/BillPaymentStatus';
export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest';
export type { Body_upload_product_image } from './models/Body_upload_product_image'; export type { Body_upload_product_image } from './models/Body_upload_product_image';
export type { ClientCreateRequest } from './models/ClientCreateRequest'; export type { ClientCreateRequest } from './models/ClientCreateRequest';
export type { ClientCreateResponse } from './models/ClientCreateResponse'; export type { ClientCreateResponse } from './models/ClientCreateResponse';
@@ -37,6 +39,8 @@ export type { ClientUpdateRequest } from './models/ClientUpdateRequest';
export type { ClientUpdateResponse } from './models/ClientUpdateResponse'; export type { ClientUpdateResponse } from './models/ClientUpdateResponse';
export type { CreateBarcodeTemplateAttributeRequest } from './models/CreateBarcodeTemplateAttributeRequest'; export type { CreateBarcodeTemplateAttributeRequest } from './models/CreateBarcodeTemplateAttributeRequest';
export type { CreateBarcodeTemplateAttributeResponse } from './models/CreateBarcodeTemplateAttributeResponse'; export type { CreateBarcodeTemplateAttributeResponse } from './models/CreateBarcodeTemplateAttributeResponse';
export type { CreateDealBillRequest } from './models/CreateDealBillRequest';
export type { CreateDealBillResponse } from './models/CreateDealBillResponse';
export type { CreatePaymentRecordRequest } from './models/CreatePaymentRecordRequest'; export type { CreatePaymentRecordRequest } from './models/CreatePaymentRecordRequest';
export type { CreatePaymentRecordResponse } from './models/CreatePaymentRecordResponse'; export type { CreatePaymentRecordResponse } from './models/CreatePaymentRecordResponse';
export type { CreatePayRateRequest } from './models/CreatePayRateRequest'; export type { CreatePayRateRequest } from './models/CreatePayRateRequest';
@@ -56,8 +60,11 @@ export type { DealAddServiceRequest } from './models/DealAddServiceRequest';
export type { DealAddServiceResponse } from './models/DealAddServiceResponse'; export type { DealAddServiceResponse } from './models/DealAddServiceResponse';
export type { DealAddServicesRequest } from './models/DealAddServicesRequest'; export type { DealAddServicesRequest } from './models/DealAddServicesRequest';
export type { DealAddServicesResponse } from './models/DealAddServicesResponse'; export type { DealAddServicesResponse } from './models/DealAddServicesResponse';
export type { DealBillRequestSchema } from './models/DealBillRequestSchema';
export type { DealChangeStatusRequest } from './models/DealChangeStatusRequest'; export type { DealChangeStatusRequest } from './models/DealChangeStatusRequest';
export type { DealChangeStatusResponse } from './models/DealChangeStatusResponse'; export type { DealChangeStatusResponse } from './models/DealChangeStatusResponse';
export type { DealCreateGuestUrlRequest } from './models/DealCreateGuestUrlRequest';
export type { DealCreateGuestUrlResponse } from './models/DealCreateGuestUrlResponse';
export type { DealCreateRequest } from './models/DealCreateRequest'; export type { DealCreateRequest } from './models/DealCreateRequest';
export type { DealDeleteProductRequest } from './models/DealDeleteProductRequest'; export type { DealDeleteProductRequest } from './models/DealDeleteProductRequest';
export type { DealDeleteProductResponse } from './models/DealDeleteProductResponse'; export type { DealDeleteProductResponse } from './models/DealDeleteProductResponse';
@@ -114,6 +121,7 @@ export type { GetAllShippingWarehousesResponse } from './models/GetAllShippingWa
export type { GetAllUsersResponse } from './models/GetAllUsersResponse'; export type { GetAllUsersResponse } from './models/GetAllUsersResponse';
export type { GetBarcodeTemplateByIdRequest } from './models/GetBarcodeTemplateByIdRequest'; export type { GetBarcodeTemplateByIdRequest } from './models/GetBarcodeTemplateByIdRequest';
export type { GetBarcodeTemplateByIdResponse } from './models/GetBarcodeTemplateByIdResponse'; export type { GetBarcodeTemplateByIdResponse } from './models/GetBarcodeTemplateByIdResponse';
export type { GetDealBillById } from './models/GetDealBillById';
export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse'; export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse';
export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest'; export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest';
export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse'; export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse';
@@ -123,6 +131,7 @@ export type { GetServiceKitSchema } from './models/GetServiceKitSchema';
export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest'; export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest';
export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse'; export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse';
export type { HTTPValidationError } from './models/HTTPValidationError'; export type { HTTPValidationError } from './models/HTTPValidationError';
export type { NotificationChannel } from './models/NotificationChannel';
export type { PaginationInfoSchema } from './models/PaginationInfoSchema'; export type { PaginationInfoSchema } from './models/PaginationInfoSchema';
export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema'; export type { PaymentRecordCreateSchema } from './models/PaymentRecordCreateSchema';
export type { PaymentRecordGetSchema } from './models/PaymentRecordGetSchema'; export type { PaymentRecordGetSchema } from './models/PaymentRecordGetSchema';
@@ -179,6 +188,7 @@ export type { ValidationError } from './models/ValidationError';
export { AuthService } from './services/AuthService'; export { AuthService } from './services/AuthService';
export { BarcodeService } from './services/BarcodeService'; export { BarcodeService } from './services/BarcodeService';
export { BillingService } from './services/BillingService';
export { ClientService } from './services/ClientService'; export { ClientService } from './services/ClientService';
export { DealService } from './services/DealService'; export { DealService } from './services/DealService';
export { MarketplaceService } from './services/MarketplaceService'; export { MarketplaceService } from './services/MarketplaceService';

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type BillPaymentStatus = {
payed: boolean;
};

View File

@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BillPaymentStatus } from './BillPaymentStatus';
import type { NotificationChannel } from './NotificationChannel';
export type BillStatusUpdateRequest = {
listenerTransactionId: number;
channel: NotificationChannel;
info: BillPaymentStatus;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateDealBillRequest = {
dealId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateDealBillResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealBillRequestSchema = {
dealId: number;
createdAt: string;
paid: boolean;
pdfUrl: (string | null);
invoiceNumber: (string | null);
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealCreateGuestUrlRequest = {
dealId: number;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DealCreateGuestUrlResponse = {
ok: boolean;
message: string;
url: string;
};

View File

@@ -3,6 +3,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { ClientSchema } from './ClientSchema'; import type { ClientSchema } from './ClientSchema';
import type { DealBillRequestSchema } from './DealBillRequestSchema';
import type { DealProductSchema } from './DealProductSchema'; import type { DealProductSchema } from './DealProductSchema';
import type { DealServiceSchema } from './DealServiceSchema'; import type { DealServiceSchema } from './DealServiceSchema';
import type { DealStatusHistorySchema } from './DealStatusHistorySchema'; import type { DealStatusHistorySchema } from './DealStatusHistorySchema';
@@ -18,8 +19,10 @@ export type DealSchema = {
statusHistory: Array<DealStatusHistorySchema>; statusHistory: Array<DealStatusHistorySchema>;
isDeleted: boolean; isDeleted: boolean;
isCompleted: boolean; isCompleted: boolean;
isLocked: boolean;
client: ClientSchema; client: ClientSchema;
comment: string; comment: string;
shippingWarehouse?: (ShippingWarehouseSchema | string | null); shippingWarehouse?: (ShippingWarehouseSchema | string | null);
billRequest?: (DealBillRequestSchema | null);
}; };

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DealBillRequestSchema } from './DealBillRequestSchema';
export type GetDealBillById = {
dealBill: DealBillRequestSchema;
};

View File

@@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type NotificationChannel = 'PAYMENT_DETAILS' | 'PAYMENT_VERIFICATION';

View File

@@ -0,0 +1,74 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BillStatusUpdateRequest } from '../models/BillStatusUpdateRequest';
import type { CreateDealBillRequest } from '../models/CreateDealBillRequest';
import type { CreateDealBillResponse } from '../models/CreateDealBillResponse';
import type { GetDealBillById } from '../models/GetDealBillById';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class BillingService {
/**
* Webhook
* @returns any Successful Response
* @throws ApiError
*/
public static webhookBillingWebhookPost({
requestBody,
}: {
requestBody: BillStatusUpdateRequest,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/billing/webhook',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Create Deal Bill
* @returns CreateDealBillResponse Successful Response
* @throws ApiError
*/
public static createDealBill({
requestBody,
}: {
requestBody: CreateDealBillRequest,
}): CancelablePromise<CreateDealBillResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/billing/create-deal-bill',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Deal Bill By Id
* @returns GetDealBillById Successful Response
* @throws ApiError
*/
public static getDealBillById({
dealId,
}: {
dealId: number,
}): CancelablePromise<GetDealBillById> {
return __request(OpenAPI, {
method: 'GET',
url: '/billing/deal-bill-request/{deal_id}',
path: {
'deal_id': dealId,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -12,6 +12,8 @@ import type { DealAddServicesRequest } from '../models/DealAddServicesRequest';
import type { DealAddServicesResponse } from '../models/DealAddServicesResponse'; import type { DealAddServicesResponse } from '../models/DealAddServicesResponse';
import type { DealChangeStatusRequest } from '../models/DealChangeStatusRequest'; import type { DealChangeStatusRequest } from '../models/DealChangeStatusRequest';
import type { DealChangeStatusResponse } from '../models/DealChangeStatusResponse'; import type { DealChangeStatusResponse } from '../models/DealChangeStatusResponse';
import type { DealCreateGuestUrlRequest } from '../models/DealCreateGuestUrlRequest';
import type { DealCreateGuestUrlResponse } from '../models/DealCreateGuestUrlResponse';
import type { DealCreateRequest } from '../models/DealCreateRequest'; import type { DealCreateRequest } from '../models/DealCreateRequest';
import type { DealDeleteProductRequest } from '../models/DealDeleteProductRequest'; import type { DealDeleteProductRequest } from '../models/DealDeleteProductRequest';
import type { DealDeleteProductResponse } from '../models/DealDeleteProductResponse'; import type { DealDeleteProductResponse } from '../models/DealDeleteProductResponse';
@@ -230,6 +232,26 @@ export class DealService {
}, },
}); });
} }
/**
* Create Guest Url
* @returns DealCreateGuestUrlResponse Successful Response
* @throws ApiError
*/
public static createDealGuestUrl({
requestBody,
}: {
requestBody: DealCreateGuestUrlRequest,
}): CancelablePromise<DealCreateGuestUrlResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/deal/create-guest-url',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Services Add * Services Add
* @returns DealAddServicesResponse Successful Response * @returns DealAddServicesResponse Successful Response

View File

@@ -0,0 +1,54 @@
import {Button, rem, Tooltip} from '@mantine/core';
import {IconCheck, IconCopy} from '@tabler/icons-react';
import {FC} from "react";
import {useClipboard} from "@mantine/hooks";
type Props = {
children: string;
value: string
onCopiedLabel: string;
}
export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
const clipboard = useClipboard();
return (
<Tooltip
label={onCopiedLabel}
offset={5}
position="bottom"
radius="xl"
transitionProps={{duration: 100, transition: 'slide-down'}}
opened={clipboard.copied}
>
<Button
variant="light"
rightSection={
clipboard.copied ? (
<IconCheck
style={{width: rem(20), height: rem(20)}}
stroke={1.5}
/>
) : (
<IconCopy
style={{width: rem(20), height: rem(20)}}
stroke={1.5}
/>
)
}
radius="xl"
size="md"
styles={{
root: {
paddingRight: rem(14),
},
section: {marginLeft: rem(22)},
}}
onClick={() => clipboard.copy(value)}
>
{children}
</Button>
</Tooltip>
);
}
export default ButtonCopy;

View File

@@ -0,0 +1,51 @@
import {Button, rem, Tooltip} from '@mantine/core';
import {IconCheck, IconCopy} from '@tabler/icons-react';
import {FC} from "react";
type Props = {
children: string;
onCopyClick: () => void;
onCopiedLabel: string;
copied: boolean;
}
export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopyClick, copied}) => {
return (
<Tooltip
label={onCopiedLabel}
offset={5}
position="bottom"
radius="xl"
transitionProps={{duration: 100, transition: 'slide-down'}}
opened={copied}
>
<Button
variant="light"
rightSection={
copied ? (
<IconCheck
style={{width: rem(20), height: rem(20)}}
stroke={1.5}
/>
) : (
<IconCopy
style={{width: rem(20), height: rem(20)}}
stroke={1.5}
/>
)
}
radius="xl"
size="md"
styles={{
root: {
paddingRight: rem(14),
},
section: {marginLeft: rem(22)},
}}
onClick={onCopyClick}
>
{children}
</Button>
</Tooltip>
);
}

View File

@@ -1,8 +1,10 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit"; import {createSlice, PayloadAction} from "@reduxjs/toolkit";
import {jwtDecode, JwtPayload} from "jwt-decode";
interface AuthState { interface AuthState {
isAuthorized: boolean; isAuthorized: boolean;
accessToken: string; accessToken: string;
isGuest: boolean;
} }
const initialState = (): AuthState => { const initialState = (): AuthState => {
@@ -12,7 +14,8 @@ const initialState = (): AuthState => {
} }
return { return {
accessToken: "", accessToken: "",
isAuthorized: false isAuthorized: false,
isGuest: false
} }
} }
@@ -21,8 +24,19 @@ const authSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
login: (state, action: PayloadAction<{ accessToken: string }>) => { login: (state, action: PayloadAction<{ accessToken: string }>) => {
state.accessToken = action.payload.accessToken; try {
state.isAuthorized = true; const {sub} = jwtDecode<JwtPayload>(action.payload.accessToken);
state.accessToken = action.payload.accessToken;
state.isAuthorized = true;
if (sub === "guest")
state.isGuest = true;
} catch (_) {
const url = window.location.href;
const urlObj = new URL(url);
urlObj.search = '';
history.replaceState(null, '', urlObj);
window.location.reload();
}
}, },
logout: (state) => { logout: (state) => {
state.isAuthorized = false; state.isAuthorized = false;

View File

@@ -0,0 +1 @@
export {DealPage} from './ui/DealPage'

View File

@@ -0,0 +1,38 @@
import {useParams} from "@tanstack/react-router";
import {DealPageContextProvider, useDealPageContext} from "../../LeadsPage/contexts/DealPageContext.tsx";
import ProductAndServiceTab from "../../LeadsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
import {FC, useEffect} from "react";
import {DealService} from "../../../client";
export type Props = {
dealId: number;
}
const DealPageContent: FC<Props> = ({dealId}) => {
const {setSelectedDeal} = useDealPageContext();
useEffect(() => {
DealService.getDealById({dealId}).then(deal => {
setSelectedDeal(deal);
})
}, []);
return (
<ProductAndServiceTab/>
)
}
const DealPageWrapper: FC<{ children: React.ReactNode }> = ({children}) => {
return (
<DealPageContextProvider>
{children}
</DealPageContextProvider>
)
}
export const DealPage = () => {
const {dealId} = useParams({strict: false});
return (
<DealPageWrapper>
<DealPageContent dealId={parseInt(dealId || "-1")}/>
</DealPageWrapper>
)
}

View File

@@ -6,6 +6,8 @@ import ServiceWithPriceInput from "../../../../components/ServiceWithPriceInput/
import {isNumber} from "lodash"; import {isNumber} from "lodash";
import {notifications} from "../../../../shared/lib/notifications.ts"; import {notifications} from "../../../../shared/lib/notifications.ts";
import {IconTrash} from "@tabler/icons-react"; import {IconTrash} from "@tabler/icons-react";
import {useSelector} from "react-redux";
import {RootState} from "../../../../redux/store.ts";
type RestProps = { type RestProps = {
quantity: number; quantity: number;
@@ -13,6 +15,8 @@ type RestProps = {
type Props = BaseFormInputProps<DealProductServiceSchema[]> & RestProps; type Props = BaseFormInputProps<DealProductServiceSchema[]> & RestProps;
const DealProductServiceTable: FC<Props> = (props: Props) => { const DealProductServiceTable: FC<Props> = (props: Props) => {
const {value, onChange, quantity, error} = props; const {value, onChange, quantity, error} = props;
const authState = useSelector((state: RootState) => state.auth);
const [innerValue, setInnerValue] = useState<Partial<DealProductServiceSchema>[]>(value || []); const [innerValue, setInnerValue] = useState<Partial<DealProductServiceSchema>[]>(value || []);
const onServiceChange = (idx: number, value: ServiceSchema) => { const onServiceChange = (idx: number, value: ServiceSchema) => {
setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, service: value} : item)); setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, service: value} : item));
@@ -68,8 +72,8 @@ const DealProductServiceTable: FC<Props> = (props: Props) => {
placeholder: "Введите стоимость", placeholder: "Введите стоимость",
hideControls: true, hideControls: true,
style: {width: "100%"}, style: {width: "100%"},
suffix: "₽" suffix: "₽",
disabled: authState.isGuest
}} }}
containerProps={{w: "100%"}} containerProps={{w: "100%"}}
quantity={quantity} quantity={quantity}

View File

@@ -9,6 +9,9 @@ import {notifications} from "../../../../../shared/lib/notifications.ts";
import {useQueryClient} from "@tanstack/react-query"; import {useQueryClient} from "@tanstack/react-query";
import ShippingWarehouseAutocomplete import ShippingWarehouseAutocomplete
from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx"; from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import {ButtonCopyControlled} from "../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
import {useClipboard} from "@mantine/hooks";
import ButtonCopy from "../../../../../components/ButtonCopy/ButtonCopy.tsx";
type Props = { type Props = {
deal: DealSchema deal: DealSchema
@@ -18,6 +21,7 @@ type FormType = Omit<DealSchema, 'statusHistory' | 'services' | 'products'>
const Content: FC<Props> = ({deal}) => { const Content: FC<Props> = ({deal}) => {
const {setSelectedDeal} = useDealPageContext(); const {setSelectedDeal} = useDealPageContext();
const clipboard = useClipboard();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const initialValues: FormType = deal; const initialValues: FormType = deal;
@@ -68,6 +72,20 @@ const Content: FC<Props> = ({deal}) => {
return !["string", "null", "undefined"].includes((typeof value)); return !["string", "null", "undefined"].includes((typeof value));
} }
const onCopyGuestUrlClick = () => {
DealService.createDealGuestUrl({
requestBody: {
dealId: deal.id
}
}).then(({ok, message, url}) => {
if (!ok)
notifications.guess(ok, {message});
clipboard.copy(
`${window.location.origin}/${url}`
);
});
}
return ( return (
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}> <form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
<Flex direction={'column'}> <Flex direction={'column'}>
@@ -138,15 +156,41 @@ const Content: FC<Props> = ({deal}) => {
</Fieldset> </Fieldset>
<Flex mt={'md'} gap={rem(10)} align={'center'} justify={'flex-end'}> <Flex mt={'md'} gap={rem(10)} align={'center'} justify={'flex-end'}>
<Flex align={'center'} gap={rem(10)} justify={'center'}> <Flex align={'center'} gap={rem(10)} justify={'center'}>
<Checkbox
label={"Сделка завершена"}
{...form.getInputProps('isCompleted')}
/>
<Checkbox <Flex gap={rem(10)}>
label={"Сделка удалена"} {(deal.billRequest && deal.billRequest.pdfUrl) &&
{...form.getInputProps('isDeleted')} <ButtonCopy
/> onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={deal.billRequest.pdfUrl}
>
Ссылка на оплату
</ButtonCopy>
}
<ButtonCopyControlled
onCopyClick={onCopyGuestUrlClick}
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
copied={clipboard.copied}>
Ссылка на редактирование
</ButtonCopyControlled>
</Flex>
<Flex gap={rem(10)}>
<Checkbox
label={"Оплачен"}
checked={deal.billRequest?.paid || false}
disabled
/>
<Checkbox
label={"Сделка завершена"}
{...form.getInputProps('isCompleted')}
/>
<Checkbox
label={"Сделка удалена"}
{...form.getInputProps('isDeleted')}
/>
</Flex>
</Flex> </Flex>
<Divider <Divider
orientation={'vertical'} orientation={'vertical'}

View File

@@ -5,6 +5,8 @@ import {useForm} from "@mantine/form";
import {ComboboxItem, ComboboxItemGroup, NumberInput, OptionsFilter} from "@mantine/core"; import {ComboboxItem, ComboboxItemGroup, NumberInput, OptionsFilter} from "@mantine/core";
import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx"; import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
import {ServiceType} from "../../../shared/enums/ServiceType.ts"; import {ServiceType} from "../../../shared/enums/ServiceType.ts";
import {useSelector} from "react-redux";
import {RootState} from "../../../redux/store.ts";
type RestProps = { type RestProps = {
serviceIds?: number[]; serviceIds?: number[];
@@ -15,6 +17,8 @@ const AddDealServiceModal = ({
id, id,
innerProps innerProps
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const authState = useSelector((state: RootState) => state.auth);
const isEditing = 'element' in innerProps; const isEditing = 'element' in innerProps;
const form = useForm<Partial<DealServiceSchema>>({ const form = useForm<Partial<DealServiceSchema>>({
initialValues: isEditing ? innerProps.element : { initialValues: isEditing ? innerProps.element : {
@@ -62,7 +66,8 @@ const AddDealServiceModal = ({
...form.getInputProps('price'), ...form.getInputProps('price'),
label: "Цена", label: "Цена",
placeholder: "Введите цену", placeholder: "Введите цену",
style: {width: '100%'} style: {width: '100%'},
disabled: authState.isGuest
}} }}
quantity={form.values.quantity || 1} quantity={form.values.quantity || 1}
containerProps={{ containerProps={{

View File

@@ -6,6 +6,8 @@ import {isNil, isNumber} from "lodash";
import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx"; import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
import {Flex} from "@mantine/core"; import {Flex} from "@mantine/core";
import {ServiceType} from "../../../shared/enums/ServiceType.ts"; import {ServiceType} from "../../../shared/enums/ServiceType.ts";
import {useSelector} from "react-redux";
import {RootState} from "../../../redux/store.ts";
type RestProps = { type RestProps = {
quantity: number; quantity: number;
@@ -17,6 +19,8 @@ const ProductServiceFormModal = ({
context, context,
id, innerProps id, innerProps
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const authState = useSelector((state: RootState) => state.auth);
const isEditing = 'onChange' in innerProps; const isEditing = 'onChange' in innerProps;
const initialValues: Partial<DealProductServiceSchema> = isEditing ? innerProps.element : { const initialValues: Partial<DealProductServiceSchema> = isEditing ? innerProps.element : {
service: undefined, service: undefined,
@@ -57,8 +61,8 @@ const ProductServiceFormModal = ({
...form.getInputProps('price'), ...form.getInputProps('price'),
label: "Цена", label: "Цена",
placeholder: "Введите цену", placeholder: "Введите цену",
style: {width: "100%"} style: {width: "100%"},
disabled: authState.isGuest
}} }}
filterType={ServiceType.PRODUCT_SERVICE} filterType={ServiceType.PRODUCT_SERVICE}
containerProps={{ containerProps={{

View File

@@ -5,6 +5,11 @@
max-height: 95vh; max-height: 95vh;
} }
.container-disabled {
pointer-events: none;
opacity: 0.4;
}
.products-list { .products-list {
width: 60%; width: 60%;
display: flex; display: flex;

View File

@@ -1,17 +1,26 @@
import {FC} from "react"; import {FC} from "react";
import styles from './ProductAndServiceTab.module.css'; import styles from './ProductAndServiceTab.module.css';
import ProductView from "./components/ProductView/ProductView.tsx"; import ProductView from "./components/ProductView/ProductView.tsx";
import {Button, Flex, ScrollArea, Title} from "@mantine/core"; import {Button, Divider, Flex, rem, ScrollArea, Text, Title} from "@mantine/core";
import DealServicesTable from "./components/DealServicesTable/DealServicesTable.tsx"; import DealServicesTable from "./components/DealServicesTable/DealServicesTable.tsx";
import useDealProductAndServiceTabState from "./hooks/useProductAndServiceTabState.tsx"; import useDealProductAndServiceTabState from "./hooks/useProductAndServiceTabState.tsx";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import {DealProductSchema, DealService, GetServiceKitSchema} from "../../../../client"; import {
BillingService,
DealProductSchema,
DealService,
GetServiceKitSchema,
ProductSchema,
ProductService
} from "../../../../client";
import {notifications} from "../../../../shared/lib/notifications.ts"; import {notifications} from "../../../../shared/lib/notifications.ts";
import {CreateProductRequest} from "../../../ProductsPage/types.ts";
import classNames from "classnames";
const ProductAndServiceTab: FC = () => { const ProductAndServiceTab: FC = () => {
const {dealState, dealServicesState, dealProductsState} = useDealProductAndServiceTabState(); const {dealState, dealServicesState, dealProductsState} = useDealProductAndServiceTabState();
const onCreateProductClick = () => { const onAddProductClick = () => {
if (!dealProductsState.onCreate || !dealState.deal) return; if (!dealProductsState.onCreate || !dealState.deal) return;
const productIds = dealState.deal.products.map(product => product.product.id); const productIds = dealState.deal.products.map(product => product.product.id);
modals.openContextModal({ modals.openContextModal({
@@ -88,14 +97,79 @@ const ProductAndServiceTab: FC = () => {
await dealState.refetch(); await dealState.refetch();
}); });
} }
const onCreateProduct = (newProduct: CreateProductRequest) => {
ProductService.createProduct({
requestBody: newProduct
}).then(({ok, message}) => {
notifications.guess(ok, {message: message});
})
}
const onCreateProductClick = () => {
if (!dealState.deal) return;
modals.openContextModal({
modal: "createProduct",
title: 'Создание товара',
withCloseButton: false,
innerProps: {
clientId: dealState.deal.clientId,
onCreate: onCreateProduct
}
})
}
const onProductEdit = (product: ProductSchema) => {
ProductService.updateProduct({requestBody: {product}})
.then(async ({ok, message}) => {
notifications.guess(ok, {message});
if (!ok) return;
await dealState.refetch();
})
}
const onCreateBillClick = () => {
if (!dealState.deal) return;
const dealId = dealState.deal.id;
modals.openConfirmModal({
title: "Выставление счета",
size: "xl",
children:
<Text style={{textAlign: "justify"}}>
Создание заявки на выставление счета, подтвержденное нажатием кнопки "Выставить", заблокирует
возможность
редактирования товаров и услуг сделки. Пожалуйста, проверьте всю информацию на точность и полноту
перед подтверждением.
</Text>,
onConfirm: () => {
BillingService.createDealBill({
requestBody: {
dealId
}
}).then(async ({ok, message}) => {
notifications.guess(ok, {message});
if (ok) notifications.success({message: "Ссылка на оплату доступна во вкладе общее"});
await dealState.refetch();
})
},
labels: {
confirm: "Выставить",
cancel: "Отмена"
}
});
}
return ( return (
<div className={styles['container']}> <div
className={
classNames(styles['container'],
dealState.deal?.billRequest && styles['container-disabled']
)
}>
<div className={styles['products-list']}> <div className={styles['products-list']}>
<ScrollArea offsetScrollbars> <ScrollArea offsetScrollbars>
{dealState.deal?.products.map(product => ( {dealState.deal?.products.map(product => (
<ProductView <ProductView
onProductEdit={onProductEdit}
onKitAdd={onKitAdd} onKitAdd={onKitAdd}
onCopyServices={onCopyServicesClick} onCopyServices={onCopyServicesClick}
key={product.product.id} key={product.product.id}
@@ -113,12 +187,25 @@ const ProductAndServiceTab: FC = () => {
onKitAdd={onDealKitAdd} onKitAdd={onDealKitAdd}
{...dealServicesState} {...dealServicesState}
/> />
<Divider my={rem(15)}/>
<div className={styles['deal-container-buttons']}> <div className={styles['deal-container-buttons']}>
<Button <Button
variant={"default"}
fullWidth
onClick={onCreateProductClick} onClick={onCreateProductClick}
>Создать товар</Button>
<Button
onClick={onAddProductClick}
variant={"default"} variant={"default"}
fullWidth>Добавить товар</Button> fullWidth>Добавить товар</Button>
</div> </div>
<Divider my={rem(15)}/>
<div className={styles['deal-container-buttons']}>
<Button
onClick={onCreateBillClick}
variant={"default"}
fullWidth>Выставить счет</Button>
</div>
</Flex> </Flex>
<Flex direction={"column"} className={styles['deal-container-wrapper']}> <Flex direction={"column"} className={styles['deal-container-wrapper']}>
<Title order={3}>Общая стоимость всех услуг: {getTotalPrice().toLocaleString("ru")}</Title> <Title order={3}>Общая стоимость всех услуг: {getTotalPrice().toLocaleString("ru")}</Title>

View File

@@ -7,12 +7,15 @@ import {modals} from "@mantine/modals";
import {isNumber} from "lodash"; import {isNumber} from "lodash";
import SimpleUsersTable from "../../../../components/SimpleUsersTable/SimpleUsersTable.tsx"; import SimpleUsersTable from "../../../../components/SimpleUsersTable/SimpleUsersTable.tsx";
import {ServiceType} from "../../../../../../shared/enums/ServiceType.ts"; import {ServiceType} from "../../../../../../shared/enums/ServiceType.ts";
import {useSelector} from "react-redux";
import {RootState} from "../../../../../../redux/store.ts";
type RestProps = { type RestProps = {
onKitAdd?: (kit: GetServiceKitSchema) => void onKitAdd?: (kit: GetServiceKitSchema) => void
}; };
type Props = CRUDTableProps<DealServiceSchema> & RestProps; type Props = CRUDTableProps<DealServiceSchema> & RestProps;
const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange, onKitAdd}) => { const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange, onKitAdd}) => {
const authState = useSelector((state: RootState) => state.auth);
const [currentService, setCurrentService] = useState<DealServiceSchema | undefined>(); const [currentService, setCurrentService] = useState<DealServiceSchema | undefined>();
const [employeesModalVisible, setEmployeesModalVisible] = useState(false); const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
@@ -118,11 +121,13 @@ const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange, onKi
<IconTrash/> <IconTrash/>
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="Сотрудники"> {!authState.isGuest &&
<ActionIcon onClick={() => onEmployeeClick(service)} variant={"default"}> <Tooltip label="Сотрудники">
<IconUsersGroup/> <ActionIcon onClick={() => onEmployeeClick(service)} variant={"default"}>
</ActionIcon> <IconUsersGroup/>
</Tooltip> </ActionIcon>
</Tooltip>
}
<Text <Text
flex={1} flex={1}
>{service.service.name}</Text> >{service.service.name}</Text>
@@ -135,6 +140,7 @@ const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange, onKi
onChange={event => isNumber(event) && onPriceChange(service, event)} onChange={event => isNumber(event) && onPriceChange(service, event)}
suffix={"₽"} suffix={"₽"}
value={service.price} value={service.price}
disabled={authState.isGuest}
/> />
</Flex> </Flex>
))} ))}
@@ -146,7 +152,7 @@ const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange, onKi
order={3} order={3}
>Итог: {items.reduce((acc, item) => acc + (item.price * item.quantity), 0)}</Title> >Итог: {items.reduce((acc, item) => acc + (item.price * item.quantity), 0)}</Title>
</Flex> </Flex>
<Flex direction={"column"} gap={rem(10)} pb={rem(10)} mt={"auto"}> <Flex direction={"column"} gap={rem(10)} mt={"auto"}>
<Button <Button
onClick={onCreateClick} onClick={onCreateClick}
fullWidth fullWidth

View File

@@ -8,6 +8,8 @@ import {ActionIcon, Button, Flex, Modal, rem, Tooltip} from "@mantine/core";
import {IconEdit, IconTrash, IconUsersGroup} from "@tabler/icons-react"; import {IconEdit, IconTrash, IconUsersGroup} from "@tabler/icons-react";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import SimpleUsersTable from "../../../../components/SimpleUsersTable/SimpleUsersTable.tsx"; import SimpleUsersTable from "../../../../components/SimpleUsersTable/SimpleUsersTable.tsx";
import {useSelector} from "react-redux";
import {RootState} from "../../../../../../redux/store.ts";
type RestProps = { type RestProps = {
quantity: number; quantity: number;
@@ -25,6 +27,8 @@ const ProductServicesTable: FC<Props> = ({
onCopyServices, onCopyServices,
onKitAdd onKitAdd
}) => { }) => {
const authState = useSelector((state: RootState) => state.auth);
const columns = useProductServicesTableColumns({data: items, quantity}); const columns = useProductServicesTableColumns({data: items, quantity});
const serviceIds = items.map(service => service.service.id); const serviceIds = items.map(service => service.service.id);
@@ -124,13 +128,15 @@ const ProductServicesTable: FC<Props> = ({
<IconEdit/> <IconEdit/>
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="Сотрудники"> {!authState.isGuest &&
<ActionIcon onClick={() => onEmployeeClick(row.original)} variant={"default"}> <Tooltip label="Сотрудники">
<IconUsersGroup/> <ActionIcon onClick={() => onEmployeeClick(row.original)} variant={"default"}>
</ActionIcon> <IconUsersGroup/>
</Tooltip> </ActionIcon>
</Tooltip>
}
</Flex> </Flex>
) ),
} as MRT_TableOptions<DealProductServiceSchema>} } as MRT_TableOptions<DealProductServiceSchema>}
/> />
</Flex> </Flex>

View File

@@ -1,6 +1,8 @@
import {useMemo} from "react"; import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table"; import {MRT_ColumnDef} from "mantine-react-table";
import {DealProductServiceSchema} from "../../../../../../client"; import {DealProductServiceSchema} from "../../../../../../client";
import {useSelector} from "react-redux";
import {RootState} from "../../../../../../redux/store.ts";
type Props = { type Props = {
data: DealProductServiceSchema[]; data: DealProductServiceSchema[];
@@ -8,14 +10,19 @@ type Props = {
} }
const useProductServicesTableColumns = (props: Props) => { const useProductServicesTableColumns = (props: Props) => {
const {data, quantity} = props; const {data, quantity} = props;
const authState = useSelector((state: RootState) => state.auth);
const totalPrice = useMemo(() => data.reduce((acc, row) => acc + (row.price * quantity), 0), [data, quantity]); const totalPrice = useMemo(() => data.reduce((acc, row) => acc + (row.price * quantity), 0), [data, quantity]);
const totalCost = useMemo(() => data.reduce((acc, row) => acc + ((row.service.cost || 0) * quantity), 0), [data, quantity]); const totalCost = useMemo(() => data.reduce((acc, row) => acc + ((row.service.cost || 0) * quantity), 0), [data, quantity]);
const hideGuestColumns = [
"service.cost"
]
return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(() => [ return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(() => [
{ {
accessorKey: "service.name", accessorKey: "service.name",
header: "Услуга", header: "Услуга",
}, },
{ {
enableHiding: true,
accessorKey: "service.cost", accessorKey: "service.cost",
header: "Себестоимость", header: "Себестоимость",
Footer: () => <>Итоговая себестоимость: {totalCost.toLocaleString("ru")}</>, Footer: () => <>Итоговая себестоимость: {totalCost.toLocaleString("ru")}</>,
@@ -25,7 +32,7 @@ const useProductServicesTableColumns = (props: Props) => {
header: "Цена", header: "Цена",
Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}</>, Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}</>,
} }
], [totalPrice]); ], [totalPrice]).filter(columnDef => !(hideGuestColumns.includes(columnDef.accessorKey || "") && authState.isGuest));
} }
export default useProductServicesTableColumns; export default useProductServicesTableColumns;

View File

@@ -9,7 +9,7 @@ import styles from './ProductView.module.css';
import {ActionIcon, Flex, Image, NumberInput, rem, Spoiler, Text, Title, Tooltip} from '@mantine/core'; import {ActionIcon, Flex, Image, NumberInput, rem, Spoiler, Text, Title, Tooltip} from '@mantine/core';
import ProductServicesTable from "../ProductServicesTable/ProductServicesTable.tsx"; import ProductServicesTable from "../ProductServicesTable/ProductServicesTable.tsx";
import {isNil, isNumber} from "lodash"; import {isNil, isNumber} from "lodash";
import {IconBarcode, IconTrash} from "@tabler/icons-react"; import {IconBarcode, IconEdit, IconTrash} from "@tabler/icons-react";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import {ServiceType} from "../../../../../../shared/enums/ServiceType.ts"; import {ServiceType} from "../../../../../../shared/enums/ServiceType.ts";
@@ -19,6 +19,7 @@ type Props = {
onDelete?: (item: DealProductSchema) => void onDelete?: (item: DealProductSchema) => void
onCopyServices?: (item: DealProductSchema) => void; onCopyServices?: (item: DealProductSchema) => void;
onKitAdd?: (item: DealProductSchema, kit: GetServiceKitSchema) => void; onKitAdd?: (item: DealProductSchema, kit: GetServiceKitSchema) => void;
onProductEdit: (product: ProductSchema) => void;
} }
type ProductFieldNames = { type ProductFieldNames = {
[K in keyof ProductSchema]: string [K in keyof ProductSchema]: string
@@ -31,7 +32,14 @@ export const ProductFieldNames: Partial<ProductFieldNames> = {
composition: "Состав", composition: "Состав",
additionalInfo: "Доп. информация", additionalInfo: "Доп. информация",
} }
const ProductView: FC<Props> = ({product, onDelete, onChange, onCopyServices, onKitAdd}) => { const ProductView: FC<Props> = ({
product,
onDelete,
onChange,
onCopyServices,
onKitAdd,
onProductEdit
}) => {
const onDeleteClick = () => { const onDeleteClick = () => {
if (!onDelete) return; if (!onDelete) return;
onDelete(product); onDelete(product);
@@ -91,6 +99,18 @@ const ProductView: FC<Props> = ({product, onDelete, onChange, onCopyServices, on
}) })
} }
const onProductEditClick = () => {
modals.openContextModal({
modal: "createProduct",
title: 'Редактирование товара',
withCloseButton: false,
innerProps: {
onChange: (newProduct) => onProductEdit(newProduct),
product: product.product,
}
})
}
return ( return (
<div className={styles['container']}> <div className={styles['container']}>
<div className={styles['data-container']}> <div className={styles['data-container']}>
@@ -145,6 +165,14 @@ const ProductView: FC<Props> = ({product, onDelete, onChange, onCopyServices, on
<IconBarcode/> <IconBarcode/>
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip
onClick={onProductEditClick}
label="Редактировать товар">
<ActionIcon
variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip onClick={onDeleteClick} label="Удалить товар"> <Tooltip onClick={onDeleteClick} label="Удалить товар">
<ActionIcon <ActionIcon
variant={"default"}> variant={"default"}>

View File

@@ -3,7 +3,7 @@ import {AppShell, Flex, rem} from "@mantine/core";
import {useSelector} from "react-redux"; import {useSelector} from "react-redux";
import {RootState} from "../../redux/store.ts"; import {RootState} from "../../redux/store.ts";
import styles from './PageWrapper.module.css'; import styles from './PageWrapper.module.css';
import { Navbar } from "../../components/Navbar/Navbar.tsx"; import {Navbar} from "../../components/Navbar/Navbar.tsx";
export type Props = { export type Props = {
children: ReactNode; children: ReactNode;
@@ -14,14 +14,14 @@ const PageWrapper: FC<Props> = ({children}) => {
<AppShell <AppShell
layout={"alt"} layout={"alt"}
withBorder={false} withBorder={false}
navbar={authState.isAuthorized ? { navbar={(authState.isAuthorized && !authState.isGuest) ? {
width: "130px", width: "130px",
breakpoint: "sm" breakpoint: "sm"
} : undefined} } : undefined}
> >
<AppShell.Navbar> <AppShell.Navbar>
{authState.isAuthorized && {(authState.isAuthorized && !authState.isGuest) &&
<Flex className={styles['main-container']} h={"100%"} w={"100%"} <Flex className={styles['main-container']} h={"100%"} w={"100%"}
pl={rem(20)} pl={rem(20)}
py={rem(20)} py={rem(20)}
@@ -31,8 +31,16 @@ const PageWrapper: FC<Props> = ({children}) => {
</Flex> </Flex>
} }
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main className={styles['main-container']}> <AppShell.Main
<div className={styles['container']}> style={
authState.isGuest ? {backgroundColor: "var(--mantine-color-dark-8)"} : {}
}
className={styles['main-container']}
>
<div
className={styles['container']}
>
{children} {children}
</div> </div>
</AppShell.Main> </AppShell.Main>

View File

@@ -1,14 +1,19 @@
import {useMatch, useMatches} from "@tanstack/react-router"; import {useMatch, useMatches, useSearch} from "@tanstack/react-router";
import {useEffect} from "react"; import {useEffect} from "react";
import {useSelector} from "react-redux"; import {useSelector} from "react-redux";
import {RootState} from "../../redux/store.ts"; import {RootState, useAppDispatch} from "../../redux/store.ts";
import {OpenAPI} from "../../client"; import {OpenAPI} from "../../client";
import PageWrapper from "../PageWrapper/PageWrapper.tsx"; import PageWrapper from "../PageWrapper/PageWrapper.tsx";
import {LoadingOverlay} from "@mantine/core"; import {LoadingOverlay} from "@mantine/core";
import {AnimatePresence} from "framer-motion"; import {AnimatePresence} from "framer-motion";
import AnimatedOutlet from "../../components/AnimatedOutlet/au.tsx"; import AnimatedOutlet from "../../components/AnimatedOutlet/au.tsx";
import {SearchParams} from "../../shared/lib/general.ts";
import {login} from "../../features/authSlice.ts";
const RootPage = () => { const RootPage = () => {
const search: SearchParams = useSearch({strict: false});
const dispatch = useAppDispatch();
const matches = useMatches(); const matches = useMatches();
const match = useMatch({strict: false}); const match = useMatch({strict: false});
const nextMatchIndex = matches.findIndex((d) => d.id === match.id) + 1; const nextMatchIndex = matches.findIndex((d) => d.id === match.id) + 1;
@@ -27,6 +32,10 @@ const RootPage = () => {
rewriteLocalStorage(); rewriteLocalStorage();
setOpenApiToken(); setOpenApiToken();
}, [authState]); }, [authState]);
useEffect(() => {
if (!search.accessToken) return;
dispatch(login({accessToken: search.accessToken.toString()}))
}, [search])
return ( return (
<> <>
<LoadingOverlay visible={uiState.isLoading}/> <LoadingOverlay visible={uiState.isLoading}/>

View File

@@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes // Import Routes
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as DealsDealIdImport } from './routes/deals.$dealId'
// Create Virtual Routes // Create Virtual Routes
@@ -73,6 +74,11 @@ const IndexLazyRoute = IndexLazyImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) } as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
const DealsDealIdRoute = DealsDealIdImport.update({
path: '/deals/$dealId',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface // Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -140,6 +146,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TestLazyImport preLoaderRoute: typeof TestLazyImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/deals/$dealId': {
id: '/deals/$dealId'
path: '/deals/$dealId'
fullPath: '/deals/$dealId'
preLoaderRoute: typeof DealsDealIdImport
parentRoute: typeof rootRoute
}
} }
} }
@@ -155,6 +168,7 @@ export const routeTree = rootRoute.addChildren({
ProductsLazyRoute, ProductsLazyRoute,
ServicesLazyRoute, ServicesLazyRoute,
TestLazyRoute, TestLazyRoute,
DealsDealIdRoute,
}) })
/* prettier-ignore-end */ /* prettier-ignore-end */
@@ -173,7 +187,8 @@ export const routeTree = rootRoute.addChildren({
"/login", "/login",
"/products", "/products",
"/services", "/services",
"/test" "/test",
"/deals/$dealId"
] ]
}, },
"/": { "/": {
@@ -202,6 +217,9 @@ export const routeTree = rootRoute.addChildren({
}, },
"/test": { "/test": {
"filePath": "test.lazy.tsx" "filePath": "test.lazy.tsx"
},
"/deals/$dealId": {
"filePath": "deals.$dealId.tsx"
} }
} }
} }

View File

@@ -1,10 +1,15 @@
import {createRootRoute, redirect} from "@tanstack/react-router"; import {createRootRoute, redirect} from "@tanstack/react-router";
import RootPage from "../pages/RootPage/RootPage.tsx"; import RootPage from "../pages/RootPage/RootPage.tsx";
import {SearchParams} from "../shared/lib/general.ts";
export const Route = createRootRoute({ export const Route = createRootRoute({
component: RootPage, component: RootPage,
beforeLoad: async ({location}) => { beforeLoad: async ({location, search}) => {
const isAuthorized = JSON.parse(localStorage.getItem('authState') || '{}')['isAuthorized']; const isAuthorized = JSON.parse(localStorage.getItem('authState') || '{}')['isAuthorized'];
const searchParams: SearchParams = search;
if (searchParams.accessToken) {
return;
}
if (!isAuthorized && location.pathname !== '/login') { if (!isAuthorized && location.pathname !== '/login') {
throw redirect({ throw redirect({
to: '/login', to: '/login',

View File

@@ -0,0 +1,8 @@
import {createFileRoute} from "@tanstack/react-router";
import {DealPage} from "../pages/DealPage";
export const Route = createFileRoute("/deals/$dealId")({
component: DealPage
})

View File

@@ -1,7 +1,4 @@
import {createLazyFileRoute} from "@tanstack/react-router"; import {createLazyFileRoute} from "@tanstack/react-router";
import DebouncedNumberInput from "../components/DebouncedNumberInput/DebouncedNumberInput.tsx";
import {useEffect, useState} from "react";
import {isNumber} from "lodash";
export const Route = createLazyFileRoute('/test')({ export const Route = createLazyFileRoute('/test')({
component: TestPage component: TestPage
@@ -9,14 +6,8 @@ export const Route = createLazyFileRoute('/test')({
function TestPage() { function TestPage() {
const [value, setValue] = useState(0);
useEffect(() => {
}, [value]);
return ( return (
<> <>
<DebouncedNumberInput
onChange={(event) => isNumber(event) && setValue(event)}
/>
</> </>
); );
} }

View File

@@ -0,0 +1,7 @@
export type SearchParams = {
accessToken?: string;
}
export type JwtPayload = {
sub: number | string
}