@tuturuuu/ui 0.0.4 → 0.2.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.
Files changed (1210) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +2 -2
  3. package/biome.json +5 -0
  4. package/components.json +1 -1
  5. package/jsr.json +9 -9
  6. package/package.json +422 -92
  7. package/postcss.config.mjs +1 -1
  8. package/src/components/ui/__tests__/avatar.test.tsx +53 -0
  9. package/src/components/ui/accordion.tsx +4 -4
  10. package/src/components/ui/alert-dialog.tsx +8 -6
  11. package/src/components/ui/alert.tsx +2 -2
  12. package/src/components/ui/avatar.tsx +6 -3
  13. package/src/components/ui/badge.tsx +6 -4
  14. package/src/components/ui/breadcrumb.tsx +3 -4
  15. package/src/components/ui/button.tsx +3 -3
  16. package/src/components/ui/calendar-app/calendar-client-page.tsx +69 -0
  17. package/src/components/ui/calendar-app/calendar-page-shell.tsx +59 -0
  18. package/src/components/ui/calendar-app/components/actions-dropdown.tsx +269 -0
  19. package/src/components/ui/calendar-app/components/auto-schedule-comprehensive-dialog.tsx +220 -0
  20. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +2 -0
  21. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +1433 -0
  22. package/src/components/ui/calendar-app/components/calendar-connections.tsx +843 -0
  23. package/src/components/ui/calendar-app/components/calendar-header-actions.tsx +67 -0
  24. package/src/components/ui/calendar-app/components/calendar-types.ts +46 -0
  25. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +353 -0
  26. package/src/components/ui/calendar-app/components/e2ee-status-badge.tsx +237 -0
  27. package/src/components/ui/calendar-app/components/experimental-feature-dialog.tsx +56 -0
  28. package/src/components/ui/calendar-app/components/habits-panel.tsx +332 -0
  29. package/src/components/ui/calendar-app/components/priority-dropdown.tsx +113 -0
  30. package/src/components/ui/calendar-app/components/priority-view.tsx +939 -0
  31. package/src/components/ui/calendar-app/components/quick-calendar-toggle.tsx +1 -0
  32. package/src/components/ui/calendar-app/components/quick-task-dialog.tsx +335 -0
  33. package/src/components/ui/calendar-app/components/quick-task-timer.tsx +112 -0
  34. package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.tsx +415 -0
  35. package/src/components/ui/calendar-app/components/scheduling-dialog.tsx +660 -0
  36. package/src/components/ui/calendar-app/components/sidebar/calendar-sidebar.tsx +128 -0
  37. package/src/components/ui/calendar-app/components/sidebar/index.tsx +1 -0
  38. package/src/components/ui/calendar-app/components/smart-schedule-button.tsx +117 -0
  39. package/src/components/ui/calendar-app/components/smart-schedule-preview-dialog.tsx +845 -0
  40. package/src/components/ui/calendar-app/components/smart-schedule-preview-panel.tsx +941 -0
  41. package/src/components/ui/calendar-app/components/sync-debug-panel.tsx +708 -0
  42. package/src/components/ui/calendar-app/components/task-fetcher.ts +51 -0
  43. package/src/components/ui/calendar-app/components/task-form.tsx +132 -0
  44. package/src/components/ui/calendar-app/components/task-list-form.tsx +84 -0
  45. package/src/components/ui/calendar-app/components/task-scheduler-panel.tsx +490 -0
  46. package/src/components/ui/calendar-app/components/tasks-sidebar-content.tsx +232 -0
  47. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +143 -0
  48. package/src/components/ui/calendar-app/components/test-event-generator-button.tsx +94 -0
  49. package/src/components/ui/calendar-app/components/time-tracker/components/active-session-card.tsx +113 -0
  50. package/src/components/ui/calendar-app/components/time-tracker/components/index.ts +3 -0
  51. package/src/components/ui/calendar-app/components/time-tracker/components/session-card.tsx +153 -0
  52. package/src/components/ui/calendar-app/components/time-tracker/components/stats-cards.tsx +58 -0
  53. package/src/components/ui/calendar-app/components/time-tracker/dialogs/create-task-dialog.tsx +190 -0
  54. package/src/components/ui/calendar-app/components/time-tracker/dialogs/delete-session-dialog.tsx +52 -0
  55. package/src/components/ui/calendar-app/components/time-tracker/dialogs/edit-session-dialog.tsx +173 -0
  56. package/src/components/ui/calendar-app/components/time-tracker/dialogs/index.ts +3 -0
  57. package/src/components/ui/calendar-app/components/time-tracker/hooks/index.ts +2 -0
  58. package/src/components/ui/calendar-app/components/time-tracker/hooks/use-sessions.ts +191 -0
  59. package/src/components/ui/calendar-app/components/time-tracker/hooks/use-time-tracker.ts +408 -0
  60. package/src/components/ui/calendar-app/components/time-tracker/index.tsx +902 -0
  61. package/src/components/ui/calendar-app/components/time-tracker/tabs/history-tab.tsx +28 -0
  62. package/src/components/ui/calendar-app/components/time-tracker/tabs/index.ts +2 -0
  63. package/src/components/ui/calendar-app/components/time-tracker/tabs/recent-sessions-tab.tsx +173 -0
  64. package/src/components/ui/calendar-app/components/time-tracker/time-tracker-trigger.tsx +50 -0
  65. package/src/components/ui/calendar-app/components/time-tracker/utils/category-colors.ts +38 -0
  66. package/src/components/ui/calendar-app/components/time-tracker/utils/format-time.ts +26 -0
  67. package/src/components/ui/calendar-app/components/time-tracker/utils/index.ts +2 -0
  68. package/src/components/ui/calendar-app/components/time-tracker.tsx +2040 -0
  69. package/src/components/ui/calendar-app/hooks/index.ts +9 -0
  70. package/src/components/ui/calendar-app/hooks/use-calendar-settings.ts +130 -0
  71. package/src/components/ui/calendar-app/hooks/use-e2ee.ts +283 -0
  72. package/src/components/ui/calendar.tsx +212 -64
  73. package/src/components/ui/card.tsx +3 -3
  74. package/src/components/ui/carousel.tsx +6 -5
  75. package/src/components/ui/chart.tsx +76 -24
  76. package/src/components/ui/chat/ai-message-markdown.tsx +59 -0
  77. package/src/components/ui/chat/ai-message-parts.tsx +176 -0
  78. package/src/components/ui/chat/ai-message-render-registry.tsx +203 -0
  79. package/src/components/ui/chat/ai-message-render-utils.ts +220 -0
  80. package/src/components/ui/chat/ai-message-tool-part.tsx +433 -0
  81. package/src/components/ui/chat/ai-message-tool-utils.ts +138 -0
  82. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +164 -0
  83. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +268 -0
  84. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
  85. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +246 -0
  86. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +297 -0
  87. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +136 -0
  88. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +335 -0
  89. package/src/components/ui/chat/chat-agent-details-utils.test.ts +161 -0
  90. package/src/components/ui/chat/chat-agent-details-utils.tsx +280 -0
  91. package/src/components/ui/chat/chat-ai-credit-source-picker.tsx +210 -0
  92. package/src/components/ui/chat/chat-ai-details-panels.tsx +421 -0
  93. package/src/components/ui/chat/chat-ai-details-sidebar.tsx +206 -0
  94. package/src/components/ui/chat/chat-ai-models.test.ts +37 -0
  95. package/src/components/ui/chat/chat-ai-models.ts +23 -0
  96. package/src/components/ui/chat/chat-shared-content-sidebar.tsx +209 -0
  97. package/src/components/ui/chat/chat-sidebar-groups.test.ts +64 -0
  98. package/src/components/ui/chat/chat-sidebar-items.tsx +195 -0
  99. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
  100. package/src/components/ui/chat/chat-sidebar-panel.tsx +193 -0
  101. package/src/components/ui/chat/chat-sidebar.tsx +505 -0
  102. package/src/components/ui/chat/chat-utils.test.ts +280 -0
  103. package/src/components/ui/chat/chat-workspace-header.tsx +429 -0
  104. package/src/components/ui/chat/chat-workspace.tsx +575 -0
  105. package/src/components/ui/chat/conversation-type-selector.tsx +51 -0
  106. package/src/components/ui/chat/create-conversation-dialog.test.tsx +89 -0
  107. package/src/components/ui/chat/create-conversation-dialog.tsx +377 -0
  108. package/src/components/ui/chat/directory-user-picker.tsx +160 -0
  109. package/src/components/ui/chat/friend-request-items.tsx +124 -0
  110. package/src/components/ui/chat/friend-requests-button.tsx +243 -0
  111. package/src/components/ui/chat/friend-requests-panel.tsx +116 -0
  112. package/src/components/ui/chat/hooks-ai.ts +73 -0
  113. package/src/components/ui/chat/hooks-attachments.ts +83 -0
  114. package/src/components/ui/chat/hooks-conversations.ts +205 -0
  115. package/src/components/ui/chat/hooks-directory.ts +45 -0
  116. package/src/components/ui/chat/hooks-friends.ts +74 -0
  117. package/src/components/ui/chat/hooks-messages.test.tsx +67 -0
  118. package/src/components/ui/chat/hooks-messages.ts +707 -0
  119. package/src/components/ui/chat/hooks-realtime.ts +157 -0
  120. package/src/components/ui/chat/hooks-shared-content.ts +56 -0
  121. package/src/components/ui/chat/hooks.ts +9 -0
  122. package/src/components/ui/chat/message-attachment-button.tsx +188 -0
  123. package/src/components/ui/chat/message-attachment-preview-dialog.tsx +141 -0
  124. package/src/components/ui/chat/message-bubble.tsx +372 -0
  125. package/src/components/ui/chat/message-composer.tsx +213 -0
  126. package/src/components/ui/chat/message-links.tsx +193 -0
  127. package/src/components/ui/chat/message-list.tsx +318 -0
  128. package/src/components/ui/chat/message-markdown.tsx +112 -0
  129. package/src/components/ui/chat/message-youtube.ts +48 -0
  130. package/src/components/ui/chat/query-keys.ts +48 -0
  131. package/src/components/ui/chat/utils.ts +284 -0
  132. package/src/components/ui/checkbox.tsx +24 -8
  133. package/src/components/ui/codeblock.tsx +7 -7
  134. package/src/components/ui/color-picker.tsx +9 -8
  135. package/src/components/ui/command.tsx +19 -11
  136. package/src/components/ui/context-menu.tsx +10 -10
  137. package/src/components/ui/currency-input.tsx +464 -0
  138. package/src/components/ui/custom/__tests__/facebook-mockup.test.tsx +291 -0
  139. package/src/components/ui/custom/__tests__/report-preview-pagination.test.ts +189 -0
  140. package/src/components/ui/custom/__tests__/report-preview.test.tsx +201 -0
  141. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +144 -0
  142. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +10 -0
  143. package/src/components/ui/custom/autosize-textarea.tsx +3 -2
  144. package/src/components/ui/custom/calendar/core.tsx +11 -3
  145. package/src/components/ui/custom/calendar/day-cell.tsx +58 -44
  146. package/src/components/ui/custom/calendar/month-header.tsx +5 -7
  147. package/src/components/ui/custom/calendar/month-view.tsx +21 -9
  148. package/src/components/ui/custom/calendar/year-calendar.tsx +51 -10
  149. package/src/components/ui/custom/calendar/year-view.tsx +9 -5
  150. package/src/components/ui/custom/combobox.tsx +357 -89
  151. package/src/components/ui/custom/common-footer.tsx +89 -39
  152. package/src/components/ui/custom/compared-date-range-picker.tsx +478 -261
  153. package/src/components/ui/custom/data-pagination.tsx +446 -0
  154. package/src/components/ui/custom/date-input.tsx +4 -7
  155. package/src/components/ui/custom/education/certificates/certificate-viewer.tsx +98 -0
  156. package/src/components/ui/custom/education/certificates/download-button-pdf.tsx +82 -0
  157. package/src/components/ui/custom/education/certificates/types.ts +25 -0
  158. package/src/components/ui/custom/education/courses/course-card-view.tsx +252 -0
  159. package/src/components/ui/custom/education/courses/course-form.tsx +248 -0
  160. package/src/components/ui/custom/education/courses/course-header.tsx +38 -0
  161. package/src/components/ui/custom/education/courses/course-pagination.tsx +62 -0
  162. package/src/components/ui/custom/education/courses/course-row-actions.tsx +130 -0
  163. package/src/components/ui/custom/education/modules/content-section.tsx +73 -0
  164. package/src/components/ui/custom/education/modules/course-module-form.tsx +234 -0
  165. package/src/components/ui/custom/education/modules/course-module-row-actions.tsx +126 -0
  166. package/src/components/ui/custom/education/modules/link-button.tsx +66 -0
  167. package/src/components/ui/custom/education/modules/module-toggle.tsx +115 -0
  168. package/src/components/ui/custom/education/modules/resources/delete-resource.tsx +46 -0
  169. package/src/components/ui/custom/education/modules/resources/file-display.tsx +84 -0
  170. package/src/components/ui/custom/education/modules/resources/file-upload-form.tsx +544 -0
  171. package/src/components/ui/custom/education/modules/resources/pdf-viewer.tsx +119 -0
  172. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +62 -0
  173. package/src/components/ui/custom/education/modules/youtube/embed.tsx +20 -0
  174. package/src/components/ui/custom/education/modules/youtube/form.tsx +112 -0
  175. package/src/components/ui/custom/education/shell/education-content-surface.tsx +31 -0
  176. package/src/components/ui/custom/education/shell/education-kpi-strip.tsx +48 -0
  177. package/src/components/ui/custom/education/shell/education-page-header.tsx +56 -0
  178. package/src/components/ui/custom/empty-card.tsx +2 -2
  179. package/src/components/ui/custom/facebook-mockup/defaults.ts +81 -0
  180. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +313 -0
  181. package/src/components/ui/custom/facebook-mockup/form.tsx +629 -0
  182. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +111 -0
  183. package/src/components/ui/custom/facebook-mockup/preview.tsx +760 -0
  184. package/src/components/ui/custom/facebook-mockup/types.ts +44 -0
  185. package/src/components/ui/custom/feature-summary.tsx +61 -7
  186. package/src/components/ui/custom/file-uploader.tsx +18 -15
  187. package/src/components/ui/custom/get-started-button.tsx +33 -0
  188. package/src/components/ui/custom/get-started-gradient-button.tsx +23 -0
  189. package/src/components/ui/custom/gradient-headline.tsx +25 -0
  190. package/src/components/ui/custom/icon-picker/icon-options.ts +24240 -0
  191. package/src/components/ui/custom/icon-picker/icon-picker.tsx +608 -0
  192. package/src/components/ui/custom/icon-picker/index.ts +45 -0
  193. package/src/components/ui/custom/icon-picker/types.ts +133 -0
  194. package/src/components/ui/custom/input-field.tsx +3 -1
  195. package/src/components/ui/custom/language-dropdown-item.tsx +55 -0
  196. package/src/components/ui/custom/language-dropdown-wrapper.tsx +34 -0
  197. package/src/components/ui/custom/language-toggle.tsx +43 -0
  198. package/src/components/ui/custom/language-wrapper.tsx +23 -0
  199. package/src/components/ui/custom/lead-generation-preview.tsx +310 -0
  200. package/src/components/ui/custom/loading-indicator.tsx +3 -0
  201. package/src/components/ui/custom/loading.tsx +8 -0
  202. package/src/components/ui/custom/logo-title.tsx +20 -0
  203. package/src/components/ui/custom/missed-entry/image-upload-section.tsx +192 -0
  204. package/src/components/ui/custom/modifiable-dialog-trigger.tsx +13 -7
  205. package/src/components/ui/custom/month-picker.tsx +8 -8
  206. package/src/components/ui/custom/nav-link.tsx +474 -0
  207. package/src/components/ui/custom/nav.tsx +86 -0
  208. package/src/components/ui/custom/navigation.tsx +192 -0
  209. package/src/components/ui/custom/notification-popover-client.tsx +758 -0
  210. package/src/components/ui/custom/production-indicator.tsx +22 -0
  211. package/src/components/ui/custom/qr/color.tsx +36 -0
  212. package/src/components/ui/custom/qr/display.tsx +82 -0
  213. package/src/components/ui/custom/qr/formats.tsx +52 -0
  214. package/src/components/ui/custom/qr/image-upload.tsx +440 -0
  215. package/src/components/ui/custom/qr/qr.tsx +170 -0
  216. package/src/components/ui/custom/qr/styles.tsx +133 -0
  217. package/src/components/ui/custom/qr/workspace-title.tsx +47 -0
  218. package/src/components/ui/custom/report-preview-pagination.ts +579 -0
  219. package/src/components/ui/custom/report-preview.tsx +745 -126
  220. package/src/components/ui/custom/search-bar.tsx +8 -12
  221. package/src/components/ui/custom/select-field.tsx +5 -3
  222. package/src/components/ui/custom/settings/appearance-settings.tsx +129 -0
  223. package/src/components/ui/custom/settings/lunar-calendar-settings.tsx +36 -0
  224. package/src/components/ui/custom/settings/sidebar-settings.tsx +135 -0
  225. package/src/components/ui/custom/settings/task-settings.tsx +236 -0
  226. package/src/components/ui/custom/settings-dialog-shell.tsx +481 -0
  227. package/src/components/ui/custom/settings-item-tab.tsx +29 -0
  228. package/src/components/ui/custom/sidebar-context.tsx +148 -0
  229. package/src/components/ui/custom/sidebar-footer-actions.tsx +337 -0
  230. package/src/components/ui/custom/staff-toolbar.tsx +16 -0
  231. package/src/components/ui/custom/structure.tsx +213 -0
  232. package/src/components/ui/custom/system-language-dropdown-item.tsx +46 -0
  233. package/src/components/ui/custom/system-language-wrapper.tsx +15 -0
  234. package/src/components/ui/custom/tables/custom-data-table.tsx +130 -0
  235. package/src/components/ui/custom/tables/data-table-column-header.tsx +9 -7
  236. package/src/components/ui/custom/tables/data-table-create-button.tsx +2 -2
  237. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +5 -5
  238. package/src/components/ui/custom/tables/data-table-pagination.tsx +69 -91
  239. package/src/components/ui/custom/tables/data-table-refresh-button.tsx +2 -2
  240. package/src/components/ui/custom/tables/data-table-toolbar.tsx +42 -19
  241. package/src/components/ui/custom/tables/data-table-view-options.tsx +27 -15
  242. package/src/components/ui/custom/tables/data-table.tsx +150 -56
  243. package/src/components/ui/custom/tailwind-indicator.tsx +14 -0
  244. package/src/components/ui/custom/theme-dropdown-items.tsx +90 -0
  245. package/src/components/ui/custom/theme-toggle.tsx +35 -0
  246. package/src/components/ui/custom/tuturuuu-logo.tsx +16 -0
  247. package/src/components/ui/custom/uploaded-files-card.tsx +1 -0
  248. package/src/components/ui/custom/user-filters.tsx +487 -0
  249. package/src/components/ui/custom/version-badge.test.tsx +102 -0
  250. package/src/components/ui/custom/version-badge.tsx +169 -0
  251. package/src/components/ui/custom/view-toggle.tsx +63 -0
  252. package/src/components/ui/custom/workspace-access/adapters.test.ts +31 -0
  253. package/src/components/ui/custom/workspace-access/adapters.ts +169 -0
  254. package/src/components/ui/custom/workspace-access/index.ts +32 -0
  255. package/src/components/ui/custom/workspace-access/member-filter-utils.test.ts +175 -0
  256. package/src/components/ui/custom/workspace-access/member-filter-utils.ts +190 -0
  257. package/src/components/ui/custom/workspace-access/types.ts +150 -0
  258. package/src/components/ui/custom/workspace-access/workspace-access-client-pages.tsx +26 -0
  259. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +92 -0
  260. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +102 -0
  261. package/src/components/ui/custom/workspace-access/workspace-access-labels.ts +53 -0
  262. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +249 -0
  263. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +84 -0
  264. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +72 -0
  265. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +388 -0
  266. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +200 -0
  267. package/src/components/ui/custom/workspace-access/workspace-access-permission-checklist.tsx +87 -0
  268. package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.tsx +35 -0
  269. package/src/components/ui/custom/workspace-access/workspace-access-role-editor-dialog.tsx +180 -0
  270. package/src/components/ui/custom/workspace-access/workspace-access-role-editor-labels.ts +33 -0
  271. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +171 -0
  272. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +71 -0
  273. package/src/components/ui/custom/workspace-select.tsx +849 -0
  274. package/src/components/ui/custom/workspace-wrapper.tsx +142 -0
  275. package/src/components/ui/date-time-picker.tsx +706 -0
  276. package/src/components/ui/dialog.tsx +23 -14
  277. package/src/components/ui/diff-viewer.tsx +921 -0
  278. package/src/components/ui/drawer.tsx +5 -5
  279. package/src/components/ui/dropdown-menu.tsx +18 -10
  280. package/src/components/ui/finance/analytics/analytics-date-controls.tsx +173 -0
  281. package/src/components/ui/finance/analytics/analytics-page.test.tsx +182 -0
  282. package/src/components/ui/finance/analytics/analytics-page.tsx +296 -0
  283. package/src/components/ui/finance/analytics/balance-trend-chart.tsx +288 -0
  284. package/src/components/ui/finance/analytics/category-spending-chart.test.tsx +75 -0
  285. package/src/components/ui/finance/analytics/category-spending-chart.tsx +213 -0
  286. package/src/components/ui/finance/analytics/income-expense-chart-utils.ts +52 -0
  287. package/src/components/ui/finance/analytics/income-expense-chart.tsx +242 -0
  288. package/src/components/ui/finance/analytics/income-expense-view-mode-control.tsx +37 -0
  289. package/src/components/ui/finance/analytics/spending-trends-chart.tsx +156 -0
  290. package/src/components/ui/finance/budgets/budget-alerts.test.tsx +133 -0
  291. package/src/components/ui/finance/budgets/budget-alerts.tsx +111 -0
  292. package/src/components/ui/finance/budgets/budget-card.test.tsx +72 -0
  293. package/src/components/ui/finance/budgets/budget-card.tsx +233 -0
  294. package/src/components/ui/finance/budgets/budgets-page.test.tsx +56 -0
  295. package/src/components/ui/finance/budgets/budgets-page.tsx +179 -0
  296. package/src/components/ui/finance/budgets/form-fields.tsx +304 -0
  297. package/src/components/ui/finance/budgets/form-schema.ts +39 -0
  298. package/src/components/ui/finance/budgets/form.tsx +141 -0
  299. package/src/components/ui/finance/categories-tags-tabs.tsx +80 -0
  300. package/src/components/ui/finance/debts/debt-loan-card.test.tsx +75 -0
  301. package/src/components/ui/finance/debts/debt-loan-card.tsx +212 -0
  302. package/src/components/ui/finance/debts/debt-loan-detail-cards.test.tsx +89 -0
  303. package/src/components/ui/finance/debts/debt-loan-detail-cards.tsx +207 -0
  304. package/src/components/ui/finance/debts/debt-loan-detail-page.tsx +301 -0
  305. package/src/components/ui/finance/debts/debt-loan-form-schema.ts +30 -0
  306. package/src/components/ui/finance/debts/debt-loan-form.tsx +394 -0
  307. package/src/components/ui/finance/debts/debt-loan-list.tsx +58 -0
  308. package/src/components/ui/finance/debts/debt-loan-summary.tsx +94 -0
  309. package/src/components/ui/finance/debts/debts-page.test.tsx +97 -0
  310. package/src/components/ui/finance/debts/debts-page.tsx +230 -0
  311. package/src/components/ui/finance/debts/index.ts +6 -0
  312. package/src/components/ui/finance/debts/query-invalidation.test.ts +62 -0
  313. package/src/components/ui/finance/debts/query-invalidation.ts +44 -0
  314. package/src/components/ui/finance/finance-layout.tsx +98 -0
  315. package/src/components/ui/finance/finance-overview-metrics.tsx +120 -0
  316. package/src/components/ui/finance/finance-page.tsx +169 -0
  317. package/src/components/ui/finance/finance-route-context.tsx +50 -0
  318. package/src/components/ui/finance/invoices/attendance-calendar.tsx +247 -0
  319. package/src/components/ui/finance/invoices/charts/invoice-totals-chart.test.tsx +67 -0
  320. package/src/components/ui/finance/invoices/charts/invoice-totals-chart.tsx +868 -0
  321. package/src/components/ui/finance/invoices/columns.test.tsx +66 -0
  322. package/src/components/ui/finance/invoices/columns.tsx +365 -0
  323. package/src/components/ui/finance/invoices/components/invoice-blocked-state.tsx +30 -0
  324. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +120 -0
  325. package/src/components/ui/finance/invoices/components/invoice-content-editor.tsx +71 -0
  326. package/src/components/ui/finance/invoices/components/invoice-customer-select-card.tsx +342 -0
  327. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +216 -0
  328. package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +219 -0
  329. package/src/components/ui/finance/invoices/components/subscription-attendance-summary.tsx +272 -0
  330. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +346 -0
  331. package/src/components/ui/finance/invoices/create-promotion-dialog.tsx +67 -0
  332. package/src/components/ui/finance/invoices/export-dialog-content.tsx +492 -0
  333. package/src/components/ui/finance/invoices/hooks/use-best-promotion-selection.ts +119 -0
  334. package/src/components/ui/finance/invoices/hooks/use-invoice-analytics.ts +79 -0
  335. package/src/components/ui/finance/invoices/hooks/use-invoice-rounding.ts +33 -0
  336. package/src/components/ui/finance/invoices/hooks/use-invoice-subtotal.ts +13 -0
  337. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +412 -0
  338. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +191 -0
  339. package/src/components/ui/finance/invoices/hooks.ts +878 -0
  340. package/src/components/ui/finance/invoices/internal-api.ts +466 -0
  341. package/src/components/ui/finance/invoices/invoice-analytics.tsx +167 -0
  342. package/src/components/ui/finance/invoices/invoice-page.tsx +468 -0
  343. package/src/components/ui/finance/invoices/invoice-visibility-format.test.ts +90 -0
  344. package/src/components/ui/finance/invoices/invoice-visibility-format.ts +67 -0
  345. package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +172 -0
  346. package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +349 -0
  347. package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +360 -0
  348. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.test.tsx +227 -0
  349. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +409 -0
  350. package/src/components/ui/finance/invoices/invoiceId/invoice-edit-form.tsx +161 -0
  351. package/src/components/ui/finance/invoices/invoiceId/product-card.tsx +90 -0
  352. package/src/components/ui/finance/invoices/invoiceId/promotion-card.tsx +76 -0
  353. package/src/components/ui/finance/invoices/invoices-table.tsx +284 -0
  354. package/src/components/ui/finance/invoices/new-invoice-page.tsx +215 -0
  355. package/src/components/ui/finance/invoices/pending-columns.tsx +308 -0
  356. package/src/components/ui/finance/invoices/pending-invoices-tab.tsx +43 -0
  357. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +258 -0
  358. package/src/components/ui/finance/invoices/product-selection.test.tsx +78 -0
  359. package/src/components/ui/finance/invoices/product-selection.tsx +345 -0
  360. package/src/components/ui/finance/invoices/promotion-form.tsx +352 -0
  361. package/src/components/ui/finance/invoices/query-invalidation.ts +65 -0
  362. package/src/components/ui/finance/invoices/row-actions.tsx +148 -0
  363. package/src/components/ui/finance/invoices/standard-invoice.tsx +595 -0
  364. package/src/components/ui/finance/invoices/subscription-invoice.tsx +1009 -0
  365. package/src/components/ui/finance/invoices/types.ts +93 -0
  366. package/src/components/ui/finance/invoices/user-filter-wrapper.tsx +59 -0
  367. package/src/components/ui/finance/invoices/utils.test.ts +165 -0
  368. package/src/components/ui/finance/invoices/utils.ts +484 -0
  369. package/src/components/ui/finance/invoices/wallet-filter-wrapper.tsx +47 -0
  370. package/src/components/ui/finance/recurring/form-fields.tsx +142 -0
  371. package/src/components/ui/finance/recurring/form-schedule-fields.tsx +112 -0
  372. package/src/components/ui/finance/recurring/form-schema.ts +32 -0
  373. package/src/components/ui/finance/recurring/form.tsx +130 -0
  374. package/src/components/ui/finance/recurring/recurring-sections.tsx +141 -0
  375. package/src/components/ui/finance/recurring/recurring-transaction-card.tsx +152 -0
  376. package/src/components/ui/finance/recurring/recurring-transactions-page.test.tsx +87 -0
  377. package/src/components/ui/finance/recurring/recurring-transactions-page.tsx +154 -0
  378. package/src/components/ui/finance/recurring/upcoming-transaction-card.tsx +84 -0
  379. package/src/components/ui/finance/shared/charts/category-breakdown-chart-body.tsx +118 -0
  380. package/src/components/ui/finance/shared/charts/category-breakdown-chart-controls.tsx +146 -0
  381. package/src/components/ui/finance/shared/charts/category-breakdown-chart-legend.tsx +49 -0
  382. package/src/components/ui/finance/shared/charts/category-breakdown-chart-tooltip.tsx +109 -0
  383. package/src/components/ui/finance/shared/charts/category-breakdown-chart-types.ts +24 -0
  384. package/src/components/ui/finance/shared/charts/category-breakdown-chart-utils.ts +207 -0
  385. package/src/components/ui/finance/shared/charts/category-breakdown-chart.tsx +263 -0
  386. package/src/components/ui/finance/shared/charts/daily-total-chart-client.tsx +384 -0
  387. package/src/components/ui/finance/shared/charts/daily-total-chart.tsx +299 -0
  388. package/src/components/ui/finance/shared/charts/monthly-total-chart-client.tsx +391 -0
  389. package/src/components/ui/finance/shared/charts/monthly-total-chart.tsx +296 -0
  390. package/src/components/ui/finance/shared/charts/use-finance-confidential-visibility.ts +5 -0
  391. package/src/components/ui/finance/shared/confidential-toggle.tsx +78 -0
  392. package/src/components/ui/finance/shared/create-dialog-feature-summary.tsx +23 -0
  393. package/src/components/ui/finance/shared/dashboard-header.tsx +19 -0
  394. package/src/components/ui/finance/shared/date-picker.tsx +74 -0
  395. package/src/components/ui/finance/shared/date-range-filter-wrapper.tsx +86 -0
  396. package/src/components/ui/finance/shared/date-range-picker.tsx +42 -0
  397. package/src/components/ui/finance/shared/empty-state.tsx +41 -0
  398. package/src/components/ui/finance/shared/filter.tsx +267 -0
  399. package/src/components/ui/finance/shared/finance-display-amount.tsx +25 -0
  400. package/src/components/ui/finance/shared/loaders/statistics.tsx +18 -0
  401. package/src/components/ui/finance/shared/loaders/table-skeleton.tsx +5 -0
  402. package/src/components/ui/finance/shared/metrics.tsx +69 -0
  403. package/src/components/ui/finance/shared/month-picker.tsx +177 -0
  404. package/src/components/ui/finance/shared/month-range-picker.tsx +50 -0
  405. package/src/components/ui/finance/shared/next-charts.tsx +77 -0
  406. package/src/components/ui/finance/shared/numbers-visibility-toggle.test.tsx +38 -0
  407. package/src/components/ui/finance/shared/numbers-visibility-toggle.tsx +43 -0
  408. package/src/components/ui/finance/shared/quick-actions.tsx +143 -0
  409. package/src/components/ui/finance/shared/use-finance-confidential-visibility.ts +68 -0
  410. package/src/components/ui/finance/shared/year-picker.tsx +178 -0
  411. package/src/components/ui/finance/shared/year-range-picker.tsx +50 -0
  412. package/src/components/ui/finance/statistics/card.tsx +195 -0
  413. package/src/components/ui/finance/statistics/expense.tsx +63 -0
  414. package/src/components/ui/finance/statistics/income.tsx +63 -0
  415. package/src/components/ui/finance/statistics/invoices.tsx +77 -0
  416. package/src/components/ui/finance/statistics/monthly-expense.tsx +63 -0
  417. package/src/components/ui/finance/statistics/monthly-income.tsx +63 -0
  418. package/src/components/ui/finance/statistics/total-balance.test.tsx +79 -0
  419. package/src/components/ui/finance/statistics/total-balance.tsx +64 -0
  420. package/src/components/ui/finance/statistics/transaction-categories.tsx +59 -0
  421. package/src/components/ui/finance/statistics/transactions.tsx +90 -0
  422. package/src/components/ui/finance/statistics/wallets.tsx +55 -0
  423. package/src/components/ui/finance/tags/tag-badge.tsx +36 -0
  424. package/src/components/ui/finance/tags/tag-manager.test.tsx +128 -0
  425. package/src/components/ui/finance/tags/tag-manager.tsx +646 -0
  426. package/src/components/ui/finance/transactions/TRANSACTIONS_UI_REVAMP.md +328 -0
  427. package/src/components/ui/finance/transactions/UI_IMPROVEMENTS.md +444 -0
  428. package/src/components/ui/finance/transactions/__tests__/export-utils.test.ts +167 -0
  429. package/src/components/ui/finance/transactions/categories/amount-filter-wrapper.tsx +55 -0
  430. package/src/components/ui/finance/transactions/categories/amount-filter.tsx +135 -0
  431. package/src/components/ui/finance/transactions/categories/categories-data-table.tsx +274 -0
  432. package/src/components/ui/finance/transactions/categories/columns.test.tsx +113 -0
  433. package/src/components/ui/finance/transactions/categories/columns.tsx +253 -0
  434. package/src/components/ui/finance/transactions/categories/form.tsx +388 -0
  435. package/src/components/ui/finance/transactions/categories/hooks.test.tsx +72 -0
  436. package/src/components/ui/finance/transactions/categories/hooks.ts +95 -0
  437. package/src/components/ui/finance/transactions/categories/row-actions.tsx +132 -0
  438. package/src/components/ui/finance/transactions/categories/transactions-categories-page.tsx +96 -0
  439. package/src/components/ui/finance/transactions/categories/type-filter-wrapper.tsx +39 -0
  440. package/src/components/ui/finance/transactions/categories/type-filter.tsx +148 -0
  441. package/src/components/ui/finance/transactions/category-filter-wrapper.tsx +41 -0
  442. package/src/components/ui/finance/transactions/category-filter.tsx +252 -0
  443. package/src/components/ui/finance/transactions/columns.test.tsx +119 -0
  444. package/src/components/ui/finance/transactions/columns.tsx +313 -0
  445. package/src/components/ui/finance/transactions/confidential-field.test.tsx +32 -0
  446. package/src/components/ui/finance/transactions/confidential-field.tsx +289 -0
  447. package/src/components/ui/finance/transactions/export-dialog-content.tsx +393 -0
  448. package/src/components/ui/finance/transactions/export-utils.ts +99 -0
  449. package/src/components/ui/finance/transactions/form-basic-tab.tsx +360 -0
  450. package/src/components/ui/finance/transactions/form-content-dialog.tsx +163 -0
  451. package/src/components/ui/finance/transactions/form-more-tab.tsx +203 -0
  452. package/src/components/ui/finance/transactions/form-schema.ts +52 -0
  453. package/src/components/ui/finance/transactions/form-types.ts +39 -0
  454. package/src/components/ui/finance/transactions/form-utils.tsx +60 -0
  455. package/src/components/ui/finance/transactions/form.test.tsx +151 -0
  456. package/src/components/ui/finance/transactions/form.tsx +942 -0
  457. package/src/components/ui/finance/transactions/hooks/use-filter-reset.ts +27 -0
  458. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +1127 -0
  459. package/src/components/ui/finance/transactions/internal-api.ts +148 -0
  460. package/src/components/ui/finance/transactions/money-lover-import-dialog-utils.test.ts +16 -0
  461. package/src/components/ui/finance/transactions/money-lover-import-dialog-utils.ts +17 -0
  462. package/src/components/ui/finance/transactions/money-lover-import-dialog.tsx +908 -0
  463. package/src/components/ui/finance/transactions/period-charts/activity-distribution-chart.tsx +315 -0
  464. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +820 -0
  465. package/src/components/ui/finance/transactions/period-charts/category-donut-chart.tsx +609 -0
  466. package/src/components/ui/finance/transactions/period-charts/index.ts +4 -0
  467. package/src/components/ui/finance/transactions/period-charts/period-breakdown-panel.tsx +492 -0
  468. package/src/components/ui/finance/transactions/query-invalidation.test.ts +91 -0
  469. package/src/components/ui/finance/transactions/query-invalidation.ts +116 -0
  470. package/src/components/ui/finance/transactions/row-actions.tsx +145 -0
  471. package/src/components/ui/finance/transactions/tag-filter-wrapper.tsx +40 -0
  472. package/src/components/ui/finance/transactions/tag-filter.test.tsx +49 -0
  473. package/src/components/ui/finance/transactions/tag-filter.tsx +171 -0
  474. package/src/components/ui/finance/transactions/transaction-attachments-field.tsx +787 -0
  475. package/src/components/ui/finance/transactions/transaction-card.tsx +576 -0
  476. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +101 -0
  477. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +947 -0
  478. package/src/components/ui/finance/transactions/transaction-statistics.test.tsx +58 -0
  479. package/src/components/ui/finance/transactions/transaction-statistics.tsx +280 -0
  480. package/src/components/ui/finance/transactions/transaction-type-filter-wrapper.tsx +40 -0
  481. package/src/components/ui/finance/transactions/transaction-type-filter.tsx +95 -0
  482. package/src/components/ui/finance/transactions/transactionId/bill.tsx +307 -0
  483. package/src/components/ui/finance/transactions/transactionId/objects.tsx +149 -0
  484. package/src/components/ui/finance/transactions/transactionId/row-actions.tsx +263 -0
  485. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +334 -0
  486. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +76 -0
  487. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +56 -0
  488. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +213 -0
  489. package/src/components/ui/finance/transactions/transactions-page.tsx +116 -0
  490. package/src/components/ui/finance/transactions/transfer-fields.tsx +194 -0
  491. package/src/components/ui/finance/transactions/user-filter-wrapper.tsx +42 -0
  492. package/src/components/ui/finance/transactions/user-filter.test.tsx +66 -0
  493. package/src/components/ui/finance/transactions/user-filter.tsx +282 -0
  494. package/src/components/ui/finance/transactions/view-mode-toggle.tsx +124 -0
  495. package/src/components/ui/finance/transactions/wallet-filter-wrapper.tsx +41 -0
  496. package/src/components/ui/finance/transactions/wallet-filter.test.tsx +52 -0
  497. package/src/components/ui/finance/transactions/wallet-filter.tsx +204 -0
  498. package/src/components/ui/finance/wallets/columns.tsx +324 -0
  499. package/src/components/ui/finance/wallets/form.test.tsx +78 -0
  500. package/src/components/ui/finance/wallets/form.tsx +491 -0
  501. package/src/components/ui/finance/wallets/query-invalidation.ts +58 -0
  502. package/src/components/ui/finance/wallets/row-actions.tsx +166 -0
  503. package/src/components/ui/finance/wallets/wallet-form-schema.ts +63 -0
  504. package/src/components/ui/finance/wallets/wallet-icon-display.tsx +71 -0
  505. package/src/components/ui/finance/wallets/wallet-icon-image-picker.tsx +369 -0
  506. package/src/components/ui/finance/wallets/wallet-images.ts +180 -0
  507. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +265 -0
  508. package/src/components/ui/finance/wallets/walletId/interest/index.ts +18 -0
  509. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-chart.tsx +296 -0
  510. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-detection-banner.tsx +77 -0
  511. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-disable-dialog.tsx +55 -0
  512. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-hero.tsx +149 -0
  513. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-pending-deposits.tsx +185 -0
  514. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-projections.tsx +259 -0
  515. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-rate-dialog.tsx +89 -0
  516. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-rate-history.tsx +116 -0
  517. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-section.tsx +260 -0
  518. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-settings.tsx +427 -0
  519. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-setup-dialog.tsx +206 -0
  520. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-summary.tsx +244 -0
  521. package/src/components/ui/finance/wallets/walletId/interest/wallet-interest-transparency.tsx +176 -0
  522. package/src/components/ui/finance/wallets/walletId/wallet-delete-button.test.tsx +78 -0
  523. package/src/components/ui/finance/wallets/walletId/wallet-delete-button.tsx +99 -0
  524. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +119 -0
  525. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +36 -0
  526. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +161 -0
  527. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +365 -0
  528. package/src/components/ui/finance/wallets/walletId/wallet-role-access-dialog.tsx +45 -0
  529. package/src/components/ui/finance/wallets/walletId/wallet-role-access.tsx +538 -0
  530. package/src/components/ui/finance/wallets/wallets-data-table.tsx +137 -0
  531. package/src/components/ui/finance/wallets/wallets-page.test.tsx +107 -0
  532. package/src/components/ui/finance/wallets/wallets-page.tsx +136 -0
  533. package/src/components/ui/form-required-indicator.tsx +31 -0
  534. package/src/components/ui/form.tsx +36 -13
  535. package/src/components/ui/hover-card.tsx +2 -2
  536. package/src/components/ui/input-otp.tsx +4 -4
  537. package/src/components/ui/input.tsx +2 -2
  538. package/src/components/ui/kbd.tsx +28 -0
  539. package/src/components/ui/label.tsx +2 -2
  540. package/src/components/ui/legacy/calendar/__tests__/event-utils.test.ts +247 -0
  541. package/src/components/ui/legacy/calendar/agenda-view.tsx +398 -0
  542. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +1033 -0
  543. package/src/components/ui/legacy/calendar/calendar-cell.tsx +1018 -0
  544. package/src/components/ui/legacy/calendar/calendar-column.tsx +31 -0
  545. package/src/components/ui/legacy/calendar/calendar-content.tsx +670 -0
  546. package/src/components/ui/legacy/calendar/calendar-header.tsx +195 -0
  547. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +328 -0
  548. package/src/components/ui/legacy/calendar/calendar-settings-dialog.tsx +340 -0
  549. package/src/components/ui/legacy/calendar/calendar-view-with-trail.tsx +48 -0
  550. package/src/components/ui/legacy/calendar/calendar-view.tsx +28 -0
  551. package/src/components/ui/legacy/calendar/color-highlights.ts +55 -0
  552. package/src/components/ui/legacy/calendar/config.ts +8 -0
  553. package/src/components/ui/legacy/calendar/create-event-button.tsx +155 -0
  554. package/src/components/ui/legacy/calendar/day-title.tsx +68 -0
  555. package/src/components/ui/legacy/calendar/dynamic-island.tsx +245 -0
  556. package/src/components/ui/legacy/calendar/event-card.tsx +1430 -0
  557. package/src/components/ui/legacy/calendar/event-form-components.tsx +593 -0
  558. package/src/components/ui/legacy/calendar/event-modal.tsx +1544 -0
  559. package/src/components/ui/legacy/calendar/event-utils.ts +149 -0
  560. package/src/components/ui/legacy/calendar/location-timeline.tsx +1482 -0
  561. package/src/components/ui/legacy/calendar/month-calendar.tsx +986 -0
  562. package/src/components/ui/legacy/calendar/month-cell.tsx +33 -0
  563. package/src/components/ui/legacy/calendar/month-grid.tsx +105 -0
  564. package/src/components/ui/legacy/calendar/settings/analytics-charts.tsx +414 -0
  565. package/src/components/ui/legacy/calendar/settings/appearance-settings.tsx +218 -0
  566. package/src/components/ui/legacy/calendar/settings/calendar-sync-dashboard.tsx +217 -0
  567. package/src/components/ui/legacy/calendar/settings/category-color-settings.tsx +475 -0
  568. package/src/components/ui/legacy/calendar/settings/color-picker.tsx +177 -0
  569. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +562 -0
  570. package/src/components/ui/legacy/calendar/settings/hour-settings.tsx +292 -0
  571. package/src/components/ui/legacy/calendar/settings/notification-settings.tsx +254 -0
  572. package/src/components/ui/legacy/calendar/settings/settings-context.tsx +257 -0
  573. package/src/components/ui/legacy/calendar/settings/smart-scheduling-settings.tsx +325 -0
  574. package/src/components/ui/legacy/calendar/settings/summary-cards.tsx +93 -0
  575. package/src/components/ui/legacy/calendar/settings/sync-logs-table.tsx +392 -0
  576. package/src/components/ui/legacy/calendar/settings/task-settings.tsx +330 -0
  577. package/src/components/ui/legacy/calendar/settings/time-range-picker.tsx +666 -0
  578. package/src/components/ui/legacy/calendar/settings/timezone-settings.tsx +580 -0
  579. package/src/components/ui/legacy/calendar/settings/types.ts +119 -0
  580. package/src/components/ui/legacy/calendar/settings-button.tsx +41 -0
  581. package/src/components/ui/legacy/calendar/smart-calendar.tsx +82 -0
  582. package/src/components/ui/legacy/calendar/time-indicator-line.tsx +61 -0
  583. package/src/components/ui/legacy/calendar/time-indicator-text.tsx +61 -0
  584. package/src/components/ui/legacy/calendar/time-indicator.tsx +19 -0
  585. package/src/components/ui/legacy/calendar/time-trail.tsx +52 -0
  586. package/src/components/ui/legacy/calendar/weekday-bar.tsx +78 -0
  587. package/src/components/ui/legacy/calendar/year-calendar.tsx +345 -0
  588. package/src/components/ui/legacy/meet/client-wrapper.tsx +74 -0
  589. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +404 -0
  590. package/src/components/ui/legacy/meet/date-selector.tsx +46 -0
  591. package/src/components/ui/legacy/meet/edit-plan-dialog.tsx +501 -0
  592. package/src/components/ui/legacy/meet/experimental-notice.tsx +39 -0
  593. package/src/components/ui/legacy/meet/form.tsx +94 -0
  594. package/src/components/ui/legacy/meet/multiple-choice-vote.tsx +556 -0
  595. package/src/components/ui/legacy/meet/page.tsx +331 -0
  596. package/src/components/ui/legacy/meet/pagination.tsx +87 -0
  597. package/src/components/ui/legacy/meet/planId/account-badge.tsx +21 -0
  598. package/src/components/ui/legacy/meet/planId/agenda-details.tsx +131 -0
  599. package/src/components/ui/legacy/meet/planId/all-availabilities.tsx +105 -0
  600. package/src/components/ui/legacy/meet/planId/availability-planner.tsx +51 -0
  601. package/src/components/ui/legacy/meet/planId/confirm-button.tsx +53 -0
  602. package/src/components/ui/legacy/meet/planId/copy-link-button.tsx +218 -0
  603. package/src/components/ui/legacy/meet/planId/date-planner.tsx +243 -0
  604. package/src/components/ui/legacy/meet/planId/day-planner.tsx +78 -0
  605. package/src/components/ui/legacy/meet/planId/day-planners.tsx +151 -0
  606. package/src/components/ui/legacy/meet/planId/day-time.tsx +32 -0
  607. package/src/components/ui/legacy/meet/planId/download-as-png.tsx +16 -0
  608. package/src/components/ui/legacy/meet/planId/email-button.tsx +32 -0
  609. package/src/components/ui/legacy/meet/planId/logged-in-as-button.tsx +56 -0
  610. package/src/components/ui/legacy/meet/planId/page.tsx +242 -0
  611. package/src/components/ui/legacy/meet/planId/plan-details-client.tsx +205 -0
  612. package/src/components/ui/legacy/meet/planId/plan-login.tsx +342 -0
  613. package/src/components/ui/legacy/meet/planId/plan-user-filter-accordion.tsx +227 -0
  614. package/src/components/ui/legacy/meet/planId/plan-user-filter.tsx +68 -0
  615. package/src/components/ui/legacy/meet/planId/preview-day-time.tsx +303 -0
  616. package/src/components/ui/legacy/meet/planId/selectable-day-time.tsx +247 -0
  617. package/src/components/ui/legacy/meet/planId/show-qr-button.tsx +53 -0
  618. package/src/components/ui/legacy/meet/planId/sidebar-display.tsx +68 -0
  619. package/src/components/ui/legacy/meet/planId/sticky-bottom-indicator.tsx +28 -0
  620. package/src/components/ui/legacy/meet/planId/time-column.tsx +57 -0
  621. package/src/components/ui/legacy/meet/planId/unified-availability.tsx +93 -0
  622. package/src/components/ui/legacy/meet/planId/utility-buttons.tsx +54 -0
  623. package/src/components/ui/legacy/meet/plans-grid.tsx +192 -0
  624. package/src/components/ui/legacy/meet/plans-list-view.tsx +148 -0
  625. package/src/components/ui/legacy/meet/plans-loading-skeleton.tsx +61 -0
  626. package/src/components/ui/legacy/meet/time-selector.tsx +67 -0
  627. package/src/components/ui/legacy/meet/timezone-selector.tsx +72 -0
  628. package/src/components/ui/legacy/meet/user-time.tsx +7 -0
  629. package/src/components/ui/legacy/meet/view-toggle.tsx +44 -0
  630. package/src/components/ui/legacy/polls/poll-display.tsx +388 -0
  631. package/src/components/ui/legacy/polls/where-tu-meet.tsx +80 -0
  632. package/src/components/ui/markdown.tsx +2 -2
  633. package/src/components/ui/menubar.tsx +11 -11
  634. package/src/components/ui/navbar.tsx +150 -0
  635. package/src/components/ui/navigation-menu.tsx +9 -9
  636. package/src/components/ui/pagination.tsx +4 -5
  637. package/src/components/ui/popover.tsx +2 -2
  638. package/src/components/ui/progress.tsx +15 -4
  639. package/src/components/ui/radio-group.tsx +3 -3
  640. package/src/components/ui/report-problem-dialog.tsx +980 -0
  641. package/src/components/ui/resizable.tsx +35 -13
  642. package/src/components/ui/scroll-area.tsx +3 -3
  643. package/src/components/ui/select.tsx +9 -8
  644. package/src/components/ui/separator.tsx +2 -2
  645. package/src/components/ui/sheet.tsx +13 -11
  646. package/src/components/ui/sidebar.tsx +18 -17
  647. package/src/components/ui/slider.tsx +3 -3
  648. package/src/components/ui/sonner.tsx +2 -2
  649. package/src/components/ui/sticky-bottom-bar.tsx +51 -0
  650. package/src/components/ui/switch.tsx +2 -2
  651. package/src/components/ui/table.tsx +2 -2
  652. package/src/components/ui/tabs.tsx +3 -3
  653. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +757 -0
  654. package/src/components/ui/text-editor/__tests__/extensions-integration.test.ts +131 -0
  655. package/src/components/ui/text-editor/__tests__/extensions.test.ts +69 -0
  656. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +799 -0
  657. package/src/components/ui/text-editor/__tests__/inline-task-conversion.test.tsx +46 -0
  658. package/src/components/ui/text-editor/__tests__/keyboard.test.ts +176 -0
  659. package/src/components/ui/text-editor/__tests__/list-converter-extension.test.ts +51 -0
  660. package/src/components/ui/text-editor/__tests__/markdown-paste-extension.test.ts +266 -0
  661. package/src/components/ui/text-editor/__tests__/media-utils.test.ts +425 -0
  662. package/src/components/ui/text-editor/__tests__/task-mention-checkbox.test.ts +251 -0
  663. package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +476 -0
  664. package/src/components/ui/text-editor/__tests__/text-replacements.test.ts +39 -0
  665. package/src/components/ui/text-editor/__tests__/upload-placeholder.test.ts +279 -0
  666. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +711 -0
  667. package/src/components/ui/text-editor/content-migration.ts +199 -0
  668. package/src/components/ui/text-editor/draggable-node-container.tsx +164 -0
  669. package/src/components/ui/text-editor/editor.tsx +707 -0
  670. package/src/components/ui/text-editor/extensions.ts +232 -0
  671. package/src/components/ui/text-editor/image-extension.ts +907 -0
  672. package/src/components/ui/text-editor/keyboard.ts +58 -0
  673. package/src/components/ui/text-editor/list-converter-extension.ts +55 -0
  674. package/src/components/ui/text-editor/list-item-extension.ts +13 -0
  675. package/src/components/ui/text-editor/list-item-view.tsx +17 -0
  676. package/src/components/ui/text-editor/markdown-paste-extension.ts +610 -0
  677. package/src/components/ui/text-editor/media-utils.ts +198 -0
  678. package/src/components/ui/text-editor/mention-extension.tsx +604 -0
  679. package/src/components/ui/text-editor/node-drag-extension.ts +300 -0
  680. package/src/components/ui/text-editor/task-item-checkbox-extension.ts +32 -0
  681. package/src/components/ui/text-editor/task-item-checkbox-view.tsx +227 -0
  682. package/src/components/ui/text-editor/task-item-checkbox.ts +100 -0
  683. package/src/components/ui/text-editor/task-mention-chip.tsx +1397 -0
  684. package/src/components/ui/text-editor/task-mention-resolution.ts +235 -0
  685. package/src/components/ui/text-editor/task-summary-popover.tsx +360 -0
  686. package/src/components/ui/text-editor/text-replacements.ts +65 -0
  687. package/src/components/ui/text-editor/text-shortcuts-extension.ts +60 -0
  688. package/src/components/ui/text-editor/tool-bar.tsx +1294 -0
  689. package/src/components/ui/text-editor/types.ts +1 -0
  690. package/src/components/ui/text-editor/upload-errors.ts +93 -0
  691. package/src/components/ui/text-editor/upload-placeholder.ts +124 -0
  692. package/src/components/ui/text-editor/video-extension.ts +435 -0
  693. package/src/components/ui/textarea.tsx +2 -2
  694. package/src/components/ui/time-picker-input.tsx +6 -6
  695. package/src/components/ui/time-picker-utils.tsx +2 -2
  696. package/src/components/ui/time-tracker/types.ts +112 -0
  697. package/src/components/ui/toast.tsx +9 -9
  698. package/src/components/ui/toaster.tsx +10 -14
  699. package/src/components/ui/toggle-group.tsx +2 -2
  700. package/src/components/ui/toggle.tsx +2 -2
  701. package/src/components/ui/tooltip.tsx +3 -3
  702. package/src/components/ui/tu-do/boards/__tests__/quick-create-board-dialog.test.tsx +52 -0
  703. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +130 -0
  704. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +100 -0
  705. package/src/components/ui/tu-do/boards/analytics/GanttChart.tsx +419 -0
  706. package/src/components/ui/tu-do/boards/analytics/GanttControls.tsx +123 -0
  707. package/src/components/ui/tu-do/boards/analytics/GanttHeader.tsx +48 -0
  708. package/src/components/ui/tu-do/boards/analytics/GanttTimeline.tsx +355 -0
  709. package/src/components/ui/tu-do/boards/analytics/StatusDistribution.tsx +130 -0
  710. package/src/components/ui/tu-do/boards/analytics/TaskCreationAnalytics.tsx +285 -0
  711. package/src/components/ui/tu-do/boards/analytics/TaskDetailCard.tsx +270 -0
  712. package/src/components/ui/tu-do/boards/analytics/TaskGroup.tsx +282 -0
  713. package/src/components/ui/tu-do/boards/analytics/TaskWorkflowAnalytics.tsx +280 -0
  714. package/src/components/ui/tu-do/boards/board-selector.tsx +301 -0
  715. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +280 -0
  716. package/src/components/ui/tu-do/boards/boardId/__tests__/list-actions.test.tsx +199 -0
  717. package/src/components/ui/tu-do/boards/boardId/board-column-initial-load.test.ts +38 -0
  718. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +755 -0
  719. package/src/components/ui/tu-do/boards/boardId/board-text-utils.test.ts +24 -0
  720. package/src/components/ui/tu-do/boards/boardId/board-text-utils.ts +11 -0
  721. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +536 -0
  722. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +176 -0
  723. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +94 -0
  724. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-menu.tsx +689 -0
  725. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-custom-date-dialog.tsx +82 -0
  726. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-delete-dialog.tsx +58 -0
  727. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +471 -0
  728. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +657 -0
  729. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +429 -0
  730. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +294 -0
  731. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +316 -0
  732. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations.ts +12 -0
  733. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +499 -0
  734. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-i18n.ts +524 -0
  735. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +34 -0
  736. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +60 -0
  737. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +312 -0
  738. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-personal-external-move.ts +132 -0
  739. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +42 -0
  740. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-applied-sets.ts +79 -0
  741. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +58 -0
  742. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-filtered-resources.ts +54 -0
  743. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +63 -0
  744. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.ts +86 -0
  745. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +62 -0
  746. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +98 -0
  747. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/kanban-sort-helpers.ts +76 -0
  748. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +325 -0
  749. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-geometry.test.ts +67 -0
  750. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-geometry.ts +94 -0
  751. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +156 -0
  752. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-pending.test.ts +37 -0
  753. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-pending.ts +42 -0
  754. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-preview.test.ts +318 -0
  755. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-preview.ts +260 -0
  756. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-types.ts +39 -0
  757. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +686 -0
  758. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +1503 -0
  759. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-constants.ts +43 -0
  760. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-column-width.ts +30 -0
  761. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +383 -0
  762. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +228 -0
  763. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +221 -0
  764. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-tasks.test.ts +267 -0
  765. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-tasks.ts +112 -0
  766. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +45 -0
  767. package/src/components/ui/tu-do/boards/boardId/kanban/selection/use-keyboard-shortcuts.ts +137 -0
  768. package/src/components/ui/tu-do/boards/boardId/kanban/selection/use-multi-select.ts +119 -0
  769. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +495 -0
  770. package/src/components/ui/tu-do/boards/boardId/list-actions.tsx +522 -0
  771. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-estimation-menu.test.tsx +261 -0
  772. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +491 -0
  773. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +11 -0
  774. package/src/components/ui/tu-do/boards/boardId/menus/task-assignees-menu.tsx +176 -0
  775. package/src/components/ui/tu-do/boards/boardId/menus/task-blocking-menu.tsx +304 -0
  776. package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +180 -0
  777. package/src/components/ui/tu-do/boards/boardId/menus/task-estimation-menu.tsx +103 -0
  778. package/src/components/ui/tu-do/boards/boardId/menus/task-labels-menu.tsx +134 -0
  779. package/src/components/ui/tu-do/boards/boardId/menus/task-move-menu.tsx +66 -0
  780. package/src/components/ui/tu-do/boards/boardId/menus/task-parent-menu.tsx +228 -0
  781. package/src/components/ui/tu-do/boards/boardId/menus/task-picker-popover.tsx +300 -0
  782. package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +176 -0
  783. package/src/components/ui/tu-do/boards/boardId/menus/task-projects-menu.tsx +143 -0
  784. package/src/components/ui/tu-do/boards/boardId/menus/task-related-menu.tsx +242 -0
  785. package/src/components/ui/tu-do/boards/boardId/status-grouped-board.tsx +533 -0
  786. package/src/components/ui/tu-do/boards/boardId/status-section.tsx +274 -0
  787. package/src/components/ui/tu-do/boards/boardId/task-actions.tsx +675 -0
  788. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +41 -0
  789. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +47 -0
  790. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +106 -0
  791. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardHeader.tsx +97 -0
  792. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardMetadata.tsx +152 -0
  793. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +109 -0
  794. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +61 -0
  795. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +87 -0
  796. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-label-options.ts +24 -0
  797. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-visibility.test.ts +41 -0
  798. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-visibility.ts +14 -0
  799. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +2543 -0
  800. package/src/components/ui/tu-do/boards/boardId/task-dialogs/BoardEstimationConfigDialog.tsx +499 -0
  801. package/src/components/ui/tu-do/boards/boardId/task-dialogs/TaskCustomDateDialog.tsx +109 -0
  802. package/src/components/ui/tu-do/boards/boardId/task-dialogs/TaskDeleteDialog.tsx +96 -0
  803. package/src/components/ui/tu-do/boards/boardId/task-dialogs/TaskNewLabelDialog.tsx +178 -0
  804. package/src/components/ui/tu-do/boards/boardId/task-dialogs/TaskNewProjectDialog.tsx +127 -0
  805. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +986 -0
  806. package/src/components/ui/tu-do/boards/boardId/task-form.tsx +498 -0
  807. package/src/components/ui/tu-do/boards/boardId/task-list-drag-preview.test.ts +101 -0
  808. package/src/components/ui/tu-do/boards/boardId/task-list-form.tsx +91 -0
  809. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +513 -0
  810. package/src/components/ui/tu-do/boards/boardId/task-parent-badge-state.test.ts +64 -0
  811. package/src/components/ui/tu-do/boards/boardId/task-parent-badge-state.ts +31 -0
  812. package/src/components/ui/tu-do/boards/boardId/task.tsx +2 -0
  813. package/src/components/ui/tu-do/boards/boardId/timeline/task-edit-dialog.tsx +109 -0
  814. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +94 -0
  815. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +380 -0
  816. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +350 -0
  817. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +347 -0
  818. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +134 -0
  819. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +468 -0
  820. package/src/components/ui/tu-do/boards/boardId/timeline-board.test.tsx +217 -0
  821. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +793 -0
  822. package/src/components/ui/tu-do/boards/boards-list-skeleton.tsx +62 -0
  823. package/src/components/ui/tu-do/boards/columns.tsx +159 -0
  824. package/src/components/ui/tu-do/boards/copy-board-dialog.tsx +156 -0
  825. package/src/components/ui/tu-do/boards/enhanced-boards-view.tsx +347 -0
  826. package/src/components/ui/tu-do/boards/form.tsx +264 -0
  827. package/src/components/ui/tu-do/boards/quick-create-board-dialog.tsx +75 -0
  828. package/src/components/ui/tu-do/boards/row-actions.tsx +397 -0
  829. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +182 -0
  830. package/src/components/ui/tu-do/boards/workspace-projects-page.tsx +155 -0
  831. package/src/components/ui/tu-do/cycles/task-cycles-client.tsx +744 -0
  832. package/src/components/ui/tu-do/cycles/task-cycles-page.tsx +103 -0
  833. package/src/components/ui/tu-do/drafts/draft-card.tsx +204 -0
  834. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +197 -0
  835. package/src/components/ui/tu-do/drafts/drafts-page.tsx +105 -0
  836. package/src/components/ui/tu-do/drafts/task-drafts-page.tsx +90 -0
  837. package/src/components/ui/tu-do/estimates/client.tsx +292 -0
  838. package/src/components/ui/tu-do/estimates/edit-estimation-dialog.tsx +374 -0
  839. package/src/components/ui/tu-do/estimates/task-estimates-page.tsx +57 -0
  840. package/src/components/ui/tu-do/estimates/use-task-estimates.ts +319 -0
  841. package/src/components/ui/tu-do/habits/client.tsx +260 -0
  842. package/src/components/ui/tu-do/habits/habit-card.tsx +183 -0
  843. package/src/components/ui/tu-do/habits/habit-form-dialog.tsx +701 -0
  844. package/src/components/ui/tu-do/habits/habits-page.tsx +27 -0
  845. package/src/components/ui/tu-do/hooks/__tests__/useDraftPersistence.test.ts +494 -0
  846. package/src/components/ui/tu-do/hooks/__tests__/useTaskDialog.test.tsx +127 -0
  847. package/src/components/ui/tu-do/hooks/__tests__/useTaskDialogState.test.ts +217 -0
  848. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +524 -0
  849. package/src/components/ui/tu-do/hooks/useDraftPersistence.ts +199 -0
  850. package/src/components/ui/tu-do/hooks/useTaskCardRelationships.ts +340 -0
  851. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +101 -0
  852. package/src/components/ui/tu-do/hooks/useTaskDialogState.ts +145 -0
  853. package/src/components/ui/tu-do/hooks/useTaskFormState.ts +206 -0
  854. package/src/components/ui/tu-do/hooks/useTaskKeyboardShortcuts.ts +58 -0
  855. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +416 -0
  856. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +461 -0
  857. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +921 -0
  858. package/src/components/ui/tu-do/initiatives/task-initiatives-page.tsx +116 -0
  859. package/src/components/ui/tu-do/labels/client.tsx +201 -0
  860. package/src/components/ui/tu-do/labels/components/delete-label-dialog.tsx +57 -0
  861. package/src/components/ui/tu-do/labels/components/label-card.tsx +86 -0
  862. package/src/components/ui/tu-do/labels/components/label-dialog.tsx +259 -0
  863. package/src/components/ui/tu-do/labels/components/label-list.tsx +83 -0
  864. package/src/components/ui/tu-do/labels/hooks/use-task-labels.ts +170 -0
  865. package/src/components/ui/tu-do/labels/task-labels-page.tsx +53 -0
  866. package/src/components/ui/tu-do/labels/types.ts +6 -0
  867. package/src/components/ui/tu-do/logs/columns.tsx +755 -0
  868. package/src/components/ui/tu-do/logs/logs-client.tsx +1006 -0
  869. package/src/components/ui/tu-do/logs/logs-timeline.tsx +2483 -0
  870. package/src/components/ui/tu-do/logs/task-logs-page.tsx +59 -0
  871. package/src/components/ui/tu-do/my-tasks/__tests__/my-tasks-content.test.tsx +408 -0
  872. package/src/components/ui/tu-do/my-tasks/__tests__/use-my-tasks-query.test.ts +469 -0
  873. package/src/components/ui/tu-do/my-tasks/__tests__/use-my-tasks-state.test.ts +1017 -0
  874. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +516 -0
  875. package/src/components/ui/tu-do/my-tasks/ai-credit-indicator.tsx +143 -0
  876. package/src/components/ui/tu-do/my-tasks/board-selector-dialog.tsx +239 -0
  877. package/src/components/ui/tu-do/my-tasks/command-bar.tsx +1351 -0
  878. package/src/components/ui/tu-do/my-tasks/label-project-filter.tsx +194 -0
  879. package/src/components/ui/tu-do/my-tasks/my-task-context-menu.tsx +356 -0
  880. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +304 -0
  881. package/src/components/ui/tu-do/my-tasks/my-tasks-data-loader.tsx +17 -0
  882. package/src/components/ui/tu-do/my-tasks/my-tasks-filters.tsx +258 -0
  883. package/src/components/ui/tu-do/my-tasks/my-tasks-header.tsx +121 -0
  884. package/src/components/ui/tu-do/my-tasks/my-tasks-page.tsx +40 -0
  885. package/src/components/ui/tu-do/my-tasks/personal-placement-dialog.tsx +188 -0
  886. package/src/components/ui/tu-do/my-tasks/task-filter.tsx +235 -0
  887. package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +782 -0
  888. package/src/components/ui/tu-do/my-tasks/task-list.tsx +258 -0
  889. package/src/components/ui/tu-do/my-tasks/task-preview-dialog.tsx +1597 -0
  890. package/src/components/ui/tu-do/my-tasks/task-section.tsx +261 -0
  891. package/src/components/ui/tu-do/my-tasks/use-my-tasks-query.ts +140 -0
  892. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +983 -0
  893. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +332 -0
  894. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +121 -0
  895. package/src/components/ui/tu-do/notes/note-list.tsx +799 -0
  896. package/src/components/ui/tu-do/notes/notes-content.tsx +178 -0
  897. package/src/components/ui/tu-do/notes/notes-page.tsx +27 -0
  898. package/src/components/ui/tu-do/projects/components/index.ts +11 -0
  899. package/src/components/ui/tu-do/projects/components/project-actions-menu.tsx +66 -0
  900. package/src/components/ui/tu-do/projects/components/project-badges.tsx +84 -0
  901. package/src/components/ui/tu-do/projects/components/project-filter-menu.tsx +170 -0
  902. package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +182 -0
  903. package/src/components/ui/tu-do/projects/components/project-list-item.tsx +179 -0
  904. package/src/components/ui/tu-do/projects/components/project-metrics.tsx +87 -0
  905. package/src/components/ui/tu-do/projects/components/project-progress-meter.tsx +33 -0
  906. package/src/components/ui/tu-do/projects/components/project-sort-menu.tsx +71 -0
  907. package/src/components/ui/tu-do/projects/components/projects-empty-state.tsx +38 -0
  908. package/src/components/ui/tu-do/projects/components/projects-loading-state.tsx +26 -0
  909. package/src/components/ui/tu-do/projects/components/projects-toolbar.tsx +179 -0
  910. package/src/components/ui/tu-do/projects/dialogs/create-project-dialog.tsx +117 -0
  911. package/src/components/ui/tu-do/projects/dialogs/edit-project-dialog.tsx +125 -0
  912. package/src/components/ui/tu-do/projects/dialogs/index.ts +3 -0
  913. package/src/components/ui/tu-do/projects/dialogs/manage-tasks-dialog.tsx +205 -0
  914. package/src/components/ui/tu-do/projects/hooks/index.ts +2 -0
  915. package/src/components/ui/tu-do/projects/hooks/use-project-filters.ts +248 -0
  916. package/src/components/ui/tu-do/projects/hooks/use-task-projects.ts +238 -0
  917. package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +163 -0
  918. package/src/components/ui/tu-do/projects/projectId/components/documents-tab.tsx +90 -0
  919. package/src/components/ui/tu-do/projects/projectId/components/index.ts +9 -0
  920. package/src/components/ui/tu-do/projects/projectId/components/overview/description-card.tsx +77 -0
  921. package/src/components/ui/tu-do/projects/projectId/components/overview/linked-documents-card.tsx +81 -0
  922. package/src/components/ui/tu-do/projects/projectId/components/overview/linked-tasks-card.tsx +85 -0
  923. package/src/components/ui/tu-do/projects/projectId/components/overview/updates-card.tsx +85 -0
  924. package/src/components/ui/tu-do/projects/projectId/components/overview-tab.tsx +35 -0
  925. package/src/components/ui/tu-do/projects/projectId/components/project-configuration.tsx +229 -0
  926. package/src/components/ui/tu-do/projects/projectId/components/project-header.tsx +115 -0
  927. package/src/components/ui/tu-do/projects/projectId/components/project-lead-selector.tsx +55 -0
  928. package/src/components/ui/tu-do/projects/projectId/components/project-overview-context.tsx +82 -0
  929. package/src/components/ui/tu-do/projects/projectId/components/project-sidebar.tsx +246 -0
  930. package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +264 -0
  931. package/src/components/ui/tu-do/projects/projectId/components/update-card.tsx +162 -0
  932. package/src/components/ui/tu-do/projects/projectId/components/updates-tab.tsx +129 -0
  933. package/src/components/ui/tu-do/projects/projectId/dialogs/index.ts +1 -0
  934. package/src/components/ui/tu-do/projects/projectId/dialogs/link-task-dialog.tsx +114 -0
  935. package/src/components/ui/tu-do/projects/projectId/hooks/index.ts +4 -0
  936. package/src/components/ui/tu-do/projects/projectId/hooks/use-animation-variants.ts +68 -0
  937. package/src/components/ui/tu-do/projects/projectId/hooks/use-project-form.ts +214 -0
  938. package/src/components/ui/tu-do/projects/projectId/hooks/use-project-updates.ts +209 -0
  939. package/src/components/ui/tu-do/projects/projectId/hooks/use-task-linking.ts +101 -0
  940. package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +85 -0
  941. package/src/components/ui/tu-do/projects/projectId/task-project-detail-page.tsx +78 -0
  942. package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +365 -0
  943. package/src/components/ui/tu-do/projects/projectId/types.ts +50 -0
  944. package/src/components/ui/tu-do/projects/task-projects-client.tsx +226 -0
  945. package/src/components/ui/tu-do/projects/task-projects-page.tsx +97 -0
  946. package/src/components/ui/tu-do/projects/types.ts +59 -0
  947. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +404 -0
  948. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +721 -0
  949. package/src/components/ui/tu-do/providers/workspace-presence-provider.tsx +88 -0
  950. package/src/components/ui/tu-do/shared/AccessibleButton.tsx +79 -0
  951. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +72 -0
  952. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +152 -0
  953. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +172 -0
  954. package/src/components/ui/tu-do/shared/__tests__/board-query-cache.test.ts +219 -0
  955. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +613 -0
  956. package/src/components/ui/tu-do/shared/__tests__/create-list-dialog.test.tsx +118 -0
  957. package/src/components/ui/tu-do/shared/__tests__/task-detail-page.test.tsx +170 -0
  958. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +708 -0
  959. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +85 -0
  960. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +588 -0
  961. package/src/components/ui/tu-do/shared/assignee-select.tsx +541 -0
  962. package/src/components/ui/tu-do/shared/board-broadcast-context.tsx +45 -0
  963. package/src/components/ui/tu-do/shared/board-client.tsx +268 -0
  964. package/src/components/ui/tu-do/shared/board-config-storage.ts +88 -0
  965. package/src/components/ui/tu-do/shared/board-header.tsx +1248 -0
  966. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +1105 -0
  967. package/src/components/ui/tu-do/shared/board-query-cache.ts +157 -0
  968. package/src/components/ui/tu-do/shared/board-switcher.tsx +298 -0
  969. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +572 -0
  970. package/src/components/ui/tu-do/shared/board-views.tsx +847 -0
  971. package/src/components/ui/tu-do/shared/clear-menu-item.tsx +34 -0
  972. package/src/components/ui/tu-do/shared/create-list-dialog.tsx +401 -0
  973. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +204 -0
  974. package/src/components/ui/tu-do/shared/cursor-overlay.tsx +78 -0
  975. package/src/components/ui/tu-do/shared/custom-date-picker/custom-date-picker-dialog.tsx +157 -0
  976. package/src/components/ui/tu-do/shared/description-overflow-warning-dialog.tsx +68 -0
  977. package/src/components/ui/tu-do/shared/edit-list-dialog.tsx +383 -0
  978. package/src/components/ui/tu-do/shared/empty-state-card.tsx +46 -0
  979. package/src/components/ui/tu-do/shared/estimation-mapping.test.ts +32 -0
  980. package/src/components/ui/tu-do/shared/estimation-mapping.ts +74 -0
  981. package/src/components/ui/tu-do/shared/estimation-utils.ts +54 -0
  982. package/src/components/ui/tu-do/shared/fade-setting-initializer.tsx +59 -0
  983. package/src/components/ui/tu-do/shared/label-chip.tsx +46 -0
  984. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +150 -0
  985. package/src/components/ui/tu-do/shared/list-view-sorting.test.ts +50 -0
  986. package/src/components/ui/tu-do/shared/list-view-sorting.ts +108 -0
  987. package/src/components/ui/tu-do/shared/list-view.tsx +987 -0
  988. package/src/components/ui/tu-do/shared/mention-system/__tests__/mention-system.test.ts +250 -0
  989. package/src/components/ui/tu-do/shared/mention-system/mention-menu.tsx +217 -0
  990. package/src/components/ui/tu-do/shared/mention-system/types.ts +141 -0
  991. package/src/components/ui/tu-do/shared/mention-system/use-mention-suggestions.ts +244 -0
  992. package/src/components/ui/tu-do/shared/progressive-loader-context.tsx +45 -0
  993. package/src/components/ui/tu-do/shared/recent-sidebar-events.ts +58 -0
  994. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +603 -0
  995. package/src/components/ui/tu-do/shared/relationship-task-identifier.ts +14 -0
  996. package/src/components/ui/tu-do/shared/slash-commands/__tests__/slash-commands.test.ts +315 -0
  997. package/src/components/ui/tu-do/shared/slash-commands/definitions.ts +167 -0
  998. package/src/components/ui/tu-do/shared/slash-commands/slash-command-menu.tsx +114 -0
  999. package/src/components/ui/tu-do/shared/sync-warning-dialog.tsx +133 -0
  1000. package/src/components/ui/tu-do/shared/task-board-errors.ts +19 -0
  1001. package/src/components/ui/tu-do/shared/task-detail-page.tsx +176 -0
  1002. package/src/components/ui/tu-do/shared/task-detail-server-page.tsx +37 -0
  1003. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +557 -0
  1004. package/src/components/ui/tu-do/shared/task-dialog-wrapper.tsx +28 -0
  1005. package/src/components/ui/tu-do/shared/task-edit-dialog/components/mobile-floating-save-button.tsx +46 -0
  1006. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +164 -0
  1007. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +491 -0
  1008. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +511 -0
  1009. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-picker-panel.tsx +220 -0
  1010. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +127 -0
  1011. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-trigger-styles.ts +69 -0
  1012. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +140 -0
  1013. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +163 -0
  1014. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-suggestion-menus.tsx +148 -0
  1015. package/src/components/ui/tu-do/shared/task-edit-dialog/constants.ts +18 -0
  1016. package/src/components/ui/tu-do/shared/task-edit-dialog/context-menu-guard.test.ts +37 -0
  1017. package/src/components/ui/tu-do/shared/task-edit-dialog/description-diff-viewer.tsx +1665 -0
  1018. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +715 -0
  1019. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-dialog-close.test.ts +218 -0
  1020. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-dialog-keyboard-shortcuts.test.ts +140 -0
  1021. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-overrides.test.ts +412 -0
  1022. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +237 -0
  1023. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-yjs-sync.test.ts +190 -0
  1024. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-update-shared-task.test.ts +103 -0
  1025. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +127 -0
  1026. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-editor-commands.ts +314 -0
  1027. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-suggestion-menus.ts +503 -0
  1028. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-change-detection.ts +175 -0
  1029. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +332 -0
  1030. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-dependencies.ts +773 -0
  1031. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-dialog-close.ts +193 -0
  1032. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-dialog-keyboard-shortcuts.ts +365 -0
  1033. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +215 -0
  1034. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-state.ts +307 -0
  1035. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +536 -0
  1036. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-overrides.ts +122 -0
  1037. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +271 -0
  1038. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +149 -0
  1039. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +545 -0
  1040. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-revert.ts +140 -0
  1041. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +206 -0
  1042. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +1450 -0
  1043. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-snapshot.ts +59 -0
  1044. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-yjs-sync.ts +224 -0
  1045. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-update-shared-task.ts +55 -0
  1046. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +348 -0
  1047. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/components/clickable-task-item.tsx +90 -0
  1048. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/components/index.ts +4 -0
  1049. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/components/subtask-action-buttons.tsx +140 -0
  1050. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/components/tab-button.tsx +76 -0
  1051. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/components/task-relationship-action-buttons.tsx +178 -0
  1052. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +129 -0
  1053. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/index.ts +35 -0
  1054. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +65 -0
  1055. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +76 -0
  1056. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +82 -0
  1057. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/task-search-popover.tsx +229 -0
  1058. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +186 -0
  1059. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +320 -0
  1060. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +748 -0
  1061. package/src/components/ui/tu-do/shared/task-edit-dialog/task-delete-dialog.tsx +104 -0
  1062. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +213 -0
  1063. package/src/components/ui/tu-do/shared/task-edit-dialog/task-instances-section.tsx +173 -0
  1064. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +1895 -0
  1065. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +273 -0
  1066. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +162 -0
  1067. package/src/components/ui/tu-do/shared/task-edit-dialog/types/pending-relationship.test.ts +52 -0
  1068. package/src/components/ui/tu-do/shared/task-edit-dialog/types/pending-relationship.ts +134 -0
  1069. package/src/components/ui/tu-do/shared/task-edit-dialog/types.ts +64 -0
  1070. package/src/components/ui/tu-do/shared/task-edit-dialog/user-display.test.ts +58 -0
  1071. package/src/components/ui/tu-do/shared/task-edit-dialog/user-display.ts +45 -0
  1072. package/src/components/ui/tu-do/shared/task-edit-dialog/utils/inline-task-target-list.test.ts +98 -0
  1073. package/src/components/ui/tu-do/shared/task-edit-dialog/utils/inline-task-target-list.ts +39 -0
  1074. package/src/components/ui/tu-do/shared/task-edit-dialog/utils.test.ts +404 -0
  1075. package/src/components/ui/tu-do/shared/task-edit-dialog/utils.ts +473 -0
  1076. package/src/components/ui/tu-do/shared/task-edit-dialog/yjs-prosemirror-compat.test.ts +65 -0
  1077. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2168 -0
  1078. package/src/components/ui/tu-do/shared/task-estimation-display.tsx +52 -0
  1079. package/src/components/ui/tu-do/shared/task-estimation-picker.tsx +223 -0
  1080. package/src/components/ui/tu-do/shared/task-filter.types.ts +65 -0
  1081. package/src/components/ui/tu-do/shared/task-label-selector.tsx +217 -0
  1082. package/src/components/ui/tu-do/shared/task-labels-display.test.tsx +51 -0
  1083. package/src/components/ui/tu-do/shared/task-labels-display.tsx +119 -0
  1084. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +72 -0
  1085. package/src/components/ui/tu-do/shared/task-open-events.ts +94 -0
  1086. package/src/components/ui/tu-do/shared/task-projects-display.tsx +80 -0
  1087. package/src/components/ui/tu-do/shared/task-resource-search-field.tsx +49 -0
  1088. package/src/components/ui/tu-do/shared/task-resource-search-filters.ts +27 -0
  1089. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +352 -0
  1090. package/src/components/ui/tu-do/shared/task-share-dialog/components/share-link-settings.tsx +86 -0
  1091. package/src/components/ui/tu-do/shared/task-share-dialog/components/shares-list.tsx +85 -0
  1092. package/src/components/ui/tu-do/shared/task-share-dialog/hooks/use-task-sharing.ts +296 -0
  1093. package/src/components/ui/tu-do/shared/task-share-dialog.tsx +144 -0
  1094. package/src/components/ui/tu-do/shared/task-url.ts +56 -0
  1095. package/src/components/ui/tu-do/shared/text-diff-viewer.tsx +55 -0
  1096. package/src/components/ui/tu-do/shared/types.ts +14 -0
  1097. package/src/components/ui/tu-do/shared/unsaved-changes-warning-dialog.tsx +71 -0
  1098. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +310 -0
  1099. package/src/components/ui/tu-do/shared/user-avatar.tsx +46 -0
  1100. package/src/components/ui/tu-do/shared/user-presence-avatars.tsx +415 -0
  1101. package/src/components/ui/tu-do/shared/utils/translate-task-list-display-name.ts +28 -0
  1102. package/src/components/ui/tu-do/tasks-route-context.tsx +50 -0
  1103. package/src/components/ui/tu-do/templates/client.tsx +486 -0
  1104. package/src/components/ui/tu-do/templates/marketplace/client.tsx +451 -0
  1105. package/src/components/ui/tu-do/templates/marketplace/task-marketplace-page.tsx +169 -0
  1106. package/src/components/ui/tu-do/templates/save-as-template-dialog.tsx +381 -0
  1107. package/src/components/ui/tu-do/templates/task-templates-page.tsx +162 -0
  1108. package/src/components/ui/tu-do/templates/templateId/client.tsx +659 -0
  1109. package/src/components/ui/tu-do/templates/templateId/edit-template-dialog.tsx +300 -0
  1110. package/src/components/ui/tu-do/templates/templateId/share-template-dialog.tsx +261 -0
  1111. package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +121 -0
  1112. package/src/components/ui/tu-do/templates/templateId/task-template-detail-page.tsx +89 -0
  1113. package/src/components/ui/tu-do/templates/templateId/use-template-dialog.tsx +165 -0
  1114. package/src/components/ui/tu-do/templates/types.ts +53 -0
  1115. package/src/components/ui/tu-do/utils/__tests__/label-colors.test.ts +43 -0
  1116. package/src/components/ui/tu-do/utils/__tests__/taskColorUtils.test.ts +267 -0
  1117. package/src/components/ui/tu-do/utils/__tests__/taskConstants.test.ts +178 -0
  1118. package/src/components/ui/tu-do/utils/__tests__/taskDateUtils.test.ts +226 -0
  1119. package/src/components/ui/tu-do/utils/__tests__/weekDateUtils.test.ts +182 -0
  1120. package/src/components/ui/tu-do/utils/label-colors.test.ts +15 -0
  1121. package/src/components/ui/tu-do/utils/label-colors.ts +59 -0
  1122. package/src/components/ui/tu-do/utils/taskColorUtils.ts +116 -0
  1123. package/src/components/ui/tu-do/utils/taskConstants.ts +102 -0
  1124. package/src/components/ui/tu-do/utils/taskDateUtils.ts +81 -0
  1125. package/src/components/ui/tu-do/utils/taskPriorityUtils.tsx +80 -0
  1126. package/src/components/ui/tu-do/utils/weekDateUtils.ts +64 -0
  1127. package/src/constants/boards.ts +9 -0
  1128. package/src/declarations.d.ts +2 -0
  1129. package/src/globals.css +609 -200
  1130. package/src/gsap.ts +2 -0
  1131. package/src/hooks/__tests__/supabase-provider.test.ts +346 -0
  1132. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +90 -0
  1133. package/src/hooks/__tests__/use-forwarded-ref.test.tsx +127 -0
  1134. package/src/hooks/__tests__/use-local-storage.test.tsx +243 -0
  1135. package/src/hooks/__tests__/use-mobile.test.tsx +222 -0
  1136. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +163 -0
  1137. package/src/hooks/__tests__/use-notifications.test.ts +59 -0
  1138. package/src/hooks/__tests__/use-stable-array.test.tsx +214 -0
  1139. package/src/hooks/__tests__/use-task-actions.test.tsx +1893 -0
  1140. package/src/hooks/__tests__/use-workspace-presence.test.tsx +217 -0
  1141. package/src/hooks/__tests__/useBoardRealtime.test.tsx +1156 -0
  1142. package/src/hooks/supabase-provider.ts +725 -0
  1143. package/src/hooks/task-actions-personal-external.ts +308 -0
  1144. package/src/hooks/time-blocking-provider.tsx +756 -0
  1145. package/src/hooks/use-ai-credits.ts +78 -0
  1146. package/src/hooks/use-analytics-filters.ts +269 -0
  1147. package/src/hooks/use-at-bottom.tsx +64 -0
  1148. package/src/hooks/use-board-actions.ts +213 -0
  1149. package/src/hooks/use-calendar-preferences.tsx +24 -0
  1150. package/src/hooks/use-calendar-sync.tsx +1184 -0
  1151. package/src/hooks/use-calendar.tsx +1972 -0
  1152. package/src/hooks/use-callback-ref.ts +1 -1
  1153. package/src/hooks/use-controllable-state.ts +3 -4
  1154. package/src/hooks/use-copy-to-clipboard.ts +1 -3
  1155. package/src/hooks/use-currency-formatter.ts +81 -0
  1156. package/src/hooks/use-debounce.ts +15 -0
  1157. package/src/hooks/use-dom-resolved-theme.ts +81 -0
  1158. package/src/hooks/use-enter-submit.tsx +23 -0
  1159. package/src/hooks/use-exchange-rates.ts +23 -0
  1160. package/src/hooks/use-finance-transaction-preferences.ts +58 -0
  1161. package/src/hooks/use-form.ts +8 -6
  1162. package/src/hooks/use-interest-preferences.ts +104 -0
  1163. package/src/hooks/use-local-storage.ts +42 -0
  1164. package/src/hooks/use-notifications.ts +704 -0
  1165. package/src/hooks/use-page-visibility.ts +27 -0
  1166. package/src/hooks/use-popover-manager.tsx +98 -0
  1167. package/src/hooks/use-semantic-task-search.ts +76 -0
  1168. package/src/hooks/use-session-history-query.ts +1 -0
  1169. package/src/hooks/use-stable-array.ts +23 -0
  1170. package/src/hooks/use-task-actions.ts +1915 -0
  1171. package/src/hooks/use-task-analytics.ts +354 -0
  1172. package/src/hooks/use-toast.ts +2 -3
  1173. package/src/hooks/use-user-config.ts +109 -0
  1174. package/src/hooks/use-view-transition.ts +69 -0
  1175. package/src/hooks/use-workspace-config.ts +49 -0
  1176. package/src/hooks/use-workspace-currency.ts +26 -0
  1177. package/src/hooks/use-workspace-members.ts +58 -0
  1178. package/src/hooks/use-workspace-permission.ts +61 -0
  1179. package/src/hooks/use-workspace-presence.ts +554 -0
  1180. package/src/hooks/use-workspace-user.ts +33 -0
  1181. package/src/hooks/use-yjs-collaboration.ts +297 -0
  1182. package/src/hooks/useBoardRealtime.ts +372 -0
  1183. package/src/hooks/useBoardRealtime.types.ts +45 -0
  1184. package/src/hooks/useBoardRealtimeEventHandler.ts +332 -0
  1185. package/src/hooks/useCursorTracking.ts +338 -0
  1186. package/src/hooks/useHorizontalScroll.ts +215 -0
  1187. package/src/hooks/usePresence.ts +272 -0
  1188. package/src/hooks/useSearchParams.tsx +133 -0
  1189. package/src/lib/__tests__/lunar-calendar.test.ts +137 -0
  1190. package/src/lib/calendar/planning-query-client.ts +197 -0
  1191. package/src/lib/calendar/preview-engine.ts +14 -0
  1192. package/src/lib/calendar-settings-resolver.ts +200 -0
  1193. package/src/lib/lunar-calendar.ts +71 -0
  1194. package/src/lib/template-background.ts +159 -0
  1195. package/src/lib/workspace-actions.ts +457 -0
  1196. package/src/utils/__tests__/label-colors.test.ts +219 -0
  1197. package/src/utils/__tests__/priority-styles.test.ts +78 -0
  1198. package/src/utils/label-colors.ts +4 -0
  1199. package/src/utils/priority-styles.ts +42 -0
  1200. package/src/xlsx.ts +3 -0
  1201. package/tsconfig.json +3 -11
  1202. package/tsconfig.typecheck.json +9 -0
  1203. package/vendor/xlsx-0.20.3.tgz +0 -0
  1204. package/vitest.config.ts +42 -0
  1205. package/vitest.setup.ts +37 -0
  1206. package/eslint.config.mjs +0 -20
  1207. package/rollup.config.js +0 -40
  1208. package/src/components/ui/icons.tsx +0 -506
  1209. package/src/components/ui/tag-input.tsx +0 -141
  1210. /package/src/hooks/{use-mobile.tsx → use-mobile.ts} +0 -0
