@tulip-systems/create-app 0.12.2 → 0.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/templates/basic/package.json +5 -5
- package/dist/templates/code/package.json +33 -14
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf +0 -0
- package/dist/templates/code/public/images/preview/hero-home.webp +0 -0
- package/dist/templates/code/src/app/admin/customers/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/code/src/app/admin/projects/[id]/(default)/page.tsx +2 -0
- package/dist/templates/code/src/app/admin/prospects/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/code/src/app/admin/quotations/(list)/layout.tsx +24 -0
- package/dist/templates/code/src/app/admin/quotations/(list)/page.tsx +23 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/layout.tsx +40 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/page.tsx +56 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/preview/page.tsx +17 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/commands.tsx +24 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/config.ts +14 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/create.tsx +152 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/inline.tsx +90 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/preview.tsx +90 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/registry.ts +13 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/render.tsx +190 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/styles.ts +24 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/config.ts +13 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/create.tsx +175 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/inline.tsx +93 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/preview.tsx +25 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/config.ts +18 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/create.tsx +177 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/inline.tsx +96 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/preview.tsx +32 -0
- package/dist/templates/code/src/app/admin/quotations/_components/create-command.tsx +97 -0
- package/dist/templates/code/src/app/admin/quotations/_components/tables.tsx +68 -0
- package/dist/templates/code/src/app/admin/quotations/_components/toolbars.tsx +18 -0
- package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +35 -0
- package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +71 -0
- package/dist/templates/code/src/app/admin/quotations/_config/filters.ts +22 -0
- package/dist/templates/code/src/app/admin/quotations/_config/sorting.ts +23 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/router.ts +260 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/schema.ts +42 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/validators.ts +20 -0
- package/dist/templates/code/src/app/admin/tasks/[id]/(default)/@dashboard/page.tsx +6 -4
- package/dist/templates/code/src/lib/config/editor.ts +54 -0
- package/dist/templates/code/src/lib/config/paths.tsx +8 -0
- package/dist/templates/code/src/lib/utils/fonts.ts +102 -0
- package/dist/templates/code/src/server/auth/permissions.ts +4 -0
- package/dist/templates/code/src/server/db/schema.ts +1 -0
- package/dist/templates/code/src/server/router/router.ts +10 -0
- package/dist/templates/spark/package.json +30 -13
- package/dist/templates/spark/src/app/admin/assignments/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/companies/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/customers/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/emails/_components/send-email.tsx +5 -6
- package/dist/templates/spark/src/app/admin/executions/[id]/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/products/[id]/(preview)/page.tsx +2 -2
- package/dist/templates/spark/src/app/admin/products/[id]/dashboard/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/products/[id]/dashboard/editor/page.tsx +6 -8
- package/dist/templates/spark/src/app/admin/projects/[id]/(dashboard)/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/projects/[id]/messages/page.tsx +7 -1
- package/dist/templates/spark/src/app/admin/purchase-orders/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/purchase-orders/_config/columns-data.tsx +7 -1
- package/dist/templates/spark/src/app/admin/quotation-items/_config/columns-data.tsx +7 -1
- package/dist/templates/spark/src/app/admin/quotation-items/_config/columns-inline.tsx +2 -0
- package/dist/templates/spark/src/app/admin/quotations/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/run-tasks/_config/columns-data.tsx +7 -1
- package/dist/templates/spark/src/app/admin/runs/[id]/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/runs/[id]/reports/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/sales-invoice-items/_config/columns-data.tsx +7 -1
- package/dist/templates/spark/src/app/admin/sales-invoice-items/_config/columns-inline.tsx +2 -0
- package/dist/templates/spark/src/app/admin/sales-order-items/_config/columns-data.tsx +7 -1
- package/dist/templates/spark/src/app/admin/sales-order-items/_config/columns-inline.tsx +2 -0
- package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/suppliers/[id]/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/tasks/_config/columns-data.tsx +7 -1
- package/dist/templates/spark/src/app/admin/tasks/_config/columns-inline.tsx +2 -0
- package/dist/templates/spark/src/app/admin/technical-sheets/[id]/page.tsx +2 -0
- package/dist/templates/spark/src/app/admin/technical-sheets/_config/columns-data.tsx +13 -2
- package/dist/templates/spark/src/app/admin/template-tasks/_config/columns-data.tsx +7 -1
- package/dist/templates/spark/src/app/admin/template-tasks/_config/columns-inline.tsx +2 -0
- package/dist/templates/spark/src/app/admin/templates/[id]/@dashboard/page.tsx +2 -0
- package/dist/templates/spark/src/lib/config/editor.ts +54 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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 { TableCell, type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
|
|
4
|
+
import { createTableSelectCell } from "@tulip-systems/data-tables/client";
|
|
5
|
+
import { format } from "date-fns";
|
|
6
|
+
import Link from "next/link";
|
|
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 { toast } from "@tulip-systems/ui/sonner";
|
|
11
|
+
import { EyeIcon } from "lucide-react";
|
|
12
|
+
import Link from "next/link";
|
|
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);
|