@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 type { BlockDefinition } from "@tulip-systems/blocks";
2
+ import { z } from "zod";
3
+
4
+ export const quotationSummaryBlock = {
5
+ slug: "summary",
6
+ label: "Summary",
7
+ description: "Short commercial summary for the quotation.",
8
+ schema: z.object({
9
+ heading: z.string().min(1),
10
+ body: z.string().min(1),
11
+ highlights: z.array(z.string().min(1)),
12
+ }),
13
+ } as const satisfies BlockDefinition;
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
4
+ import type { DefaultError } from "@tanstack/react-query";
5
+ import type { BlockCreateComponentProps } from "@tulip-systems/blocks";
6
+ import {
7
+ CreateBlock,
8
+ CreateBlockCancel,
9
+ CreateBlockContent,
10
+ CreateBlockDescription,
11
+ CreateBlockFields,
12
+ CreateBlockFooter,
13
+ CreateBlockHeader,
14
+ CreateBlockSubmit,
15
+ CreateBlockTitle,
16
+ CreateBlockTrigger,
17
+ CreateBlockTriggerContent,
18
+ CreateBlockTriggerDescription,
19
+ CreateBlockTriggerIcon,
20
+ CreateBlockTriggerTitle,
21
+ } from "@tulip-systems/blocks/client";
22
+ import { Input } from "@tulip-systems/ui/input";
23
+ import {
24
+ Form,
25
+ FormControl,
26
+ FormDescription,
27
+ FormField,
28
+ FormItem,
29
+ FormLabel,
30
+ FormMessage,
31
+ } from "@tulip-systems/ui/react-hook-form";
32
+ import { Textarea } from "@tulip-systems/ui/textarea";
33
+ import { BlocksIcon } from "lucide-react";
34
+ import { useForm } from "react-hook-form";
35
+ import type { z } from "zod";
36
+ import { quotationSummaryBlock } from "./config";
37
+
38
+ export type QuotationSummaryCreateValues = z.input<typeof quotationSummaryBlock.schema>;
39
+
40
+ type QuotationSummaryFormValues = Omit<QuotationSummaryCreateValues, "highlights"> & {
41
+ highlights: string;
42
+ };
43
+
44
+ type QuotationSummaryCreateBlockInput = {
45
+ slug: typeof quotationSummaryBlock.slug;
46
+ data: QuotationSummaryCreateValues;
47
+ };
48
+
49
+ type QuotationSummaryCreateBlockProps<
50
+ TData = unknown,
51
+ TError = DefaultError,
52
+ TVariables = undefined,
53
+ TOnMutateResult = unknown,
54
+ > = BlockCreateComponentProps<
55
+ QuotationSummaryCreateBlockInput,
56
+ TData,
57
+ TError,
58
+ TVariables,
59
+ TOnMutateResult
60
+ >;
61
+
62
+ export function QuotationSummaryCreateBlock<
63
+ TData = unknown,
64
+ TError = DefaultError,
65
+ TVariables = undefined,
66
+ TOnMutateResult = unknown,
67
+ >({
68
+ variables,
69
+ mutation,
70
+ }: QuotationSummaryCreateBlockProps<TData, TError, TVariables, TOnMutateResult>) {
71
+ const form = useForm<QuotationSummaryFormValues>({
72
+ resolver: standardSchemaResolver(
73
+ quotationSummaryBlock.schema.extend({ highlights: quotationSummaryBlock.schema.shape.body }),
74
+ ),
75
+ defaultValues: {
76
+ heading: "Heldere scope, snelle oplevering",
77
+ body: "Beschrijf hier kort de waarde en scope van deze offerte.",
78
+ highlights: "Vaste opleverdatum, Duidelijke scope, Transparante prijs",
79
+ },
80
+ });
81
+
82
+ return (
83
+ <CreateBlock>
84
+ <CreateBlockTrigger>
85
+ <CreateBlockTriggerIcon>
86
+ <BlocksIcon />
87
+ </CreateBlockTriggerIcon>
88
+
89
+ <CreateBlockTriggerContent>
90
+ <CreateBlockTriggerTitle>{quotationSummaryBlock.label}</CreateBlockTriggerTitle>
91
+ <CreateBlockTriggerDescription>
92
+ {quotationSummaryBlock.description}
93
+ </CreateBlockTriggerDescription>
94
+ </CreateBlockTriggerContent>
95
+ </CreateBlockTrigger>
96
+
97
+ <Form {...form}>
98
+ <CreateBlockContent
99
+ className="sm:max-w-xl"
100
+ variables={(values: QuotationSummaryFormValues) =>
101
+ variables({
102
+ slug: quotationSummaryBlock.slug,
103
+ data: {
104
+ heading: values.heading,
105
+ body: values.body,
106
+ highlights: values.highlights
107
+ .split(",")
108
+ .map((item) => item.trim())
109
+ .filter(Boolean),
110
+ },
111
+ })
112
+ }
113
+ mutation={mutation}
114
+ >
115
+ <CreateBlockHeader>
116
+ <CreateBlockTitle>{quotationSummaryBlock.label} toevoegen</CreateBlockTitle>
117
+ <CreateBlockDescription>
118
+ Vul de verplichte velden in om het blok toe te voegen.
119
+ </CreateBlockDescription>
120
+ </CreateBlockHeader>
121
+
122
+ <CreateBlockFields>
123
+ <FormField
124
+ control={form.control}
125
+ name="heading"
126
+ render={({ field }) => (
127
+ <FormItem>
128
+ <FormLabel>Kop</FormLabel>
129
+ <FormControl>
130
+ <Input {...field} />
131
+ </FormControl>
132
+ <FormMessage />
133
+ </FormItem>
134
+ )}
135
+ />
136
+
137
+ <FormField
138
+ control={form.control}
139
+ name="body"
140
+ render={({ field }) => (
141
+ <FormItem>
142
+ <FormLabel>Beschrijving</FormLabel>
143
+ <FormControl>
144
+ <Textarea {...field} />
145
+ </FormControl>
146
+ <FormMessage />
147
+ </FormItem>
148
+ )}
149
+ />
150
+
151
+ <FormField
152
+ control={form.control}
153
+ name="highlights"
154
+ render={({ field }) => (
155
+ <FormItem>
156
+ <FormLabel>Highlights</FormLabel>
157
+ <FormControl>
158
+ <Input {...field} />
159
+ </FormControl>
160
+ <FormDescription>Komma-gescheiden lijst.</FormDescription>
161
+ <FormMessage />
162
+ </FormItem>
163
+ )}
164
+ />
165
+ </CreateBlockFields>
166
+
167
+ <CreateBlockFooter>
168
+ <CreateBlockCancel>Annuleren</CreateBlockCancel>
169
+ <CreateBlockSubmit>Blok toevoegen</CreateBlockSubmit>
170
+ </CreateBlockFooter>
171
+ </CreateBlockContent>
172
+ </Form>
173
+ </CreateBlock>
174
+ );
175
+ }
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import type { BlockComponentProps } from "@tulip-systems/blocks";
4
+ import {
5
+ InlineBlock,
6
+ InlineBlockContent,
7
+ InlineBlockDragHandle,
8
+ InlineBlockHeader,
9
+ InlineBlockSortable,
10
+ InlineBlockTrigger,
11
+ } from "@tulip-systems/blocks/client";
12
+ import { DropdownCommandMenu } from "@tulip-systems/commands/client";
13
+ import {
14
+ InlineField,
15
+ InlineFieldError,
16
+ InlineFieldLabel,
17
+ InlineStringInput,
18
+ } from "@tulip-systems/inline/client";
19
+ import { GripIcon } from "lucide-react";
20
+ import { z } from "zod";
21
+ import { orpc } from "@/server/router/client";
22
+ import { quotationInlineBlockCommands } from "../commands";
23
+ import { quotationSummaryBlock } from "./config";
24
+
25
+ const summaryMutation = orpc.quotationBlocks.update.mutationOptions({
26
+ meta: { invalidates: [orpc.quotationBlocks.key()] },
27
+ });
28
+
29
+ export function QuotationSummaryInlineBlock({
30
+ block,
31
+ }: BlockComponentProps<typeof quotationSummaryBlock>) {
32
+ return (
33
+ <InlineBlockSortable id={block.id}>
34
+ <InlineBlock>
35
+ <InlineBlockHeader>
36
+ <InlineBlockDragHandle>
37
+ <GripIcon className="h-4 w-4" />
38
+ <span className="sr-only">Blok verplaatsen</span>
39
+ </InlineBlockDragHandle>
40
+ <InlineBlockTrigger>{quotationSummaryBlock.label}</InlineBlockTrigger>
41
+ <DropdownCommandMenu
42
+ data={{ id: block.id }}
43
+ commands={quotationInlineBlockCommands}
44
+ className="ml-2 size-7"
45
+ />
46
+ </InlineBlockHeader>
47
+
48
+ <InlineBlockContent className="grid gap-4 md:grid-cols-2">
49
+ <InlineField>
50
+ <InlineFieldLabel>Kop</InlineFieldLabel>
51
+ <InlineStringInput
52
+ initialValue={block.data.heading}
53
+ variables={(value) => ({ id: block.id, data: { heading: value ?? "" } })}
54
+ mutation={summaryMutation}
55
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
56
+ />
57
+ <InlineFieldError />
58
+ </InlineField>
59
+
60
+ <InlineField className="md:col-span-2">
61
+ <InlineFieldLabel>Beschrijving</InlineFieldLabel>
62
+ <InlineStringInput
63
+ initialValue={block.data.body}
64
+ variables={(value) => ({ id: block.id, data: { body: value ?? "" } })}
65
+ mutation={summaryMutation}
66
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
67
+ />
68
+ <InlineFieldError />
69
+ </InlineField>
70
+
71
+ <InlineField className="md:col-span-2">
72
+ <InlineFieldLabel>Highlights</InlineFieldLabel>
73
+ <InlineStringInput
74
+ initialValue={block.data.highlights.join(", ")}
75
+ variables={(value) => ({
76
+ id: block.id,
77
+ data: {
78
+ highlights: (value ?? "")
79
+ .split(",")
80
+ .map((item) => item.trim())
81
+ .filter(Boolean),
82
+ },
83
+ })}
84
+ mutation={summaryMutation}
85
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
86
+ />
87
+ <InlineFieldError />
88
+ </InlineField>
89
+ </InlineBlockContent>
90
+ </InlineBlock>
91
+ </InlineBlockSortable>
92
+ );
93
+ }
@@ -0,0 +1,25 @@
1
+ import type { BlockComponentProps } from "@tulip-systems/blocks";
2
+ import type { quotationSummaryBlock } from "./config";
3
+
4
+ export function QuotationSummaryPreview({
5
+ block,
6
+ }: BlockComponentProps<typeof quotationSummaryBlock>) {
7
+ const { body, heading, highlights } = block.data;
8
+
9
+ return (
10
+ <section className="grid gap-6 rounded-3xl border bg-background p-8 md:grid-cols-[1fr_18rem]">
11
+ <div>
12
+ <h2 className="font-semibold text-2xl tracking-tight">{heading}</h2>
13
+ <p className="mt-4 text-muted-foreground leading-7">{body}</p>
14
+ </div>
15
+
16
+ <ul className="space-y-3">
17
+ {highlights.map((highlight) => (
18
+ <li key={highlight} className="rounded-2xl bg-muted/50 px-4 py-3 text-sm">
19
+ {highlight}
20
+ </li>
21
+ ))}
22
+ </ul>
23
+ </section>
24
+ );
25
+ }
@@ -0,0 +1,18 @@
1
+ import type { BlockDefinition } from "@tulip-systems/blocks";
2
+ import { z } from "zod";
3
+
4
+ export const quotationTotalsBlock = {
5
+ slug: "totals",
6
+ label: "Totals",
7
+ description: "Line totals and final quotation amount.",
8
+ schema: z.object({
9
+ currency: z.string().min(1),
10
+ items: z.array(
11
+ z.object({
12
+ label: z.string().min(1),
13
+ amount: z.number(),
14
+ }),
15
+ ),
16
+ total: z.number(),
17
+ }),
18
+ } as const satisfies BlockDefinition;
@@ -0,0 +1,177 @@
1
+ "use client";
2
+
3
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
4
+ import type { DefaultError } from "@tanstack/react-query";
5
+ import type { BlockCreateComponentProps } from "@tulip-systems/blocks";
6
+ import {
7
+ CreateBlock,
8
+ CreateBlockCancel,
9
+ CreateBlockContent,
10
+ CreateBlockDescription,
11
+ CreateBlockFields,
12
+ CreateBlockFooter,
13
+ CreateBlockHeader,
14
+ CreateBlockSubmit,
15
+ CreateBlockTitle,
16
+ CreateBlockTrigger,
17
+ CreateBlockTriggerContent,
18
+ CreateBlockTriggerDescription,
19
+ CreateBlockTriggerIcon,
20
+ CreateBlockTriggerTitle,
21
+ } from "@tulip-systems/blocks/client";
22
+ import { Input } from "@tulip-systems/ui/input";
23
+ import {
24
+ Form,
25
+ FormControl,
26
+ FormField,
27
+ FormItem,
28
+ FormLabel,
29
+ FormMessage,
30
+ } from "@tulip-systems/ui/react-hook-form";
31
+ import { BlocksIcon } from "lucide-react";
32
+ import { useForm } from "react-hook-form";
33
+ import { z } from "zod";
34
+ import { quotationTotalsBlock } from "./config";
35
+
36
+ export type QuotationTotalsCreateValues = z.input<typeof quotationTotalsBlock.schema>;
37
+
38
+ const totalsCreateFormSchema = z.object({
39
+ currency: quotationTotalsBlock.schema.shape.currency,
40
+ itemLabel: quotationTotalsBlock.schema.shape.items.element.shape.label,
41
+ itemAmount: quotationTotalsBlock.schema.shape.items.element.shape.amount,
42
+ });
43
+
44
+ type QuotationTotalsFormValues = z.input<typeof totalsCreateFormSchema>;
45
+
46
+ type QuotationTotalsCreateBlockInput = {
47
+ slug: typeof quotationTotalsBlock.slug;
48
+ data: QuotationTotalsCreateValues;
49
+ };
50
+
51
+ type QuotationTotalsCreateBlockProps<
52
+ TData = unknown,
53
+ TError = DefaultError,
54
+ TVariables = undefined,
55
+ TOnMutateResult = unknown,
56
+ > = BlockCreateComponentProps<
57
+ QuotationTotalsCreateBlockInput,
58
+ TData,
59
+ TError,
60
+ TVariables,
61
+ TOnMutateResult
62
+ >;
63
+
64
+ export function QuotationTotalsCreateBlock<
65
+ TData = unknown,
66
+ TError = DefaultError,
67
+ TVariables = undefined,
68
+ TOnMutateResult = unknown,
69
+ >({
70
+ variables,
71
+ mutation,
72
+ }: QuotationTotalsCreateBlockProps<TData, TError, TVariables, TOnMutateResult>) {
73
+ const form = useForm<QuotationTotalsFormValues>({
74
+ resolver: standardSchemaResolver(totalsCreateFormSchema),
75
+ defaultValues: {
76
+ currency: "EUR",
77
+ itemLabel: "Implementatie",
78
+ itemAmount: 0,
79
+ },
80
+ });
81
+
82
+ return (
83
+ <CreateBlock>
84
+ <CreateBlockTrigger>
85
+ <CreateBlockTriggerIcon>
86
+ <BlocksIcon />
87
+ </CreateBlockTriggerIcon>
88
+
89
+ <CreateBlockTriggerContent>
90
+ <CreateBlockTriggerTitle>{quotationTotalsBlock.label}</CreateBlockTriggerTitle>
91
+ <CreateBlockTriggerDescription>
92
+ {quotationTotalsBlock.description}
93
+ </CreateBlockTriggerDescription>
94
+ </CreateBlockTriggerContent>
95
+ </CreateBlockTrigger>
96
+
97
+ <Form {...form}>
98
+ <CreateBlockContent
99
+ className="sm:max-w-xl"
100
+ variables={(values: QuotationTotalsFormValues) =>
101
+ variables({
102
+ slug: quotationTotalsBlock.slug,
103
+ data: {
104
+ currency: values.currency,
105
+ items: [{ label: values.itemLabel, amount: values.itemAmount }],
106
+ total: values.itemAmount,
107
+ },
108
+ })
109
+ }
110
+ mutation={mutation}
111
+ >
112
+ <CreateBlockHeader>
113
+ <CreateBlockTitle>{quotationTotalsBlock.label} toevoegen</CreateBlockTitle>
114
+ <CreateBlockDescription>
115
+ Vul de verplichte velden in om het blok toe te voegen.
116
+ </CreateBlockDescription>
117
+ </CreateBlockHeader>
118
+
119
+ <CreateBlockFields>
120
+ <FormField
121
+ control={form.control}
122
+ name="currency"
123
+ render={({ field }) => (
124
+ <FormItem>
125
+ <FormLabel>Valuta</FormLabel>
126
+ <FormControl>
127
+ <Input {...field} />
128
+ </FormControl>
129
+ <FormMessage />
130
+ </FormItem>
131
+ )}
132
+ />
133
+
134
+ <div className="grid gap-4 md:grid-cols-2">
135
+ <FormField
136
+ control={form.control}
137
+ name="itemLabel"
138
+ render={({ field }) => (
139
+ <FormItem>
140
+ <FormLabel>Regel</FormLabel>
141
+ <FormControl>
142
+ <Input {...field} />
143
+ </FormControl>
144
+ <FormMessage />
145
+ </FormItem>
146
+ )}
147
+ />
148
+
149
+ <FormField
150
+ control={form.control}
151
+ name="itemAmount"
152
+ render={({ field }) => (
153
+ <FormItem>
154
+ <FormLabel>Bedrag</FormLabel>
155
+ <FormControl>
156
+ <Input
157
+ {...field}
158
+ type="number"
159
+ onChange={(event) => field.onChange(event.currentTarget.valueAsNumber || 0)}
160
+ />
161
+ </FormControl>
162
+ <FormMessage />
163
+ </FormItem>
164
+ )}
165
+ />
166
+ </div>
167
+ </CreateBlockFields>
168
+
169
+ <CreateBlockFooter>
170
+ <CreateBlockCancel>Annuleren</CreateBlockCancel>
171
+ <CreateBlockSubmit>Blok toevoegen</CreateBlockSubmit>
172
+ </CreateBlockFooter>
173
+ </CreateBlockContent>
174
+ </Form>
175
+ </CreateBlock>
176
+ );
177
+ }
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import type { BlockComponentProps } from "@tulip-systems/blocks";
4
+ import {
5
+ InlineBlock,
6
+ InlineBlockContent,
7
+ InlineBlockDragHandle,
8
+ InlineBlockHeader,
9
+ InlineBlockSortable,
10
+ InlineBlockTrigger,
11
+ } from "@tulip-systems/blocks/client";
12
+ import { DropdownCommandMenu } from "@tulip-systems/commands/client";
13
+ import {
14
+ InlineField,
15
+ InlineFieldError,
16
+ InlineFieldLabel,
17
+ InlineNumberInput,
18
+ InlineStringInput,
19
+ } from "@tulip-systems/inline/client";
20
+ import { GripIcon } from "lucide-react";
21
+ import { z } from "zod";
22
+ import { orpc } from "@/server/router/client";
23
+ import { quotationInlineBlockCommands } from "../commands";
24
+ import { quotationTotalsBlock } from "./config";
25
+
26
+ const totalsMutation = orpc.quotationBlocks.update.mutationOptions({
27
+ meta: { invalidates: [orpc.quotationBlocks.key()] },
28
+ });
29
+
30
+ export function QuotationTotalsInlineBlock({
31
+ block,
32
+ }: BlockComponentProps<typeof quotationTotalsBlock>) {
33
+ return (
34
+ <InlineBlockSortable id={block.id}>
35
+ <InlineBlock>
36
+ <InlineBlockHeader>
37
+ <InlineBlockDragHandle>
38
+ <GripIcon className="h-4 w-4" />
39
+ <span className="sr-only">Blok verplaatsen</span>
40
+ </InlineBlockDragHandle>
41
+ <InlineBlockTrigger>{quotationTotalsBlock.label}</InlineBlockTrigger>
42
+ <DropdownCommandMenu
43
+ data={{ id: block.id }}
44
+ commands={quotationInlineBlockCommands}
45
+ className="ml-2 size-7"
46
+ />
47
+ </InlineBlockHeader>
48
+
49
+ <InlineBlockContent className="grid gap-4 md:grid-cols-2">
50
+ <InlineField>
51
+ <InlineFieldLabel>Valuta</InlineFieldLabel>
52
+ <InlineStringInput
53
+ initialValue={block.data.currency}
54
+ variables={(value) => ({ id: block.id, data: { currency: value ?? "EUR" } })}
55
+ mutation={totalsMutation}
56
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
57
+ />
58
+ <InlineFieldError />
59
+ </InlineField>
60
+
61
+ <InlineField>
62
+ <InlineFieldLabel>Totaal</InlineFieldLabel>
63
+ <InlineNumberInput
64
+ initialValue={block.data.total}
65
+ variables={(value) => ({ id: block.id, data: { total: value ?? 0 } })}
66
+ mutation={totalsMutation}
67
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.number().min(0) } }}
68
+ />
69
+ <InlineFieldError />
70
+ </InlineField>
71
+
72
+ {block.data.items.slice(0, 2).map((item, index) => (
73
+ <InlineField key={item.label}>
74
+ <InlineFieldLabel>Regel {index + 1}</InlineFieldLabel>
75
+ <InlineStringInput
76
+ initialValue={`${item.label}: ${item.amount}`}
77
+ variables={(value) => ({
78
+ id: block.id,
79
+ data: {
80
+ items: block.data.items.with(index, {
81
+ ...item,
82
+ label: value?.split(":")[0]?.trim() || item.label,
83
+ }),
84
+ },
85
+ })}
86
+ mutation={totalsMutation}
87
+ strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
88
+ />
89
+ <InlineFieldError />
90
+ </InlineField>
91
+ ))}
92
+ </InlineBlockContent>
93
+ </InlineBlock>
94
+ </InlineBlockSortable>
95
+ );
96
+ }
@@ -0,0 +1,32 @@
1
+ import type { BlockComponentProps } from "@tulip-systems/blocks";
2
+ import type { quotationTotalsBlock } from "./config";
3
+
4
+ function formatCurrency(value: number, currency: string) {
5
+ return new Intl.NumberFormat("nl-BE", { style: "currency", currency }).format(value);
6
+ }
7
+
8
+ export function QuotationTotalsPreview({
9
+ block,
10
+ }: BlockComponentProps<typeof quotationTotalsBlock>) {
11
+ const { currency, items, total } = block.data;
12
+
13
+ return (
14
+ <section className="rounded-3xl border bg-card p-8 shadow-sm">
15
+ <h2 className="font-semibold text-2xl tracking-tight">Prijsopbouw</h2>
16
+
17
+ <div className="mt-6 divide-y rounded-2xl border">
18
+ {items.map((item) => (
19
+ <div key={item.label} className="flex items-center justify-between gap-4 p-4">
20
+ <span className="text-muted-foreground">{item.label}</span>
21
+ <span className="font-medium">{formatCurrency(item.amount, currency)}</span>
22
+ </div>
23
+ ))}
24
+
25
+ <div className="flex items-center justify-between gap-4 bg-muted/40 p-4 font-semibold text-lg">
26
+ <span>Totaal</span>
27
+ <span>{formatCurrency(total, currency)}</span>
28
+ </div>
29
+ </div>
30
+ </section>
31
+ );
32
+ }