@@ -0,0 +1,1972 @@
1
+ import { createWorkspaceCalendarEvent } from '@tuturuuu/internal-api';
2
+ import { createClient } from '@tuturuuu/supabase/next/client';
3
+ import type {
4
+ Workspace,
5
+ WorkspaceCalendarGoogleTokenClient,
6
+ } from '@tuturuuu/types';
7
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
8
+ import type { SupportedColor } from '@tuturuuu/types/primitives/SupportedColors';
9
+ import {
10
+ createAllDayEvent,
11
+ isAllDayEvent,
12
+ } from '@tuturuuu/utils/calendar-utils';
13
+ import dayjs from 'dayjs';
14
+ import moment from 'moment';
15
+ import 'moment/locale/vi';
16
+ import {
17
+ createContext,
18
+ type ReactNode,
19
+ useCallback,
20
+ useContext,
21
+ useEffect,
22
+ useMemo,
23
+ useRef,
24
+ useState,
25
+ } from 'react';
26
+ import { toast } from '../components/ui/sonner';
27
+ import { useCalendarSync } from './use-calendar-sync';
28
+
29
+ // Utility function to round time to nearest 15-minute interval
30
+ const roundToNearest15Minutes = (date: Date): Date => {
31
+ const minutes = date.getMinutes();
32
+ const remainder = minutes % 15;
33
+ const roundedMinutes =
34
+ remainder < 8 ? minutes - remainder : minutes + (15 - remainder);
35
+ const roundedDate = new Date(date);
36
+ roundedDate.setMinutes(roundedMinutes);
37
+ roundedDate.setSeconds(0);
38
+ roundedDate.setMilliseconds(0);
39
+ return roundedDate;
40
+ };
41
+
42
+ // Function to create a unique signature for an event based on its content
43
+ const createEventSignature = (event: CalendarEvent): string => {
44
+ return `${event.title}|${event.description || ''}|${event.start_at}|${event.end_at}`;
45
+ };
46
+
47
+ type TaskDragData = {
48
+ name?: string;
49
+ priority?: string | null;
50
+ totalDuration?: number;
51
+ };
52
+
53
+ function patchWorkspaceCalendarEventCache(
54
+ queryClient: any,
55
+ wsId: string,
56
+ updater: (events: CalendarEvent[]) => CalendarEvent[]
57
+ ) {
58
+ queryClient.setQueriesData(
59
+ { queryKey: ['databaseCalendarEvents', wsId], exact: false },
60
+ (existing: CalendarEvent[] | null | undefined) =>
61
+ updater(Array.isArray(existing) ? existing : [])
62
+ );
63
+ }
64
+
65
+ function invalidateTaskSchedulingQueries(queryClient: any, wsId?: string) {
66
+ queryClient.invalidateQueries({ queryKey: ['schedulable-tasks'] });
67
+ queryClient.invalidateQueries({ queryKey: ['scheduled-events-batch'] });
68
+ queryClient.invalidateQueries({ queryKey: ['task-schedule-batch'] });
69
+ queryClient.invalidateQueries({
70
+ queryKey: ['task-schedule-history'],
71
+ exact: false,
72
+ });
73
+ queryClient.invalidateQueries({
74
+ queryKey: ['habit-schedule-history'],
75
+ exact: false,
76
+ });
77
+
78
+ if (wsId) {
79
+ queryClient.invalidateQueries({
80
+ queryKey: ['databaseCalendarEvents', wsId],
81
+ exact: false,
82
+ });
83
+ queryClient.invalidateQueries({
84
+ queryKey: ['habits', wsId],
85
+ exact: false,
86
+ });
87
+ }
88
+ }
89
+
90
+ // Updated context with improved type definitions
91
+ const CalendarContext = createContext<{
92
+ getEvent: (eventId: string) => CalendarEvent | undefined;
93
+ getCurrentEvents: (date?: Date) => CalendarEvent[];
94
+ getUpcomingEvent: () => CalendarEvent | undefined;
95
+ getEvents: () => CalendarEvent[];
96
+ getGoogleEvents: () => CalendarEvent[];
97
+ getEventLevel: (eventId: string) => number;
98
+ addEvent: (
99
+ event: Omit<CalendarEvent, 'id'>
100
+ ) => Promise<CalendarEvent | undefined>;
101
+ addEmptyEvent: (date: Date, isAllDay?: boolean) => CalendarEvent;
102
+ addEmptyEventWithDuration: (startDate: Date, endDate: Date) => CalendarEvent;
103
+ updateEvent: (
104
+ eventId: string,
105
+ data: Partial<CalendarEvent>
106
+ ) => Promise<CalendarEvent | undefined>;
107
+ deleteEvent: (eventId: string) => Promise<void>;
108
+ isModalOpen: boolean;
109
+ activeEvent: CalendarEvent | undefined;
110
+ openModal: (
111
+ eventId?: string,
112
+ modalType?: 'all-day' | 'event',
113
+ options?: { defaultNewEventTab?: 'manual' | 'ai' }
114
+ ) => void;
115
+ closeModal: () => void;
116
+ isEditing: () => boolean;
117
+ hideModal: () => void;
118
+ showModal: () => void;
119
+ getModalStatus: (id: string) => boolean;
120
+ getActiveEvent: () => CalendarEvent | undefined;
121
+ isModalActive: () => boolean;
122
+ // google calendar API
123
+ syncGoogleCalendarNow: (
124
+ progressCallback?: (progress: {
125
+ phase: 'fetch' | 'delete' | 'update' | 'insert' | 'complete';
126
+ current: number;
127
+ total: number;
128
+ changesMade: boolean;
129
+ statusMessage?: string;
130
+ }) => void
131
+ ) => Promise<boolean>;
132
+
133
+ isDragging: boolean;
134
+ setIsDragging: (v: boolean) => void;
135
+ hoveredBaseEventId: string | null;
136
+ setHoveredBaseEventId: (id: string | null) => void;
137
+ hoveredEventColumn: number | null;
138
+ setHoveredEventColumn: (column: number | null) => void;
139
+ // Task scheduling
140
+ scheduleTaskAsEvent?: (
141
+ taskId: string,
142
+ startAt: Date,
143
+ endAt: Date,
144
+ taskData?: TaskDragData
145
+ ) => Promise<void>;
146
+ // Callback when a task is scheduled (for UI refresh)
147
+ onTaskScheduled?: () => void;
148
+ setOnTaskScheduled: (callback: (() => void) | undefined) => void;
149
+ // Preview events (for demo/preview mode)
150
+ previewEvents: CalendarEvent[];
151
+ setPreviewEvents: (events: CalendarEvent[]) => void;
152
+ clearPreviewEvents: () => void;
153
+ // Affected events (events that will be modified/deleted during preview)
154
+ affectedEventIds: Set<string>;
155
+ setAffectedEventIds: (ids: Set<string> | string[]) => void;
156
+ clearAffectedEventIds: () => void;
157
+ // Hide non-preview events during preview mode (for performance)
158
+ hideNonPreviewEvents: boolean;
159
+ setHideNonPreviewEvents: (hide: boolean) => void;
160
+ // UX: allow callers (e.g. create button) to influence default tab for *new* events.
161
+ defaultNewEventTab: 'manual' | 'ai';
162
+ readOnly: boolean;
163
+ }>({
164
+ getEvent: () => undefined,
165
+ getCurrentEvents: () => [],
166
+ getUpcomingEvent: () => undefined,
167
+ getEvents: () => [],
168
+ getGoogleEvents: () => [],
169
+ getEventLevel: () => 0,
170
+ addEvent: () => Promise.resolve({} as CalendarEvent),
171
+ addEmptyEvent: () => ({}) as CalendarEvent,
172
+ addEmptyEventWithDuration: () => ({}) as CalendarEvent,
173
+ updateEvent: () => Promise.resolve({} as CalendarEvent),
174
+ deleteEvent: () => Promise.resolve(),
175
+ isModalOpen: false,
176
+ activeEvent: undefined,
177
+ openModal: () => undefined,
178
+ closeModal: () => undefined,
179
+ isEditing: () => false,
180
+ hideModal: () => undefined,
181
+ showModal: () => undefined,
182
+ getModalStatus: () => false,
183
+ getActiveEvent: () => undefined,
184
+ isModalActive: () => false,
185
+ // Google Calendar API
186
+ syncGoogleCalendarNow: () => Promise.resolve(false),
187
+
188
+ isDragging: false,
189
+ setIsDragging: () => undefined,
190
+ hoveredBaseEventId: null,
191
+ setHoveredBaseEventId: () => undefined,
192
+ hoveredEventColumn: null,
193
+ setHoveredEventColumn: () => undefined,
194
+ // Task scheduling
195
+ scheduleTaskAsEvent: undefined,
196
+ // Callback when a task is scheduled
197
+ onTaskScheduled: undefined,
198
+ setOnTaskScheduled: () => undefined,
199
+ // Preview events
200
+ previewEvents: [],
201
+ setPreviewEvents: () => undefined,
202
+ clearPreviewEvents: () => undefined,
203
+ // Affected events
204
+ affectedEventIds: new Set<string>(),
205
+ setAffectedEventIds: () => undefined,
206
+ clearAffectedEventIds: () => undefined,
207
+ // Hide non-preview events
208
+ hideNonPreviewEvents: false,
209
+ setHideNonPreviewEvents: () => undefined,
210
+ defaultNewEventTab: 'manual',
211
+ readOnly: false,
212
+ });
213
+
214
+ // Add this interface before the updateEvent function
215
+ interface PendingEventUpdate extends Partial<CalendarEvent> {
216
+ _updateId?: string;
217
+ _timestamp: number;
218
+ _eventId: string;
219
+ _resolve?: (value: CalendarEvent) => void;
220
+ _reject?: (reason: any) => void;
221
+ }
222
+
223
+ /**
224
+ * Syncs task total_duration after a calendar event is resized or moved.
225
+ * - Uses canonical workspace calendar events as the source of truth
226
+ * - If total scheduled time exceeds task's total_duration, auto-increase it
227
+ */
228
+ async function syncTaskDurationAfterEventChange(
229
+ supabase: any,
230
+ eventId: string,
231
+ eventData: { start_at: string; end_at: string; task_id?: string | null },
232
+ options?: { calendarWsId?: string; isPersonalCalendar?: boolean }
233
+ ) {
234
+ try {
235
+ // Get task_id either from event or junction table
236
+ let taskId = eventData.task_id;
237
+
238
+ if (!taskId) {
239
+ // Try to find task from junction table
240
+ const { data: junction } = await supabase
241
+ .from('task_calendar_events')
242
+ .select('task_id')
243
+ .eq('event_id', eventId)
244
+ .single();
245
+
246
+ if (!junction?.task_id) {
247
+ // No linked task, nothing to sync
248
+ return;
249
+ }
250
+ taskId = junction.task_id;
251
+ }
252
+
253
+ const calendarWsId = options?.calendarWsId;
254
+ if (!calendarWsId) {
255
+ return;
256
+ }
257
+ const resizedEventMinutes = Math.round(
258
+ (new Date(eventData.end_at).getTime() -
259
+ new Date(eventData.start_at).getTime()) /
260
+ 60000
261
+ );
262
+
263
+ let totalScheduledMinutes = resizedEventMinutes;
264
+
265
+ const scheduleResponse = await fetch(
266
+ options?.isPersonalCalendar
267
+ ? `/api/v1/users/me/tasks/${taskId}/schedule`
268
+ : `/api/v1/workspaces/${calendarWsId}/tasks/${taskId}/schedule`,
269
+ {
270
+ cache: 'no-store',
271
+ }
272
+ );
273
+
274
+ if (scheduleResponse.ok) {
275
+ const schedulePayload = (await scheduleResponse.json()) as {
276
+ scheduling?: {
277
+ scheduledMinutes?: number;
278
+ };
279
+ };
280
+
281
+ if (
282
+ typeof schedulePayload.scheduling?.scheduledMinutes === 'number' &&
283
+ Number.isFinite(schedulePayload.scheduling.scheduledMinutes)
284
+ ) {
285
+ totalScheduledMinutes = schedulePayload.scheduling.scheduledMinutes;
286
+ }
287
+ }
288
+
289
+ const totalScheduledHours = totalScheduledMinutes / 60;
290
+
291
+ // Personal workspace calendars should not mutate shared task duration.
292
+ // Instead, keep a per-user scheduling estimate.
293
+ if (options?.isPersonalCalendar) {
294
+ const {
295
+ data: { user },
296
+ } = await supabase.auth.getUser();
297
+ if (!user?.id) return;
298
+
299
+ const { data: existing } = await supabase
300
+ .from('task_user_scheduling_settings')
301
+ .select('total_duration')
302
+ .eq('task_id', taskId)
303
+ .eq('user_id', user.id)
304
+ .maybeSingle();
305
+
306
+ const currentDuration = (existing as any)?.total_duration || 0;
307
+ if (totalScheduledHours > currentDuration) {
308
+ const { error: updateError } = await supabase
309
+ .from('task_user_scheduling_settings')
310
+ .upsert(
311
+ {
312
+ task_id: taskId,
313
+ user_id: user.id,
314
+ total_duration: totalScheduledHours,
315
+ },
316
+ { onConflict: 'task_id,user_id' }
317
+ );
318
+ if (updateError) {
319
+ console.error(
320
+ 'Failed to update personal task duration:',
321
+ updateError
322
+ );
323
+ }
324
+ }
325
+ return;
326
+ }
327
+
328
+ // Non-personal calendars also use per-user scheduling settings.
329
+ const {
330
+ data: { user },
331
+ } = await supabase.auth.getUser();
332
+ if (!user?.id) return;
333
+
334
+ const { data: existing } = await supabase
335
+ .from('task_user_scheduling_settings')
336
+ .select('total_duration')
337
+ .eq('task_id', taskId)
338
+ .eq('user_id', user.id)
339
+ .maybeSingle();
340
+
341
+ const currentDuration = (existing as any)?.total_duration || 0;
342
+ if (totalScheduledHours > currentDuration) {
343
+ const { error: updateError } = await supabase
344
+ .from('task_user_scheduling_settings')
345
+ .upsert(
346
+ {
347
+ task_id: taskId,
348
+ user_id: user.id,
349
+ total_duration: totalScheduledHours,
350
+ },
351
+ { onConflict: 'task_id,user_id' }
352
+ );
353
+
354
+ if (updateError) {
355
+ console.error('Failed to update task duration:', updateError);
356
+ }
357
+ }
358
+ } catch (error) {
359
+ console.error('Error syncing task duration:', error);
360
+ }
361
+ }
362
+
363
+ export const CalendarProvider = ({
364
+ ws,
365
+ useQuery,
366
+ useQueryClient,
367
+ children,
368
+ experimentalGoogleToken,
369
+ readOnly = false,
370
+ }: {
371
+ ws?: Workspace;
372
+ useQuery: any;
373
+ useQueryClient: any;
374
+ children: ReactNode;
375
+ experimentalGoogleToken?: WorkspaceCalendarGoogleTokenClient | null;
376
+ readOnly?: boolean;
377
+ }) => {
378
+ const queryClient = useQueryClient();
379
+
380
+ // Add debounce timer reference for update events
381
+ const updateDebounceTimerRef = useRef<NodeJS.Timeout | null>(null);
382
+ const pendingUpdatesRef = useRef<Map<string, PendingEventUpdate>>(
383
+ new Map<string, PendingEventUpdate>()
384
+ );
385
+
386
+ // Queue for processing updates in order
387
+ const updateQueueRef = useRef<PendingEventUpdate[]>([]);
388
+ const isProcessingQueueRef = useRef<boolean>(false);
389
+
390
+ const { events, refresh } = useCalendarSync();
391
+
392
+ // Modal state
393
+ const [activeEventId, setActiveEventId] = useState<string | null>(null);
394
+ const [isModalHidden, setModalHidden] = useState(false);
395
+ const [pendingNewEvent, setPendingNewEvent] =
396
+ useState<Partial<CalendarEvent> | null>(null);
397
+ const [defaultNewEventTab, setDefaultNewEventTab] = useState<'manual' | 'ai'>(
398
+ 'manual'
399
+ );
400
+
401
+ // Callback for when a task is scheduled (allows components to refresh)
402
+ const [onTaskScheduled, setOnTaskScheduled] = useState<
403
+ (() => void) | undefined
404
+ >(undefined);
405
+
406
+ // Preview events state (for demo/preview mode)
407
+ const [previewEvents, setPreviewEventsState] = useState<CalendarEvent[]>([]);
408
+
409
+ // Affected events state (events that will be modified/deleted during preview)
410
+ const [affectedEventIds, setAffectedEventIdsState] = useState<Set<string>>(
411
+ new Set()
412
+ );
413
+
414
+ // Wrapper functions for preview events
415
+ const setPreviewEvents = useCallback((newEvents: CalendarEvent[]) => {
416
+ // Add preview flags to all events
417
+ const eventsWithFlags = newEvents.map((e) => ({
418
+ ...e,
419
+ _isPreview: true as const,
420
+ }));
421
+ setPreviewEventsState(eventsWithFlags);
422
+ }, []);
423
+
424
+ const clearPreviewEvents = useCallback(() => {
425
+ setPreviewEventsState([]);
426
+ // Also clear affected events and hide state when clearing preview
427
+ setAffectedEventIdsState(new Set());
428
+ setHideNonPreviewEvents(false);
429
+ }, []);
430
+
431
+ // Wrapper functions for affected events
432
+ const setAffectedEventIds = useCallback((ids: Set<string> | string[]) => {
433
+ const idSet = ids instanceof Set ? ids : new Set(ids);
434
+ setAffectedEventIdsState(idSet);
435
+ }, []);
436
+
437
+ const clearAffectedEventIds = useCallback(() => {
438
+ setAffectedEventIdsState(new Set());
439
+ }, []);
440
+
441
+ // Hide non-preview events state (for performance during preview)
442
+ const [hideNonPreviewEvents, setHideNonPreviewEvents] = useState(false);
443
+
444
+ // Event getters
445
+ const getEvent = useCallback(
446
+ (eventId: string) => {
447
+ // Handle IDs for split multi-day events (they contain a dash and date)
448
+ const originalId = eventId.includes('-')
449
+ ? eventId.split('-')[0]
450
+ : eventId;
451
+ return events.find((e: Partial<CalendarEvent>) => e.id === originalId);
452
+ },
453
+ [events]
454
+ );
455
+
456
+ const getCurrentEvents = useCallback(
457
+ (date?: Date) => {
458
+ const targetDate = date || new Date();
459
+ const targetDay = new Date(
460
+ targetDate.getFullYear(),
461
+ targetDate.getMonth(),
462
+ targetDate.getDate()
463
+ );
464
+
465
+ return events.filter((e: CalendarEvent) => {
466
+ const eventStart = new Date(e.start_at);
467
+ const eventEnd = new Date(e.end_at);
468
+
469
+ // Normalize dates to compare just the date part (ignoring time)
470
+ const eventStartDay = new Date(
471
+ eventStart.getFullYear(),
472
+ eventStart.getMonth(),
473
+ eventStart.getDate()
474
+ );
475
+
476
+ const eventEndDay = new Date(
477
+ eventEnd.getFullYear(),
478
+ eventEnd.getMonth(),
479
+ eventEnd.getDate()
480
+ );
481
+
482
+ // Use only time-based logic for all-day detection
483
+ const isAllDay = isAllDayEvent(e);
484
+
485
+ // For all-day events, treat end date as exclusive (consistent with week view)
486
+ // For timed events, treat end date as inclusive
487
+ if (isAllDay) {
488
+ return eventStartDay <= targetDay && eventEndDay > targetDay;
489
+ } else {
490
+ return eventStartDay <= targetDay && eventEndDay >= targetDay;
491
+ }
492
+ });
493
+ },
494
+ [events]
495
+ );
496
+
497
+ const getUpcomingEvent = useCallback(() => {
498
+ const now = new Date();
499
+ // Get the next event that is happening
500
+ return events.find((e: CalendarEvent) => {
501
+ const start = e.start_at;
502
+ const end = e.end_at;
503
+ const startDate = moment(start).toDate();
504
+ const endDate = moment(end).toDate();
505
+ const isSameDay =
506
+ startDate.getDate() === now.getDate() &&
507
+ startDate.getMonth() === now.getMonth() &&
508
+ startDate.getFullYear() === now.getFullYear();
509
+ return isSameDay && startDate > now && endDate > now;
510
+ });
511
+ }, [events]);
512
+
513
+ // Merge real events with preview events
514
+ const getEvents = useCallback(() => {
515
+ return [...events, ...previewEvents];
516
+ }, [events, previewEvents]);
517
+
518
+ // Event level calculation for overlapping events
519
+ const getEventLevel = useCallback(
520
+ (eventId: string) => {
521
+ // Handle IDs for split multi-day events (they contain a dash and date)
522
+ const originalId = eventId.includes('-')
523
+ ? eventId.split('-')[0]
524
+ : eventId;
525
+ const event = events.find((e: CalendarEvent) => e.id === originalId);
526
+ if (!event) return 0;
527
+
528
+ const eventIndex = events.findIndex(
529
+ (e: CalendarEvent) => e.id === originalId
530
+ );
531
+ const prevEvents = events
532
+ .slice(0, eventIndex)
533
+ .filter((e: CalendarEvent) => {
534
+ if (e.id === originalId) return false;
535
+
536
+ const eventStart = moment(event.start_at).toDate();
537
+ const eventEnd = moment(event.end_at).toDate();
538
+ const eStart = moment(e.start_at).toDate();
539
+ const eEnd = moment(e.end_at).toDate();
540
+
541
+ // For multi-day events, we need to check if they overlap on the same day
542
+ const eventStartDay = new Date(
543
+ eventStart.getFullYear(),
544
+ eventStart.getMonth(),
545
+ eventStart.getDate()
546
+ );
547
+
548
+ const eStartDay = new Date(
549
+ eStart.getFullYear(),
550
+ eStart.getMonth(),
551
+ eStart.getDate()
552
+ );
553
+
554
+ if (eventStartDay.getTime() !== eStartDay.getTime()) return false;
555
+
556
+ // Check for time overlap
557
+ return !(eEnd <= eventStart || eStart >= eventEnd);
558
+ });
559
+
560
+ if (prevEvents.length === 0) return 0;
561
+
562
+ const prevEventLevels = prevEvents.map((e: CalendarEvent) =>
563
+ getEventLevel(e.id)
564
+ ) as number[];
565
+
566
+ return Math.max(...prevEventLevels) + 1;
567
+ },
568
+ [events]
569
+ );
570
+
571
+ // CRUD operations with Supabase
572
+ const addEvent = useCallback(
573
+ async (event: Omit<CalendarEvent, 'id'>) => {
574
+ if (readOnly) {
575
+ console.warn('Calendar is in read-only mode');
576
+ return undefined;
577
+ }
578
+ if (!ws) throw new Error('No workspace selected');
579
+
580
+ // Round start and end times to nearest 15-minute interval
581
+ const startDate = roundToNearest15Minutes(new Date(event.start_at));
582
+ const endDate = roundToNearest15Minutes(new Date(event.end_at));
583
+
584
+ const eventColor = event.color || 'BLUE';
585
+
586
+ // Create an event signature to check for duplicates
587
+ const newEventSignature = `${event.title || ''}|${event.description || ''}|${startDate.toISOString()}|${endDate.toISOString()}`;
588
+
589
+ // Check existing events for potential duplicates to prevent race condition
590
+ const duplicates = events.filter((e: CalendarEvent) => {
591
+ const existingSignature = createEventSignature(e);
592
+ return existingSignature === newEventSignature;
593
+ });
594
+
595
+ // If duplicates already exist, return the first one
596
+ if (duplicates.length > 0) {
597
+ // Clear any pending new event
598
+ setPendingNewEvent(null);
599
+
600
+ // Return the existing event
601
+ return duplicates[0];
602
+ }
603
+
604
+ // No duplicates, proceed with creating the event via API (handles E2EE encryption)
605
+ const response = await fetch(
606
+ `/api/v1/workspaces/${ws.id}/calendar/events`,
607
+ {
608
+ method: 'POST',
609
+ headers: { 'Content-Type': 'application/json' },
610
+ body: JSON.stringify({
611
+ title: event.title || '',
612
+ description: event.description || '',
613
+ start_at: startDate.toISOString(),
614
+ end_at: endDate.toISOString(),
615
+ color: eventColor as SupportedColor,
616
+ location: event.location || '',
617
+ locked: true,
618
+ }),
619
+ }
620
+ );
621
+
622
+ if (!response.ok) {
623
+ const errorData = await response.json();
624
+ throw new Error(errorData.error || 'Failed to create event');
625
+ }
626
+
627
+ const data = (await response.json()) as CalendarEvent;
628
+
629
+ // Refresh the query cache after adding an event
630
+ refresh();
631
+
632
+ if (data) {
633
+ // Clear any pending new event
634
+ setPendingNewEvent(null);
635
+ return data as CalendarEvent;
636
+ }
637
+
638
+ return {} as CalendarEvent;
639
+ },
640
+ [ws, refresh, events, readOnly]
641
+ );
642
+
643
+ const addEmptyEvent = useCallback(
644
+ (date: Date, isAllDay?: boolean) => {
645
+ // NOTE: This implementation uses createAllDayEvent helper for proper timezone handling
646
+ // This ensures all-day events are created at midnight in the user's timezone rather than UTC
647
+ // The workaround is necessary because dayjs timezone handling for all-day events can be inconsistent
648
+ // across different browsers and timezone configurations
649
+ const selectedDate = dayjs(date);
650
+
651
+ let start_at: string;
652
+ let end_at: string;
653
+
654
+ if (isAllDay) {
655
+ // Use the new createAllDayEvent helper for proper timezone handling
656
+ const allDayTimes = createAllDayEvent(
657
+ selectedDate.toDate(),
658
+ undefined,
659
+ 1
660
+ );
661
+ start_at = allDayTimes.start_at;
662
+ end_at = allDayTimes.end_at;
663
+ } else {
664
+ // Round to nearest 15-minute interval
665
+ const startTime = roundToNearest15Minutes(selectedDate.toDate());
666
+ const endTime = new Date(startTime);
667
+
668
+ // Use default task duration of 60 minutes
669
+ const defaultDuration = 60;
670
+ endTime.setMinutes(endTime.getMinutes() + defaultDuration);
671
+
672
+ start_at = startTime.toISOString();
673
+ end_at = endTime.toISOString();
674
+ }
675
+
676
+ // Use default color
677
+ const defaultColor = 'BLUE';
678
+
679
+ // Create a new event with default values
680
+ const newEvent: CalendarEvent = {
681
+ id: 'new',
682
+ title: '',
683
+ description: '',
684
+ start_at,
685
+ end_at,
686
+ color: defaultColor,
687
+ ws_id: ws?.id || '',
688
+ };
689
+
690
+ // Store the pending new event
691
+ setPendingNewEvent(newEvent);
692
+ setActiveEventId('new');
693
+
694
+ // Open the modal with the pending event
695
+ setModalHidden(false);
696
+
697
+ // Return the pending event object
698
+ return newEvent as CalendarEvent;
699
+ },
700
+ [ws?.id]
701
+ );
702
+
703
+ const addEmptyEventWithDuration = useCallback(
704
+ (startDate: Date, endDate: Date) => {
705
+ // Round start and end times to nearest 15-minute interval
706
+ const roundedStartDate = roundToNearest15Minutes(startDate);
707
+ const roundedEndDate = roundToNearest15Minutes(endDate);
708
+
709
+ // Use default color
710
+ const defaultColor = 'BLUE';
711
+
712
+ // Create a new event with default values
713
+ const newEvent: CalendarEvent = {
714
+ id: 'new',
715
+ title: '',
716
+ description: '',
717
+ start_at: roundedStartDate.toISOString(),
718
+ end_at: roundedEndDate.toISOString(),
719
+ color: defaultColor,
720
+ ws_id: ws?.id || '',
721
+ };
722
+
723
+ // Store the pending new event
724
+ setPendingNewEvent(newEvent);
725
+ setActiveEventId('new');
726
+
727
+ // Open the modal with the pending event
728
+ setModalHidden(false);
729
+
730
+ // Return the pending event object
731
+ return newEvent as CalendarEvent;
732
+ },
733
+ [ws?.id]
734
+ );
735
+
736
+ // Process the update queue
737
+ const processUpdateQueue = useCallback(async () => {
738
+ if (readOnly) {
739
+ updateQueueRef.current = [];
740
+ pendingUpdatesRef.current.clear();
741
+ isProcessingQueueRef.current = false;
742
+ return;
743
+ }
744
+ if (isProcessingQueueRef.current || updateQueueRef.current.length === 0) {
745
+ return;
746
+ }
747
+
748
+ isProcessingQueueRef.current = true;
749
+
750
+ try {
751
+ // Sort the queue by timestamp to process oldest updates first
752
+ updateQueueRef.current.sort((a, b) => a._timestamp - b._timestamp);
753
+
754
+ // Take the first item from the queue
755
+ const update = updateQueueRef.current.shift();
756
+
757
+ if (!update?._eventId) {
758
+ isProcessingQueueRef.current = false;
759
+ return;
760
+ }
761
+
762
+ const eventId = update._eventId;
763
+ const {
764
+ _updateId,
765
+ _timestamp,
766
+ _eventId,
767
+ _resolve,
768
+ _reject,
769
+ ...updateData
770
+ } = update;
771
+
772
+ // Check if the event exists before trying to update
773
+ const existingEvent = events.find((e: CalendarEvent) => e.id === eventId);
774
+ if (!existingEvent) {
775
+ const errorMsg = `Event with ID ${eventId} not found in local events`;
776
+ if (_reject) {
777
+ _reject(new Error(errorMsg));
778
+ }
779
+ return;
780
+ }
781
+
782
+ // Validate workspace ownership
783
+ if (existingEvent.ws_id !== ws?.id) {
784
+ const errorMsg = `Event ${eventId} does not belong to current workspace (${ws?.id})`;
785
+ if (_reject) {
786
+ _reject(new Error(errorMsg));
787
+ }
788
+ return;
789
+ }
790
+
791
+ try {
792
+ // Clean up the update data to ensure no undefined values and exclude system fields
793
+ const cleanUpdateData: Partial<CalendarEvent> = {
794
+ ...(updateData.title !== undefined && { title: updateData.title }),
795
+ ...(updateData.description !== undefined && {
796
+ description: updateData.description,
797
+ }),
798
+ ...(updateData.start_at !== undefined && {
799
+ start_at: updateData.start_at,
800
+ }),
801
+ ...(updateData.end_at !== undefined && { end_at: updateData.end_at }),
802
+ ...(updateData.color !== undefined && { color: updateData.color }),
803
+ ...(updateData.location !== undefined && {
804
+ location: updateData.location,
805
+ }),
806
+ ...(updateData.locked !== undefined && { locked: updateData.locked }),
807
+ };
808
+
809
+ // ws is guaranteed to be defined here (validated above at line 732)
810
+ const wsId = ws!.id;
811
+
812
+ // Use API endpoint which handles E2EE encryption
813
+ const response = await fetch(
814
+ `/api/v1/workspaces/${wsId}/calendar/events/${eventId}`,
815
+ {
816
+ method: 'PUT',
817
+ headers: { 'Content-Type': 'application/json' },
818
+ body: JSON.stringify(cleanUpdateData),
819
+ }
820
+ );
821
+
822
+ if (!response.ok) {
823
+ const errorData = await response.json();
824
+ throw new Error(errorData.error || 'Failed to update event');
825
+ }
826
+
827
+ // The API response includes task_id from the database which is not in CalendarEvent type
828
+ const data = (await response.json()) as CalendarEvent & {
829
+ task_id?: string | null;
830
+ };
831
+
832
+ // If event times changed, sync task's total_duration
833
+ if (data) {
834
+ patchWorkspaceCalendarEventCache(queryClient, wsId, (existing) =>
835
+ existing.map((event) =>
836
+ event.id === eventId
837
+ ? ({ ...event, ...data } as CalendarEvent)
838
+ : event
839
+ )
840
+ );
841
+ }
842
+
843
+ if (data && (cleanUpdateData.start_at || cleanUpdateData.end_at)) {
844
+ const supabase = createClient();
845
+ await syncTaskDurationAfterEventChange(
846
+ supabase,
847
+ eventId,
848
+ {
849
+ start_at: data.start_at,
850
+ end_at: data.end_at,
851
+ task_id: data.task_id,
852
+ },
853
+ { calendarWsId: wsId, isPersonalCalendar: !!ws?.personal }
854
+ );
855
+ invalidateTaskSchedulingQueries(queryClient, wsId);
856
+ // Notify components to refresh server data
857
+ onTaskScheduled?.();
858
+ }
859
+
860
+ // Refresh the query cache after updating an event
861
+ refresh();
862
+
863
+ if (data) {
864
+ // Resolve the promise for this update
865
+ if (_resolve) {
866
+ _resolve(data as CalendarEvent);
867
+ }
868
+ } else {
869
+ if (_reject) {
870
+ _reject(
871
+ new Error(`Failed to update event ${eventId} - no data returned`)
872
+ );
873
+ }
874
+ }
875
+ } catch (err) {
876
+ if (_reject) {
877
+ _reject(err);
878
+ }
879
+ }
880
+ } finally {
881
+ isProcessingQueueRef.current = false;
882
+
883
+ // Process the next item in the queue if there are any
884
+ if (updateQueueRef.current.length > 0) {
885
+ setTimeout(processUpdateQueue, 50); // Small delay to prevent blocking
886
+ }
887
+ }
888
+ }, [refresh, events, ws, queryClient, onTaskScheduled, readOnly]);
889
+
890
+ const updateEvent = useCallback(
891
+ async (eventId: string, eventUpdates: Partial<CalendarEvent>) => {
892
+ if (readOnly) {
893
+ console.warn('Calendar is in read-only mode');
894
+ return undefined;
895
+ }
896
+ if (!ws) throw new Error('No workspace selected');
897
+
898
+ // Clean and validate the event updates - only allow known CalendarEvent fields
899
+ const allowedFields: (keyof CalendarEvent)[] = [
900
+ 'title',
901
+ 'description',
902
+ 'start_at',
903
+ 'end_at',
904
+ 'color',
905
+ 'location',
906
+ 'locked',
907
+ ];
908
+
909
+ const cleanedUpdates: Partial<CalendarEvent> = {};
910
+ for (const field of allowedFields) {
911
+ if (eventUpdates[field] !== undefined) {
912
+ (cleanedUpdates as any)[field] = eventUpdates[field];
913
+ }
914
+ }
915
+
916
+ // Round start and end times to nearest 15-minute interval if they exist
917
+ if (cleanedUpdates.start_at) {
918
+ const startDate = roundToNearest15Minutes(
919
+ new Date(cleanedUpdates.start_at)
920
+ );
921
+ cleanedUpdates.start_at = startDate.toISOString();
922
+ }
923
+ if (cleanedUpdates.end_at) {
924
+ const endDate = roundToNearest15Minutes(
925
+ new Date(cleanedUpdates.end_at)
926
+ );
927
+ cleanedUpdates.end_at = endDate.toISOString();
928
+ }
929
+
930
+ // If times were changed (moved/resized), mark as locked to preserve user preference
931
+ // This ensures manually moved events won't be rescheduled by Smart Schedule
932
+ if (cleanedUpdates.start_at || cleanedUpdates.end_at) {
933
+ cleanedUpdates.locked = true;
934
+ }
935
+
936
+ // If this is a newly created event that hasn't been saved to the database yet
937
+ if (pendingNewEvent && eventId === 'new') {
938
+ const newEventData = {
939
+ ...pendingNewEvent,
940
+ ...cleanedUpdates,
941
+ };
942
+ // Check for potential duplicates before creating a new event
943
+ if (cleanedUpdates.title || pendingNewEvent.title) {
944
+ const startDate = roundToNearest15Minutes(
945
+ new Date(newEventData.start_at || new Date())
946
+ );
947
+ const endDate = roundToNearest15Minutes(
948
+ new Date(newEventData.end_at || new Date())
949
+ );
950
+
951
+ const newEventSignature = `${newEventData.title || ''}|${newEventData.description || ''}|${startDate.toISOString()}|${endDate.toISOString()}`;
952
+
953
+ // Check existing events for potential duplicates
954
+ const duplicates = events.filter((e: CalendarEvent) => {
955
+ const existingSignature = createEventSignature(e);
956
+ return existingSignature === newEventSignature;
957
+ });
958
+
959
+ // If duplicates already exist, return the first one
960
+ if (duplicates.length > 0) {
961
+ // Clear any pending new event
962
+ setPendingNewEvent(null);
963
+
964
+ // Return the existing event
965
+ return duplicates[0];
966
+ }
967
+ }
968
+
969
+ // Create a new event instead of updating
970
+ const result = await addEvent(
971
+ newEventData as Omit<CalendarEvent, 'id'>
972
+ );
973
+ return result;
974
+ }
975
+
976
+ // Generate a unique update ID to track this specific update request
977
+ const updateId = `${eventId}-${Date.now()}`;
978
+ const timestamp = Date.now();
979
+
980
+ // Create a promise that will resolve when the update is actually performed
981
+ return new Promise<CalendarEvent>((resolve, reject) => {
982
+ // Create the update object with the promise callbacks
983
+ const updateObject: PendingEventUpdate = {
984
+ ...cleanedUpdates,
985
+ _updateId: updateId,
986
+ _timestamp: timestamp,
987
+ _eventId: eventId,
988
+ _resolve: resolve,
989
+ _reject: reject,
990
+ };
991
+
992
+ // Store the latest update for this event
993
+ pendingUpdatesRef.current.set(eventId, updateObject);
994
+
995
+ // Add to the queue
996
+ updateQueueRef.current.push(updateObject);
997
+
998
+ // Clear any existing timer
999
+ if (updateDebounceTimerRef.current) {
1000
+ clearTimeout(updateDebounceTimerRef.current);
1001
+ }
1002
+
1003
+ // Start processing the queue after a short delay
1004
+ updateDebounceTimerRef.current = setTimeout(() => {
1005
+ updateDebounceTimerRef.current = null;
1006
+ processUpdateQueue();
1007
+ }, 250); // Reduced from 2000ms to 250ms for better responsiveness
1008
+ });
1009
+ },
1010
+ [ws, processUpdateQueue, pendingNewEvent, addEvent, events, readOnly]
1011
+ );
1012
+
1013
+ const deleteEvent = useCallback(
1014
+ async (eventId: string) => {
1015
+ if (readOnly) {
1016
+ console.warn('Calendar is in read-only mode');
1017
+ return;
1018
+ }
1019
+ // If this is a pending new event that hasn't been saved yet
1020
+ if (pendingNewEvent && eventId === 'new') {
1021
+ // Just clear the pending event
1022
+ setPendingNewEvent(null);
1023
+ setActiveEventId(null);
1024
+ return;
1025
+ }
1026
+
1027
+ if (!ws) throw new Error('No workspace selected');
1028
+
1029
+ // Find the event first to get the Google Calendar ID
1030
+ const eventToDelete = events.find(
1031
+ (e: CalendarEvent) => e.id === eventId
1032
+ ) as (CalendarEvent & { task_id?: string | null }) | undefined;
1033
+ const googleCalendarEventId = eventToDelete?.google_event_id;
1034
+
1035
+ // --- Google Calendar Sync (Delete) ---
1036
+ if (googleCalendarEventId && experimentalGoogleToken) {
1037
+ // Check if ID exists and feature enabled
1038
+ try {
1039
+ const syncResponse = await fetch('/api/v1/calendar/auth/sync', {
1040
+ method: 'DELETE',
1041
+ headers: { 'Content-Type': 'application/json' },
1042
+ body: JSON.stringify({ googleCalendarEventId }),
1043
+ });
1044
+
1045
+ if (!syncResponse.ok) {
1046
+ const errorData = await syncResponse.json();
1047
+ if (errorData.eventNotFound) {
1048
+ console.warn(
1049
+ `Google event ${googleCalendarEventId} not found during delete sync. Proceeding with local delete.`
1050
+ );
1051
+ // Don't throw, just log. The event is gone from Google anyway.
1052
+ } else if (errorData.needsReAuth) {
1053
+ // Notify user to re-authenticate with Google Calendar
1054
+ toast.error('Google Calendar authentication expired', {
1055
+ description:
1056
+ 'Please re-authenticate your Google Calendar connection to continue syncing events.',
1057
+ action: {
1058
+ label: 'Re-authenticate',
1059
+ onClick: () => {
1060
+ // Redirect to Google Calendar auth page or open auth modal
1061
+ // This could be enhanced to open a specific auth flow
1062
+ window.open(
1063
+ `/api/v1/calendar/auth?wsId=${ws?.id}`,
1064
+ '_blank'
1065
+ );
1066
+ },
1067
+ },
1068
+ });
1069
+ // Continue with local delete - don't block user action
1070
+ console.warn(
1071
+ 'Google Calendar re-authentication required, proceeding with local delete'
1072
+ );
1073
+ } else {
1074
+ // Throw an error to potentially stop the local delete or notify user
1075
+ throw new Error(
1076
+ `Google Calendar sync (DELETE) failed: ${syncResponse.statusText} - ${JSON.stringify(errorData)}`
1077
+ );
1078
+ }
1079
+ }
1080
+ } catch (_) {
1081
+ // Failed to sync delete with Google Calendar
1082
+ }
1083
+ } else if (experimentalGoogleToken && !googleCalendarEventId) {
1084
+ // Event has no Google Calendar ID, skipping delete sync
1085
+ }
1086
+
1087
+ if (!ws?.id) {
1088
+ throw new Error('No workspace selected');
1089
+ }
1090
+
1091
+ const deleteResponse = await fetch(
1092
+ `/api/v1/workspaces/${ws.id}/calendar/events/${eventId}`,
1093
+ {
1094
+ method: 'DELETE',
1095
+ }
1096
+ );
1097
+
1098
+ if (!deleteResponse.ok) {
1099
+ const errorData = await deleteResponse.json().catch(() => null);
1100
+ throw new Error(errorData?.error || 'Failed to delete event');
1101
+ }
1102
+
1103
+ const deleteResult = (await deleteResponse.json()) as {
1104
+ linkedTaskId?: string | null;
1105
+ skippedHabitId?: string | null;
1106
+ };
1107
+
1108
+ const hasLinkedTask =
1109
+ !!deleteResult.linkedTaskId || !!eventToDelete?.task_id;
1110
+ const hasLinkedHabit = !!deleteResult.skippedHabitId;
1111
+
1112
+ patchWorkspaceCalendarEventCache(queryClient, ws.id, (existing) =>
1113
+ existing.filter((event) => event.id !== eventId)
1114
+ );
1115
+
1116
+ // Refresh the query cache after deleting an event
1117
+ refresh();
1118
+ setActiveEventId(null);
1119
+
1120
+ // If this was a task-linked event, refresh task queries
1121
+ if (hasLinkedTask || hasLinkedHabit) {
1122
+ invalidateTaskSchedulingQueries(queryClient, ws.id);
1123
+ onTaskScheduled?.();
1124
+ }
1125
+ },
1126
+ [
1127
+ ws,
1128
+ refresh,
1129
+ pendingNewEvent,
1130
+ events,
1131
+ experimentalGoogleToken,
1132
+ queryClient,
1133
+ onTaskScheduled,
1134
+ readOnly,
1135
+ ]
1136
+ );
1137
+
1138
+ // Automatically fetch Google Calendar events
1139
+ const fetchGoogleCalendarEvents = async () => {
1140
+ if (!ws?.id) {
1141
+ throw new Error('No workspace selected');
1142
+ }
1143
+ const response = await fetch(`/api/v1/calendar/auth/fetch?wsId=${ws.id}`);
1144
+ if (!response.ok) {
1145
+ throw new Error('Failed to fetch Google Calendar events');
1146
+ }
1147
+ return await response.json();
1148
+ };
1149
+
1150
+ // Query to fetch Google Calendar events every 1 hour
1151
+ const { data: googleData } = useQuery({
1152
+ queryKey: ['googleCalendarEvents', ws?.id],
1153
+ queryFn: fetchGoogleCalendarEvents,
1154
+ enabled: !!ws?.id && !!experimentalGoogleToken?.id,
1155
+ refetchInterval: 1000 * 60 * 60, // Fetch every 1 hour
1156
+ staleTime: 1000 * 60 * 60, // Data is considered fresh for 1 hour
1157
+ });
1158
+
1159
+ const googleEvents = useMemo(() => googleData?.events || [], [googleData]);
1160
+
1161
+ const getGoogleEvents = useCallback(() => googleEvents, [googleEvents]);
1162
+
1163
+ // Function to synchronize local events with Google Calendar
1164
+ const syncEvents = useCallback(
1165
+ async (
1166
+ progressCallback?: (progress: {
1167
+ phase: 'delete' | 'update' | 'insert' | 'complete';
1168
+ current: number;
1169
+ total: number;
1170
+ changesMade: boolean;
1171
+ }) => void
1172
+ ) => {
1173
+ const workspaceId = ws?.id;
1174
+ if (!workspaceId || !googleEvents.length || !experimentalGoogleToken)
1175
+ return;
1176
+
1177
+ // Get local events that are synced with Google Calendar
1178
+ const localGoogleEvents: CalendarEvent[] = events.filter(
1179
+ (e: CalendarEvent) => e.google_event_id
1180
+ );
1181
+
1182
+ // Create a map for faster lookups of local events
1183
+ const localEventMap = new Map<string, CalendarEvent>();
1184
+ localGoogleEvents.forEach((event) => {
1185
+ if (event.google_event_id) {
1186
+ localEventMap.set(event.google_event_id, event);
1187
+ }
1188
+ });
1189
+
1190
+ // Create a set of google_event_id from Google Calendar events for quick lookup
1191
+ const googleEventIds: Set<string | undefined> = new Set(
1192
+ googleEvents.map((e: { google_event_id?: string }) => e.google_event_id)
1193
+ );
1194
+
1195
+ // Identify events to delete: local events not present in Google Calendar
1196
+ const eventsToDelete = localGoogleEvents.filter(
1197
+ (e) => e.google_event_id && !googleEventIds.has(e.google_event_id)
1198
+ );
1199
+
1200
+ // Initialize batch operations - we'll perform these in a more optimized way
1201
+ const eventsToUpdate: Array<{ id: string; data: any }> = [];
1202
+ const eventsToInsert: Array<any> = [];
1203
+ let changesMade = false;
1204
+
1205
+ // Report initial progress
1206
+ if (progressCallback) {
1207
+ progressCallback({
1208
+ phase: 'delete',
1209
+ current: 0,
1210
+ total: eventsToDelete.length,
1211
+ changesMade: false,
1212
+ });
1213
+ }
1214
+
1215
+ // Handle events to delete
1216
+ if (eventsToDelete.length > 0) {
1217
+ changesMade = true;
1218
+ // Delete events in batches for better performance
1219
+ const batchSize = 10;
1220
+ for (let i = 0; i < eventsToDelete.length; i += batchSize) {
1221
+ const batch = eventsToDelete.slice(i, i + batchSize);
1222
+ const eventIds = batch.map((e) => e.id);
1223
+
1224
+ // Report progress
1225
+ if (progressCallback) {
1226
+ progressCallback({
1227
+ phase: 'delete',
1228
+ current: i + batch.length,
1229
+ total: eventsToDelete.length,
1230
+ changesMade: true,
1231
+ });
1232
+ }
1233
+
1234
+ try {
1235
+ await Promise.all(
1236
+ eventIds.map(async (eventId) => {
1237
+ const response = await fetch(
1238
+ `/api/v1/workspaces/${workspaceId}/calendar/events/${eventId}`,
1239
+ {
1240
+ method: 'DELETE',
1241
+ }
1242
+ );
1243
+
1244
+ if (!response.ok) {
1245
+ const errorData = await response.json().catch(() => null);
1246
+ throw new Error(
1247
+ errorData?.error || 'Failed to delete calendar event'
1248
+ );
1249
+ }
1250
+ })
1251
+ );
1252
+ } catch (_) {
1253
+ // Failed to delete events batch
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ // Gather events to update or insert
1259
+ for (const gEvent of googleEvents) {
1260
+ // Skip events without google_event_id
1261
+ if (!gEvent.google_event_id) continue;
1262
+
1263
+ const localEvent = localEventMap.get(gEvent.google_event_id);
1264
+
1265
+ if (localEvent) {
1266
+ // Check if there are any significant changes in the event details that require an update
1267
+ // For encrypted events, we only check non-encrypted fields (dates, color)
1268
+ // since we can't compare encrypted content with plaintext Google data
1269
+ const isEncrypted = localEvent.is_encrypted === true;
1270
+
1271
+ const hasNonEncryptedChanges =
1272
+ localEvent.start_at !== gEvent.start_at ||
1273
+ localEvent.end_at !== gEvent.end_at ||
1274
+ localEvent.color !== gEvent.color;
1275
+
1276
+ const hasContentChanges =
1277
+ !isEncrypted &&
1278
+ (localEvent.title !== gEvent.title ||
1279
+ localEvent.description !== (gEvent.description || '') ||
1280
+ localEvent.location !== (gEvent.location || ''));
1281
+
1282
+ const hasChanges = hasNonEncryptedChanges || hasContentChanges;
1283
+
1284
+ // Only update if there are actual changes
1285
+ if (hasChanges) {
1286
+ changesMade = true;
1287
+
1288
+ // For encrypted events, only update non-encrypted fields (dates, color)
1289
+ // to preserve the encrypted title/description/location
1290
+ if (isEncrypted) {
1291
+ eventsToUpdate.push({
1292
+ id: localEvent.id,
1293
+ data: {
1294
+ start_at: gEvent.start_at,
1295
+ end_at: gEvent.end_at,
1296
+ color: gEvent.color || 'BLUE',
1297
+ // Don't update title, description, location - they are encrypted
1298
+ },
1299
+ });
1300
+ } else {
1301
+ eventsToUpdate.push({
1302
+ id: localEvent.id,
1303
+ data: {
1304
+ title: gEvent.title,
1305
+ description: gEvent.description || '',
1306
+ start_at: gEvent.start_at,
1307
+ end_at: gEvent.end_at,
1308
+ color: gEvent.color || 'BLUE',
1309
+ location: gEvent.location || '',
1310
+ },
1311
+ });
1312
+ }
1313
+ }
1314
+ } else {
1315
+ // Check for content-based duplicates before adding
1316
+ const potentialDuplicates = events.filter(
1317
+ (localEvent: CalendarEvent) => {
1318
+ return (
1319
+ localEvent.title === gEvent.title &&
1320
+ localEvent.description === (gEvent.description || '') &&
1321
+ localEvent.start_at === gEvent.start_at &&
1322
+ localEvent.end_at === gEvent.end_at
1323
+ );
1324
+ }
1325
+ );
1326
+
1327
+ if (potentialDuplicates.length > 0) {
1328
+ if (potentialDuplicates[0]) {
1329
+ changesMade = true;
1330
+ // Update the existing event with the Google Event ID rather than creating a new one
1331
+ eventsToUpdate.push({
1332
+ id: potentialDuplicates[0].id,
1333
+ data: {
1334
+ google_event_id: gEvent.google_event_id,
1335
+ },
1336
+ });
1337
+ continue;
1338
+ }
1339
+ }
1340
+
1341
+ // No duplicates found, add to insert batch
1342
+ changesMade = true;
1343
+ eventsToInsert.push({
1344
+ title: gEvent.title,
1345
+ description: gEvent.description || '',
1346
+ start_at: gEvent.start_at,
1347
+ end_at: gEvent.end_at,
1348
+ color: gEvent.color || 'BLUE',
1349
+ location: gEvent.location || '',
1350
+ ws_id: ws?.id ?? '',
1351
+ google_event_id: gEvent.google_event_id,
1352
+ locked: gEvent.locked || false,
1353
+ created_at: new Date().toISOString(),
1354
+ });
1355
+ }
1356
+ }
1357
+
1358
+ // Process batch updates
1359
+ if (eventsToUpdate.length > 0) {
1360
+ // Report progress for update phase
1361
+ if (progressCallback) {
1362
+ progressCallback({
1363
+ phase: 'update',
1364
+ current: 0,
1365
+ total: eventsToUpdate.length,
1366
+ changesMade: changesMade,
1367
+ });
1368
+ }
1369
+
1370
+ const batchSize = 5; // Smaller batch size for updates to be safer
1371
+
1372
+ for (let i = 0; i < eventsToUpdate.length; i += batchSize) {
1373
+ const batch = eventsToUpdate.slice(i, i + batchSize);
1374
+
1375
+ // Report progress update
1376
+ if (progressCallback) {
1377
+ progressCallback({
1378
+ phase: 'update',
1379
+ current: i + batch.length,
1380
+ total: eventsToUpdate.length,
1381
+ changesMade: changesMade,
1382
+ });
1383
+ }
1384
+
1385
+ // Process each update one by one to ensure reliability
1386
+ for (const item of batch) {
1387
+ try {
1388
+ const response = await fetch(
1389
+ `/api/v1/workspaces/${workspaceId}/calendar/events/${item.id}`,
1390
+ {
1391
+ method: 'PUT',
1392
+ headers: { 'Content-Type': 'application/json' },
1393
+ body: JSON.stringify(item.data),
1394
+ }
1395
+ );
1396
+
1397
+ if (!response.ok) {
1398
+ const errorData = await response.json().catch(() => null);
1399
+ throw new Error(
1400
+ errorData?.error || 'Failed to update calendar event'
1401
+ );
1402
+ }
1403
+ } catch (_) {
1404
+ // Failed to update event
1405
+ }
1406
+ }
1407
+ }
1408
+ }
1409
+
1410
+ // Process batch inserts
1411
+ if (eventsToInsert.length > 0) {
1412
+ // Report progress for insert phase
1413
+ if (progressCallback) {
1414
+ progressCallback({
1415
+ phase: 'insert',
1416
+ current: 0,
1417
+ total: eventsToInsert.length,
1418
+ changesMade: changesMade,
1419
+ });
1420
+ }
1421
+
1422
+ const batchSize = 10;
1423
+
1424
+ for (let i = 0; i < eventsToInsert.length; i += batchSize) {
1425
+ const batch = eventsToInsert.slice(i, i + batchSize);
1426
+
1427
+ // Report progress update
1428
+ if (progressCallback) {
1429
+ progressCallback({
1430
+ phase: 'insert',
1431
+ current: i + batch.length,
1432
+ total: eventsToInsert.length,
1433
+ changesMade: changesMade,
1434
+ });
1435
+ }
1436
+
1437
+ try {
1438
+ await Promise.all(
1439
+ batch.map(async (event) => {
1440
+ const response = await fetch(
1441
+ `/api/v1/workspaces/${workspaceId}/calendar/events`,
1442
+ {
1443
+ method: 'POST',
1444
+ headers: { 'Content-Type': 'application/json' },
1445
+ body: JSON.stringify(event),
1446
+ }
1447
+ );
1448
+
1449
+ if (!response.ok) {
1450
+ const errorData = await response.json().catch(() => null);
1451
+ throw new Error(
1452
+ errorData?.error || 'Failed to insert calendar event'
1453
+ );
1454
+ }
1455
+ })
1456
+ );
1457
+ } catch (_) {
1458
+ // Failed to insert events batch
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ // Report completion
1464
+ if (progressCallback) {
1465
+ progressCallback({
1466
+ phase: 'complete',
1467
+ current: 1,
1468
+ total: 1,
1469
+ changesMade: changesMade,
1470
+ });
1471
+ }
1472
+
1473
+ // Only refresh local events if changes were made
1474
+ if (changesMade) {
1475
+ queryClient.invalidateQueries(['calendarEvents', ws?.id]);
1476
+ }
1477
+ },
1478
+ [googleEvents, events, ws?.id, queryClient, experimentalGoogleToken]
1479
+ );
1480
+
1481
+ // Modal management
1482
+ const openModal = useCallback(
1483
+ (
1484
+ eventId?: string,
1485
+ _modalType?: 'all-day' | 'event',
1486
+ options?: { defaultNewEventTab?: 'manual' | 'ai' }
1487
+ ) => {
1488
+ if (eventId) {
1489
+ // Opening an existing event
1490
+ setActiveEventId(eventId);
1491
+ setPendingNewEvent(null);
1492
+ } else {
1493
+ // Creating a new event
1494
+ setDefaultNewEventTab(options?.defaultNewEventTab ?? 'manual');
1495
+
1496
+ const now = roundToNearest15Minutes(new Date());
1497
+ const oneHourLater = new Date(now);
1498
+ oneHourLater.setHours(oneHourLater.getHours() + 1);
1499
+
1500
+ // Create a pending new event
1501
+ const newEvent: Omit<CalendarEvent, 'id'> = {
1502
+ title: '',
1503
+ description: '',
1504
+ start_at: now.toISOString(),
1505
+ end_at: oneHourLater.toISOString(),
1506
+ color: 'BLUE',
1507
+ };
1508
+
1509
+ setPendingNewEvent(newEvent);
1510
+ setActiveEventId('new');
1511
+ }
1512
+ setModalHidden(false);
1513
+ },
1514
+ []
1515
+ );
1516
+
1517
+ const closeModal = useCallback(() => {
1518
+ setActiveEventId(null);
1519
+ setPendingNewEvent(null);
1520
+ }, []);
1521
+
1522
+ const activeEvent = useMemo(() => {
1523
+ // If it's a pending new event
1524
+ if (pendingNewEvent && activeEventId === 'new') {
1525
+ return {
1526
+ id: 'new', // Use 'new' as the ID for pending events
1527
+ ...pendingNewEvent,
1528
+ } as CalendarEvent;
1529
+ }
1530
+
1531
+ // Otherwise try to find an existing event
1532
+ return activeEventId && activeEventId !== 'new'
1533
+ ? events.find((e: Partial<CalendarEvent>) => e.id === activeEventId)
1534
+ : undefined;
1535
+ }, [activeEventId, events, pendingNewEvent]);
1536
+
1537
+ const isEditing = useCallback(() => !!activeEventId, [activeEventId]);
1538
+ const hideModal = useCallback(() => setModalHidden(true), []);
1539
+ const showModal = useCallback(() => setModalHidden(false), []);
1540
+
1541
+ // Legacy support
1542
+ const getModalStatus = useCallback(
1543
+ (id: string) => (isModalHidden ? false : activeEventId === id),
1544
+ [isModalHidden, activeEventId]
1545
+ );
1546
+
1547
+ const getActiveEvent = useCallback(
1548
+ () => (isModalHidden ? undefined : activeEvent),
1549
+ [isModalHidden, activeEvent]
1550
+ );
1551
+
1552
+ const isModalActive = useCallback(
1553
+ () => (isModalHidden ? false : activeEventId !== null),
1554
+ [isModalHidden, activeEventId]
1555
+ );
1556
+
1557
+ // Add this new function for manual Google Calendar sync
1558
+ const syncGoogleCalendarNow = useCallback(
1559
+ async (
1560
+ progressCallback?: (progress: {
1561
+ phase: 'fetch' | 'delete' | 'update' | 'insert' | 'complete';
1562
+ current: number;
1563
+ total: number;
1564
+ changesMade: boolean;
1565
+ statusMessage?: string;
1566
+ }) => void
1567
+ ) => {
1568
+ if (!experimentalGoogleToken || !ws?.id) {
1569
+ return false;
1570
+ }
1571
+
1572
+ try {
1573
+ // First, capture current events count for comparison
1574
+ const beforeCount = events.length;
1575
+ const beforeGoogleCount = googleEvents.length;
1576
+
1577
+ // Report fetch starting
1578
+ if (progressCallback) {
1579
+ progressCallback({
1580
+ phase: 'fetch',
1581
+ current: 0,
1582
+ total: 1,
1583
+ changesMade: false,
1584
+ statusMessage: 'Fetching events from Google Calendar...',
1585
+ });
1586
+ }
1587
+
1588
+ // Force fetch the latest events from Google with a cache-busting parameter
1589
+ try {
1590
+ const response = await fetch(
1591
+ `/api/v1/calendar/auth/fetch?wsId=${ws.id}&force=true&t=${Date.now()}`
1592
+ );
1593
+ const data = await response.json();
1594
+
1595
+ if (!response.ok) {
1596
+ // Include the full error details from Google API
1597
+ const errorDetails = data.error || response.statusText;
1598
+ const statusCode = response.status;
1599
+ const googleError = data.googleError || data.details || '';
1600
+ throw new Error(
1601
+ `Failed to fetch Google Calendar events: ${errorDetails} (${statusCode})${googleError ? ` - Google API: ${googleError}` : ''}`
1602
+ );
1603
+ }
1604
+
1605
+ // Update googleEvents directly through queryClient for faster UI response
1606
+ queryClient.setQueryData(['googleCalendarEvents', ws?.id], data);
1607
+
1608
+ // Report fetch complete
1609
+ if (progressCallback) {
1610
+ progressCallback({
1611
+ phase: 'fetch',
1612
+ current: 1,
1613
+ total: 1,
1614
+ changesMade: data.events?.length !== beforeGoogleCount,
1615
+ statusMessage: `Fetched ${data.events?.length || 0} events from Google Calendar`,
1616
+ });
1617
+ }
1618
+ } catch (fetchError) {
1619
+ if (progressCallback) {
1620
+ progressCallback({
1621
+ phase: 'fetch',
1622
+ current: 0,
1623
+ total: 1,
1624
+ changesMade: false,
1625
+ statusMessage:
1626
+ fetchError instanceof Error
1627
+ ? fetchError.message
1628
+ : 'Failed to fetch events from Google Calendar',
1629
+ });
1630
+ }
1631
+
1632
+ // Propagate the error instead of returning false
1633
+ throw fetchError;
1634
+ }
1635
+
1636
+ // Manually run the sync process with progress tracking
1637
+ let changesMade = false;
1638
+ await syncEvents((progress) => {
1639
+ if (progressCallback) {
1640
+ // Forward the progress updates
1641
+ progressCallback({
1642
+ ...progress,
1643
+ statusMessage:
1644
+ progress.phase === 'delete'
1645
+ ? `Removing ${progress.total} deleted events (${progress.current}/${progress.total})`
1646
+ : progress.phase === 'update'
1647
+ ? `Updating ${progress.total} events (${progress.current}/${progress.total})`
1648
+ : progress.phase === 'insert'
1649
+ ? `Adding ${progress.total} new events (${progress.current}/${progress.total})`
1650
+ : progress.phase === 'complete'
1651
+ ? 'Sync completed'
1652
+ : undefined,
1653
+ });
1654
+ }
1655
+
1656
+ // Track if any changes were made
1657
+ if (progress.changesMade) {
1658
+ changesMade = true;
1659
+ }
1660
+ });
1661
+
1662
+ // Force refresh of local events
1663
+ if (changesMade) {
1664
+ if (progressCallback) {
1665
+ progressCallback({
1666
+ phase: 'complete',
1667
+ current: 1,
1668
+ total: 1,
1669
+ changesMade: true,
1670
+ statusMessage: 'Refreshing your calendar...',
1671
+ });
1672
+ }
1673
+
1674
+ await queryClient.invalidateQueries({
1675
+ queryKey: ['calendarEvents', ws?.id],
1676
+ refetchType: 'all',
1677
+ });
1678
+ }
1679
+
1680
+ // Calculate changes
1681
+ const afterCount = getEvents().length;
1682
+ const countDifference = afterCount - beforeCount;
1683
+
1684
+ // Final callback with summary
1685
+ if (progressCallback) {
1686
+ progressCallback({
1687
+ phase: 'complete',
1688
+ current: 1,
1689
+ total: 1,
1690
+ changesMade: changesMade,
1691
+ statusMessage: changesMade
1692
+ ? `Sync complete. ${Math.abs(countDifference)} events ${countDifference >= 0 ? 'added' : 'removed'}.`
1693
+ : 'Sync complete. No changes needed.',
1694
+ });
1695
+ }
1696
+
1697
+ // Return success with indication if changes were made
1698
+ return changesMade || beforeGoogleCount !== googleEvents.length;
1699
+ } catch (_) {
1700
+ if (progressCallback) {
1701
+ progressCallback({
1702
+ phase: 'complete',
1703
+ current: 1,
1704
+ total: 1,
1705
+ changesMade: false,
1706
+ statusMessage: 'Sync failed. Please try again.',
1707
+ });
1708
+ }
1709
+
1710
+ return false;
1711
+ }
1712
+ },
1713
+ [
1714
+ experimentalGoogleToken,
1715
+ ws?.id,
1716
+ events.length,
1717
+ googleEvents.length,
1718
+ queryClient,
1719
+ syncEvents,
1720
+ getEvents,
1721
+ ]
1722
+ );
1723
+
1724
+ const [isDragging, setIsDragging] = useState(false);
1725
+ const [hoveredBaseEventId, setHoveredBaseEventId] = useState<string | null>(
1726
+ null
1727
+ );
1728
+ const [hoveredEventColumn, setHoveredEventColumn] = useState<number | null>(
1729
+ null
1730
+ );
1731
+
1732
+ // Schedule a task as a calendar event (for drag-and-drop from sidebar)
1733
+ const scheduleTaskAsEvent = useCallback(
1734
+ async (
1735
+ taskId: string,
1736
+ startAt: Date,
1737
+ endAt: Date,
1738
+ taskData?: TaskDragData
1739
+ ) => {
1740
+ if (!ws?.id) {
1741
+ throw new Error('No workspace selected');
1742
+ }
1743
+
1744
+ const supabase = createClient();
1745
+
1746
+ // Calculate scheduled duration in hours
1747
+ const scheduledHours =
1748
+ (endAt.getTime() - startAt.getTime()) / (1000 * 60 * 60);
1749
+
1750
+ const {
1751
+ data: { user },
1752
+ error: authError,
1753
+ } = await supabase.auth.getUser();
1754
+ if (authError || !user?.id) {
1755
+ throw authError ?? new Error('Not signed in');
1756
+ }
1757
+
1758
+ const { data: existingSettings } = await (supabase as any)
1759
+ .from('task_user_scheduling_settings')
1760
+ .select('total_duration, calendar_hours')
1761
+ .eq('task_id', taskId)
1762
+ .eq('user_id', user.id)
1763
+ .maybeSingle();
1764
+
1765
+ // Check if task has scheduling configured (has duration and calendar_hours)
1766
+ const hasSchedulingConfigured =
1767
+ existingSettings?.total_duration &&
1768
+ existingSettings.total_duration > 0 &&
1769
+ existingSettings?.calendar_hours;
1770
+
1771
+ // Always enable auto_schedule when dragging to calendar.
1772
+ // Task scheduling settings are per-user regardless of calendar type.
1773
+ const updatePayload = hasSchedulingConfigured
1774
+ ? { auto_schedule: true }
1775
+ : {
1776
+ total_duration: scheduledHours,
1777
+ calendar_hours: 'personal_hours' as const,
1778
+ is_splittable: false,
1779
+ auto_schedule: true,
1780
+ };
1781
+
1782
+ const { error: updateError } = await (supabase as any)
1783
+ .from('task_user_scheduling_settings')
1784
+ .upsert(
1785
+ {
1786
+ task_id: taskId,
1787
+ user_id: user.id,
1788
+ ...updatePayload,
1789
+ },
1790
+ { onConflict: 'task_id,user_id' }
1791
+ );
1792
+ if (updateError) {
1793
+ console.error('Failed to update task scheduling:', updateError);
1794
+ }
1795
+
1796
+ // Map priority to color (priority can be string like 'critical', 'high', etc.)
1797
+ const priorityColorMap: Record<string, SupportedColor> = {
1798
+ critical: 'RED',
1799
+ high: 'ORANGE',
1800
+ normal: 'YELLOW',
1801
+ low: 'BLUE',
1802
+ };
1803
+ const priority = String(taskData?.priority ?? 'normal');
1804
+ const eventColor = priorityColorMap[priority] || 'BLUE';
1805
+
1806
+ const event = await createWorkspaceCalendarEvent(
1807
+ ws.id,
1808
+ {
1809
+ title: taskData?.name || 'Task',
1810
+ start_at: startAt.toISOString(),
1811
+ end_at: endAt.toISOString(),
1812
+ color: eventColor,
1813
+ locked: true,
1814
+ task_id: taskId,
1815
+ },
1816
+ {
1817
+ fetch: (input, init) => fetch(input, { ...init, cache: 'no-store' }),
1818
+ }
1819
+ );
1820
+
1821
+ patchWorkspaceCalendarEventCache(queryClient, ws.id, (existing) => {
1822
+ if (existing.some((item) => item.id === event.id)) {
1823
+ return existing;
1824
+ }
1825
+
1826
+ return [...existing, event].sort(
1827
+ (a, b) =>
1828
+ new Date(a.start_at).getTime() - new Date(b.start_at).getTime()
1829
+ );
1830
+ });
1831
+
1832
+ if (event) {
1833
+ // Sync total_duration if total scheduled exceeds current duration
1834
+ // Use the new duration if we just set defaults, otherwise use existing
1835
+ const currentDuration = hasSchedulingConfigured
1836
+ ? existingSettings?.total_duration || 0
1837
+ : scheduledHours;
1838
+
1839
+ const { data: scheduledEvents } = await supabase
1840
+ .from('workspace_calendar_events')
1841
+ .select('start_at, end_at')
1842
+ .eq('task_id', taskId)
1843
+ .eq('ws_id', ws.id);
1844
+
1845
+ const totalScheduledMinutes = (scheduledEvents || []).reduce(
1846
+ (sum: number, scheduledEvent: { start_at: string; end_at: string }) =>
1847
+ sum +
1848
+ Math.round(
1849
+ (new Date(scheduledEvent.end_at).getTime() -
1850
+ new Date(scheduledEvent.start_at).getTime()) /
1851
+ 60000
1852
+ ),
1853
+ 0
1854
+ );
1855
+ const totalScheduledHours = totalScheduledMinutes / 60;
1856
+
1857
+ // If scheduled exceeds current duration, update per-user duration.
1858
+ if (totalScheduledHours > currentDuration) {
1859
+ const {
1860
+ data: { user },
1861
+ } = await supabase.auth.getUser();
1862
+ if (user?.id) {
1863
+ const { error: updateError } = await (supabase as any)
1864
+ .from('task_user_scheduling_settings')
1865
+ .upsert(
1866
+ {
1867
+ task_id: taskId,
1868
+ user_id: user.id,
1869
+ total_duration: totalScheduledHours,
1870
+ },
1871
+ { onConflict: 'task_id,user_id' }
1872
+ );
1873
+ if (updateError) {
1874
+ console.error('Failed to update task duration:', updateError);
1875
+ }
1876
+ }
1877
+ }
1878
+ }
1879
+
1880
+ // Refresh queries
1881
+ refresh();
1882
+ // Invalidate all task-related queries to ensure UI updates
1883
+ invalidateTaskSchedulingQueries(queryClient, ws.id);
1884
+
1885
+ // Call the callback to notify components (e.g., for server data refresh)
1886
+ onTaskScheduled?.();
1887
+ },
1888
+ [ws?.id, refresh, queryClient, onTaskScheduled]
1889
+ );
1890
+
1891
+ const values = {
1892
+ getEvent,
1893
+ getCurrentEvents,
1894
+ getUpcomingEvent,
1895
+ getEvents,
1896
+ getGoogleEvents,
1897
+ getEventLevel,
1898
+
1899
+ addEvent,
1900
+ addEmptyEvent,
1901
+ addEmptyEventWithDuration,
1902
+ updateEvent,
1903
+ deleteEvent,
1904
+
1905
+ // New API
1906
+ isModalOpen: !isModalHidden && activeEventId !== null,
1907
+ activeEvent,
1908
+ openModal,
1909
+ closeModal,
1910
+
1911
+ // Legacy API
1912
+ getModalStatus,
1913
+ getActiveEvent,
1914
+ isModalActive,
1915
+ isEditing,
1916
+ hideModal,
1917
+ showModal,
1918
+
1919
+ // Google Calendar API
1920
+ syncGoogleCalendarNow,
1921
+
1922
+ isDragging,
1923
+ setIsDragging,
1924
+ hoveredBaseEventId,
1925
+ setHoveredBaseEventId,
1926
+ hoveredEventColumn,
1927
+ setHoveredEventColumn,
1928
+ // Task scheduling
1929
+ scheduleTaskAsEvent,
1930
+ onTaskScheduled,
1931
+ setOnTaskScheduled,
1932
+ // Preview events
1933
+ previewEvents,
1934
+ setPreviewEvents,
1935
+ clearPreviewEvents,
1936
+ // Affected events
1937
+ affectedEventIds,
1938
+ setAffectedEventIds,
1939
+ clearAffectedEventIds,
1940
+ // Hide non-preview events
1941
+ hideNonPreviewEvents,
1942
+ setHideNonPreviewEvents,
1943
+ defaultNewEventTab,
1944
+ readOnly,
1945
+ };
1946
+
1947
+ // Clean up any pending updates when component unmounts
1948
+ useEffect(() => {
1949
+ return () => {
1950
+ if (updateDebounceTimerRef.current) {
1951
+ clearTimeout(updateDebounceTimerRef.current);
1952
+ }
1953
+
1954
+ // Clear the update queue
1955
+ updateQueueRef.current = [];
1956
+ pendingUpdatesRef.current.clear();
1957
+ };
1958
+ }, []);
1959
+
1960
+ return (
1961
+ <CalendarContext.Provider value={values}>
1962
+ {children}
1963
+ </CalendarContext.Provider>
1964
+ );
1965
+ };
1966
+
1967
+ export const useCalendar = () => {
1968
+ const context = useContext(CalendarContext);
1969
+ if (context === undefined)
1970
+ throw new Error('useCalendar() must be used within a CalendarProvider.');
1971
+ return context;
1972
+ };