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:
Misaka
2026-03-02 23:10:15 +08:00
parent 982fb8fde6
commit a05c8a9037
57 changed files with 5150 additions and 974 deletions

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}