@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.
Files changed (57) hide show
  1. package/dist/templates/basic/package.json +5 -5
  2. package/dist/templates/code/package.json +16 -14
  3. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf +0 -0
  4. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf +0 -0
  5. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf +0 -0
  6. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf +0 -0
  7. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf +0 -0
  8. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf +0 -0
  9. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf +0 -0
  10. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf +0 -0
  11. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf +0 -0
  12. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf +0 -0
  13. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf +0 -0
  14. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf +0 -0
  15. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf +0 -0
  16. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf +0 -0
  17. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf +0 -0
  18. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf +0 -0
  19. package/dist/templates/code/public/images/preview/hero-home.webp +0 -0
  20. package/dist/templates/code/src/app/admin/quotations/(list)/layout.tsx +24 -0
  21. package/dist/templates/code/src/app/admin/quotations/(list)/page.tsx +23 -0
  22. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/layout.tsx +40 -0
  23. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/page.tsx +56 -0
  24. package/dist/templates/code/src/app/admin/quotations/[id]/preview/page.tsx +17 -0
  25. package/dist/templates/code/src/app/admin/quotations/_blocks/commands.tsx +24 -0
  26. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/config.ts +14 -0
  27. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/create.tsx +152 -0
  28. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/inline.tsx +90 -0
  29. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/preview.tsx +90 -0
  30. package/dist/templates/code/src/app/admin/quotations/_blocks/registry.ts +13 -0
  31. package/dist/templates/code/src/app/admin/quotations/_blocks/render.tsx +190 -0
  32. package/dist/templates/code/src/app/admin/quotations/_blocks/styles.ts +24 -0
  33. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/config.ts +13 -0
  34. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/create.tsx +175 -0
  35. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/inline.tsx +93 -0
  36. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/preview.tsx +25 -0
  37. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/config.ts +18 -0
  38. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/create.tsx +177 -0
  39. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/inline.tsx +96 -0
  40. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/preview.tsx +32 -0
  41. package/dist/templates/code/src/app/admin/quotations/_components/create-command.tsx +97 -0
  42. package/dist/templates/code/src/app/admin/quotations/_components/tables.tsx +68 -0
  43. package/dist/templates/code/src/app/admin/quotations/_components/toolbars.tsx +18 -0
  44. package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +35 -0
  45. package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +71 -0
  46. package/dist/templates/code/src/app/admin/quotations/_config/filters.ts +22 -0
  47. package/dist/templates/code/src/app/admin/quotations/_config/sorting.ts +23 -0
  48. package/dist/templates/code/src/app/admin/quotations/_lib/router.ts +260 -0
  49. package/dist/templates/code/src/app/admin/quotations/_lib/schema.ts +42 -0
  50. package/dist/templates/code/src/app/admin/quotations/_lib/validators.ts +20 -0
  51. package/dist/templates/code/src/lib/config/paths.tsx +8 -0
  52. package/dist/templates/code/src/lib/utils/fonts.ts +102 -0
  53. package/dist/templates/code/src/server/auth/permissions.ts +4 -0
  54. package/dist/templates/code/src/server/db/schema.ts +1 -0
  55. package/dist/templates/code/src/server/router/router.ts +10 -0
  56. package/dist/templates/spark/package.json +13 -13
  57. 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);