@tuturuuu/ui 0.1.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 +71 -0
- package/package.json +82 -70
- 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/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
- package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -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/legacy/meet/planId/page.tsx +10 -4
- package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
- package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
- 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/__tests__/use-task-realtime-sync.test.tsx +37 -9
- 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-realtime-sync.ts +89 -70
- 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
|
@@ -20,7 +20,8 @@ export type UserGroup = {
|
|
|
20
20
|
workspace_user_groups: WorkspaceUserGroup | null;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
type
|
|
23
|
+
export type SubscriptionCoverageInvoice = {
|
|
24
|
+
created_at?: string | null;
|
|
24
25
|
group_id?: string;
|
|
25
26
|
valid_until?: string | null;
|
|
26
27
|
};
|
|
@@ -119,6 +120,67 @@ export const formatMonthLabel = (month: string, locale: string): string => {
|
|
|
119
120
|
});
|
|
120
121
|
};
|
|
121
122
|
|
|
123
|
+
const getComparableTimestamp = (value: string | null | undefined): number => {
|
|
124
|
+
if (!value) return 0;
|
|
125
|
+
const timestamp = new Date(value).getTime();
|
|
126
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const getComparableValidUntilTimestamp = (
|
|
130
|
+
invoice: SubscriptionCoverageInvoice
|
|
131
|
+
): number => {
|
|
132
|
+
const validUntil = parseLocalCalendarDate(invoice.valid_until);
|
|
133
|
+
return Number.isNaN(validUntil.getTime()) ? 0 : validUntil.getTime();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const getSubscriptionCoverageInvoiceForGroup = (
|
|
137
|
+
latestInvoices: SubscriptionCoverageInvoice[],
|
|
138
|
+
groupId: string
|
|
139
|
+
): SubscriptionCoverageInvoice | undefined =>
|
|
140
|
+
latestInvoices
|
|
141
|
+
.filter((invoice) => invoice.group_id === groupId)
|
|
142
|
+
.filter((invoice) => getComparableValidUntilTimestamp(invoice) > 0)
|
|
143
|
+
.sort((a, b) => {
|
|
144
|
+
const validUntilDiff =
|
|
145
|
+
getComparableValidUntilTimestamp(b) -
|
|
146
|
+
getComparableValidUntilTimestamp(a);
|
|
147
|
+
if (validUntilDiff !== 0) return validUntilDiff;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
getComparableTimestamp(b.created_at) -
|
|
151
|
+
getComparableTimestamp(a.created_at)
|
|
152
|
+
);
|
|
153
|
+
})[0];
|
|
154
|
+
|
|
155
|
+
export const isSubscriptionMonthCoveredByInvoice = (
|
|
156
|
+
selectedMonth: string,
|
|
157
|
+
invoice: SubscriptionCoverageInvoice | null | undefined
|
|
158
|
+
): boolean => {
|
|
159
|
+
if (!invoice?.valid_until) return false;
|
|
160
|
+
|
|
161
|
+
const selectedMonthStart = getMonthStartDate(selectedMonth);
|
|
162
|
+
const validUntilMonthStart = getMonthStartDate(invoice.valid_until);
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
Number.isNaN(selectedMonthStart.getTime()) ||
|
|
166
|
+
Number.isNaN(validUntilMonthStart.getTime())
|
|
167
|
+
) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return selectedMonthStart < validUntilMonthStart;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const isSubscriptionMonthPaidForGroup = (
|
|
175
|
+
groupId: string,
|
|
176
|
+
selectedMonth: string,
|
|
177
|
+
latestInvoices: SubscriptionCoverageInvoice[]
|
|
178
|
+
): boolean =>
|
|
179
|
+
isSubscriptionMonthCoveredByInvoice(
|
|
180
|
+
selectedMonth,
|
|
181
|
+
getSubscriptionCoverageInvoiceForGroup(latestInvoices, groupId)
|
|
182
|
+
);
|
|
183
|
+
|
|
122
184
|
export const getAttendanceStats = (
|
|
123
185
|
attendance: AttendanceRecord[]
|
|
124
186
|
): AttendanceStats => {
|
|
@@ -159,10 +221,13 @@ export const getEffectiveAttendanceDays = (
|
|
|
159
221
|
};
|
|
160
222
|
|
|
161
223
|
const getGroupValidUntilDate = (
|
|
162
|
-
latestInvoices:
|
|
224
|
+
latestInvoices: SubscriptionCoverageInvoice[],
|
|
163
225
|
groupId: string
|
|
164
226
|
): Date | null => {
|
|
165
|
-
const latestInvoice =
|
|
227
|
+
const latestInvoice = getSubscriptionCoverageInvoiceForGroup(
|
|
228
|
+
latestInvoices,
|
|
229
|
+
groupId
|
|
230
|
+
);
|
|
166
231
|
if (!latestInvoice?.valid_until) return null;
|
|
167
232
|
|
|
168
233
|
const validUntil = parseLocalCalendarDate(latestInvoice.valid_until);
|
|
@@ -185,7 +250,7 @@ export const getBillableSessionsForGroups = (
|
|
|
185
250
|
userGroups: UserGroup[],
|
|
186
251
|
groupIds: string[],
|
|
187
252
|
selectedMonth: string,
|
|
188
|
-
latestInvoices:
|
|
253
|
+
latestInvoices: SubscriptionCoverageInvoice[] = []
|
|
189
254
|
): BillableSession[] => {
|
|
190
255
|
if (!selectedMonth || groupIds.length === 0) return [];
|
|
191
256
|
|
|
@@ -220,7 +285,7 @@ export const getBillableAttendanceRecords = (
|
|
|
220
285
|
attendance: AttendanceRecord[],
|
|
221
286
|
groupIds: string[],
|
|
222
287
|
selectedMonth: string,
|
|
223
|
-
latestInvoices:
|
|
288
|
+
latestInvoices: SubscriptionCoverageInvoice[] = []
|
|
224
289
|
): AttendanceRecord[] => {
|
|
225
290
|
if (!Array.isArray(attendance) || !selectedMonth || groupIds.length === 0) {
|
|
226
291
|
return [];
|
|
@@ -384,7 +449,7 @@ export type AvailableMonthOption = {
|
|
|
384
449
|
export const getAvailableMonths = (
|
|
385
450
|
userGroups: UserGroup[],
|
|
386
451
|
groupIds: string[],
|
|
387
|
-
latestInvoices:
|
|
452
|
+
latestInvoices: SubscriptionCoverageInvoice[],
|
|
388
453
|
locale: string,
|
|
389
454
|
selectedMonthFallback: string | null = null
|
|
390
455
|
): AvailableMonthOption[] => {
|
|
@@ -411,17 +476,10 @@ export const getAvailableMonths = (
|
|
|
411
476
|
while (currentDate <= normalizedLatestEnd) {
|
|
412
477
|
const value = formatMonthValue(currentDate);
|
|
413
478
|
const label = formatMonthLabel(value, locale);
|
|
414
|
-
const itemMonthStart = new Date(currentDate);
|
|
415
|
-
itemMonthStart.setDate(1);
|
|
416
479
|
|
|
417
|
-
const isPaid = groupIds.every((groupId) =>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
if (!latestInvoice?.valid_until) return false;
|
|
422
|
-
const validUntilMonthStart = getMonthStartDate(latestInvoice.valid_until);
|
|
423
|
-
return itemMonthStart < validUntilMonthStart;
|
|
424
|
-
});
|
|
480
|
+
const isPaid = groupIds.every((groupId) =>
|
|
481
|
+
isSubscriptionMonthPaidForGroup(groupId, value, latestInvoices)
|
|
482
|
+
);
|
|
425
483
|
|
|
426
484
|
months.push({ value, label, isPaid });
|
|
427
485
|
currentDate.setMonth(currentDate.getMonth() + 1);
|
|
@@ -434,7 +492,7 @@ export const getTotalSessionsForGroups = (
|
|
|
434
492
|
userGroups: UserGroup[],
|
|
435
493
|
groupIds: string[],
|
|
436
494
|
selectedMonth: string,
|
|
437
|
-
latestInvoices:
|
|
495
|
+
latestInvoices: SubscriptionCoverageInvoice[] = []
|
|
438
496
|
): number => {
|
|
439
497
|
return getBillableSessionsForGroups(
|
|
440
498
|
userGroups,
|
|
@@ -8,18 +8,20 @@ import {
|
|
|
8
8
|
interface FinanceDisplayAmountProps {
|
|
9
9
|
value: string;
|
|
10
10
|
className?: string;
|
|
11
|
+
alwaysShow?: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function FinanceDisplayAmount({
|
|
14
15
|
value,
|
|
15
16
|
className,
|
|
17
|
+
alwaysShow = false,
|
|
16
18
|
}: FinanceDisplayAmountProps) {
|
|
17
19
|
const { isConfidential: areNumbersHidden } =
|
|
18
20
|
useFinanceConfidentialVisibility();
|
|
19
21
|
|
|
20
22
|
return (
|
|
21
23
|
<span className={className}>
|
|
22
|
-
{areNumbersHidden ? FINANCE_HIDDEN_AMOUNT : value}
|
|
24
|
+
{areNumbersHidden && !alwaysShow ? FINANCE_HIDDEN_AMOUNT : value}
|
|
23
25
|
</span>
|
|
24
26
|
);
|
|
25
27
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { FinancePermissionWarningContent } from './finance-permission-warning-dialog';
|
|
4
|
+
|
|
5
|
+
vi.mock('next-intl', () => ({
|
|
6
|
+
useTranslations: () => (key: string, values?: Record<string, string>) =>
|
|
7
|
+
values ? `${key}:${Object.values(values).join(',')}` : key,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
11
|
+
toast: {
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('FinancePermissionWarningContent', () => {
|
|
18
|
+
it('shows missing permissions and request identity details', () => {
|
|
19
|
+
render(
|
|
20
|
+
<FinancePermissionWarningContent
|
|
21
|
+
missingPermissions={['create_invoices']}
|
|
22
|
+
user={{
|
|
23
|
+
displayName: 'Jane Doe',
|
|
24
|
+
email: 'jane@example.com',
|
|
25
|
+
id: 'user-1',
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(screen.getByText('create_invoices')).toBeVisible();
|
|
31
|
+
expect(screen.getByText('user-1')).toBeVisible();
|
|
32
|
+
expect(screen.getByText('Jane Doe')).toBeVisible();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle, Copy } from '@tuturuuu/icons';
|
|
4
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from '@tuturuuu/ui/dialog';
|
|
14
|
+
import { toast } from '@tuturuuu/ui/sonner';
|
|
15
|
+
import { useTranslations } from 'next-intl';
|
|
16
|
+
import type { ReactNode } from 'react';
|
|
17
|
+
import { useMemo, useState } from 'react';
|
|
18
|
+
|
|
19
|
+
export interface FinancePermissionRequestUser {
|
|
20
|
+
displayName?: string | null;
|
|
21
|
+
email?: string | null;
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface FinancePermissionWarningContentProps {
|
|
26
|
+
missingPermissions: string[];
|
|
27
|
+
user?: FinancePermissionRequestUser | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FinancePermissionWarningDialogProps
|
|
31
|
+
extends FinancePermissionWarningContentProps {
|
|
32
|
+
defaultOpen?: boolean;
|
|
33
|
+
trigger?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveDisplayName(
|
|
37
|
+
user: FinancePermissionRequestUser | null | undefined,
|
|
38
|
+
fallback: string
|
|
39
|
+
) {
|
|
40
|
+
return user?.displayName || user?.email || user?.id || fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function FinancePermissionWarningContent({
|
|
44
|
+
missingPermissions,
|
|
45
|
+
user,
|
|
46
|
+
}: FinancePermissionWarningContentProps) {
|
|
47
|
+
const t = useTranslations();
|
|
48
|
+
const fallbackUser = t('finance-permission-warning.unknown_user');
|
|
49
|
+
const displayName = resolveDisplayName(user, fallbackUser);
|
|
50
|
+
const userId = user?.id || fallbackUser;
|
|
51
|
+
const permissionList = missingPermissions.join(', ');
|
|
52
|
+
const requestText = useMemo(
|
|
53
|
+
() =>
|
|
54
|
+
t('finance-permission-warning.request_template', {
|
|
55
|
+
displayName,
|
|
56
|
+
permissions: permissionList,
|
|
57
|
+
userId,
|
|
58
|
+
}),
|
|
59
|
+
[displayName, permissionList, t, userId]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const copyRequest = async () => {
|
|
63
|
+
try {
|
|
64
|
+
await navigator.clipboard.writeText(requestText);
|
|
65
|
+
toast.success(t('finance-permission-warning.copy_success'));
|
|
66
|
+
} catch {
|
|
67
|
+
toast.error(t('finance-permission-warning.copy_error'));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="space-y-4">
|
|
73
|
+
<div className="rounded-lg border border-dynamic-orange/30 bg-dynamic-orange/5 p-3">
|
|
74
|
+
<div className="flex items-start gap-3">
|
|
75
|
+
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-dynamic-orange" />
|
|
76
|
+
<div className="space-y-1">
|
|
77
|
+
<p className="font-medium text-sm">
|
|
78
|
+
{t('finance-permission-warning.summary')}
|
|
79
|
+
</p>
|
|
80
|
+
<p className="text-muted-foreground text-sm">
|
|
81
|
+
{t('finance-permission-warning.description')}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="grid gap-3 rounded-lg border p-3 text-sm">
|
|
88
|
+
<div className="grid gap-1">
|
|
89
|
+
<span className="font-medium text-muted-foreground">
|
|
90
|
+
{t('finance-permission-warning.permissions_to_add')}
|
|
91
|
+
</span>
|
|
92
|
+
<div className="flex flex-wrap gap-2">
|
|
93
|
+
{missingPermissions.map((permission) => (
|
|
94
|
+
<code
|
|
95
|
+
key={permission}
|
|
96
|
+
className="rounded bg-muted px-2 py-1 font-mono text-xs"
|
|
97
|
+
>
|
|
98
|
+
{permission}
|
|
99
|
+
</code>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="grid gap-1">
|
|
105
|
+
<span className="font-medium text-muted-foreground">
|
|
106
|
+
{t('finance-permission-warning.user_id')}
|
|
107
|
+
</span>
|
|
108
|
+
<code className="break-all rounded bg-muted px-2 py-1 font-mono text-xs">
|
|
109
|
+
{userId}
|
|
110
|
+
</code>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="grid gap-1">
|
|
114
|
+
<span className="font-medium text-muted-foreground">
|
|
115
|
+
{t('finance-permission-warning.display_name')}
|
|
116
|
+
</span>
|
|
117
|
+
<span>{displayName}</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<DialogFooter>
|
|
122
|
+
<Button type="button" variant="outline" onClick={copyRequest}>
|
|
123
|
+
<Copy className="mr-2 h-4 w-4" />
|
|
124
|
+
{t('finance-permission-warning.copy_request')}
|
|
125
|
+
</Button>
|
|
126
|
+
</DialogFooter>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function FinancePermissionWarningDialog({
|
|
132
|
+
defaultOpen = false,
|
|
133
|
+
missingPermissions,
|
|
134
|
+
trigger,
|
|
135
|
+
user,
|
|
136
|
+
}: FinancePermissionWarningDialogProps) {
|
|
137
|
+
const t = useTranslations();
|
|
138
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
142
|
+
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
|
143
|
+
<DialogContent>
|
|
144
|
+
<DialogHeader>
|
|
145
|
+
<DialogTitle>{t('finance-permission-warning.title')}</DialogTitle>
|
|
146
|
+
<DialogDescription>
|
|
147
|
+
{t('finance-permission-warning.dialog_description')}
|
|
148
|
+
</DialogDescription>
|
|
149
|
+
</DialogHeader>
|
|
150
|
+
<FinancePermissionWarningContent
|
|
151
|
+
missingPermissions={missingPermissions}
|
|
152
|
+
user={user}
|
|
153
|
+
/>
|
|
154
|
+
</DialogContent>
|
|
155
|
+
</Dialog>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -27,6 +27,7 @@ import { computeAccessibleLabelStyles } from '@tuturuuu/utils/label-colors';
|
|
|
27
27
|
import { format } from 'date-fns';
|
|
28
28
|
import { enUS, vi } from 'date-fns/locale';
|
|
29
29
|
import { useTranslations } from 'next-intl';
|
|
30
|
+
import type { ReactNode } from 'react';
|
|
30
31
|
import type { UseFormReturn } from 'react-hook-form';
|
|
31
32
|
|
|
32
33
|
import type { TransactionFormValues } from './form-schema';
|
|
@@ -44,7 +45,9 @@ interface FormBasicTabProps {
|
|
|
44
45
|
loading: boolean;
|
|
45
46
|
hasFormPermission: boolean;
|
|
46
47
|
originWalletDisabled?: boolean;
|
|
48
|
+
originWalletPermissionWarning?: ReactNode;
|
|
47
49
|
destinationWalletDisabled?: boolean;
|
|
50
|
+
destinationWalletPermissionWarning?: ReactNode;
|
|
48
51
|
isTransfer: boolean;
|
|
49
52
|
suggestedExchangeRate: number | null;
|
|
50
53
|
isDestinationOverridden: boolean;
|
|
@@ -73,7 +76,9 @@ export function FormBasicTab({
|
|
|
73
76
|
loading,
|
|
74
77
|
hasFormPermission,
|
|
75
78
|
originWalletDisabled = false,
|
|
79
|
+
originWalletPermissionWarning,
|
|
76
80
|
destinationWalletDisabled = false,
|
|
81
|
+
destinationWalletPermissionWarning,
|
|
77
82
|
isTransfer,
|
|
78
83
|
suggestedExchangeRate,
|
|
79
84
|
isDestinationOverridden,
|
|
@@ -138,6 +143,7 @@ export function FormBasicTab({
|
|
|
138
143
|
})}
|
|
139
144
|
</FormDescription>
|
|
140
145
|
)}
|
|
146
|
+
{originWalletDisabled && originWalletPermissionWarning}
|
|
141
147
|
<FormMessage />
|
|
142
148
|
</FormItem>
|
|
143
149
|
)}
|
|
@@ -192,6 +198,8 @@ export function FormBasicTab({
|
|
|
192
198
|
destinationWalletDisabled
|
|
193
199
|
}
|
|
194
200
|
/>
|
|
201
|
+
{destinationWalletDisabled &&
|
|
202
|
+
destinationWalletPermissionWarning}
|
|
195
203
|
<FormMessage />
|
|
196
204
|
</FormItem>
|
|
197
205
|
)}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '@tuturuuu/ui/form';
|
|
13
13
|
import { Switch } from '@tuturuuu/ui/switch';
|
|
14
14
|
import { useTranslations } from 'next-intl';
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
15
16
|
import type { UseFormReturn } from 'react-hook-form';
|
|
16
17
|
import type { TransactionFormValues } from './form-schema';
|
|
17
18
|
import type { NewContent, NewContentType } from './form-types';
|
|
@@ -23,6 +24,7 @@ interface FormMoreTabProps {
|
|
|
23
24
|
loading: boolean;
|
|
24
25
|
hasFormPermission: boolean;
|
|
25
26
|
canManageConfidential: boolean;
|
|
27
|
+
confidentialPermissionWarning?: ReactNode;
|
|
26
28
|
isTransfer: boolean;
|
|
27
29
|
setNewContentType: (value: NewContentType) => void;
|
|
28
30
|
setNewContent: (value: NewContent) => void;
|
|
@@ -35,6 +37,7 @@ export function FormMoreTab({
|
|
|
35
37
|
loading,
|
|
36
38
|
hasFormPermission,
|
|
37
39
|
canManageConfidential,
|
|
40
|
+
confidentialPermissionWarning,
|
|
38
41
|
isTransfer,
|
|
39
42
|
setNewContentType,
|
|
40
43
|
setNewContent,
|
|
@@ -198,6 +201,11 @@ export function FormMoreTab({
|
|
|
198
201
|
</div>
|
|
199
202
|
</div>
|
|
200
203
|
)}
|
|
204
|
+
|
|
205
|
+
{!canManageConfidential &&
|
|
206
|
+
!isTransfer &&
|
|
207
|
+
hasFormPermission &&
|
|
208
|
+
confidentialPermissionWarning}
|
|
201
209
|
</div>
|
|
202
210
|
);
|
|
203
211
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Database } from '@tuturuuu/types';
|
|
2
|
+
import type { FinancePermissionRequestUser } from '../shared/finance-permission-warning-dialog';
|
|
2
3
|
import type { TransactionFormValues } from './form-schema';
|
|
3
4
|
|
|
4
5
|
type DbTransaction = Database['public']['Tables']['wallet_transactions']['Row'];
|
|
@@ -32,6 +33,7 @@ export interface TransactionFormProps {
|
|
|
32
33
|
canUpdateConfidentialTransactions?: boolean;
|
|
33
34
|
canChangeFinanceWallets?: boolean;
|
|
34
35
|
canSetFinanceWalletsOnCreate?: boolean;
|
|
36
|
+
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export type NewContentType = 'wallet' | 'transaction-category' | 'tag';
|
|
@@ -63,6 +63,13 @@ vi.mock('@tuturuuu/internal-api/finance', () => ({
|
|
|
63
63
|
updateWallet: vi.fn(),
|
|
64
64
|
}));
|
|
65
65
|
|
|
66
|
+
vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
67
|
+
toast: {
|
|
68
|
+
error: vi.fn(),
|
|
69
|
+
success: vi.fn(),
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
72
|
+
|
|
66
73
|
function renderTransactionForm() {
|
|
67
74
|
const queryClient = new QueryClient({
|
|
68
75
|
defaultOptions: {
|
|
@@ -148,4 +155,40 @@ describe('TransactionForm', () => {
|
|
|
148
155
|
|
|
149
156
|
expect(screen.getByText('transaction-data-table.tab_basic')).toBeVisible();
|
|
150
157
|
});
|
|
158
|
+
|
|
159
|
+
it('shows a permission request when create transaction access is missing', () => {
|
|
160
|
+
const queryClient = new QueryClient({
|
|
161
|
+
defaultOptions: {
|
|
162
|
+
queries: {
|
|
163
|
+
retry: false,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
render(
|
|
169
|
+
<QueryClientProvider client={queryClient}>
|
|
170
|
+
<TransactionsCreateSummary
|
|
171
|
+
canChangeFinanceWallets
|
|
172
|
+
canCreateConfidentialTransactions={false}
|
|
173
|
+
canCreateTransactions={false}
|
|
174
|
+
canSetFinanceWalletsOnCreate
|
|
175
|
+
createDescription="Create description"
|
|
176
|
+
createTitle="Create"
|
|
177
|
+
defaultOpen
|
|
178
|
+
description="Transactions description"
|
|
179
|
+
permissionRequestUser={{
|
|
180
|
+
displayName: 'Jane Doe',
|
|
181
|
+
id: 'user-1',
|
|
182
|
+
}}
|
|
183
|
+
pluralTitle="Transactions"
|
|
184
|
+
singularTitle="Transaction"
|
|
185
|
+
wsId="ws-1"
|
|
186
|
+
/>
|
|
187
|
+
</QueryClientProvider>
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(screen.getByText('create_transactions')).toBeVisible();
|
|
191
|
+
expect(screen.getByText('user-1')).toBeVisible();
|
|
192
|
+
expect(screen.getByText('Jane Doe')).toBeVisible();
|
|
193
|
+
});
|
|
151
194
|
});
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
type WorkspaceStorageListItem,
|
|
22
22
|
} from '@tuturuuu/internal-api';
|
|
23
23
|
import { Button } from '@tuturuuu/ui/button';
|
|
24
|
+
import {
|
|
25
|
+
FinancePermissionWarningContent,
|
|
26
|
+
FinancePermissionWarningDialog,
|
|
27
|
+
} from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
|
|
24
28
|
import { Form } from '@tuturuuu/ui/form';
|
|
25
29
|
import { useExchangeRates } from '@tuturuuu/ui/hooks/use-exchange-rates';
|
|
26
30
|
import { useFinanceTransactionPreferences } from '@tuturuuu/ui/hooks/use-finance-transaction-preferences';
|
|
@@ -69,6 +73,7 @@ export function TransactionForm({
|
|
|
69
73
|
canUpdateConfidentialTransactions,
|
|
70
74
|
canChangeFinanceWallets = true,
|
|
71
75
|
canSetFinanceWalletsOnCreate = true,
|
|
76
|
+
permissionRequestUser,
|
|
72
77
|
}: TransactionFormProps) {
|
|
73
78
|
const t = useTranslations();
|
|
74
79
|
const locale = useLocale();
|
|
@@ -325,6 +330,43 @@ export function TransactionForm({
|
|
|
325
330
|
isEditMode &&
|
|
326
331
|
!canChangeFinanceWallets &&
|
|
327
332
|
!!data?.transfer?.linked_wallet_id;
|
|
333
|
+
const createWalletPermissionWarning = (
|
|
334
|
+
<FinancePermissionWarningDialog
|
|
335
|
+
missingPermissions={['set_finance_wallets_on_create']}
|
|
336
|
+
user={permissionRequestUser}
|
|
337
|
+
trigger={
|
|
338
|
+
<Button type="button" variant="outline" size="sm">
|
|
339
|
+
{t('finance-permission-warning.open_request')}
|
|
340
|
+
</Button>
|
|
341
|
+
}
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
const editWalletPermissionWarning = (
|
|
345
|
+
<FinancePermissionWarningDialog
|
|
346
|
+
missingPermissions={['change_finance_wallets']}
|
|
347
|
+
user={permissionRequestUser}
|
|
348
|
+
trigger={
|
|
349
|
+
<Button type="button" variant="outline" size="sm">
|
|
350
|
+
{t('finance-permission-warning.open_request')}
|
|
351
|
+
</Button>
|
|
352
|
+
}
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
355
|
+
const confidentialPermissionWarning = (
|
|
356
|
+
<FinancePermissionWarningDialog
|
|
357
|
+
missingPermissions={[
|
|
358
|
+
isCreateMode
|
|
359
|
+
? 'create_confidential_transactions'
|
|
360
|
+
: 'update_confidential_transactions',
|
|
361
|
+
]}
|
|
362
|
+
user={permissionRequestUser}
|
|
363
|
+
trigger={
|
|
364
|
+
<Button type="button" variant="outline" size="sm">
|
|
365
|
+
{t('finance-permission-warning.open_request')}
|
|
366
|
+
</Button>
|
|
367
|
+
}
|
|
368
|
+
/>
|
|
369
|
+
);
|
|
328
370
|
|
|
329
371
|
const refreshTransactions = async () => {
|
|
330
372
|
await invalidateTransactionMutationQueries(queryClient, wsId);
|
|
@@ -790,6 +832,17 @@ export function TransactionForm({
|
|
|
790
832
|
// Disable transfer mode when editing existing non-transfer transactions
|
|
791
833
|
const canToggleTransfer = !data?.id || !!data?.transfer;
|
|
792
834
|
|
|
835
|
+
if (!hasFormPermission) {
|
|
836
|
+
return (
|
|
837
|
+
<FinancePermissionWarningContent
|
|
838
|
+
missingPermissions={[
|
|
839
|
+
isCreateMode ? 'create_transactions' : 'update_transactions',
|
|
840
|
+
]}
|
|
841
|
+
user={permissionRequestUser}
|
|
842
|
+
/>
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
793
846
|
return (
|
|
794
847
|
<>
|
|
795
848
|
<FormContentDialog
|
|
@@ -858,7 +911,13 @@ export function TransactionForm({
|
|
|
858
911
|
originWalletDisabled={
|
|
859
912
|
shouldLockCreateOriginWallet || shouldLockEditOriginWallet
|
|
860
913
|
}
|
|
914
|
+
originWalletPermissionWarning={
|
|
915
|
+
shouldLockCreateOriginWallet
|
|
916
|
+
? createWalletPermissionWarning
|
|
917
|
+
: editWalletPermissionWarning
|
|
918
|
+
}
|
|
861
919
|
destinationWalletDisabled={shouldLockEditDestinationWallet}
|
|
920
|
+
destinationWalletPermissionWarning={editWalletPermissionWarning}
|
|
862
921
|
isTransfer={isTransfer}
|
|
863
922
|
suggestedExchangeRate={suggestedExchangeRate}
|
|
864
923
|
isDestinationOverridden={isDestinationOverridden}
|
|
@@ -913,6 +972,7 @@ export function TransactionForm({
|
|
|
913
972
|
loading={loading}
|
|
914
973
|
hasFormPermission={!!hasFormPermission}
|
|
915
974
|
canManageConfidential={!!canManageConfidential}
|
|
975
|
+
confidentialPermissionWarning={confidentialPermissionWarning}
|
|
916
976
|
isTransfer={isTransfer}
|
|
917
977
|
setNewContentType={setNewContentType}
|
|
918
978
|
setNewContent={setNewContent}
|
|
@@ -51,6 +51,10 @@ dayjs.extend(timezone);
|
|
|
51
51
|
dayjs.extend(isoWeek);
|
|
52
52
|
|
|
53
53
|
import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger';
|
|
54
|
+
import {
|
|
55
|
+
type FinancePermissionRequestUser,
|
|
56
|
+
FinancePermissionWarningDialog,
|
|
57
|
+
} from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
|
|
54
58
|
import { useLocale, useTranslations } from 'next-intl';
|
|
55
59
|
import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs';
|
|
56
60
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -92,6 +96,7 @@ interface InfiniteTransactionsListProps {
|
|
|
92
96
|
canViewConfidentialCategory?: boolean;
|
|
93
97
|
/** Hide transaction creator (useful for personal workspaces) */
|
|
94
98
|
isPersonalWorkspace?: boolean;
|
|
99
|
+
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
interface TransactionResponse {
|
|
@@ -133,6 +138,7 @@ export function InfiniteTransactionsList({
|
|
|
133
138
|
canViewConfidentialDescription: _canViewConfidentialDescription,
|
|
134
139
|
canViewConfidentialCategory: _canViewConfidentialCategory,
|
|
135
140
|
isPersonalWorkspace,
|
|
141
|
+
permissionRequestUser,
|
|
136
142
|
}: InfiniteTransactionsListProps) {
|
|
137
143
|
const t = useTranslations();
|
|
138
144
|
const locale = useLocale();
|
|
@@ -886,6 +892,25 @@ export function InfiniteTransactionsList({
|
|
|
886
892
|
</span>
|
|
887
893
|
</Button>
|
|
888
894
|
)}
|
|
895
|
+
{canCreateTransactions === false && (
|
|
896
|
+
<FinancePermissionWarningDialog
|
|
897
|
+
missingPermissions={['create_transactions']}
|
|
898
|
+
user={permissionRequestUser}
|
|
899
|
+
trigger={
|
|
900
|
+
<Button
|
|
901
|
+
type="button"
|
|
902
|
+
variant="outline"
|
|
903
|
+
size="icon"
|
|
904
|
+
className="h-8 w-8 shrink-0"
|
|
905
|
+
>
|
|
906
|
+
<Plus className="h-4 w-4" />
|
|
907
|
+
<span className="sr-only">
|
|
908
|
+
{t('finance-permission-warning.open_request')}
|
|
909
|
+
</span>
|
|
910
|
+
</Button>
|
|
911
|
+
}
|
|
912
|
+
/>
|
|
913
|
+
)}
|
|
889
914
|
</div>
|
|
890
915
|
</div>
|
|
891
916
|
</div>
|
|
@@ -1029,6 +1054,7 @@ export function InfiniteTransactionsList({
|
|
|
1029
1054
|
canUpdateConfidentialTransactions={
|
|
1030
1055
|
canUpdateConfidentialTransactions
|
|
1031
1056
|
}
|
|
1057
|
+
permissionRequestUser={permissionRequestUser}
|
|
1032
1058
|
onFinish={() => {
|
|
1033
1059
|
handleTransactionUpdate();
|
|
1034
1060
|
handleCloseDialog();
|
|
@@ -1061,6 +1087,7 @@ export function InfiniteTransactionsList({
|
|
|
1061
1087
|
canCreateConfidentialTransactions={
|
|
1062
1088
|
canCreateConfidentialTransactions
|
|
1063
1089
|
}
|
|
1090
|
+
permissionRequestUser={permissionRequestUser}
|
|
1064
1091
|
onFinish={() => {
|
|
1065
1092
|
handleTransactionUpdate();
|
|
1066
1093
|
setCreateForGroupDate(null);
|