feat: added tags for cards, aligned status headers
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
"@mantine/modals": "^7.11.2",
|
||||
"@mantine/notifications": "^7.11.2",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"@tabler/icons-react": "3.11.0",
|
||||
"@tanstack/react-query": "^5.51.9",
|
||||
"@tanstack/react-router": "^1.45.6",
|
||||
"@tanstack/router-devtools": "^1.45.6",
|
||||
@@ -75,10 +75,10 @@
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-preset-mantine": "^1.16.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass": "^1.77.8",
|
||||
"sass": "^1.81.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^7.16.1",
|
||||
"vite": "^5.3.4",
|
||||
"vite": "^6.0.1",
|
||||
"yarn-upgrade-all": "^0.7.2"
|
||||
},
|
||||
"packageManager": "yarn@4.1.0"
|
||||
|
||||
@@ -27,6 +27,7 @@ export type { BarcodeTemplateUpdateRequest } from './models/BarcodeTemplateUpdat
|
||||
export type { BarcodeTemplateUpdateResponse } from './models/BarcodeTemplateUpdateResponse';
|
||||
export type { BaseAttributeSchema } from './models/BaseAttributeSchema';
|
||||
export type { BaseBoardSchema } from './models/BaseBoardSchema';
|
||||
export type { BaseCardTagSchema } from './models/BaseCardTagSchema';
|
||||
export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
|
||||
export type { BaseEnumSchema } from './models/BaseEnumSchema';
|
||||
export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
|
||||
@@ -101,6 +102,7 @@ export type { CardStatusHistorySchema } from './models/CardStatusHistorySchema';
|
||||
export type { CardSummary } from './models/CardSummary';
|
||||
export type { CardSummaryReorderRequest } from './models/CardSummaryReorderRequest';
|
||||
export type { CardSummaryResponse } from './models/CardSummaryResponse';
|
||||
export type { CardTagSchema } from './models/CardTagSchema';
|
||||
export type { CardUpdateGeneralInfoRequest } from './models/CardUpdateGeneralInfoRequest';
|
||||
export type { CardUpdateGeneralInfoResponse } from './models/CardUpdateGeneralInfoResponse';
|
||||
export type { CardUpdateProductQuantityRequest } from './models/CardUpdateProductQuantityRequest';
|
||||
@@ -169,6 +171,8 @@ export type { CreateShippingWarehouseRequest } from './models/CreateShippingWare
|
||||
export type { CreateShippingWarehouseResponse } from './models/CreateShippingWarehouseResponse';
|
||||
export type { CreateStatusRequest } from './models/CreateStatusRequest';
|
||||
export type { CreateStatusResponse } from './models/CreateStatusResponse';
|
||||
export type { CreateTagRequest } from './models/CreateTagRequest';
|
||||
export type { CreateTagResponse } from './models/CreateTagResponse';
|
||||
export type { CreateTaskResponse } from './models/CreateTaskResponse';
|
||||
export type { CreateTransactionTagRequest } from './models/CreateTransactionTagRequest';
|
||||
export type { CreateUserRequest } from './models/CreateUserRequest';
|
||||
@@ -196,6 +200,7 @@ export type { DeleteShippingProductResponse } from './models/DeleteShippingProdu
|
||||
export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWarehouseRequest';
|
||||
export type { DeleteShippingWarehouseResponse } from './models/DeleteShippingWarehouseResponse';
|
||||
export type { DeleteStatusResponse } from './models/DeleteStatusResponse';
|
||||
export type { DeleteTagResponse } from './models/DeleteTagResponse';
|
||||
export type { DeleteTransactionResponse } from './models/DeleteTransactionResponse';
|
||||
export type { DeleteTransactionTagResponse } from './models/DeleteTransactionTagResponse';
|
||||
export type { DeleteUserRequest } from './models/DeleteUserRequest';
|
||||
@@ -346,6 +351,8 @@ export type { StartPauseByShiftIdResponse } from './models/StartPauseByShiftIdRe
|
||||
export type { StartPauseByUserIdResponse } from './models/StartPauseByUserIdResponse';
|
||||
export type { StartShiftResponse } from './models/StartShiftResponse';
|
||||
export type { StatusSchema } from './models/StatusSchema';
|
||||
export type { SwitchTagRequest } from './models/SwitchTagRequest';
|
||||
export type { SwitchTagResponse } from './models/SwitchTagResponse';
|
||||
export type { SynchronizeMarketplaceRequest } from './models/SynchronizeMarketplaceRequest';
|
||||
export type { TaskInfoResponse } from './models/TaskInfoResponse';
|
||||
export type { TimeTrackingData } from './models/TimeTrackingData';
|
||||
@@ -393,6 +400,8 @@ export type { UpdateStatusOrderRequest } from './models/UpdateStatusOrderRequest
|
||||
export type { UpdateStatusOrderResponse } from './models/UpdateStatusOrderResponse';
|
||||
export type { UpdateStatusRequest } from './models/UpdateStatusRequest';
|
||||
export type { UpdateStatusResponse } from './models/UpdateStatusResponse';
|
||||
export type { UpdateTagRequest } from './models/UpdateTagRequest';
|
||||
export type { UpdateTagResponse } from './models/UpdateTagResponse';
|
||||
export type { UpdateTimeTrackingRecordRequest } from './models/UpdateTimeTrackingRecordRequest';
|
||||
export type { UpdateTimeTrackingRecordResponse } from './models/UpdateTimeTrackingRecordResponse';
|
||||
export type { UpdateTransactionRequest } from './models/UpdateTransactionRequest';
|
||||
@@ -421,6 +430,7 @@ export { BillingService } from './services/BillingService';
|
||||
export { BoardService } from './services/BoardService';
|
||||
export { CardService } from './services/CardService';
|
||||
export { CardGroupService } from './services/CardGroupService';
|
||||
export { CardTagService } from './services/CardTagService';
|
||||
export { ClientService } from './services/ClientService';
|
||||
export { DepartmentService } from './services/DepartmentService';
|
||||
export { MarketplaceService } from './services/MarketplaceService';
|
||||
|
||||
9
src/client/models/BaseCardTagSchema.ts
Normal file
9
src/client/models/BaseCardTagSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type BaseCardTagSchema = {
|
||||
name: string;
|
||||
projectId: number;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ export type CardGeneralInfoSchema = {
|
||||
manager?: (UserSchema | null);
|
||||
boardId: number;
|
||||
statusId: number;
|
||||
isServicesProfitAccounted: boolean;
|
||||
clientId: (number | null);
|
||||
tags: Array<string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { CardGroupSchema } from './CardGroupSchema';
|
||||
import type { CardProductSchema } from './CardProductSchema';
|
||||
import type { CardServiceSchema } from './CardServiceSchema';
|
||||
import type { CardStatusHistorySchema } from './CardStatusHistorySchema';
|
||||
import type { CardTagSchema } from './CardTagSchema';
|
||||
import type { ClientSchema } from './ClientSchema';
|
||||
import type { PalletSchema } from './PalletSchema';
|
||||
import type { ShippingWarehouseSchema } from './ShippingWarehouseSchema';
|
||||
@@ -39,6 +40,7 @@ export type CardSchema = {
|
||||
pallets?: Array<PalletSchema>;
|
||||
boxes?: Array<BoxSchema>;
|
||||
employees?: Array<CardEmployeesSchema>;
|
||||
tags?: Array<CardTagSchema>;
|
||||
attributes: Array<CardAttributeSchema>;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { BaseMarketplaceSchema } from './BaseMarketplaceSchema';
|
||||
import type { BoardSchema } from './BoardSchema';
|
||||
import type { CardBillRequestSchema } from './CardBillRequestSchema';
|
||||
import type { CardGroupSchema } from './CardGroupSchema';
|
||||
import type { CardTagSchema } from './CardTagSchema';
|
||||
import type { StatusSchema } from './StatusSchema';
|
||||
export type CardSummary = {
|
||||
id: number;
|
||||
@@ -18,6 +19,7 @@ export type CardSummary = {
|
||||
rank: number;
|
||||
baseMarketplace?: (BaseMarketplaceSchema | null);
|
||||
totalProducts: number;
|
||||
tags: Array<CardTagSchema>;
|
||||
shipmentWarehouseId: (number | null);
|
||||
shipmentWarehouseName: (string | null);
|
||||
billRequest?: (CardBillRequestSchema | null);
|
||||
|
||||
10
src/client/models/CardTagSchema.ts
Normal file
10
src/client/models/CardTagSchema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CardTagSchema = {
|
||||
name: string;
|
||||
projectId: number;
|
||||
id: number;
|
||||
};
|
||||
|
||||
9
src/client/models/CreateTagRequest.ts
Normal file
9
src/client/models/CreateTagRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BaseCardTagSchema } from './BaseCardTagSchema';
|
||||
export type CreateTagRequest = {
|
||||
tag: BaseCardTagSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/CreateTagResponse.ts
Normal file
9
src/client/models/CreateTagResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreateTagResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/DeleteTagResponse.ts
Normal file
9
src/client/models/DeleteTagResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DeleteTagResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AttributeSchema } from './AttributeSchema';
|
||||
import type { CardTagSchema } from './CardTagSchema';
|
||||
import type { ModuleSchema } from './ModuleSchema';
|
||||
export type FullProjectSchema = {
|
||||
name: string;
|
||||
id: number;
|
||||
attributes: Array<AttributeSchema>;
|
||||
modules: Array<ModuleSchema>;
|
||||
tags: Array<CardTagSchema>;
|
||||
boardsCount: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export type GetProfitChartDataRequest = {
|
||||
projectId: number;
|
||||
boardId: number;
|
||||
cardStatusId: number;
|
||||
cardTagId: number;
|
||||
managerId: number;
|
||||
expenseTagId: number;
|
||||
incomeTagId: number;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type GetProfitTableDataRequest = {
|
||||
projectId: number;
|
||||
boardId: number;
|
||||
cardStatusId: number;
|
||||
cardTagId: number;
|
||||
managerId: number;
|
||||
expenseTagId: number;
|
||||
incomeTagId: number;
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
/* eslint-disable */
|
||||
export type ProductsAndServicesGeneralInfoSchema = {
|
||||
shippingWarehouse?: (string | null);
|
||||
isServicesProfitAccounted: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ProfitTableGroupBy = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
export type ProfitTableGroupBy = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AttributeSchema } from './AttributeSchema';
|
||||
import type { CardTagSchema } from './CardTagSchema';
|
||||
import type { ModuleSchema } from './ModuleSchema';
|
||||
export type ProjectSchema = {
|
||||
name: string;
|
||||
id: number;
|
||||
attributes: Array<AttributeSchema>;
|
||||
modules: Array<ModuleSchema>;
|
||||
tags: Array<CardTagSchema>;
|
||||
};
|
||||
|
||||
|
||||
10
src/client/models/SwitchTagRequest.ts
Normal file
10
src/client/models/SwitchTagRequest.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type SwitchTagRequest = {
|
||||
tagId: number;
|
||||
cardId?: (number | null);
|
||||
groupId?: (number | null);
|
||||
};
|
||||
|
||||
9
src/client/models/SwitchTagResponse.ts
Normal file
9
src/client/models/SwitchTagResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type SwitchTagResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateTagRequest.ts
Normal file
9
src/client/models/UpdateTagRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CardTagSchema } from './CardTagSchema';
|
||||
export type UpdateTagRequest = {
|
||||
tag: CardTagSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateTagResponse.ts
Normal file
9
src/client/models/UpdateTagResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateTagResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
97
src/client/services/CardTagService.ts
Normal file
97
src/client/services/CardTagService.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CreateTagRequest } from '../models/CreateTagRequest';
|
||||
import type { CreateTagResponse } from '../models/CreateTagResponse';
|
||||
import type { DeleteTagResponse } from '../models/DeleteTagResponse';
|
||||
import type { SwitchTagRequest } from '../models/SwitchTagRequest';
|
||||
import type { SwitchTagResponse } from '../models/SwitchTagResponse';
|
||||
import type { UpdateTagRequest } from '../models/UpdateTagRequest';
|
||||
import type { UpdateTagResponse } from '../models/UpdateTagResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class CardTagService {
|
||||
/**
|
||||
* Create Tag
|
||||
* @returns CreateTagResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createTag({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: CreateTagRequest,
|
||||
}): CancelablePromise<CreateTagResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/card-tag/',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update Tag
|
||||
* @returns UpdateTagResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static updateTag({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: UpdateTagRequest,
|
||||
}): CancelablePromise<UpdateTagResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/card-tag/',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Delete Tag
|
||||
* @returns DeleteTagResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteTag({
|
||||
cardTagId,
|
||||
}: {
|
||||
cardTagId: number,
|
||||
}): CancelablePromise<DeleteTagResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/card-tag/{card_tag_id}',
|
||||
path: {
|
||||
'card_tag_id': cardTagId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Switch Tag
|
||||
* @returns SwitchTagResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static switchTag({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: SwitchTagRequest,
|
||||
}): CancelablePromise<SwitchTagResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/card-tag/switch',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import CardTags from "../CardTags/CardTags.tsx";
|
||||
|
||||
type Props = {
|
||||
cards: CardSummary[];
|
||||
@@ -75,6 +76,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<CardTags groupId={group.id} tags={cards[0].tags}/>
|
||||
{isServicesAndProductsIncluded && (
|
||||
<Flex
|
||||
p={rem(10)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useContextMenu } from "mantine-contextmenu";
|
||||
import useCardSummaryState from "./useCardSummaryState.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import CardTags from "../CardTags/CardTags.tsx";
|
||||
|
||||
type Props = {
|
||||
cardSummary: CardSummary;
|
||||
@@ -26,7 +27,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const isClientIncluded = isModuleInProject(Modules.CLIENTS, selectedProject);
|
||||
|
||||
const onDealSummaryClick = () => {
|
||||
const onCardSummaryClick = () => {
|
||||
CardService.getCardById({ cardId: cardSummary.id }).then(card => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
@@ -59,7 +60,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
icon: <IconTrash />,
|
||||
},
|
||||
])}
|
||||
onClick={() => onDealSummaryClick()}
|
||||
onClick={() => onCardSummaryClick()}
|
||||
className={styles["container"]}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
@@ -95,6 +96,9 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{!cardSummary.group?.id && (
|
||||
<CardTags cardId={cardSummary.id} tags={cardSummary.tags}/>
|
||||
)}
|
||||
<Flex align={"center"} justify={"space-between"}>
|
||||
<Flex align={"center"} gap={rem(5)}>
|
||||
<CopyButton value={cardSummary.id.toString()}>
|
||||
|
||||
21
src/components/Dnd/Cards/CardTags/CardTags.module.css
Normal file
21
src/components/Dnd/Cards/CardTags/CardTags.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.add-tag-button {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
border: 1px gray dashed;
|
||||
border-radius: 50%;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-tag-button:hover {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.add-tag-button-icon {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.add-tag-button-icon:hover {
|
||||
color: white;
|
||||
}
|
||||
98
src/components/Dnd/Cards/CardTags/CardTags.tsx
Normal file
98
src/components/Dnd/Cards/CardTags/CardTags.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Center, Checkbox, Group, Menu, Pill, rem, Stack, Text } from "@mantine/core";
|
||||
import { CardTagSchema, CardTagService } from "../../../../client";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import styles from "./CardTags.module.css";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx";
|
||||
|
||||
type Props = {
|
||||
cardId?: number;
|
||||
groupId?: number;
|
||||
tags: CardTagSchema[];
|
||||
}
|
||||
|
||||
const CardTags = ({ tags, cardId, groupId }: Props) => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const { refetchCards } = useCardPageContext();
|
||||
|
||||
if (selectedProject?.tags.length === 0) return;
|
||||
|
||||
const onTagClick = (tag: CardTagSchema, event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
CardTagService.switchTag({
|
||||
requestBody: {
|
||||
cardId: cardId ?? null,
|
||||
groupId: groupId ?? null,
|
||||
tagId: tag.id,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) notifications.error({ message });
|
||||
refetchCards();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const addTagButton = (
|
||||
<Menu withArrow>
|
||||
<Menu.Target>
|
||||
<button
|
||||
onClick={e => e.stopPropagation()}
|
||||
className={classNames(styles["add-tag-button"])}
|
||||
>
|
||||
<Center>
|
||||
<IconPlus
|
||||
size={"1.2em"}
|
||||
className={classNames(styles["add-tag-button-icon"])}
|
||||
/>
|
||||
</Center>
|
||||
</button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Stack
|
||||
p={"xs"}
|
||||
gap={"sm"}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{selectedProject?.tags.map(tag => (
|
||||
<Group key={tag.id} wrap={"nowrap"}>
|
||||
<Checkbox
|
||||
checked={!!tags.find(cardTag => cardTag.id === tag.id)}
|
||||
onClick={(event) => onTagClick(tag, event)}
|
||||
/>
|
||||
<Text>{tag.name}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const pills = tags.map(tag => {
|
||||
return (
|
||||
<Pill
|
||||
key={tag.id}
|
||||
style={{
|
||||
color: "gray",
|
||||
backgroundColor: "var(--mantine-color-dark-6)",
|
||||
border: "1px solid gray",
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</Pill>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Group gap={rem(4)}>
|
||||
{addTagButton}
|
||||
{pills}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTags;
|
||||
@@ -1,5 +1,5 @@
|
||||
import styles from "../../../../pages/CardsPage/ui/CardsPage.module.css";
|
||||
import { Divider, Text, Title } from "@mantine/core";
|
||||
import { Divider, Stack, Text, Title } from "@mantine/core";
|
||||
import getColumnColor from "../../Cards/CardsDndColumn/utils/getColumnColor.ts";
|
||||
import { BoardSchema, CardSummary, StatusSchema } from "../../../../client";
|
||||
import { getPluralForm } from "../../../../shared/lib/utils.ts";
|
||||
@@ -10,6 +10,7 @@ import { useContextMenu } from "mantine-contextmenu";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import useStatus from "./hooks/useStatus.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -25,6 +26,10 @@ const Status = ({ summaries, status, board, dragState, index, refetch }: Props)
|
||||
const isDropDisabled = dragState === DragState.DRAG_CARD;
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, board?.project);
|
||||
|
||||
const {
|
||||
divRefs,
|
||||
} = useEqualHeightsContext();
|
||||
|
||||
const {
|
||||
onEditStatusClick,
|
||||
onDeleteStatusClick,
|
||||
@@ -60,7 +65,30 @@ const Status = ({ summaries, status, board, dragState, index, refetch }: Props)
|
||||
},
|
||||
]);
|
||||
|
||||
const header = (
|
||||
<Stack h={"100%"} gap={0} justify={"space-between"}>
|
||||
<Stack gap={0}>
|
||||
<Title
|
||||
style={{ textAlign: "center" }}
|
||||
size={"h4"}>
|
||||
{status.name}
|
||||
</Title>
|
||||
<Text style={{ textAlign: "center", textWrap: "nowrap" }}>
|
||||
{getDealsText()}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Divider
|
||||
size={"xl"}
|
||||
color={getColumnColor(index)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
ref={el => (divRefs.current[index] = el)}
|
||||
>
|
||||
<Droppable
|
||||
isDropDisabled={isDropDisabled}
|
||||
droppableId={"status-" + status.id.toString()}
|
||||
@@ -70,6 +98,7 @@ const Status = ({ summaries, status, board, dragState, index, refetch }: Props)
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Draggable
|
||||
draggableId={"status-" + status.id.toString()}
|
||||
@@ -84,19 +113,7 @@ const Status = ({ summaries, status, board, dragState, index, refetch }: Props)
|
||||
className={styles["header-statuses"]}
|
||||
onContextMenu={contextMenu()}
|
||||
>
|
||||
<Title
|
||||
style={{ textAlign: "center" }}
|
||||
size={"h4"}>
|
||||
{status.name}
|
||||
</Title>
|
||||
<Text style={{ textAlign: "center", textWrap: "nowrap" }}>
|
||||
{getDealsText()}
|
||||
</Text>
|
||||
<Divider
|
||||
size={"xl"}
|
||||
my={10}
|
||||
color={getColumnColor(index)}
|
||||
/>
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
@@ -104,6 +121,7 @@ const Status = ({ summaries, status, board, dragState, index, refetch }: Props)
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { createContext, FC, useContext, useEffect, useRef, useState } from "react";
|
||||
import { StatusSchema } from "../../../../../client";
|
||||
|
||||
type EqualHeightsContextState = {
|
||||
divRefs: React.MutableRefObject<(HTMLDivElement | null)[]>;
|
||||
};
|
||||
|
||||
const EqualHeightsContext = createContext<EqualHeightsContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
type EqualHeightsContextProps = {
|
||||
statuses: StatusSchema[];
|
||||
}
|
||||
|
||||
const useEqualHeightsContextState = ({ statuses }: EqualHeightsContextProps) => {
|
||||
const divRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const updateHeights = () => {
|
||||
divRefs.current.forEach(div => {
|
||||
if (div) {
|
||||
div.style.minHeight = "auto";
|
||||
div.style.height = "auto";
|
||||
}
|
||||
});
|
||||
|
||||
const heights = divRefs.current.map(div => div?.offsetHeight || 0);
|
||||
const newMaxHeight = Math.max(...heights);
|
||||
|
||||
divRefs.current.forEach(div => {
|
||||
if (div) {
|
||||
div.style.minHeight = newMaxHeight + "px";
|
||||
div.style.height = newMaxHeight + "px";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowWidth(window.innerWidth);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateHeights();
|
||||
}, [windowWidth, divRefs.current.length, statuses]);
|
||||
|
||||
return {
|
||||
divRefs,
|
||||
};
|
||||
};
|
||||
|
||||
type EqualHeightsContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
} & EqualHeightsContextProps;
|
||||
|
||||
export const EqualHeightsContextProvider: FC<EqualHeightsContextProviderProps> = ({ children, statuses }) => {
|
||||
const state = useEqualHeightsContextState({ statuses });
|
||||
|
||||
return (
|
||||
<EqualHeightsContext.Provider value={state}>
|
||||
{children}
|
||||
</EqualHeightsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEqualHeightsContext = () => {
|
||||
const context = useContext(EqualHeightsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useEqualHeightsContext must be used within a EqualHeightsContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { Flex, rem, Stack } from "@mantine/core";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
import useDnd from "../../../../pages/CardsPage/hooks/useDnd.tsx";
|
||||
import Status from "../Status/Status.tsx";
|
||||
import { EqualHeightsContextProvider } from "../Status/contexts/EqualHeightContext.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -67,6 +68,7 @@ const Statuses = ({
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<EqualHeightsContextProvider statuses={statuses}>
|
||||
<Flex
|
||||
justify={"space-between"}
|
||||
direction={"column"}
|
||||
@@ -80,6 +82,7 @@ const Statuses = ({
|
||||
</Flex>
|
||||
<CardsDndFooter dragState={dragState} />
|
||||
</Flex>
|
||||
</EqualHeightsContextProvider>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
26
src/components/Selects/CardTagSelect/CardTagSelect.tsx
Normal file
26
src/components/Selects/CardTagSelect/CardTagSelect.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { FC } from "react";
|
||||
import { CardTagSchema, ProjectSchema } from "../../../client";
|
||||
import ObjectSelect, { ObjectSelectProps } from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
|
||||
type OtherProps = {
|
||||
project: ProjectSchema | null;
|
||||
}
|
||||
|
||||
type SelectProps = Omit<ObjectSelectProps<CardTagSchema | null>, "data" | "getLabelFn" | "getValueFn">;
|
||||
|
||||
type Props = OtherProps & SelectProps;
|
||||
|
||||
const CardTagSelect: FC<Props> = ({ project, ...props }) => {
|
||||
const onClear = () => props.onChange(null);
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={project?.tags ?? []}
|
||||
searchable
|
||||
placeholder={"Выберите тег"}
|
||||
onClear={onClear}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CardTagSelect;
|
||||
@@ -38,6 +38,7 @@ import StatusModal from "../pages/CardsPage/modals/StatusModal/StatusModal.tsx";
|
||||
import AttributeModal from "../pages/AdminPage/tabs/Attributes/modals/AttributeModal.tsx";
|
||||
import CreateProjectModal
|
||||
from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/General/modals/CreateProjectModal.tsx";
|
||||
import CardTagModal from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/Tags/modals/CardTagModal.tsx";
|
||||
|
||||
export const modals = {
|
||||
enterDeadline: EnterDeadlineModal,
|
||||
@@ -77,4 +78,5 @@ export const modals = {
|
||||
statusModal: StatusModal,
|
||||
attributeModal: AttributeModal,
|
||||
createProjectModal: CreateProjectModal,
|
||||
cardTagModal: CardTagModal,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { TagsInput, TagsInputProps } from "@mantine/core";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
|
||||
type Props = Omit<TagsInputProps, "data">
|
||||
|
||||
const CardTagsInput = (props: Props) => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
|
||||
return (
|
||||
<TagsInput
|
||||
{...props}
|
||||
data={selectedProject?.tags.map(tag => tag.name)}
|
||||
label={"Теги"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTagsInput;
|
||||
@@ -42,7 +42,7 @@ const CardEditDrawer: FC = () => {
|
||||
|
||||
const getTabPanel = (value: string, component: ReactNode) => {
|
||||
return (
|
||||
<Tabs.Panel value={value}>
|
||||
<Tabs.Panel key={value} value={value}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -61,6 +61,7 @@ const CardEditDrawer: FC = () => {
|
||||
const getTabs = () => {
|
||||
const moduleTabs = modules.map(module => (
|
||||
<Tabs.Tab
|
||||
key={module.info.key}
|
||||
value={module.info.key}
|
||||
leftSection={module.info.icon}
|
||||
>
|
||||
|
||||
@@ -22,9 +22,10 @@ import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyCon
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import ProjectSelect from "../../../../../../components/ProjectSelect/ProjectSelect.tsx";
|
||||
import BoardSelect from "../../../../../../components/BoardSelect/BoardSelect.tsx";
|
||||
import CardStatusSelect from "../../../../../../components/DealStatusSelect/CardStatusSelect.tsx";
|
||||
import CardStatusSelect from "../../../../../../components/CardStatusSelect/CardStatusSelect.tsx";
|
||||
import CardAttributeFields from "../../../../../../components/CardAttributeFields/CardAttributeFields.tsx";
|
||||
import getAttributesFromCard from "../../../../../../components/CardAttributeFields/utils/getAttributesFromCard.ts";
|
||||
import CardTagsInput from "../../../../components/CardTagsInput/CardTagsInput.tsx";
|
||||
|
||||
type Props = {
|
||||
card: CardSchema;
|
||||
@@ -41,6 +42,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
const clipboard = useClipboard();
|
||||
const queryClient = useQueryClient();
|
||||
const [project, setProject] = useState<ProjectSchema | null>(card.board.project);
|
||||
const [cardTags, setCardTags] = useState<string[]>(card.tags?.map(tag => tag.name) ?? []);
|
||||
|
||||
const getInitialValues = (card: CardSchema): CardGeneralFormType => {
|
||||
return {
|
||||
@@ -82,6 +84,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
boardId: values.board.id,
|
||||
clientId: values.client?.id ?? null,
|
||||
attributes,
|
||||
tags: cardTags,
|
||||
},
|
||||
},
|
||||
}).then(({ ok, message }) => {
|
||||
@@ -125,6 +128,20 @@ const Content: FC<Props> = ({ card }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const cancelChanges = () => {
|
||||
form.reset();
|
||||
setCardTags(card.tags?.map(tag => tag.name) ?? []);
|
||||
};
|
||||
|
||||
const isEqualValues = () => {
|
||||
const initialCardTagsSet = new Set(card.tags?.map(tag => tag.name) ?? []);
|
||||
|
||||
const tagsEqual = initialCardTagsSet.size === cardTags.length &&
|
||||
cardTags.every(element => initialCardTagsSet.has(element));
|
||||
|
||||
return isEqual(initialValues, form.values) && tagsEqual;
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
|
||||
<Flex
|
||||
@@ -177,6 +194,12 @@ const Content: FC<Props> = ({ card }) => {
|
||||
placeholder={"Введите коментарий"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
{project && project?.tags.length > 0 && (
|
||||
<CardTagsInput
|
||||
value={cardTags}
|
||||
onChange={setCardTags}
|
||||
/>
|
||||
)}
|
||||
{project && (
|
||||
<CardAttributeFields
|
||||
project={project}
|
||||
@@ -223,14 +246,14 @@ const Content: FC<Props> = ({ card }) => {
|
||||
<Button
|
||||
color={"red"}
|
||||
type={"reset"}
|
||||
disabled={isEqual(initialValues, form.values)}
|
||||
onClick={() => form.reset()}>
|
||||
disabled={isEqualValues()}
|
||||
onClick={cancelChanges}>
|
||||
Отменить изменения
|
||||
</Button>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
disabled={isEqual(initialValues, form.values)}>
|
||||
disabled={isEqualValues()}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Box, Drawer, rem, Tabs } from "@mantine/core";
|
||||
import { IconHexagons, IconSettings, IconSubtask } from "@tabler/icons-react";
|
||||
import { IconHexagons, IconSettings, IconSubtask, IconTags } from "@tabler/icons-react";
|
||||
import { ReactNode } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
|
||||
import General from "./tabs/General/General.tsx";
|
||||
import Attributes from "./tabs/Attributes/Attributes.tsx";
|
||||
import Modules from "./tabs/Modules/Modules.tsx";
|
||||
import Tags from "./tabs/Tags/Tags.tsx";
|
||||
|
||||
|
||||
const ProjectEditDrawer = () => {
|
||||
@@ -67,11 +68,17 @@ const ProjectEditDrawer = () => {
|
||||
leftSection={<IconSubtask />}>
|
||||
Атрибуты
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"tags"}
|
||||
leftSection={<IconTags />}>
|
||||
Теги
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
{getTabPanel("general", <General />)}
|
||||
{getTabPanel("attributes", <Attributes />)}
|
||||
{getTabPanel("modules", <Modules />)}
|
||||
{getTabPanel("tags", <Tags />)}
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ActionIcon, Flex, Group, rem, Stack, Tooltip } from "@mantine/core";
|
||||
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import tagsTableColumns from "./hooks/tagsTableColumns.tsx";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { CardTagSchema } from "../../../../../../client";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import useTags from "./hooks/useTags.tsx";
|
||||
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
|
||||
|
||||
|
||||
const Tags = () => {
|
||||
const columns = tagsTableColumns();
|
||||
|
||||
const {
|
||||
project,
|
||||
onDeleteClick,
|
||||
onChangeClick,
|
||||
onCreateClick,
|
||||
} = useTags();
|
||||
|
||||
return (
|
||||
<Stack gap={rem(10)}>
|
||||
<Group>
|
||||
<InlineButton onClick={onCreateClick}>
|
||||
<IconPlus />
|
||||
Создать
|
||||
</InlineButton>
|
||||
</Group>
|
||||
<BaseTable
|
||||
data={project?.tags}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
|
||||
renderRowActions: ({ row }) => {
|
||||
return (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onChangeClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
} as MRT_TableOptions<CardTagSchema>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { CardTagSchema } from "../../../../../../../client";
|
||||
|
||||
const useTagsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<CardTagSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "name",
|
||||
size: 1000,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export default useTagsTableColumns;
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
CancelablePromise,
|
||||
CardTagSchema,
|
||||
CardTagService,
|
||||
CreateTagResponse,
|
||||
DeleteTagResponse,
|
||||
UpdateTagResponse,
|
||||
} from "../../../../../../../client";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { Text } from "@mantine/core";
|
||||
import { useProjectsContext } from "../../../../../../../contexts/ProjectsContext.tsx";
|
||||
import { useCardPageContext } from "../../../../../contexts/CardPageContext.tsx";
|
||||
|
||||
|
||||
const useTags = () => {
|
||||
const { selectedProject: project, refetchProjects } = useProjectsContext();
|
||||
const { refetchCards } = useCardPageContext();
|
||||
|
||||
const processResponse = (
|
||||
response: CancelablePromise<DeleteTagResponse | UpdateTagResponse | CreateTagResponse>,
|
||||
) => {
|
||||
response
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
refetchProjects();
|
||||
refetchCards();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onDelete = (tag: CardTagSchema) => {
|
||||
const response = CardTagService.deleteTag({
|
||||
cardTagId: tag.id,
|
||||
});
|
||||
|
||||
processResponse(response);
|
||||
};
|
||||
|
||||
const onDeleteClick = (tag: CardTagSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление тега",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить тег {tag.name}
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(tag),
|
||||
});
|
||||
};
|
||||
|
||||
const onChange = (tag: CardTagSchema) => {
|
||||
const response = CardTagService.updateTag({
|
||||
requestBody: { tag },
|
||||
});
|
||||
|
||||
processResponse(response);
|
||||
};
|
||||
|
||||
const onChangeClick = (tag: CardTagSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "cardTagModal",
|
||||
innerProps: {
|
||||
element: tag,
|
||||
onChange,
|
||||
},
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onCreate = (tag: CardTagSchema) => {
|
||||
if (!project) return;
|
||||
|
||||
const response = CardTagService.createTag({
|
||||
requestBody: {
|
||||
tag: {
|
||||
name: tag.name,
|
||||
projectId: project.id,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
processResponse(response);
|
||||
};
|
||||
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "cardTagModal",
|
||||
innerProps: {
|
||||
onCreate,
|
||||
},
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
project,
|
||||
onDeleteClick,
|
||||
onChangeClick,
|
||||
onCreateClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTags;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { CardTagSchema } from "../../../../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { TextInput } from "@mantine/core";
|
||||
|
||||
type Props = CreateEditFormProps<CardTagSchema>;
|
||||
|
||||
const CardTagModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValue: Partial<CardTagSchema> = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const form = useForm<Partial<CardTagSchema>>({
|
||||
initialValues: initialValue,
|
||||
validate: {
|
||||
name: name => !name && "Необходимо указать название тега",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
placeholder={"Введите название тега"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTagModal;
|
||||
@@ -3,7 +3,7 @@ import { Flex, Modal, NumberInput, rem } from "@mantine/core";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { CardsPageState } from "../hooks/useCardsPageState.tsx";
|
||||
import ObjectSelect from "../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import CardStatusSelect from "../../../components/DealStatusSelect/CardStatusSelect.tsx";
|
||||
import CardStatusSelect from "../../../components/CardStatusSelect/CardStatusSelect.tsx";
|
||||
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.delete {
|
||||
|
||||
@@ -6,13 +6,15 @@ import useAllTransactionTagsList from "../../../AdminPage/hooks/useAllTransactio
|
||||
type IsIncome = {
|
||||
isIncome: boolean;
|
||||
}
|
||||
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<TransactionTagSchema | null>,
|
||||
"data" | "getValueFn" | "getLabelFn"
|
||||
> & IsIncome;
|
||||
const TransactionTagSelect: FC<Props> = props => {
|
||||
|
||||
const TransactionTagSelect: FC<Props> = ({ isIncome, ...props }) => {
|
||||
let { objects: tags } = useAllTransactionTagsList();
|
||||
tags = tags.filter(tag => tag.isIncome === props.isIncome);
|
||||
tags = tags.filter(tag => tag.isIncome === isIncome);
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Checkbox, CheckboxProps, Divider, Stack, Text } from "@mantine/core";
|
||||
import ClientSelectNew from "../../../../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
|
||||
import {
|
||||
BaseMarketplaceSchema,
|
||||
BoardSchema,
|
||||
BoardSchema, CardTagSchema,
|
||||
ClientSchema,
|
||||
ProjectSchema,
|
||||
StatusSchema,
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
import { ObjectSelectProps } from "../../../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import BaseMarketplaceSelect
|
||||
from "../../../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import CardStatusSelect from "../../../../../../components/DealStatusSelect/CardStatusSelect.tsx";
|
||||
import CardStatusSelect from "../../../../../../components/CardStatusSelect/CardStatusSelect.tsx";
|
||||
import { ProfitTableSegmentedControl } from "../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
|
||||
import ManagerSelect from "../../../../../../components/ManagerSelect/ManagerSelect.tsx";
|
||||
import TransactionTagSelect from "../../../../components/ExpenseTagSelect/TransactionTagSelect.tsx";
|
||||
import BoardSelect from "../../../../../../components/BoardSelect/BoardSelect.tsx";
|
||||
import ProjectSelect from "../../../../../../components/ProjectSelect/ProjectSelect.tsx";
|
||||
import CardTagSelect from "../../../../../../components/Selects/CardTagSelect/CardTagSelect.tsx";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
type SelectProps<T> = Omit<
|
||||
@@ -41,6 +43,8 @@ type FiltersProps = {
|
||||
|
||||
statusSelectProps?: Omit<ObjectSelectProps<StatusSchema | null>, "data">;
|
||||
|
||||
cardTagSelectProps?: Omit<ObjectSelectProps<CardTagSchema | null>, "data">;
|
||||
|
||||
managerSelectProps?: SelectProps<UserSchema | null | undefined>;
|
||||
onManagerClear?: () => void;
|
||||
|
||||
@@ -78,6 +82,21 @@ export const Filters = (props: FiltersProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.boardSelectProps?.onClear) {
|
||||
props.boardSelectProps.onClear();
|
||||
}
|
||||
if (props.cardTagSelectProps && !props.projectSelectProps?.value) {
|
||||
props.cardTagSelectProps.onChange(null);
|
||||
}
|
||||
}, [props.projectSelectProps?.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.statusSelectProps?.onClear) {
|
||||
props.statusSelectProps.onClear();
|
||||
}
|
||||
}, [props.boardSelectProps?.value]);
|
||||
|
||||
return (
|
||||
<Stack mb={"lg"}>
|
||||
<Divider />
|
||||
@@ -104,6 +123,7 @@ export const Filters = (props: FiltersProps) => {
|
||||
project={props.projectSelectProps?.value ?? null}
|
||||
{...props.boardSelectProps}
|
||||
clearable
|
||||
disabled={!props.projectSelectProps?.value}
|
||||
/>
|
||||
}
|
||||
{props.statusSelectProps &&
|
||||
@@ -111,6 +131,16 @@ export const Filters = (props: FiltersProps) => {
|
||||
board={props.boardSelectProps?.value ?? null}
|
||||
{...props.statusSelectProps}
|
||||
clearable
|
||||
disabled={!props.boardSelectProps?.value}
|
||||
/>
|
||||
}
|
||||
{props.cardTagSelectProps &&
|
||||
<CardTagSelect
|
||||
project={props.projectSelectProps?.value ?? null}
|
||||
{...props.cardTagSelectProps}
|
||||
clearable
|
||||
searchable
|
||||
disabled={!props.projectSelectProps?.value}
|
||||
/>
|
||||
}
|
||||
{props.clientSelectProps &&
|
||||
|
||||
@@ -19,6 +19,7 @@ export const useProfitTableColumns = ({ groupTableBy, statuses }: Props) => {
|
||||
[GroupStatisticsTable.BY_MARKETPLACES]: "Маркетплейс",
|
||||
[GroupStatisticsTable.BY_WAREHOUSES]: "Склад отгрузки",
|
||||
[GroupStatisticsTable.BY_MANAGERS]: "Менеджер",
|
||||
[GroupStatisticsTable.BY_TAGS]: "Тег",
|
||||
};
|
||||
|
||||
const getConditionalColumns = (): MRT_ColumnDef<ProfitTableDataItem>[] => {
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum GroupStatisticsTable {
|
||||
BY_WAREHOUSES,
|
||||
BY_MARKETPLACES,
|
||||
BY_MANAGERS,
|
||||
BY_TAGS,
|
||||
}
|
||||
|
||||
type ControlProps = Omit<SegmentedControlProps, "data">;
|
||||
@@ -22,7 +23,7 @@ type OtherProps = {
|
||||
|
||||
type Props = ControlProps & OtherProps;
|
||||
|
||||
export const ProfitTableSegmentedControl: FC<Props> = props => {
|
||||
export const ProfitTableSegmentedControl: FC<Props> = ({ selectedBoard, selectedProject, ...props}) => {
|
||||
const data: (string | SegmentedControlItem)[] = [
|
||||
{
|
||||
label: "По датам",
|
||||
@@ -39,12 +40,17 @@ export const ProfitTableSegmentedControl: FC<Props> = props => {
|
||||
{
|
||||
label: "По доскам",
|
||||
value: GroupStatisticsTable.BY_BOARDS.toString(),
|
||||
disabled: !props.selectedProject,
|
||||
disabled: !selectedProject,
|
||||
},
|
||||
{
|
||||
label: "По статусам",
|
||||
value: GroupStatisticsTable.BY_STATUSES.toString(),
|
||||
disabled: !props.selectedBoard,
|
||||
disabled: !selectedBoard,
|
||||
},
|
||||
{
|
||||
label: "По тегам",
|
||||
value: GroupStatisticsTable.BY_TAGS.toString(),
|
||||
disabled: !selectedProject,
|
||||
},
|
||||
{
|
||||
label: "По складам отгрузки",
|
||||
@@ -67,15 +73,15 @@ export const ProfitTableSegmentedControl: FC<Props> = props => {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value === GroupStatisticsTable.BY_STATUSES.toString()) {
|
||||
if (!props.selectedProject) {
|
||||
if (!selectedProject) {
|
||||
setGrouping(GroupStatisticsTable.BY_PROJECTS);
|
||||
} else if (!props.selectedBoard) {
|
||||
} else if (!selectedBoard) {
|
||||
setGrouping(GroupStatisticsTable.BY_BOARDS);
|
||||
}
|
||||
} else if (props.value === GroupStatisticsTable.BY_BOARDS.toString() && !props.selectedProject) {
|
||||
} else if (props.value === GroupStatisticsTable.BY_BOARDS.toString() && !selectedProject) {
|
||||
setGrouping(GroupStatisticsTable.BY_PROJECTS);
|
||||
}
|
||||
}, [props.selectedBoard, props.selectedProject]);
|
||||
}, [selectedBoard, selectedProject]);
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
|
||||
@@ -29,10 +29,11 @@ const useProfitTabContextState = () => {
|
||||
project: null,
|
||||
board: null,
|
||||
status: null,
|
||||
cardTag: null,
|
||||
manager: null,
|
||||
expenseTag: null,
|
||||
incomeTag: null,
|
||||
isCompletedOnly: true,
|
||||
isCompletedOnly: false,
|
||||
},
|
||||
});
|
||||
const [isChartLoading, setIsChartLoading] = useState(false);
|
||||
@@ -53,6 +54,7 @@ const useProfitTabContextState = () => {
|
||||
projectId: form.values.project?.id ?? -1,
|
||||
boardId: form.values.board?.id ?? -1,
|
||||
cardStatusId: form.values.status?.id ?? -1,
|
||||
cardTagId: form.values.cardTag?.id ?? -1,
|
||||
managerId: form.values.manager?.id ?? -1,
|
||||
expenseTagId: form.values.expenseTag?.id ?? -1,
|
||||
incomeTagId: form.values.incomeTag?.id ?? -1,
|
||||
|
||||
@@ -43,6 +43,7 @@ export const ProfitFiltersModal = ({ form }: Props) => {
|
||||
projectSelectProps={form.getInputProps("project")}
|
||||
boardSelectProps={form.getInputProps("board")}
|
||||
statusSelectProps={form.getInputProps("status")}
|
||||
cardTagSelectProps={form.getInputProps("cardTag")}
|
||||
managerSelectProps={form.getInputProps("manager")}
|
||||
onManagerClear={() => form.setFieldValue("manager", null)}
|
||||
isCompletedOnlyCheckboxProps={form.getInputProps("isCompletedOnly", { type: "checkbox" })}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
GroupStatisticsTable,
|
||||
} from "../tabs/ProfitTab/components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
|
||||
import {
|
||||
BaseMarketplaceSchema, BoardSchema,
|
||||
BaseMarketplaceSchema, BoardSchema, CardTagSchema,
|
||||
ClientSchema,
|
||||
ProjectSchema,
|
||||
StatusSchema,
|
||||
@@ -18,6 +18,7 @@ export interface FormFilters {
|
||||
project: ProjectSchema | null;
|
||||
board: BoardSchema | null;
|
||||
status: StatusSchema | null;
|
||||
cardTag: CardTagSchema | null;
|
||||
manager: UserSchema | null;
|
||||
isCompletedOnly: boolean;
|
||||
expenseTag: TransactionTagSchema | null;
|
||||
|
||||
Reference in New Issue
Block a user