Add field management module with database schema and validation

- Introduced `FieldModule` to manage cycles and plans, including read and write controllers.
- Created database migrations for `company_settings`, `cycles`, `cycle_weekdays`, `cycle_destinations`, `plans`, `plan_destinations`, `plan_invoices`, and `plan_packing_slips` tables, including constraints and unique indexes.
- Developed service and repository layers for handling cycle and plan data operations.
- Added unit tests for the cycles and plans services, repositories, and controllers to ensure functionality and correctness.
- Updated application module to include the new `FieldModule` for better organization.
This commit is contained in:
shancheas
2026-08-25 18:20:19 +07:00
parent 34d4f2c120
commit c9f9b31abf
53 changed files with 5990 additions and 5 deletions
+111
View File
@@ -0,0 +1,111 @@
import { sql } from 'drizzle-orm';
import {
bigint,
index,
integer,
jsonb,
pgTable,
text,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import { branches } from './branches-table';
import { customers } from './customers-table';
import { employees } from './employees-table';
import { packingSlips } from './packing-slips-table';
import { primaryEntityColumns } from './primary-entity-columns';
import { salesInvoices } from './sales-invoices-table';
import { users } from './schema';
import type { StoredRouteGeometry } from './cycles-table';
export const plans = pgTable(
'plans',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
employeeId: uuid('employee_id')
.notNull()
.references(() => employees.id, { onDelete: 'restrict' }),
purpose: text('purpose').notNull(),
date: bigint('date', { mode: 'number' }).notNull(),
startBranchId: uuid('start_branch_id')
.notNull()
.references(() => branches.id, { onDelete: 'restrict' }),
endBranchId: uuid('end_branch_id')
.notNull()
.references(() => branches.id, { onDelete: 'restrict' }),
routeGeometry: jsonb('route_geometry')
.$type<StoredRouteGeometry>()
.notNull(),
...primaryEntityColumns(users),
},
(t) => [
uniqueIndex('plans_employee_purpose_date_live_unique')
.on(t.employeeId, t.purpose, t.date)
.where(sql`${t.status} <> 'archived'`),
index('plans_employee_id_idx').on(t.employeeId),
],
);
export const planDestinations = pgTable(
'plan_destinations',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
planId: uuid('plan_id')
.notNull()
.references(() => plans.id, { onDelete: 'cascade' }),
customerId: uuid('customer_id')
.notNull()
.references(() => customers.id, { onDelete: 'restrict' }),
sortOrder: integer('sort_order').notNull(),
},
(t) => [
uniqueIndex('plan_destinations_plan_customer_unique').on(
t.planId,
t.customerId,
),
index('plan_destinations_plan_id_idx').on(t.planId),
],
);
export const planInvoices = pgTable(
'plan_invoices',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
planId: uuid('plan_id')
.notNull()
.references(() => plans.id, { onDelete: 'cascade' }),
invoiceId: uuid('invoice_id')
.notNull()
.references(() => salesInvoices.id, { onDelete: 'restrict' }),
},
(t) => [
uniqueIndex('plan_invoices_plan_invoice_unique').on(t.planId, t.invoiceId),
index('plan_invoices_plan_id_idx').on(t.planId),
],
);
export const planPackingSlips = pgTable(
'plan_packing_slips',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
planId: uuid('plan_id')
.notNull()
.references(() => plans.id, { onDelete: 'cascade' }),
packingSlipId: uuid('packing_slip_id')
.notNull()
.references(() => packingSlips.id, { onDelete: 'restrict' }),
},
(t) => [
uniqueIndex('plan_packing_slips_plan_slip_unique').on(
t.planId,
t.packingSlipId,
),
index('plan_packing_slips_plan_id_idx').on(t.planId),
],
);
export type PlanRow = typeof plans.$inferSelect;
export type NewPlanRow = typeof plans.$inferInsert;
export type PlanDestinationRow = typeof planDestinations.$inferSelect;
export type PlanInvoiceRow = typeof planInvoices.$inferSelect;
export type PlanPackingSlipRow = typeof planPackingSlips.$inferSelect;