@tulip-systems/create-app 0.12.3 → 0.13.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 (190) hide show
  1. package/dist/index.mjs +2866 -2949
  2. package/dist/index.mjs.map +1 -1
  3. package/dist/templates/basic/package.json +37 -38
  4. package/dist/templates/basic/src/app/layout.tsx +20 -10
  5. package/dist/templates/code/package.json +81 -71
  6. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf +0 -0
  7. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf +0 -0
  8. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf +0 -0
  9. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf +0 -0
  10. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf +0 -0
  11. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf +0 -0
  12. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf +0 -0
  13. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf +0 -0
  14. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf +0 -0
  15. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf +0 -0
  16. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf +0 -0
  17. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf +0 -0
  18. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf +0 -0
  19. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf +0 -0
  20. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf +0 -0
  21. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf +0 -0
  22. package/dist/templates/code/public/images/preview/hero-home.webp +0 -0
  23. package/dist/templates/code/public/sw.js +35 -0
  24. package/dist/templates/code/src/app/admin/contacts/_components/grid.tsx +1 -1
  25. package/dist/templates/code/src/app/admin/customers/_config/columns-data.tsx +1 -1
  26. package/dist/templates/code/src/app/admin/employees/_config/columns-data.tsx +1 -1
  27. package/dist/templates/code/src/app/admin/history/[id]/layout.tsx +33 -0
  28. package/dist/templates/code/src/app/admin/history/[id]/loading.tsx +11 -0
  29. package/dist/templates/code/src/app/admin/history/[id]/page.tsx +30 -0
  30. package/dist/templates/code/src/app/admin/history/_components/diff.client.tsx +377 -0
  31. package/dist/templates/code/src/app/admin/history/_components/field-comparison.tsx +153 -0
  32. package/dist/templates/code/src/app/admin/history/_components/tables.tsx +47 -0
  33. package/dist/templates/code/src/app/admin/history/_components/toolbars.tsx +54 -0
  34. package/dist/templates/code/src/app/admin/history/_config/columns-data.tsx +61 -0
  35. package/dist/templates/code/src/app/admin/history/_config/filters.ts +46 -0
  36. package/dist/templates/code/src/app/admin/history/_config/sorting.ts +30 -0
  37. package/dist/templates/code/src/app/admin/history/_lib/helpers.ts +98 -0
  38. package/dist/templates/code/src/app/admin/history/_lib/router.ts +100 -0
  39. package/dist/templates/code/src/app/admin/history/_lib/schema.ts +45 -0
  40. package/dist/templates/code/src/app/admin/history/_lib/service.server.ts +30 -0
  41. package/dist/templates/code/src/app/admin/history/_lib/validators.ts +118 -0
  42. package/dist/templates/code/src/app/admin/initiatives/_config/columns-data.tsx +1 -1
  43. package/dist/templates/code/src/app/admin/notifications/_components/command-toggle-push.tsx +115 -0
  44. package/dist/templates/code/src/app/admin/notifications/_components/tables.tsx +37 -0
  45. package/dist/templates/code/src/app/admin/notifications/_components/toolbars.tsx +21 -0
  46. package/dist/templates/code/src/app/admin/notifications/_config/columns-data.tsx +64 -0
  47. package/dist/templates/code/src/app/admin/notifications/_config/commands.tsx +40 -0
  48. package/dist/templates/code/src/app/admin/notifications/_lib/listeners.server.ts +69 -0
  49. package/dist/templates/code/src/app/admin/notifications/_lib/push.server.ts +80 -0
  50. package/dist/templates/code/src/app/admin/notifications/_lib/router.ts +82 -0
  51. package/dist/templates/code/src/app/admin/notifications/_lib/schema.ts +49 -0
  52. package/dist/templates/code/src/app/admin/notifications/_lib/validators.ts +19 -0
  53. package/dist/templates/code/src/app/admin/notifications/layout.tsx +17 -0
  54. package/dist/templates/code/src/app/admin/notifications/loading.tsx +9 -0
  55. package/dist/templates/code/src/app/admin/notifications/page.tsx +28 -0
  56. package/dist/templates/code/src/app/admin/projects/[id]/(default)/page.tsx +1 -1
  57. package/dist/templates/code/src/app/admin/projects/[id]/timeline/page.tsx +1 -1
  58. package/dist/templates/code/src/app/admin/projects/_config/columns-data.tsx +1 -1
  59. package/dist/templates/code/src/app/admin/projects/_lib/events.ts +64 -0
  60. package/dist/templates/code/src/app/admin/projects/_lib/listeners.server.ts +82 -0
  61. package/dist/templates/code/src/app/admin/projects/_lib/router.ts +145 -20
  62. package/dist/templates/code/src/app/admin/projects/timeline/page.tsx +1 -1
  63. package/dist/templates/code/src/app/admin/prospects/_config/columns-data.tsx +1 -1
  64. package/dist/templates/code/src/app/admin/queue/_components/create-command.tsx +97 -0
  65. package/dist/templates/code/src/app/admin/queue/_components/tables.tsx +37 -0
  66. package/dist/templates/code/src/app/admin/queue/_components/toolbars.tsx +16 -0
  67. package/dist/templates/code/src/app/admin/queue/_config/columns-data.tsx +44 -0
  68. package/dist/templates/code/src/app/admin/queue/_config/commands.tsx +39 -0
  69. package/dist/templates/code/src/app/admin/queue/_lib/queue.ts +27 -0
  70. package/dist/templates/code/src/app/admin/queue/_lib/router.ts +33 -0
  71. package/dist/templates/code/src/app/admin/queue/_lib/schema.ts +12 -0
  72. package/dist/templates/code/src/app/admin/queue/_lib/validators.ts +15 -0
  73. package/dist/templates/code/src/app/admin/quotations/(list)/layout.tsx +24 -0
  74. package/dist/templates/code/src/app/admin/quotations/(list)/page.tsx +23 -0
  75. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/layout.tsx +40 -0
  76. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/page.tsx +56 -0
  77. package/dist/templates/code/src/app/admin/quotations/[id]/preview/page.tsx +17 -0
  78. package/dist/templates/code/src/app/admin/quotations/_blocks/commands.tsx +24 -0
  79. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/config.ts +14 -0
  80. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/create.tsx +152 -0
  81. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/inline.tsx +90 -0
  82. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/preview.tsx +90 -0
  83. package/dist/templates/code/src/app/admin/quotations/_blocks/registry.ts +13 -0
  84. package/dist/templates/code/src/app/admin/quotations/_blocks/render.tsx +190 -0
  85. package/dist/templates/code/src/app/admin/quotations/_blocks/styles.ts +24 -0
  86. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/config.ts +13 -0
  87. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/create.tsx +175 -0
  88. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/inline.tsx +93 -0
  89. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/preview.tsx +25 -0
  90. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/config.ts +18 -0
  91. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/create.tsx +177 -0
  92. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/inline.tsx +96 -0
  93. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/preview.tsx +32 -0
  94. package/dist/templates/code/src/app/admin/quotations/_components/create-command.tsx +97 -0
  95. package/dist/templates/code/src/app/admin/quotations/_components/tables.tsx +68 -0
  96. package/dist/templates/code/src/app/admin/quotations/_components/toolbars.tsx +18 -0
  97. package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +35 -0
  98. package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +71 -0
  99. package/dist/templates/code/src/app/admin/quotations/_config/filters.ts +22 -0
  100. package/dist/templates/code/src/app/admin/quotations/_config/sorting.ts +23 -0
  101. package/dist/templates/code/src/app/admin/quotations/_lib/router.ts +260 -0
  102. package/dist/templates/code/src/app/admin/quotations/_lib/schema.ts +42 -0
  103. package/dist/templates/code/src/app/admin/quotations/_lib/validators.ts +20 -0
  104. package/dist/templates/code/src/app/admin/settings/history/layout.tsx +6 -0
  105. package/dist/templates/code/src/app/admin/settings/history/loading.tsx +5 -0
  106. package/dist/templates/code/src/app/admin/settings/history/page.tsx +13 -0
  107. package/dist/templates/code/src/app/admin/settings/layout.tsx +11 -0
  108. package/dist/templates/code/src/app/admin/settings/queue/loading.tsx +9 -0
  109. package/dist/templates/code/src/app/admin/settings/queue/page.tsx +21 -0
  110. package/dist/templates/code/src/app/admin/tasks/_components/domains-tabs-grid.tsx +1 -1
  111. package/dist/templates/code/src/app/admin/tasks/_config/columns-data.tsx +1 -1
  112. package/dist/templates/code/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
  113. package/dist/templates/code/src/app/admin/tasks/_lib/events.ts +40 -0
  114. package/dist/templates/code/src/app/admin/tasks/_lib/listeners.server.ts +46 -0
  115. package/dist/templates/code/src/app/admin/tasks/_lib/router.ts +222 -90
  116. package/dist/templates/code/src/app/admin/time/_config/columns-data.tsx +1 -1
  117. package/dist/templates/code/src/app/admin/time/_config/columns-inline.tsx +1 -1
  118. package/dist/templates/code/src/app/api/[transport]/route.ts +31 -21
  119. package/dist/templates/code/src/app/api/queue/dispatch/route.ts +4 -0
  120. package/dist/templates/code/src/app/auth/forget-password/page.tsx +1 -1
  121. package/dist/templates/code/src/app/auth/layout.tsx +1 -1
  122. package/dist/templates/code/src/app/auth/reset-password/page.tsx +1 -1
  123. package/dist/templates/code/src/app/layout.tsx +25 -13
  124. package/dist/templates/code/src/env.ts +12 -0
  125. package/dist/templates/code/src/lib/config/paths.tsx +16 -0
  126. package/dist/templates/code/src/lib/utils/fonts.ts +102 -0
  127. package/dist/templates/code/src/server/auth/constants.ts +1 -1
  128. package/dist/templates/code/src/server/auth/init.ts +12 -5
  129. package/dist/templates/code/src/server/auth/permissions.ts +7 -0
  130. package/dist/templates/code/src/server/auth/schema.ts +100 -1
  131. package/dist/templates/code/src/server/db/schema.ts +4 -0
  132. package/dist/templates/code/src/server/events/handlers.ts +17 -0
  133. package/dist/templates/code/src/server/events/init.ts +18 -0
  134. package/dist/templates/code/src/server/events/listeners.ts +17 -0
  135. package/dist/templates/code/src/server/queue/handlers.ts +5 -0
  136. package/dist/templates/code/src/server/queue/init.ts +16 -0
  137. package/dist/templates/code/src/server/router/router.ts +23 -0
  138. package/dist/templates/spark/package.json +64 -65
  139. package/dist/templates/spark/src/app/admin/assignments/[id]/(default)/@dashboard/page.tsx +1 -1
  140. package/dist/templates/spark/src/app/admin/assignments/_config/columns-data.tsx +1 -1
  141. package/dist/templates/spark/src/app/admin/companies/_config/columns-data.tsx +1 -1
  142. package/dist/templates/spark/src/app/admin/contacts/_components/grid.tsx +1 -1
  143. package/dist/templates/spark/src/app/admin/customers/_config/columns-data.tsx +1 -1
  144. package/dist/templates/spark/src/app/admin/drive/_config/columns-data.tsx +1 -1
  145. package/dist/templates/spark/src/app/admin/emails/_components/send-email.tsx +1 -1
  146. package/dist/templates/spark/src/app/admin/emails/_config/columns-data.tsx +1 -1
  147. package/dist/templates/spark/src/app/admin/employees/_config/columns-data.tsx +1 -1
  148. package/dist/templates/spark/src/app/admin/executions/[id]/@dashboard/page.tsx +1 -1
  149. package/dist/templates/spark/src/app/admin/executions/_config/columns-data.tsx +1 -1
  150. package/dist/templates/spark/src/app/admin/products/[id]/(preview)/page.tsx +1 -1
  151. package/dist/templates/spark/src/app/admin/products/_config/columns-data.tsx +1 -1
  152. package/dist/templates/spark/src/app/admin/projects/[id]/(dashboard)/page.tsx +1 -1
  153. package/dist/templates/spark/src/app/admin/projects/[id]/messages/page.tsx +1 -1
  154. package/dist/templates/spark/src/app/admin/projects/[id]/statistics/page.tsx +1 -1
  155. package/dist/templates/spark/src/app/admin/projects/_config/columns-data.tsx +1 -1
  156. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-data.tsx +1 -1
  157. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-inline.tsx +1 -1
  158. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/commands.tsx +1 -1
  159. package/dist/templates/spark/src/app/admin/purchase-orders/[id]/(default)/@dashboard/page.tsx +1 -1
  160. package/dist/templates/spark/src/app/admin/purchase-orders/_config/columns-data.tsx +1 -1
  161. package/dist/templates/spark/src/app/admin/quotation-items/_config/commands.tsx +1 -1
  162. package/dist/templates/spark/src/app/admin/quotations/[id]/(default)/@dashboard/page.tsx +1 -1
  163. package/dist/templates/spark/src/app/admin/quotations/_config/columns-data.tsx +1 -1
  164. package/dist/templates/spark/src/app/admin/run-tasks/_config/columns-data.tsx +1 -1
  165. package/dist/templates/spark/src/app/admin/runs/[id]/@dashboard/page.tsx +1 -1
  166. package/dist/templates/spark/src/app/admin/runs/_config/columns-data.tsx +1 -1
  167. package/dist/templates/spark/src/app/admin/sales-invoices/[id]/@dashboard/page.tsx +1 -1
  168. package/dist/templates/spark/src/app/admin/sales-invoices/_config/columns-data.tsx +1 -1
  169. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-data.tsx +1 -1
  170. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-inline.tsx +1 -1
  171. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/commands.tsx +1 -1
  172. package/dist/templates/spark/src/app/admin/sales-order-items/_config/commands.tsx +1 -1
  173. package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/@dashboard/page.tsx +1 -1
  174. package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/layout.tsx +1 -1
  175. package/dist/templates/spark/src/app/admin/sales-orders/_config/columns-data.tsx +1 -1
  176. package/dist/templates/spark/src/app/admin/schedule/_components/schedule-card.tsx +1 -1
  177. package/dist/templates/spark/src/app/admin/schedule/_components/schedule-month-view.tsx +1 -1
  178. package/dist/templates/spark/src/app/admin/suppliers/_config/columns-data.tsx +1 -1
  179. package/dist/templates/spark/src/app/admin/tasks/_components/subproject-selector.tsx +1 -1
  180. package/dist/templates/spark/src/app/admin/tasks/_config/columns-data.tsx +1 -1
  181. package/dist/templates/spark/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
  182. package/dist/templates/spark/src/app/admin/tasks/_config/commands.tsx +1 -1
  183. package/dist/templates/spark/src/app/admin/technical-sheets/[id]/page.tsx +1 -1
  184. package/dist/templates/spark/src/app/admin/technical-sheets/_config/columns-data.tsx +1 -1
  185. package/dist/templates/spark/src/app/admin/technical-sheets/_config/commands.tsx +1 -1
  186. package/dist/templates/spark/src/app/admin/templates/_config/columns-data.tsx +1 -1
  187. package/dist/templates/spark/src/app/auth/layout.tsx +1 -1
  188. package/dist/templates/spark/src/app/layout.tsx +25 -10
  189. package/dist/templates/spark/src/server/auth/init.ts +1 -0
  190. package/package.json +6 -6
