Shared Package
The packages/shared directory contains shared code used across all ForgeX microservices (bids, projects, field). This ensures consistency in types, constants, and utility functions while avoiding code duplication.
The shared package is referenced directly via relative paths. No build step or npm publish required — changes are immediately available to all services.
Package Structure
packages/shared/
├── README.md
├── package.json
├── types/
│ └── index.ts # TypeScript type definitions & enums
├── constants/
│ ├── bidStatuses.js # Bid status definitions & transitions
│ ├── defaults.js # Default values (rates, prices, settings)
│ ├── exportOptions.js # PDF export configuration
│ ├── features.js # Feature flags & API endpoints
│ └── validation.js # Validation rules & constraints
└── utils/
├── calculations.js # Cost/quantity calculation utilities
├── formatters.js # Formatting utilities (JS version)
├── formatters.ts # Formatting utilities (TS version)
└── normalize.js # Data normalization utilities
Types (types/index.ts)
Central TypeScript type definitions and enums used across all services.
Enums
- UserRole
- BidStatus
- ModuleType
- ProjectStatus
- PurchaseOrderStatus
- TimesheetStatus
Defines user roles and permissions across the platform:
export enum UserRole {
ADMIN = 'ADMIN', // Phase 1 - Full access
ESTIMATOR = 'ESTIMATOR', // Phase 1 - Bid creation/editing
PM = 'PM', // Phase 1 - Read-only bids, Phase 2 - Project management
OPS = 'OPS', // Phase 2 - Purchase orders
ACCOUNTING = 'ACCOUNTING', // Phase 2 - Cost reconciliation
FOREMAN = 'FOREMAN' // Phase 3 - Field operations
}
Usage:
import { UserRole } from '../../../../packages/shared/types';
if (user.role === UserRole.ADMIN) {
// Allow deletion
}
Bid lifecycle statuses:
export enum BidStatus {
DRAFT = 'DRAFT', // Initial state, fully editable
SUBMITTED = 'SUBMITTED', // Sent to client
NEGOTIATION = 'NEGOTIATION', // Under discussion
AWARDED = 'AWARDED', // Won the bid
COMPLETED = 'COMPLETED', // Project completed
LOST = 'LOST' // Did not win
}
Valid Transitions:
- DRAFT → SUBMITTED
- SUBMITTED → NEGOTIATION, AWARDED, LOST
- NEGOTIATION → AWARDED, LOST, SUBMITTED
- AWARDED → COMPLETED
Estimation module types:
export enum ModuleType {
CONCRETE = 'CONCRETE',
LABOR = 'LABOR',
EQUIPMENT = 'EQUIPMENT',
MATERIALS = 'MATERIALS',
SUBLET = 'SUBLET',
MISC = 'MISC'
}
Used for cost rollup and categorization.
Project lifecycle statuses (Phase 2):
export enum ProjectStatus {
ACTIVE = 'ACTIVE',
COMPLETED = 'COMPLETED',
ON_HOLD = 'ON_HOLD',
ARCHIVED = 'ARCHIVED'
}
PO workflow statuses (Phase 2):
export enum PurchaseOrderStatus {
DRAFT = 'DRAFT',
SUBMITTED = 'SUBMITTED',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
POSTED = 'POSTED'
}
Timesheet statuses (Phase 3):
export enum TimesheetStatus {
OPEN = 'OPEN',
SUBMITTED = 'SUBMITTED',
ALLOCATED = 'ALLOCATED'
}
Interfaces
User
export interface User {
id: string;
email: string;
googleId?: string;
name?: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
Client
export interface Client {
id: string;
name: string;
email: string;
createdBy: string;
createdAt: Date;
}
Bid
export interface Bid {
id: string;
clientId: string;
jobName: string;
location: string;
status: BidStatus;
createdBy: string;
createdAt: Date;
lastUpdated: Date;
lastUpdatedBy?: string;
// Cost rollup fields
concreteCost: number;
laborCost: number;
equipmentCost: number;
materialCost: number;
subletCost: number;
miscCost: number;
subtotalCost: number;
overheadAmount: number;
profitAmount: number;
totalCost: number;
}
Scope
export interface Scope {
id: string;
bidId: string;
name: string;
shape: string;
length?: number;
width?: number;
height?: number;
diameter?: number;
cubicYards?: number;
rebarWasteOverride?: number;
createdBy: string;
createdAt: Date;
// Cost rollup fields
concreteCost: number;
laborCost: number;
equipmentCost: number;
materialCost: number;
subletCost: number;
miscCost: number;
totalCost: number;
}
BidWithDetails (Extended)
Extended Bid interface with project details:
export interface BidWithDetails extends Bid {
taxExempt?: boolean;
perDiemEnabled?: boolean;
notes?: string | null;
insuranceType?: InsuranceType;
certifiedPayroll?: boolean;
c3Required?: boolean;
drugTestingRequired?: boolean;
attachments?: BidAttachment[];
exclusions?: BidExclusion[];
}
InsuranceType:
export type InsuranceType = 'CCIP' | 'OCIP' | 'RCIP' | 'NONE' | null;
Constants
features.js - Feature Flags
Controls module and role visibility across all services:
module.exports = {
// Module availability flags
ENABLED_MODULES: {
BID_MANAGER: true, // Phase 1 - LIVE
PROJECT_MANAGEMENT: false, // Phase 2 - Coming soon
FIELD_OPERATIONS: false, // Phase 3 - Coming soon
},
// Role visibility flags
ENABLED_ROLES: {
ADMIN: true, // Phase 1
ESTIMATOR: true, // Phase 1
PM: true, // Phase 1 (read-only)
OPS: false, // Phase 2
ACCOUNTING: false,// Phase 2
FOREMAN: false, // Phase 3
},
// Service API endpoints
API_ENDPOINTS: {
BIDS: process.env.BIDS_API_URL || 'http://localhost:5001',
PROJECTS: process.env.PROJECTS_API_URL || 'http://localhost:5002',
FIELD: process.env.FIELD_API_URL || 'http://localhost:5003',
},
// Pub/Sub topic names
PUBSUB_TOPICS: {
BID_AWARDED: 'bid-awarded',
PROJECT_UPDATED: 'project-updated',
TIMESHEET_SUBMITTED: 'timesheet-submitted',
}
};
Usage:
const { ENABLED_MODULES } = require('../../../../packages/shared/constants/features');
if (ENABLED_MODULES.PROJECT_MANAGEMENT) {
// Show Projects tile
}
See Feature Flags for details.
defaults.js - Default Values
Centralized default values for rates, prices, and settings:
module.exports = {
REBAR: {
BASE_PRICE: 0.85,
TAX_RATE: 0.0825,
WASTE_PERCENT: 0,
STANDARD_WEIGHTS: {
'#2': 0.167,
'#3': 0.376,
'#4': 0.668,
'#5': 1.043,
'#6': 1.502,
'#7': 2.044,
'#8': 2.670,
'#9': 3.400,
'#10': 4.303,
'#11': 5.310
}
},
LABOR: {
PER_DIEM_RATE: 138,
DEFAULT_HOURS_PER_DAY: 8,
DEFAULT_DAYS: 1
},
EQUIPMENT: {
DEFAULT_DELIVERY_FEE: 300,
DEFAULT_FUEL_CHARGE_PER_DAY: 50,
DEFAULT_RENTAL_TAX_RATE: 0.1225,
FORMWORK_TAX_RATE: 0,
DEFAULT_DURATION_VALUE: 1,
DEFAULT_DURATION_UNIT: 'day'
},
MATERIALS: {
DEFAULT_WASTE_PERCENT: 0,
DEFAULT_TAX_RATE: 0.0825
},
SUBCONTRACTOR: {
DEFAULT_WASTE_PERCENT: 0,
DEFAULT_UNIT: 'LS'
},
BID: {
DEFAULT_LOCATION: 'Texas',
DEFAULT_STATUS: 'DRAFT',
DEFAULT_TAX_EXEMPT: false,
DEFAULT_PER_DIEM_ENABLED: false
}
};
Usage:
const { EQUIPMENT } = require('../../../../packages/shared/constants/defaults');
const deliveryFee = item.deliveryFee ?? EQUIPMENT.DEFAULT_DELIVERY_FEE;
bidStatuses.js - Status Definitions
Bid status transitions and validation:
module.exports = {
STATUSES: {
DRAFT: 'DRAFT',
SUBMITTED: 'SUBMITTED',
NEGOTIATION: 'NEGOTIATION',
AWARDED: 'AWARDED',
COMPLETED: 'COMPLETED',
LOST: 'LOST'
},
VALID_TRANSITIONS: {
DRAFT: ['SUBMITTED'],
SUBMITTED: ['NEGOTIATION', 'AWARDED', 'LOST'],
NEGOTIATION: ['AWARDED', 'LOST', 'SUBMITTED'],
AWARDED: ['COMPLETED'],
COMPLETED: [],
LOST: []
},
// Check if transition is valid
canTransition(from, to) {
return this.VALID_TRANSITIONS[from]?.includes(to) ?? false;
}
};
Usage:
const { canTransition } = require('../../../../packages/shared/constants/bidStatuses');
if (!canTransition(bid.status, newStatus)) {
throw new Error('Invalid status transition');
}
validation.js - Validation Rules
Validation constraints for inputs:
module.exports = {
BID: {
JOB_NAME_MAX_LENGTH: 255,
LOCATION_MAX_LENGTH: 255,
NOTES_MAX_LENGTH: 5000
},
SCOPE: {
NAME_MAX_LENGTH: 255,
MAX_MULTIPLIER: 1000,
MIN_MULTIPLIER: 1
},
ITEM: {
DESCRIPTION_MAX_LENGTH: 500,
MAX_QUANTITY: 1000000,
MIN_QUANTITY: 0
},
CLIENT: {
NAME_MAX_LENGTH: 255,
EMAIL_MAX_LENGTH: 255
}
};
exportOptions.js - Export Configuration
PDF export field definitions and options:
module.exports = {
FIELD_GROUPS: {
BASIC: ['jobName', 'clientName', 'location'],
COMPLIANCE: ['insuranceType', 'certifiedPayroll', 'c3Required'],
COSTS: ['concreteCost', 'laborCost', 'equipmentCost']
},
EXCLUDED_FIELDS: ['createdAt', 'updatedAt', 'id'],
DATE_FORMAT: 'YYYY-MM-DD',
CURRENCY_FORMAT: { style: 'currency', currency: 'USD' }
};
Utilities
formatters.ts / formatters.js - Formatting
Consistent formatting utilities for currency, percentages, and quantities:
- formatCurrency
- formatPercent
- formatQuantity
export const formatCurrency = (
value: number | string | null | undefined,
options?: Intl.NumberFormatOptions
): string => {
const numeric = Number(value || 0);
if (!Number.isFinite(numeric)) {
return '$0.00';
}
return numeric.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
...options
});
};
Usage:
import { formatCurrency } from '../../../../packages/shared/utils/formatters';
formatCurrency(1234.56) // "$1,234.56"
formatCurrency(null) // "$0.00"
export const formatPercent = (
value: number | string | null | undefined,
fractionDigits: number = 1
): string => {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return '0%';
// Normalize: 0-1 = decimal, else percentage
const normalized = Math.abs(numeric) <= 1 ? numeric * 100 : numeric;
return `${normalized.toFixed(fractionDigits)}%`;
};
Usage:
formatPercent(0.1) // "10.0%"
formatPercent(10) // "10.0%"
formatPercent(10.5) // "10.5%"
export const formatQuantity = (
value: number | string | null | undefined,
fractionDigits: number = 2
): string => {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return '0.00';
return numeric.toLocaleString(undefined, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits
});
};
Usage:
formatQuantity(1234.567) // "1,234.57"
formatQuantity(1234.567, 0) // "1,235"
calculations.js - Calculation Utilities
Common calculation functions:
module.exports = {
/**
* Calculate rebar pounds from cubic yards
*/
calculateRebarPounds(cubicYards, poundsPerCY) {
return cubicYards * poundsPerCY;
},
/**
* Calculate cubic yards from dimensions
*/
calculateCubicYards(length, width, depth) {
return (length * width * depth) / 27; // 27 cubic feet per cubic yard
},
/**
* Apply waste percentage
*/
applyWaste(quantity, wastePercent) {
return quantity * (1 + wastePercent / 100);
},
/**
* Calculate tax amount
*/
calculateTax(amount, taxRate) {
return amount * taxRate;
}
};