Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 7 additions & 70 deletions backend/src/opsce/assets/entities/asset.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import {
UpdateDateColumn,
DeleteDateColumn,
ManyToOne,

OneToMany,

JoinColumn,
Index,
Check,
Expand Down Expand Up @@ -110,9 +107,9 @@ export enum AssetCondition {
}

export enum DepreciationMethod {
STRAIGHT_LINE = 'straight_line',
DECLINING_BALANCE = 'declining_balance',
NONE = 'none',
STRAIGHT_LINE = 'straight_line',
DECLINING_BALANCE = 'declining_balance',
NONE = 'none',
}

export enum MaintenanceFrequency {
Expand Down Expand Up @@ -183,27 +180,13 @@ export interface AssetCheckout {
// ─── Entity ───────────────────────────────────────────────────────────────────

@Entity('assets')

@Index('IDX_ASSET_STATUS_CATEGORY', ['status', 'category'])
@Index('IDX_ASSET_DEPT_STATUS', ['departmentId', 'status'])
@Index('IDX_ASSET_DEPT_STATUS', ['departmentId', 'status'])
@Index('IDX_ASSET_LOCATION_STATUS', ['locationId', 'status'])
@Index('IDX_ASSET_ASSIGNED_USER', ['assignedToUserId'])
@Index('IDX_ASSET_DELETED_AT', ['deletedAt'])

@Index('IDX_ASSET_STATUS_CATEGORY', ['status', 'category'])
@Index('IDX_ASSET_DEPT_STATUS', ['departmentId', 'status'])
@Index('IDX_ASSET_LOCATION_STATUS', ['locationId', 'status'])
@Index('IDX_ASSET_ASSIGNED_USER', ['assignedToUserId'])
@Index('IDX_ASSET_DELETED_AT', ['deletedAt'])

@Index('IDX_ASSET_ASSIGNED_USER', ['assignedToUserId'])
@Index('IDX_ASSET_DELETED_AT', ['deletedAt'])
@Check(`"name" <> ''`)
@Check(`"purchaseValue" IS NULL OR "purchaseValue" >= 0`)
@Check(`"currentValue" IS NULL OR "currentValue" >= 0`)
@Check(`"residualValue" IS NULL OR "residualValue" >= 0`)
@Check(`"usefulLifeYears" IS NULL OR "usefulLifeYears" > 0`)
@Check(
`"warrantyExpiryDate" IS NULL OR "purchaseDate" IS NULL OR "warrantyExpiryDate" >= "purchaseDate"`,
)
@Check(`"warrantyExpiryDate" IS NULL OR "purchaseDate" IS NULL OR "warrantyExpiryDate" >= "purchaseDate"`)
export class Asset {

// ─── Identity ───────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -441,14 +424,6 @@ export class Asset {
@Column({ type: 'date', nullable: true })
expectedReturnDate?: Date;

/**
* Rolling checkout history (last 50 entries).
* Full history should be stored in a dedicated `asset_checkouts` table
* for high-volume assets.
*/
@Column({ type: 'jsonb', nullable: true, default: [] })
checkoutHistory?: AssetCheckout[];

// ─── Placement ───────────────────────────────────────────────────────────────

@Column({ type: 'uuid', nullable: true })
Expand Down Expand Up @@ -577,44 +552,6 @@ export class Asset {
return Math.max(0, parseFloat(total.toFixed(2)));
}

/**
* Estimated current book value based on straight-line depreciation.
* Returns currentValue when explicitly set, falls back to computed value.
*/
get estimatedBookValue(): number | null {
if (this.currentValue != null) return Number(this.currentValue);

if (this.accruedDepreciation == null || this.purchaseValue == null)
return null;
return Math.max(
this.residualValue ?? 0,
parseFloat(
(Number(this.purchaseValue) - this.accruedDepreciation).toFixed(2),
),

if (this.accruedDepreciation == null || this.purchaseValue == null) return null;
return Math.max(
this.residualValue ?? 0,
parseFloat((Number(this.purchaseValue) - this.accruedDepreciation).toFixed(2)),

);
}

get isWarrantyExpired(): boolean {
if (!this.warrantyExpiryDate) return false;
return new Date(this.warrantyExpiryDate) < new Date();
}

get isInsuranceExpired(): boolean {
if (!this.insuranceExpiryDate) return false;
return new Date(this.insuranceExpiryDate) < new Date();
}

get isMaintenanceOverdue(): boolean {
if (!this.nextMaintenanceDue) return false;
return new Date(this.nextMaintenanceDue) < new Date();
}

// ─── Lifecycle hooks ─────────────────────────────────────────────────────────

@BeforeInsert()
Expand Down
52 changes: 13 additions & 39 deletions backend/src/opsce/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
BeforeInsert,
BeforeUpdate,
Check,
OneToMany,
} from 'typeorm';
import { Exclude } from 'class-transformer';
import * as bcrypt from 'bcrypt';
Expand Down Expand Up @@ -41,6 +40,7 @@ const MAX_FAILED_ATTEMPTS = 5;
@Check(`"failedLoginAttempts" >= 0`)
@Check(`"failedLoginAttempts" <= ${MAX_FAILED_ATTEMPTS}`)
export class User {

// ─── Identity ──────────────────────────────────────────────────────────

@PrimaryGeneratedColumn('uuid')
Expand Down Expand Up @@ -135,13 +135,9 @@ export class User {
@Column({ nullable: true, length: 64 })
timezone?: string;

// ─── Preferences (JSON) ────────────────────────────────────────────────
// ─── Preferences ───────────────────────────────────────────────────────

/**
* Arbitrary user preferences stored as a JSON column.
* Using `simple-json` avoids a separate table for simple key-value pairs.
*/
@Column({ type: 'simple-json', nullable: true })
@Column({ type: 'jsonb', nullable: true })
preferences?: Record<string, unknown>;

// ─── Timestamps ────────────────────────────────────────────────────────
Expand All @@ -155,6 +151,15 @@ export class User {
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date;

// ─── Transient / virtual fields ────────────────────────────────────────

/**
* Transient plain-text password. Set via `setPassword()` — never persisted.
* Excluded from serialisation by class-transformer.
*/
@Exclude()
private _plainPassword?: string;

// ─── Lifecycle hooks ───────────────────────────────────────────────────

/**
Expand All @@ -179,15 +184,6 @@ export class User {
}
}

// ─── Transient / virtual fields ────────────────────────────────────────

/**
* Transient plain-text password. Set via `setPassword()` — never persisted.
* Excluded from serialisation by class-transformer.
*/
@Exclude()
private _plainPassword?: string;

// ─── Domain methods ────────────────────────────────────────────────────

/**
Expand All @@ -209,9 +205,7 @@ export class User {
return bcrypt.compare(candidate, this.passwordHash);
}

/**
* Record a successful login, resetting the failure counter and lock.
*/
/** Record a successful login, resetting the failure counter and lock. */
recordLoginSuccess(ip?: string): void {
this.failedLoginAttempts = 0;
this.lockedUntil = undefined;
Expand Down Expand Up @@ -267,24 +261,4 @@ export class User {
this.passwordResetExpiresAt > new Date()
);
}

/**
* Resolve a user's display name in priority order:
* username → first word of fullName → email local-part.
*/
get displayName(): string {
if (this.username) return this.username;
if (this.fullName) return this.fullName.split(' ')[0];
return this.email.split('@')[0];
}

/** Guard: only ADMIN users may perform privileged operations. */
isAdmin(): boolean {
return this.role === UserRole.ADMIN;
}

/** Guard: ADMIN or MANAGER may perform management operations. */
canManage(): boolean {
return this.role === UserRole.ADMIN || this.role === UserRole.MANAGER;
}
}
Loading