@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,260 @@
|
|
|
1
|
+
import "server-cli-only";
|
|
2
|
+
|
|
3
|
+
import { ORPCError } from "@orpc/server";
|
|
4
|
+
import { moveBlock, reindexBlocks } from "@tulip-systems/blocks";
|
|
5
|
+
import {
|
|
6
|
+
bulkActionSchema,
|
|
7
|
+
offsetPaginationResponse,
|
|
8
|
+
offsetPaginationSchema,
|
|
9
|
+
} from "@tulip-systems/query";
|
|
10
|
+
import { asc, desc, eq, inArray } from "drizzle-orm";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { getTableData } from "@/app/lib/utils/table-data.server";
|
|
13
|
+
import { permissionMiddleware } from "@/server/router/middleware";
|
|
14
|
+
import { protectedProcedure } from "@/server/router/procedures";
|
|
15
|
+
import {
|
|
16
|
+
type QuotationBlock,
|
|
17
|
+
type QuotationBlockInput,
|
|
18
|
+
quotationBlocks,
|
|
19
|
+
} from "../_blocks/registry";
|
|
20
|
+
import {
|
|
21
|
+
quotationDatabaseFiltersCompiler,
|
|
22
|
+
quotationFiltersParser,
|
|
23
|
+
quotationFiltersSchema,
|
|
24
|
+
} from "../_config/filters";
|
|
25
|
+
import {
|
|
26
|
+
quotationTableDatabaseSortsCompiler,
|
|
27
|
+
quotationTableSortsParser,
|
|
28
|
+
quotationTableSortsSchema,
|
|
29
|
+
} from "../_config/sorting";
|
|
30
|
+
import { quotationBlocks as quotationBlocksTable, quotations } from "./schema";
|
|
31
|
+
import { createQuotationSchema, updateQuotationSchema } from "./validators";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Router for handling operations related to quotations, including listing, retrieving by ID, creating, updating, and deleting quotations.
|
|
35
|
+
*/
|
|
36
|
+
export const quotationsRouter = {
|
|
37
|
+
list: protectedProcedure
|
|
38
|
+
.use(permissionMiddleware({ quotation: ["view"] }))
|
|
39
|
+
.input(
|
|
40
|
+
z.object({
|
|
41
|
+
pagination: offsetPaginationSchema,
|
|
42
|
+
filters: quotationFiltersSchema,
|
|
43
|
+
sorts: quotationTableSortsSchema,
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
.handler(async ({ context, input }) => {
|
|
47
|
+
const parsedSorts = quotationTableSortsParser.parse(input.sorts);
|
|
48
|
+
const orderBy = quotationTableDatabaseSortsCompiler(quotations).parse(parsedSorts);
|
|
49
|
+
|
|
50
|
+
const parsedFilters = quotationFiltersParser.parse(input.filters);
|
|
51
|
+
const where = quotationDatabaseFiltersCompiler(quotations).parse(parsedFilters);
|
|
52
|
+
|
|
53
|
+
const { data, total } = await getTableData(context, quotations, {
|
|
54
|
+
pagination: input.pagination,
|
|
55
|
+
where,
|
|
56
|
+
orderBy,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return offsetPaginationResponse({ data, pagination: input.pagination, total });
|
|
60
|
+
}),
|
|
61
|
+
getById: protectedProcedure
|
|
62
|
+
.use(permissionMiddleware({ quotation: ["view"] }))
|
|
63
|
+
.input(z.object({ id: z.string() }))
|
|
64
|
+
.handler(async ({ context, input }) => {
|
|
65
|
+
const data = await context.db.query.quotations.findFirst({
|
|
66
|
+
where: eq(quotations.id, input.id),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return data ?? null;
|
|
70
|
+
}),
|
|
71
|
+
create: protectedProcedure
|
|
72
|
+
.use(permissionMiddleware({ quotation: ["create"] }))
|
|
73
|
+
.input(createQuotationSchema)
|
|
74
|
+
.handler(async ({ context, input }) => {
|
|
75
|
+
return context.db.transaction(async (tx) => {
|
|
76
|
+
const [quotation] = await tx.insert(quotations).values(input).returning();
|
|
77
|
+
|
|
78
|
+
if (!quotation) {
|
|
79
|
+
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
80
|
+
message: "Offerte kon niet worden aangemaakt",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return quotation;
|
|
85
|
+
});
|
|
86
|
+
}),
|
|
87
|
+
update: protectedProcedure
|
|
88
|
+
.use(permissionMiddleware({ quotation: ["update"] }))
|
|
89
|
+
.input(z.object({ id: z.string(), data: updateQuotationSchema.partial() }))
|
|
90
|
+
.handler(async ({ context, input }) => {
|
|
91
|
+
const [result] = await context.db
|
|
92
|
+
.update(quotations)
|
|
93
|
+
.set(input.data)
|
|
94
|
+
.where(eq(quotations.id, input.id))
|
|
95
|
+
.returning();
|
|
96
|
+
|
|
97
|
+
if (!result) {
|
|
98
|
+
throw new ORPCError("NOT_FOUND", {
|
|
99
|
+
message: "Offerte kon niet worden gewijzigd",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
delete: protectedProcedure
|
|
107
|
+
.use(permissionMiddleware({ quotation: ["delete"] }))
|
|
108
|
+
.input(bulkActionSchema)
|
|
109
|
+
.handler(async ({ context, input }) => {
|
|
110
|
+
await context.db.delete(quotations).where(inArray(quotations.id, input.ids));
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Quotation blocks router that handles operations related to quotation blocks, including listing, creating, updating, and moving blocks.
|
|
116
|
+
*/
|
|
117
|
+
export const quotationBlocksRouter = {
|
|
118
|
+
list: protectedProcedure
|
|
119
|
+
.use(permissionMiddleware({ quotation: ["view"] }))
|
|
120
|
+
.input(z.object({ quotationId: z.string() }))
|
|
121
|
+
.handler(async ({ context, input }) => {
|
|
122
|
+
const blocks = await context.db.query.quotationBlocks.findMany({
|
|
123
|
+
where: eq(quotationBlocksTable.quotationId, input.quotationId),
|
|
124
|
+
orderBy: asc(quotationBlocksTable.index),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return blocks;
|
|
128
|
+
}),
|
|
129
|
+
create: protectedProcedure
|
|
130
|
+
.use(permissionMiddleware({ quotation: ["update"] }))
|
|
131
|
+
.input(z.object({ quotationId: z.string(), block: quotationBlocks.createSchema }))
|
|
132
|
+
.handler(async ({ context, input }) => {
|
|
133
|
+
const block = input.block as QuotationBlockInput;
|
|
134
|
+
const lastBlock = await context.db
|
|
135
|
+
.select({ index: quotationBlocksTable.index })
|
|
136
|
+
.from(quotationBlocksTable)
|
|
137
|
+
.where(eq(quotationBlocksTable.quotationId, input.quotationId))
|
|
138
|
+
.orderBy(desc(quotationBlocksTable.index))
|
|
139
|
+
.limit(1);
|
|
140
|
+
|
|
141
|
+
const [created] = await context.db
|
|
142
|
+
.insert(quotationBlocksTable)
|
|
143
|
+
.values({
|
|
144
|
+
quotationId: input.quotationId,
|
|
145
|
+
slug: block.slug,
|
|
146
|
+
data: block.data,
|
|
147
|
+
index: (lastBlock[0]?.index ?? -1) + 1,
|
|
148
|
+
})
|
|
149
|
+
.returning();
|
|
150
|
+
|
|
151
|
+
if (!created) {
|
|
152
|
+
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
153
|
+
message: "Blok kon niet worden aangemaakt",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return quotationBlocks.selectSchema.parse(created) as QuotationBlock;
|
|
158
|
+
}),
|
|
159
|
+
update: protectedProcedure
|
|
160
|
+
.use(permissionMiddleware({ quotation: ["update"] }))
|
|
161
|
+
.input(z.object({ id: z.string(), data: z.record(z.string(), z.unknown()) }))
|
|
162
|
+
.handler(async ({ context, input }) => {
|
|
163
|
+
const existing = await context.db.query.quotationBlocks.findFirst({
|
|
164
|
+
where: eq(quotationBlocksTable.id, input.id),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!existing) {
|
|
168
|
+
throw new ORPCError("NOT_FOUND", {
|
|
169
|
+
message: "Blok kon niet worden gevonden",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const patchedData = quotationBlocks.patchData({ block: existing, patch: input.data });
|
|
174
|
+
|
|
175
|
+
if (!patchedData.success) {
|
|
176
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
177
|
+
message: "Onbekend bloktype",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const [updated] = await context.db
|
|
182
|
+
.update(quotationBlocksTable)
|
|
183
|
+
.set({ data: patchedData.data })
|
|
184
|
+
.where(eq(quotationBlocksTable.id, input.id))
|
|
185
|
+
.returning();
|
|
186
|
+
|
|
187
|
+
if (!updated) {
|
|
188
|
+
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
189
|
+
message: "Blok kon niet worden gewijzigd",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return quotationBlocks.selectSchema.parse(updated) as QuotationBlock;
|
|
194
|
+
}),
|
|
195
|
+
delete: protectedProcedure
|
|
196
|
+
.use(permissionMiddleware({ quotation: ["update"] }))
|
|
197
|
+
.input(z.object({ id: z.string() }))
|
|
198
|
+
.handler(async ({ context, input }) => {
|
|
199
|
+
const existing = await context.db.query.quotationBlocks.findFirst({
|
|
200
|
+
where: eq(quotationBlocksTable.id, input.id),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!existing) {
|
|
204
|
+
throw new ORPCError("NOT_FOUND", {
|
|
205
|
+
message: "Blok kon niet worden gevonden",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await context.db.transaction(async (tx) => {
|
|
210
|
+
await tx.delete(quotationBlocksTable).where(eq(quotationBlocksTable.id, input.id));
|
|
211
|
+
|
|
212
|
+
const blocks = await tx.query.quotationBlocks.findMany({
|
|
213
|
+
where: eq(quotationBlocksTable.quotationId, existing.quotationId),
|
|
214
|
+
orderBy: asc(quotationBlocksTable.index),
|
|
215
|
+
});
|
|
216
|
+
const reindexedBlocks = reindexBlocks(blocks);
|
|
217
|
+
|
|
218
|
+
await Promise.all(
|
|
219
|
+
reindexedBlocks.map((block) =>
|
|
220
|
+
tx
|
|
221
|
+
.update(quotationBlocksTable)
|
|
222
|
+
.set({ index: block.index })
|
|
223
|
+
.where(eq(quotationBlocksTable.id, block.id)),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return { success: true };
|
|
229
|
+
}),
|
|
230
|
+
move: protectedProcedure
|
|
231
|
+
.use(permissionMiddleware({ quotation: ["update"] }))
|
|
232
|
+
.input(z.object({ quotationId: z.string(), id: z.string(), toIndex: z.number().int().min(0) }))
|
|
233
|
+
.handler(async ({ context, input }) => {
|
|
234
|
+
const blocks = await context.db.query.quotationBlocks.findMany({
|
|
235
|
+
where: eq(quotationBlocksTable.quotationId, input.quotationId),
|
|
236
|
+
orderBy: asc(quotationBlocksTable.index),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const result = moveBlock(blocks, { id: input.id, toIndex: input.toIndex });
|
|
240
|
+
|
|
241
|
+
if (!result) {
|
|
242
|
+
throw new ORPCError("NOT_FOUND", {
|
|
243
|
+
message: "Blok kon niet worden gevonden",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await context.db.transaction(async (tx) => {
|
|
248
|
+
await Promise.all(
|
|
249
|
+
result.blocks.map((block) =>
|
|
250
|
+
tx
|
|
251
|
+
.update(quotationBlocksTable)
|
|
252
|
+
.set({ index: block.index })
|
|
253
|
+
.where(eq(quotationBlocksTable.id, block.id)),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return z.array(quotationBlocks.selectSchema).parse(result.blocks) as QuotationBlock[];
|
|
259
|
+
}),
|
|
260
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { relations } from "drizzle-orm";
|
|
2
|
+
import { index, pgTable } from "drizzle-orm/pg-core";
|
|
3
|
+
import { baseColumns } from "@/server/db/helpers";
|
|
4
|
+
|
|
5
|
+
export const quotations = pgTable("quotations", (t) => ({
|
|
6
|
+
...baseColumns,
|
|
7
|
+
title: t.varchar().notNull(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
export const quotationsRelations = relations(quotations, ({ many }) => ({
|
|
11
|
+
blocks: many(quotationBlocks),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
*
|
|
17
|
+
* Quotations Blocks
|
|
18
|
+
*/
|
|
19
|
+
export const quotationBlocks = pgTable(
|
|
20
|
+
"quotation_blocks",
|
|
21
|
+
(t) => ({
|
|
22
|
+
...baseColumns,
|
|
23
|
+
slug: t.varchar().notNull(),
|
|
24
|
+
data: t.jsonb().notNull(),
|
|
25
|
+
index: t.doublePrecision().notNull(),
|
|
26
|
+
quotationId: t
|
|
27
|
+
.uuid()
|
|
28
|
+
.notNull()
|
|
29
|
+
.references(() => quotations.id, { onDelete: "cascade" }),
|
|
30
|
+
}),
|
|
31
|
+
(table) => [
|
|
32
|
+
index("quotation_blocks_quotation_order_idx").on(table.quotationId, table.index),
|
|
33
|
+
index("quotation_blocks_quotation_slug_idx").on(table.quotationId, table.slug),
|
|
34
|
+
],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const quotationBlocksRelations = relations(quotationBlocks, ({ one }) => ({
|
|
38
|
+
quotation: one(quotations, {
|
|
39
|
+
fields: [quotationBlocks.quotationId],
|
|
40
|
+
references: [quotations.id],
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { quotations } from "./schema";
|
|
4
|
+
|
|
5
|
+
export const quotationSchema = createSelectSchema(quotations);
|
|
6
|
+
export type Quotation = z.infer<typeof quotationSchema>;
|
|
7
|
+
|
|
8
|
+
export const createQuotationSchema = createInsertSchema(quotations, {
|
|
9
|
+
title: z.string().min(1, "Titel is verplicht"),
|
|
10
|
+
}).omit({ id: true, createdAt: true, updatedAt: true });
|
|
11
|
+
|
|
12
|
+
export const updateQuotationSchema = createInsertSchema(quotations, {
|
|
13
|
+
title: z.string().min(1, "Titel is verplicht"),
|
|
14
|
+
}).omit({ id: true, createdAt: true, updatedAt: true });
|
|
15
|
+
|
|
16
|
+
export type CreateQuotationInput = z.input<typeof createQuotationSchema>;
|
|
17
|
+
export type CreateQuotationSchema = z.infer<typeof createQuotationSchema>;
|
|
18
|
+
|
|
19
|
+
export type UpdateQuotationInput = z.input<typeof updateQuotationSchema>;
|
|
20
|
+
export type UpdateQuotationSchema = z.infer<typeof updateQuotationSchema>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useSuspenseQueries, useSuspenseQuery } from "@tanstack/react-query";
|
|
4
4
|
import { EditorContent, EditorMenuFixed } from "@tulip-systems/editor/client";
|
|
5
|
-
import {
|
|
5
|
+
import { StorageUploadExtension } from "@tulip-systems/editor-storage";
|
|
6
6
|
import {
|
|
7
7
|
InlineComboboxDropdown,
|
|
8
8
|
InlineEditor,
|
|
@@ -22,6 +22,7 @@ import { toast } from "@tulip-systems/ui/sonner";
|
|
|
22
22
|
import { notFound } from "next/navigation";
|
|
23
23
|
import { use } from "react";
|
|
24
24
|
import { z } from "zod";
|
|
25
|
+
import { richTextEditorPreset } from "@/lib/config/editor";
|
|
25
26
|
import { orpc } from "@/server/router/client";
|
|
26
27
|
import { uploadClient } from "@/server/storage/client";
|
|
27
28
|
import { storyPointsConfig } from "../../../_lib/constants";
|
|
@@ -175,8 +176,9 @@ export default function TaskDetailPage(props: PageProps<"/admin/tasks/[id]">) {
|
|
|
175
176
|
mutate: { on: "change", debounceMs: 700 },
|
|
176
177
|
validate: { change: z.any() },
|
|
177
178
|
}}
|
|
178
|
-
extensions={
|
|
179
|
-
|
|
179
|
+
extensions={[
|
|
180
|
+
...richTextEditorPreset,
|
|
181
|
+
StorageUploadExtension.configure({
|
|
180
182
|
allowedMimeTypes: ["image/png", "image/jpeg", "image/gif", "image/webp"],
|
|
181
183
|
variables: {},
|
|
182
184
|
uploadClient,
|
|
@@ -190,7 +192,7 @@ export default function TaskDetailPage(props: PageProps<"/admin/tasks/[id]">) {
|
|
|
190
192
|
});
|
|
191
193
|
},
|
|
192
194
|
}),
|
|
193
|
-
}
|
|
195
|
+
]}
|
|
194
196
|
className="min-h-40"
|
|
195
197
|
>
|
|
196
198
|
<EditorMenuFixed />
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
|
2
|
+
import Blockquote from "@tiptap/extension-blockquote";
|
|
3
|
+
import Bold from "@tiptap/extension-bold";
|
|
4
|
+
import Document from "@tiptap/extension-document";
|
|
5
|
+
import HardBreak from "@tiptap/extension-hard-break";
|
|
6
|
+
import Heading from "@tiptap/extension-heading";
|
|
7
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
8
|
+
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
|
9
|
+
import Italic from "@tiptap/extension-italic";
|
|
10
|
+
import Link from "@tiptap/extension-link";
|
|
11
|
+
import { BulletList, ListItem, ListKeymap, OrderedList } from "@tiptap/extension-list";
|
|
12
|
+
import Paragraph from "@tiptap/extension-paragraph";
|
|
13
|
+
import Strike from "@tiptap/extension-strike";
|
|
14
|
+
import Text from "@tiptap/extension-text";
|
|
15
|
+
import { Color, TextStyle } from "@tiptap/extension-text-style";
|
|
16
|
+
import Underline from "@tiptap/extension-underline";
|
|
17
|
+
import { Dropcursor, Gapcursor, TrailingNode, UndoRedo } from "@tiptap/extensions";
|
|
18
|
+
import { defineEditorPreset } from "@tulip-systems/editor/client";
|
|
19
|
+
import { StorageImage } from "@tulip-systems/editor-storage";
|
|
20
|
+
|
|
21
|
+
const imageResizeOptions = {
|
|
22
|
+
enabled: true,
|
|
23
|
+
directions: ["left", "right"] as ResizableNodeViewDirection[],
|
|
24
|
+
minHeight: 50,
|
|
25
|
+
minWidth: 50,
|
|
26
|
+
alwaysPreserveAspectRatio: true,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const richTextEditorPreset = defineEditorPreset([
|
|
30
|
+
Document,
|
|
31
|
+
Text,
|
|
32
|
+
Paragraph,
|
|
33
|
+
Blockquote,
|
|
34
|
+
HardBreak,
|
|
35
|
+
Heading,
|
|
36
|
+
HorizontalRule,
|
|
37
|
+
ListItem,
|
|
38
|
+
OrderedList,
|
|
39
|
+
BulletList,
|
|
40
|
+
StorageImage.configure({ resize: imageResizeOptions }),
|
|
41
|
+
Bold,
|
|
42
|
+
Italic,
|
|
43
|
+
Link,
|
|
44
|
+
Strike,
|
|
45
|
+
Underline,
|
|
46
|
+
Highlight.configure({ multicolor: true }),
|
|
47
|
+
Dropcursor,
|
|
48
|
+
Gapcursor,
|
|
49
|
+
UndoRedo,
|
|
50
|
+
ListKeymap,
|
|
51
|
+
TrailingNode,
|
|
52
|
+
TextStyle,
|
|
53
|
+
Color,
|
|
54
|
+
]);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
FoldersIcon,
|
|
16
16
|
HomeIcon,
|
|
17
17
|
ListTodoIcon,
|
|
18
|
+
ReceiptTextIcon,
|
|
18
19
|
Table2Icon,
|
|
19
20
|
TimerIcon,
|
|
20
21
|
UserSearchIcon,
|
|
@@ -140,6 +141,13 @@ export function AdminSidebar() {
|
|
|
140
141
|
Prospects
|
|
141
142
|
</PathLink>
|
|
142
143
|
</Path>
|
|
144
|
+
|
|
145
|
+
<Path segment="quotations">
|
|
146
|
+
<PathLink href="/admin/quotations">
|
|
147
|
+
<ReceiptTextIcon />
|
|
148
|
+
Offertes
|
|
149
|
+
</PathLink>
|
|
150
|
+
</Path>
|
|
143
151
|
</PathGroupItems>
|
|
144
152
|
</PathGroup>
|
|
145
153
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import localFont from "next/font/local";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Neue Haas Grotesk Display Pro — de sans-font van de app (`--font-sans`).
|
|
5
|
+
*
|
|
6
|
+
* Belangrijk: deze font gebruikt een niet-standaard weight-schaal. Elke `weight`
|
|
7
|
+
* hieronder is gelijkgesteld aan de échte `usWeightClass` van het bestand, zodat
|
|
8
|
+
* numerieke gewichten in de browser exact zo resolven als in Figma:
|
|
9
|
+
*
|
|
10
|
+
* 250 UltraThin · 300 Thin · 350 XLight · 400 Light · 450 Roman ·
|
|
11
|
+
* 500 Medium · 700 Bold · 900 Black
|
|
12
|
+
*
|
|
13
|
+
* De typography-schaal gebruikt 400 (→ Light) en 500 (→ Medium) — dit matcht
|
|
14
|
+
* `font-weight: 400/500` uit de Figma-specs (waar 400 óók de Light-cut is, niet
|
|
15
|
+
* de Roman). De Roman-cut (55) zit bewust op 450 en is dus niet `font-normal`.
|
|
16
|
+
*/
|
|
17
|
+
export const fontNeueHaas = localFont({
|
|
18
|
+
variable: "--font-sans",
|
|
19
|
+
display: "swap",
|
|
20
|
+
src: [
|
|
21
|
+
{
|
|
22
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf",
|
|
23
|
+
weight: "250",
|
|
24
|
+
style: "normal",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf",
|
|
28
|
+
weight: "250",
|
|
29
|
+
style: "italic",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf",
|
|
33
|
+
weight: "300",
|
|
34
|
+
style: "normal",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf",
|
|
38
|
+
weight: "300",
|
|
39
|
+
style: "italic",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf",
|
|
43
|
+
weight: "350",
|
|
44
|
+
style: "normal",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf",
|
|
48
|
+
weight: "350",
|
|
49
|
+
style: "italic",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf",
|
|
53
|
+
weight: "400",
|
|
54
|
+
style: "normal",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf",
|
|
58
|
+
weight: "400",
|
|
59
|
+
style: "italic",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf",
|
|
63
|
+
weight: "450",
|
|
64
|
+
style: "normal",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf",
|
|
68
|
+
weight: "450",
|
|
69
|
+
style: "italic",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf",
|
|
73
|
+
weight: "500",
|
|
74
|
+
style: "normal",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf",
|
|
78
|
+
weight: "500",
|
|
79
|
+
style: "italic",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf",
|
|
83
|
+
weight: "700",
|
|
84
|
+
style: "normal",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf",
|
|
88
|
+
weight: "700",
|
|
89
|
+
style: "italic",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf",
|
|
93
|
+
weight: "900",
|
|
94
|
+
style: "normal",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf",
|
|
98
|
+
weight: "900",
|
|
99
|
+
style: "italic",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
@@ -11,6 +11,7 @@ const statement = createStatements({
|
|
|
11
11
|
customer: ["view", "create", "update", "archive", "delete"],
|
|
12
12
|
task: ["view", "create", "update", "delete"],
|
|
13
13
|
project: ["view", "view.budget", "view.drive", "create", "update", "archive", "delete"],
|
|
14
|
+
quotation: ["view", "create", "update", "delete"],
|
|
14
15
|
initiative: ["view", "view.budget", "view.stats", "create", "update", "delete"],
|
|
15
16
|
domain: ["view", "create", "update", "delete"],
|
|
16
17
|
time: ["view", "create", "update", "delete"],
|
|
@@ -33,6 +34,7 @@ const developer = ac.newRole({
|
|
|
33
34
|
customer: ["view", "create", "update", "archive", "delete"],
|
|
34
35
|
task: ["view", "create", "update", "delete"],
|
|
35
36
|
project: ["view", "view.budget", "view.drive", "create", "update", "archive", "delete"],
|
|
37
|
+
quotation: ["view", "create", "update", "delete"],
|
|
36
38
|
initiative: ["view", "view.budget", "view.stats", "create", "update", "delete"],
|
|
37
39
|
domain: ["view", "create", "update", "delete"],
|
|
38
40
|
time: ["view", "create", "update", "delete"],
|
|
@@ -50,6 +52,7 @@ const admin = ac.newRole({
|
|
|
50
52
|
customer: ["view", "create", "update", "archive", "delete"],
|
|
51
53
|
task: ["view", "create", "update", "delete"],
|
|
52
54
|
project: ["view", "view.budget", "view.drive", "create", "update", "archive", "delete"],
|
|
55
|
+
quotation: ["view", "create", "update", "delete"],
|
|
53
56
|
initiative: ["view", "view.budget", "view.stats", "create", "update", "delete"],
|
|
54
57
|
domain: ["view", "create", "update", "delete"],
|
|
55
58
|
time: ["view", "create", "update", "delete"],
|
|
@@ -65,6 +68,7 @@ const worker = ac.newRole({
|
|
|
65
68
|
customer: ["view"],
|
|
66
69
|
task: ["view", "create", "update", "delete"],
|
|
67
70
|
project: ["view", "view.drive", "create", "update"],
|
|
71
|
+
quotation: ["view", "create", "update"],
|
|
68
72
|
initiative: ["view", "view.stats", "update"],
|
|
69
73
|
domain: ["view", "create", "update", "delete"],
|
|
70
74
|
time: ["view", "create", "update", "delete"],
|
|
@@ -13,6 +13,7 @@ export * from "@/app/admin/domains/_lib/schema";
|
|
|
13
13
|
export * from "@/app/admin/employees/_lib/schema";
|
|
14
14
|
export * from "@/app/admin/initiatives/_lib/schema";
|
|
15
15
|
export * from "@/app/admin/projects/_lib/schema";
|
|
16
|
+
export * from "@/app/admin/quotations/_lib/schema";
|
|
16
17
|
export * from "@/app/admin/tasks/_lib/schema";
|
|
17
18
|
export * from "@/app/admin/time/_lib/schema";
|
|
18
19
|
export * from "@/app/admin/prospects/_lib/schema";
|
|
@@ -66,6 +66,16 @@ export const appRouter = {
|
|
|
66
66
|
default: module.prospectsRouter,
|
|
67
67
|
})),
|
|
68
68
|
),
|
|
69
|
+
quotations: lazy(() =>
|
|
70
|
+
import("@/app/admin/quotations/_lib/router").then((module) => ({
|
|
71
|
+
default: module.quotationsRouter,
|
|
72
|
+
})),
|
|
73
|
+
),
|
|
74
|
+
quotationBlocks: lazy(() =>
|
|
75
|
+
import("@/app/admin/quotations/_lib/router").then((module) => ({
|
|
76
|
+
default: module.quotationBlocksRouter,
|
|
77
|
+
})),
|
|
78
|
+
),
|
|
69
79
|
};
|
|
70
80
|
|
|
71
81
|
// export type definition of API
|
|
@@ -35,19 +35,36 @@
|
|
|
35
35
|
"@tanstack/db": "^0.6.17",
|
|
36
36
|
"@tanstack/react-query": "^5.101.4",
|
|
37
37
|
"@tanstack/react-table": "^8.21.3",
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
47
|
-
"@
|
|
48
|
-
"@
|
|
49
|
-
"@
|
|
50
|
-
"@
|
|
38
|
+
"@tiptap/core": "^3.29.2",
|
|
39
|
+
"@tiptap/extension-blockquote": "^3.29.2",
|
|
40
|
+
"@tiptap/extension-bold": "^3.29.2",
|
|
41
|
+
"@tiptap/extension-document": "^3.29.2",
|
|
42
|
+
"@tiptap/extension-hard-break": "^3.29.2",
|
|
43
|
+
"@tiptap/extension-heading": "^3.29.2",
|
|
44
|
+
"@tiptap/extension-highlight": "^3.29.2",
|
|
45
|
+
"@tiptap/extension-horizontal-rule": "^3.29.2",
|
|
46
|
+
"@tiptap/extension-italic": "^3.29.2",
|
|
47
|
+
"@tiptap/extension-link": "^3.29.2",
|
|
48
|
+
"@tiptap/extension-list": "^3.29.2",
|
|
49
|
+
"@tiptap/extension-paragraph": "^3.29.2",
|
|
50
|
+
"@tiptap/extension-strike": "^3.29.2",
|
|
51
|
+
"@tiptap/extension-text": "^3.29.2",
|
|
52
|
+
"@tiptap/extension-text-style": "^3.29.2",
|
|
53
|
+
"@tiptap/extension-underline": "^3.29.2",
|
|
54
|
+
"@tiptap/extensions": "^3.29.2",
|
|
55
|
+
"@tulip-systems/app": "0.12.4",
|
|
56
|
+
"@tulip-systems/auth": "0.12.4",
|
|
57
|
+
"@tulip-systems/commands": "0.12.4",
|
|
58
|
+
"@tulip-systems/core": "0.12.4",
|
|
59
|
+
"@tulip-systems/data-tables": "0.12.4",
|
|
60
|
+
"@tulip-systems/drive": "0.12.4",
|
|
61
|
+
"@tulip-systems/drive-local": "0.12.4",
|
|
62
|
+
"@tulip-systems/editor": "0.12.4",
|
|
63
|
+
"@tulip-systems/editor-storage": "0.12.4",
|
|
64
|
+
"@tulip-systems/inline": "0.12.4",
|
|
65
|
+
"@tulip-systems/query": "0.12.4",
|
|
66
|
+
"@tulip-systems/storage": "0.12.4",
|
|
67
|
+
"@tulip-systems/ui": "0.12.4",
|
|
51
68
|
"@vercel/functions": "^3.9.0",
|
|
52
69
|
"@zip.js/zip.js": "^2.8.34",
|
|
53
70
|
"better-auth": "^1.6.26",
|
|
@@ -24,6 +24,7 @@ import { generateClientHref } from "@/app/admin/clients/_lib/helpers";
|
|
|
24
24
|
import { EmployeeAvatars, EmployeeAvatarsLoading } from "@/app/admin/employees/_components/avatars";
|
|
25
25
|
import { calculateTotalDuration } from "@/app/admin/registrations/_lib/helpers";
|
|
26
26
|
import { Icons } from "@/components/icons";
|
|
27
|
+
import { richTextEditorPreset } from "@/lib/config/editor";
|
|
27
28
|
import { orpc } from "@/server/router/client";
|
|
28
29
|
|
|
29
30
|
export default function AssignmentDetailPage(props: PageProps<"/admin/assignments/[id]">) {
|
|
@@ -99,6 +100,7 @@ export default function AssignmentDetailPage(props: PageProps<"/admin/assignment
|
|
|
99
100
|
validate: { change: z.any() },
|
|
100
101
|
}}
|
|
101
102
|
className="min-h-24"
|
|
103
|
+
extensions={richTextEditorPreset}
|
|
102
104
|
>
|
|
103
105
|
<EditorMenuBubble />
|
|
104
106
|
<EditorContent />
|