@tulip-systems/create-app 0.12.3 → 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 +16 -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/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/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 +13 -13
- 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);
|