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:
+109
-1
@@ -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;
|
||||
Reference in New Issue
Block a user