@tuturuuu/ui 0.30.1 → 0.31.0
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 +49 -0
- package/biome.json +1 -1
- package/package.json +103 -57
- package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
- package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
- package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
- package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
- package/src/components/ui/custom/nav-link.tsx +7 -7
- package/src/components/ui/custom/nav.tsx +15 -63
- package/src/components/ui/custom/notification-card.tsx +361 -0
- package/src/components/ui/custom/notification-list.tsx +178 -0
- package/src/components/ui/custom/notification-popover-client.tsx +12 -780
- package/src/components/ui/custom/notification-runtime.tsx +23 -0
- package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
- package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
- package/src/components/ui/custom/satellite-brand.tsx +122 -0
- package/src/components/ui/custom/satellite-content.tsx +148 -0
- package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
- package/src/components/ui/custom/satellite-language-item.tsx +26 -0
- package/src/components/ui/custom/satellite-navigation.tsx +86 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
- package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
- package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
- package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
- package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
- package/src/components/ui/custom/satellite-shell.tsx +50 -0
- package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
- package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
- package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
- package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
- package/src/components/ui/custom/sidebar-context.tsx +5 -14
- package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
- package/src/components/ui/custom/structure.tsx +14 -0
- package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
- package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
- package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +49 -13
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -12
- package/src/components/ui/finance/invoices/utils.test.ts +6 -5
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/columns.test.tsx +25 -1
- package/src/components/ui/finance/transactions/columns.tsx +45 -16
- package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
- package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
- package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
- package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
- package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
- package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
- package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
- package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
- package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
- package/src/components/ui/pwa-status.test.tsx +53 -0
- package/src/components/ui/pwa-status.tsx +113 -0
- package/src/components/ui/report-problem-dialog-content.tsx +350 -0
- package/src/components/ui/report-problem-dialog.tsx +6 -975
- package/src/components/ui/report-problem-model.ts +133 -0
- package/src/components/ui/report-problem-mutation.ts +122 -0
- package/src/components/ui/save-notification.test.tsx +36 -0
- package/src/components/ui/save-notification.tsx +17 -0
- package/src/components/ui/text-editor/task-item-checkbox-view.test.tsx +85 -0
- package/src/components/ui/text-editor/task-item-checkbox-view.tsx +28 -18
- package/src/components/ui/use-report-problem.ts +488 -0
- package/src/declarations.d.ts +2 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
- package/src/hooks/calendar-date-utils.ts +11 -0
- package/src/hooks/calendar-event-lookup.test.ts +77 -0
- package/src/hooks/calendar-event-lookup.ts +47 -0
- package/src/hooks/calendar-range-cache.ts +52 -0
- package/src/hooks/pwa-install-prompt.ts +44 -0
- package/src/hooks/use-calendar-sync.tsx +23 -100
- package/src/hooks/use-calendar.tsx +21 -77
- package/src/hooks/use-notifications.ts +9 -24
- package/src/hooks/use-open-initial-calendar-event.ts +27 -0
- package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
- package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
- package/src/hooks/use-workspace-presence.ts +0 -18
- package/src/lib/tasks-app-url.test.ts +25 -0
- package/src/lib/tasks-app-url.ts +8 -0
|
@@ -30,15 +30,28 @@ const testState = vi.hoisted(() => {
|
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
32
|
InvoicePaymentSettings: vi.fn(),
|
|
33
|
+
SubscriptionAttendanceSummary: vi.fn(),
|
|
33
34
|
categories: [] as Array<{ id: string; name: string }>,
|
|
34
35
|
groupIds: ['group-1'],
|
|
35
36
|
month: '2026-07',
|
|
37
|
+
setMonth: vi.fn(),
|
|
36
38
|
productSelectionInjected: false,
|
|
37
39
|
products: [product],
|
|
38
40
|
selectedProducts: [{ inventory, product, quantity: 1 }],
|
|
39
41
|
useCategories: vi.fn(),
|
|
40
42
|
useWallets: vi.fn(),
|
|
41
43
|
userId: 'user-1',
|
|
44
|
+
userGroups: [
|
|
45
|
+
{
|
|
46
|
+
workspace_user_groups: {
|
|
47
|
+
ending_date: null as string | null,
|
|
48
|
+
id: 'group-1',
|
|
49
|
+
name: 'Group 1',
|
|
50
|
+
sessions: ['2026-07-05'],
|
|
51
|
+
starting_date: '2026-07-01',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
42
55
|
wallets: [] as Array<{
|
|
43
56
|
currency: string;
|
|
44
57
|
id: string;
|
|
@@ -67,7 +80,7 @@ vi.mock('nuqs', () => ({
|
|
|
67
80
|
useQueryState: (key: string, options?: { defaultValue?: unknown }) => {
|
|
68
81
|
if (key === 'user_id') return [testState.userId, vi.fn()];
|
|
69
82
|
if (key === 'group_ids') return [testState.groupIds, vi.fn()];
|
|
70
|
-
if (key === 'month') return [testState.month,
|
|
83
|
+
if (key === 'month') return [testState.month, testState.setMonth];
|
|
71
84
|
return [options?.defaultValue ?? '', vi.fn()];
|
|
72
85
|
},
|
|
73
86
|
}));
|
|
@@ -113,7 +126,10 @@ vi.mock('./components/invoice-products-permission-warning', () => ({
|
|
|
113
126
|
}));
|
|
114
127
|
|
|
115
128
|
vi.mock('./components/subscription-attendance-summary', () => ({
|
|
116
|
-
SubscriptionAttendanceSummary: () =>
|
|
129
|
+
SubscriptionAttendanceSummary: (props: unknown) => {
|
|
130
|
+
testState.SubscriptionAttendanceSummary(props);
|
|
131
|
+
return null;
|
|
132
|
+
},
|
|
117
133
|
}));
|
|
118
134
|
|
|
119
135
|
vi.mock('./components/subscription-group-selector', () => ({
|
|
@@ -178,17 +194,7 @@ vi.mock('./hooks', () => ({
|
|
|
178
194
|
isLoading: false,
|
|
179
195
|
}),
|
|
180
196
|
useUserGroups: () => ({
|
|
181
|
-
data:
|
|
182
|
-
{
|
|
183
|
-
workspace_user_groups: {
|
|
184
|
-
ending_date: null,
|
|
185
|
-
id: 'group-1',
|
|
186
|
-
name: 'Group 1',
|
|
187
|
-
sessions: ['2026-07-05'],
|
|
188
|
-
starting_date: '2026-07-01',
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
],
|
|
197
|
+
data: testState.userGroups,
|
|
192
198
|
isLoading: false,
|
|
193
199
|
}),
|
|
194
200
|
useUserLinkedPromotions: () => ({ data: [] }),
|
|
@@ -262,9 +268,14 @@ function renderSubscriptionInvoice(
|
|
|
262
268
|
describe('SubscriptionInvoice checkout defaults', () => {
|
|
263
269
|
beforeEach(() => {
|
|
264
270
|
testState.InvoicePaymentSettings.mockClear();
|
|
271
|
+
testState.SubscriptionAttendanceSummary.mockClear();
|
|
265
272
|
testState.useCategories.mockClear();
|
|
266
273
|
testState.useWallets.mockClear();
|
|
267
274
|
testState.productSelectionInjected = false;
|
|
275
|
+
testState.month = '2026-07';
|
|
276
|
+
testState.setMonth.mockClear();
|
|
277
|
+
testState.userGroups[0]!.workspace_user_groups.ending_date = null;
|
|
278
|
+
testState.userGroups[0]!.workspace_user_groups.sessions = ['2026-07-05'];
|
|
268
279
|
testState.products[0]!.finance_category_id = null;
|
|
269
280
|
testState.wallets = [];
|
|
270
281
|
testState.categories = [];
|
|
@@ -328,4 +339,29 @@ describe('SubscriptionInvoice checkout defaults', () => {
|
|
|
328
339
|
)
|
|
329
340
|
);
|
|
330
341
|
});
|
|
342
|
+
|
|
343
|
+
it('keeps an advance month selected after the group schedule end', async () => {
|
|
344
|
+
testState.month = '2026-09';
|
|
345
|
+
testState.userGroups[0]!.workspace_user_groups.ending_date = '2026-08-31';
|
|
346
|
+
testState.userGroups[0]!.workspace_user_groups.sessions = [
|
|
347
|
+
'2026-07-05',
|
|
348
|
+
'2026-08-05',
|
|
349
|
+
'2026-09-05',
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
renderSubscriptionInvoice();
|
|
353
|
+
|
|
354
|
+
await waitFor(() =>
|
|
355
|
+
expect(testState.SubscriptionAttendanceSummary).toHaveBeenCalledWith(
|
|
356
|
+
expect.objectContaining({
|
|
357
|
+
availableMonths: expect.arrayContaining([
|
|
358
|
+
expect.objectContaining({ value: '2026-09' }),
|
|
359
|
+
]),
|
|
360
|
+
selectedMonth: '2026-09',
|
|
361
|
+
})
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
expect(testState.setMonth).not.toHaveBeenCalledWith('2026-07');
|
|
365
|
+
expect(testState.setMonth).not.toHaveBeenCalledWith('2026-08');
|
|
366
|
+
});
|
|
331
367
|
});
|
|
@@ -728,30 +728,22 @@ export function SubscriptionInvoice({
|
|
|
728
728
|
}
|
|
729
729
|
lastMonthSyncRef.current = null;
|
|
730
730
|
|
|
731
|
-
const { earliestStart
|
|
732
|
-
userGroups,
|
|
733
|
-
selectedGroupIds
|
|
734
|
-
);
|
|
731
|
+
const { earliestStart } = getGroupsDateRange(userGroups, selectedGroupIds);
|
|
735
732
|
|
|
736
|
-
if (!earliestStart
|
|
733
|
+
if (!earliestStart) return;
|
|
737
734
|
|
|
738
735
|
const currentMonth = getMonthStartDate(selectedMonth);
|
|
739
736
|
if (Number.isNaN(currentMonth.getTime())) return;
|
|
740
737
|
|
|
741
738
|
const earliestMonthStart = getMonthStartDate(earliestStart);
|
|
742
|
-
const latestMonthStart = getMonthStartDate(latestEnd);
|
|
743
739
|
|
|
744
|
-
if (currentMonth < earliestMonthStart
|
|
740
|
+
if (currentMonth < earliestMonthStart) {
|
|
745
741
|
// Only correct once per groups-selection to avoid update loops
|
|
746
742
|
if (lastValidatedKeyRef.current === monthValidationKey) return;
|
|
747
743
|
lastValidatedKeyRef.current = monthValidationKey;
|
|
748
744
|
|
|
749
745
|
const now = getCurrentBillingDate(workspaceTimezone);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (now >= earliestStart && now <= latestEnd) defaultMonth = now;
|
|
753
|
-
else if (now > latestEnd) defaultMonth = latestEnd;
|
|
754
|
-
else defaultMonth = earliestStart;
|
|
746
|
+
const defaultMonth = now >= earliestStart ? now : earliestStart;
|
|
755
747
|
|
|
756
748
|
const nextMonth = formatMonthValue(defaultMonth);
|
|
757
749
|
if (nextMonth !== selectedMonth) {
|
|
@@ -233,7 +233,7 @@ describe('subscription invoice coverage', () => {
|
|
|
233
233
|
).toBe(true);
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
it('
|
|
236
|
+
it('keeps future billing months available after a group ending date', () => {
|
|
237
237
|
const groups = [
|
|
238
238
|
{
|
|
239
239
|
workspace_user_groups: {
|
|
@@ -247,10 +247,11 @@ describe('subscription invoice coverage', () => {
|
|
|
247
247
|
] satisfies UserGroup[];
|
|
248
248
|
|
|
249
249
|
expect(
|
|
250
|
-
getAvailableMonths(groups, [groupId], [], 'en-US', '2026-06',
|
|
251
|
-
(
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
getAvailableMonths(groups, [groupId], [], 'en-US', '2026-06', 3, {
|
|
251
|
+
now: new Date(2026, 5, 15),
|
|
252
|
+
workspaceTimezone: 'UTC',
|
|
253
|
+
}).map((month) => month.value)
|
|
254
|
+
).toEqual(['2026-06', '2026-07', '2026-08', '2026-09']);
|
|
254
255
|
});
|
|
255
256
|
|
|
256
257
|
it('skips paid months and uses scheduled sessions for future prepaid quantities', () => {
|
|
@@ -524,7 +524,9 @@ export const getAvailableMonths = (
|
|
|
524
524
|
? getMonthStartDate(selectedMonthFallback)
|
|
525
525
|
: currentMonthStart;
|
|
526
526
|
const resolvedRangeEnd =
|
|
527
|
-
futureMonthHorizon > 0 &&
|
|
527
|
+
futureMonthHorizon > 0 && futureHorizonEnd > resolvedLatestEnd
|
|
528
|
+
? futureHorizonEnd
|
|
529
|
+
: resolvedLatestEnd;
|
|
528
530
|
|
|
529
531
|
const months: AvailableMonthOption[] = [];
|
|
530
532
|
const currentDate = new Date(earliestStart);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { ColumnDef } from '@tuturuuu/ui/custom/tables/data-table';
|
|
3
|
-
import {
|
|
3
|
+
import { renderToString } from 'react-dom/server';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { transactionColumns } from './columns';
|
|
5
6
|
|
|
6
7
|
vi.mock('next-intl', () => ({
|
|
@@ -82,6 +83,10 @@ describe('transactionColumns', () => {
|
|
|
82
83
|
'finance-confidential-mode=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;Secure';
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.useRealTimers();
|
|
88
|
+
});
|
|
89
|
+
|
|
85
90
|
it('masks the amount column when finance numbers are globally hidden', () => {
|
|
86
91
|
render(<TransactionAmountCell amount={123} />);
|
|
87
92
|
|
|
@@ -116,4 +121,23 @@ describe('transactionColumns', () => {
|
|
|
116
121
|
expect(screen.getByText('Cash')).toBeVisible();
|
|
117
122
|
expect(screen.getByText('Food & Beverage')).toBeVisible();
|
|
118
123
|
});
|
|
124
|
+
|
|
125
|
+
it('keeps server-rendered transaction dates stable as time advances', () => {
|
|
126
|
+
vi.useFakeTimers();
|
|
127
|
+
vi.setSystemTime('2026-08-31T00:59:00.000Z');
|
|
128
|
+
|
|
129
|
+
const transaction = {
|
|
130
|
+
taken_at: '2026-08-31T00:00:00.000Z',
|
|
131
|
+
};
|
|
132
|
+
const firstRender = renderToString(
|
|
133
|
+
<TransactionColumnCell accessorKey="taken_at" original={transaction} />
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
vi.setSystemTime('2026-08-31T02:01:00.000Z');
|
|
137
|
+
const laterRender = renderToString(
|
|
138
|
+
<TransactionColumnCell accessorKey="taken_at" original={transaction} />
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(laterRender).toBe(firstRender);
|
|
142
|
+
});
|
|
119
143
|
});
|
|
@@ -12,6 +12,7 @@ import { formatCurrency } from '@tuturuuu/utils/format';
|
|
|
12
12
|
import moment from 'moment';
|
|
13
13
|
import 'moment/locale/vi';
|
|
14
14
|
import { useLocale } from 'next-intl';
|
|
15
|
+
import { useSyncExternalStore } from 'react';
|
|
15
16
|
import { DataTableColumnHeader } from '../../custom/tables/data-table-column-header';
|
|
16
17
|
import {
|
|
17
18
|
FINANCE_HIDDEN_AMOUNT,
|
|
@@ -27,6 +28,44 @@ interface TransactionExtraData {
|
|
|
27
28
|
isPersonalWorkspace?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
const subscribeToHydration = () => () => undefined;
|
|
32
|
+
const getHydratedSnapshot = () => true;
|
|
33
|
+
const getServerSnapshot = () => false;
|
|
34
|
+
|
|
35
|
+
function TransactionDateCell({
|
|
36
|
+
includeTime = false,
|
|
37
|
+
locale,
|
|
38
|
+
value,
|
|
39
|
+
}: {
|
|
40
|
+
includeTime?: boolean;
|
|
41
|
+
locale: string;
|
|
42
|
+
value: unknown;
|
|
43
|
+
}) {
|
|
44
|
+
const isHydrated = useSyncExternalStore(
|
|
45
|
+
subscribeToHydration,
|
|
46
|
+
getHydratedSnapshot,
|
|
47
|
+
getServerSnapshot
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!value) return <div className="min-w-32">-</div>;
|
|
51
|
+
|
|
52
|
+
const localDate = moment(value);
|
|
53
|
+
if (!localDate.isValid()) return <div className="min-w-32">-</div>;
|
|
54
|
+
|
|
55
|
+
const format = includeTime ? 'DD/MM/YYYY, HH:mm:ss' : 'DD/MM/YYYY';
|
|
56
|
+
const absoluteDate = isHydrated
|
|
57
|
+
? localDate.locale(locale).format(format)
|
|
58
|
+
: moment.utc(value).locale(locale).format(format);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="min-w-32">
|
|
62
|
+
{isHydrated && !includeTime
|
|
63
|
+
? `${localDate.locale(locale).fromNow()}, ${absoluteDate}`
|
|
64
|
+
: absoluteDate}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
30
69
|
function TransactionAmountCell({
|
|
31
70
|
amount,
|
|
32
71
|
currency,
|
|
@@ -265,15 +304,7 @@ export const transactionColumns = ({
|
|
|
265
304
|
/>
|
|
266
305
|
),
|
|
267
306
|
cell: ({ row }) => (
|
|
268
|
-
<
|
|
269
|
-
{row.getValue('taken_at')
|
|
270
|
-
? `${moment(row.getValue('taken_at')).locale(locale).fromNow()}, ${moment(
|
|
271
|
-
row.getValue('taken_at')
|
|
272
|
-
)
|
|
273
|
-
.locale(locale)
|
|
274
|
-
.format('DD/MM/YYYY')}`
|
|
275
|
-
: '-'}
|
|
276
|
-
</div>
|
|
307
|
+
<TransactionDateCell locale={locale} value={row.getValue('taken_at')} />
|
|
277
308
|
),
|
|
278
309
|
},
|
|
279
310
|
{
|
|
@@ -286,13 +317,11 @@ export const transactionColumns = ({
|
|
|
286
317
|
/>
|
|
287
318
|
),
|
|
288
319
|
cell: ({ row }) => (
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
: '-'}
|
|
295
|
-
</div>
|
|
320
|
+
<TransactionDateCell
|
|
321
|
+
includeTime
|
|
322
|
+
locale={locale}
|
|
323
|
+
value={row.getValue('created_at')}
|
|
324
|
+
/>
|
|
296
325
|
),
|
|
297
326
|
},
|
|
298
327
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { CalendarX2, Clock, MapPin, Sun } from '@tuturuuu/icons';
|
|
3
|
+
import { CalendarX2, Clock, MapPin, Plus, Search, Sun } from '@tuturuuu/icons';
|
|
4
4
|
import type { Workspace } from '@tuturuuu/types';
|
|
5
5
|
import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
|
|
6
6
|
import { useCalendar } from '@tuturuuu/ui/hooks/use-calendar';
|
|
@@ -17,16 +17,27 @@ import {
|
|
|
17
17
|
isYesterday,
|
|
18
18
|
startOfDay,
|
|
19
19
|
} from 'date-fns';
|
|
20
|
+
import dayjs from 'dayjs';
|
|
21
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
22
|
+
import utc from 'dayjs/plugin/utc';
|
|
20
23
|
import { useTranslations } from 'next-intl';
|
|
21
|
-
import { useMemo } from 'react';
|
|
24
|
+
import { useMemo, useState } from 'react';
|
|
22
25
|
import {
|
|
23
26
|
formatLunarDay,
|
|
24
27
|
getLunarDate,
|
|
25
28
|
getLunarHolidayName,
|
|
26
29
|
isSpecialLunarDate,
|
|
27
30
|
} from '../../../../lib/lunar-calendar';
|
|
31
|
+
import { Button } from '../../button';
|
|
32
|
+
import { Input } from '../../input';
|
|
33
|
+
import { calendarDraftDate } from './calendar-period';
|
|
34
|
+
import { useCalendarSettings } from './settings/settings-context';
|
|
35
|
+
|
|
36
|
+
dayjs.extend(utc);
|
|
37
|
+
dayjs.extend(timezone);
|
|
28
38
|
|
|
29
39
|
interface AgendaViewProps {
|
|
40
|
+
readOnly?: boolean;
|
|
30
41
|
startDate: Date;
|
|
31
42
|
workspace?: Workspace;
|
|
32
43
|
locale?: string;
|
|
@@ -173,7 +184,7 @@ function EventCard({
|
|
|
173
184
|
{/* Content */}
|
|
174
185
|
<div className="min-w-0 flex-1">
|
|
175
186
|
<span className="line-clamp-1 font-medium text-foreground text-sm leading-tight">
|
|
176
|
-
{event.title || '
|
|
187
|
+
{event.title || t('views.untitled_event')}
|
|
177
188
|
</span>
|
|
178
189
|
|
|
179
190
|
{(event.location || (!isAllDay && event.description)) && (
|
|
@@ -258,7 +269,7 @@ function DateHeader({
|
|
|
258
269
|
today ? 'text-primary-foreground/70' : 'text-muted-foreground'
|
|
259
270
|
)}
|
|
260
271
|
>
|
|
261
|
-
{
|
|
272
|
+
{date.toLocaleDateString(locale, { weekday: 'short' })}
|
|
262
273
|
</span>
|
|
263
274
|
</div>
|
|
264
275
|
|
|
@@ -271,7 +282,11 @@ function DateHeader({
|
|
|
271
282
|
today ? 'text-primary' : 'text-foreground'
|
|
272
283
|
)}
|
|
273
284
|
>
|
|
274
|
-
{
|
|
285
|
+
{date.toLocaleDateString(locale, {
|
|
286
|
+
weekday: 'long',
|
|
287
|
+
month: 'long',
|
|
288
|
+
day: 'numeric',
|
|
289
|
+
})}
|
|
275
290
|
</span>
|
|
276
291
|
{relativeLabel && (
|
|
277
292
|
<span
|
|
@@ -313,11 +328,24 @@ function DateHeader({
|
|
|
313
328
|
|
|
314
329
|
export const AgendaView = ({
|
|
315
330
|
startDate,
|
|
331
|
+
readOnly = false,
|
|
316
332
|
locale = 'en',
|
|
317
333
|
daysToShow = 30,
|
|
318
334
|
}: AgendaViewProps) => {
|
|
319
335
|
const t = useTranslations('calendar');
|
|
320
|
-
const {
|
|
336
|
+
const {
|
|
337
|
+
getCurrentEvents,
|
|
338
|
+
openModal,
|
|
339
|
+
addEmptyEvent,
|
|
340
|
+
readOnly: providerReadOnly,
|
|
341
|
+
} = useCalendar();
|
|
342
|
+
const { settings } = useCalendarSettings();
|
|
343
|
+
const cannotCreate = readOnly || providerReadOnly;
|
|
344
|
+
const createEvent = () => {
|
|
345
|
+
if (cannotCreate) return;
|
|
346
|
+
addEmptyEvent(calendarDraftDate(startDate, settings?.timezone?.timezone));
|
|
347
|
+
};
|
|
348
|
+
const [query, setQuery] = useState('');
|
|
321
349
|
const { timeFormat: rawTimeFormat } = useCalendarPreferences();
|
|
322
350
|
const { value: showLunar } = useUserBooleanConfig(
|
|
323
351
|
'SHOW_LUNAR_CALENDAR',
|
|
@@ -331,7 +359,11 @@ export const AgendaView = ({
|
|
|
331
359
|
|
|
332
360
|
for (let i = 0; i < daysToShow; i++) {
|
|
333
361
|
const currentDate = addDays(startOfDay(startDate), i);
|
|
334
|
-
const events = getCurrentEvents(currentDate)
|
|
362
|
+
const events = getCurrentEvents(currentDate).filter((event) =>
|
|
363
|
+
`${event.title ?? ''} ${event.location ?? ''}`
|
|
364
|
+
.toLocaleLowerCase(locale)
|
|
365
|
+
.includes(query.toLocaleLowerCase(locale))
|
|
366
|
+
);
|
|
335
367
|
|
|
336
368
|
if (events.length > 0) {
|
|
337
369
|
const sortedEvents = [...events].sort((a, b) => {
|
|
@@ -348,50 +380,110 @@ export const AgendaView = ({
|
|
|
348
380
|
}
|
|
349
381
|
|
|
350
382
|
return groups;
|
|
351
|
-
}, [startDate, daysToShow, getCurrentEvents]);
|
|
352
|
-
|
|
353
|
-
if (groupedEvents.length === 0) {
|
|
354
|
-
return (
|
|
355
|
-
<div className="flex h-full flex-col items-center justify-center p-8 text-center">
|
|
356
|
-
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-muted">
|
|
357
|
-
<CalendarX2 className="h-8 w-8 text-muted-foreground" />
|
|
358
|
-
</div>
|
|
359
|
-
<h3 className="mb-1 font-semibold text-lg">
|
|
360
|
-
{t('agenda_empty_title')}
|
|
361
|
-
</h3>
|
|
362
|
-
<p className="max-w-xs text-muted-foreground text-sm">
|
|
363
|
-
{t('agenda_empty_subtitle', { days: daysToShow })}
|
|
364
|
-
</p>
|
|
365
|
-
</div>
|
|
366
|
-
);
|
|
367
|
-
}
|
|
383
|
+
}, [startDate, daysToShow, getCurrentEvents, query, locale]);
|
|
368
384
|
|
|
385
|
+
const uniqueEvents = new Set(
|
|
386
|
+
groupedEvents.flatMap((group) => group.events.map((event) => event.id))
|
|
387
|
+
).size;
|
|
369
388
|
return (
|
|
370
|
-
<div
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
389
|
+
<div
|
|
390
|
+
className="h-full overflow-auto bg-background"
|
|
391
|
+
data-calendar-view="agenda"
|
|
392
|
+
>
|
|
393
|
+
<div className="mx-auto max-w-4xl px-4 py-5 sm:px-8">
|
|
394
|
+
<div className="mb-6 flex flex-wrap items-end justify-between gap-4 border-b pb-5">
|
|
395
|
+
<div className="space-y-1">
|
|
396
|
+
<h2 className="font-semibold text-xl tracking-tight">
|
|
397
|
+
{t('agenda')}
|
|
398
|
+
</h2>
|
|
399
|
+
<p className="text-muted-foreground text-sm tabular-nums">
|
|
400
|
+
{startDate.toLocaleDateString(locale, {
|
|
401
|
+
month: 'short',
|
|
402
|
+
day: 'numeric',
|
|
403
|
+
})}{' '}
|
|
404
|
+
—{' '}
|
|
405
|
+
{addDays(startDate, daysToShow - 1).toLocaleDateString(locale, {
|
|
406
|
+
month: 'short',
|
|
407
|
+
day: 'numeric',
|
|
408
|
+
year: 'numeric',
|
|
409
|
+
})}
|
|
410
|
+
</p>
|
|
411
|
+
<p className="text-sm">
|
|
412
|
+
{t('agenda_event_count', { count: uniqueEvents })}
|
|
413
|
+
</p>
|
|
414
|
+
</div>
|
|
415
|
+
<div className="flex w-full items-center gap-2 sm:w-auto">
|
|
416
|
+
<div className="relative flex-1">
|
|
417
|
+
<Search className="absolute top-2.5 left-3 size-4 text-muted-foreground" />
|
|
418
|
+
<Input
|
|
419
|
+
className="pl-9"
|
|
420
|
+
value={query}
|
|
421
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
422
|
+
placeholder={t('views.search_events')}
|
|
423
|
+
aria-label={t('views.search_events')}
|
|
424
|
+
/>
|
|
392
425
|
</div>
|
|
426
|
+
<Button
|
|
427
|
+
variant="outline"
|
|
428
|
+
size="icon"
|
|
429
|
+
aria-label={t('views.create_event')}
|
|
430
|
+
disabled={cannotCreate}
|
|
431
|
+
onClick={createEvent}
|
|
432
|
+
>
|
|
433
|
+
<Plus className="size-4" />
|
|
434
|
+
</Button>
|
|
393
435
|
</div>
|
|
394
|
-
|
|
436
|
+
</div>
|
|
437
|
+
{groupedEvents.length ? (
|
|
438
|
+
groupedEvents.map(({ date, events }) => (
|
|
439
|
+
<section key={date.toISOString()} className="relative mb-5">
|
|
440
|
+
<DateHeader
|
|
441
|
+
date={date}
|
|
442
|
+
eventCount={events.length}
|
|
443
|
+
locale={locale}
|
|
444
|
+
showLunar={showLunar}
|
|
445
|
+
t={t}
|
|
446
|
+
/>
|
|
447
|
+
<div className="ml-5 space-y-2 border-l pb-2 pl-4 sm:ml-5 sm:pl-8">
|
|
448
|
+
{events.map((event) => (
|
|
449
|
+
<EventCard
|
|
450
|
+
key={event.id}
|
|
451
|
+
event={event}
|
|
452
|
+
timePattern={timePattern}
|
|
453
|
+
timeFormat={timeFormat}
|
|
454
|
+
t={t}
|
|
455
|
+
onOpen={openModal}
|
|
456
|
+
/>
|
|
457
|
+
))}
|
|
458
|
+
</div>
|
|
459
|
+
</section>
|
|
460
|
+
))
|
|
461
|
+
) : (
|
|
462
|
+
<div className="flex min-h-64 flex-col items-center justify-center gap-3 text-center">
|
|
463
|
+
<CalendarX2 className="size-8 text-muted-foreground" />
|
|
464
|
+
<h3 className="font-semibold">
|
|
465
|
+
{t(query ? 'views.no_matches' : 'agenda_empty_title')}
|
|
466
|
+
</h3>
|
|
467
|
+
{!query && (
|
|
468
|
+
<p className="max-w-sm text-muted-foreground text-sm">
|
|
469
|
+
{t('agenda_empty_subtitle', { days: daysToShow })}
|
|
470
|
+
</p>
|
|
471
|
+
)}
|
|
472
|
+
{query ? (
|
|
473
|
+
<Button variant="outline" onClick={() => setQuery('')}>
|
|
474
|
+
{t('views.clear_search')}
|
|
475
|
+
</Button>
|
|
476
|
+
) : (
|
|
477
|
+
<Button
|
|
478
|
+
variant="outline"
|
|
479
|
+
disabled={cannotCreate}
|
|
480
|
+
onClick={createEvent}
|
|
481
|
+
>
|
|
482
|
+
{t('views.create_event')}
|
|
483
|
+
</Button>
|
|
484
|
+
)}
|
|
485
|
+
</div>
|
|
486
|
+
)}
|
|
395
487
|
</div>
|
|
396
488
|
</div>
|
|
397
489
|
);
|