@@ -0,0 +1,13 @@
1
+ import { DataTable } from "@tulip-systems/data-tables/client";
2
+ import { HistoryPaginationTableProvider } from "@/app/admin/history/_components/tables";
3
+ import { HistoryTableToolbar } from "@/app/admin/history/_components/toolbars";
4
+ import { historyColumns } from "@/app/admin/history/_config/columns-data";
5
+
6
+ export default function Page() {
7
+ return (
8
+ <HistoryPaginationTableProvider columns={historyColumns}>
9
+ <HistoryTableToolbar />
10
+ <DataTable />
11
+ </HistoryPaginationTableProvider>
12
+ );
13
+ }
@@ -38,6 +38,17 @@ export default function Layout(props: PropsWithChildren) {
38
38
  href: "/admin/settings/theme",
39
39
  segment: "theme",
40
40
  },
41
+ {
42
+ name: "Queue",
43
+ href: "/admin/settings/queue",
44
+ segment: "queue",
45
+ },
46
+ {
47
+ name: "History",
48
+ href: "/admin/settings/history",
49
+ segment: "history",
50
+ permission: { history: ["view"] },
51
+ },
41
52
  ]}
42
53
  />
43
54
  </BottomBar>
@@ -0,0 +1,9 @@
1
+ import { TableSkeleton } from "@tulip-systems/data-tables";
2
+
3
+ export default function Loading() {
4
+ return (
5
+ <div className="p-content">
6
+ <TableSkeleton />
7
+ </div>
8
+ );
9
+ }
@@ -0,0 +1,21 @@
1
+ import { InlineCommandMenu } from "@tulip-systems/commands/client";
2
+ import { TableLayout } from "@tulip-systems/data-tables";
3
+ import { DataTable } from "@tulip-systems/data-tables/client";
4
+ import { QueueExecutionsTableProvider } from "@/app/admin/queue/_components/tables";
5
+ import { QueueExecutionsTableToolbar } from "@/app/admin/queue/_components/toolbars";
6
+ import { queueExecutionColumns } from "@/app/admin/queue/_config/columns-data";
7
+ import { queueDeleteCommand, queueEnqueueCommand } from "@/app/admin/queue/_config/commands";
8
+
9
+ export default function Page() {
10
+ return (
11
+ <TableLayout className="p-content">
12
+ <QueueExecutionsTableProvider columns={queueExecutionColumns} commands={[queueDeleteCommand]}>
13
+ <QueueExecutionsTableToolbar>
14
+ <InlineCommandMenu data={null} commands={[queueEnqueueCommand]} />
15
+ </QueueExecutionsTableToolbar>
16
+
17
+ <DataTable />
18
+ </QueueExecutionsTableProvider>
19
+ </TableLayout>
20
+ );
21
+ }
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import type { Tab } from "@tulip-systems/app/components";
4
+ import { Link } from "@tulip-systems/core/navigation";
4
5
  import { buttonVariants } from "@tulip-systems/ui/button";
