diff --git a/apps/api/src/policies/lookups.controller.ts b/apps/api/src/policies/lookups.controller.ts index 9ca29a9..7a31e55 100644 --- a/apps/api/src/policies/lookups.controller.ts +++ b/apps/api/src/policies/lookups.controller.ts @@ -6,11 +6,14 @@ import { Param, Patch, Post, + Req, UseGuards, } from "@nestjs/common"; +import { Request } from "express"; import { AuthenticatedGuard } from "../auth/authenticated.guard"; import { AbilityGuard } from "../auth/ability.guard"; import { RequireAbility } from "../auth/require-ability.decorator"; +import { AuditService } from "../common/audit.service"; import { PoliciesService } from "./policies.service"; import { AdjusterDto, @@ -29,7 +32,14 @@ import { @UseGuards(AuthenticatedGuard, AbilityGuard) @Controller("lookups") export class LookupsController { - constructor(private readonly policies: PoliciesService) {} + constructor( + private readonly policies: PoliciesService, + private readonly audit: AuditService, + ) {} + + private actingId(req: Request): string { + return (req.user as { id: string }).id; + } @Get() list() { @@ -38,49 +48,99 @@ export class LookupsController { @Post("providers") @RequireAbility("lookup:manage") - createProvider(@Body() dto: ProviderDto) { - return this.policies.createProvider(dto); + async createProvider(@Body() dto: ProviderDto, @Req() req: Request) { + const row = await this.policies.createProvider(dto); + void this.audit.log(this.actingId(req), "lookup.provider.create", { + providerId: row.id, + name: row.name, + }); + return row; } @Patch("providers/:id") @RequireAbility("lookup:manage") - updateProvider(@Param("id") id: string, @Body() dto: UpdateProviderDto) { - return this.policies.updateProvider(id, dto); + async updateProvider( + @Param("id") id: string, + @Body() dto: UpdateProviderDto, + @Req() req: Request, + ) { + const row = await this.policies.updateProvider(id, dto); + void this.audit.log(this.actingId(req), "lookup.provider.update", { + providerId: id, + }); + return row; } @Delete("providers/:id") @RequireAbility("lookup:manage") - removeProvider(@Param("id") id: string) { - return this.policies.removeProvider(id); + async removeProvider(@Param("id") id: string, @Req() req: Request) { + const row = await this.policies.removeProvider(id); + void this.audit.log(this.actingId(req), "lookup.provider.delete", { + providerId: id, + }); + return row; } @Post("policy-types") @RequireAbility("lookup:manage") - createType(@Body() dto: PolicyTypeDto) { - return this.policies.createPolicyType(dto); + async createType(@Body() dto: PolicyTypeDto, @Req() req: Request) { + const row = await this.policies.createPolicyType(dto); + void this.audit.log(this.actingId(req), "lookup.policyType.create", { + policyTypeId: row.id, + name: row.name, + }); + return row; } @Patch("policy-types/:id") @RequireAbility("lookup:manage") - updateType(@Param("id") id: string, @Body() dto: UpdatePolicyTypeDto) { - return this.policies.updatePolicyType(id, dto); + async updateType( + @Param("id") id: string, + @Body() dto: UpdatePolicyTypeDto, + @Req() req: Request, + ) { + const row = await this.policies.updatePolicyType(id, dto); + void this.audit.log(this.actingId(req), "lookup.policyType.update", { + policyTypeId: id, + }); + return row; } @Delete("policy-types/:id") @RequireAbility("lookup:manage") - removeType(@Param("id") id: string) { - return this.policies.removePolicyType(id); + async removeType(@Param("id") id: string, @Req() req: Request) { + const row = await this.policies.removePolicyType(id); + void this.audit.log(this.actingId(req), "lookup.policyType.delete", { + policyTypeId: id, + }); + return row; } @Post("adjusters") @RequireAbility("lookup:manage") - createAdjuster(@Body() dto: AdjusterDto) { - return this.policies.createAdjuster(dto); + async createAdjuster(@Body() dto: AdjusterDto, @Req() req: Request) { + const row = await this.policies.createAdjuster(dto); + void this.audit.log(this.actingId(req), "lookup.adjuster.create", { + adjusterId: row.id, + }); + return row; } @Patch("adjusters/:id") @RequireAbility("lookup:manage") - updateAdjuster(@Param("id") id: string, @Body() dto: UpdateAdjusterDto) { - return this.policies.updateAdjuster(id, dto); + async updateAdjuster( + @Param("id") id: string, + @Body() dto: UpdateAdjusterDto, + @Req() req: Request, + ) { + const row = await this.policies.updateAdjuster(id, dto); + void this.audit.log(this.actingId(req), "lookup.adjuster.update", { + adjusterId: id, + }); + return row; } @Delete("adjusters/:id") @RequireAbility("lookup:manage") - removeAdjuster(@Param("id") id: string) { - return this.policies.removeAdjuster(id); + async removeAdjuster(@Param("id") id: string, @Req() req: Request) { + const row = await this.policies.removeAdjuster(id); + void this.audit.log(this.actingId(req), "lookup.adjuster.delete", { + adjusterId: id, + }); + return row; } } diff --git a/apps/web/src/app/banco/page.tsx b/apps/web/src/app/banco/page.tsx index ff2794f..83028fb 100644 --- a/apps/web/src/app/banco/page.tsx +++ b/apps/web/src/app/banco/page.tsx @@ -3,11 +3,14 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; import { + createBankMovement, getBankFacets, getBankStats, getBankSummary, listBankMovements, + voidBankMovement, } from "@/lib/api"; +import { useCan } from "@/lib/abilities"; import { bankDirectionLabel, bankSourceLabel, @@ -27,6 +30,7 @@ import type { BankStats, BankSummary, BankTotals, + CreateBankMovementInput, } from "@/lib/types"; /** @@ -77,6 +81,8 @@ export default function BancoPage() { } function BankBrowser() { + const canCapture = useCan("bank:create"); + const canVoid = useCan("bank:void"); const [stats, setStats] = useState(null); const [facets, setFacets] = useState(null); const [view, setView] = useState("movimientos"); @@ -93,6 +99,7 @@ function BankBrowser() { const [summaryYear, setSummaryYear] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [captureOpen, setCaptureOpen] = useState(false); const debounceRef = useRef>(); @@ -237,8 +244,28 @@ function BankBrowser() { ))} + {view === "movimientos" && canCapture && ( + + )} + {view === "movimientos" && captureOpen && ( + { + setCaptureOpen(false); + runSearch(movements?.page ?? 1); + getBankStats().then(setStats).catch(() => setStats(null)); + }} + onCancel={() => setCaptureOpen(false)} + /> + )} + {view === "movimientos" && (