task-b complate
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { assignAsset, parseAssignmentPayload, requireAssetAccess } from '../../_lib';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireAssetAccess(PERMISSIONS.assetAssign);
|
||||
const payload = parseAssignmentPayload(await request.json());
|
||||
|
||||
await assignAsset(organization.id, id, session.user.id, payload);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Asset assigned successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to assign asset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { getAssetMovements, requireAssetAccess } from '../../_lib';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization } = await requireAssetAccess(PERMISSIONS.assetRead);
|
||||
const movements = await getAssetMovements(organization.id, id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Asset movements loaded successfully',
|
||||
movements
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load asset movements' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { parseRepairPayload, repairAsset, requireAssetAccess } from '../../_lib';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireAssetAccess(PERMISSIONS.assetRepair);
|
||||
const payload = parseRepairPayload(await request.json());
|
||||
|
||||
await repairAsset(organization.id, id, session.user.id, payload);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Asset repair recorded successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to record repair' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { parseReturnPayload, requireAssetAccess, returnAsset } from '../../_lib';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireAssetAccess(PERMISSIONS.assetReturn);
|
||||
const payload = parseReturnPayload(await request.json());
|
||||
|
||||
await returnAsset(organization.id, id, session.user.id, payload);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Asset returned successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to return asset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { assets } from '@/db/schema';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
getAssetById,
|
||||
getAssetMovements,
|
||||
getAssetRepairs,
|
||||
parseAssetPayload,
|
||||
requireAssetAccess,
|
||||
updateAsset
|
||||
} from '../_lib';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization } = await requireAssetAccess(PERMISSIONS.assetRead);
|
||||
const [asset, movements, repairs] = await Promise.all([
|
||||
getAssetById(organization.id, id),
|
||||
getAssetMovements(organization.id, id),
|
||||
getAssetRepairs(organization.id, id)
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Asset loaded successfully',
|
||||
asset,
|
||||
movements,
|
||||
repairs
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load asset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireAssetAccess(PERMISSIONS.assetUpdate);
|
||||
const payload = parseAssetPayload(await request.json());
|
||||
|
||||
await updateAsset(organization.id, id, session.user.id, payload);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Asset updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to update asset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization } = await requireAssetAccess(PERMISSIONS.assetUpdate);
|
||||
|
||||
await db.delete(assets).where(and(eq(assets.id, id), eq(assets.organizationId, organization.id)));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Asset deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to delete asset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { parseTransferPayload, requireAssetAccess, transferAsset } from '../../_lib';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireAssetAccess(PERMISSIONS.assetTransfer);
|
||||
const payload = parseTransferPayload(await request.json());
|
||||
|
||||
await transferAsset(organization.id, id, session.user.id, payload);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Asset transferred successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to transfer asset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,926 +0,0 @@
|
||||
import { and, asc, desc, eq, isNull, sql } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
assetAssignments,
|
||||
assetMovements,
|
||||
assetRepairs,
|
||||
assets,
|
||||
departments,
|
||||
employees,
|
||||
locations,
|
||||
organizations,
|
||||
sites,
|
||||
users
|
||||
} from '@/db/schema';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
import { db } from '@/lib/db';
|
||||
import type {
|
||||
Asset,
|
||||
AssetAssignmentPayload,
|
||||
AssetFilters,
|
||||
AssetMutationPayload,
|
||||
AssetOptionsResponse,
|
||||
AssetRepair,
|
||||
AssetRepairPayload,
|
||||
AssetReturnPayload,
|
||||
AssetTransferPayload,
|
||||
AssetMovement
|
||||
} from '@/features/assets/api/types';
|
||||
|
||||
const assetSchema = z.object({
|
||||
assetCode: z.string().min(1),
|
||||
assetName: z.string().min(1),
|
||||
assetType: z.enum(['hardware', 'software']),
|
||||
assetCategory: z.string().min(1),
|
||||
brand: z.string().optional().or(z.literal('')),
|
||||
model: z.string().optional().or(z.literal('')),
|
||||
specification: z.string().optional().or(z.literal('')),
|
||||
serialNumber: z.string().optional().or(z.literal('')),
|
||||
siteId: z.string().optional().nullable(),
|
||||
departmentId: z.string().optional().nullable(),
|
||||
locationId: z.string().optional().nullable(),
|
||||
currentEmployeeId: z.string().optional().nullable(),
|
||||
custodianTeam: z.string().optional().or(z.literal('')),
|
||||
purchaseDate: z.string().optional().nullable(),
|
||||
warrantyExpiryDate: z.string().optional().nullable(),
|
||||
eosDate: z.string().optional().nullable(),
|
||||
eolDate: z.string().optional().nullable(),
|
||||
eopDate: z.string().optional().nullable(),
|
||||
status: z.enum(['AVAILABLE', 'ASSIGNED', 'IN_REPAIR', 'LOST', 'RETIRED']),
|
||||
assetCondition: z.enum(['NORMAL', 'DAMAGED']),
|
||||
dispositionStatus: z.enum([
|
||||
'NONE',
|
||||
'WAITING_REPAIR',
|
||||
'WAITING_WRITE_OFF',
|
||||
'WRITE_OFF_COMPLETED'
|
||||
]),
|
||||
notes: z.string().optional().or(z.literal(''))
|
||||
});
|
||||
|
||||
const assignmentSchema = z.object({
|
||||
employeeId: z.string().min(1),
|
||||
departmentId: z.string().optional().nullable(),
|
||||
locationId: z.string().optional().nullable(),
|
||||
siteId: z.string().optional().nullable(),
|
||||
assignDate: z.string().min(1),
|
||||
documentAttachment: z.string().min(1),
|
||||
reason: z.string().optional().or(z.literal(''))
|
||||
});
|
||||
|
||||
const transferSchema = z.object({
|
||||
newEmployeeId: z.string().optional().nullable(),
|
||||
newDepartmentId: z.string().optional().nullable(),
|
||||
newLocationId: z.string().optional().nullable(),
|
||||
newSiteId: z.string().optional().nullable(),
|
||||
newAssetCode: z.string().optional().or(z.literal('')),
|
||||
transferDate: z.string().min(1),
|
||||
referenceDocument: z.string().min(1),
|
||||
reason: z.string().optional().or(z.literal(''))
|
||||
});
|
||||
|
||||
const returnSchema = z.object({
|
||||
returnDate: z.string().min(1),
|
||||
assetCondition: z.enum(['NORMAL', 'DAMAGED']),
|
||||
remark: z.string().min(1)
|
||||
});
|
||||
|
||||
const repairSchema = z.object({
|
||||
repairDate: z.string().optional().nullable(),
|
||||
vendor: z.string().optional().or(z.literal('')),
|
||||
problem: z.string().min(1),
|
||||
resolution: z.string().optional().or(z.literal('')),
|
||||
cost: z.number().nullable().optional(),
|
||||
markAsRepair: z.boolean().optional().default(true)
|
||||
});
|
||||
|
||||
function asDate(value?: string | null) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function iso(value: Date | string | null) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||
}
|
||||
|
||||
function sanitizeText(value?: string | null) {
|
||||
return value && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
type AssetRow = {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
assetUid: string;
|
||||
assetCode: string;
|
||||
assetName: string;
|
||||
companyName: string | null;
|
||||
assetType: string;
|
||||
assetCategory: string;
|
||||
brand: string | null;
|
||||
model: string | null;
|
||||
specification: string | null;
|
||||
serialNumber: string | null;
|
||||
siteId: string | null;
|
||||
siteName: string | null;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
locationId: string | null;
|
||||
locationName: string | null;
|
||||
currentEmployeeId: string | null;
|
||||
currentEmployeeName: string | null;
|
||||
custodianTeam: string | null;
|
||||
purchaseDate: Date | null;
|
||||
warrantyExpiryDate: Date | null;
|
||||
eosDate: Date | null;
|
||||
eolDate: Date | null;
|
||||
eopDate: Date | null;
|
||||
status: string;
|
||||
assetCondition: string;
|
||||
dispositionStatus: string;
|
||||
notes: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
function formatAsset(row: AssetRow): Asset {
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
assetUid: row.assetUid,
|
||||
assetCode: row.assetCode,
|
||||
assetName: row.assetName,
|
||||
companyName: row.companyName,
|
||||
assetType: row.assetType === 'software' ? 'software' : 'hardware',
|
||||
assetCategory: row.assetCategory,
|
||||
brand: row.brand,
|
||||
model: row.model,
|
||||
specification: row.specification,
|
||||
serialNumber: row.serialNumber,
|
||||
siteId: row.siteId,
|
||||
siteName: row.siteName,
|
||||
departmentId: row.departmentId,
|
||||
departmentName: row.departmentName,
|
||||
locationId: row.locationId,
|
||||
locationName: row.locationName,
|
||||
currentEmployeeId: row.currentEmployeeId,
|
||||
currentEmployeeName: row.currentEmployeeName,
|
||||
custodianTeam: row.custodianTeam,
|
||||
purchaseDate: iso(row.purchaseDate),
|
||||
warrantyExpiryDate: iso(row.warrantyExpiryDate),
|
||||
eosDate: iso(row.eosDate),
|
||||
eolDate: iso(row.eolDate),
|
||||
eopDate: iso(row.eopDate),
|
||||
status: (['AVAILABLE', 'ASSIGNED', 'IN_REPAIR', 'LOST', 'RETIRED'] as const).includes(
|
||||
row.status as Asset['status']
|
||||
)
|
||||
? (row.status as Asset['status'])
|
||||
: 'AVAILABLE',
|
||||
assetCondition: (['NORMAL', 'DAMAGED'] as const).includes(row.assetCondition as Asset['assetCondition'])
|
||||
? (row.assetCondition as Asset['assetCondition'])
|
||||
: 'NORMAL',
|
||||
dispositionStatus: (
|
||||
['NONE', 'WAITING_REPAIR', 'WAITING_WRITE_OFF', 'WRITE_OFF_COMPLETED'] as const
|
||||
).includes(row.dispositionStatus as Asset['dispositionStatus'])
|
||||
? (row.dispositionStatus as Asset['dispositionStatus'])
|
||||
: 'NONE',
|
||||
notes: row.notes,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function requireAssetAccess(permission: string) {
|
||||
return requireOrganizationAccess({ permission });
|
||||
}
|
||||
|
||||
export function parseAssetPayload(body: unknown): AssetMutationPayload {
|
||||
const parsed = assetSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid asset payload');
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function parseAssignmentPayload(body: unknown): AssetAssignmentPayload {
|
||||
const parsed = assignmentSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid assignment payload');
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function parseTransferPayload(body: unknown): AssetTransferPayload {
|
||||
const parsed = transferSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid transfer payload');
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function parseReturnPayload(body: unknown): AssetReturnPayload {
|
||||
const parsed = returnSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid return payload');
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function parseRepairPayload(body: unknown): AssetRepairPayload {
|
||||
const parsed = repairSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid repair payload');
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
async function getBaseAssetRows(organizationId: string) {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: assets.id,
|
||||
organizationId: assets.organizationId,
|
||||
assetUid: assets.assetUid,
|
||||
assetCode: assets.assetCode,
|
||||
assetName: assets.assetName,
|
||||
companyName: organizations.name,
|
||||
assetType: assets.assetType,
|
||||
assetCategory: assets.assetCategory,
|
||||
brand: assets.brand,
|
||||
model: assets.model,
|
||||
specification: assets.specification,
|
||||
serialNumber: assets.serialNumber,
|
||||
siteId: assets.siteId,
|
||||
siteName: sites.name,
|
||||
departmentId: assets.departmentId,
|
||||
departmentName: departments.name,
|
||||
locationId: assets.locationId,
|
||||
locationName: locations.name,
|
||||
currentEmployeeId: assets.currentEmployeeId,
|
||||
currentEmployeeName: employees.name,
|
||||
custodianTeam: assets.custodianTeam,
|
||||
purchaseDate: assets.purchaseDate,
|
||||
warrantyExpiryDate: assets.warrantyExpiryDate,
|
||||
eosDate: assets.eosDate,
|
||||
eolDate: assets.eolDate,
|
||||
eopDate: assets.eopDate,
|
||||
status: assets.status,
|
||||
assetCondition: assets.assetCondition,
|
||||
dispositionStatus: assets.dispositionStatus,
|
||||
notes: assets.notes,
|
||||
createdAt: assets.createdAt,
|
||||
updatedAt: assets.updatedAt
|
||||
})
|
||||
.from(assets)
|
||||
.innerJoin(organizations, eq(organizations.id, assets.organizationId))
|
||||
.leftJoin(sites, eq(sites.id, assets.siteId))
|
||||
.leftJoin(departments, eq(departments.id, assets.departmentId))
|
||||
.leftJoin(locations, eq(locations.id, assets.locationId))
|
||||
.leftJoin(employees, eq(employees.id, assets.currentEmployeeId))
|
||||
.where(eq(assets.organizationId, organizationId));
|
||||
|
||||
return rows as AssetRow[];
|
||||
}
|
||||
|
||||
export async function listAssetsForOrganization(organizationId: string, filters: AssetFilters) {
|
||||
let rows = await getBaseAssetRows(organizationId);
|
||||
|
||||
if (filters.search) {
|
||||
const search = filters.search.toLowerCase();
|
||||
rows = rows.filter((row) =>
|
||||
[
|
||||
row.assetUid,
|
||||
row.assetCode,
|
||||
row.assetName,
|
||||
row.assetCategory,
|
||||
row.companyName,
|
||||
row.brand,
|
||||
row.model,
|
||||
row.serialNumber
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
const selected = filters.status
|
||||
.split(/[.,]/)
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
rows = rows.filter((row) => selected.includes(row.status));
|
||||
}
|
||||
|
||||
if (filters.assetType) {
|
||||
const selected = filters.assetType
|
||||
.split(/[.,]/)
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
rows = rows.filter((row) => selected.includes(row.assetType));
|
||||
}
|
||||
|
||||
if (filters.assetCondition) {
|
||||
const selected = filters.assetCondition
|
||||
.split(/[.,]/)
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
rows = rows.filter((row) => selected.includes(row.assetCondition));
|
||||
}
|
||||
|
||||
if (filters.dispositionStatus) {
|
||||
const selected = filters.dispositionStatus
|
||||
.split(/[.,]/)
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
rows = rows.filter((row) => selected.includes(row.dispositionStatus));
|
||||
}
|
||||
|
||||
if (filters.sort) {
|
||||
try {
|
||||
const [primary] = JSON.parse(filters.sort) as Array<{ id: string; desc: boolean }>;
|
||||
if (primary) {
|
||||
rows = rows.toSorted((a, b) => {
|
||||
const left = String((a as Record<string, unknown>)[primary.id] ?? '');
|
||||
const right = String((b as Record<string, unknown>)[primary.id] ?? '');
|
||||
return primary.desc ? right.localeCompare(left) : left.localeCompare(right);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
rows = rows.toSorted((a, b) => a.assetCode.localeCompare(b.assetCode));
|
||||
}
|
||||
} else {
|
||||
rows = rows.toSorted((a, b) => a.assetCode.localeCompare(b.assetCode));
|
||||
}
|
||||
|
||||
const page = filters.page ?? 1;
|
||||
const limit = filters.limit ?? 10;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
return {
|
||||
total: rows.length,
|
||||
offset,
|
||||
limit,
|
||||
assets: rows.slice(offset, offset + limit).map(formatAsset)
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAssetById(organizationId: string, assetId: string) {
|
||||
const rows = await getBaseAssetRows(organizationId);
|
||||
const asset = rows.find((row) => row.id === assetId);
|
||||
|
||||
if (!asset) {
|
||||
throw new AuthError('Asset not found', 404);
|
||||
}
|
||||
|
||||
return formatAsset(asset);
|
||||
}
|
||||
|
||||
export async function getAssetRepairs(organizationId: string, assetId: string) {
|
||||
const rows = await db.query.assetRepairs.findMany({
|
||||
where: and(eq(assetRepairs.organizationId, organizationId), eq(assetRepairs.assetId, assetId)),
|
||||
orderBy: desc(assetRepairs.repairDate)
|
||||
});
|
||||
|
||||
return rows.map<AssetRepair>((row) => ({
|
||||
id: row.id,
|
||||
assetId: row.assetId,
|
||||
repairDate: row.repairDate.toISOString(),
|
||||
vendor: row.vendor,
|
||||
problem: row.problem,
|
||||
resolution: row.resolution,
|
||||
cost: row.cost ? Number(row.cost) : null
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getAssetMovements(organizationId: string, assetId?: string) {
|
||||
const conditions = [eq(assetMovements.organizationId, organizationId)];
|
||||
if (assetId) {
|
||||
conditions.push(eq(assetMovements.assetId, assetId));
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: assetMovements.id,
|
||||
assetId: assetMovements.assetId,
|
||||
assetUid: assets.assetUid,
|
||||
assetCode: assets.assetCode,
|
||||
assetName: assets.assetName,
|
||||
eventType: assetMovements.eventType,
|
||||
eventDate: assetMovements.eventDate,
|
||||
reason: assetMovements.reason,
|
||||
referenceDocument: assetMovements.referenceDocument,
|
||||
performedByName: users.name,
|
||||
metadata: assetMovements.metadata
|
||||
})
|
||||
.from(assetMovements)
|
||||
.innerJoin(assets, eq(assets.id, assetMovements.assetId))
|
||||
.leftJoin(users, eq(users.id, assetMovements.performedByUserId))
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(assetMovements.eventDate));
|
||||
|
||||
return rows.map<AssetMovement>((row) => ({
|
||||
id: row.id,
|
||||
assetId: row.assetId,
|
||||
assetUid: row.assetUid,
|
||||
assetCode: row.assetCode,
|
||||
assetName: row.assetName,
|
||||
eventType: row.eventType as AssetMovement['eventType'],
|
||||
eventDate: row.eventDate.toISOString(),
|
||||
reason: row.reason,
|
||||
referenceDocument: row.referenceDocument,
|
||||
performedByName: row.performedByName,
|
||||
metadata: (row.metadata as Record<string, unknown> | null) ?? null
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getAssetOptions(organizationId: string): Promise<AssetOptionsResponse> {
|
||||
const [siteRows, departmentRows, locationRows, employeeRows] = await Promise.all([
|
||||
db.select().from(sites).where(eq(sites.organizationId, organizationId)).orderBy(asc(sites.name)),
|
||||
db
|
||||
.select()
|
||||
.from(departments)
|
||||
.where(eq(departments.organizationId, organizationId))
|
||||
.orderBy(asc(departments.name)),
|
||||
listLocations(organizationId),
|
||||
listEmployees(organizationId)
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Asset options loaded successfully',
|
||||
sites: siteRows.map((row) => ({
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
code: row.code,
|
||||
name: row.name
|
||||
})),
|
||||
departments: departmentRows.map((row) => ({
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
code: row.code,
|
||||
name: row.name
|
||||
})),
|
||||
locations: locationRows,
|
||||
employees: employeeRows
|
||||
};
|
||||
}
|
||||
|
||||
async function listLocations(organizationId: string) {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: locations.id,
|
||||
organizationId: locations.organizationId,
|
||||
siteId: locations.siteId,
|
||||
siteName: sites.name,
|
||||
building: locations.building,
|
||||
floor: locations.floor,
|
||||
area: locations.area,
|
||||
name: locations.name
|
||||
})
|
||||
.from(locations)
|
||||
.leftJoin(sites, eq(sites.id, locations.siteId))
|
||||
.where(eq(locations.organizationId, organizationId))
|
||||
.orderBy(asc(locations.name));
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function listEmployees(organizationId: string) {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: employees.id,
|
||||
organizationId: employees.organizationId,
|
||||
employeeNo: employees.employeeNo,
|
||||
name: employees.name,
|
||||
departmentId: employees.departmentId,
|
||||
departmentName: departments.name,
|
||||
siteId: employees.siteId,
|
||||
siteName: sites.name,
|
||||
status: employees.status
|
||||
})
|
||||
.from(employees)
|
||||
.leftJoin(departments, eq(departments.id, employees.departmentId))
|
||||
.leftJoin(sites, eq(sites.id, employees.siteId))
|
||||
.where(eq(employees.organizationId, organizationId))
|
||||
.orderBy(asc(employees.name));
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function buildAssetUid() {
|
||||
const result = await db.select({ count: sql<number>`count(*)::int` }).from(assets);
|
||||
const nextValue = (result[0]?.count ?? 0) + 1;
|
||||
return `AST-${String(nextValue).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
function buildSnapshot(asset: Asset) {
|
||||
return {
|
||||
assetCode: asset.assetCode,
|
||||
assetName: asset.assetName,
|
||||
siteId: asset.siteId,
|
||||
departmentId: asset.departmentId,
|
||||
locationId: asset.locationId,
|
||||
currentEmployeeId: asset.currentEmployeeId,
|
||||
status: asset.status,
|
||||
assetCondition: asset.assetCondition,
|
||||
dispositionStatus: asset.dispositionStatus
|
||||
};
|
||||
}
|
||||
|
||||
async function insertMovement(tx: Pick<typeof db, 'insert'>, values: {
|
||||
assetId: string;
|
||||
organizationId: string;
|
||||
eventType: string;
|
||||
performedByUserId: string;
|
||||
reason?: string | null;
|
||||
referenceDocument?: string | null;
|
||||
snapshotBefore?: Record<string, unknown> | null;
|
||||
snapshotAfter?: Record<string, unknown> | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
eventDate?: Date;
|
||||
}) {
|
||||
await tx.insert(assetMovements).values({
|
||||
id: crypto.randomUUID(),
|
||||
assetId: values.assetId,
|
||||
organizationId: values.organizationId,
|
||||
eventType: values.eventType,
|
||||
eventDate: values.eventDate ?? new Date(),
|
||||
reason: values.reason ?? null,
|
||||
referenceDocument: values.referenceDocument ?? null,
|
||||
performedByUserId: values.performedByUserId,
|
||||
snapshotBefore: values.snapshotBefore ?? null,
|
||||
snapshotAfter: values.snapshotAfter ?? null,
|
||||
metadata: values.metadata ?? null
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAsset(organizationId: string, performedByUserId: string, payload: AssetMutationPayload) {
|
||||
const assetId = crypto.randomUUID();
|
||||
const assetUid = await buildAssetUid();
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(assets).values({
|
||||
id: assetId,
|
||||
organizationId,
|
||||
assetUid,
|
||||
assetCode: payload.assetCode,
|
||||
assetName: payload.assetName,
|
||||
assetType: payload.assetType,
|
||||
assetCategory: payload.assetCategory,
|
||||
brand: sanitizeText(payload.brand),
|
||||
model: sanitizeText(payload.model),
|
||||
specification: sanitizeText(payload.specification),
|
||||
serialNumber: sanitizeText(payload.serialNumber),
|
||||
siteId: payload.siteId ?? null,
|
||||
departmentId: payload.departmentId ?? null,
|
||||
locationId: payload.locationId ?? null,
|
||||
currentEmployeeId: payload.currentEmployeeId ?? null,
|
||||
custodianTeam: sanitizeText(payload.custodianTeam),
|
||||
purchaseDate: asDate(payload.purchaseDate),
|
||||
warrantyExpiryDate: asDate(payload.warrantyExpiryDate),
|
||||
eosDate: asDate(payload.eosDate),
|
||||
eolDate: asDate(payload.eolDate),
|
||||
eopDate: asDate(payload.eopDate),
|
||||
status: payload.status,
|
||||
assetCondition: payload.assetCondition,
|
||||
dispositionStatus: payload.dispositionStatus,
|
||||
notes: sanitizeText(payload.notes)
|
||||
});
|
||||
|
||||
await insertMovement(tx, {
|
||||
assetId,
|
||||
organizationId,
|
||||
eventType: 'CREATE',
|
||||
performedByUserId,
|
||||
snapshotAfter: {
|
||||
assetCode: payload.assetCode,
|
||||
assetName: payload.assetName,
|
||||
status: payload.status,
|
||||
assetCondition: payload.assetCondition,
|
||||
dispositionStatus: payload.dispositionStatus,
|
||||
assetUid
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return assetId;
|
||||
}
|
||||
|
||||
export async function updateAsset(
|
||||
organizationId: string,
|
||||
assetId: string,
|
||||
performedByUserId: string,
|
||||
payload: AssetMutationPayload
|
||||
) {
|
||||
const currentAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(assets)
|
||||
.set({
|
||||
assetCode: payload.assetCode,
|
||||
assetName: payload.assetName,
|
||||
assetType: payload.assetType,
|
||||
assetCategory: payload.assetCategory,
|
||||
brand: sanitizeText(payload.brand),
|
||||
model: sanitizeText(payload.model),
|
||||
specification: sanitizeText(payload.specification),
|
||||
serialNumber: sanitizeText(payload.serialNumber),
|
||||
siteId: payload.siteId ?? null,
|
||||
departmentId: payload.departmentId ?? null,
|
||||
locationId: payload.locationId ?? null,
|
||||
currentEmployeeId: payload.currentEmployeeId ?? null,
|
||||
custodianTeam: sanitizeText(payload.custodianTeam),
|
||||
purchaseDate: asDate(payload.purchaseDate),
|
||||
warrantyExpiryDate: asDate(payload.warrantyExpiryDate),
|
||||
eosDate: asDate(payload.eosDate),
|
||||
eolDate: asDate(payload.eolDate),
|
||||
eopDate: asDate(payload.eopDate),
|
||||
status: payload.status,
|
||||
assetCondition: payload.assetCondition,
|
||||
dispositionStatus: payload.dispositionStatus,
|
||||
notes: sanitizeText(payload.notes),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(assets.id, assetId), eq(assets.organizationId, organizationId)));
|
||||
|
||||
const updatedAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await insertMovement(tx, {
|
||||
assetId,
|
||||
organizationId,
|
||||
eventType: 'UPDATE',
|
||||
performedByUserId,
|
||||
snapshotBefore: buildSnapshot(currentAsset),
|
||||
snapshotAfter: buildSnapshot(updatedAsset)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function assignAsset(
|
||||
organizationId: string,
|
||||
assetId: string,
|
||||
performedByUserId: string,
|
||||
payload: AssetAssignmentPayload
|
||||
) {
|
||||
const currentAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(assetAssignments).values({
|
||||
id: crypto.randomUUID(),
|
||||
assetId,
|
||||
organizationId,
|
||||
employeeId: payload.employeeId,
|
||||
departmentId: payload.departmentId ?? currentAsset.departmentId,
|
||||
locationId: payload.locationId ?? currentAsset.locationId,
|
||||
siteId: payload.siteId ?? currentAsset.siteId,
|
||||
assignedAt: asDate(payload.assignDate) ?? new Date(),
|
||||
documentAttachment: sanitizeText(payload.documentAttachment),
|
||||
assignedByUserId: performedByUserId
|
||||
});
|
||||
|
||||
await tx
|
||||
.update(assets)
|
||||
.set({
|
||||
currentEmployeeId: payload.employeeId,
|
||||
departmentId: payload.departmentId ?? currentAsset.departmentId,
|
||||
locationId: payload.locationId ?? currentAsset.locationId,
|
||||
siteId: payload.siteId ?? currentAsset.siteId,
|
||||
status: 'ASSIGNED',
|
||||
dispositionStatus: 'NONE',
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(assets.id, assetId), eq(assets.organizationId, organizationId)));
|
||||
|
||||
const updatedAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await insertMovement(tx, {
|
||||
assetId,
|
||||
organizationId,
|
||||
eventType: 'ASSIGN',
|
||||
performedByUserId,
|
||||
reason: payload.reason ?? null,
|
||||
referenceDocument: payload.documentAttachment,
|
||||
eventDate: asDate(payload.assignDate) ?? new Date(),
|
||||
snapshotBefore: buildSnapshot(currentAsset),
|
||||
snapshotAfter: buildSnapshot(updatedAsset),
|
||||
metadata: {
|
||||
employeeId: payload.employeeId,
|
||||
departmentId: payload.departmentId ?? null,
|
||||
locationId: payload.locationId ?? null,
|
||||
siteId: payload.siteId ?? null,
|
||||
documentAttachment: payload.documentAttachment
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function transferAsset(
|
||||
organizationId: string,
|
||||
assetId: string,
|
||||
performedByUserId: string,
|
||||
payload: AssetTransferPayload
|
||||
) {
|
||||
const currentAsset = await getAssetById(organizationId, assetId);
|
||||
const transferEventDate = asDate(payload.transferDate) ?? new Date();
|
||||
const nextAssetCode = sanitizeText(payload.newAssetCode) ?? currentAsset.assetCode;
|
||||
const nextEmployeeId = payload.newEmployeeId ?? currentAsset.currentEmployeeId;
|
||||
const nextDepartmentId = payload.newDepartmentId ?? currentAsset.departmentId;
|
||||
const nextLocationId = payload.newLocationId ?? currentAsset.locationId;
|
||||
const nextSiteId = payload.newSiteId ?? currentAsset.siteId;
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(assetAssignments)
|
||||
.set({
|
||||
returnedAt: transferEventDate,
|
||||
returnedByUserId: performedByUserId,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(assetAssignments.assetId, assetId),
|
||||
eq(assetAssignments.organizationId, organizationId),
|
||||
isNull(assetAssignments.returnedAt)
|
||||
)
|
||||
);
|
||||
|
||||
if (nextEmployeeId) {
|
||||
await tx.insert(assetAssignments).values({
|
||||
id: crypto.randomUUID(),
|
||||
assetId,
|
||||
organizationId,
|
||||
employeeId: nextEmployeeId,
|
||||
departmentId: nextDepartmentId,
|
||||
locationId: nextLocationId,
|
||||
siteId: nextSiteId,
|
||||
assignedAt: transferEventDate,
|
||||
documentAttachment: sanitizeText(payload.referenceDocument),
|
||||
assignedByUserId: performedByUserId
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(assets)
|
||||
.set({
|
||||
currentEmployeeId: nextEmployeeId,
|
||||
departmentId: nextDepartmentId,
|
||||
locationId: nextLocationId,
|
||||
siteId: nextSiteId,
|
||||
assetCode: nextAssetCode,
|
||||
status: nextEmployeeId ? 'ASSIGNED' : 'AVAILABLE',
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(assets.id, assetId), eq(assets.organizationId, organizationId)));
|
||||
|
||||
const updatedAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await insertMovement(tx, {
|
||||
assetId,
|
||||
organizationId,
|
||||
eventType: 'TRANSFER',
|
||||
performedByUserId,
|
||||
reason: payload.reason ?? null,
|
||||
referenceDocument: payload.referenceDocument,
|
||||
eventDate: transferEventDate,
|
||||
snapshotBefore: buildSnapshot(currentAsset),
|
||||
snapshotAfter: buildSnapshot(updatedAsset),
|
||||
metadata: {
|
||||
oldEmployeeId: currentAsset.currentEmployeeId,
|
||||
newEmployeeId: nextEmployeeId,
|
||||
oldDepartmentId: currentAsset.departmentId,
|
||||
newDepartmentId: nextDepartmentId,
|
||||
oldLocationId: currentAsset.locationId,
|
||||
newLocationId: nextLocationId,
|
||||
oldSiteId: currentAsset.siteId,
|
||||
newSiteId: nextSiteId,
|
||||
oldAssetCode: currentAsset.assetCode,
|
||||
newAssetCode: nextAssetCode
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function returnAsset(
|
||||
organizationId: string,
|
||||
assetId: string,
|
||||
performedByUserId: string,
|
||||
payload: AssetReturnPayload
|
||||
) {
|
||||
const currentAsset = await getAssetById(organizationId, assetId);
|
||||
const returnEventDate = asDate(payload.returnDate) ?? new Date();
|
||||
const nextDispositionStatus = payload.assetCondition === 'DAMAGED' ? 'WAITING_REPAIR' : 'NONE';
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(assetAssignments)
|
||||
.set({
|
||||
returnedAt: returnEventDate,
|
||||
returnedByUserId: performedByUserId,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(assetAssignments.assetId, assetId),
|
||||
eq(assetAssignments.organizationId, organizationId),
|
||||
isNull(assetAssignments.returnedAt)
|
||||
)
|
||||
);
|
||||
|
||||
await tx
|
||||
.update(assets)
|
||||
.set({
|
||||
currentEmployeeId: null,
|
||||
status: 'AVAILABLE',
|
||||
assetCondition: payload.assetCondition,
|
||||
dispositionStatus: nextDispositionStatus,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(assets.id, assetId), eq(assets.organizationId, organizationId)));
|
||||
|
||||
const updatedAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await insertMovement(tx, {
|
||||
assetId,
|
||||
organizationId,
|
||||
eventType: 'RETURN',
|
||||
performedByUserId,
|
||||
reason: payload.remark,
|
||||
eventDate: returnEventDate,
|
||||
snapshotBefore: buildSnapshot(currentAsset),
|
||||
snapshotAfter: buildSnapshot(updatedAsset),
|
||||
metadata: {
|
||||
assetCondition: payload.assetCondition,
|
||||
dispositionStatus: nextDispositionStatus
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function repairAsset(
|
||||
organizationId: string,
|
||||
assetId: string,
|
||||
performedByUserId: string,
|
||||
payload: AssetRepairPayload
|
||||
) {
|
||||
const currentAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(assetRepairs).values({
|
||||
id: crypto.randomUUID(),
|
||||
assetId,
|
||||
organizationId,
|
||||
repairDate: asDate(payload.repairDate) ?? new Date(),
|
||||
vendor: sanitizeText(payload.vendor),
|
||||
problem: payload.problem,
|
||||
resolution: sanitizeText(payload.resolution),
|
||||
cost: payload.cost != null ? String(payload.cost) : null,
|
||||
createdByUserId: performedByUserId
|
||||
});
|
||||
|
||||
if (payload.markAsRepair ?? true) {
|
||||
await tx
|
||||
.update(assets)
|
||||
.set({
|
||||
status: 'IN_REPAIR',
|
||||
dispositionStatus: 'WAITING_REPAIR',
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(assets.id, assetId), eq(assets.organizationId, organizationId)));
|
||||
}
|
||||
|
||||
const updatedAsset = await getAssetById(organizationId, assetId);
|
||||
|
||||
await insertMovement(tx, {
|
||||
assetId,
|
||||
organizationId,
|
||||
eventType: 'REPAIR',
|
||||
performedByUserId,
|
||||
eventDate: asDate(payload.repairDate) ?? new Date(),
|
||||
snapshotBefore: buildSnapshot(currentAsset),
|
||||
snapshotAfter: buildSnapshot(updatedAsset),
|
||||
metadata: {
|
||||
vendor: payload.vendor ?? null,
|
||||
problem: payload.problem,
|
||||
resolution: payload.resolution ?? null,
|
||||
cost: payload.cost ?? null
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { startOfMonth } from 'date-fns';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import {
|
||||
getAssetMovements,
|
||||
listAssetsForOrganization,
|
||||
requireAssetAccess
|
||||
} from '../../_lib';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { organization } = await requireAssetAccess(PERMISSIONS.assetRead);
|
||||
const [result, movements] = await Promise.all([
|
||||
listAssetsForOrganization(organization.id, {
|
||||
page: 1,
|
||||
limit: 10000
|
||||
}),
|
||||
getAssetMovements(organization.id)
|
||||
]);
|
||||
|
||||
const monthStart = startOfMonth(new Date());
|
||||
|
||||
const summary = result.assets.reduce(
|
||||
(acc, asset) => {
|
||||
acc.cards.totalAssets += 1;
|
||||
if (asset.status === 'ASSIGNED') acc.cards.assignedAssets += 1;
|
||||
if (asset.status === 'AVAILABLE') acc.cards.inStockAssets += 1;
|
||||
if (!asset.currentEmployeeId) acc.cards.withoutUser += 1;
|
||||
if (!asset.locationId) acc.cards.withoutLocation += 1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
cards: {
|
||||
totalAssets: 0,
|
||||
assignedAssets: 0,
|
||||
inStockAssets: 0,
|
||||
transferThisMonth: 0,
|
||||
withoutUser: 0,
|
||||
withoutLocation: 0
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
summary.cards.transferThisMonth = movements.filter((movement) => {
|
||||
return (
|
||||
movement.eventType === 'TRANSFER' && new Date(movement.eventDate).getTime() >= monthStart.getTime()
|
||||
);
|
||||
}).length;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Asset dashboard summary loaded successfully',
|
||||
...summary
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load dashboard summary' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { getAssetMovements, requireAssetAccess } from '../_lib';
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const { organization } = await requireAssetAccess(PERMISSIONS.assetRead);
|
||||
const movements = await getAssetMovements(organization.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Asset history loaded successfully',
|
||||
movements
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load asset history' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { getAssetOptions, requireAssetAccess } from '../_lib';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { organization } = await requireAssetAccess(PERMISSIONS.assetRead);
|
||||
const response = await getAssetOptions(organization.id);
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load asset options' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
createAsset,
|
||||
listAssetsForOrganization,
|
||||
parseAssetPayload,
|
||||
requireAssetAccess
|
||||
} from './_lib';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization } = await requireAssetAccess(PERMISSIONS.assetRead);
|
||||
const { searchParams } = request.nextUrl;
|
||||
|
||||
const result = await listAssetsForOrganization(organization.id, {
|
||||
page: Number(searchParams.get('page') ?? 1),
|
||||
limit: Number(searchParams.get('limit') ?? 10),
|
||||
search: searchParams.get('search') ?? undefined,
|
||||
status: searchParams.get('status') ?? undefined,
|
||||
assetCondition: searchParams.get('assetCondition') ?? undefined,
|
||||
dispositionStatus: searchParams.get('dispositionStatus') ?? undefined,
|
||||
assetType: searchParams.get('assetType') ?? undefined,
|
||||
sort: searchParams.get('sort') ?? undefined
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Assets loaded successfully',
|
||||
total_assets: result.total,
|
||||
offset: result.offset,
|
||||
limit: result.limit,
|
||||
assets: result.assets
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load assets' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session } = await requireAssetAccess(PERMISSIONS.assetCreate);
|
||||
const payload = parseAssetPayload(await request.json());
|
||||
const id = await createAsset(organization.id, session.user.id, payload);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Asset created successfully',
|
||||
id
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to create asset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { exampleProducts } from '@/features/example-dashboard/data';
|
||||
|
||||
function parseCategories(value: string | null) {
|
||||
if (!value) return [];
|
||||
return value
|
||||
.split(/[.,]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function sortProducts(
|
||||
items: typeof exampleProducts,
|
||||
sort: string | null
|
||||
) {
|
||||
const defaultSorted = items.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (!sort) {
|
||||
return defaultSorted;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(sort) as Array<{ id: keyof (typeof exampleProducts)[number]; desc: boolean }>;
|
||||
const [primarySort] = parsed;
|
||||
|
||||
if (!primarySort?.id) {
|
||||
return defaultSorted;
|
||||
}
|
||||
|
||||
const sorted = items.toSorted((left, right) => {
|
||||
const a = left[primarySort.id];
|
||||
const b = right[primarySort.id];
|
||||
|
||||
if (typeof a === 'number' && typeof b === 'number') {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
return String(a).localeCompare(String(b));
|
||||
});
|
||||
|
||||
return primarySort.desc ? sorted.toReversed() : sorted;
|
||||
} catch {
|
||||
return defaultSorted;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const page = Number(searchParams.get('page') ?? 1);
|
||||
const limit = Number(searchParams.get('limit') ?? 10);
|
||||
const categories = parseCategories(searchParams.get('categories'));
|
||||
const search = searchParams.get('search')?.trim().toLowerCase();
|
||||
const sort = searchParams.get('sort');
|
||||
|
||||
const filtered = exampleProducts.filter((product) => {
|
||||
const matchesCategory = categories.length ? categories.includes(product.category) : true;
|
||||
const matchesSearch = search
|
||||
? [product.name, product.description, product.category].some((value) =>
|
||||
value.toLowerCase().includes(search)
|
||||
)
|
||||
: true;
|
||||
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
const sorted = sortProducts(filtered, sort);
|
||||
const offset = (page - 1) * limit;
|
||||
const products = sorted.slice(offset, offset + limit);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Example products loaded successfully',
|
||||
total_products: sorted.length,
|
||||
offset,
|
||||
limit,
|
||||
products
|
||||
});
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { exampleUsers } from '@/features/example-dashboard/data';
|
||||
|
||||
function parseRoles(value: string | null) {
|
||||
if (!value) return [];
|
||||
return value
|
||||
.split(/[.,]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function sortUsers(
|
||||
items: typeof exampleUsers,
|
||||
sort: string | null
|
||||
) {
|
||||
const defaultSorted = items.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (!sort) {
|
||||
return defaultSorted;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(sort) as Array<{ id: string; desc: boolean }>;
|
||||
const [primarySort] = parsed;
|
||||
|
||||
if (!primarySort?.id) {
|
||||
return defaultSorted;
|
||||
}
|
||||
|
||||
const sorted = items.toSorted((left, right) => {
|
||||
const a =
|
||||
primarySort.id === 'role'
|
||||
? `${left.systemRole}:${left.activeMembershipRole ?? ''}`
|
||||
: primarySort.id === 'organizations'
|
||||
? left.organizations.map((organization) => organization.name).join(', ')
|
||||
: left.name;
|
||||
const b =
|
||||
primarySort.id === 'role'
|
||||
? `${right.systemRole}:${right.activeMembershipRole ?? ''}`
|
||||
: primarySort.id === 'organizations'
|
||||
? right.organizations.map((organization) => organization.name).join(', ')
|
||||
: right.name;
|
||||
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return primarySort.desc ? sorted.toReversed() : sorted;
|
||||
} catch {
|
||||
return defaultSorted;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const page = Number(searchParams.get('page') ?? 1);
|
||||
const limit = Number(searchParams.get('limit') ?? 10);
|
||||
const roles = parseRoles(searchParams.get('roles'));
|
||||
const search = searchParams.get('search')?.trim().toLowerCase();
|
||||
const sort = searchParams.get('sort');
|
||||
|
||||
const filtered = exampleUsers.filter((user) => {
|
||||
const matchesSearch = search
|
||||
? [user.name, user.email].some((value) => value.toLowerCase().includes(search))
|
||||
: true;
|
||||
const matchesRole = roles.length
|
||||
? roles.includes(user.systemRole) ||
|
||||
user.memberships.some((membership) => roles.includes(membership.membershipRole))
|
||||
: true;
|
||||
|
||||
return matchesSearch && matchesRole;
|
||||
});
|
||||
|
||||
const sorted = sortUsers(filtered, sort);
|
||||
const offset = (page - 1) * limit;
|
||||
const users = sorted.slice(offset, offset + limit);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Example users loaded successfully',
|
||||
total_users: sorted.length,
|
||||
offset,
|
||||
limit,
|
||||
users
|
||||
});
|
||||
}
|
||||
60
src/app/api/foundation/master-options/route.ts
Normal file
60
src/app/api/foundation/master-options/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { listMasterOptions } from '@/features/foundation/master-options/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.organizationManage
|
||||
});
|
||||
const { searchParams } = request.nextUrl;
|
||||
const page = Number(searchParams.get('page') ?? 1);
|
||||
const limit = Number(searchParams.get('limit') ?? 10);
|
||||
const search = searchParams.get('search') ?? undefined;
|
||||
const category = searchParams.get('category') ?? undefined;
|
||||
const sort = searchParams.get('sort') ?? undefined;
|
||||
const result = await listMasterOptions({
|
||||
organizationId: organization.id,
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
category,
|
||||
sort
|
||||
});
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Master options loaded successfully',
|
||||
total_items: result.totalItems,
|
||||
offset,
|
||||
limit,
|
||||
items: result.items.map((item) => ({
|
||||
id: item.id,
|
||||
organization_id: item.organizationId,
|
||||
category: item.category,
|
||||
code: item.code,
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
parent_id: item.parentId,
|
||||
parent_label: item.parentLabel,
|
||||
sort_order: item.sortOrder,
|
||||
is_active: item.isActive,
|
||||
deleted_at: item.deletedAt,
|
||||
updated_at: item.updatedAt
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load master options' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import {
|
||||
assertMasterDataEntity,
|
||||
deleteMasterData,
|
||||
parseMasterDataPayload,
|
||||
requireMasterDataAccess,
|
||||
updateMasterData
|
||||
} from '../../_lib';
|
||||
|
||||
type Params = { params: Promise<{ entity: string; id: string }> };
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { entity: rawEntity, id } = await params;
|
||||
const entity = assertMasterDataEntity(rawEntity);
|
||||
const { organization } = await requireMasterDataAccess();
|
||||
const payload = parseMasterDataPayload(entity, await request.json());
|
||||
|
||||
await updateMasterData(entity, organization.id, id, payload);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `${entity} updated successfully`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to update master data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { entity: rawEntity, id } = await params;
|
||||
const entity = assertMasterDataEntity(rawEntity);
|
||||
const { organization } = await requireMasterDataAccess();
|
||||
|
||||
await deleteMasterData(entity, organization.id, id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `${entity} deleted successfully`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to delete master data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import {
|
||||
assertMasterDataEntity,
|
||||
createMasterData,
|
||||
listMasterData,
|
||||
parseMasterDataPayload,
|
||||
requireMasterDataAccess
|
||||
} from '../_lib';
|
||||
|
||||
type Params = { params: Promise<{ entity: string }> };
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { entity: rawEntity } = await params;
|
||||
const entity = assertMasterDataEntity(rawEntity);
|
||||
const { organization } = await requireMasterDataAccess();
|
||||
const items = await listMasterData(entity, organization.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: `${entity} loaded successfully`,
|
||||
items
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load master data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { entity: rawEntity } = await params;
|
||||
const entity = assertMasterDataEntity(rawEntity);
|
||||
const { organization } = await requireMasterDataAccess();
|
||||
const payload = parseMasterDataPayload(entity, await request.json());
|
||||
const id = await createMasterData(entity, organization.id, payload);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: `${entity} created successfully`,
|
||||
id
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to create master data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { departments, employees, locations, sites } from '@/db/schema';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { requireOrganizationAccess } from '@/lib/auth/session';
|
||||
import { db } from '@/lib/db';
|
||||
import type {
|
||||
Department,
|
||||
Employee,
|
||||
Location,
|
||||
MasterDataEntity,
|
||||
MasterDataMutationPayload,
|
||||
Site
|
||||
} from '@/features/assets/api/types';
|
||||
|
||||
const masterDataEntities = ['sites', 'departments', 'locations', 'employees'] as const;
|
||||
|
||||
const entitySchemaMap = {
|
||||
sites: z.object({
|
||||
code: z.string().min(1),
|
||||
name: z.string().min(1)
|
||||
}),
|
||||
departments: z.object({
|
||||
code: z.string().min(1),
|
||||
name: z.string().min(1)
|
||||
}),
|
||||
locations: z.object({
|
||||
name: z.string().min(1),
|
||||
siteId: z.string().optional().nullable(),
|
||||
building: z.string().optional(),
|
||||
floor: z.string().optional(),
|
||||
area: z.string().optional()
|
||||
}),
|
||||
employees: z.object({
|
||||
employeeNo: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
departmentId: z.string().optional().nullable(),
|
||||
siteId: z.string().optional().nullable(),
|
||||
status: z.string().default('active')
|
||||
})
|
||||
} as const;
|
||||
|
||||
export async function requireMasterDataAccess() {
|
||||
return requireOrganizationAccess({ permission: PERMISSIONS.masterDataManage });
|
||||
}
|
||||
|
||||
export function assertMasterDataEntity(entity: string): MasterDataEntity {
|
||||
if ((masterDataEntities as readonly string[]).includes(entity)) {
|
||||
return entity as MasterDataEntity;
|
||||
}
|
||||
|
||||
throw new Error('Invalid master data entity');
|
||||
}
|
||||
|
||||
export function parseMasterDataPayload(
|
||||
entity: MasterDataEntity,
|
||||
body: unknown
|
||||
): MasterDataMutationPayload {
|
||||
const schema = entitySchemaMap[entity];
|
||||
const parsed = schema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid master data payload');
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export async function listMasterData(entity: MasterDataEntity, organizationId: string) {
|
||||
switch (entity) {
|
||||
case 'sites': {
|
||||
const rows = await db.query.sites.findMany({
|
||||
where: eq(sites.organizationId, organizationId)
|
||||
});
|
||||
|
||||
return rows.map<Site>((row) => ({
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
code: row.code,
|
||||
name: row.name
|
||||
}));
|
||||
}
|
||||
case 'departments': {
|
||||
const rows = await db.query.departments.findMany({
|
||||
where: eq(departments.organizationId, organizationId)
|
||||
});
|
||||
|
||||
return rows.map<Department>((row) => ({
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
code: row.code,
|
||||
name: row.name
|
||||
}));
|
||||
}
|
||||
case 'locations': {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: locations.id,
|
||||
organizationId: locations.organizationId,
|
||||
siteId: locations.siteId,
|
||||
siteName: sites.name,
|
||||
building: locations.building,
|
||||
floor: locations.floor,
|
||||
area: locations.area,
|
||||
name: locations.name
|
||||
})
|
||||
.from(locations)
|
||||
.leftJoin(sites, eq(sites.id, locations.siteId))
|
||||
.where(eq(locations.organizationId, organizationId));
|
||||
|
||||
return rows.map<Location>((row) => ({
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
siteId: row.siteId,
|
||||
siteName: row.siteName,
|
||||
building: row.building,
|
||||
floor: row.floor,
|
||||
area: row.area,
|
||||
name: row.name
|
||||
}));
|
||||
}
|
||||
case 'employees': {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: employees.id,
|
||||
organizationId: employees.organizationId,
|
||||
employeeNo: employees.employeeNo,
|
||||
name: employees.name,
|
||||
departmentId: employees.departmentId,
|
||||
departmentName: departments.name,
|
||||
siteId: employees.siteId,
|
||||
siteName: sites.name,
|
||||
status: employees.status
|
||||
})
|
||||
.from(employees)
|
||||
.leftJoin(departments, eq(departments.id, employees.departmentId))
|
||||
.leftJoin(sites, eq(sites.id, employees.siteId))
|
||||
.where(eq(employees.organizationId, organizationId));
|
||||
|
||||
return rows.map<Employee>((row) => ({
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
employeeNo: row.employeeNo,
|
||||
name: row.name,
|
||||
departmentId: row.departmentId,
|
||||
departmentName: row.departmentName,
|
||||
siteId: row.siteId,
|
||||
siteName: row.siteName,
|
||||
status: row.status
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMasterData(
|
||||
entity: MasterDataEntity,
|
||||
organizationId: string,
|
||||
payload: MasterDataMutationPayload
|
||||
) {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
switch (entity) {
|
||||
case 'sites':
|
||||
await db.insert(sites).values({
|
||||
id,
|
||||
organizationId,
|
||||
code: payload.code!,
|
||||
name: payload.name
|
||||
});
|
||||
break;
|
||||
case 'departments':
|
||||
await db.insert(departments).values({
|
||||
id,
|
||||
organizationId,
|
||||
code: payload.code!,
|
||||
name: payload.name
|
||||
});
|
||||
break;
|
||||
case 'locations':
|
||||
await db.insert(locations).values({
|
||||
id,
|
||||
organizationId,
|
||||
siteId: payload.siteId ?? null,
|
||||
building: payload.building ?? null,
|
||||
floor: payload.floor ?? null,
|
||||
area: payload.area ?? null,
|
||||
name: payload.name
|
||||
});
|
||||
break;
|
||||
case 'employees':
|
||||
await db.insert(employees).values({
|
||||
id,
|
||||
organizationId,
|
||||
employeeNo: payload.employeeNo!,
|
||||
name: payload.name,
|
||||
departmentId: payload.departmentId ?? null,
|
||||
siteId: payload.siteId ?? null,
|
||||
status: payload.status ?? 'active'
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function updateMasterData(
|
||||
entity: MasterDataEntity,
|
||||
organizationId: string,
|
||||
id: string,
|
||||
payload: MasterDataMutationPayload
|
||||
) {
|
||||
switch (entity) {
|
||||
case 'sites':
|
||||
await db
|
||||
.update(sites)
|
||||
.set({ code: payload.code!, name: payload.name, updatedAt: new Date() })
|
||||
.where(and(eq(sites.id, id), eq(sites.organizationId, organizationId)));
|
||||
break;
|
||||
case 'departments':
|
||||
await db
|
||||
.update(departments)
|
||||
.set({ code: payload.code!, name: payload.name, updatedAt: new Date() })
|
||||
.where(and(eq(departments.id, id), eq(departments.organizationId, organizationId)));
|
||||
break;
|
||||
case 'locations':
|
||||
await db
|
||||
.update(locations)
|
||||
.set({
|
||||
siteId: payload.siteId ?? null,
|
||||
building: payload.building ?? null,
|
||||
floor: payload.floor ?? null,
|
||||
area: payload.area ?? null,
|
||||
name: payload.name,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(locations.id, id), eq(locations.organizationId, organizationId)));
|
||||
break;
|
||||
case 'employees':
|
||||
await db
|
||||
.update(employees)
|
||||
.set({
|
||||
employeeNo: payload.employeeNo!,
|
||||
name: payload.name,
|
||||
departmentId: payload.departmentId ?? null,
|
||||
siteId: payload.siteId ?? null,
|
||||
status: payload.status ?? 'active',
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(employees.id, id), eq(employees.organizationId, organizationId)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMasterData(entity: MasterDataEntity, organizationId: string, id: string) {
|
||||
switch (entity) {
|
||||
case 'sites':
|
||||
await db.delete(sites).where(and(eq(sites.id, id), eq(sites.organizationId, organizationId)));
|
||||
break;
|
||||
case 'departments':
|
||||
await db
|
||||
.delete(departments)
|
||||
.where(and(eq(departments.id, id), eq(departments.organizationId, organizationId)));
|
||||
break;
|
||||
case 'locations':
|
||||
await db
|
||||
.delete(locations)
|
||||
.where(and(eq(locations.id, id), eq(locations.organizationId, organizationId)));
|
||||
break;
|
||||
case 'employees':
|
||||
await db
|
||||
.delete(employees)
|
||||
.where(and(eq(employees.id, id), eq(employees.organizationId, organizationId)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user