5
6
  import { cn } from "@tulip-systems/ui/cn";
6
7
  import { Skeleton } from "@tulip-systems/ui/skeleton";
7
- import Link from "next/link";
8
8
  import { useSelectedLayoutSegment } from "next/navigation";
9
9
 
10
10
  type TaskSubprojectTabGridProps = {
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { formatDate } from "@tulip-systems/core";
4
+ import { Link } from "@tulip-systems/core/navigation";
4
5
  import { TableCell, type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
5
6
  import { createTableSelectCell } from "@tulip-systems/data-tables/client";
6
7
  import { cn } from "@tulip-systems/ui/cn";
7
- import Link from "next/link";
8
8
  import type { TaskTableData } from "../_components/tables";
9
9
  import { TaskStatusField } from "./status";
10
10
 
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { Link } from "@tulip-systems/core/navigation";
2
3
  import { type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
3
4
  import {
4
5
  createTableIndexCell,
@@ -15,7 +16,6 @@ import {
15
16
  InlineTableCell,
16
17
  InlineTableCellError,
17
18
  } from "@tulip-systems/inline/client";
18
- import Link from "next/link";
19
19
  import { z } from "zod";
20
20
  import { orpc } from "@/server/router/client";
21
21
  import type { TaskTableData, TaskTableMeta } from "../_components/tables";
@@ -0,0 +1,40 @@
1
+ import { defineEvent } from "@tulip-systems/events";
2
+ import { z } from "zod";
3
+ import { eventHistoryMetadata, historyJSONSchema } from "@/app/admin/history/_lib/validators";
4
+ import { taskStatusSchema } from "./validators";
5
+
6
+ /**
7
+ * Published for each created or imported task. Carries actor, identity, project context,
8
+ * initial status and the complete inserted row snapshot.
9
+ */
10
+ export const taskCreatedEvent = defineEvent("task.created", {
11
+ version: 2,
12
+ schema: z.object({
13
+ ...eventHistoryMetadata,
14
+ taskId: z.uuid(),
15
+ projectId: z.uuid(),
16
+ projectTitle: z.string(),
17
+ code: z.string(),
18
+ title: z.string(),
19
+ status: taskStatusSchema,
20
+ after: z.record(z.string(), historyJSONSchema),
21
+ }),
22
+ });
23
+
24
+ /**
25
+ * Published for general edits, status changes and assignment changes, once per affected task.
26
+ * Carries actor, identity, project context and complete before/after row snapshots.
27
+ */
28
+ export const taskUpdatedEvent = defineEvent("task.updated", {
29
+ version: 1,
30
+ schema: z.object({
31
+ ...eventHistoryMetadata,
32
+ taskId: z.uuid(),
33
+ projectId: z.uuid(),
34
+ projectTitle: z.string(),
35
+ code: z.string(),
36
+ title: z.string(),
37
+ before: z.record(z.string(), historyJSONSchema),
38
+ after: z.record(z.string(), historyJSONSchema),
39
+ }),
40
+ });
@@ -0,0 +1,46 @@
1
+ import "server-cli-only";
2
+
3
+ import { createHistoryDiff } from "@/app/admin/history/_lib/helpers";
4
+ import { createHistoryEntry } from "@/app/admin/history/_lib/service.server";
5
+ import { taskCreatedEvent, taskUpdatedEvent } from "./events";
6
+
7
+ /**
8
+ * Tasks supply historical labels, project context and diffs from captured row snapshots.
9
+ */
10
+ export const taskHistoryEventListeners = {
11
+ taskCreated: taskCreatedEvent.listen(async ({ event, payload: p }) => {
12
+ await createHistoryEntry({
13
+ eventId: event.id,
14
+ eventName: event.name,
15
+ eventVersion: event.version,
16
+ occurredAt: event.occurredAt,
17
+ actorId: p.actor.id,
18
+ actorName: p.actor.name,
19
+ resourceType: "tasks",
20
+ resourceId: p.taskId,
21
+ resourceLabel: `${p.code} · ${p.title}`,
22
+ context: [{ type: "projects", id: p.projectId, label: p.projectTitle }],
23
+ action: "created",
24
+ diff: createHistoryDiff(null, p.after),
25
+ });
26
+ }),
27
+ taskUpdated: taskUpdatedEvent.listen(async ({ event, payload: p }) => {
28
+ const diff = createHistoryDiff(p.before, p.after);
29
+ if (!diff.changes.some((change) => change.path !== "/updated_at")) return;
30
+
31
+ await createHistoryEntry({
32
+ eventId: event.id,
33
+ eventName: event.name,
34
+ eventVersion: event.version,
35
+ occurredAt: event.occurredAt,
36
+ actorId: p.actor.id,
37
+ actorName: p.actor.name,
38
+ resourceType: "tasks",
39
+ resourceId: p.taskId,
40
+ resourceLabel: `${p.code} · ${p.title}`,
41
+ context: [{ type: "projects", id: p.projectId, label: p.projectTitle }],
42
+ action: "updated",
43
+ diff,
44
+ });
45
+ }),
46
+ };
@@ -8,12 +8,15 @@ import {
8
8
  offsetPaginationSchema,
9
9
  parseOffsetPagination,
10
10
  } from "@tulip-systems/query";
11
- import { and, asc, count, desc, eq, gte, inArray, sql } from "drizzle-orm";
11
+ import { and, asc, count, eq, getTableColumns, gte, inArray, sql } from "drizzle-orm";
12
+ import { after } from "next/server";
12
13
  import z from "zod";
13
14
  import { domains } from "@/app/admin/domains/_lib/schema";
15
+ import type { JSONValue } from "@/app/admin/history/_lib/validators";
14
16
  import { initiatives } from "@/app/admin/initiatives/_lib/schema";
15
17
  import { getTableData } from "@/app/lib/utils/table-data.server";
16
18
  import { employees, projects, timeEntries, users } from "@/server/db/schema";
19
+ import { events } from "@/server/events/init";
17
20
  import { permissionMiddleware } from "@/server/router/middleware";
18
21
  import { protectedProcedure } from "@/server/router/procedures";
19
22
  import type { TaskTableData } from "../_components/tables";
@@ -28,6 +31,7 @@ import {
28
31
  taskTableSortsParser,
29
32
  taskTableSortsSchema,
30
33
  } from "../_config/sorting";
34
+ import { taskCreatedEvent, taskUpdatedEvent } from "./events";
31
35
  import { extractTaskCodeSQL, generateTaskCode } from "./helpers";
32
36
  import { tasks } from "./schema";
33
37
  import {
@@ -265,17 +269,6 @@ export const tasksRouter = {
265
269
  .use(permissionMiddleware({ task: ["create"] }))
266
270
  .input(createTaskSchema)
267
271
  .handler(async ({ context, input }) => {
268
- const [project] = await context.db
269
- .select()
270
- .from(projects)
271
- .where(eq(projects.id, input.projectId));
272
-
273
- if (!project) {
274
- throw new ORPCError("NOT_FOUND", {
275
- message: "Project kon niet worden gevonden",
276
- });
277
- }
278
-
279
272
  if (input.parentId) {
280
273
  const [parent] = await context.db.select().from(tasks).where(eq(tasks.id, input.parentId));
281
274
 
@@ -292,51 +285,61 @@ export const tasksRouter = {
292
285
  }
293
286
  }
294
287
 
295
- const [lastTask] = await context.db
296
- .select({ index: tasks.index })
297
- .from(tasks)
298
- .limit(1)
299
- .where(eq(tasks.projectId, input.projectId))
300
- .orderBy(desc(tasks.index));
301
-
302
- const [lastCode] = await context.db
303
- .select({ lastNumber: extractTaskCodeSQL(tasks.code) })
304
- .from(tasks)
305
- .limit(1)
306
- .where(eq(tasks.projectId, input.projectId))
307
- .orderBy(desc(extractTaskCodeSQL(tasks.code)));
308
-
309
- const index = (lastTask?.index ?? 0) + 1;
310
-
311
- const lastNumber = lastCode?.lastNumber ?? null;
312
- const code = generateTaskCode(project.key, lastNumber);
288
+ const { result, projectTitle } = await context.db.transaction(async (tx) => {
289
+ const [project] = await tx
290
+ .select({ key: projects.key, title: projects.title })
291
+ .from(projects)
292
+ .where(eq(projects.id, input.projectId))
293
+ .for("update");
294
+ if (!project) {
295
+ throw new ORPCError("NOT_FOUND", { message: "Project kon niet worden gevonden" });
296
+ }
313
297
 
314
- const [result] = await context.db
315
- .insert(tasks)
316
- .values({ ...input, code, index })
317
- .returning();
298
+ // Allocate the task code and ordering position while holding the project lock.
299
+ const [lastTask] = await tx
300
+ .select({
301
+ index: sql<number>`coalesce(max(${tasks.index}), 0)`,
302
+ number: sql<number>`coalesce(max(${extractTaskCodeSQL(tasks.code)}), 0)`,
303
+ })
304
+ .from(tasks)
305
+ .where(eq(tasks.projectId, input.projectId));
306
+
307
+ const [result] = await tx
308
+ .insert(tasks)
309
+ .values({
310
+ ...input,
311
+ index: (lastTask?.index ?? 0) + 1,
312
+ code: generateTaskCode(project.key, lastTask?.number ?? 0),
313
+ })
314
+ .returning({
315
+ ...getTableColumns(tasks),
316
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
317
+ });
318
+ if (!result) throw new ORPCError("INTERNAL_SERVER_ERROR");
319
+ return { result, projectTitle: project.title };
320
+ });
318
321
 
319
- if (!result) {
320
- throw new ORPCError("INTERNAL_SERVER_ERROR", {
321
- message: "Task kon niet worden aangemaakt",
322
- });
323
- }
322
+ after(async () => {
323
+ await events.publish(
324
+ taskCreatedEvent,
325
+ {
326
+ actor: { id: context.user.id, name: context.user.name },
327
+ taskId: result.id,
328
+ projectId: result.projectId,
329
+ projectTitle,
330
+ code: result.code,
331
+ title: result.title,
332
+ status: result.status,
333
+ after: result.after,
334
+ },
335
+ { occurredAt: new Date().toISOString() },
336
+ );
337
+ });
324
338
  }),
325
339
  importExcel: protectedProcedure
326
340
  .use(permissionMiddleware({ task: ["create"] }))
327
341
  .input(importExcelTasksSchema)
328
342
  .handler(async ({ context, input }) => {
329
- const [project] = await context.db
330
- .select()
331
- .from(projects)
332
- .where(eq(projects.id, input.projectId));
333
-
334
- if (!project) {
335
- throw new ORPCError("NOT_FOUND", {
336
- message: "Project kon niet worden gevonden",
337
- });
338
- }
339
-
340
343
  const assigneeEmails = input.items
341
344
  .map((item) => item.assignee)
342
345
  .filter((email): email is string => Boolean(email));
@@ -347,25 +350,8 @@ export const tasksRouter = {
347
350
  .leftJoin(users, eq(users.id, employees.userId))
348
351
  .where(inArray(users.email, assigneeEmails));
349
352
 
350
- const [lastTask] = await context.db
351
- .select({ index: tasks.index })
352
- .from(tasks)
353
- .limit(1)
354
- .where(eq(tasks.projectId, input.projectId))
355
- .orderBy(desc(tasks.index));
356
-
357
- const [lastCode] = await context.db
358
- .select({ lastNumber: extractTaskCodeSQL(tasks.code) })
359
- .from(tasks)
360
- .limit(1)
361
- .where(eq(tasks.projectId, input.projectId))
362
- .orderBy(desc(extractTaskCodeSQL(tasks.code)));
363
-
364
- const lastIndex = (lastTask?.index ?? 0) + 1;
365
- const lastNumber = lastCode?.lastNumber ?? 0;
366
-
367
353
  const values = input.items.map(
368
- (item, index) =>
354
+ (item) =>
369
355
  ({
370
356
  projectId: input.projectId,
371
357
  initiativeId: input.initiativeId,
@@ -377,20 +363,67 @@ export const tasksRouter = {
377
363
  ? (assignees.find((assignee) => assignee.email === item.assignee)?.id ?? null)
378
364
  : null,
379
365
  description: convertMarkdownToEditorContent(item.description ?? ""),
380
- code: generateTaskCode(project.key, lastNumber + index),
381
- index: lastIndex + index,
382
366
  }) satisfies Partial<Task>,
383
367
  );
384
368
 
385
- const results = await context.db.insert(tasks).values(values).returning();
369
+ if (!values.length) return [];
370
+ const { results, projectTitle } = await context.db.transaction(async (tx) => {
371
+ const [project] = await tx
372
+ .select({ key: projects.key, title: projects.title })
373
+ .from(projects)
374
+ .where(eq(projects.id, input.projectId))
375
+ .for("update");
376
+ if (!project) {
377
+ throw new ORPCError("NOT_FOUND", { message: "Project kon niet worden gevonden" });
378
+ }
386
379
 
387
- if (!results) {
388
- throw new ORPCError("BAD_REQUEST", {
389
- message: "Tasks konden niet worden geïmporteerd",
390
- });
391
- }
380
+ // Allocate the task code and ordering position while holding the project lock.
381
+ const [lastTask] = await tx
382
+ .select({
383
+ index: sql<number>`coalesce(max(${tasks.index}), 0)`,
384
+ number: sql<number>`coalesce(max(${extractTaskCodeSQL(tasks.code)}), 0)`,
385
+ })
386
+ .from(tasks)
387
+ .where(eq(tasks.projectId, input.projectId));
388
+
389
+ const results = await tx
390
+ .insert(tasks)
391
+ .values(
392
+ values.map((value, index) => ({
393
+ ...value,
394
+ index: (lastTask?.index ?? 0) + index + 1,
395
+ code: generateTaskCode(project.key, (lastTask?.number ?? 0) + index),
396
+ })),
397
+ )
398
+ .returning({
399
+ ...getTableColumns(tasks),
400
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
401
+ });
402
+ return { results, projectTitle: project.title };
403
+ });
404
+
405
+ after(async () => {
406
+ await Promise.all(
407
+ results.map((task) =>
408
+ events.publish(
409
+ taskCreatedEvent,
410
+ {
411
+ actor: { id: context.user.id, name: context.user.name },
412
+ taskId: task.id,
413
+ projectId: task.projectId,
414
+ projectTitle,
415
+ code: task.code,
416
+ title: task.title,
417
+ status: task.status,
418
+ after: task.after,
419
+ },
420
+ { occurredAt: new Date().toISOString() },
421
+ ),
422
+ ),
423
+ );
424
+ });
392
425
 
393
- return results;
426
+ return results.map(({ after: _after, ...task }) => task);
394
427
  }),
395
428
  update: protectedProcedure
396
429
  .use(permissionMiddleware({ task: ["update"] }))
@@ -400,33 +433,132 @@ export const tasksRouter = {
400
433
  const [result] = await context.db
401
434
  .update(tasks)
402
435
  .set(input.data)
403
- .where(eq(tasks.id, input.id))
404
- .returning();
405
-
406
- if (!result) {
407
- throw new ORPCError("INTERNAL_SERVER_ERROR", {
408
- message: "Task kon niet worden aangemaakt",
436
+ .from(projects)
437
+ .where(
438
+ and(eq(tasks.id, input.id), eq(projects.id, input.data.projectId ?? tasks.projectId)),
439
+ )
440
+ .returning({
441
+ id: tasks.id,
442
+ projectId: tasks.projectId,
443
+ code: tasks.code,
444
+ title: tasks.title,
445
+ projectTitle: projects.title,
446
+ before: sql<Record<string, JSONValue>>`to_jsonb(OLD)`,
447
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
409
448
  });
410
- }
449
+
450
+ if (!result) throw new ORPCError("NOT_FOUND", { message: "Task kon niet worden gewijzigd" });
451
+
452
+ after(async () => {
453
+ await events.publish(
454
+ taskUpdatedEvent,
455
+ {
456
+ actor: { id: context.user.id, name: context.user.name },
457
+ taskId: result.id,
458
+ projectId: result.projectId,
459
+ projectTitle: result.projectTitle,
460
+ code: result.code,
461
+ title: result.title,
462
+ before: result.before,
463
+ after: result.after,
464
+ },
465
+ { occurredAt: new Date().toISOString() },
466
+ );
467
+ });
411
468
  }),
412
469
  setStatus: protectedProcedure
413
470
  .use(permissionMiddleware({ task: ["update"] }))
414
471
  .input(bulkActionSchema.extend({ status: taskStatusSchema }))
415
472
  .handler(async ({ context, input }) => {
416
- await context.db
473
+ const updated = await context.db
417
474
  .update(tasks)
418
475
  .set({ status: input.status })
419
- .where(inArray(tasks.id, input.ids))
420
- .returning();
476
+ .from(projects)
477
+ .where(
478
+ and(
479
+ eq(tasks.projectId, projects.id),
480
+ inArray(tasks.id, input.ids),
481
+ sql`${tasks.status} IS DISTINCT FROM ${input.status}`,
482
+ ),
483
+ )
484
+ .returning({
485
+ id: tasks.id,
486
+ projectId: tasks.projectId,
487
+ projectTitle: projects.title,
488
+ code: tasks.code,
489
+ title: tasks.title,
490
+ before: sql<Record<string, JSONValue>>`to_jsonb(OLD)`,
491
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
492
+ });
493
+
494
+ after(async () => {
495
+ await Promise.all(
496
+ updated.map((task) =>
497
+ events.publish(
498
+ taskUpdatedEvent,
499
+ {
500
+ actor: { id: context.user.id, name: context.user.name },
501
+ taskId: task.id,
502
+ projectId: task.projectId,
503
+ projectTitle: task.projectTitle,
504
+ code: task.code,
505
+ title: task.title,
506
+ before: task.before,
507
+ after: task.after,
508
+ },
509
+ { occurredAt: new Date().toISOString() },
510
+ ),
511
+ ),
512
+ );
513
+ });
421
514
  }),
422
515
  setAssignee: protectedProcedure
423
516
  .use(permissionMiddleware({ task: ["update"] }))
424
- .input(bulkActionSchema.extend({ assigneeId: z.string() }))
517
+ .input(bulkActionSchema.extend({ assigneeId: z.uuid().nullable() }))
425
518
  .handler(async ({ context, input }) => {
426
- await context.db
519
+ const updated = await context.db
427
520
  .update(tasks)
428
521
  .set({ assigneeId: input.assigneeId })
429
- .where(inArray(tasks.id, input.ids));
522
+ .from(projects)
523
+ .where(
524
+ and(
525
+ eq(tasks.projectId, projects.id),
526
+ inArray(tasks.id, input.ids),
527
+ sql`${tasks.assigneeId} IS DISTINCT FROM ${input.assigneeId}`,
528
+ ),
529
+ )
530
+ .returning({
531
+ id: tasks.id,
532
+ projectId: tasks.projectId,
533
+ projectTitle: projects.title,
534
+ code: tasks.code,
535
+ title: tasks.title,
536
+ before: sql<Record<string, JSONValue>>`to_jsonb(OLD)`,
537
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
538
+ });
539
+
540
+ if (!updated.length) return;
541
+
542
+ after(async () => {
543
+ await Promise.all(
544
+ updated.map((task) =>
545
+ events.publish(
546
+ taskUpdatedEvent,
547
+ {
548
+ actor: { id: context.user.id, name: context.user.name },
549
+ taskId: task.id,
550
+ projectId: task.projectId,
551
+ projectTitle: task.projectTitle,
552
+ code: task.code,
553
+ title: task.title,
554
+ before: task.before,
555
+ after: task.after,
556
+ },
557
+ { occurredAt: new Date().toISOString() },
558
+ ),
559
+ ),
560
+ );
561
+ });
430
562
  }),
431
563
  move: protectedProcedure
432
564
  .use(permissionMiddleware({ task: ["update"] }))
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import { formatDateTime, formatDuration } from "@tulip-systems/core";
4
+ import { Link } from "@tulip-systems/core/navigation";
4
5
  import { TableCell, type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
5
6
  import { createTableSelectCell } from "@tulip-systems/data-tables/client";
6
- import Link from "next/link";
7
7
  import type { User } from "@/server/auth/validators";
8
8
  import type { Customer } from "../../customers/_lib/validators";
9
9
  import type { Employee } from "../../employees/_lib/validators";
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { formatDuration } from "@tulip-systems/core";
4
+ import { Link } from "@tulip-systems/core/navigation";
4
5
  import { type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
5
6
  import { createTableSelectCell, TableReadOnlyCell } from "@tulip-systems/data-tables/client";
6
7
  import {
@@ -9,7 +10,6 @@ import {
9
10
  InlineTableCell,
10
11
  InlineTableCellError,
11
12
  } from "@tulip-systems/inline/client";
12
- import Link from "next/link";
13
13
  import { z } from "zod";
14
14
  import { orpc } from "@/server/router/client";
15
15
  import { formatDurationInDecimalHours } from "../_lib/helpers";