@tuturuuu/ui 0.2.0 → 0.3.1
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/CHANGELOG.md +53 -0
- package/package.json +79 -67
- package/src/components/ui/__tests__/avatar.test.tsx +8 -5
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
- package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
- package/src/components/ui/chart.test.tsx +29 -0
- package/src/components/ui/chart.tsx +12 -3
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
- package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
- package/src/components/ui/custom/common-footer.tsx +16 -1
- package/src/components/ui/custom/production-indicator.tsx +1 -1
- package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
- package/src/components/ui/custom/settings/task-settings.tsx +18 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
- package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
- package/src/components/ui/custom/sidebar-context.tsx +61 -61
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
- package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
- package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
- package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
- package/src/components/ui/custom/workspace-select.tsx +33 -12
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
- package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
- package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
- package/src/components/ui/finance/invoices/hooks.ts +75 -20
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
- package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
- package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
- package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
- package/src/components/ui/finance/invoices/utils.test.ts +50 -0
- package/src/components/ui/finance/invoices/utils.ts +75 -17
- package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/form.test.tsx +43 -0
- package/src/components/ui/finance/transactions/form.tsx +60 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
- package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
- package/src/components/ui/legacy/meet/page.test.ts +180 -0
- package/src/components/ui/legacy/meet/page.tsx +87 -39
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
- package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
- package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
- package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
- package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
- package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
- package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
- package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
- package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
- package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
- package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
- package/src/hooks/use-calendar-sync.tsx +22 -277
- package/src/hooks/use-calendar.tsx +95 -525
- package/src/hooks/use-task-actions.ts +43 -117
- package/src/hooks/use-user-config.ts +1 -1
- package/src/hooks/use-workspace-config.ts +6 -2
- package/src/hooks/use-workspace-presence.ts +1 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
|
@@ -13,12 +13,20 @@ import { useQueryState } from 'nuqs';
|
|
|
13
13
|
import { useEffect, useMemo, useState } from 'react';
|
|
14
14
|
import { useDebounce } from '../../../../hooks/use-debounce';
|
|
15
15
|
import { useFinanceHref } from '../finance-route-context';
|
|
16
|
+
import {
|
|
17
|
+
type FinancePermissionRequestUser,
|
|
18
|
+
FinancePermissionWarningDialog,
|
|
19
|
+
} from '../shared/finance-permission-warning-dialog';
|
|
16
20
|
import { useFinanceConfidentialVisibility } from '../shared/use-finance-confidential-visibility';
|
|
17
21
|
import { InvoiceBlockedState } from './components/invoice-blocked-state';
|
|
18
22
|
import { InvoiceCheckoutSummary } from './components/invoice-checkout-summary';
|
|
19
23
|
import { InvoiceContentEditor } from './components/invoice-content-editor';
|
|
20
24
|
import { InvoiceCustomerSelectCard } from './components/invoice-customer-select-card';
|
|
21
25
|
import { InvoicePaymentSettings } from './components/invoice-payment-settings';
|
|
26
|
+
import {
|
|
27
|
+
InvoiceProductsPermissionWarning,
|
|
28
|
+
isPermissionRequestError,
|
|
29
|
+
} from './components/invoice-products-permission-warning';
|
|
22
30
|
import { InvoiceUserHistoryAccordion } from './components/invoice-user-history-accordion';
|
|
23
31
|
import { CreatePromotionDialog } from './create-promotion-dialog';
|
|
24
32
|
import type { AvailablePromotion } from './hooks';
|
|
@@ -51,6 +59,9 @@ interface Props {
|
|
|
51
59
|
defaultCurrency?: 'VND' | 'USD';
|
|
52
60
|
canChangeFinanceWallets?: boolean;
|
|
53
61
|
canSetFinanceWalletsOnCreate?: boolean;
|
|
62
|
+
canReadInvoiceProducts?: boolean;
|
|
63
|
+
canReadInvoiceProductStock?: boolean;
|
|
64
|
+
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
export function StandardInvoice({
|
|
@@ -62,6 +73,9 @@ export function StandardInvoice({
|
|
|
62
73
|
defaultCurrency = 'USD',
|
|
63
74
|
canChangeFinanceWallets = true,
|
|
64
75
|
canSetFinanceWalletsOnCreate = true,
|
|
76
|
+
canReadInvoiceProducts = true,
|
|
77
|
+
canReadInvoiceProductStock = true,
|
|
78
|
+
permissionRequestUser,
|
|
65
79
|
}: Props) {
|
|
66
80
|
const t = useTranslations();
|
|
67
81
|
const router = useRouter();
|
|
@@ -77,6 +91,10 @@ export function StandardInvoice({
|
|
|
77
91
|
const [selectedUserId, setSelectedUserId] = useQueryState('user_id', {
|
|
78
92
|
defaultValue: '',
|
|
79
93
|
});
|
|
94
|
+
const [selectedProducts, setSelectedProducts] = useState<
|
|
95
|
+
SelectedProductItem[]
|
|
96
|
+
>([]);
|
|
97
|
+
const hasSelectedProducts = selectedProducts.length > 0;
|
|
80
98
|
|
|
81
99
|
// Data queries
|
|
82
100
|
const {
|
|
@@ -89,24 +107,42 @@ export function StandardInvoice({
|
|
|
89
107
|
isFetching: isFetchingCustomers,
|
|
90
108
|
isFetchingNextPage: isFetchingMoreCustomers,
|
|
91
109
|
} = useInvoiceCustomerSearch(wsId, debouncedCustomerSearch, selectedUserId);
|
|
92
|
-
const {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
const {
|
|
111
|
+
data: products = [],
|
|
112
|
+
error: productsError,
|
|
113
|
+
isLoading: productsLoading,
|
|
114
|
+
} = useProducts(wsId, { enabled: canReadInvoiceProducts });
|
|
115
|
+
const { data: availablePromotions = [] } = useAvailablePromotions(
|
|
116
|
+
wsId,
|
|
117
|
+
selectedUserId,
|
|
118
|
+
{
|
|
119
|
+
enabled: hasSelectedProducts,
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
const { data: promotionsAllowed = true } = useInvoicePromotionConfig(wsId, {
|
|
123
|
+
enabled: !!selectedUserId && hasSelectedProducts,
|
|
124
|
+
});
|
|
96
125
|
const { data: linkedPromotions = [] } = useUserLinkedPromotions(
|
|
97
126
|
wsId,
|
|
98
|
-
selectedUserId
|
|
127
|
+
selectedUserId,
|
|
128
|
+
{ enabled: hasSelectedProducts }
|
|
99
129
|
);
|
|
100
130
|
const { data: referralDiscountRows = [] } = useUserReferralDiscounts(
|
|
101
131
|
wsId,
|
|
102
|
-
selectedUserId
|
|
132
|
+
selectedUserId,
|
|
133
|
+
{ enabled: hasSelectedProducts }
|
|
103
134
|
);
|
|
104
|
-
const { data: wallets = []
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
const { data: wallets = [] } = useWallets(wsId, {
|
|
136
|
+
enabled: hasSelectedProducts,
|
|
137
|
+
});
|
|
138
|
+
const { data: categories = [] } = useCategories(wsId, {
|
|
139
|
+
enabled: hasSelectedProducts,
|
|
140
|
+
});
|
|
107
141
|
|
|
108
142
|
// Blocked groups check
|
|
109
|
-
const { data: blockedGroupIds = [] } = useInvoiceBlockedGroups(wsId
|
|
143
|
+
const { data: blockedGroupIds = [] } = useInvoiceBlockedGroups(wsId, {
|
|
144
|
+
enabled: !!selectedUserId,
|
|
145
|
+
});
|
|
110
146
|
const { data: userGroups = [], isLoading: userGroupsLoading } = useUserGroups(
|
|
111
147
|
wsId,
|
|
112
148
|
selectedUserId
|
|
@@ -128,9 +164,6 @@ export function StandardInvoice({
|
|
|
128
164
|
}, [selectedUserId, blockedGroupIds, userGroups]);
|
|
129
165
|
|
|
130
166
|
// State management
|
|
131
|
-
const [selectedProducts, setSelectedProducts] = useState<
|
|
132
|
-
SelectedProductItem[]
|
|
133
|
-
>([]);
|
|
134
167
|
const [selectedWalletId, setSelectedWalletId] = useState<string>(
|
|
135
168
|
defaultWalletId || ''
|
|
136
169
|
);
|
|
@@ -139,6 +172,18 @@ export function StandardInvoice({
|
|
|
139
172
|
canChangeFinanceWallets,
|
|
140
173
|
canSetFinanceWalletsOnCreate,
|
|
141
174
|
});
|
|
175
|
+
const walletPermissionWarning =
|
|
176
|
+
isWalletSelectionLocked && permissionRequestUser ? (
|
|
177
|
+
<FinancePermissionWarningDialog
|
|
178
|
+
missingPermissions={['set_finance_wallets_on_create']}
|
|
179
|
+
user={permissionRequestUser}
|
|
180
|
+
trigger={
|
|
181
|
+
<Button type="button" variant="outline" size="sm">
|
|
182
|
+
{t('finance-permission-warning.open_request')}
|
|
183
|
+
</Button>
|
|
184
|
+
}
|
|
185
|
+
/>
|
|
186
|
+
) : null;
|
|
142
187
|
|
|
143
188
|
useEffect(() => {
|
|
144
189
|
if (defaultWalletId) {
|
|
@@ -160,13 +205,23 @@ export function StandardInvoice({
|
|
|
160
205
|
(promotion: AvailablePromotion) =>
|
|
161
206
|
promotion.id === selectedPromotionId
|
|
162
207
|
);
|
|
163
|
-
const isLoadingData =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
208
|
+
const isLoadingData = usersLoading || productsLoading || userGroupsLoading;
|
|
209
|
+
|
|
210
|
+
const invoiceProductMissingPermissions = useMemo(() => {
|
|
211
|
+
const missingPermissions = new Set<string>();
|
|
212
|
+
|
|
213
|
+
if (!canReadInvoiceProducts || isPermissionRequestError(productsError)) {
|
|
214
|
+
missingPermissions.add('create_inventory_sales');
|
|
215
|
+
missingPermissions.add('view_inventory_catalog');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!canReadInvoiceProductStock) {
|
|
219
|
+
missingPermissions.add('create_inventory_sales');
|
|
220
|
+
missingPermissions.add('view_inventory_stock');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return [...missingPermissions];
|
|
224
|
+
}, [canReadInvoiceProductStock, canReadInvoiceProducts, productsError]);
|
|
170
225
|
|
|
171
226
|
const referralDiscountMap = useMemo(() => {
|
|
172
227
|
const map = new Map<string, number>();
|
|
@@ -439,12 +494,18 @@ export function StandardInvoice({
|
|
|
439
494
|
</InvoiceCustomerSelectCard>
|
|
440
495
|
|
|
441
496
|
{!isBlocked && (
|
|
442
|
-
<
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
497
|
+
<div className="space-y-3">
|
|
498
|
+
<ProductSelection
|
|
499
|
+
products={products}
|
|
500
|
+
selectedProducts={selectedProducts}
|
|
501
|
+
onSelectedProductsChange={setSelectedProducts}
|
|
502
|
+
currency={defaultCurrency}
|
|
503
|
+
/>
|
|
504
|
+
<InvoiceProductsPermissionWarning
|
|
505
|
+
missingPermissions={invoiceProductMissingPermissions}
|
|
506
|
+
user={permissionRequestUser}
|
|
507
|
+
/>
|
|
508
|
+
</div>
|
|
448
509
|
)}
|
|
449
510
|
</div>
|
|
450
511
|
<div className="space-y-6">
|
|
@@ -472,6 +533,7 @@ export function StandardInvoice({
|
|
|
472
533
|
onWalletChange={setSelectedWalletId}
|
|
473
534
|
onCategoryChange={setSelectedCategoryId}
|
|
474
535
|
walletDisabled={isWalletSelectionLocked}
|
|
536
|
+
walletPermissionWarning={walletPermissionWarning}
|
|
475
537
|
showPromotion
|
|
476
538
|
currency={defaultCurrency}
|
|
477
539
|
promotionsAllowed={promotionsAllowed}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useQueryClient } from '@tanstack/react-query';
|
|
4
|
-
import { Calculator, Loader2, Plus } from '@tuturuuu/icons';
|
|
4
|
+
import { AlertTriangle, Calculator, Loader2, Plus } from '@tuturuuu/icons';
|
|
5
5
|
import { Button } from '@tuturuuu/ui/button';
|
|
6
6
|
import { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card';
|
|
7
7
|
import { Separator } from '@tuturuuu/ui/separator';
|
|
@@ -20,12 +20,20 @@ import {
|
|
|
20
20
|
} from 'react';
|
|
21
21
|
import { useDebounce } from '../../../../hooks/use-debounce';
|
|
22
22
|
import { useFinanceHref } from '../finance-route-context';
|
|
23
|
+
import {
|
|
24
|
+
type FinancePermissionRequestUser,
|
|
25
|
+
FinancePermissionWarningDialog,
|
|
26
|
+
} from '../shared/finance-permission-warning-dialog';
|
|
23
27
|
import { useFinanceConfidentialVisibility } from '../shared/use-finance-confidential-visibility';
|
|
24
28
|
import { InvoiceBlockedState } from './components/invoice-blocked-state';
|
|
25
29
|
import { InvoiceCheckoutSummary } from './components/invoice-checkout-summary';
|
|
26
30
|
import { InvoiceContentEditor } from './components/invoice-content-editor';
|
|
27
31
|
import { InvoiceCustomerSelectCard } from './components/invoice-customer-select-card';
|
|
28
32
|
import { InvoicePaymentSettings } from './components/invoice-payment-settings';
|
|
33
|
+
import {
|
|
34
|
+
InvoiceProductsPermissionWarning,
|
|
35
|
+
isPermissionRequestError,
|
|
36
|
+
} from './components/invoice-products-permission-warning';
|
|
29
37
|
import { SubscriptionAttendanceSummary } from './components/subscription-attendance-summary';
|
|
30
38
|
import { SubscriptionGroupSelector } from './components/subscription-group-selector';
|
|
31
39
|
import { CreatePromotionDialog } from './create-promotion-dialog';
|
|
@@ -62,6 +70,7 @@ import {
|
|
|
62
70
|
getLinkedFinanceCategorySelection,
|
|
63
71
|
getMonthStartDate,
|
|
64
72
|
getSubscriptionAttendanceDisplayData,
|
|
73
|
+
isSubscriptionMonthPaidForGroup,
|
|
65
74
|
} from './utils';
|
|
66
75
|
|
|
67
76
|
interface Props {
|
|
@@ -75,6 +84,10 @@ interface Props {
|
|
|
75
84
|
defaultCurrency?: 'VND' | 'USD';
|
|
76
85
|
canChangeFinanceWallets?: boolean;
|
|
77
86
|
canSetFinanceWalletsOnCreate?: boolean;
|
|
87
|
+
canReadInvoiceProducts?: boolean;
|
|
88
|
+
canReadInvoiceProductStock?: boolean;
|
|
89
|
+
canReadGroupLinkedProducts?: boolean;
|
|
90
|
+
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
export function SubscriptionInvoice({
|
|
@@ -88,6 +101,10 @@ export function SubscriptionInvoice({
|
|
|
88
101
|
defaultCurrency = 'USD',
|
|
89
102
|
canChangeFinanceWallets = true,
|
|
90
103
|
canSetFinanceWalletsOnCreate = true,
|
|
104
|
+
canReadInvoiceProducts = true,
|
|
105
|
+
canReadInvoiceProductStock = true,
|
|
106
|
+
canReadGroupLinkedProducts = true,
|
|
107
|
+
permissionRequestUser,
|
|
91
108
|
}: Props) {
|
|
92
109
|
const t = useTranslations();
|
|
93
110
|
const locale = useLocale();
|
|
@@ -115,6 +132,10 @@ export function SubscriptionInvoice({
|
|
|
115
132
|
|
|
116
133
|
const [customerSearch, setCustomerSearch] = useState('');
|
|
117
134
|
const [debouncedCustomerSearch] = useDebounce(customerSearch, 300);
|
|
135
|
+
const [subscriptionSelectedProducts, setSubscriptionSelectedProducts] =
|
|
136
|
+
useState<SelectedProductItem[]>([]);
|
|
137
|
+
const hasSelectedGroups = selectedGroupIds.length > 0;
|
|
138
|
+
const hasSelectedProducts = subscriptionSelectedProducts.length > 0;
|
|
118
139
|
|
|
119
140
|
const updateSearchParam = useCallback(
|
|
120
141
|
(key: string, value: string) => {
|
|
@@ -143,23 +164,41 @@ export function SubscriptionInvoice({
|
|
|
143
164
|
isFetching: isFetchingCustomers,
|
|
144
165
|
isFetchingNextPage: isFetchingMoreCustomers,
|
|
145
166
|
} = useInvoiceCustomerSearch(wsId, debouncedCustomerSearch, selectedUserId);
|
|
146
|
-
const {
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
const {
|
|
168
|
+
data: products = [],
|
|
169
|
+
error: productsError,
|
|
170
|
+
isLoading: productsLoading,
|
|
171
|
+
} = useProducts(wsId, {
|
|
172
|
+
enabled: canReadInvoiceProducts && hasSelectedGroups,
|
|
173
|
+
});
|
|
174
|
+
const { data: availablePromotions = [] } = useAvailablePromotions(
|
|
175
|
+
wsId,
|
|
176
|
+
selectedUserId,
|
|
177
|
+
{
|
|
178
|
+
enabled: hasSelectedProducts,
|
|
179
|
+
}
|
|
180
|
+
);
|
|
149
181
|
const { data: linkedPromotions = [] } = useUserLinkedPromotions(
|
|
150
182
|
wsId,
|
|
151
|
-
selectedUserId
|
|
183
|
+
selectedUserId,
|
|
184
|
+
{ enabled: hasSelectedProducts }
|
|
152
185
|
);
|
|
153
186
|
const { data: referralDiscountRows = [] } = useUserReferralDiscounts(
|
|
154
187
|
wsId,
|
|
155
|
-
selectedUserId
|
|
188
|
+
selectedUserId,
|
|
189
|
+
{ enabled: hasSelectedProducts }
|
|
156
190
|
);
|
|
157
|
-
const { data: wallets = []
|
|
158
|
-
|
|
159
|
-
|
|
191
|
+
const { data: wallets = [] } = useWallets(wsId, {
|
|
192
|
+
enabled: hasSelectedProducts,
|
|
193
|
+
});
|
|
194
|
+
const { data: categories = [] } = useCategories(wsId, {
|
|
195
|
+
enabled: hasSelectedProducts,
|
|
196
|
+
});
|
|
160
197
|
|
|
161
198
|
// Blocked groups check
|
|
162
|
-
const { data: blockedGroupIds = [] } = useInvoiceBlockedGroups(wsId
|
|
199
|
+
const { data: blockedGroupIds = [] } = useInvoiceBlockedGroups(wsId, {
|
|
200
|
+
enabled: hasSelectedGroups,
|
|
201
|
+
});
|
|
163
202
|
|
|
164
203
|
const isBlocked = useMemo(
|
|
165
204
|
() => selectedGroupIds.some((groupId) => blockedGroupIds.includes(groupId)),
|
|
@@ -175,6 +214,18 @@ export function SubscriptionInvoice({
|
|
|
175
214
|
canChangeFinanceWallets,
|
|
176
215
|
canSetFinanceWalletsOnCreate,
|
|
177
216
|
});
|
|
217
|
+
const walletPermissionWarning =
|
|
218
|
+
isWalletSelectionLocked && permissionRequestUser ? (
|
|
219
|
+
<FinancePermissionWarningDialog
|
|
220
|
+
missingPermissions={['set_finance_wallets_on_create']}
|
|
221
|
+
user={permissionRequestUser}
|
|
222
|
+
trigger={
|
|
223
|
+
<Button type="button" variant="outline" size="sm">
|
|
224
|
+
{t('finance-permission-warning.open_request')}
|
|
225
|
+
</Button>
|
|
226
|
+
}
|
|
227
|
+
/>
|
|
228
|
+
) : null;
|
|
178
229
|
|
|
179
230
|
useEffect(() => {
|
|
180
231
|
if (defaultWalletId) {
|
|
@@ -202,19 +253,22 @@ export function SubscriptionInvoice({
|
|
|
202
253
|
// Track previous user ID to detect user changes (skip initial mount for reset)
|
|
203
254
|
const prevUserIdRef = useRef<string | null>(null);
|
|
204
255
|
|
|
205
|
-
// Product selection state
|
|
206
|
-
const [subscriptionSelectedProducts, setSubscriptionSelectedProducts] =
|
|
207
|
-
useState<SelectedProductItem[]>([]);
|
|
208
|
-
|
|
209
256
|
// Subscription-specific queries
|
|
210
257
|
const { data: userGroups = [], isLoading: userGroupsLoading } = useUserGroups(
|
|
211
258
|
wsId,
|
|
212
259
|
selectedUserId
|
|
213
260
|
);
|
|
214
|
-
const {
|
|
215
|
-
|
|
261
|
+
const {
|
|
262
|
+
data: groupProducts = [],
|
|
263
|
+
error: groupProductsError,
|
|
264
|
+
isLoading: groupProductsLoading,
|
|
265
|
+
} = useMultiGroupProducts(wsId, selectedGroupIds, {
|
|
266
|
+
enabled: canReadGroupLinkedProducts,
|
|
267
|
+
});
|
|
216
268
|
|
|
217
|
-
const { data: useAttendanceBased = true } = useInvoiceAttendanceConfig(wsId
|
|
269
|
+
const { data: useAttendanceBased = true } = useInvoiceAttendanceConfig(wsId, {
|
|
270
|
+
enabled: hasSelectedGroups,
|
|
271
|
+
});
|
|
218
272
|
|
|
219
273
|
const availableMonthOptions = useMemo(
|
|
220
274
|
() =>
|
|
@@ -286,15 +340,15 @@ export function SubscriptionInvoice({
|
|
|
286
340
|
|
|
287
341
|
// A month is considered paid ONLY if ALL selected groups have paid for it.
|
|
288
342
|
// If ANY selected group has not paid, we allow creating an invoice.
|
|
289
|
-
|
|
290
|
-
const latestInvoice = latestSubscriptionInvoices.find(
|
|
291
|
-
(inv) => inv.group_id === groupId
|
|
292
|
-
);
|
|
293
|
-
if (!latestInvoice?.valid_until) return false;
|
|
343
|
+
if (Number.isNaN(selectedMonthStart.getTime())) return false;
|
|
294
344
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
345
|
+
return selectedGroupIds.every((groupId) =>
|
|
346
|
+
isSubscriptionMonthPaidForGroup(
|
|
347
|
+
groupId,
|
|
348
|
+
effectiveSelectedMonth,
|
|
349
|
+
latestSubscriptionInvoices
|
|
350
|
+
)
|
|
351
|
+
);
|
|
298
352
|
}, [effectiveSelectedMonth, latestSubscriptionInvoices, selectedGroupIds]);
|
|
299
353
|
|
|
300
354
|
const billableAttendance = useMemo(
|
|
@@ -386,12 +440,42 @@ export function SubscriptionInvoice({
|
|
|
386
440
|
subscriptionInvoiceContextLoading ||
|
|
387
441
|
groupProductsLoading;
|
|
388
442
|
|
|
389
|
-
const isLoadingData =
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
443
|
+
const isLoadingData = usersLoading || productsLoading;
|
|
444
|
+
|
|
445
|
+
const invoiceProductMissingPermissions = useMemo(() => {
|
|
446
|
+
const missingPermissions = new Set<string>();
|
|
447
|
+
|
|
448
|
+
if (!canReadInvoiceProducts || isPermissionRequestError(productsError)) {
|
|
449
|
+
missingPermissions.add('create_inventory_sales');
|
|
450
|
+
missingPermissions.add('view_inventory_catalog');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!canReadInvoiceProductStock) {
|
|
454
|
+
missingPermissions.add('create_inventory_sales');
|
|
455
|
+
missingPermissions.add('view_inventory_stock');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (
|
|
459
|
+
!canReadGroupLinkedProducts ||
|
|
460
|
+
isPermissionRequestError(groupProductsError)
|
|
461
|
+
) {
|
|
462
|
+
missingPermissions.add('create_invoices');
|
|
463
|
+
missingPermissions.add('view_user_groups');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (isPermissionRequestError(subscriptionInvoiceContextError)) {
|
|
467
|
+
missingPermissions.add('create_invoices');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return [...missingPermissions];
|
|
471
|
+
}, [
|
|
472
|
+
canReadGroupLinkedProducts,
|
|
473
|
+
canReadInvoiceProductStock,
|
|
474
|
+
canReadInvoiceProducts,
|
|
475
|
+
groupProductsError,
|
|
476
|
+
productsError,
|
|
477
|
+
subscriptionInvoiceContextError,
|
|
478
|
+
]);
|
|
395
479
|
|
|
396
480
|
const referralDiscountMap = useMemo(() => {
|
|
397
481
|
const map = new Map<string, number>();
|
|
@@ -820,22 +904,45 @@ export function SubscriptionInvoice({
|
|
|
820
904
|
)}
|
|
821
905
|
|
|
822
906
|
{selectedGroupIds.length > 0 && !isSelectedMonthPaid && !isBlocked && (
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
907
|
+
<>
|
|
908
|
+
<ProductSelection
|
|
909
|
+
products={products}
|
|
910
|
+
selectedProducts={subscriptionSelectedProducts}
|
|
911
|
+
onSelectedProductsChange={setSubscriptionSelectedProducts}
|
|
912
|
+
currency={defaultCurrency}
|
|
913
|
+
groupLinkedProducts={(groupProducts || [])
|
|
914
|
+
.map((item) => ({
|
|
915
|
+
productId: item.workspace_products?.id,
|
|
916
|
+
groupName:
|
|
917
|
+
item.workspace_user_groups?.name ||
|
|
918
|
+
t('ws-invoices.no_name'),
|
|
919
|
+
}))
|
|
920
|
+
.filter(
|
|
921
|
+
(item): item is { productId: string; groupName: string } =>
|
|
922
|
+
!!item.productId
|
|
923
|
+
)}
|
|
924
|
+
/>
|
|
925
|
+
<InvoiceProductsPermissionWarning
|
|
926
|
+
missingPermissions={invoiceProductMissingPermissions}
|
|
927
|
+
user={permissionRequestUser}
|
|
928
|
+
/>
|
|
929
|
+
{!isLoadingSubscriptionData &&
|
|
930
|
+
subscriptionSelectedProducts.length === 0 && (
|
|
931
|
+
<div className="rounded-lg border border-dynamic-yellow/30 bg-dynamic-yellow/10 p-3 text-dynamic-yellow text-sm">
|
|
932
|
+
<div className="flex items-start gap-2">
|
|
933
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
|
934
|
+
<div>
|
|
935
|
+
<p className="font-medium">
|
|
936
|
+
{t('ws-invoices.month_unpaid')}
|
|
937
|
+
</p>
|
|
938
|
+
<p className="text-muted-foreground">
|
|
939
|
+
{t('ws-invoices.no_products_to_invoice')}
|
|
940
|
+
</p>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
837
944
|
)}
|
|
838
|
-
|
|
945
|
+
</>
|
|
839
946
|
)}
|
|
840
947
|
|
|
841
948
|
<InvoiceContentEditor
|
|
@@ -890,6 +997,7 @@ export function SubscriptionInvoice({
|
|
|
890
997
|
onWalletChange={setSelectedWalletId}
|
|
891
998
|
onCategoryChange={setSelectedCategoryId}
|
|
892
999
|
walletDisabled={isWalletSelectionLocked}
|
|
1000
|
+
walletPermissionWarning={walletPermissionWarning}
|
|
893
1001
|
showPromotion
|
|
894
1002
|
currency={defaultCurrency}
|
|
895
1003
|
promotionsAllowed={true}
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
getBillableSessionsForGroups,
|
|
5
5
|
getLinkedFinanceCategorySelection,
|
|
6
6
|
getSubscriptionAttendanceDisplayData,
|
|
7
|
+
getSubscriptionCoverageInvoiceForGroup,
|
|
8
|
+
isSubscriptionMonthPaidForGroup,
|
|
7
9
|
type UserGroup,
|
|
8
10
|
} from './utils';
|
|
9
11
|
|
|
@@ -135,6 +137,54 @@ describe('subscription invoice attendance display data', () => {
|
|
|
135
137
|
});
|
|
136
138
|
});
|
|
137
139
|
|
|
140
|
+
describe('subscription invoice coverage', () => {
|
|
141
|
+
it('treats valid_until as the exclusive first unpaid month', () => {
|
|
142
|
+
const latestInvoices = [{ group_id: groupId, valid_until: '2026-06-01' }];
|
|
143
|
+
|
|
144
|
+
expect(
|
|
145
|
+
isSubscriptionMonthPaidForGroup(groupId, '2026-05', latestInvoices)
|
|
146
|
+
).toBe(true);
|
|
147
|
+
expect(
|
|
148
|
+
isSubscriptionMonthPaidForGroup(groupId, '2026-06', latestInvoices)
|
|
149
|
+
).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('treats null and invalid valid_until values as unpaid', () => {
|
|
153
|
+
expect(
|
|
154
|
+
isSubscriptionMonthPaidForGroup(groupId, '2026-05', [
|
|
155
|
+
{ group_id: groupId, valid_until: null },
|
|
156
|
+
])
|
|
157
|
+
).toBe(false);
|
|
158
|
+
expect(
|
|
159
|
+
isSubscriptionMonthPaidForGroup(groupId, '2026-05', [
|
|
160
|
+
{ group_id: groupId, valid_until: 'not-a-date' },
|
|
161
|
+
])
|
|
162
|
+
).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('uses the furthest valid_until over the newest created_at', () => {
|
|
166
|
+
const latestInvoices = [
|
|
167
|
+
{
|
|
168
|
+
created_at: '2026-06-10T00:00:00.000Z',
|
|
169
|
+
group_id: groupId,
|
|
170
|
+
valid_until: '2026-06-01',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
created_at: '2026-05-10T00:00:00.000Z',
|
|
174
|
+
group_id: groupId,
|
|
175
|
+
valid_until: '2026-07-01',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
getSubscriptionCoverageInvoiceForGroup(latestInvoices, groupId)
|
|
181
|
+
).toEqual(latestInvoices[1]);
|
|
182
|
+
expect(
|
|
183
|
+
isSubscriptionMonthPaidForGroup(groupId, '2026-06', latestInvoices)
|
|
184
|
+
).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
138
188
|
describe('linked finance category selection', () => {
|
|
139
189
|
it('returns the single linked finance category across selected products', () => {
|
|
140
190
|
expect(
|