@floristcloud/api-lib 1.2.106 → 1.2.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/client/create-client.command.js +2 -0
- package/build/commands/client/get-client-list.query.js +8 -0
- package/build/commands/client/index.js +1 -0
- package/build/commands/client/update-client-contract-data.command.js +22 -0
- package/build/commands/client/update-client.command.js +2 -0
- package/build/commands/money-account/get-money-account-by-criteria.query.js +7 -2
- package/build/commands/money-account/get-money-account-list.query.js +5 -1
- package/build/commands/pre-order/group/group-create-standing-orders-from-pre-orders.command.js +39 -0
- package/build/commands/pre-order/group/index.js +1 -0
- package/build/commands/report/export-inventory-stock.query.js +4 -17
- package/build/commands/report/export-inventory-turnover.query.js +4 -27
- package/build/commands/report/get-inventory-stock.query.js +34 -0
- package/build/commands/report/get-inventory-turnover.query.js +43 -0
- package/build/commands/report/get-report-by-client-weeks.query.js +19 -0
- package/build/commands/report/get-report-group-by-client.query.js +21 -2
- package/build/commands/report/get-report-group-by-position.query.js +21 -2
- package/build/commands/report/get-sales-by-client.query.js +45 -0
- package/build/commands/report/index.js +3 -0
- package/build/commands/settings/get-company-profile.query.js +24 -0
- package/build/commands/settings/index.js +1 -0
- package/build/constant/error-i18n-key.js +4 -0
- package/build/constant/error.js +21 -0
- package/build/enum/client-contract-status.enum.js +13 -0
- package/build/enum/index.js +3 -0
- package/build/enum/report-sort-by.enum.js +13 -0
- package/build/helpers/index.js +2 -0
- package/build/helpers/pagination.helper.js +12 -0
- package/build/helpers/query-boolean.helper.js +16 -0
- package/build/schemas/client/client.schema.js +2 -0
- package/build/schemas/index.js +7 -0
- package/build/schemas/money-account/money-account-with-balance.schema.js +13 -0
- package/build/schemas/pre-order/pre-order.schema.js +16 -2
- package/build/schemas/report/inventory-stock-category-item.schema.js +10 -0
- package/build/schemas/report/inventory-stock-detailed-item.schema.js +14 -0
- package/build/schemas/report/inventory-turnover-category-item.schema.js +15 -0
- package/build/schemas/report/inventory-turnover-detailed-item.schema.js +19 -0
- package/build/schemas/report/sales-by-client-row.schema.js +45 -0
- package/build/schemas/report/sales-by-client-top-item.schema.js +9 -0
- package/commands/client/create-client.command.ts +2 -0
- package/commands/client/get-client-list.query.ts +8 -0
- package/commands/client/index.ts +1 -0
- package/commands/client/update-client-contract-data.command.ts +24 -0
- package/commands/client/update-client.command.ts +2 -0
- package/commands/money-account/get-money-account-by-criteria.query.ts +10 -3
- package/commands/money-account/get-money-account-list.query.ts +8 -2
- package/commands/pre-order/group/group-create-standing-orders-from-pre-orders.command.ts +42 -0
- package/commands/pre-order/group/index.ts +1 -0
- package/commands/report/export-inventory-stock.query.ts +4 -19
- package/commands/report/export-inventory-turnover.query.ts +4 -29
- package/commands/report/get-inventory-stock.query.ts +36 -0
- package/commands/report/get-inventory-turnover.query.ts +45 -0
- package/commands/report/get-report-by-client-weeks.query.ts +20 -1
- package/commands/report/get-report-group-by-client.query.ts +22 -3
- package/commands/report/get-report-group-by-position.query.ts +23 -4
- package/commands/report/get-sales-by-client.query.ts +47 -0
- package/commands/report/index.ts +3 -0
- package/commands/settings/get-company-profile.query.ts +24 -0
- package/commands/settings/index.ts +1 -0
- package/constant/error-i18n-key.ts +5 -0
- package/constant/error.ts +21 -0
- package/enum/client-contract-status.enum.ts +9 -0
- package/enum/index.ts +3 -0
- package/enum/report-sort-by.enum.ts +9 -0
- package/helpers/index.ts +2 -0
- package/helpers/pagination.helper.ts +9 -0
- package/helpers/query-boolean.helper.ts +14 -0
- package/package.json +1 -1
- package/schemas/client/client.schema.ts +2 -0
- package/schemas/index.ts +7 -0
- package/schemas/money-account/money-account-with-balance.schema.ts +11 -0
- package/schemas/pre-order/pre-order.schema.ts +16 -1
- package/schemas/report/inventory-stock-category-item.schema.ts +8 -0
- package/schemas/report/inventory-stock-detailed-item.schema.ts +12 -0
- package/schemas/report/inventory-turnover-category-item.schema.ts +13 -0
- package/schemas/report/inventory-turnover-detailed-item.schema.ts +17 -0
- package/schemas/report/sales-by-client-row.schema.ts +43 -0
- package/schemas/report/sales-by-client-top-item.schema.ts +7 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { dateRangeCheck } from '../../helpers/date.helper';
|
|
3
|
+
import { isPageAccompaniedByCount } from '../../helpers/pagination.helper';
|
|
4
|
+
import { queryBooleanSchema } from '../../helpers/query-boolean.helper';
|
|
3
5
|
import { ClientByWeeksSchema } from '../../schemas';
|
|
4
6
|
|
|
5
7
|
const GetReportByClientWeeksRequestSchema = z
|
|
@@ -7,6 +9,15 @@ const GetReportByClientWeeksRequestSchema = z
|
|
|
7
9
|
startDate: z.coerce.date(),
|
|
8
10
|
endDate: z.coerce.date(),
|
|
9
11
|
responsibleUserUUID: z.string().optional(),
|
|
12
|
+
// Отчёт — это матрица «неделя × клиент», поэтому по умолчанию отдаёт строку и на нулевые недели.
|
|
13
|
+
// Флаг оставляет только недели с заказами: то, что обычно и нужно, и на порядок меньше данных.
|
|
14
|
+
onlyWithOrders: queryBooleanSchema,
|
|
15
|
+
// Фильтр по группе клиента (client."group") — дешёвый, применяется до CROSS JOIN недель.
|
|
16
|
+
clientGroup: z.string().optional(),
|
|
17
|
+
// Пагинация по строкам матрицы. Без count лимит не накладывается — поведение по умолчанию
|
|
18
|
+
// прежнее, а page без count отклоняется: иначе листание молча возвращало бы одну и ту же выдачу.
|
|
19
|
+
page: z.coerce.number().int().positive().optional(),
|
|
20
|
+
count: z.coerce.number().int().positive().optional(),
|
|
10
21
|
})
|
|
11
22
|
.refine(
|
|
12
23
|
value => {
|
|
@@ -20,11 +31,19 @@ const GetReportByClientWeeksRequestSchema = z
|
|
|
20
31
|
message: 'validation.dateRange',
|
|
21
32
|
path: ['endDate', 'startDate'],
|
|
22
33
|
},
|
|
23
|
-
)
|
|
34
|
+
)
|
|
35
|
+
.refine(isPageAccompaniedByCount, {
|
|
36
|
+
message: 'validation.pageRequiresCount',
|
|
37
|
+
path: ['page'],
|
|
38
|
+
});
|
|
24
39
|
|
|
25
40
|
const GetReportGroupByClientResponseSchema = z.object({
|
|
26
41
|
message: z.string().optional(),
|
|
27
42
|
data: z.array(ClientByWeeksSchema),
|
|
43
|
+
// `total` лежит РЯДОМ с data, а не внутри неё: панель читает `data` как плоский массив строк,
|
|
44
|
+
// и обёртка `{ list, total }` (как в остальных пагинируемых отчётах) сломала бы ей рендер.
|
|
45
|
+
// Без total последняя страница неотличима от полной выдачи.
|
|
46
|
+
total: z.number().describe('Rows in the whole report, before "page"/"count" are applied.'),
|
|
28
47
|
});
|
|
29
48
|
|
|
30
49
|
export namespace GetReportByClientWeeksContractQuery {
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { OrderByEnum, ReportSortByEnum } from '../../enum';
|
|
3
|
+
import { dateRangeCheck } from '../../helpers/date.helper';
|
|
3
4
|
|
|
4
5
|
const GetReportGroupByClientRequestSchema = z
|
|
5
6
|
.object({
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
// Даты приходят «сырым» днём — раскладку в полуинтервал [start, endExclusive) делает сервис
|
|
8
|
+
// в таймзоне компании, как в cash-flow. Прежний setStartDay/setEndDay в UTC разворачивал день
|
|
9
|
+
// второй раз, и в таймзоне с плюсовым сдвигом конец периода уезжал на сутки вперёд.
|
|
10
|
+
startDate: z.coerce.date(),
|
|
11
|
+
endDate: z.coerce.date(),
|
|
12
|
+
// Сортировка НЕ имеет значения по умолчанию: без явного sortBy/sortOrder строки отдаются
|
|
13
|
+
// в том порядке, в каком их вернул отчёт (панель рендерит map в порядке ключей, и тихая
|
|
14
|
+
// пересортировка меняла бы ей вид таблицы). Исключение — limit: он срезает хвост, что без
|
|
15
|
+
// сортировки бессмысленно, поэтому при limit применяется порядок «по прибыли, убывание».
|
|
16
|
+
sortBy: z
|
|
17
|
+
.nativeEnum(ReportSortByEnum)
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Sort metric. Without it (and without "limit") rows keep the report order.'),
|
|
20
|
+
sortOrder: z.nativeEnum(OrderByEnum).optional().describe('Sort direction. Defaults to "desc" once sorting applies.'),
|
|
21
|
+
// Без значения — ограничения нет: панель по-прежнему получает все строки.
|
|
22
|
+
limit: z.coerce.number().int().positive().optional().describe('Keep only the top N rows; implies sorting by gross profit desc.'),
|
|
8
23
|
})
|
|
9
24
|
.refine(
|
|
10
25
|
value => {
|
|
@@ -43,6 +58,10 @@ const GetReportGroupByClientResponseSchema = z.object({
|
|
|
43
58
|
marginality: z.number(),
|
|
44
59
|
}),
|
|
45
60
|
),
|
|
61
|
+
// Аддитивно к прежней форме: число клиентов во ВСЁМ отчёте до усечения и применённый limit.
|
|
62
|
+
// Без них усечённый ответ неотличим от полного.
|
|
63
|
+
total: z.number().describe('Client rows in the whole report, before "limit".'),
|
|
64
|
+
limit: z.number().optional().describe('Row limit that actually cut rows off. Absent when the response is complete.'),
|
|
46
65
|
}),
|
|
47
66
|
});
|
|
48
67
|
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { PositionReportTypeEnum } from '../../enum';
|
|
3
|
-
import { dateRangeCheck
|
|
2
|
+
import { OrderByEnum, PositionReportTypeEnum, ReportSortByEnum } from '../../enum';
|
|
3
|
+
import { dateRangeCheck } from '../../helpers/date.helper';
|
|
4
4
|
|
|
5
5
|
const GetReportGroupByPositionRequestSchema = z
|
|
6
6
|
.object({
|
|
7
7
|
type: z.nativeEnum(PositionReportTypeEnum).default(PositionReportTypeEnum.PRODUCT),
|
|
8
8
|
categoryUUID: z.string().optional(),
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Даты приходят «сырым» днём — раскладку в полуинтервал [start, endExclusive) делает сервис
|
|
10
|
+
// в таймзоне компании, как в cash-flow. Прежний setStartDay/setEndDay в UTC разворачивал день
|
|
11
|
+
// второй раз, и в таймзоне с плюсовым сдвигом конец периода уезжал на сутки вперёд.
|
|
12
|
+
startDate: z.coerce.date(),
|
|
13
|
+
endDate: z.coerce.date(),
|
|
14
|
+
// Сортировка НЕ имеет значения по умолчанию: без явного sortBy/sortOrder строки отдаются
|
|
15
|
+
// в том порядке, в каком их вернул отчёт (панель рендерит map в порядке ключей, и тихая
|
|
16
|
+
// пересортировка меняла бы ей вид таблицы). Исключение — limit: он срезает хвост, что без
|
|
17
|
+
// сортировки бессмысленно, поэтому при limit применяется порядок «по прибыли, убывание».
|
|
18
|
+
sortBy: z
|
|
19
|
+
.nativeEnum(ReportSortByEnum)
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Sort metric. Without it (and without "limit") rows keep the report order.'),
|
|
22
|
+
sortOrder: z.nativeEnum(OrderByEnum).optional().describe('Sort direction. Defaults to "desc" once sorting applies.'),
|
|
23
|
+
// Без значения — ограничения нет: панель по-прежнему получает все строки.
|
|
24
|
+
limit: z.coerce.number().int().positive().optional().describe('Keep only the top N rows; implies sorting by gross profit desc.'),
|
|
11
25
|
})
|
|
12
26
|
.refine(
|
|
13
27
|
value => {
|
|
@@ -67,6 +81,11 @@ const GetReportGroupByPositionResponseSchema = z.object({
|
|
|
67
81
|
}),
|
|
68
82
|
)
|
|
69
83
|
.optional(),
|
|
84
|
+
// Аддитивно к прежней форме: отчёт отдаёт ЛИБО категории, ЛИБО товары (при categoryUUID),
|
|
85
|
+
// поэтому total относится к той группировке, что вернулась. Вместе с limit это единственный
|
|
86
|
+
// способ отличить усечённый ответ от полного.
|
|
87
|
+
total: z.number().describe('Rows in the whole report (of the returned grouping), before "limit".'),
|
|
88
|
+
limit: z.number().optional().describe('Row limit that actually cut rows off. Absent when the response is complete.'),
|
|
70
89
|
}),
|
|
71
90
|
});
|
|
72
91
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { OrderByEnum, ReportSortByEnum } from '../../enum';
|
|
3
|
+
import { dateRangeCheck, isStatementPeriodWithinLimit } from '../../helpers/date.helper';
|
|
4
|
+
import { isPageAccompaniedByCount } from '../../helpers/pagination.helper';
|
|
5
|
+
import { SalesByClientRowSchema } from '../../schemas/report/sales-by-client-row.schema';
|
|
6
|
+
|
|
7
|
+
const GetSalesByClientRequestSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
startDate: z.coerce.date(),
|
|
10
|
+
endDate: z.coerce.date(),
|
|
11
|
+
// По умолчанию самые прибыльные клиенты сверху, чтобы count срезал хвост, а не топ.
|
|
12
|
+
sortBy: z.nativeEnum(ReportSortByEnum).optional().default(ReportSortByEnum.GROSS_PROFIT),
|
|
13
|
+
sortOrder: z.nativeEnum(OrderByEnum).optional().default(OrderByEnum.DESC),
|
|
14
|
+
// Пагинация по строкам отчёта. Без count лимит не накладывается — отдаём всех клиентов,
|
|
15
|
+
// а page без count отклоняется: иначе листание молча возвращало бы одну и ту же выдачу.
|
|
16
|
+
page: z.coerce.number().int().positive().optional().describe('1-based page number, requires "count".'),
|
|
17
|
+
count: z.coerce.number().int().positive().optional().describe('Rows per page. Without it every client row is returned.'),
|
|
18
|
+
})
|
|
19
|
+
.refine(value => dateRangeCheck(value.startDate, value.endDate), {
|
|
20
|
+
message: 'validation.dateRange',
|
|
21
|
+
path: ['endDate', 'startDate'],
|
|
22
|
+
})
|
|
23
|
+
.refine(value => isStatementPeriodWithinLimit(value.startDate, value.endDate), {
|
|
24
|
+
message: 'validation.reportPeriodMax',
|
|
25
|
+
path: ['endDate', 'startDate'],
|
|
26
|
+
})
|
|
27
|
+
.refine(isPageAccompaniedByCount, {
|
|
28
|
+
message: 'validation.pageRequiresCount',
|
|
29
|
+
path: ['page'],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const GetSalesByClientResponseSchema = z.object({
|
|
33
|
+
message: z.string().optional(),
|
|
34
|
+
data: z.object({
|
|
35
|
+
list: z.array(SalesByClientRowSchema),
|
|
36
|
+
// Число клиентов во всём отчёте — не размер страницы.
|
|
37
|
+
total: z.number(),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export namespace GetSalesByClientContractQuery {
|
|
42
|
+
export const RequestSchema = GetSalesByClientRequestSchema;
|
|
43
|
+
export type Request = z.infer<typeof RequestSchema>;
|
|
44
|
+
|
|
45
|
+
export const ResponseSchema = GetSalesByClientResponseSchema;
|
|
46
|
+
export type Response = z.infer<typeof ResponseSchema>;
|
|
47
|
+
}
|
package/commands/report/index.ts
CHANGED
|
@@ -12,3 +12,6 @@ export * from './export-client-statement.query';
|
|
|
12
12
|
export * from './get-report-by-client-weeks.query';
|
|
13
13
|
export * from './get-daily-orders-summary.query';
|
|
14
14
|
export * from './get-cash-flow.query';
|
|
15
|
+
export * from './get-inventory-stock.query';
|
|
16
|
+
export * from './get-inventory-turnover.query';
|
|
17
|
+
export * from './get-sales-by-client.query';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Company-level facts every panel role may read: who the company is, how it is named, which time
|
|
5
|
+
* zone its calendar days are counted in and which currency its figures are in. `companyUUID` is
|
|
6
|
+
* echoed so a caller can prove the profile belongs to its own tenant — the ADMIN-only
|
|
7
|
+
* `settings/for-admin` answer carries no such marker and cannot be cross-checked.
|
|
8
|
+
*/
|
|
9
|
+
const CompanyProfileSchema = z.object({
|
|
10
|
+
companyUUID: z.string(),
|
|
11
|
+
companyName: z.string().nullable(),
|
|
12
|
+
timeZone: z.string().nullable(),
|
|
13
|
+
currency: z.string().nullable(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const GetCompanyProfileResponseSchema = z.object({
|
|
17
|
+
message: z.string().optional(),
|
|
18
|
+
data: CompanyProfileSchema,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export namespace GetCompanyProfileContractQuery {
|
|
22
|
+
export const ResponseSchema = GetCompanyProfileResponseSchema;
|
|
23
|
+
export type Response = z.infer<typeof ResponseSchema>;
|
|
24
|
+
}
|
|
@@ -70,6 +70,11 @@ export const ErrorI18nKey = {
|
|
|
70
70
|
soCriticalError: 'preOrderFromStandingOrder.criticalError',
|
|
71
71
|
soNoPreOrdersCreated: 'preOrderFromStandingOrder.noPreOrdersCreated',
|
|
72
72
|
|
|
73
|
+
// Pre-order → standing-order conversion warnings
|
|
74
|
+
preOrderToSoSourceEmpty: 'preOrderToStandingOrder.sourceEmpty',
|
|
75
|
+
preOrderToSoItemSkipped: 'preOrderToStandingOrder.itemSkipped',
|
|
76
|
+
preOrderToSoProductNotCreated: 'preOrderToStandingOrder.productNotCreated',
|
|
77
|
+
|
|
73
78
|
// Copy pre-order per-item warnings
|
|
74
79
|
preOrderCopyItemNoMatches: 'preOrderCopy.itemNoMatches',
|
|
75
80
|
preOrderCopyItemInsufficientQuantity: 'preOrderCopy.itemInsufficientQuantity',
|
package/constant/error.ts
CHANGED
|
@@ -888,6 +888,27 @@ export const ERRORS = {
|
|
|
888
888
|
message: 'No pre-orders were created for this standing order: no matching items found in the selected collections',
|
|
889
889
|
httpCode: 400,
|
|
890
890
|
},
|
|
891
|
+
// PRE ORDER — pre-order → standing-order conversion
|
|
892
|
+
PRE_ORDER_TO_STANDING_ORDER_FAILED: {
|
|
893
|
+
code: 'PO044',
|
|
894
|
+
message: 'Failed to create standing order from pre-order',
|
|
895
|
+
httpCode: 500,
|
|
896
|
+
},
|
|
897
|
+
PRE_ORDER_TO_SO_SOURCE_EMPTY: {
|
|
898
|
+
code: 'PO045',
|
|
899
|
+
message: 'Pre-order has no positions to convert into a standing order',
|
|
900
|
+
httpCode: 400,
|
|
901
|
+
},
|
|
902
|
+
PRE_ORDER_TO_SO_ITEM_SKIPPED: {
|
|
903
|
+
code: 'PO046',
|
|
904
|
+
message: 'Position was skipped: a standing-order position requires both a name and a category',
|
|
905
|
+
httpCode: 400,
|
|
906
|
+
},
|
|
907
|
+
PRE_ORDER_TO_SO_PRODUCT_NOT_CREATED: {
|
|
908
|
+
code: 'PO047',
|
|
909
|
+
message: 'Failed to create standing-order position from pre-order position',
|
|
910
|
+
httpCode: 400,
|
|
911
|
+
},
|
|
891
912
|
// PRE ORDER — copy pre-order per-item warnings
|
|
892
913
|
PRE_ORDER_COPY_ITEM_NO_MATCHES: {
|
|
893
914
|
code: 'PO032',
|
package/enum/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from './order-distribute-status-list';
|
|
|
36
36
|
export * from './order-active-status-list';
|
|
37
37
|
export * from './order-price-correction-skipped-status-list';
|
|
38
38
|
export * from './client-status.enum';
|
|
39
|
+
export * from './client-contract-status.enum';
|
|
39
40
|
export * from './client-type.enum';
|
|
40
41
|
export * from './consignment-distribution-validation-status.enum';
|
|
41
42
|
export * from './consignment-distribution-validation-type.enum';
|
|
@@ -95,3 +96,5 @@ export * from './legal-identifier-field.enum';
|
|
|
95
96
|
export * from './permission-key.enum';
|
|
96
97
|
export * from './cash-flow-item-status.enum';
|
|
97
98
|
export * from './payment-invoice-status.enum';
|
|
99
|
+
export * from './order-by.enum';
|
|
100
|
+
export * from './report-sort-by.enum';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Метрика сортировки для отчётов-группировок (по позициям и по клиентам).
|
|
3
|
+
* Значения совпадают с именами полей ответа, чтобы клиент мог сортировать «по тому, что видит».
|
|
4
|
+
*/
|
|
5
|
+
export enum ReportSortByEnum {
|
|
6
|
+
GROSS_PROFIT = 'grossProfit',
|
|
7
|
+
TOTAL_SALES = 'totalSales',
|
|
8
|
+
SOLD_UNITS = 'soldUnits',
|
|
9
|
+
}
|
package/helpers/index.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `page` имеет смысл только вместе с `count`.
|
|
3
|
+
*
|
|
4
|
+
* Раньше `page` без `count` молча игнорировался и отчёт отдавался целиком: агент, листающий
|
|
5
|
+
* страницы, бесконечно получал одну и ту же выдачу и не мог отличить это от «страниц больше нет».
|
|
6
|
+
* Поэтому неполная пара отклоняется валидацией — единообразно во всех постраничных отчётах.
|
|
7
|
+
*/
|
|
8
|
+
export const isPageAccompaniedByCount = (value: { page?: number; count?: number }): boolean =>
|
|
9
|
+
value.page === undefined || value.count !== undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Boolean-флаг, приходящий в query-строке.
|
|
5
|
+
*
|
|
6
|
+
* Не `z.coerce.boolean()`: в query всегда приходит текст, а `Boolean('false') === true` —
|
|
7
|
+
* такой флаг невозможно выключить явным значением. Принимаем `true` / `1` как включение,
|
|
8
|
+
* всё остальное — выключение. По умолчанию флаг выключен, чтобы поведение без параметра не менялось.
|
|
9
|
+
*/
|
|
10
|
+
export const queryBooleanSchema = z
|
|
11
|
+
.union([z.boolean(), z.string()])
|
|
12
|
+
.optional()
|
|
13
|
+
.default(false)
|
|
14
|
+
.transform(value => (typeof value === 'boolean' ? value : value === 'true' || value === '1'));
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ClientStatusEnum } from '../../enum/client-status.enum';
|
|
3
|
+
import { ClientContractStatusEnum } from '../../enum/client-contract-status.enum';
|
|
3
4
|
import { ClientTypeEnum } from '../../enum/client-type.enum';
|
|
4
5
|
import { UserSchema } from '../user/user.schema';
|
|
5
6
|
import { ClientIndividualProfileSchema } from './client-individual-profile.schema';
|
|
@@ -17,6 +18,7 @@ export const ClientSchema = z.object({
|
|
|
17
18
|
isCommon: z.boolean(),
|
|
18
19
|
contractNumber: z.string().nullable(),
|
|
19
20
|
contractDate: z.date().nullable(),
|
|
21
|
+
contractStatus: z.nativeEnum(ClientContractStatusEnum),
|
|
20
22
|
address: z.string().nullable(),
|
|
21
23
|
city: z.string().nullable(),
|
|
22
24
|
code: z.string().nullable(),
|
package/schemas/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from './product-image/product-image.schema';
|
|
|
22
22
|
export * from './settings/settings.schema';
|
|
23
23
|
export * from './payment-invoice/payment-invoice.schema';
|
|
24
24
|
export * from './money-account/money-account.schema';
|
|
25
|
+
export * from './money-account/money-account-with-balance.schema';
|
|
25
26
|
export * from './domain/domain.schema';
|
|
26
27
|
export * from './category/category.schema';
|
|
27
28
|
export * from './branch/branch.schema';
|
|
@@ -52,6 +53,12 @@ export * from './consignment/consignment-distribution-validation.schema';
|
|
|
52
53
|
export * from './product-image-template/product-image-template.schema';
|
|
53
54
|
export * from './report/client-statement.schema';
|
|
54
55
|
export * from './report/client-by-weeks.schema';
|
|
56
|
+
export * from './report/inventory-stock-category-item.schema';
|
|
57
|
+
export * from './report/inventory-stock-detailed-item.schema';
|
|
58
|
+
export * from './report/inventory-turnover-category-item.schema';
|
|
59
|
+
export * from './report/inventory-turnover-detailed-item.schema';
|
|
60
|
+
export * from './report/sales-by-client-top-item.schema';
|
|
61
|
+
export * from './report/sales-by-client-row.schema';
|
|
55
62
|
export * from './action-log/index';
|
|
56
63
|
export * from './delivery-route/delivery-route.schema';
|
|
57
64
|
export * from './product/product-multiplicity-option.schema';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { MoneyAccountSchema } from './money-account.schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Денежный счёт в ответах списков. `balance` — текущий остаток счёта (приход минус расход),
|
|
6
|
+
* приходит только при `?withBalance=true`: поле аддитивное, клиенты без этого параметра
|
|
7
|
+
* получают ровно прежнюю форму ответа.
|
|
8
|
+
*/
|
|
9
|
+
export const MoneyAccountWithBalanceSchema = MoneyAccountSchema.extend({
|
|
10
|
+
balance: z.number().optional(),
|
|
11
|
+
});
|
|
@@ -43,7 +43,22 @@ export const CreatePreOrderSchema = z.object({
|
|
|
43
43
|
status: z.nativeEnum(PreOrderStatusEnum),
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
// STANDING_ORDER is not a status a pre-order can be switched into: an SO must live in the
|
|
47
|
+
// STANDING_ORDER_CATALOG collection with its own 1:1 collection items. Flipping the status alone
|
|
48
|
+
// leaves the pre-order half-converted (shared catalog items, stale reserved balances), so the
|
|
49
|
+
// panel update endpoints only accept the regular lifecycle statuses. Use the pre-order → SO
|
|
50
|
+
// conversion (POST /pre-order/group-actions/create-standing-orders-from-pre-orders) instead.
|
|
51
|
+
export const UpdatablePreOrderStatusSchema = z.enum([
|
|
52
|
+
PreOrderStatusEnum.NEW,
|
|
53
|
+
PreOrderStatusEnum.WEBSHOP_NEW,
|
|
54
|
+
PreOrderStatusEnum.ACCEPTED,
|
|
55
|
+
PreOrderStatusEnum.VERIFIED,
|
|
56
|
+
PreOrderStatusEnum.COMPLETED,
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
export const UpdatePreOrderSchema = PreOrderSchema.omit({ uuid: true, preOrderNum: true, createdAt: true })
|
|
60
|
+
.partial()
|
|
61
|
+
.extend({ status: UpdatablePreOrderStatusSchema.optional() });
|
|
47
62
|
|
|
48
63
|
export const CreatePreOrderWebshopSchema = PreOrderSchema.pick({ branchUUID: true, comment: true }).extend({
|
|
49
64
|
preOrderCollectionUUID: z.string(),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Строка отчёта об остатках с детализацией по name/size/grower (`detailed=true`). */
|
|
4
|
+
export const InventoryStockDetailedItemSchema = z.object({
|
|
5
|
+
categoryUUID: z.string(),
|
|
6
|
+
categoryName: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
color: z.string().nullable(),
|
|
9
|
+
size: z.string().nullable(),
|
|
10
|
+
grower: z.string().nullable(),
|
|
11
|
+
balance: z.number(),
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Строка отчёта об оборачиваемости в разрезе категорий (`detailed=false`). */
|
|
4
|
+
export const InventoryTurnoverCategoryItemSchema = z.object({
|
|
5
|
+
categoryUUID: z.string(),
|
|
6
|
+
categoryName: z.string(),
|
|
7
|
+
initialBalance: z.number(), // начальный остаток
|
|
8
|
+
receipts: z.number(), // приходы
|
|
9
|
+
sales: z.number(), // продажи
|
|
10
|
+
currentBalance: z.number(), // текущий остаток
|
|
11
|
+
daysInStock: z.number(), // дни в наличии на складе
|
|
12
|
+
turnoverRate: z.number(), // коэффициент оборачиваемости
|
|
13
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Строка отчёта об оборачиваемости с детализацией по name/size/grower (`detailed=true`). */
|
|
4
|
+
export const InventoryTurnoverDetailedItemSchema = z.object({
|
|
5
|
+
categoryUUID: z.string(),
|
|
6
|
+
categoryName: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
color: z.string().nullable(),
|
|
9
|
+
size: z.string().nullable(),
|
|
10
|
+
grower: z.string().nullable(), // производитель
|
|
11
|
+
initialBalance: z.number(), // начальный остаток
|
|
12
|
+
receipts: z.number(), // приходы
|
|
13
|
+
sales: z.number(), // продажи
|
|
14
|
+
currentBalance: z.number(), // текущий остаток
|
|
15
|
+
daysInStock: z.number(), // дни в наличии на складе
|
|
16
|
+
turnoverRate: z.number(), // коэффициент оборачиваемости
|
|
17
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SalesByClientTopItemSchema } from './sales-by-client-top-item.schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Строка отчёта «продажи по клиентам» — те же данные, что уходят в Excel из `export/sales/clients`.
|
|
6
|
+
* Опциональные поля отсутствуют в ответе, когда у клиента нет соответствующего значения.
|
|
7
|
+
*/
|
|
8
|
+
export const SalesByClientRowSchema = z.object({
|
|
9
|
+
uuid: z.string(),
|
|
10
|
+
clientName: z.string(),
|
|
11
|
+
responsibleUserName: z.string().optional(),
|
|
12
|
+
totalOrders: z.number(),
|
|
13
|
+
soldUnits: z.number(),
|
|
14
|
+
soldUnitsByUnit: z.object({
|
|
15
|
+
PIECE: z.number(),
|
|
16
|
+
PACK: z.number(),
|
|
17
|
+
BOX: z.number(),
|
|
18
|
+
}),
|
|
19
|
+
totalSales: z.number(),
|
|
20
|
+
avgCheck: z.number(),
|
|
21
|
+
totalWriteOff: z.number(),
|
|
22
|
+
grossProfit: z.number(),
|
|
23
|
+
marginality: z.number(),
|
|
24
|
+
// Оплаты (INCOME) и предоплаты (PREPAYMENT) клиента за период
|
|
25
|
+
totalPayments: z.number(),
|
|
26
|
+
totalPrepayments: z.number(),
|
|
27
|
+
// Сумма услуг (order_service_line) за период — отдельно, вне товарных sales/margin
|
|
28
|
+
totalServiceAmount: z.number(),
|
|
29
|
+
// Текущий баланс клиента (на момент формирования отчёта)
|
|
30
|
+
balance: z.number(),
|
|
31
|
+
lastOrderDate: z.coerce.date().optional(),
|
|
32
|
+
// Топ-5 по сумме выручки за период
|
|
33
|
+
topCategories: z.array(SalesByClientTopItemSchema),
|
|
34
|
+
topProducts: z.array(SalesByClientTopItemSchema),
|
|
35
|
+
// Статическая информация о клиенте
|
|
36
|
+
code: z.string().optional(),
|
|
37
|
+
city: z.string().optional(),
|
|
38
|
+
contactName: z.string().optional(),
|
|
39
|
+
phone: z.string().optional(),
|
|
40
|
+
clientType: z.string().optional(),
|
|
41
|
+
status: z.string().optional(),
|
|
42
|
+
group: z.string().optional(),
|
|
43
|
+
});
|