@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,97 @@
1
+ "use client";
2
+
3
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
4
+ import type { DefaultError, MutationOptions } from "@tanstack/react-query";
5
+ import {
6
+ CommandFormDialog,
7
+ CommandFormDialogCancel,
8
+ CommandFormDialogContent,
9
+ CommandFormDialogFields,
10
+ CommandFormDialogFooter,
11
+ CommandFormDialogHeader,
12
+ CommandFormDialogSubmit,
13
+ CommandFormDialogTitle,
14
+ CommandFormDialogTrigger,
15
+ } from "@tulip-systems/commands/client";
16
+ import { Input } from "@tulip-systems/ui/input";
17
+ import {
18
+ Form,
19
+ FormControl,
20
+ FormField,
21
+ FormItem,
22
+ FormLabel,
23
+ FormMessage,
24
+ } from "@tulip-systems/ui/react-hook-form";
25
+ import type React from "react";
26
+ import { useForm } from "react-hook-form";
27
+ import {
28
+ type CreateQuotationInput,
29
+ type CreateQuotationSchema,
30
+ createQuotationSchema,
31
+ } from "../_lib/validators";
32
+
33
+ export type CreateQuotationCommandProps<
34
+ TData = unknown,
35
+ TError = DefaultError,
36
+ TVariables = undefined,
37
+ TOnMutateResult = unknown,
38
+ > = {
39
+ variables: (values: CreateQuotationSchema) => TVariables;
40
+ mutation: MutationOptions<TData, TError, TVariables, TOnMutateResult>;
41
+ defaultValues?: Partial<CreateQuotationInput>;
42
+ children?: React.ReactNode;
43
+ };
44
+
45
+ export function CreateQuotationCommand<
46
+ TData = unknown,
47
+ TError = DefaultError,
48
+ TVariables = undefined,
49
+ TOnMutateResult = unknown,
50
+ >({
51
+ variables,
52
+ mutation,
53
+ defaultValues,
54
+ children,
55
+ }: CreateQuotationCommandProps<TData, TError, TVariables, TOnMutateResult>) {
56
+ const form = useForm<CreateQuotationInput>({
57
+ resolver: standardSchemaResolver(createQuotationSchema),
58
+ defaultValues,
59
+ });
60
+
61
+ return (
62
+ <CommandFormDialog>
63
+ {children}
64
+
65
+ <Form {...form}>
66
+ <CommandFormDialogContent variables={variables} mutation={mutation}>
67
+ <CommandFormDialogHeader>
68
+ <CommandFormDialogTitle>Offerte Toevoegen</CommandFormDialogTitle>
69
+ </CommandFormDialogHeader>
70
+
71
+ <CommandFormDialogFields>
72
+ <FormField
73
+ control={form.control}
74
+ name="title"
75
+ render={({ field }) => (
76
+ <FormItem>
77
+ <FormLabel>Titel</FormLabel>
78
+ <FormControl>
79
+ <Input {...field} />
80
+ </FormControl>
81
+ <FormMessage />
82
+ </FormItem>
83
+ )}
84
+ />
85
+ </CommandFormDialogFields>
86
+
87
+ <CommandFormDialogFooter>
88
+ <CommandFormDialogCancel>Annuleren</CommandFormDialogCancel>
89
+ <CommandFormDialogSubmit>Toevoegen</CommandFormDialogSubmit>
90
+ </CommandFormDialogFooter>
91
+ </CommandFormDialogContent>
92
+ </Form>
93
+ </CommandFormDialog>
94
+ );
95
+ }
96
+
97
+ export const CreateQuotationCommandTrigger = CommandFormDialogTrigger;
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import { useSuspenseQuery } from "@tanstack/react-query";
4
+ import type { VisibilityState } from "@tanstack/react-table";
5
+ import type { CommandFor } from "@tulip-systems/commands";
6
+ import type { TableColumnDef } from "@tulip-systems/data-tables";
7
+ import {
8
+ createTableConfig,
9
+ TableConfigProvider,
10
+ usePaginationStrategy,
11
+ } from "@tulip-systems/data-tables/client";
12
+ import { offsetPagination } from "@tulip-systems/query";
13
+ import {
14
+ type UseFiltersOptions,
15
+ useFilters,
16
+ usePagination,
17
+ useSorting,
18
+ } from "@tulip-systems/query/client";
19
+ import { type PropsWithChildren, useDeferredValue } from "react";
20
+ import { orpc } from "@/server/router/client";
21
+ import type { QuotationColumn } from "../_config/columns-data";
22
+ import { quotationFilters } from "../_config/filters";
23
+ import { quotationTableSorts } from "../_config/sorting";
24
+
25
+ type QuotationTableProviderProps = PropsWithChildren<{
26
+ columns: TableColumnDef<QuotationColumn>[];
27
+ commands?: CommandFor<QuotationColumn[]>[];
28
+ filterOptions?: UseFiltersOptions<typeof quotationFilters>;
29
+ columnVisibility?: VisibilityState;
30
+ }>;
31
+
32
+ export function QuotationPaginationTableProvider({
33
+ columns,
34
+ commands,
35
+ filterOptions,
36
+ columnVisibility,
37
+ children,
38
+ }: QuotationTableProviderProps) {
39
+ const pagination = usePagination(offsetPagination);
40
+ const filtering = useFilters(quotationFilters, filterOptions);
41
+ const sorting = useSorting(quotationTableSorts);
42
+
43
+ const input = useDeferredValue({
44
+ pagination: pagination.input,
45
+ filters: filtering.input,
46
+ sorts: sorting.input,
47
+ });
48
+
49
+ const response = useSuspenseQuery(orpc.quotations.list.queryOptions({ input }));
50
+
51
+ const queryData = response.data.data as QuotationColumn[];
52
+ const { total } = response.data.pagination;
53
+
54
+ const strategy = usePaginationStrategy({ pagination, total });
55
+
56
+ const config = createTableConfig<QuotationColumn>({
57
+ queryData,
58
+ getRowId: (row) => row.id,
59
+ columns,
60
+ strategy,
61
+ filtering,
62
+ sorting,
63
+ commands,
64
+ columnVisibility,
65
+ });
66
+
67
+ return <TableConfigProvider config={config}>{children}</TableConfigProvider>;
68
+ }
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ import { TableToolbar, TableToolbarList } from "@tulip-systems/data-tables";
4
+ import { resolveFilterDescriptors } from "@tulip-systems/query";
5
+ import { FilterSearch } from "@tulip-systems/query/client";
6
+ import { quotationFilters } from "../_config/filters";
7
+
8
+ const quotationFilterDescriptors = resolveFilterDescriptors(quotationFilters);
9
+
10
+ export function QuotationTableToolbar() {
11
+ return (
12
+ <TableToolbar>
13
+ <TableToolbarList>
14
+ <FilterSearch placeholder="Zoeken..." filter={quotationFilterDescriptors.search} />
15
+ </TableToolbarList>
16
+ </TableToolbar>
17
+ );
18
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { Link } from "@tulip-systems/core/navigation";
4
+ import { TableCell, type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
5
+ import { createTableSelectCell } from "@tulip-systems/data-tables/client";
6
+ import { format } from "date-fns";
7
+ import type { Quotation } from "../_lib/validators";
8
+
9
+ export type QuotationColumn = Quotation;
10
+
11
+ export const quotationColumns: TableColumnDef<QuotationColumn>[] = [
12
+ createTableSelectCell(),
13
+ {
14
+ id: "title",
15
+ accessorKey: "title",
16
+ header: ({ column }) => <TableColumnHeader column={column} title="Titel" />,
17
+ cell: ({ row }) => (
18
+ <TableCell>
19
+ <Link href={`/admin/quotations/${row.original.id}`} className="truncate hover:underline">
20
+ {row.getValue("title")}
21
+ </Link>
22
+ </TableCell>
23
+ ),
24
+ },
25
+ {
26
+ id: "createdAt",
27
+ accessorKey: "createdAt",
28
+ header: ({ column }) => <TableColumnHeader column={column} title="Aangemaakt" />,
29
+ cell: ({ row }) => (
30
+ <TableCell>
31
+ <span className="truncate">{format(row.original.createdAt, "dd/MM/yyyy")}</span>
32
+ </TableCell>
33
+ ),
34
+ },
35
+ ];
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { commandBuilder, ensureArray } from "@tulip-systems/commands";
4
+ import {
5
+ CommandEmpty,
6
+ CommandLabel,
7
+ type CommandMutationMeta,
8
+ DeleteCommand,
9
+ } from "@tulip-systems/commands/client";
10
+ import { Link } from "@tulip-systems/core/navigation";
11
+ import { toast } from "@tulip-systems/ui/sonner";
12
+ import { EyeIcon } from "lucide-react";
13
+ import { Icons } from "@/components/icons";
14
+ import { orpc } from "@/server/router/client";
15
+ import {
16
+ CreateQuotationCommand,
17
+ CreateQuotationCommandTrigger,
18
+ } from "../_components/create-command";
19
+ import type { CreateQuotationSchema, Quotation } from "../_lib/validators";
20
+
21
+ type QuotationCommandItem = Pick<Quotation, "id">;
22
+
23
+ export const quotationCreateCommand = commandBuilder
24
+ .$type<Partial<CreateQuotationSchema>>()
25
+ .permission({ quotation: ["create"] })
26
+ .render(() => (
27
+ <CreateQuotationCommand
28
+ variables={(values) => values}
29
+ mutation={orpc.quotations.create.mutationOptions({
30
+ onSuccess: (data, _vars, _result, context) => {
31
+ toast.success("Offerte succesvol toegevoegd");
32
+ const meta = context.meta as CommandMutationMeta;
33
+ if (data.id) meta.router.push(`/admin/quotations/${data.id}`);
34
+ },
35
+ })}
36
+ >
37
+ <CreateQuotationCommandTrigger label="Toevoegen" hotkey="mod+i">
38
+ <Icons.add className="size-4" />
39
+ <CommandLabel />
40
+ </CreateQuotationCommandTrigger>
41
+ </CreateQuotationCommand>
42
+ ));
43
+
44
+ export const quotationPreviewCommand = commandBuilder
45
+ .$type<QuotationCommandItem>()
46
+ .permission({ quotation: ["view"] })
47
+ .render(({ data }) => (
48
+ <Link href={`/admin/quotations/${data.id}/preview`} target="_blank" rel="noopener noreferrer">
49
+ <CommandEmpty label="Preview">
50
+ <EyeIcon className="size-4" />
51
+ <CommandLabel />
52
+ </CommandEmpty>
53
+ </Link>
54
+ ));
55
+
56
+ export const quotationDeleteCommand = commandBuilder
57
+ .$type<QuotationCommandItem | QuotationCommandItem[]>()
58
+ .transform(ensureArray)
59
+ .permission({ quotation: ["delete"] })
60
+ .render(({ data }) => (
61
+ <DeleteCommand
62
+ variables={{ ids: data.map((item) => item.id) }}
63
+ mutation={orpc.quotations.delete.mutationOptions({
64
+ onSuccess: (_data, _vars, _result, context) => {
65
+ toast.success("Offerte succesvol verwijderd");
66
+ const meta = context.meta as CommandMutationMeta;
67
+ meta.router.push("/admin/quotations");
68
+ },
69
+ })}
70
+ />
71
+ ));
@@ -0,0 +1,22 @@
1
+ import {
2
+ createFilteringParser,
3
+ defineFilters,
4
+ resolveFiltersSchema,
5
+ stringFilter,
6
+ } from "@tulip-systems/query";
7
+ import { createDatabaseFiltersCompiler } from "@tulip-systems/query/drizzle";
8
+ import { createLocalFiltersCompiler } from "@tulip-systems/query/tanstack-db";
9
+ import type z from "zod";
10
+
11
+ export const quotationFilters = defineFilters({
12
+ search: stringFilter.build((value) =>
13
+ value ? { field: "title", operator: "ilike", value: `%${value}%` } : null,
14
+ ),
15
+ });
16
+
17
+ export const quotationFiltersSchema = resolveFiltersSchema(quotationFilters);
18
+ export type QuotationFilters = z.input<typeof quotationFiltersSchema>;
19
+
20
+ export const quotationFiltersParser = createFilteringParser(quotationFilters);
21
+ export const quotationDatabaseFiltersCompiler = createDatabaseFiltersCompiler(quotationFilters);
22
+ export const quotationLocalFiltersCompiler = createLocalFiltersCompiler(quotationFilters);
@@ -0,0 +1,23 @@
1
+ import {
2
+ createSortingParser,
3
+ defineSorting,
4
+ resolveSortingSchema,
5
+ sortOption,
6
+ } from "@tulip-systems/query";
7
+ import { createDatabaseSortingCompiler } from "@tulip-systems/query/drizzle";
8
+ import { createLocalSortingCompiler } from "@tulip-systems/query/tanstack-db";
9
+
10
+ export const quotationTableSorts = defineSorting({
11
+ title: sortOption((direction) => ({ field: "title", direction })),
12
+ createdAt: sortOption((direction) => ({ field: "createdAt", direction })),
13
+ });
14
+
15
+ export const quotationTableSortsSchema = resolveSortingSchema(quotationTableSorts);
16
+
17
+ export const quotationTableSortsParser = createSortingParser(quotationTableSorts, {
18
+ fallback: [{ key: "createdAt", direction: "desc" }],
19
+ });
20
+
21
+ export const quotationTableLocalSortsCompiler = createLocalSortingCompiler(quotationTableSorts);
22
+ export const quotationTableDatabaseSortsCompiler =
23
+ createDatabaseSortingCompiler(quotationTableSorts);
@@ -0,0 +1,260 @@
1
+ import "server-cli-only";
2
+
3
+ import { ORPCError } from "@orpc/server";
4
+ import { moveBlock, reindexBlocks } from "@tulip-systems/blocks";
5
+ import {
6
+ bulkActionSchema,
7
+ offsetPaginationResponse,
8
+ offsetPaginationSchema,
9
+ } from "@tulip-systems/query";
10
+ import { asc, desc, eq, inArray } from "drizzle-orm";
11
+ import { z } from "zod";
12
+ import { getTableData } from "@/app/lib/utils/table-data.server";
13
+ import { permissionMiddleware } from "@/server/router/middleware";
14
+ import { protectedProcedure } from "@/server/router/procedures";
15
+ import {
16
+ type QuotationBlock,
17
+ type QuotationBlockInput,
18
+ quotationBlocks,
19
+ } from "../_blocks/registry";
20
+ import {
21
+ quotationDatabaseFiltersCompiler,
22
+ quotationFiltersParser,
23
+ quotationFiltersSchema,
24
+ } from "../_config/filters";
25
+ import {
26
+ quotationTableDatabaseSortsCompiler,
27
+ quotationTableSortsParser,
28
+ quotationTableSortsSchema,
29
+ } from "../_config/sorting";
30
+ import { quotationBlocks as quotationBlocksTable, quotations } from "./schema";
31
+ import { createQuotationSchema, updateQuotationSchema } from "./validators";
32
+
33
+ /**
34
+ * Router for handling operations related to quotations, including listing, retrieving by ID, creating, updating, and deleting quotations.
35
+ */
36
+ export const quotationsRouter = {
37
+ list: protectedProcedure
38
+ .use(permissionMiddleware({ quotation: ["view"] }))
39
+ .input(
40
+ z.object({
41
+ pagination: offsetPaginationSchema,
42
+ filters: quotationFiltersSchema,
43
+ sorts: quotationTableSortsSchema,
44
+ }),
45
+ )
46
+ .handler(async ({ context, input }) => {
47
+ const parsedSorts = quotationTableSortsParser.parse(input.sorts);
48
+ const orderBy = quotationTableDatabaseSortsCompiler(quotations).parse(parsedSorts);
49
+
50
+ const parsedFilters = quotationFiltersParser.parse(input.filters);
51
+ const where = quotationDatabaseFiltersCompiler(quotations).parse(parsedFilters);
52
+
53
+ const { data, total } = await getTableData(context, quotations, {
54
+ pagination: input.pagination,
55
+ where,
56
+ orderBy,
57
+ });
58
+
59
+ return offsetPaginationResponse({ data, pagination: input.pagination, total });
60
+ }),
61
+ getById: protectedProcedure
62
+ .use(permissionMiddleware({ quotation: ["view"] }))
63
+ .input(z.object({ id: z.string() }))
64
+ .handler(async ({ context, input }) => {
65
+ const data = await context.db.query.quotations.findFirst({
66
+ where: eq(quotations.id, input.id),
67
+ });
68
+
69
+ return data ?? null;
70
+ }),
71
+ create: protectedProcedure
72
+ .use(permissionMiddleware({ quotation: ["create"] }))
73
+ .input(createQuotationSchema)
74
+ .handler(async ({ context, input }) => {
75
+ return context.db.transaction(async (tx) => {
76
+ const [quotation] = await tx.insert(quotations).values(input).returning();
77
+
78
+ if (!quotation) {
79
+ throw new ORPCError("INTERNAL_SERVER_ERROR", {
80
+ message: "Offerte kon niet worden aangemaakt",
81
+ });
82
+ }
83
+
84
+ return quotation;
85
+ });
86
+ }),
87
+ update: protectedProcedure
88
+ .use(permissionMiddleware({ quotation: ["update"] }))
89
+ .input(z.object({ id: z.string(), data: updateQuotationSchema.partial() }))
90
+ .handler(async ({ context, input }) => {
91
+ const [result] = await context.db
92
+ .update(quotations)
93
+ .set(input.data)
94
+ .where(eq(quotations.id, input.id))
95
+ .returning();
96
+
97
+ if (!result) {
98
+ throw new ORPCError("NOT_FOUND", {
99
+ message: "Offerte kon niet worden gewijzigd",
100
+ });
101
+ }
102
+
103
+ return result;
104
+ }),
105
+
106
+ delete: protectedProcedure
107
+ .use(permissionMiddleware({ quotation: ["delete"] }))
108
+ .input(bulkActionSchema)
109
+ .handler(async ({ context, input }) => {
110
+ await context.db.delete(quotations).where(inArray(quotations.id, input.ids));
111
+ }),
112
+ };
113
+
114
+ /**
115
+ * Quotation blocks router that handles operations related to quotation blocks, including listing, creating, updating, and moving blocks.
116
+ */
117
+ export const quotationBlocksRouter = {
118
+ list: protectedProcedure
119
+ .use(permissionMiddleware({ quotation: ["view"] }))
120
+ .input(z.object({ quotationId: z.string() }))
121
+ .handler(async ({ context, input }) => {
122
+ const blocks = await context.db.query.quotationBlocks.findMany({
123
+ where: eq(quotationBlocksTable.quotationId, input.quotationId),
124
+ orderBy: asc(quotationBlocksTable.index),
125
+ });
126
+
127
+ return blocks;
128
+ }),
129
+ create: protectedProcedure
130
+ .use(permissionMiddleware({ quotation: ["update"] }))
131
+ .input(z.object({ quotationId: z.string(), block: quotationBlocks.createSchema }))
132
+ .handler(async ({ context, input }) => {
133
+ const block = input.block as QuotationBlockInput;
134
+ const lastBlock = await context.db
135
+ .select({ index: quotationBlocksTable.index })
136
+ .from(quotationBlocksTable)
137
+ .where(eq(quotationBlocksTable.quotationId, input.quotationId))
138
+ .orderBy(desc(quotationBlocksTable.index))
139
+ .limit(1);
140
+
141
+ const [created] = await context.db
142
+ .insert(quotationBlocksTable)
143
+ .values({
144
+ quotationId: input.quotationId,
145
+ slug: block.slug,
146
+ data: block.data,
147
+ index: (lastBlock[0]?.index ?? -1) + 1,
148
+ })
149
+ .returning();
150
+
151
+ if (!created) {
152
+ throw new ORPCError("INTERNAL_SERVER_ERROR", {
153
+ message: "Blok kon niet worden aangemaakt",
154
+ });
155
+ }
156
+
157
+ return quotationBlocks.selectSchema.parse(created) as QuotationBlock;
158
+ }),
159
+ update: protectedProcedure
160
+ .use(permissionMiddleware({ quotation: ["update"] }))
161
+ .input(z.object({ id: z.string(), data: z.record(z.string(), z.unknown()) }))
162
+ .handler(async ({ context, input }) => {
163
+ const existing = await context.db.query.quotationBlocks.findFirst({
164
+ where: eq(quotationBlocksTable.id, input.id),
165
+ });
166
+
167
+ if (!existing) {
168
+ throw new ORPCError("NOT_FOUND", {
169
+ message: "Blok kon niet worden gevonden",
170
+ });
171
+ }
172
+
173
+ const patchedData = quotationBlocks.patchData({ block: existing, patch: input.data });
174
+
175
+ if (!patchedData.success) {
176
+ throw new ORPCError("BAD_REQUEST", {
177
+ message: "Onbekend bloktype",
178
+ });
179
+ }
180
+
181
+ const [updated] = await context.db
182
+ .update(quotationBlocksTable)
183
+ .set({ data: patchedData.data })
184
+ .where(eq(quotationBlocksTable.id, input.id))
185
+ .returning();
186
+
187
+ if (!updated) {
188
+ throw new ORPCError("INTERNAL_SERVER_ERROR", {
189
+ message: "Blok kon niet worden gewijzigd",
190
+ });
191
+ }
192
+
193
+ return quotationBlocks.selectSchema.parse(updated) as QuotationBlock;
194
+ }),
195
+ delete: protectedProcedure
196
+ .use(permissionMiddleware({ quotation: ["update"] }))
197
+ .input(z.object({ id: z.string() }))
198
+ .handler(async ({ context, input }) => {
199
+ const existing = await context.db.query.quotationBlocks.findFirst({
200
+ where: eq(quotationBlocksTable.id, input.id),
201
+ });
202
+
203
+ if (!existing) {
204
+ throw new ORPCError("NOT_FOUND", {
205
+ message: "Blok kon niet worden gevonden",
206
+ });
207
+ }
208
+
209
+ await context.db.transaction(async (tx) => {
210
+ await tx.delete(quotationBlocksTable).where(eq(quotationBlocksTable.id, input.id));
211
+
212
+ const blocks = await tx.query.quotationBlocks.findMany({
213
+ where: eq(quotationBlocksTable.quotationId, existing.quotationId),
214
+ orderBy: asc(quotationBlocksTable.index),
215
+ });
216
+ const reindexedBlocks = reindexBlocks(blocks);
217
+
218
+ await Promise.all(
219
+ reindexedBlocks.map((block) =>
220
+ tx
221
+ .update(quotationBlocksTable)
222
+ .set({ index: block.index })
223
+ .where(eq(quotationBlocksTable.id, block.id)),
224
+ ),
225
+ );
226
+ });
227
+
228
+ return { success: true };
229
+ }),
230
+ move: protectedProcedure
231
+ .use(permissionMiddleware({ quotation: ["update"] }))
232
+ .input(z.object({ quotationId: z.string(), id: z.string(), toIndex: z.number().int().min(0) }))
233
+ .handler(async ({ context, input }) => {
234
+ const blocks = await context.db.query.quotationBlocks.findMany({
235
+ where: eq(quotationBlocksTable.quotationId, input.quotationId),
236
+ orderBy: asc(quotationBlocksTable.index),
237
+ });
238
+
239
+ const result = moveBlock(blocks, { id: input.id, toIndex: input.toIndex });
240
+
241
+ if (!result) {
242
+ throw new ORPCError("NOT_FOUND", {
243
+ message: "Blok kon niet worden gevonden",
244
+ });
245
+ }
246
+
247
+ await context.db.transaction(async (tx) => {
248
+ await Promise.all(
249
+ result.blocks.map((block) =>
250
+ tx
251
+ .update(quotationBlocksTable)
252
+ .set({ index: block.index })
253
+ .where(eq(quotationBlocksTable.id, block.id)),
254
+ ),
255
+ );
256
+ });
257
+
258
+ return z.array(quotationBlocks.selectSchema).parse(result.blocks) as QuotationBlock[];
259
+ }),
260
+ };
@@ -0,0 +1,42 @@
1
+ import { relations } from "drizzle-orm";
2
+ import { index, pgTable } from "drizzle-orm/pg-core";
3
+ import { baseColumns } from "@/server/db/helpers";
4
+
5
+ export const quotations = pgTable("quotations", (t) => ({
6
+ ...baseColumns,
7
+ title: t.varchar().notNull(),
8
+ }));
9
+
10
+ export const quotationsRelations = relations(quotations, ({ many }) => ({
11
+ blocks: many(quotationBlocks),
12
+ }));
13
+
14
+ /**
15
+ *
16
+ *
17
+ * Quotations Blocks
18
+ */
19
+ export const quotationBlocks = pgTable(
20
+ "quotation_blocks",
21
+ (t) => ({
22
+ ...baseColumns,
23
+ slug: t.varchar().notNull(),
24
+ data: t.jsonb().notNull(),
25
+ index: t.doublePrecision().notNull(),
26
+ quotationId: t
27
+ .uuid()
28
+ .notNull()
29
+ .references(() => quotations.id, { onDelete: "cascade" }),
30
+ }),
31
+ (table) => [
32
+ index("quotation_blocks_quotation_order_idx").on(table.quotationId, table.index),
33
+ index("quotation_blocks_quotation_slug_idx").on(table.quotationId, table.slug),
34
+ ],
35
+ );
36
+
37
+ export const quotationBlocksRelations = relations(quotationBlocks, ({ one }) => ({
38
+ quotation: one(quotations, {
39
+ fields: [quotationBlocks.quotationId],
40
+ references: [quotations.id],
41
+ }),
42
+ }));
@@ -0,0 +1,20 @@
1
+ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
2
+ import { z } from "zod";
3
+ import { quotations } from "./schema";
4
+
5
+ export const quotationSchema = createSelectSchema(quotations);
6
+ export type Quotation = z.infer<typeof quotationSchema>;
7
+
8
+ export const createQuotationSchema = createInsertSchema(quotations, {
9
+ title: z.string().min(1, "Titel is verplicht"),
10
+ }).omit({ id: true, createdAt: true, updatedAt: true });
11
+
12
+ export const updateQuotationSchema = createInsertSchema(quotations, {
13
+ title: z.string().min(1, "Titel is verplicht"),
14
+ }).omit({ id: true, createdAt: true, updatedAt: true });
15
+
16
+ export type CreateQuotationInput = z.input<typeof createQuotationSchema>;
17
+ export type CreateQuotationSchema = z.infer<typeof createQuotationSchema>;
18
+
19
+ export type UpdateQuotationInput = z.input<typeof updateQuotationSchema>;
20
+ export type UpdateQuotationSchema = z.infer<typeof updateQuotationSchema>;
@@ -0,0 +1,6 @@
1
+ import { TableLayout } from "@tulip-systems/data-tables";
2
+ import type { PropsWithChildren } from "react";
3
+
4
+ export default function Layout(props: PropsWithChildren) {
5
+ return <TableLayout className="p-content">{props.children}</TableLayout>;
6
+ }
@@ -0,0 +1,5 @@
1
+ import { TableSkeleton } from "@tulip-systems/data-tables";
2
+
3
+ export default function Loading() {
4
+ return <TableSkeleton />;
5
+ }