feat(05-01): append five offer tables + Drizzle relations to schema.ts

- Add primaryKey to drizzle-orm/pg-core import
- Define offer_macros, offer_micros, offer_services, offer_micro_services, project_offers
- Add Drizzle relations for all five new tables (+ projectOffers in projectsRelations)
- Export TypeScript infer types for all new tables
This commit is contained in:
2026-05-30 15:26:02 +02:00
parent c83e5e5a7e
commit f0034410ff
+109 -1
View File
@@ -5,6 +5,7 @@ import {
numeric,
timestamp,
boolean,
primaryKey,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { nanoid } from "nanoid";
@@ -194,6 +195,78 @@ export const settings = pgTable("settings", {
updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
// ============ OFFER MACROS ============
export const offer_macros = pgTable("offer_macros", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
internal_name: text("internal_name").notNull(),
public_name: text("public_name").notNull(),
transformation_promise: text("transformation_promise"),
sort_order: integer("sort_order").notNull().default(0),
created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// ============ OFFER MICROS ============
export const offer_micros = pgTable("offer_micros", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
macro_id: text("macro_id")
.notNull()
.references(() => offer_macros.id, { onDelete: "cascade" }),
internal_name: text("internal_name").notNull(),
public_name: text("public_name").notNull(),
transformation_promise: text("transformation_promise"),
duration_months: integer("duration_months").notNull().default(1),
sort_order: integer("sort_order").notNull().default(0),
});
// ============ OFFER SERVICES (distinct from service_catalog — marketing/transformation semantics) ============
export const offer_services = pgTable("offer_services", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
price: numeric("price", { precision: 10, scale: 2 }).notNull(),
transformation_description: text("transformation_description"),
active: boolean("active").notNull().default(true),
});
// ============ OFFER MICRO SERVICES (junction: offer_micros <-> offer_services) ============
export const offer_micro_services = pgTable(
"offer_micro_services",
{
micro_id: text("micro_id")
.notNull()
.references(() => offer_micros.id, { onDelete: "cascade" }),
service_id: text("service_id")
.notNull()
.references(() => offer_services.id, { onDelete: "cascade" }),
},
(t) => ({
pk: primaryKey({ columns: [t.micro_id, t.service_id] }),
})
);
// ============ PROJECT OFFERS (assignment of micro-offer to project) ============
export const project_offers = pgTable("project_offers", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
project_id: text("project_id")
.notNull()
.references(() => projects.id, { onDelete: "cascade" }),
micro_id: text("micro_id")
.notNull()
.references(() => offer_micros.id, { onDelete: "restrict" }),
// NOT NULL with defaultNow — required for forecast computation (null start_date breaks forecast)
start_date: timestamp("start_date", { withTimezone: true }).notNull().defaultNow(),
// Offer-level accepted total — separate from projects.accepted_total (quote builder total)
accepted_total: numeric("accepted_total", { precision: 10, scale: 2 }),
created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// ============ RELATIONS ============
export const clientsRelations = relations(clients, ({ many }) => ({
@@ -208,6 +281,7 @@ export const projectsRelations = relations(projects, ({ one, many }) => ({
notes: many(notes),
quote_items: many(quote_items),
time_entries: many(time_entries),
projectOffers: many(project_offers),
}));
export const phasesRelations = relations(phases, ({ one, many }) => ({
@@ -268,6 +342,30 @@ export const serviceCatalogRelations = relations(
})
);
export const offerMacrosRelations = relations(offer_macros, ({ many }) => ({
micros: many(offer_micros),
}));
export const offerMicrosRelations = relations(offer_micros, ({ one, many }) => ({
macro: one(offer_macros, { fields: [offer_micros.macro_id], references: [offer_macros.id] }),
services: many(offer_micro_services),
projectOffers: many(project_offers),
}));
export const offerServicesRelations = relations(offer_services, ({ many }) => ({
microAssignments: many(offer_micro_services),
}));
export const offerMicroServicesRelations = relations(offer_micro_services, ({ one }) => ({
micro: one(offer_micros, { fields: [offer_micro_services.micro_id], references: [offer_micros.id] }),
service: one(offer_services, { fields: [offer_micro_services.service_id], references: [offer_services.id] }),
}));
export const projectOffersRelations = relations(project_offers, ({ one }) => ({
project: one(projects, { fields: [project_offers.project_id], references: [projects.id] }),
micro: one(offer_micros, { fields: [project_offers.micro_id], references: [offer_micros.id] }),
}));
// ============ TYPESCRIPT TYPES (for use in API routes and Server Components) ============
export type Client = typeof clients.$inferSelect;
@@ -295,4 +393,14 @@ export type NewQuoteItem = typeof quote_items.$inferInsert;
export type TimeEntry = typeof time_entries.$inferSelect;
export type NewTimeEntry = typeof time_entries.$inferInsert;
export type Setting = typeof settings.$inferSelect;
export type NewSetting = typeof settings.$inferInsert;
export type NewSetting = typeof settings.$inferInsert;
export type OfferMacro = typeof offer_macros.$inferSelect;
export type NewOfferMacro = typeof offer_macros.$inferInsert;
export type OfferMicro = typeof offer_micros.$inferSelect;
export type NewOfferMicro = typeof offer_micros.$inferInsert;
export type OfferService = typeof offer_services.$inferSelect;
export type NewOfferService = typeof offer_services.$inferInsert;
export type OfferMicroService = typeof offer_micro_services.$inferSelect;
export type NewOfferMicroService = typeof offer_micro_services.$inferInsert;
export type ProjectOffer = typeof project_offers.$inferSelect;
export type NewProjectOffer = typeof project_offers.$inferInsert;