feat: add TypeORM, logger, schemas, hooks and stores
- Add TypeORM integration with data-source, entities and repositories - Add logger service for structured logging - Add Zod validation schemas for auth, cleaner and extractor - Add custom React hooks (useAuth, useCleaner, useExtractor, useValidation) - Add Zustand stores (useAppStore, useUserStore) - Add UI components (Button, Modal, Toast) - Add error types and ErpBrowserManager - Refactor IPC handlers and services - Add unit tests for new modules Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Repository for DiscreteMaterialPlan entity
|
||||
*
|
||||
* Provides type-safe database operations for discrete material plan data.
|
||||
*/
|
||||
|
||||
import { DataSource, Repository, In } from 'typeorm'
|
||||
import { DiscreteMaterialPlan, MaterialPlanRecordData } from '../entities/DiscreteMaterialPlan'
|
||||
import { getDataSource } from '../data-source'
|
||||
import { createLogger } from '../../logger'
|
||||
|
||||
const log = createLogger('DiscreteMaterialPlanRepository')
|
||||
|
||||
/**
|
||||
* DiscreteMaterialPlan Repository class
|
||||
*/
|
||||
export class DiscreteMaterialPlanRepository {
|
||||
private repository: Repository<DiscreteMaterialPlan> | null = null
|
||||
private dataSource: DataSource | null = null
|
||||
|
||||
/**
|
||||
* Get the repository instance
|
||||
*/
|
||||
private async getRepository(): Promise<Repository<DiscreteMaterialPlan>> {
|
||||
if (!this.repository) {
|
||||
this.dataSource = getDataSource()
|
||||
if (!this.dataSource.isInitialized) {
|
||||
await this.dataSource.initialize()
|
||||
}
|
||||
this.repository = this.dataSource.getRepository(DiscreteMaterialPlan)
|
||||
}
|
||||
return this.repository
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all records
|
||||
*/
|
||||
async queryAll(): Promise<DiscreteMaterialPlan[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.find()
|
||||
} catch (error) {
|
||||
log.error('Query all failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all records with deduplication by MaterialCode
|
||||
*/
|
||||
async queryAllDistinctByMaterialCode(): Promise<DiscreteMaterialPlan[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
|
||||
const query = `
|
||||
WITH RankedRecords AS (
|
||||
SELECT *,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY MaterialCode
|
||||
ORDER BY CreateDate ASC, SequenceNumber ASC
|
||||
) AS rn
|
||||
FROM DiscreteMaterialPlanData
|
||||
WHERE MaterialCode IS NOT NULL
|
||||
)
|
||||
SELECT * FROM RankedRecords WHERE rn = 1
|
||||
`
|
||||
|
||||
return await repo.query(query)
|
||||
} catch (error) {
|
||||
log.error('Query all distinct by material code failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by source numbers (production order numbers)
|
||||
*/
|
||||
async queryBySourceNumbers(sourceNumbers: string[]): Promise<DiscreteMaterialPlan[]> {
|
||||
if (!sourceNumbers.length) return []
|
||||
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
const batchSize = 2000
|
||||
const allResults: DiscreteMaterialPlan[] = []
|
||||
|
||||
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
|
||||
const batch = sourceNumbers.slice(i, i + batchSize)
|
||||
const results = await repo.find({
|
||||
where: { sourceNumber: In(batch) }
|
||||
})
|
||||
allResults.push(...results)
|
||||
}
|
||||
|
||||
return allResults
|
||||
} catch (error) {
|
||||
log.error('Query by source numbers failed', {
|
||||
count: sourceNumbers.length,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by source numbers with deduplication by MaterialCode
|
||||
*/
|
||||
async queryBySourceNumbersDistinct(sourceNumbers: string[]): Promise<DiscreteMaterialPlan[]> {
|
||||
if (!sourceNumbers.length) return []
|
||||
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
const batchSize = 2000
|
||||
const allResults: DiscreteMaterialPlan[] = []
|
||||
|
||||
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
|
||||
const batch = sourceNumbers.slice(i, i + batchSize)
|
||||
|
||||
const query = `
|
||||
WITH RankedRecords AS (
|
||||
SELECT *,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY MaterialCode
|
||||
ORDER BY CreateDate ASC, SequenceNumber ASC
|
||||
) AS rn
|
||||
FROM DiscreteMaterialPlanData
|
||||
WHERE SourceNumber IN (?) AND MaterialCode IS NOT NULL
|
||||
)
|
||||
SELECT * FROM RankedRecords WHERE rn = 1
|
||||
`
|
||||
|
||||
const results = await repo.query(query, [batch])
|
||||
allResults.push(...results)
|
||||
}
|
||||
|
||||
return allResults
|
||||
} catch (error) {
|
||||
log.error('Query by source numbers distinct failed', {
|
||||
count: sourceNumbers.length,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by single source number
|
||||
*/
|
||||
async queryBySourceNumber(sourceNumber: string): Promise<DiscreteMaterialPlan[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.find({ where: { sourceNumber } })
|
||||
} catch (error) {
|
||||
log.error('Query by source number failed', {
|
||||
sourceNumber,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by plan number
|
||||
*/
|
||||
async queryByPlanNumber(planNumber: string): Promise<DiscreteMaterialPlan[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.find({ where: { planNumber } })
|
||||
} catch (error) {
|
||||
log.error('Query by plan number failed', {
|
||||
planNumber,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by multiple plan numbers
|
||||
*/
|
||||
async queryByPlanNumbers(planNumbers: string[]): Promise<DiscreteMaterialPlan[]> {
|
||||
if (!planNumbers.length) return []
|
||||
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.find({
|
||||
where: { planNumber: In(planNumbers) }
|
||||
})
|
||||
} catch (error) {
|
||||
log.error('Query by plan numbers failed', {
|
||||
count: planNumbers.length,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all records
|
||||
*/
|
||||
async countAll(): Promise<number> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.count()
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique material names
|
||||
*/
|
||||
async getUniqueMaterialNames(sourceNumbers?: string[]): Promise<string[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
|
||||
let query = repo
|
||||
.createQueryBuilder('m')
|
||||
.select('DISTINCT m.materialName', 'materialName')
|
||||
.where('m.materialName IS NOT NULL')
|
||||
|
||||
if (sourceNumbers && sourceNumbers.length > 0) {
|
||||
query = query.andWhere('m.sourceNumber IN (:...sourceNumbers)', { sourceNumbers })
|
||||
}
|
||||
|
||||
const result = await query.orderBy('m.materialName', 'ASC').getRawMany()
|
||||
return result.map((r) => r.materialName).filter(Boolean)
|
||||
} catch (error) {
|
||||
log.error('Get unique material names failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
async getStatistics(): Promise<{
|
||||
totalRecords: number
|
||||
uniquePlans: number
|
||||
uniqueOrders: number
|
||||
earliestRecord: Date | null
|
||||
latestRecord: Date | null
|
||||
}> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
|
||||
const result = await repo
|
||||
.createQueryBuilder('m')
|
||||
.select('COUNT(*)', 'totalRecords')
|
||||
.addSelect('COUNT(DISTINCT m.planNumber)', 'uniquePlans')
|
||||
.addSelect('COUNT(DISTINCT m.sourceNumber)', 'uniqueOrders')
|
||||
.addSelect('MIN(m.createDate)', 'earliestRecord')
|
||||
.addSelect('MAX(m.createDate)', 'latestRecord')
|
||||
.getRawOne()
|
||||
|
||||
return {
|
||||
totalRecords: parseInt(result?.totalRecords || '0', 10),
|
||||
uniquePlans: parseInt(result?.uniquePlans || '0', 10),
|
||||
uniqueOrders: parseInt(result?.uniqueOrders || '0', 10),
|
||||
earliestRecord: result?.earliestRecord || null,
|
||||
latestRecord: result?.latestRecord || null
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Get statistics failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return {
|
||||
totalRecords: 0,
|
||||
uniquePlans: 0,
|
||||
uniqueOrders: 0,
|
||||
earliestRecord: null,
|
||||
latestRecord: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Repository for MaterialsToBeDeleted entity
|
||||
*
|
||||
* Provides type-safe database operations for materials to be deleted.
|
||||
*/
|
||||
|
||||
import { DataSource, Repository, In } from 'typeorm'
|
||||
import { MaterialsToBeDeleted, MaterialRecordData } from '../entities/MaterialsToBeDeleted'
|
||||
import { getDataSource } from '../data-source'
|
||||
import { createLogger } from '../../logger'
|
||||
|
||||
const log = createLogger('MaterialsToBeDeletedRepository')
|
||||
|
||||
/**
|
||||
* Upsert statistics
|
||||
*/
|
||||
export interface UpsertStats {
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* MaterialsToBeDeleted Repository class
|
||||
*/
|
||||
export class MaterialsToBeDeletedRepository {
|
||||
private repository: Repository<MaterialsToBeDeleted> | null = null
|
||||
private dataSource: DataSource | null = null
|
||||
|
||||
/**
|
||||
* Get the repository instance
|
||||
*/
|
||||
private async getRepository(): Promise<Repository<MaterialsToBeDeleted>> {
|
||||
if (!this.repository) {
|
||||
this.dataSource = getDataSource()
|
||||
if (!this.dataSource.isInitialized) {
|
||||
await this.dataSource.initialize()
|
||||
}
|
||||
this.repository = this.dataSource.getRepository(MaterialsToBeDeleted)
|
||||
}
|
||||
return this.repository
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a single material record
|
||||
*/
|
||||
async upsert(materialCode: string, managerName: string | null): Promise<boolean> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
|
||||
// Use upsert pattern
|
||||
let entity = await repo.findOne({ where: { materialCode } })
|
||||
|
||||
if (entity) {
|
||||
entity.managerName = managerName
|
||||
} else {
|
||||
entity = repo.create({ materialCode, managerName })
|
||||
}
|
||||
|
||||
await repo.save(entity)
|
||||
log.debug('Upserted material', { materialCode })
|
||||
return true
|
||||
} catch (error) {
|
||||
log.error('Upsert material failed', {
|
||||
materialCode,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update multiple material records in batch
|
||||
*/
|
||||
async upsertBatch(materials: MaterialRecordData[]): Promise<UpsertStats> {
|
||||
const stats: UpsertStats = {
|
||||
total: materials.length,
|
||||
success: 0,
|
||||
failed: 0
|
||||
}
|
||||
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
|
||||
for (const material of materials) {
|
||||
if (!material.materialCode?.trim()) {
|
||||
stats.failed++
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
let entity = await repo.findOne({ where: { materialCode: material.materialCode } })
|
||||
|
||||
if (entity) {
|
||||
entity.managerName = material.managerName
|
||||
} else {
|
||||
entity = repo.create({
|
||||
materialCode: material.materialCode,
|
||||
managerName: material.managerName
|
||||
})
|
||||
}
|
||||
|
||||
await repo.save(entity)
|
||||
stats.success++
|
||||
} catch {
|
||||
stats.failed++
|
||||
}
|
||||
}
|
||||
|
||||
log.info('Batch upsert completed', stats)
|
||||
return stats
|
||||
} catch (error) {
|
||||
log.error('Batch upsert failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
stats.failed = stats.total - stats.success
|
||||
return stats
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all material codes as a set
|
||||
*/
|
||||
async getAllMaterialCodes(): Promise<Set<string>> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
const records = await repo.find({
|
||||
select: ['materialCode'],
|
||||
where: { materialCode: In([]) } // This will be overridden
|
||||
})
|
||||
|
||||
// Use query builder for better performance
|
||||
const result = await repo
|
||||
.createQueryBuilder('m')
|
||||
.select('m.materialCode')
|
||||
.where('m.materialCode IS NOT NULL')
|
||||
.getMany()
|
||||
|
||||
return new Set(result.map((r) => r.materialCode).filter(Boolean))
|
||||
} catch (error) {
|
||||
log.error('Get all material codes failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all records
|
||||
*/
|
||||
async getAllRecords(): Promise<MaterialsToBeDeleted[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.find({
|
||||
order: { managerName: 'ASC', materialCode: 'ASC' }
|
||||
})
|
||||
} catch (error) {
|
||||
log.error('Get all records failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get materials by manager name
|
||||
*/
|
||||
async getByManager(managerName: string): Promise<MaterialsToBeDeleted[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.find({
|
||||
where: { managerName },
|
||||
order: { materialCode: 'ASC' }
|
||||
})
|
||||
} catch (error) {
|
||||
log.error('Get by manager failed', {
|
||||
managerName,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique manager names
|
||||
*/
|
||||
async getManagers(): Promise<string[]> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
const result = await repo
|
||||
.createQueryBuilder('m')
|
||||
.select('DISTINCT m.managerName', 'managerName')
|
||||
.where('m.managerName IS NOT NULL')
|
||||
.orderBy('m.managerName', 'ASC')
|
||||
.getRawMany()
|
||||
|
||||
return result.map((r) => r.managerName).filter(Boolean)
|
||||
} catch (error) {
|
||||
log.error('Get managers failed', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete by material code
|
||||
*/
|
||||
async deleteByMaterialCode(materialCode: string): Promise<boolean> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
const result = await repo.delete({ materialCode })
|
||||
return (result.affected ?? 0) > 0
|
||||
} catch (error) {
|
||||
log.error('Delete by material code failed', {
|
||||
materialCode,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple materials by codes
|
||||
*/
|
||||
async deleteByMaterialCodes(materialCodes: string[]): Promise<number> {
|
||||
if (!materialCodes.length) return 0
|
||||
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
const result = await repo.delete({ materialCode: In(materialCodes) })
|
||||
return result.affected ?? 0
|
||||
} catch (error) {
|
||||
log.error('Delete by material codes failed', {
|
||||
count: materialCodes.length,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a material exists
|
||||
*/
|
||||
async exists(materialCode: string): Promise<boolean> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
const count = await repo.count({ where: { materialCode } })
|
||||
return count > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all records
|
||||
*/
|
||||
async countAll(): Promise<number> {
|
||||
try {
|
||||
const repo = await this.getRepository()
|
||||
return await repo.count()
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user