unit/#107-damage-reports #125

Merged
jkeffects merged 6 commits from unit/#107-damage-reports into milestone/ff-admin-unit 2025-07-17 08:50:18 +00:00
21 changed files with 307 additions and 19 deletions
Showing only changes of commit 41c3093754 - Show all commits

View file

@ -1,9 +1,11 @@
export interface CreateDamageReportCommand { export interface CreateDamageReportCommand {
description: string; description: string;
location: string;
note: string;
reportedBy: string; reportedBy: string;
imageCount: number; images: string[];
affectedId: string; affectedId?: string;
affected: "equipment" | "vehicle" | "wearable"; affected?: "equipment" | "vehicle" | "wearable";
} }
export interface UpdateDamageReportCommand { export interface UpdateDamageReportCommand {

View file

@ -22,8 +22,10 @@ export default abstract class DamageReportCommandHandler {
.values({ .values({
status: "eingereicht", status: "eingereicht",
description: createDamageReport.description, description: createDamageReport.description,
location: createDamageReport.location,
note: createDamageReport.note,
reportedBy: createDamageReport.reportedBy, reportedBy: createDamageReport.reportedBy,
imageCount: createDamageReport.imageCount, images: createDamageReport.images,
equipmentId: createDamageReport.affected == "equipment" ? createDamageReport.affectedId : null, equipmentId: createDamageReport.affected == "equipment" ? createDamageReport.affectedId : null,
vehicleId: createDamageReport.affected == "vehicle" ? createDamageReport.affectedId : null, vehicleId: createDamageReport.affected == "vehicle" ? createDamageReport.affectedId : null,
wearableId: createDamageReport.affected == "wearable" ? createDamageReport.affectedId : null, wearableId: createDamageReport.affected == "wearable" ? createDamageReport.affectedId : null,
@ -86,6 +88,7 @@ export default abstract class DamageReportCommandHandler {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async delete(deleteDamageReport: DeleteDamageReportCommand): Promise<void> { static async delete(deleteDamageReport: DeleteDamageReportCommand): Promise<void> {
// TODO: remove related images
return await dataSource return await dataSource
.createQueryBuilder() .createQueryBuilder()
.delete() .delete()

View file

@ -79,7 +79,10 @@ export async function getDamageReportById(req: Request, res: Response): Promise<
*/ */
export async function createDamageReport(req: Request, res: Response): Promise<any> { export async function createDamageReport(req: Request, res: Response): Promise<any> {
const description = req.body.description; const description = req.body.description;
const location = req.body.location;
const note = req.body.note;
const reportedBy = req.body.reportedBy; const reportedBy = req.body.reportedBy;
const images = req.files as Express.Multer.File[];
const affectedId = req.body.affectedId; const affectedId = req.body.affectedId;
const affected = req.body.affected; const affected = req.body.affected;
@ -88,8 +91,10 @@ export async function createDamageReport(req: Request, res: Response): Promise<a
let createDamageReport: CreateDamageReportCommand = { let createDamageReport: CreateDamageReportCommand = {
description, description,
location,
note,
reportedBy, reportedBy,
imageCount: 0, images: images.map((i) => i.filename),
affectedId, affectedId,
affected, affected,
}; };

View file

@ -12,6 +12,17 @@ import { FileSystemHelper } from "../helpers/fileSystemHelper";
import { SocketConnectionTypes } from "../enums/socketEnum"; import { SocketConnectionTypes } from "../enums/socketEnum";
import SocketServer from "../websocket"; import SocketServer from "../websocket";
import BadRequestException from "../exceptions/badRequestException"; import BadRequestException from "../exceptions/badRequestException";
import EquipmentService from "../service/unit/equipment/equipmentService";
import VehicleService from "../service/unit/vehicle/vehicleService";
import WearableService from "../service/unit/wearable/wearableService";
import EquipmentFactory from "../factory/admin/unit/equipment/equipment";
import VehicleFactory from "../factory/admin/unit/vehicle/vehicle";
import WearableFactory from "../factory/admin/unit/wearable/wearable";
import { MinifiedEquipmentViewModel } from "../viewmodel/admin/unit/equipment/equipment.models";
import { MinifiedVehicleViewModel } from "../viewmodel/admin/unit/vehicle/vehicle.models";
import { MinifiedWearableViewModel } from "../viewmodel/admin/unit/wearable/wearable.models";
import DamageReportCommandHandler from "../command/unit/damageReportCommandHandler";
import { CreateDamageReportCommand } from "../command/unit/damageReportCommand";
/** /**
* @description get all calendar items by types or nscdr * @description get all calendar items by types or nscdr
@ -58,8 +69,8 @@ export async function getCalendarItemsByTypes(req: Request, res: Response): Prom
} }
/** /**
* @description get all calendar items by types or nscdr * @description check if scanner session exists
* @summary passphrase is passed as value pair like `type:passphrase` * @summary existance is checked by room exists
* @param req {Request} Express req object * @param req {Request} Express req object
* @param res {Response} Express res object * @param res {Response} Express res object
* @returns {Promise<*>} * @returns {Promise<*>}
@ -77,6 +88,58 @@ export async function checkScannerRoomExists(req: Request, res: Response): Promi
} }
} }
/**
* @description get equipment, vehicle, wearable by code
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function searchStuffByCode(req: Request, res: Response): Promise<any> {
let code = req.query.code.toString();
let e = await EquipmentService.getAllByCode(code);
let v = await VehicleService.getAllByCode(code);
let w = await WearableService.getAllByCode(code);
res.json([
...EquipmentFactory.mapToBaseMinifed(e),
...VehicleFactory.mapToBaseMinifed(v),
...WearableFactory.mapToBaseMinifed(w),
]);
}
/**
* @description create damagereport to equipment, vehicle, wearable
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createDamageReport(req: Request, res: Response): Promise<any> {
const related = (req.body.related ? JSON.parse(req.body.related) : undefined) as
| undefined
| MinifiedEquipmentViewModel
| MinifiedVehicleViewModel
| MinifiedWearableViewModel;
const description = req.body.description;
const location = req.body.location;
const note = req.body.note;
const reportedBy = req.body.reportedBy;
const images = req.files as Express.Multer.File[];
let createDamageReport: CreateDamageReportCommand = {
description: description,
location: location,
note: note,
reportedBy: reportedBy,
images: images.map((i) => i.filename),
affectedId: related ? related.id : undefined,
affected: related ? related.assigned : undefined,
};
await DamageReportCommandHandler.create(createDamageReport);
res.sendStatus(204);
}
/** /**
* @description get configuration of UI * @description get configuration of UI
* @param req {Request} Express req object * @param req {Request} Express req object
@ -91,6 +154,7 @@ export async function getApplicationConfig(req: Request, res: Response): Promise
"club.website": SettingHelper.getSetting("club.website"), "club.website": SettingHelper.getSetting("club.website"),
"app.custom_login_message": SettingHelper.getSetting("app.custom_login_message"), "app.custom_login_message": SettingHelper.getSetting("app.custom_login_message"),
"app.show_link_to_calendar": SettingHelper.getSetting("app.show_link_to_calendar"), "app.show_link_to_calendar": SettingHelper.getSetting("app.show_link_to_calendar"),
"app.show_link_to_damagereport": SettingHelper.getSetting("app.show_link_to_damagereport"),
}; };
res.json(config); res.json(config);

View file

@ -21,12 +21,28 @@ export class damageReport {
@Column({ type: "text" }) @Column({ type: "text" })
description: string; description: string;
@Column({ type: "text" })
location: string;
@Column({ type: "text" })
note: string;
@Column({
type: "text",
transformer: {
from(value: string): Array<string> {
return value.split(",");
},
to(value: Array<string>): string {
return value.join(",");
},
},
})
images: string[];
@Column({ type: "varchar", length: 255 }) @Column({ type: "varchar", length: 255 })
reportedBy: string; reportedBy: string;
@Column({ type: "int", default: 0 })
imageCount: number;
@Column({ nullable: true, default: null }) @Column({ nullable: true, default: null })
equipmentId?: string; equipmentId?: string;

View file

@ -39,7 +39,7 @@ export default abstract class DamageReportFactory {
status: record.status, status: record.status,
done: record.done, done: record.done,
description: record.description, description: record.description,
imageCount: record.imageCount, image: record.images,
reportedBy: record?.reportedBy, reportedBy: record?.reportedBy,
...assigned, ...assigned,
maintenance: record.maintenance ? MaintenanceFactory.mapToSingle(record.maintenance) : null, maintenance: record.maintenance ? MaintenanceFactory.mapToSingle(record.maintenance) : null,

View file

@ -1,5 +1,8 @@
import { equipment } from "../../../../entity/unit/equipment/equipment"; import { equipment } from "../../../../entity/unit/equipment/equipment";
import { EquipmentViewModel } from "../../../../viewmodel/admin/unit/equipment/equipment.models"; import {
EquipmentViewModel,
MinifiedEquipmentViewModel,
} from "../../../../viewmodel/admin/unit/equipment/equipment.models";
import EquipmentTypeFactory from "./equipmentType"; import EquipmentTypeFactory from "./equipmentType";
export default abstract class EquipmentFactory { export default abstract class EquipmentFactory {
@ -29,4 +32,28 @@ export default abstract class EquipmentFactory {
public static mapToBase(records: Array<equipment>): Array<EquipmentViewModel> { public static mapToBase(records: Array<equipment>): Array<EquipmentViewModel> {
return records.map((r) => this.mapToSingle(r)); return records.map((r) => this.mapToSingle(r));
} }
/**
* @description map record to minifed equipment
* @param {equipment} record
* @returns {MinifiedEquipmentViewModel}
*/
public static mapToSingleMinified(record: equipment): MinifiedEquipmentViewModel {
return {
id: record.id,
code: record?.code,
name: record.name,
type: record?.equipmentType.type,
assigned: "equipment",
};
}
/**
* @description map records to minified equipment
* @param {Array<equipment>} records
* @returns {Array<MinifiedEquipmentViewModel>}
*/
public static mapToBaseMinifed(records: Array<equipment>): Array<MinifiedEquipmentViewModel> {
return records.map((r) => this.mapToSingleMinified(r));
}
} }

View file

@ -1,5 +1,5 @@
import { vehicle } from "../../../../entity/unit/vehicle/vehicle"; import { vehicle } from "../../../../entity/unit/vehicle/vehicle";
import { VehicleViewModel } from "../../../../viewmodel/admin/unit/vehicle/vehicle.models"; import { MinifiedVehicleViewModel, VehicleViewModel } from "../../../../viewmodel/admin/unit/vehicle/vehicle.models";
import VehicleTypeFactory from "./vehicleType"; import VehicleTypeFactory from "./vehicleType";
export default abstract class VehicleFactory { export default abstract class VehicleFactory {
@ -29,4 +29,28 @@ export default abstract class VehicleFactory {
public static mapToBase(records: Array<vehicle>): Array<VehicleViewModel> { public static mapToBase(records: Array<vehicle>): Array<VehicleViewModel> {
return records.map((r) => this.mapToSingle(r)); return records.map((r) => this.mapToSingle(r));
} }
/**
* @description map record to minifed vehicle
* @param {vehicle} record
* @returns {MinifiedVehicleViewModel}
*/
public static mapToSingleMinified(record: vehicle): MinifiedVehicleViewModel {
return {
id: record.id,
code: record?.code,
name: record.name,
type: record?.vehicleType.type,
assigned: "vehicle",
};
}
/**
* @description map records to minified vehicle
* @param {Array<vehicle>} records
* @returns {Array<MinifiedVehicleViewModel>}
*/
public static mapToBaseMinifed(records: Array<vehicle>): Array<MinifiedVehicleViewModel> {
return records.map((r) => this.mapToSingleMinified(r));
}
} }

View file

@ -1,5 +1,8 @@
import { wearable } from "../../../../entity/unit/wearable/wearable"; import { wearable } from "../../../../entity/unit/wearable/wearable";
import { WearableViewModel } from "../../../../viewmodel/admin/unit/wearable/wearable.models"; import {
MinifiedWearableViewModel,
WearableViewModel,
} from "../../../../viewmodel/admin/unit/wearable/wearable.models";
import MemberFactory from "../../club/member/member"; import MemberFactory from "../../club/member/member";
import WearableTypeFactory from "./wearableType"; import WearableTypeFactory from "./wearableType";
@ -32,4 +35,28 @@ export default abstract class WearableFactory {
public static mapToBase(records: Array<wearable>): Array<WearableViewModel> { public static mapToBase(records: Array<wearable>): Array<WearableViewModel> {
return records.map((r) => this.mapToSingle(r)); return records.map((r) => this.mapToSingle(r));
} }
/**
* @description map record to minifed wearable
* @param {wearable} record
* @returns {MinifiedWearableViewModel}
*/
public static mapToSingleMinified(record: wearable): MinifiedWearableViewModel {
return {
id: record.id,
code: record?.code,
name: record.name,
type: record?.wearableType.type,
assigned: "wearable",
};
}
/**
* @description map records to minified wearable
* @param {Array<wearable>} records
* @returns {Array<MinifiedWearableViewModel>}
*/
public static mapToBaseMinifed(records: Array<wearable>): Array<MinifiedWearableViewModel> {
return records.map((r) => this.mapToSingleMinified(r));
}
} }

View file

@ -14,8 +14,6 @@ import {
UrlConverter, UrlConverter,
} from "./convertHelper"; } from "./convertHelper";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { rejects } from "assert";
import InternalException from "../exceptions/internalException";
import MailHelper from "./mailHelper"; import MailHelper from "./mailHelper";
export default abstract class SettingHelper { export default abstract class SettingHelper {

View file

@ -2,6 +2,7 @@ import multer from "multer";
import { FileSystemHelper } from "../helpers/fileSystemHelper"; import { FileSystemHelper } from "../helpers/fileSystemHelper";
import path from "path"; import path from "path";
import BadRequestException from "../exceptions/badRequestException"; import BadRequestException from "../exceptions/badRequestException";
import { v4 as uuid } from "uuid";
/**Settings image upload */ /**Settings image upload */
export const clubImageStorage = multer.diskStorage({ export const clubImageStorage = multer.diskStorage({
@ -58,3 +59,28 @@ export const inspectionFileMulter = multer({
}); });
export const inspectionFileUpload = inspectionFileMulter.array("files"); export const inspectionFileUpload = inspectionFileMulter.array("files");
/**public damage report upload */
export const pDamageReportFileStorage = multer.diskStorage({
destination: function (req, file, cb) {
FileSystemHelper.createFolder("damageReport");
cb(null, FileSystemHelper.formatPath("damageReport"));
},
filename: function (req, file, cb) {
const fileExtension = path.extname(file.originalname).toLowerCase();
cb(null, uuid() + fileExtension);
},
});
export const pDamageReportFileMulter = multer({
storage: pDamageReportFileStorage,
fileFilter(req, file, cb) {
if (file.mimetype.startsWith("image/")) {
cb(null, true);
} else {
cb(new BadRequestException("Wrong file format"));
}
},
});
export const pDamageReportFileUpload = pDamageReportFileMulter.array("images");

View file

@ -9,6 +9,8 @@ export const damage_report_table = new Table({
{ name: "status", ...getTypeByORM("varchar") }, { name: "status", ...getTypeByORM("varchar") },
{ name: "done", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }, { name: "done", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
{ name: "description", ...getTypeByORM("text") }, { name: "description", ...getTypeByORM("text") },
{ name: "location", ...getTypeByORM("text") },
{ name: "note", ...getTypeByORM("text") },
{ name: "reportedBy", ...getTypeByORM("varchar") }, { name: "reportedBy", ...getTypeByORM("varchar") },
{ name: "imageCount", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, { name: "imageCount", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) },
{ name: "equipmentId", ...getTypeByORM("uuid", true) }, { name: "equipmentId", ...getTypeByORM("uuid", true) },

View file

@ -1,13 +1,16 @@
import express from "express"; import express from "express";
import { import {
checkScannerRoomExists, checkScannerRoomExists,
createDamageReport,
getApplicationConfig, getApplicationConfig,
getApplicationFavicon, getApplicationFavicon,
getApplicationIcon, getApplicationIcon,
getApplicationLogo, getApplicationLogo,
getApplicationManifest, getApplicationManifest,
getCalendarItemsByTypes, getCalendarItemsByTypes,
searchStuffByCode,
} from "../controller/publicController"; } from "../controller/publicController";
import { pDamageReportFileUpload } from "../middleware/multer";
var router = express.Router({ mergeParams: true }); var router = express.Router({ mergeParams: true });
@ -15,8 +18,12 @@ router.get("/calendar", async (req, res) => {
await getCalendarItemsByTypes(req, res); await getCalendarItemsByTypes(req, res);
}); });
router.post("/reportdamage", async (req, res) => { router.get("/reportdamage", async (req, res) => {
res.send("TODO"); await searchStuffByCode(req, res);
});
router.post("/reportdamage", pDamageReportFileUpload, async (req, res) => {
await createDamageReport(req, res);
}); });
router.post("/checkscannerroom", async (req, res) => { router.post("/checkscannerroom", async (req, res) => {

View file

@ -58,6 +58,26 @@ export default abstract class EquipmentService {
}); });
} }
/**
* @description get equipment by code
* @returns {Promise<Array<equipment>>}
*/
static async getAllByCode(code: string): Promise<Array<equipment>> {
return await dataSource
.getRepository(equipment)
.createQueryBuilder("equipment")
.leftJoinAndSelect("equipment.equipmentType", "equipmenttype")
.where({ code: Like(`%${code}%`) })
.orderBy("name", "ASC")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "equipment", err);
});
}
/** /**
* @description get equipment by id * @description get equipment by id
* @returns {Promise<equipment>} * @returns {Promise<equipment>}

View file

@ -58,6 +58,26 @@ export default abstract class VehicleService {
}); });
} }
/**
* @description get vehicle by code
* @returns {Promise<Array<vehicle>>}
*/
static async getAllByCode(code: string): Promise<Array<vehicle>> {
return await dataSource
.getRepository(vehicle)
.createQueryBuilder("vehicle")
.leftJoinAndSelect("vehicle.vehicleType", "vehicletype")
.where({ code: Like(`%${code}%`) })
.orderBy("name", "ASC")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "vehicle", err);
});
}
/** /**
* @description get vehicle by id * @description get vehicle by id
* @returns {Promise<vehicle>} * @returns {Promise<vehicle>}

View file

@ -59,6 +59,26 @@ export default abstract class WearableService {
}); });
} }
/**
* @description get wearable by code
* @returns {Promise<Array<wearable>>}
*/
static async getAllByCode(code: string): Promise<Array<wearable>> {
return await dataSource
.getRepository(wearable)
.createQueryBuilder("wearable")
.leftJoinAndSelect("wearable.wearableType", "wearabletype")
.where({ code: Like(`%${code}%`) })
.orderBy("name", "ASC")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "wearable", err);
});
}
/** /**
* @description get wearable by id * @description get wearable by id
* @returns {Promise<wearable>} * @returns {Promise<wearable>}

View file

@ -10,6 +10,7 @@ export type SettingString =
| "club.website" | "club.website"
| "app.custom_login_message" | "app.custom_login_message"
| "app.show_link_to_calendar" | "app.show_link_to_calendar"
| "app.show_link_to_damagereport"
| "session.jwt_expiration" | "session.jwt_expiration"
| "session.refresh_expiration" | "session.refresh_expiration"
| "session.pwa_refresh_expiration" | "session.pwa_refresh_expiration"
@ -34,6 +35,7 @@ export type SettingValueMapping = {
"club.website": string; "club.website": string;
"app.custom_login_message": string; "app.custom_login_message": string;
"app.show_link_to_calendar": boolean; "app.show_link_to_calendar": boolean;
"app.show_link_to_damagereport": boolean;
"session.jwt_expiration": ms.StringValue; "session.jwt_expiration": ms.StringValue;
"session.refresh_expiration": ms.StringValue; "session.refresh_expiration": ms.StringValue;
"session.pwa_refresh_expiration": ms.StringValue; "session.pwa_refresh_expiration": ms.StringValue;
@ -68,6 +70,7 @@ export const settingsType: SettingsSchema = {
"club.website": { type: "url", optional: true }, "club.website": { type: "url", optional: true },
"app.custom_login_message": { type: "string", optional: true }, "app.custom_login_message": { type: "string", optional: true },
"app.show_link_to_calendar": { type: "boolean", default: true }, "app.show_link_to_calendar": { type: "boolean", default: true },
"app.show_link_to_damagereport": { type: "boolean", default: false },
"session.jwt_expiration": { type: "ms", default: "15m" }, "session.jwt_expiration": { type: "ms", default: "15m" },
"session.refresh_expiration": { type: "ms", default: "1d" }, "session.refresh_expiration": { type: "ms", default: "1d" },
"session.pwa_refresh_expiration": { type: "ms", default: "5d" }, "session.pwa_refresh_expiration": { type: "ms", default: "5d" },

View file

@ -26,7 +26,7 @@ export type DamageReportViewModel = {
status: string; status: string;
done: boolean; done: boolean;
description: string; description: string;
imageCount: number; image: string[];
reportedBy: string; reportedBy: string;
maintenance?: MaintenanceViewModel; maintenance?: MaintenanceViewModel;
} & DamageReportAssigned; } & DamageReportAssigned;

View file

@ -10,3 +10,11 @@ export interface EquipmentViewModel {
equipmentTypeId: string; equipmentTypeId: string;
equipmentType: EquipmentTypeViewModel; equipmentType: EquipmentTypeViewModel;
} }
export interface MinifiedEquipmentViewModel {
id: string;
code?: string;
name: string;
type: string;
assigned: "equipment";
}

View file

@ -10,3 +10,11 @@ export interface VehicleViewModel {
vehicleTypeId: string; vehicleTypeId: string;
vehicleType: VehicleTypeViewModel; vehicleType: VehicleTypeViewModel;
} }
export interface MinifiedVehicleViewModel {
id: string;
code?: string;
name: string;
type: string;
assigned: "vehicle";
}

View file

@ -13,3 +13,11 @@ export interface WearableViewModel {
wearableTypeId: string; wearableTypeId: string;
wearableType: WearableTypeViewModel; wearableType: WearableTypeViewModel;
} }
export interface MinifiedWearableViewModel {
id: string;
code?: string;
name: string;
type: string;
assigned: "wearable";
}