diff --git a/backend/src/assets/assets-import.controller.ts b/backend/src/assets/assets-import.controller.ts
new file mode 100644
index 000000000..fab82fd6b
--- /dev/null
+++ b/backend/src/assets/assets-import.controller.ts
@@ -0,0 +1,26 @@
+import {
+ Controller,
+ Post,
+ Req,
+ UploadedFile,
+ UseGuards,
+ UseInterceptors,
+} from '@nestjs/common';
+import { AuthGuard } from '@nestjs/passport';
+import { FileInterceptor } from '@nestjs/platform-express';
+import { AssetsImportService } from './assets-import.service';
+
+@Controller('assets')
+@UseGuards(AuthGuard('jwt'))
+export class AssetsImportController {
+ constructor(private readonly assetsImportService: AssetsImportService) {}
+
+ @Post('import')
+ @UseInterceptors(FileInterceptor('file'))
+ async import(
+ @UploadedFile() file: Express.Multer.File,
+ @Req() req: any,
+ ) {
+ return this.assetsImportService.import(file, req.user?.id);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/assets/assets-import.service.ts b/backend/src/assets/assets-import.service.ts
new file mode 100644
index 000000000..38e472555
--- /dev/null
+++ b/backend/src/assets/assets-import.service.ts
@@ -0,0 +1,83 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository, EntityManager } from 'typeorm';
+import { Asset, AssetStatus, AssetCondition } from '../entities/asset.entity';
+import { ImportAssetDto } from './dtos/import-asset.dto';
+import { Category } from '../../categories/entities/category.entity';
+import { Department } from '../../users/entities/department.entity';
+import * as Papa from 'papaparse';
+import * as ExcelJS from 'exceljs';
+
+@Injectable()
+export class AssetsImportService {
+ constructor(
+ @InjectRepository(Asset)
+ private readonly assetRepository: Repository,
+ @InjectRepository(Category)
+ private readonly categoryRepository: Repository,
+ @InjectRepository(Department)
+ private readonly departmentRepository: Repository,
+ private readonly entityManager: EntityManager,
+ ) {}
+
+ async import(file: Express.Multer.File, userId: string) {
+ const assetsToCreate: ImportAssetDto[] = [];
+ const errors = [];
+
+ if (file.mimetype === 'text/csv') {
+ const parsed = Papa.parse(file.buffer.toString(), { header: true });
+ assetsToCreate.push(...parsed.data as ImportAssetDto[]);
+ } else {
+ const workbook = new ExcelJS.Workbook();
+ await workbook.xlsx.load(file.buffer);
+ const worksheet = workbook.worksheets[0];
+ const headerRow = worksheet.getRow(1);
+ worksheet.eachRow((row, rowNumber) => {
+ if (rowNumber > 1) {
+ const rowData: any = {};
+ row.eachCell((cell, colNumber) => {
+ const headerCell = headerRow.getCell(colNumber);
+ rowData[headerCell.value as string] = cell.value;
+ });
+ assetsToCreate.push(rowData);
+ }
+ });
+ }
+
+ let importedCount = 0;
+
+ await this.entityManager.transaction(async (transactionalEntityManager) => {
+ for (const [index, assetData] of assetsToCreate.entries()) {
+ try {
+ const category = await this.categoryRepository.findOne({ where: { name: assetData.category } });
+ const department = await this.departmentRepository.findOne({ where: { name: assetData.department } });
+
+ if (!category || !department) {
+ errors.push({ rowIndex: index, message: 'Invalid category or department' });
+ continue;
+ }
+
+ const newAsset = this.assetRepository.create({
+ ...assetData,
+ categoryId: category.id,
+ departmentId: department.id,
+ createdBy: userId,
+ status: assetData.status || AssetStatus.ACTIVE,
+ condition: assetData.condition || AssetCondition.NEW,
+ });
+
+ await transactionalEntityManager.save(newAsset);
+ importedCount++;
+ } catch (error) {
+ errors.push({ rowIndex: index, message: error.message });
+ }
+ }
+ });
+
+ return {
+ importedCount,
+ errorCount: errors.length,
+ errors,
+ };
+ }
+}
\ No newline at end of file
diff --git a/backend/src/assets/assets.module.ts b/backend/src/assets/assets.module.ts
index e0f3de880..e4bd41851 100644
--- a/backend/src/assets/assets.module.ts
+++ b/backend/src/assets/assets.module.ts
@@ -1,13 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
-import { Asset } from './asset.entity';
+import { Asset } from './entities/asset.entity';
import { AssetsService } from './assets.service';
import { AssetsController } from './assets.controller';
+import { AssetsImportController } from './assets-import.controller';
+import { AssetsImportService } from './assets-import.service';
+import { Category } from '../categories/entities/category.entity';
+import { Department } from '../users/entities/department.entity';
@Module({
- imports: [TypeOrmModule.forFeature([Asset])],
- controllers: [AssetsController],
- providers: [AssetsService],
+ imports: [TypeOrmModule.forFeature([Asset, Category, Department])],
+ controllers: [AssetsController, AssetsImportController],
+ providers: [AssetsService, AssetsImportService],
exports: [AssetsService],
})
-export class AssetsModule {}
+export class AssetsModule {}
\ No newline at end of file
diff --git a/backend/src/assets/dtos/import-asset.dto.ts b/backend/src/assets/dtos/import-asset.dto.ts
new file mode 100644
index 000000000..9d17870c7
--- /dev/null
+++ b/backend/src/assets/dtos/import-asset.dto.ts
@@ -0,0 +1,45 @@
+import { IsString, IsOptional, IsEnum, IsNumber } from 'class-validator';
+import { AssetStatus, AssetCondition } from '../entities/asset.entity';
+
+export class ImportAssetDto {
+ @IsString()
+ name: string;
+
+ @IsString()
+ category: string;
+
+ @IsString()
+ department: string;
+
+ @IsString()
+ @IsOptional()
+ serialNumber?: string;
+
+ @IsString()
+ @IsOptional()
+ manufacturer?: string;
+
+ @IsString()
+ @IsOptional()
+ model?: string;
+
+ @IsString()
+ @IsOptional()
+ location?: string;
+
+ @IsEnum(AssetCondition)
+ @IsOptional()
+ condition?: AssetCondition;
+
+ @IsEnum(AssetStatus)
+ @IsOptional()
+ status?: AssetStatus;
+
+ @IsNumber()
+ @IsOptional()
+ purchasePrice?: number;
+
+ @IsString()
+ @IsOptional()
+ notes?: string;
+}
\ No newline at end of file
diff --git a/frontend/app/(dashboard)/assets/page.tsx b/frontend/app/(dashboard)/assets/page.tsx
index 47e9b02e4..b5c677aab 100644
--- a/frontend/app/(dashboard)/assets/page.tsx
+++ b/frontend/app/(dashboard)/assets/page.tsx
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/assets/status-badge";
import { ConditionBadge } from "@/components/assets/condition-badge";
import { CreateAssetModal } from "@/components/assets/create-asset-modal";
+import { ImportAssetModal } from "@/components/assets/import-asset-modal";
import { ScannerModal } from "@/components/assets/ScannerModal";
import { useAssets } from "@/lib/query/hooks/useAssets";
import { AssetStatus, AssetCondition } from "@/lib/query/types/asset";
@@ -17,6 +18,11 @@ const STATUS_OPTIONS = ["All", ...Object.values(AssetStatus)];
export default function AssetsPage() {
const router = useRouter();
+ const [showModal, setShowModal] = useState(false);
+ const [showImportModal, setShowImportModal] = useState(false);
+ const [search, setSearch] = useState("");
+ const [status, setStatus] = useState("");
+ const [page, setPage] = useState(1);
const [showScanner, setShowScanner] = useState(false);
const { toast } = useToast();
@@ -66,6 +72,14 @@ export default function AssetsPage() {
: "No assets yet"}
+
+