diff --git a/WagonForm.tsx b/WagonForm.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index 90e1585cf..f3afb5722 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -159,6 +159,14 @@ export class BookingsController { ); } + @Get('by-company/:companyId/customer-view') + @ApiOperation({ summary: 'List bookings for a company (customer-view shape, backoffice)' }) + findByCompanyCustomerView( + @Param('companyId', ParseUUIDPipe) companyId: string, + ) { + return this.bookingsService.findCustomerBookings(companyId); + } + @Get('list-summary') @ApiOperation({ summary: 'Booking list metrics and tab counts (backoffice)' }) @ApiOkResponse({ type: BookingListSummaryDto }) diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index d33e1c169..b6f3ee220 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -1118,4 +1118,37 @@ export class BookingsService { return this.findById(id); } + + async findCustomerBookings(companyId: string): Promise<{ + id: string; + reference: string; + status: string; + tradeDirection: string; + freightType: string; + originLabel: string; + destinationLabel: string; + totalAmount: number; + currency: string; + scheduledDate: Date | null; + createdAt: Date; + }[]> { + const { items } = await this.bookingsRepository.findAllPaginated({ + page: 1, + pageSize: 500, + companyId, + }); + return items.map((b) => ({ + id: b.id, + reference: b.reference, + status: b.status, + tradeDirection: b.tradeDirection, + freightType: b.freightType, + originLabel: b.originYard?.label ?? '', + destinationLabel: b.destinationYard?.label ?? '', + totalAmount: Number(b.totalAmount), + currency: b.paymentCurrency, + scheduledDate: b.scheduledDate ?? null, + createdAt: b.createdAt, + })); + } } diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index 18512ce09..fff8fc7e5 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -38,6 +38,9 @@ import { CompanyInfoResponseDto } from "./dto/company-info-response.dto"; import { UpdateProfileDto } from "./dto/update-profile.dto"; import { ProfileResponseDto } from "./dto/profile-response.dto"; import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-response.dto"; +import { ListCompaniesQueryDto } from "./dto/list-companies-query.dto"; +import { CompanyStatsResponseDto } from "./dto/company-stats-response.dto"; +import { UpdateCompanyProfileStatusDto } from "./dto/update-company-profile-status.dto"; import { FetchETradeDto } from "./dto/fetch-etrade.dto"; import { ETradeResponseDto } from "./dto/etrade-response.dto"; @@ -265,29 +268,19 @@ export class CompaniesController { return new ResponseCompanyDto(company); } + @Get("stats") + @ApiOperation({ summary: "Company counts by status (KPI strip)" }) + async getStats(): Promise { + return this.companiesService.getCompanyStats(); + } + @Get() - @ApiOperation({ summary: "List all companies" }) - async findAll(): Promise { - const companies = await this.companiesService.findAllCompanies(); - return companies.map((c) => new ResponseCompanyDto(c)); - } - - @Get("type/:type") - @ApiOperation({ summary: "Find companies by type" }) - async findByType(@Param("type") type: string): Promise { - const companies = await this.companiesService.findAllCompanies(); - return companies - .filter((c) => c.type === type) - .map((c) => new ResponseCompanyDto(c)); - } - - @Get("search") - @ApiOperation({ summary: "Search companies by name" }) - async search(@Query("name") name: string): Promise { - const companies = await this.companiesService.findAllCompanies(); - return companies - .filter((c) => c.name.toLowerCase().includes(name.toLowerCase())) - .map((c) => new ResponseCompanyDto(c)); + @ApiOperation({ summary: "List companies (paginated, filterable)" }) + async findAll( + @Query() query: ListCompaniesQueryDto, + ): Promise<{ items: ResponseCompanyDto[]; total: number }> { + const { items, total } = await this.companiesService.listCompanies(query); + return { items: items.map((c) => new ResponseCompanyDto(c)), total }; } @Get(":id") @@ -318,6 +311,23 @@ export class CompaniesController { await this.companiesService.deleteCompany(id); } + @Get(":companyId/documents") + @ApiOperation({ summary: "List documents uploaded for a company" }) + async listDocuments( + @Param("companyId", ParseUUIDPipe) companyId: string, + ) { + const files = await this.filesService.findByResource(companyId, "companies"); + return files.map((f) => ({ + id: f.id, + name: f.name, + code: f.code, + mimeType: f.mimeType, + size: f.size, + uploadedAt: f.createdAt, + url: f.url, + })); + } + @Post(":companyId/documents") @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @@ -329,6 +339,20 @@ export class CompaniesController { return this.filesService.uploadMany(companyId, "companies", files); } + @Patch("company-profiles/:profileId/status") + @FreightAdmin() + @ApiOperation({ summary: "Update a company profile's approval status" }) + async updateCompanyProfileStatus( + @Param("profileId", ParseUUIDPipe) profileId: string, + @Body() dto: UpdateCompanyProfileStatusDto, + ): Promise { + const profile = await this.companiesService.setCompanyProfileStatus( + profileId, + dto.status, + ); + return new ResponseCompanyProfileDto(profile); + } + @Post(":companyId/profiles") @FreightAdmin() @ApiOperation({ summary: "Add a profile (employee) to a company" }) diff --git a/apps/edr-freight-api/src/modules/companies/companies.repository.ts b/apps/edr-freight-api/src/modules/companies/companies.repository.ts index 1156823f7..b31f2939d 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.repository.ts @@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { BaseRepository } from '@edr/api-common'; import { Company } from './entities/company.entity'; +import { ListCompaniesQueryDto } from './dto/list-companies-query.dto'; +import { CompanyStatsResponseDto } from './dto/company-stats-response.dto'; @Injectable() export class CompaniesRepository extends BaseRepository { @@ -32,4 +34,68 @@ export class CompaniesRepository extends BaseRepository { const count = await this.repository.count({ where: { tin } as any }); return count > 0; } + + async findPaginated( + query: ListCompaniesQueryDto, + ): Promise<{ items: Company[]; total: number }> { + const { page = 1, pageSize = 20, search, type, status } = query; + + const qb = this.repository + .createQueryBuilder('company') + .leftJoinAndSelect('company.companyProfiles', 'companyProfiles') + .where('company.deleted_at IS NULL'); + + if (type) { + qb.andWhere('company.type = :type', { type }); + } + + if (status) { + qb.andWhere('company.status = :status', { status }); + } + + if (search) { + const term = `%${search.trim()}%`; + qb.andWhere( + `(company.name ILIKE :term + OR company.tin ILIKE :term + OR company.email ILIKE :term + OR EXISTS ( + SELECT 1 FROM freight.company_profiles cp + WHERE cp.company_id = company.id + AND cp.reference ILIKE :term + AND cp.deleted_at IS NULL + ))`, + { term }, + ); + } + + const [items, total] = await qb + .orderBy('company.name', 'ASC') + .skip((page - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + + return { items, total }; + } + + async getStats(): Promise { + const rows: { status: string; count: string }[] = await this.repository + .createQueryBuilder('company') + .select('company.status', 'status') + .addSelect('COUNT(*)', 'count') + .where('company.deleted_at IS NULL') + .groupBy('company.status') + .getRawMany(); + + const map = new Map(rows.map((r) => [r.status, parseInt(r.count, 10)])); + const total = rows.reduce((sum, r) => sum + parseInt(r.count, 10), 0); + + return { + total, + active: map.get('active') ?? 0, + pending: map.get('pending') ?? 0, + suspended: map.get('suspended') ?? 0, + blacklisted: map.get('blacklisted') ?? 0, + }; + } } diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index 9c4bba5f4..50bfe8d75 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -18,6 +18,8 @@ import { CreateCompanyWithProfileDto } from "./dto/create-company-with-profile.d import { UpdateProfileDto } from "./dto/update-profile.dto"; import { ProfileResponseDto } from "./dto/profile-response.dto"; import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-response.dto"; +import { ListCompaniesQueryDto } from "./dto/list-companies-query.dto"; +import { CompanyStatsResponseDto } from "./dto/company-stats-response.dto"; import { Company, CompanyNationality, @@ -148,6 +150,14 @@ export class CompaniesService { return { company, profile }; } + async listCompanies( + query: ListCompaniesQueryDto, + ): Promise<{ items: Company[]; total: number }> { + return this.companiesRepo.findPaginated(query); + } + + async getCompanyStats(): Promise { + return this.companiesRepo.getStats(); /** * Begin onboarding: create a DRAFT company + the user's external profile + the * chosen operational role(s) up front, so every subsequent wizard step can @@ -603,6 +613,16 @@ export class CompaniesService { } } + async setCompanyProfileStatus( + profileId: string, + status: ProfileStatus, + ): Promise { + const updated = await this.companyProfilesRepo.updateStatus(profileId, status); + if (!updated) + throw new NotFoundException(`Company profile ${profileId} not found`); + return updated; + } + async createCompanyProfile( companyId: string, profileType?: ProfileType, diff --git a/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts b/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts index 76d5ba96a..bc2d95224 100644 --- a/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts @@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { BaseRepository } from "@edr/api-common"; -import { CompanyProfile, ProfileType } from "./entities/company-profile.entity"; +import { CompanyProfile, ProfileStatus, ProfileType } from "./entities/company-profile.entity"; const SEQUENCE_MAP: Record = { [ProfileType.exporter]: "seq_company_profile_ex", @@ -58,4 +58,16 @@ export class CompanyProfileRepository extends BaseRepository { async findByReference(reference: string): Promise { return this.repository.findOne({ where: { reference } }); } + + async findById(id: string): Promise { + return this.repository.findOne({ where: { id } }); + } + + async updateStatus( + id: string, + status: ProfileStatus, + ): Promise { + await this.repository.update({ id }, { status }); + return this.repository.findOne({ where: { id } }); + } } diff --git a/apps/edr-freight-api/src/modules/companies/dto/company-stats-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/company-stats-response.dto.ts new file mode 100644 index 000000000..a6b8b3b6e --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/company-stats-response.dto.ts @@ -0,0 +1,7 @@ +export class CompanyStatsResponseDto { + total!: number; + active!: number; + pending!: number; + suspended!: number; + blacklisted!: number; +} diff --git a/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts new file mode 100644 index 000000000..c92592286 --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts @@ -0,0 +1,35 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsIn, IsInt, IsOptional, IsString, Min } from "class-validator"; +import { Transform } from "class-transformer"; +import { CompanyStatus, CompanyType } from "../entities/company.entity"; + +export class ListCompaniesQueryDto { + @ApiPropertyOptional({ default: 1 }) + @IsOptional() + @Transform(({ value }: { value: unknown }) => parseInt(String(value), 10)) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ default: 20 }) + @IsOptional() + @Transform(({ value }: { value: unknown }) => parseInt(String(value), 10)) + @IsInt() + @Min(1) + pageSize?: number = 20; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ enum: CompanyType }) + @IsOptional() + @IsIn(Object.values(CompanyType)) + type?: CompanyType; + + @ApiPropertyOptional({ enum: CompanyStatus }) + @IsOptional() + @IsIn(Object.values(CompanyStatus)) + status?: CompanyStatus; +} diff --git a/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts index d6f3f1a2c..b62182968 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts @@ -12,6 +12,7 @@ import { ResponseExternalProfileDto } from './response-external-profile.dto'; export class ResponseCompanyProfileDto { id: string; + companyId: string; type: string; reference: string; status: string; @@ -25,6 +26,7 @@ export class ResponseCompanyProfileDto { constructor(profile: CompanyProfile) { this.id = profile.id; + this.companyId = profile.companyId; this.type = profile.type; this.reference = profile.reference; this.status = profile.status; diff --git a/apps/edr-freight-api/src/modules/companies/dto/update-company-profile-status.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/update-company-profile-status.dto.ts new file mode 100644 index 000000000..96c02d846 --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/update-company-profile-status.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsIn } from "class-validator"; +import { ProfileStatus } from "../entities/company-profile.entity"; + +export class UpdateCompanyProfileStatusDto { + @ApiProperty({ enum: ProfileStatus }) + @IsIn(Object.values(ProfileStatus)) + status!: ProfileStatus; +} diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index 14308883d..f1f34c3b1 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -4,6 +4,7 @@ import { Get, HttpStatus, Param, + ParseUUIDPipe, Post, Query, Res, @@ -33,6 +34,14 @@ import { export class PaymentController { constructor(private readonly paymentService: PaymentService) { } + @Get("by-company/:companyId/customer-view") + @ApiOperation({ summary: "List payments for a company (customer-view shape, backoffice)" }) + findByCompanyCustomerView( + @Param("companyId", ParseUUIDPipe) companyId: string, + ) { + return this.paymentService.findByCompanyId(companyId); + } + @Get("summary") @BookingView() @ApiOperation({ summary: "Payment count/amount summary for dashboard cards" }) diff --git a/apps/edr-freight-api/src/modules/payment/payment.repository.ts b/apps/edr-freight-api/src/modules/payment/payment.repository.ts index 8c830a20f..25c3bdd6b 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.repository.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.repository.ts @@ -61,4 +61,56 @@ export class PaymentRepository { return this.paymentRepo.createQueryBuilder(alias); } + async findByCompanyId(companyId: string): Promise<{ + id: string; + merchantOrderId: string; + bookingReference: string; + amount: number; + currency: string; + method: string; + status: string; + paidAt: Date | null; + createdAt: Date; + }[]> { + const rows: { + id: string; + merchant_order_id: string; + booking_reference: string; + amount: number; + currency: string; + method: string; + status: string; + paid_at: Date | null; + created_at: Date; + }[] = await this.dataSource.query( + `SELECT p.id, + p.merchant_order_id, + b.reference AS booking_reference, + p.amount, + p.currency, + p.method, + p.status, + p.paid_at, + p.created_at + FROM freight.payments p + JOIN freight.bookings b ON b.id = p.ref_id + WHERE b.company_id = $1 + AND p.deleted_at IS NULL + AND b.deleted_at IS NULL + ORDER BY p.created_at DESC`, + [companyId], + ); + return rows.map((r) => ({ + id: r.id, + merchantOrderId: r.merchant_order_id, + bookingReference: r.booking_reference, + amount: Number(r.amount), + currency: r.currency, + method: r.method, + status: r.status, + paidAt: r.paid_at, + createdAt: r.created_at, + })); + } + } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index f159b8a0a..1f95c6253 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -477,4 +477,8 @@ export class PaymentService { default: return "action-required"; } } + + async findByCompanyId(companyId: string) { + return this.paymentRepo.findByCompanyId(companyId); + } } diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index e41b4d726..e91a8c810 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -1,5 +1,6 @@ import { Boxes, + Building2, Container, FileText, LayoutDashboard, @@ -27,6 +28,8 @@ import BookingContractPage from "./pages/bookings/BookingContractPage"; import BookingRequestDetailPage from "./pages/bookings/BookingRequestDetailPage"; import BookingRequestsPage from "./pages/bookings/BookingRequestsPage"; import NewBookingPage from "./pages/bookings/NewBookingPage"; +import CustomerDetailPage from "./pages/customers/CustomerDetailPage"; +import CustomersPage from "./pages/customers/CustomersPage"; import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; import MyProfilePage from "./pages/dashboard/MyProfilePage"; @@ -89,6 +92,11 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ href: "/dashboard/booking-requests", icon: , }, + { + label: "Customers", + href: "/dashboard/customers", + icon: , + }, { label: "Payments", href: "/dashboard/payments", @@ -360,6 +368,8 @@ const App = () => { } /> + } /> + } /> } /> } /> No cargoes for this container.; @@ -34,8 +40,8 @@ export function CargoesTable({ containerId }: { containerId: string }) { {cargo.status} {cargo.status === 'PENDING' && refetch()} />} - {cargo.status === 'LOADED' && } - {cargo.status === 'LOADED' && } + {cargo.status === 'LOADED' && } + {cargo.status === 'LOADED' && } ))} diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx index caa554a10..546bd0def 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx @@ -6,7 +6,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; -import { useDeliverCargo } from '@/hooks/useCargoes'; +import { useMutation } from '@tanstack/react-query'; +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; /** @@ -18,7 +19,7 @@ export function DeliverCargoDialog({ cargoId, onSuccess }: { cargoId: string; on const [receiverName, setReceiverName] = useState(''); const [pickupDate, setPickupDate] = useState(''); const [deliveryRemarks, setDeliveryRemarks] = useState(''); - const deliver = useDeliverCargo(); + const deliver = useMutation(api.cargoes.deliver.mutationOptions()); const { toast } = useToast(); const handleDeliver = async () => { diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx index 188726352..8094bd902 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx @@ -3,7 +3,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { useLoadCargo } from '@/hooks/useCargoes'; +import { useMutation } from '@tanstack/react-query'; +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; export function LoadCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuccess?: () => void }) { @@ -11,7 +12,7 @@ export function LoadCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuc const [quantity, setQuantity] = useState(0); const [weight, setWeight] = useState(0); const [volume, setVolume] = useState(); - const load = useLoadCargo(); + const load = useMutation(api.cargoes.load.mutationOptions()); const { toast } = useToast(); const handleLoad = async () => { diff --git a/apps/edr-freight-web/backoffice/src/components/customers/TableCard.tsx b/apps/edr-freight-web/backoffice/src/components/customers/TableCard.tsx new file mode 100644 index 000000000..b67181da5 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/customers/TableCard.tsx @@ -0,0 +1,32 @@ +import { Box, Card } from "@mantine/core"; +import type { ReactNode } from "react"; + +export interface TableCardProps { + children: ReactNode; + /** + * Minimum width (px) the table is forced to occupy. The Mantine `Table` is + * always `width: 100%`, so without a floor it can never overflow its + * container and the horizontal scroll never engages. Setting a floor lets + * columns keep a sensible width and the card scroll horizontally on narrow + * viewports instead of squishing. + */ + minWidth?: number; +} + +/** + * Flush card shell for a `DataTable`: a borderless, padding-less card whose + * single child is a horizontally scrollable region. Pair with the table's + * `containerClassName="border-0 shadow-none bg-transparent"` so every table on + * the customer pages reads identically (same surface, same scroll behaviour). + */ +export function TableCard({ children, minWidth = 860 }: TableCardProps) { + return ( + + + {children} + + + ); +} + +export default TableCard; diff --git a/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx b/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx new file mode 100644 index 000000000..e108d1b1f --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx @@ -0,0 +1,316 @@ +import { Badge, Button, Group, Tooltip } from "@mantine/core"; +import { useMutation } from "@tanstack/react-query"; +import { api } from "@/services/api"; + +import type { + CompanyProfile, + CompanyStatus, + CompanyType, + CustomerBookingStatus, + CustomerPaymentStatus, + ProfileStatus, + ProfileType, +} from "@/types/customer"; + +import { humanize } from "./format"; + +const badgeStyle = { + fontSize: "0.7rem", + letterSpacing: "0.04em", + whiteSpace: "nowrap" as const, +}; + +/** Shared status palette — active/paid green, pending amber, terminal red. */ +const STATUS_COLOR: Record = { + active: "edr-green", + pending: "yellow", + suspended: "orange", + blacklisted: "red", +}; + +const COMPANY_TYPE_COLOR: Record = { + customer: "edr-green", + freight_forwarder: "blue", + dj_freight_forwarder: "indigo", + transporter: "grape", +}; + +const PROFILE_TYPE_COLOR: Record = { + importer: "teal", + exporter: "cyan", + freight_forwarder: "blue", + dj_freight_forwarder: "indigo", + transporter: "grape", +}; + +export function CompanyStatusBadge({ status }: { status: CompanyStatus }) { + return ( + + {status} + + ); +} + +export function CompanyTypeBadge({ type }: { type: CompanyType }) { + return ( + + {humanize(type)} + + ); +} + +/** + * Profile chips for a company row: one chip per role (Importer / Exporter / …) + * carrying its reference code. Caps at three (a company has at most three + * profiles); any extra collapse into a `+N` chip. + */ +export function ProfileChips({ + profiles, + max = 3, +}: { + profiles: CompanyProfile[]; + max?: number; +}) { + if (!profiles.length) { + return ( + + No profiles + + ); + } + + const shown = profiles.slice(0, max); + const extra = profiles.length - shown.length; + + return ( + + {shown.map((profile) => ( + + + {humanize(profile.type)} · {profile.reference} + + + ))} + {extra > 0 ? ( + + +{extra} + + ) : null} + + ); +} + +export function ProfileTypeBadge({ type }: { type: ProfileType }) { + return ( + + {humanize(type)} + + ); +} + +export function ProfileStatusBadge({ status }: { status: ProfileStatus }) { + return ( + + {status} + + ); +} + +const BOOKING_STATUS_COLOR: Record = { + DRAFT: "gray", + SUBMITTED: "yellow", + PENDING_APPROVAL: "yellow", + APPROVED: "cyan", + PAID: "edr-green", + IN_TRANSIT: "blue", + COMPLETED: "indigo", + REJECTED: "red", + CANCELLED: "red", +}; + +export function BookingStatusBadge({ status }: { status: CustomerBookingStatus }) { + return ( + + {humanize(status)} + + ); +} + +const PAYMENT_STATUS_COLOR: Record = { + "action-required": "orange", + processing: "yellow", + success: "edr-green", + failed: "red", + canceled: "gray", + refunded: "grape", +}; + +export function PaymentStatusBadge({ status }: { status: CustomerPaymentStatus }) { + return ( + + {humanize(status)} + + ); +} + +/** + * Inline approval action buttons for a profile row. + * Transitions: pending → approve/reject | active → suspend | suspended → reactivate/blacklist | blacklisted → reinstate + */ +export function ProfileApprovalActions({ + profileId, + status, +}: { + profileId: string; + status: ProfileStatus; +}) { + const { mutate, isPending } = useMutation( + api.customers.setProfileStatus.mutationOptions(), + ); + + const act = (next: ProfileStatus) => + mutate({ profileId, status: next }); + + if (status === "pending") { + return ( + + + + + ); + } + + if (status === "active") { + return ( + + ); + } + + if (status === "suspended") { + return ( + + + + + ); + } + + if (status === "blacklisted") { + return ( + + ); + } + + return null; +} diff --git a/apps/edr-freight-web/backoffice/src/components/customers/format.ts b/apps/edr-freight-web/backoffice/src/components/customers/format.ts new file mode 100644 index 000000000..0397c1cee --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/customers/format.ts @@ -0,0 +1,38 @@ +/** Shared formatting helpers for the customer-management pages. */ + +/** snake_case / SCREAMING_CASE → Title Case. */ +export function humanize(value: string): string { + return value + .toLowerCase() + .split(/[_\s]+/) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +export function formatDate(value: string | null | undefined): string { + if (!value) return "—"; + const d = new Date(value); + return Number.isNaN(d.getTime()) + ? "—" + : d.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +export function formatMoney(amount: number, currency: string): string { + return new Intl.NumberFormat(undefined, { + style: "currency", + currency, + maximumFractionDigits: 0, + }).format(amount); +} + +export function formatBytes(bytes: number): string { + if (!bytes) return "0 B"; + const units = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const value = bytes / Math.pow(1024, i); + return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; +} diff --git a/apps/edr-freight-web/backoffice/src/components/customers/index.ts b/apps/edr-freight-web/backoffice/src/components/customers/index.ts new file mode 100644 index 000000000..61b75767a --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/customers/index.ts @@ -0,0 +1,12 @@ +export { + BookingStatusBadge, + CompanyStatusBadge, + CompanyTypeBadge, + PaymentStatusBadge, + ProfileApprovalActions, + ProfileChips, + ProfileStatusBadge, + ProfileTypeBadge, +} from "./badges"; +export { formatBytes, formatDate, formatMoney, humanize } from "./format"; +export { TableCard, type TableCardProps } from "./TableCard"; diff --git a/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx index 3ac661e1a..5f4f4d7a7 100644 --- a/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx @@ -1,6 +1,9 @@ import { useState } from "react"; import { FileSignature, Loader2 } from "lucide-react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { api } from "@/services/api"; import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad"; import { Card, @@ -10,10 +13,6 @@ import { CardTitle, } from "@/components/ui/card"; import { useAuth } from "@/auth/useAuth"; -import { - useMySignature, - useSaveSignature, -} from "@/hooks/useSavedSignature"; import { Button, Dialog, @@ -33,8 +32,10 @@ import { */ export function MySignatureCard() { const { user } = useAuth(); - const { data: saved, isLoading } = useMySignature(); - const saveMutation = useSaveSignature(); + const { data: saved, isLoading } = useQuery( + api.signatures.mySignature.queryOptions({ staleTime: 60_000 }), + ); + const saveMutation = useMutation(api.signatures.save.mutationOptions()); const [open, setOpen] = useState(false); const [signerName, setSignerName] = useState(""); @@ -56,7 +57,13 @@ export function MySignatureCard() { signerDisplayName: signerName.trim(), signatureImageBase64: signatureData, }, - { onSuccess: () => setOpen(false) }, + { + onSuccess: () => { + toast.success("Signature saved"); + setOpen(false); + }, + onError: () => toast.error("Failed to save signature"), + }, ); }; diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/AllocateBookingWizard.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/AllocateBookingWizard.tsx index 0b2e0a885..0c26c3392 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/AllocateBookingWizard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/AllocateBookingWizard.tsx @@ -31,13 +31,8 @@ import { Weight, } from "lucide-react"; -import { - useAvailableLocomotives, - useEligibleBookings, - useScheduleList, - useScheduleMutations, -} from "@/hooks/trainScheduling/useTrainScheduling"; -import { useRoutes } from "@/hooks/useRoutes"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import { trainSchedulingService } from "@/services/trainScheduling.service"; import type { BookingDetail } from "@/types/booking"; @@ -133,13 +128,27 @@ export function AllocateBookingWizard({ [originId, destinationId], ); - const eligibleQuery = useEligibleBookings(eligibleFilters, opened); - const schedulesQuery = useScheduleList(); - const routesQuery = useRoutes(); - const locomotivesQuery = useAvailableLocomotives( - scheduleMode === "new" && routeId ? routeId : undefined, + const eligibleQuery = useQuery( + api.trainScheduling.eligibleBookings.queryOptions({ + input: { filters: eligibleFilters }, + enabled: opened, + }), ); - const { create, preview, assign, finalize } = useScheduleMutations(selectedScheduleId ?? undefined); + const schedulesQuery = useQuery( + api.trainScheduling.scheduleList.queryOptions({ input: {} }), + ); + const routesQuery = useQuery(api.routes.list.queryOptions()); + const locomotivesQuery = useQuery( + api.trainScheduling.availableLocomotives.queryOptions({ + input: { + routeId: scheduleMode === "new" && routeId ? routeId : undefined, + }, + }), + ); + const create = useMutation(api.trainScheduling.createSchedule.mutationOptions()); + const preview = useMutation(api.trainScheduling.preview.mutationOptions()); + const assign = useMutation(api.trainScheduling.assignBookings.mutationOptions()); + const finalize = useMutation(api.trainScheduling.finalizeSchedule.mutationOptions()); useEffect(() => { if (scheduleMode === "new") { diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx index 515f1e361..d44ef4540 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx @@ -13,11 +13,10 @@ import { } from "@mantine/core"; import { CheckCircle2, Layers, Lock, LockOpen, PlayCircle, Repeat, XCircle } from "lucide-react"; +import { useMutation, useQuery } from "@tanstack/react-query"; + import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; -import { - useBatchActions, - useBookableSchedules, -} from "@/hooks/trainScheduling/useTrainScheduling"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import type { TrainScheduleDetail } from "@/types/trainScheduling"; @@ -33,16 +32,31 @@ const windowColor: Record = { export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) { const { toast } = useToast(); - const actions = useBatchActions(schedule.id); + const actions = { + runBatch: useMutation(api.trainScheduling.runBatch.mutationOptions()), + setWindow: useMutation(api.trainScheduling.setBookingWindow.mutationOptions()), + markPaid: useMutation(api.trainScheduling.markBookingPaid.mutationOptions()), + expire: useMutation(api.trainScheduling.expireBooking.mutationOptions()), + moveSchedule: useMutation( + api.trainScheduling.moveBookingSchedule.mutationOptions(), + ), + }; const windowStatus = (schedule as { bookingWindowStatus?: string }).bookingWindowStatus ?? "OPEN"; const locked = schedule.status === "DISPATCHED" || schedule.status === "ARRIVED"; const [moveBookingId, setMoveBookingId] = useState(null); const [moveTarget, setMoveTarget] = useState(null); - const { data: targets } = useBookableSchedules( - schedule.originStation?.id, - schedule.destinationStation?.id, + const { data: targets } = useQuery( + api.trainScheduling.bookableSchedules.queryOptions({ + input: { + originYardId: schedule.originStation?.id, + destinationYardId: schedule.destinationStation?.id, + }, + enabled: Boolean( + schedule.originStation?.id && schedule.destinationStation?.id, + ), + }), ); const moveOptions = useMemo( () => diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/AssignedBookingsPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/AssignedBookingsPanel.tsx index c1ce3a96d..914d5cb60 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/AssignedBookingsPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/AssignedBookingsPanel.tsx @@ -4,7 +4,8 @@ import { Building2, Package, TrainFront, Weight, X } from "lucide-react"; import type { TrainScheduleDetail } from "@/types/trainScheduling"; import type { BookingDetailData } from "./BookingDetailModal"; import { RemoveBookingConfirmModal, type RemovalTarget } from "./RemoveBookingConfirmModal"; -import { useScheduleMutations } from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import { freightBrand } from "@/theme/freight-brand"; @@ -22,7 +23,7 @@ export const AssignedBookingsPanel = ({ onSelect, }: AssignedBookingsPanelProps) => { const { toast } = useToast(); - const unassign = useScheduleMutations(scheduleId).unassign; + const unassign = useMutation(api.trainScheduling.unassignBooking.mutationOptions()); const isDispatched = scheduleDetail.status === "DISPATCHED"; const [removalTarget, setRemovalTarget] = useState(null); diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/CompositionBookingTabs.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/CompositionBookingTabs.tsx index 5de67daa6..64cb6d5cb 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/CompositionBookingTabs.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/CompositionBookingTabs.tsx @@ -8,10 +8,8 @@ import { UnassignedBookingsPanel } from "./UnassignedBookingsPanel"; import { RemovalLogPanel } from "./RemovalLogPanel"; import { BatchBookingList } from "./BatchBookingList"; import { BookingDetailModal, type BookingDetailData } from "./BookingDetailModal"; -import { - useCompositionRemovals, - useUnassignedBookings, -} from "@/hooks/trainScheduling/useTrainScheduling"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { freightBrand } from "@/theme/freight-brand"; interface CompositionBookingTabsProps { @@ -47,8 +45,18 @@ export const CompositionBookingTabs = ({ const [detailBooking, setDetailBooking] = useState(null); const [tab, setTab] = useState("assigned"); - const unassignedQuery = useUnassignedBookings(scheduleId); - const removalsQuery = useCompositionRemovals(scheduleId); + const unassignedQuery = useQuery( + api.trainScheduling.unassignedBookings.queryOptions({ + input: { scheduleId }, + enabled: Boolean(scheduleId), + }), + ); + const removalsQuery = useQuery( + api.trainScheduling.compositionRemovals.queryOptions({ + input: { scheduleId }, + enabled: Boolean(scheduleId), + }), + ); const { assignedCount } = useMemo(() => { const wagons = scheduleDetail.trainSet?.wagons ?? []; diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/ContainerNumberInput.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/ContainerNumberInput.tsx index 5629ba698..bb25b51bb 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/ContainerNumberInput.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/ContainerNumberInput.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { Group, TextInput, Text } from "@mantine/core"; -import { useUpdateContainerItem } from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation } from "@tanstack/react-query"; +import { api } from "@/services/api"; interface ContainerNumberInputProps { value: string | null; @@ -19,13 +20,16 @@ export const ContainerNumberInput = ({ const [inputValue, setInputValue] = useState(value ?? ""); const [error, setError] = useState(null); - const updateMutation = useUpdateContainerItem(scheduleId); + const updateMutation = useMutation( + api.trainScheduling.updateContainerItem.mutationOptions(), + ); const isLoading = updateMutation.isPending; const handleSave = async () => { try { setError(null); await updateMutation.mutateAsync({ + scheduleId, itemId, containerNumber: inputValue || null, }); diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemovalLogPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemovalLogPanel.tsx index 05f2a8328..6682cd795 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemovalLogPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemovalLogPanel.tsx @@ -1,13 +1,19 @@ import { Box, Card, Group, Stack, Text, ThemeIcon } from "@mantine/core"; import { History, PackageX } from "lucide-react"; -import { useCompositionRemovals } from "@/hooks/trainScheduling/useTrainScheduling"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; interface RemovalLogPanelProps { scheduleId: string; } export const RemovalLogPanel = ({ scheduleId }: RemovalLogPanelProps) => { - const removalQuery = useCompositionRemovals(scheduleId); + const removalQuery = useQuery( + api.trainScheduling.compositionRemovals.queryOptions({ + input: { scheduleId }, + enabled: Boolean(scheduleId), + }), + ); if (removalQuery.isLoading) { return ( diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx index 43e8c58d2..2ebfbb8ff 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx @@ -6,7 +6,8 @@ import { TrainStatsBar } from "./TrainStatsBar"; import { WagonCard } from "./WagonCard"; import { InteractiveTrainConsist } from "./InteractiveTrainConsist"; import { RemoveBookingModal } from "./RemoveBookingModal"; -import { useScheduleMutations, useRemoveWagonSlot } from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { freightBrand } from "@/theme/freight-brand"; type Wagon = TrainScheduleDetail["trainSet"]["wagons"][number]; @@ -47,8 +48,12 @@ export const TrainConsistView = ({ const [selectedWagonId, setSelectedWagonId] = useState(null); const [removeModalOpen, setRemoveModalOpen] = useState(false); - const unassignMutation = useScheduleMutations(scheduleId).unassign; - const removeWagonMutation = useRemoveWagonSlot(scheduleId); + const unassignMutation = useMutation( + api.trainScheduling.unassignBooking.mutationOptions(), + ); + const removeWagonMutation = useMutation( + api.trainScheduling.removeWagonSlot.mutationOptions(), + ); const trainSet = scheduleDetail.trainSet; const wagons = trainSet?.wagons ?? []; @@ -83,7 +88,7 @@ export const TrainConsistView = ({ const handleRemoveWagon = async (wagonId: string) => { if (confirm("Are you sure you want to remove this wagon slot?")) { - await removeWagonMutation.mutateAsync(wagonId); + await removeWagonMutation.mutateAsync({ scheduleId, wagonId }); setSelectedWagonId(null); } }; diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/UnassignedBookingsPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/UnassignedBookingsPanel.tsx index 48928be88..e5225f8dd 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/UnassignedBookingsPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/UnassignedBookingsPanel.tsx @@ -1,9 +1,7 @@ import { Badge, Box, Button, Card, Group, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core"; import { AlertTriangle, Container as ContainerIcon, MapPin, Plus, TrainFront } from "lucide-react"; -import { - useUnassignedBookings, - useScheduleMutations, -} from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import type { FleetAvailabilityRow } from "@/types/trainScheduling"; import type { BookingDetailData } from "./BookingDetailModal"; @@ -73,8 +71,15 @@ export const UnassignedBookingsPanel = ({ onSelect, }: UnassignedBookingsPanelProps) => { const { toast } = useToast(); - const unassignedQuery = useUnassignedBookings(scheduleId); - const assignMutation = useScheduleMutations(scheduleId).assignUnassigned; + const unassignedQuery = useQuery( + api.trainScheduling.unassignedBookings.queryOptions({ + input: { scheduleId }, + enabled: Boolean(scheduleId), + }), + ); + const assignMutation = useMutation( + api.trainScheduling.assignUnassignedBooking.mutationOptions(), + ); const handleAssign = async (bookingId: string, reference: string | null) => { try { diff --git a/apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx b/apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx index 07565163b..0c569e753 100644 --- a/apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/wagons/AssignWagonDialog.tsx @@ -4,17 +4,18 @@ import { Button, Group, Modal, NumberInput, Select, Stack, Text } from "@mantine import { Freight } from "@edr/types"; +import { useMutation, useQuery } from "@tanstack/react-query"; + +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; -import { useRouteYards } from "@/hooks/useRoutes"; -import { useAssignWagonToTrain, useWagons } from "@/hooks/useWagons"; export function AssignWagonDialog({ trainId }: { trainId: string }) { const [open, setOpen] = useState(false); const [wagonId, setWagonId] = useState(null); const [sequence, setSequence] = useState(""); - const { data: wagons } = useWagons(); - const { data: yards = [] } = useRouteYards(); - const assign = useAssignWagonToTrain(); + const { data: wagons } = useQuery(api.wagons.list.queryOptions({ input: {} })); + const { data: yards = [] } = useQuery(api.routes.yards.queryOptions()); + const assign = useMutation(api.wagons.assignToTrain.mutationOptions()); const { toast } = useToast(); const available = (wagons ?? []).filter( diff --git a/apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx b/apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx index d9d96c117..33a7438fe 100644 --- a/apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/wagons/WagonsTable.tsx @@ -3,15 +3,22 @@ import { Trash2 } from "lucide-react"; import type { ColumnDef } from "@edr/ui-common"; import { ActionIcon, Badge, Group, Text, Tooltip } from "@mantine/core"; +import { useMutation, useQuery } from "@tanstack/react-query"; + import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; -import { useUnassignWagon, useWagonsByTrain } from "@/hooks/useWagons"; import type { Wagon } from "@/services/wagon.service"; import { DataTable } from "@edr/ui-common"; export function WagonsTable({ trainId }: { trainId: string }) { - const { data: wagons = [], isLoading, refetch } = useWagonsByTrain(trainId); - const unassign = useUnassignWagon(); + const { data: wagons = [], isLoading, refetch } = useQuery( + api.wagons.listByTrain.queryOptions({ + input: { trainId }, + enabled: !!trainId, + }), + ); + const unassign = useMutation(api.wagons.unassign.mutationOptions()); const { toast } = useToast(); const columns = useMemo((): ColumnDef[] => { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ActivityTimeline.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ActivityTimeline.tsx index 49e8828dd..5fac23ea7 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ActivityTimeline.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ActivityTimeline.tsx @@ -9,7 +9,9 @@ import { Warehouse, } from 'lucide-react'; -import { useInventoryActivity } from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import type { ActivityType } from '@/types/warehouse'; import { formatDate, humanizeEnum } from './options'; @@ -24,7 +26,12 @@ const activityIcon: Record = { }; export function ActivityTimeline({ inventoryId }: { inventoryId: string }) { - const { data, isLoading } = useInventoryActivity(inventoryId); + const { data, isLoading } = useQuery( + api.warehouses.activity.queryOptions({ + input: { id: inventoryId }, + enabled: Boolean(inventoryId), + }), + ); const items = data ?? []; if (isLoading) { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/CreateWarehouseModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/CreateWarehouseModal.tsx index 87d5f2946..ad8b4109d 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/CreateWarehouseModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/CreateWarehouseModal.tsx @@ -9,9 +9,10 @@ import { TextInput, } from '@mantine/core'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useStations } from '@/hooks/useStations'; -import { useCreateWarehouse, useUpdateWarehouse } from '@/hooks/useWarehouses'; import type { SaveWarehousePayload, Warehouse, WarehouseType } from '@/types/warehouse'; import { extractErrorMessage, statusOptions, warehouseTypeOptions } from './options'; @@ -48,9 +49,11 @@ const emptyForm = (): FormState => ({ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWarehouseModalProps) { const isEdit = Boolean(warehouse); const { toast } = useToast(); - const createMutation = useCreateWarehouse(); - const updateMutation = useUpdateWarehouse(); - const { data: stations } = useStations(); + const createMutation = useMutation(api.warehouses.create.mutationOptions()); + const updateMutation = useMutation(api.warehouses.update.mutationOptions()); + const { data: stations } = useQuery( + api.stations.list.queryOptions({ staleTime: 5 * 60 * 1000 }), + ); const [form, setForm] = useState(emptyForm()); const stationOptions = (stations ?? []).map((s) => ({ value: s.id, label: `${s.name} (${s.code})` })); diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/CreateYardModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/CreateYardModal.tsx index 9ef9ef26c..a2eb0aa0e 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/CreateYardModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/CreateYardModal.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from 'react'; import { Button, Group, Modal, NumberInput, Select, Stack, TextInput } from '@mantine/core'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useCreateYard, useUpdateYard } from '@/hooks/useWarehouses'; import type { SaveYardPayload, WarehouseYard, WarehouseYardType } from '@/types/warehouse'; import { extractErrorMessage, statusOptions, yardTypeOptions } from './options'; @@ -36,8 +38,8 @@ const emptyForm = (): FormState => ({ export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYardModalProps) { const isEdit = Boolean(yard); const { toast } = useToast(); - const createMutation = useCreateYard(); - const updateMutation = useUpdateYard(); + const createMutation = useMutation(api.warehouses.createYard.mutationOptions()); + const updateMutation = useMutation(api.warehouses.updateYard.mutationOptions()); const [form, setForm] = useState(emptyForm()); useEffect(() => { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/CreateZoneModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/CreateZoneModal.tsx index 7a67e4ff1..05bc1ce96 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/CreateZoneModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/CreateZoneModal.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from 'react'; import { Button, Group, Modal, NumberInput, Select, Stack, TextInput } from '@mantine/core'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useCreateZone, useUpdateZone } from '@/hooks/useWarehouses'; import type { SaveZonePayload, WarehouseZone, WarehouseZoneType } from '@/types/warehouse'; import { extractErrorMessage, statusOptions, zoneTypeOptions } from './options'; @@ -36,8 +38,8 @@ const emptyForm = (): FormState => ({ export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneModalProps) { const isEdit = Boolean(zone); const { toast } = useToast(); - const createMutation = useCreateZone(); - const updateMutation = useUpdateZone(); + const createMutation = useMutation(api.warehouses.createZone.mutationOptions()); + const updateMutation = useMutation(api.warehouses.updateZone.mutationOptions()); const [form, setForm] = useState(emptyForm()); useEffect(() => { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/DeliverInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/DeliverInventoryModal.tsx index 4b5485815..d8a441de8 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/DeliverInventoryModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/DeliverInventoryModal.tsx @@ -2,8 +2,10 @@ import { useEffect, useState } from 'react'; import { Alert, Button, Group, Modal, Stack, Text, Textarea, TextInput } from '@mantine/core'; import { Info } from 'lucide-react'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useDeliverInventory } from '@/hooks/useWarehouses'; import type { WarehouseInventoryItem } from '@/types/warehouse'; import { extractErrorMessage } from './options'; @@ -15,7 +17,7 @@ interface DeliverInventoryModalProps { export function DeliverInventoryModal({ opened, onClose, item }: DeliverInventoryModalProps) { const { toast } = useToast(); - const deliverMutation = useDeliverInventory(); + const deliverMutation = useMutation(api.warehouses.deliver.mutationOptions()); const [receiverName, setReceiverName] = useState(''); const [remarks, setRemarks] = useState(''); diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx index ce4b81c05..73ea669be 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/FeePreviewModal.tsx @@ -1,13 +1,10 @@ import { Badge, Button, Card, Divider, Group, Loader, Modal, Stack, Text } from '@mantine/core'; import { CalendarClock, Coins, DoorOpen, FileText } from 'lucide-react'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { - useFeePreview, - useGateClearance, - useGenerateInvoice, - useInvoicesForInventory, -} from '@/hooks/useWarehouses'; import { extractErrorMessage } from './options'; import type { FeePreview, WarehouseInvoiceStatus } from '@/types/warehouse'; @@ -88,10 +85,20 @@ function Row({ label, value }: { label: string; value: string }) { export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) { const { toast } = useToast(); const enabledId = opened ? inventoryId ?? undefined : undefined; - const { data, isLoading } = useFeePreview(enabledId); - const { data: invoices } = useInvoicesForInventory(enabledId); - const generate = useGenerateInvoice(); - const gateClear = useGateClearance(); + const { data, isLoading } = useQuery( + api.warehouses.feePreview.queryOptions({ + input: { inventoryId: enabledId ?? '' }, + enabled: Boolean(enabledId), + }), + ); + const { data: invoices } = useQuery( + api.warehouses.invoicesForInventory.queryOptions({ + input: { inventoryId: enabledId ?? '' }, + enabled: Boolean(enabledId), + }), + ); + const generate = useMutation(api.warehouses.generateInvoice.mutationOptions()); + const gateClear = useMutation(api.warehouses.gateClearance.mutationOptions()); const activeInvoice = (invoices ?? []).find((i) => i.status !== 'CANCELLED'); diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/InspectionReportModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/InspectionReportModal.tsx index 15583345d..d56547455 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/InspectionReportModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/InspectionReportModal.tsx @@ -12,8 +12,10 @@ import { } from '@mantine/core'; import { Upload } from 'lucide-react'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useCreateInspectionReport, useUploadInspectionAttachments } from '@/hooks/useWarehouses'; import { INSPECTION_REPORT_TYPES, INSPECTION_STATUSES, @@ -45,8 +47,12 @@ const STATUS_LABELS: Record = { /** Batch 4.5 — record an inspection / damage report with optional image upload. */ export function InspectionReportModal({ opened, onClose, inventoryId }: InspectionReportModalProps) { const { toast } = useToast(); - const createReport = useCreateInspectionReport(); - const uploadAttachments = useUploadInspectionAttachments(); + const createReport = useMutation( + api.warehouses.createInspectionReport.mutationOptions(), + ); + const uploadAttachments = useMutation( + api.warehouses.uploadInspectionAttachments.mutationOptions(), + ); const [reportType, setReportType] = useState('INSPECTION'); const [inspectionStatus, setInspectionStatus] = useState('PASSED'); diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryMovementHistoryTable.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryMovementHistoryTable.tsx index 352c6bff2..304e4ca75 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryMovementHistoryTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryMovementHistoryTable.tsx @@ -1,12 +1,19 @@ import { Center, Loader, Table, Text } from '@mantine/core'; -import { useInventoryMovements } from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { formatDate } from './options'; const shortId = (id?: string | null) => (id ? `${id.slice(0, 8)}…` : '—'); export function InventoryMovementHistoryTable({ inventoryId }: { inventoryId: string }) { - const { data, isLoading } = useInventoryMovements(inventoryId); + const { data, isLoading } = useQuery( + api.warehouses.movements.queryOptions({ + input: { id: inventoryId }, + enabled: Boolean(inventoryId), + }), + ); const movements = data ?? []; if (isLoading) { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryWorkbench.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryWorkbench.tsx index 27ad0ebc6..b076510ab 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryWorkbench.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/InventoryWorkbench.tsx @@ -2,14 +2,10 @@ import { useState } from 'react'; import { Button, Center, Group, Loader, Stack, Text } from '@mantine/core'; import { ClipboardCheck } from 'lucide-react'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { - useBulkMarkInspected, - useDispatchInventory, - useMarkReadyForLoading, - useMarkReadyForPickup, - useStoreInventory, -} from '@/hooks/useWarehouses'; import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse'; import { DeliverInventoryModal } from './DeliverInventoryModal'; import { FeePreviewModal } from './FeePreviewModal'; @@ -42,11 +38,17 @@ export function InventoryWorkbench({ items, isLoading, onLastMile }: InventoryWo const [releaseItem, setReleaseItem] = useState(null); const [deliverItem, setDeliverItem] = useState(null); - const storeMutation = useStoreInventory(); - const readyMutation = useMarkReadyForLoading(); - const pickupMutation = useMarkReadyForPickup(); - const dispatchMutation = useDispatchInventory(); - const inspectMutation = useBulkMarkInspected(); + const storeMutation = useMutation(api.warehouses.store.mutationOptions()); + const readyMutation = useMutation( + api.warehouses.markReadyForLoading.mutationOptions(), + ); + const pickupMutation = useMutation( + api.warehouses.markReadyForPickup.mutationOptions(), + ); + const dispatchMutation = useMutation(api.warehouses.dispatch.mutationOptions()); + const inspectMutation = useMutation( + api.warehouses.bulkMarkInspected.mutationOptions(), + ); const [selected, setSelected] = useState>(new Set()); const allSelected = items.length > 0 && selected.size === items.length; @@ -66,10 +68,7 @@ export function InventoryWorkbench({ items, isLoading, onLastMile }: InventoryWo return; } try { - const res = (await inspectMutation.mutateAsync({ inventoryIds: [...selected] })) as { - data: { inspectedCount: number; skippedCount: number }; - }; - const r = res.data; + const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] }); toast({ title: `${r.inspectedCount} marked inspected`, description: r.skippedCount ? `${r.skippedCount} skipped` : undefined, diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/LoadInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/LoadInventoryModal.tsx index 8def582cb..7fc43c0d5 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/LoadInventoryModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/LoadInventoryModal.tsx @@ -2,8 +2,10 @@ import { useEffect, useState } from 'react'; import { Alert, Button, Group, Modal, NumberInput, Stack, Text, Textarea } from '@mantine/core'; import { Info } from 'lucide-react'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useLoadInventory } from '@/hooks/useWarehouses'; import type { WarehouseInventoryItem } from '@/types/warehouse'; import { WagonSelect } from './WagonSelect'; import { extractErrorMessage } from './options'; @@ -17,7 +19,7 @@ interface LoadInventoryModalProps { /** Load READY_FOR_LOADING inventory onto a wagon (creates a loading record). */ export function LoadInventoryModal({ opened, onClose, item }: LoadInventoryModalProps) { const { toast } = useToast(); - const loadMutation = useLoadInventory(); + const loadMutation = useMutation(api.warehouses.load.mutationOptions()); const [wagonId, setWagonId] = useState(''); const [loadedWeight, setLoadedWeight] = useState(''); const [notes, setNotes] = useState(''); diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/MoveInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/MoveInventoryModal.tsx index b76458d5e..1ec596ffe 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/MoveInventoryModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/MoveInventoryModal.tsx @@ -1,8 +1,10 @@ import { useEffect, useMemo, useState } from 'react'; import { Button, Group, Modal, Select, Stack, Textarea } from '@mantine/core'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useMoveInventory, useWarehouseYards, useWarehouseZones, useWarehouses } from '@/hooks/useWarehouses'; import type { WarehouseInventoryItem } from '@/types/warehouse'; import { extractErrorMessage } from './options'; @@ -14,7 +16,7 @@ interface MoveInventoryModalProps { export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModalProps) { const { toast } = useToast(); - const moveMutation = useMoveInventory(); + const moveMutation = useMutation(api.warehouses.move.mutationOptions()); const [warehouseId, setWarehouseId] = useState(''); const [yardId, setYardId] = useState(''); const [zoneId, setZoneId] = useState(''); @@ -29,9 +31,21 @@ export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModal } }, [opened]); - const warehousesQuery = useWarehouses({ status: 'ACTIVE' }); - const yardsQuery = useWarehouseYards(warehouseId || undefined); - const zonesQuery = useWarehouseZones(yardId || undefined); + const warehousesQuery = useQuery( + api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } } }), + ); + const yardsQuery = useQuery( + api.warehouses.listYards.queryOptions({ + input: { warehouseId }, + enabled: Boolean(warehouseId), + }), + ); + const zonesQuery = useQuery( + api.warehouses.listZones.queryOptions({ + input: { yardId }, + enabled: Boolean(yardId), + }), + ); const warehouseOptions = useMemo( () => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })), diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx index eab14ad63..3cd08461f 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx @@ -18,34 +18,14 @@ import { } from '@mantine/core'; import { ChevronDown, ChevronRight, ClipboardCheck, Info, PackageSearch, Train, Truck } from 'lucide-react'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { - useAutoUnloadArrivedBookings, - useBulkDispatchExport, - useBulkMarkInspected, - useBulkReceive, - useEligibleBookings, - useImportArriveQueue, - useImportTrainItems, - useImportUnloadedQueue, - useLoadPassedExport, - useLoadedExport, - useReadyToLoadExport, - useReceiveInventory, - useWarehouseInventory, - useWarehouseYards, - useWarehouseZones, - useWarehouses, -} from '@/hooks/useWarehouses'; import type { - AutoUnloadArrivedResult, - BulkDispatchResult, - BulkInspectResult, - BulkReceiveResult, ImportTrain, ImportTrainItem, ImportUnloadedItem, - LoadPassedExportResult, ReadyToLoadRow, ReceiveInventoryPayload, } from '@/types/warehouse'; @@ -77,9 +57,21 @@ function LocationSelects({ value: Location; onChange: (next: Location) => void; }) { - const warehousesQuery = useWarehouses({ status: 'ACTIVE' }); - const yardsQuery = useWarehouseYards(value.warehouseId || undefined); - const zonesQuery = useWarehouseZones(value.yardId || undefined); + const warehousesQuery = useQuery( + api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } } }), + ); + const yardsQuery = useQuery( + api.warehouses.listYards.queryOptions({ + input: { warehouseId: value.warehouseId ?? '' }, + enabled: Boolean(value.warehouseId), + }), + ); + const zonesQuery = useQuery( + api.warehouses.listZones.queryOptions({ + input: { yardId: value.yardId ?? '' }, + enabled: Boolean(value.yardId), + }), + ); const warehouseOptions = useMemo( () => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })), @@ -148,10 +140,12 @@ function EligibleTab({ onChanged?: () => void; }) { const { toast } = useToast(); - const { data: allRows = [], isLoading } = useEligibleBookings(enabled); + const { data: allRows = [], isLoading } = useQuery( + api.warehouses.eligibleBookings.queryOptions({ enabled }), + ); const rows = useMemo(() => allRows.filter((r) => r.direction === direction), [allRows, direction]); - const bulkReceive = useBulkReceive(); - const loadPassed = useLoadPassedExport(); + const bulkReceive = useMutation(api.warehouses.bulkReceive.mutationOptions()); + const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions()); const [selected, setSelected] = useState>(new Set()); const locationReady = Boolean(location.warehouseId && location.yardId && location.zoneId); @@ -177,10 +171,7 @@ function EligibleTab({ return; } try { - const res = (await bulkReceive.mutateAsync({ direction, ...location, bookingIds })) as { - data: BulkReceiveResult; - }; - const r = res.data; + const r = await bulkReceive.mutateAsync({ direction, ...location, bookingIds }); toast({ title: `${r.receivedCount} received at ${direction === 'EXPORT' ? 'facility' : 'warehouse'}`, description: r.skippedCount ? `${r.skippedCount} skipped` : undefined, @@ -194,8 +185,7 @@ function EligibleTab({ const loadPassedExport = async () => { try { - const res = (await loadPassed.mutateAsync(undefined)) as { data: LoadPassedExportResult }; - const r = res.data; + const r = await loadPassed.mutateAsync(undefined); toast({ title: `${r.loadedCount} loaded`, description: r.skippedCount ? `${r.skippedCount} skipped — inspection not passed` : undefined, @@ -353,8 +343,10 @@ function EligibleTab({ /** Export items that passed inspection and are queued to be loaded onto a train. */ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) { const { toast } = useToast(); - const { data: rows = [], isLoading } = useReadyToLoadExport(enabled); - const loadPassed = useLoadPassedExport(); + const { data: rows = [], isLoading } = useQuery( + api.warehouses.readyToLoadExport.queryOptions({ enabled }), + ); + const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions()); const [selected, setSelected] = useState>(new Set()); const allSelected = rows.length > 0 && selected.size === rows.length; @@ -369,8 +361,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: const autoLoad = async () => { try { - const res = (await loadPassed.mutateAsync(undefined)) as { data: LoadPassedExportResult }; - const r = res.data; + const r = await loadPassed.mutateAsync(undefined); toast({ title: `${r.loadedCount} items loaded`, description: r.skippedCount ? `${r.skippedCount} skipped` : undefined, @@ -494,8 +485,12 @@ function LoadedExportTab({ onChanged?: () => void; }) { const { toast } = useToast(); - const { data: rows = [], isLoading } = useLoadedExport(enabled); - const bulkDispatch = useBulkDispatchExport(); + const { data: rows = [], isLoading } = useQuery( + api.warehouses.loadedExport.queryOptions({ enabled }), + ); + const bulkDispatch = useMutation( + api.warehouses.bulkDispatchExport.mutationOptions(), + ); const [selected, setSelected] = useState>(new Set()); const allSelected = rows.length > 0 && selected.size === rows.length; @@ -514,8 +509,7 @@ function LoadedExportTab({ return; } try { - const res = (await bulkDispatch.mutateAsync(inventoryIds)) as { data: BulkDispatchResult }; - const r = res.data; + const r = await bulkDispatch.mutateAsync(inventoryIds); toast({ title: `${r.dispatchedCount} dispatched`, description: r.skippedCount ? `${r.skippedCount} skipped` : undefined, @@ -659,7 +653,12 @@ function LoadedExportTab({ /** Assigned bookings/items for an arrived import train (read-only detail view). */ function ImportTrainDetailTable({ scheduleId }: { scheduleId: string }) { - const { data: items = [], isLoading } = useImportTrainItems(scheduleId); + const { data: items = [], isLoading } = useQuery( + api.warehouses.importTrainItems.queryOptions({ + input: { scheduleId }, + enabled: Boolean(scheduleId), + }), + ); if (isLoading) { return ( @@ -735,18 +734,19 @@ function ImportArriveQueueTab({ onChanged?: () => void; }) { const { toast } = useToast(); - const { data: trains = [], isLoading } = useImportArriveQueue(enabled); - const autoUnloadMutation = useAutoUnloadArrivedBookings(); + const { data: trains = [], isLoading } = useQuery( + api.warehouses.importArriveQueue.queryOptions({ enabled }), + ); + const autoUnloadMutation = useMutation( + api.warehouses.autoUnloadArrivedBookings.mutationOptions(), + ); const [openId, setOpenId] = useState(null); const [busyId, setBusyId] = useState(null); const autoUnload = async (train: ImportTrain) => { setBusyId(train.scheduleId); try { - const res = (await autoUnloadMutation.mutateAsync(train.scheduleId)) as { - data: AutoUnloadArrivedResult; - }; - const r = res.data; + const r = await autoUnloadMutation.mutateAsync(train.scheduleId); const extra = [ r.skippedCount ? `${r.skippedCount} skipped` : '', r.failedCount ? `${r.failedCount} failed` : '', @@ -866,8 +866,12 @@ function ImportArriveQueueTab({ */ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) { const { toast } = useToast(); - const { data: rows = [], isLoading } = useImportUnloadedQueue(enabled); - const inspectMutation = useBulkMarkInspected(); + const { data: rows = [], isLoading } = useQuery( + api.warehouses.importUnloadedQueue.queryOptions({ enabled }), + ); + const inspectMutation = useMutation( + api.warehouses.bulkMarkInspected.mutationOptions(), + ); const [selected, setSelected] = useState>(new Set()); const [inspectId, setInspectId] = useState(null); @@ -888,10 +892,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) { return; } try { - const res = (await inspectMutation.mutateAsync({ inventoryIds: [...selected] })) as { - data: BulkInspectResult; - }; - const r = res.data; + const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] }); toast({ title: `${r.inspectedCount} marked inspected`, description: r.skippedCount ? `${r.skippedCount} skipped` : undefined, @@ -1033,8 +1034,10 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) { */ function ImportDispatchQueueTab({ enabled }: { enabled: boolean }) { const { toast } = useToast(); - const { data: items = [], isLoading } = useWarehouseInventory( - enabled ? { status: 'READY_FOR_PICKUP' } : undefined, + const { data: items = [], isLoading } = useQuery( + api.warehouses.listInventory.queryOptions({ + input: { filter: enabled ? { status: 'READY_FOR_PICKUP' } : undefined }, + }), ); return ( @@ -1155,7 +1158,9 @@ function SingleBookingReceiveModal({ onReceived, }: ReceiveInventoryModalProps) { const { toast } = useToast(); - const receiveMutation = useReceiveInventory(); + const receiveMutation = useMutation( + api.warehouses.receiveInventory.mutationOptions(), + ); const [selectedBooking, setSelectedBooking] = useState(bookingId ?? ''); const [form, setForm] = useState({ warehouseId: '', diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx index 851066713..f2edd620b 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx @@ -2,8 +2,10 @@ import { useEffect, useState } from 'react'; import { Alert, Button, Group, Modal, Stack, Text, TextInput } from '@mantine/core'; import { Info } from 'lucide-react'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useReleaseInventory } from '@/hooks/useWarehouses'; import type { WarehouseInventoryItem } from '@/types/warehouse'; import { extractErrorMessage } from './options'; @@ -15,7 +17,7 @@ interface ReleaseOrderModalProps { export function ReleaseOrderModal({ opened, onClose, item }: ReleaseOrderModalProps) { const { toast } = useToast(); - const releaseMutation = useReleaseInventory(); + const releaseMutation = useMutation(api.warehouses.release.mutationOptions()); const [reference, setReference] = useState(''); useEffect(() => { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReserveInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReserveInventoryModal.tsx index 7a7ebb55b..d436bb6f2 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReserveInventoryModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReserveInventoryModal.tsx @@ -2,8 +2,10 @@ import { useEffect, useState } from 'react'; import { Alert, Button, Group, Modal, Stack, Text } from '@mantine/core'; import { Info } from 'lucide-react'; +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { useReserveInventory } from '@/hooks/useWarehouses'; import type { WarehouseInventoryItem } from '@/types/warehouse'; import { BookingSelect } from './BookingSelect'; import { extractErrorMessage } from './options'; @@ -16,7 +18,7 @@ interface ReserveInventoryModalProps { export function ReserveInventoryModal({ opened, onClose, item }: ReserveInventoryModalProps) { const { toast } = useToast(); - const reserveMutation = useReserveInventory(); + const reserveMutation = useMutation(api.warehouses.reserve.mutationOptions()); const [bookingId, setBookingId] = useState(''); useEffect(() => { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WagonSelect.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WagonSelect.tsx index d6cc8c2cd..298df191b 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/WagonSelect.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WagonSelect.tsx @@ -1,6 +1,7 @@ import { Select } from '@mantine/core'; +import { useQuery } from '@tanstack/react-query'; -import { useLoadableWagons } from '@/hooks/useWarehouses'; +import { api } from '@/services/api'; interface WagonSelectProps { value: string; @@ -11,7 +12,9 @@ interface WagonSelectProps { /** Searchable wagon picker. Lists wagons that are loadable (read-only from scheduling). */ export function WagonSelect({ value, onChange, label = 'Wagon', required }: WagonSelectProps) { - const { data, isLoading } = useLoadableWagons(); + const { data, isLoading } = useQuery( + api.warehouses.loadableWagons.queryOptions(), + ); const options = (data ?? []).map((w) => ({ value: w.id, diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseCardView.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseCardView.tsx index 2d3dd2276..bd76b5e10 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseCardView.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseCardView.tsx @@ -2,7 +2,9 @@ import { useMemo } from 'react'; import { ActionIcon, Card, Group, SimpleGrid, Stack, Text } from '@mantine/core'; import { Building2, Eye, MapPin, Pencil } from 'lucide-react'; -import { useStations } from '@/hooks/useStations'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import type { Warehouse } from '@/types/warehouse'; import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges'; import { formatCapacity } from './options'; @@ -14,7 +16,9 @@ interface WarehouseCardViewProps { } export function WarehouseCardView({ warehouses, onView, onEdit }: WarehouseCardViewProps) { - const { data: stations } = useStations(); + const { data: stations } = useQuery( + api.stations.list.queryOptions({ staleTime: 5 * 60 * 1000 }), + ); const stationNameById = useMemo( () => new Map((stations ?? []).map((s) => [s.id, s.name])), [stations], diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseDashboardCharts.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseDashboardCharts.tsx index b2a628d88..993544fe3 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseDashboardCharts.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseDashboardCharts.tsx @@ -15,7 +15,9 @@ import { YAxis, } from 'recharts'; -import { useWarehouseInventory } from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import type { WarehouseDashboard, WarehouseInventoryItem } from '@/types/warehouse'; interface WarehouseDashboardChartsProps { @@ -38,7 +40,9 @@ type Granularity = 'week' | 'month' | 'year'; export function WarehouseDashboardCharts({ data }: WarehouseDashboardChartsProps) { const [granularity, setGranularity] = useState('month'); - const { data: inventory } = useWarehouseInventory(); + const { data: inventory } = useQuery( + api.warehouses.listInventory.queryOptions({ input: {} }), + ); const statusData = STATUS_SERIES.map((s) => ({ name: s.label, diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInfoCard.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInfoCard.tsx index 83dcc4d63..e28513821 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInfoCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInfoCard.tsx @@ -2,7 +2,9 @@ import { useState } from 'react'; import { Badge, Button, Card, Divider, Group, Stack, Text } from '@mantine/core'; import { PackagePlus, Train as TrainIcon, Warehouse as WarehouseIcon } from 'lucide-react'; -import { useBookingSchedule, useWarehouseInventory } from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { InventoryStatusBadge } from './badges'; import { FreightVisual } from './FreightVisual'; import { formatDate } from './options'; @@ -28,8 +30,15 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) { export function WarehouseInfoCard({ bookingId, bookingReference }: WarehouseInfoCardProps) { const [modalOpen, setModalOpen] = useState(false); - const { data, isLoading } = useWarehouseInventory({ bookingId }); - const { data: scheduleView } = useBookingSchedule(bookingId); + const { data, isLoading } = useQuery( + api.warehouses.listInventory.queryOptions({ input: { filter: { bookingId } } }), + ); + const { data: scheduleView } = useQuery( + api.warehouses.bookingSchedule.queryOptions({ + input: { bookingId }, + enabled: Boolean(bookingId), + }), + ); const items = data ?? []; const latest = items[0]; diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseTable.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseTable.tsx index 80b85f693..7f8dea0e5 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseTable.tsx @@ -3,7 +3,9 @@ import { ActionIcon, Group, Text } from '@mantine/core'; import { Eye, Pencil } from 'lucide-react'; import { DataTable, type ColumnDef } from '@edr/ui-common'; -import { useStations } from '@/hooks/useStations'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import type { Warehouse } from '@/types/warehouse'; import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges'; import { formatCapacity } from './options'; @@ -15,7 +17,9 @@ interface WarehouseTableProps { } export function WarehouseTable({ warehouses, onView, onEdit }: WarehouseTableProps) { - const { data: stations } = useStations(); + const { data: stations } = useQuery( + api.stations.list.queryOptions({ staleTime: 5 * 60 * 1000 }), + ); const stationNameById = useMemo( () => new Map((stations ?? []).map((s) => [s.id, s.name])), [stations], diff --git a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts index e3e1921de..aced8cda4 100644 --- a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts @@ -1,8 +1,9 @@ +import type { FleetResourceSlug } from "@/pages/fleet/config/resources"; import type { BookingListFilter } from "@/services/bookings.service"; import type { RuleEngineListParams } from "@/services/ruleEngine/ruleEngine.service"; -import type { TrainScheduleFilters } from "@/types/trainScheduling"; -import type { FleetResourceSlug } from "@/pages/fleet/config/resources"; +import type { CompanyListFilter } from "@/types/customer"; import type { RuleEngineResourceSlug } from "@/types/rule-engine"; +import type { TrainScheduleFilters } from "@/types/trainScheduling"; export const QUERY_KEYS = { USERS: { @@ -26,8 +27,13 @@ export const QUERY_KEYS = { CUSTOMERS: { ROOT: ["customers"] as const, - list: () => ["customers", "list"] as const, + stats: ["customers", "stats"] as const, + list: (filter?: CompanyListFilter) => + ["customers", "list", filter ?? {}] as const, byId: (id: string) => ["customers", "detail", id] as const, + bookings: (id: string) => ["customers", "detail", id, "bookings"] as const, + documents: (id: string) => ["customers", "detail", id, "documents"] as const, + payments: (id: string) => ["customers", "detail", id, "payments"] as const, }, BOOKINGS: { diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 832f923a0..77c4ea10e 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -71,7 +71,12 @@ export const URL_CONSTANTS = { COMPANIES: { BASE: "/companies", + STATS: "/companies/stats", BY_ID: (id: string | number) => `/companies/${id}`, + DOCUMENTS: (id: string) => `/companies/${id}/documents`, + PROFILE_STATUS: (profileId: string) => `/companies/company-profiles/${profileId}/status`, + BOOKINGS_CUSTOMER_VIEW: (id: string) => `/bookings/by-company/${id}/customer-view`, + PAYMENTS_CUSTOMER_VIEW: (id: string) => `/payments/by-company/${id}/customer-view`, }, CUSTOMERS_API: { diff --git a/apps/edr-freight-web/backoffice/src/hooks/fleet/useFleet.ts b/apps/edr-freight-web/backoffice/src/hooks/fleet/useFleet.ts deleted file mode 100644 index 29e7bf62b..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/fleet/useFleet.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - -import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; -import type { FleetListFilters, FleetResourceSlug } from "@/services/fleet/fleet.service"; -import { fleetService } from "@/services/fleet/fleet.service"; - -export function useFleetList(slug: FleetResourceSlug, filters?: FleetListFilters) { - return useQuery({ - queryKey: [...QUERY_KEYS.FLEET.list(slug), filters ?? {}], - queryFn: () => fleetService.list(slug, filters), - }); -} - -export function useFleetMutations(slug: FleetResourceSlug) { - const qc = useQueryClient(); - const invalidate = () => qc.invalidateQueries({ queryKey: QUERY_KEYS.FLEET.list(slug) }); - - const create = useMutation({ - mutationFn: (data: Record) => fleetService.create(slug, data), - onSuccess: invalidate, - }); - - const update = useMutation({ - mutationFn: ({ id, data }: { id: string; data: Record }) => - fleetService.update(slug, id, data), - onSuccess: invalidate, - }); - - const remove = useMutation({ - mutationFn: (id: string) => fleetService.remove(slug, id), - onSuccess: invalidate, - }); - - return { create, update, remove }; -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/trainScheduling/useTrainScheduling.ts b/apps/edr-freight-web/backoffice/src/hooks/trainScheduling/useTrainScheduling.ts deleted file mode 100644 index 823d13324..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/trainScheduling/useTrainScheduling.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - -import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; -import { trainSchedulingService } from "@/services/trainScheduling.service"; -import type { - AssignBookingsPayload, - CreateTrainSchedulePayload, - FreightType, - PinWagonsPayload, - RecordCheckpointPayload, - TrainScheduleFilters, - TrainSchedulePreviewPayload, -} from "@/types/trainScheduling"; - -export const useScheduleList = (freightType?: FreightType) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.schedules(), - queryFn: () => trainSchedulingService.listSchedules(freightType), - }); - -export const useBatchBoard = () => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoard(), - queryFn: () => trainSchedulingService.getBatchBoard(), - refetchInterval: 30_000, - }); - -export const useBatchBoardDetail = (scheduleId: string | undefined) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(scheduleId ?? ""), - queryFn: () => trainSchedulingService.getBatchBoardDetail(scheduleId!), - enabled: Boolean(scheduleId), - refetchInterval: 30_000, - }); - -export const useRunAllocation = (scheduleId: string) => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: () => trainSchedulingService.runAllocation(scheduleId), - onSuccess: () => { - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(scheduleId), - }); - void qc.invalidateQueries({ queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoard() }); - }, - }); -}; - -export const useScheduleDetail = (id: string | undefined, freightType?: FreightType) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.scheduleById(id ?? ""), - queryFn: () => trainSchedulingService.getScheduleById(id!, freightType), - enabled: Boolean(id), - }); - -export const useEligibleBookings = ( - filters?: TrainScheduleFilters, - enabled = true, - freightType?: FreightType, -) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.eligible(freightType, filters), - queryFn: () => trainSchedulingService.getEligibleBookings(filters, freightType), - enabled, - }); - -export const useAvailableLocomotives = (routeId?: string) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.locomotives(routeId), - queryFn: () => trainSchedulingService.getAvailableLocomotives(routeId), - enabled: routeId ? Boolean(routeId) : true, - }); - -export const useBatchActions = (scheduleId?: string) => { - const qc = useQueryClient(); - const invalidate = () => { - void qc.invalidateQueries({ queryKey: QUERY_KEYS.TRAIN_SCHEDULING.ROOT }); - void qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.ROOT }); - void qc.invalidateQueries({ queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoard() }); - if (scheduleId) { - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.scheduleById(scheduleId), - }); - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(scheduleId), - }); - } - }; - - const runBatch = useMutation({ - mutationFn: (id: string) => trainSchedulingService.runBatch(id), - onSuccess: invalidate, - }); - const setWindow = useMutation({ - mutationFn: ({ id, status }: { id: string; status: "OPEN" | "CLOSED" }) => - trainSchedulingService.setBookingWindow(id, status), - onSuccess: invalidate, - }); - const markPaid = useMutation({ - mutationFn: (bookingId: string) => trainSchedulingService.markBookingPaid(bookingId), - onSuccess: invalidate, - }); - const expire = useMutation({ - mutationFn: (bookingId: string) => trainSchedulingService.expireBooking(bookingId), - onSuccess: invalidate, - }); - const moveSchedule = useMutation({ - mutationFn: ({ bookingId, trainScheduleId }: { bookingId: string; trainScheduleId: string }) => - trainSchedulingService.moveBookingSchedule(bookingId, trainScheduleId), - onSuccess: invalidate, - }); - - return { runBatch, setWindow, markPaid, expire, moveSchedule, invalidate }; -}; - -export const useBookableSchedules = ( - originYardId?: string | null, - destinationYardId?: string | null, -) => - useQuery({ - queryKey: [ - ...QUERY_KEYS.TRAIN_SCHEDULING.ROOT, - "bookable", - originYardId ?? "", - destinationYardId ?? "", - ], - queryFn: () => - trainSchedulingService.getBookableSchedules( - originYardId ?? undefined, - destinationYardId ?? undefined, - ), - enabled: Boolean(originYardId && destinationYardId), - }); - -/** - * Day-level pool: which days have an OPEN departure on the route. Staff pick a - * day (not a train) when creating a booking; the engine assigns the train. - */ -export const useAvailableDays = ( - originYardId?: string | null, - destinationYardId?: string | null, -) => - useQuery({ - queryKey: [ - ...QUERY_KEYS.TRAIN_SCHEDULING.ROOT, - "available-days", - originYardId ?? "", - destinationYardId ?? "", - ], - queryFn: () => - trainSchedulingService.getAvailableDays( - originYardId ?? undefined, - destinationYardId ?? undefined, - ), - enabled: Boolean(originYardId && destinationYardId), - }); - -export const useTrainTrack = (id: string | undefined) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.track(id ?? ""), - queryFn: () => trainSchedulingService.getTrack(id!), - enabled: Boolean(id), - }); - -export const useScheduleMutations = (scheduleId?: string) => { - const qc = useQueryClient(); - - const invalidate = () => { - void qc.invalidateQueries({ queryKey: QUERY_KEYS.TRAIN_SCHEDULING.ROOT }); - void qc.invalidateQueries({ queryKey: QUERY_KEYS.TRAIN_SCHEDULING.schedules() }); - void qc.invalidateQueries({ queryKey: QUERY_KEYS.TRAIN_SCHEDULING.locomotives() }); - if (scheduleId) { - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.scheduleById(scheduleId), - }); - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.track(scheduleId), - }); - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.unassignedBookings(scheduleId), - }); - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.compositionRemovals(scheduleId), - }); - } - void qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.ROOT }); - }; - - const create = useMutation({ - mutationFn: ({ - freightType, - payload, - }: { - freightType?: FreightType; - payload: CreateTrainSchedulePayload; - }) => trainSchedulingService.createSchedule(payload, freightType), - onSuccess: invalidate, - }); - - const preview = useMutation({ - mutationFn: ({ - freightType, - payload, - }: { - freightType?: FreightType; - payload: TrainSchedulePreviewPayload; - }) => trainSchedulingService.preview(payload, freightType), - }); - - const assign = useMutation({ - mutationFn: ({ - id, - freightType, - payload, - }: { - id: string; - freightType?: FreightType; - payload: AssignBookingsPayload; - }) => trainSchedulingService.assignBookings(id, payload, freightType), - onSuccess: invalidate, - }); - - const assignUnassigned = useMutation({ - mutationFn: ({ id, bookingId }: { id: string; bookingId: string }) => - trainSchedulingService.assignUnassignedBooking(id, bookingId), - onSuccess: invalidate, - }); - - const unassign = useMutation({ - mutationFn: ({ id, bookingId }: { id: string; bookingId: string }) => - trainSchedulingService.unassignBooking(id, bookingId), - onSuccess: invalidate, - }); - - const pin = useMutation({ - mutationFn: ({ id, payload }: { id: string; payload: PinWagonsPayload }) => - trainSchedulingService.pinWagons(id, payload), - onSuccess: invalidate, - }); - - const finalize = useMutation({ - mutationFn: (id: string) => trainSchedulingService.finalizeSchedule(id), - onSuccess: invalidate, - }); - - const dispatch = useMutation({ - mutationFn: (id: string) => trainSchedulingService.dispatchSchedule(id), - onSuccess: invalidate, - }); - - const cancel = useMutation({ - mutationFn: ({ id, freightType }: { id: string; freightType?: FreightType }) => - trainSchedulingService.cancelSchedule(id, freightType ?? "CONTAINER"), - onSuccess: invalidate, - }); - - const recordCheckpoint = useMutation({ - mutationFn: ({ id, payload }: { id: string; payload: RecordCheckpointPayload }) => - trainSchedulingService.recordCheckpoint(id, payload), - onSuccess: invalidate, - }); - - const arrive = useMutation({ - mutationFn: (id: string) => trainSchedulingService.arriveSchedule(id), - onSuccess: invalidate, - }); - - return { - create, - preview, - assign, - assignUnassigned, - unassign, - pin, - finalize, - dispatch, - cancel, - recordCheckpoint, - arrive, - invalidate, - }; -}; - -export const useUnassignedBookings = (scheduleId: string | undefined) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.unassignedBookings(scheduleId ?? ""), - queryFn: () => trainSchedulingService.getUnassignedBookings(scheduleId!), - enabled: Boolean(scheduleId), - }); - -export const useCompositionRemovals = (scheduleId: string | undefined) => - useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.compositionRemovals(scheduleId ?? ""), - queryFn: () => trainSchedulingService.getCompositionRemovals(scheduleId!), - enabled: Boolean(scheduleId), - }); - -export const useRemoveWagonSlot = (scheduleId: string) => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (wagonId: string) => - trainSchedulingService.removeWagonSlot(scheduleId, wagonId), - onSuccess: () => { - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.scheduleById(scheduleId), - }); - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(scheduleId), - }); - }, - }); -}; - -export const useUpdateContainerItem = (scheduleId: string) => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ itemId, containerNumber }: { itemId: string; containerNumber: string | null }) => - trainSchedulingService.updateContainerItem(scheduleId, itemId, { containerNumber }), - onSuccess: () => { - void qc.invalidateQueries({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.scheduleById(scheduleId), - }); - }, - }); -}; diff --git a/apps/edr-freight-web/backoffice/src/hooks/use-cargo-types.ts b/apps/edr-freight-web/backoffice/src/hooks/use-cargo-types.ts deleted file mode 100644 index 864e9732c..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/use-cargo-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { cargoTypesService } from '@/services/cargo-types.service'; - -export const CARGO_TYPES_QUERY_KEY = ['cargo-types']; - -export function useCargoTypes() { - return useQuery({ - queryKey: CARGO_TYPES_QUERY_KEY, - queryFn: () => cargoTypesService.getCargoTypes(), - staleTime: Infinity, - }); -} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/hooks/use-container-types.ts b/apps/edr-freight-web/backoffice/src/hooks/use-container-types.ts deleted file mode 100644 index c216c2adf..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/use-container-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { containerTypesService } from '@/services/container-types.service'; - -export const CONTAINER_TYPES_QUERY_KEY = ['container-types']; - -export function useContainerTypes() { - return useQuery({ - queryKey: CONTAINER_TYPES_QUERY_KEY, - queryFn: () => containerTypesService.getContainerTypes(), - staleTime: Infinity, - }); -} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/hooks/use-wagon-types.ts b/apps/edr-freight-web/backoffice/src/hooks/use-wagon-types.ts deleted file mode 100644 index 88566c776..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/use-wagon-types.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { wagonTypesService } from '@/services/wagon-types.service'; - -export const WAGON_TYPES_QUERY_KEY = ['wagon-types']; - -export function useWagonTypes() { - return useQuery({ - queryKey: WAGON_TYPES_QUERY_KEY, - queryFn: () => wagonTypesService.getWagonTypes(), - }); -} - -export function useCreateWagonType() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: wagonTypesService.create, - onSuccess: () => qc.invalidateQueries({ queryKey: WAGON_TYPES_QUERY_KEY }), - }); -} - -export function useUpdateWagonType() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, data }: { id: string; data: Record }) => - wagonTypesService.update(id, data), - onSuccess: () => qc.invalidateQueries({ queryKey: WAGON_TYPES_QUERY_KEY }), - }); -} - -export function useDeleteWagonType() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: wagonTypesService.delete, - onSuccess: () => qc.invalidateQueries({ queryKey: WAGON_TYPES_QUERY_KEY }), - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts b/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts deleted file mode 100644 index 3c1cdf517..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { cargoService, type DeliverCargoPayload } from '@/services/cargoService'; - -export const cargoKeys = { - all: ['cargoes'] as const, - byContainer: (containerId: string) => [...cargoKeys.all, 'container', containerId] as const, - details: () => [...cargoKeys.all, 'detail'] as const, - detail: (id: string) => [...cargoKeys.details(), id] as const, -}; - -export function useCargoes() { - return useQuery({ queryKey: cargoKeys.all, queryFn: () => cargoService.getAll().then(res => res.data) }); -} - -export const useGetCargoes = useCargoes; - -export function useCargoesByContainer(containerId: string) { - return useQuery({ queryKey: cargoKeys.byContainer(containerId), queryFn: () => cargoService.getByContainer(containerId).then(res => res.data), enabled: !!containerId }); -} - -export function useCargo(id: string) { - return useQuery({ queryKey: cargoKeys.detail(id), queryFn: () => cargoService.getById(id).then(res => res.data), enabled: !!id }); -} - -export const useGetCargo = useCargo; - -export function useCreateCargo() { - const qc = useQueryClient(); - return useMutation({ mutationFn: cargoService.create, onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) }); -} - -export function useUpdateCargo() { - const qc = useQueryClient(); - return useMutation({ mutationFn: ({ id, data }: any) => cargoService.update(id, data), onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: cargoKeys.all }); - qc.invalidateQueries({ queryKey: cargoKeys.detail(id) }); - } }); -} - -export function useDeleteCargo() { - const qc = useQueryClient(); - return useMutation({ mutationFn: cargoService.delete, onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) }); -} - -export function useLoadCargo() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, quantity, weight, volume }: any) => cargoService.load(id, quantity, weight, volume), - onSuccess: (_, { id }) => qc.invalidateQueries({ queryKey: cargoKeys.all }) - }); -} - -export function useDeliverCargo() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, payload }: { id: string; payload?: DeliverCargoPayload }) => - cargoService.deliver(id, payload), - onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) - }); -} - -export function useUnloadCargo() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => cargoService.unload(id), - onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useContainers.ts b/apps/edr-freight-web/backoffice/src/hooks/useContainers.ts deleted file mode 100644 index b14d9b5c5..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useContainers.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { containerService } from '@/services/containerService'; - -export const containerKeys = { - all: ['containers'] as const, - byWagon: (wagonId: string) => [...containerKeys.all, 'wagon', wagonId] as const, - details: () => [...containerKeys.all, 'detail'] as const, - detail: (id: string) => [...containerKeys.details(), id] as const, -}; - -export function useContainers() { - return useQuery({ queryKey: containerKeys.all, queryFn: () => containerService.getAll().then(res => res.data) }); -} - -export const useGetContainers = useContainers; - -export function useContainersByWagon(wagonId: string) { - return useQuery({ queryKey: containerKeys.byWagon(wagonId), queryFn: () => containerService.getByWagon(wagonId).then(res => res.data), enabled: !!wagonId }); -} - -export function useContainer(id: string) { - return useQuery({ queryKey: containerKeys.detail(id), queryFn: () => containerService.getById(id).then(res => res.data), enabled: !!id }); -} - -export const useGetContainer = useContainer; - -export function useCreateContainer() { - const qc = useQueryClient(); - return useMutation({ mutationFn: containerService.create, onSuccess: () => qc.invalidateQueries({ queryKey: containerKeys.all }) }); -} - -export function useUpdateContainer() { - const qc = useQueryClient(); - return useMutation({ mutationFn: ({ id, data }: any) => containerService.update(id, data), onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: containerKeys.all }); - qc.invalidateQueries({ queryKey: containerKeys.detail(id) }); - } }); -} - -export function useDeleteContainer() { - const qc = useQueryClient(); - return useMutation({ mutationFn: containerService.delete, onSuccess: () => qc.invalidateQueries({ queryKey: containerKeys.all }) }); -} - -export function useAssignContainerToWagon() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ containerId, wagonId, position }: any) => containerService.assignToWagon(containerId, wagonId, position), - onSuccess: (_, { wagonId }) => qc.invalidateQueries({ queryKey: containerKeys.byWagon(wagonId) }) - }); -} - -export function useUnassignContainer() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: containerService.unassign, - onSuccess: () => qc.invalidateQueries({ queryKey: containerKeys.all }) - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useDropdownSettings.ts b/apps/edr-freight-web/backoffice/src/hooks/useDropdownSettings.ts deleted file mode 100644 index 256fc1702..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useDropdownSettings.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -import { api } from "@/services/api"; -import type { - CreateDropdownOptionDto, - CreateDropdownSettingDto, - UpdateDropdownOptionDto, - UpdateDropdownSettingDto, -} from "@/types/dropdownSettings"; - -/* ----------------------------- Mutations ----------------------------- */ - -export const useCreateDropdownSetting = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (dto: CreateDropdownSettingDto) => - api.dropdownSettings.create.call(dto), - onSuccess: () => - qc.invalidateQueries({ queryKey: api.dropdownSettings.list.queryKey() }), - }); -}; - -export const useUpdateDropdownSetting = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - id, - dto, - }: { - id: string; - dto: UpdateDropdownSettingDto; - }) => api.dropdownSettings.update.call({ id, dto }), - onSuccess: (_data, { id }) => { - qc.invalidateQueries({ queryKey: api.dropdownSettings.list.queryKey() }); - qc.invalidateQueries({ - queryKey: api.dropdownSettings.getById.queryKey({ id }), - }); - }, - }); -}; - -export const useDeleteDropdownSetting = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => api.dropdownSettings.remove.call({ id }), - onSuccess: () => - qc.invalidateQueries({ queryKey: api.dropdownSettings.list.queryKey() }), - }); -}; - -export const useReplaceDropdownOptions = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - settingId, - options, - }: { - settingId: string; - options: CreateDropdownOptionDto[]; - }) => api.dropdownSettings.replaceOptions.call({ id: settingId, options }), - onSuccess: (_data, { settingId }) => { - qc.invalidateQueries({ queryKey: api.dropdownSettings.list.queryKey() }); - qc.invalidateQueries({ - queryKey: api.dropdownSettings.getById.queryKey({ id: settingId }), - }); - }, - }); -}; - -export const useAddDropdownOption = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - settingId, - dto, - }: { - settingId: string; - dto: CreateDropdownOptionDto; - }) => api.dropdownSettings.addOption.call({ id: settingId, dto }), - onSuccess: (_data, { settingId }) => { - qc.invalidateQueries({ queryKey: api.dropdownSettings.list.queryKey() }); - qc.invalidateQueries({ - queryKey: api.dropdownSettings.getById.queryKey({ id: settingId }), - }); - }, - }); -}; - -export const useUpdateDropdownOption = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - optionId, - dto, - }: { - optionId: string; - dto: UpdateDropdownOptionDto; - }) => api.dropdownSettings.updateOption.call({ optionId, dto }), - onSuccess: () => - qc.invalidateQueries({ queryKey: api.dropdownSettings.list.queryKey() }), - }); -}; - -export const useRemoveDropdownOption = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (optionId: string) => - api.dropdownSettings.removeOption.call({ optionId }), - onSuccess: () => - qc.invalidateQueries({ queryKey: api.dropdownSettings.list.queryKey() }), - }); -}; diff --git a/apps/edr-freight-web/backoffice/src/hooks/useFacilities.ts b/apps/edr-freight-web/backoffice/src/hooks/useFacilities.ts deleted file mode 100644 index 481378ba7..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useFacilities.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { facilityService } from '@/services/facility.service'; - -export const facilityKeys = { - all: ['facilities'] as const, - list: () => ['facilities', 'list'] as const, - detail: (id: string) => ['facilities', 'detail', id] as const, -}; - -export function useFacilities() { - return useQuery({ - queryKey: facilityKeys.list(), - queryFn: () => facilityService.list().then((r) => r.data), - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useFileUploadSettings.ts b/apps/edr-freight-web/backoffice/src/hooks/useFileUploadSettings.ts deleted file mode 100644 index 748de4f88..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useFileUploadSettings.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -import { api } from "@/services/api"; -import type { - CreateFileUploadFieldDto, - CreateFileUploadSettingDto, - UpdateFileUploadFieldDto, - UpdateFileUploadSettingDto, -} from "@/types/fileUploadSettings"; - -/* ----------------------------- Mutations ----------------------------- */ - -export const useCreateFileUploadSetting = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (dto: CreateFileUploadSettingDto) => - api.fileUploadSettings.create.call(dto), - onSuccess: () => - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.list.queryKey(), - }), - }); -}; - -export const useUpdateFileUploadSetting = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - id, - dto, - }: { - id: string; - dto: UpdateFileUploadSettingDto; - }) => api.fileUploadSettings.update.call({ id, dto }), - onSuccess: (_data, { id }) => { - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.list.queryKey(), - }); - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.getById.queryKey({ id }), - }); - }, - }); -}; - -export const useDeleteFileUploadSetting = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => api.fileUploadSettings.remove.call({ id }), - onSuccess: () => - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.list.queryKey(), - }), - }); -}; - -export const useReplaceFileUploadFields = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - settingId, - fields, - }: { - settingId: string; - fields: CreateFileUploadFieldDto[]; - }) => api.fileUploadSettings.replaceFields.call({ id: settingId, fields }), - onSuccess: (_data, { settingId }) => { - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.list.queryKey(), - }); - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.getById.queryKey({ id: settingId }), - }); - }, - }); -}; - -export const useAddFileUploadField = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - settingId, - dto, - }: { - settingId: string; - dto: CreateFileUploadFieldDto; - }) => api.fileUploadSettings.addField.call({ settingId, dto }), - onSuccess: (_data, { settingId }) => { - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.list.queryKey(), - }); - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.getById.queryKey({ id: settingId }), - }); - }, - }); -}; - -export const useUpdateFileUploadField = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ - fieldId, - dto, - }: { - fieldId: string; - dto: UpdateFileUploadFieldDto; - }) => api.fileUploadSettings.updateField.call({ fieldId, dto }), - onSuccess: () => - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.list.queryKey(), - }), - }); -}; - -export const useRemoveFileUploadField = () => { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (fieldId: string) => - api.fileUploadSettings.removeField.call({ fieldId }), - onSuccess: () => - qc.invalidateQueries({ - queryKey: api.fileUploadSettings.list.queryKey(), - }), - }); -}; diff --git a/apps/edr-freight-web/backoffice/src/hooks/useLocomotives.ts b/apps/edr-freight-web/backoffice/src/hooks/useLocomotives.ts deleted file mode 100644 index ee50bd59e..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useLocomotives.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; - -import { locomotivesService } from '@/services/locomotives.service'; - -export const locomotiveKeys = { - all: ['locomotives'] as const, - details: () => [...locomotiveKeys.all, 'detail'] as const, - detail: (id: string) => [...locomotiveKeys.details(), id] as const, -}; - -export function useLocomotives() { - return useQuery({ - queryKey: locomotiveKeys.all, - queryFn: () => locomotivesService.getAll().then((response) => response.data), - }); -} - -export function useCreateLocomotive() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: locomotivesService.create, - onSuccess: () => qc.invalidateQueries({ queryKey: locomotiveKeys.all }), - }); -} - -export function useUpdateLocomotive() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, data }: { id: string; data: Record }) => - locomotivesService.update(id, data), - onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: locomotiveKeys.all }); - qc.invalidateQueries({ queryKey: locomotiveKeys.detail(id) }); - }, - }); -} - -export function useDecommissionLocomotive() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: locomotivesService.decommission, - onSuccess: (_, id) => { - qc.invalidateQueries({ queryKey: locomotiveKeys.all }); - qc.invalidateQueries({ queryKey: locomotiveKeys.detail(id) }); - }, - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/usePayments.ts b/apps/edr-freight-web/backoffice/src/hooks/usePayments.ts deleted file mode 100644 index e9a09760b..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/usePayments.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { - paymentsService, - type PaymentListFilter, -} from "@/services/payments.service"; - -export function usePaymentList(filter?: PaymentListFilter, enabled = true) { - return useQuery({ - queryKey: ["payments", "list", filter ?? {}], - queryFn: () => paymentsService.list(filter), - enabled, - }); -} - -export function usePaymentSummary(enabled = true) { - return useQuery({ - queryKey: ["payments", "summary"], - queryFn: () => paymentsService.getSummary(), - staleTime: 30_000, - enabled, - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useRoutes.ts b/apps/edr-freight-web/backoffice/src/hooks/useRoutes.ts deleted file mode 100644 index 3ae4ba924..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useRoutes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; - -import { routesService } from '@/services/routes.service'; - -export const routeKeys = { - all: ['routes'] as const, - yards: ['routes', 'yards'] as const, - details: () => [...routeKeys.all, 'detail'] as const, - detail: (id: string) => [...routeKeys.details(), id] as const, -}; - -export function useRoutes() { - return useQuery({ - queryKey: routeKeys.all, - queryFn: () => routesService.getAll().then((response) => response.data), - }); -} - -export function useRouteYards() { - return useQuery({ - queryKey: routeKeys.yards, - queryFn: () => routesService.getYards().then((response) => response.data.data), - }); -} - -export function useCreateRoute() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: routesService.create, - onSuccess: () => qc.invalidateQueries({ queryKey: routeKeys.all }), - }); -} - -export function useUpdateRoute() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, data }: { id: string; data: Record }) => - routesService.update(id, data), - onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: routeKeys.all }); - qc.invalidateQueries({ queryKey: routeKeys.detail(id) }); - }, - }); -} - -export function useDeactivateRoute() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: routesService.deactivate, - onSuccess: (_, id) => { - qc.invalidateQueries({ queryKey: routeKeys.all }); - qc.invalidateQueries({ queryKey: routeKeys.detail(id) }); - }, - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useSavedSignature.ts b/apps/edr-freight-web/backoffice/src/hooks/useSavedSignature.ts deleted file mode 100644 index b8c9480a7..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useSavedSignature.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import toast from "react-hot-toast"; - -import { - signaturesService, - type SaveSignaturePayload, -} from "@/services/signatures.service"; - -const SAVED_SIGNATURE_KEY = ["me", "signature"] as const; - -export function useMySignature() { - return useQuery({ - queryKey: SAVED_SIGNATURE_KEY, - queryFn: () => signaturesService.getMySignature(), - staleTime: 60_000, - }); -} - -export function useSaveSignature() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (payload: SaveSignaturePayload) => - signaturesService.saveMySignature(payload), - onSuccess: () => { - toast.success("Signature saved"); - void qc.invalidateQueries({ queryKey: SAVED_SIGNATURE_KEY }); - }, - onError: () => toast.error("Failed to save signature"), - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useStations.ts b/apps/edr-freight-web/backoffice/src/hooks/useStations.ts deleted file mode 100644 index b7ab95d0a..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useStations.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { trainSchedulingService } from '@/services/trainScheduling.service'; -import { QUERY_KEYS } from '@/constants/QUERY_KEYS'; - -/** - * The 21 network stations / yards, sourced from the existing booking - * reference-data API. Reused as the parent "Facility / Port" for warehouses. - */ -export function useStations() { - return useQuery({ - queryKey: QUERY_KEYS.TRAIN_SCHEDULING.stations(), - queryFn: () => trainSchedulingService.getStations(), - staleTime: 5 * 60 * 1000, - }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useTrains.ts b/apps/edr-freight-web/backoffice/src/hooks/useTrains.ts deleted file mode 100644 index c2b24d294..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useTrains.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { trainService } from '@/services/trains.service'; - -export const trainKeys = { - all: ['trains'] as const, - lists: () => [...trainKeys.all, 'list'] as const, - details: () => [...trainKeys.all, 'detail'] as const, - detail: (id: string) => [...trainKeys.details(), id] as const, -}; - -export function useTrains() { - return useQuery({ queryKey: trainKeys.lists(), queryFn: () => trainService.getAll().then(res => res.data) }); -} - -export const useGetTrains = useTrains; - -export function useTrain(id: string) { - return useQuery({ queryKey: trainKeys.detail(id), queryFn: () => trainService.getById(id).then(res => res.data), enabled: !!id }); -} - -export const useGetTrain = useTrain; - -export function useCreateTrain() { - const qc = useQueryClient(); - return useMutation({ mutationFn: trainService.create, onSuccess: () => qc.invalidateQueries({ queryKey: trainKeys.lists() }) }); -} - -export function useUpdateTrain() { - const qc = useQueryClient(); - return useMutation({ mutationFn: ({ id, data }: any) => trainService.update(id, data), onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: trainKeys.lists() }); - qc.invalidateQueries({ queryKey: trainKeys.detail(id) }); - } }); -} - -export function useDeleteTrain() { - const qc = useQueryClient(); - return useMutation({ mutationFn: trainService.delete, onSuccess: () => qc.invalidateQueries({ queryKey: trainKeys.lists() }) }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useWagons.ts b/apps/edr-freight-web/backoffice/src/hooks/useWagons.ts deleted file mode 100644 index b672a2a84..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useWagons.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { wagonService } from '@/services/wagon.service'; - -export type WagonListFilters = import('@/services/wagon.service').WagonListFilters; - -export const wagonKeys = { - all: ['wagons'] as const, - list: (filters?: WagonListFilters) => [...wagonKeys.all, 'list', filters ?? {}] as const, - byTrain: (trainId: string) => [...wagonKeys.all, 'train', trainId] as const, - details: () => [...wagonKeys.all, 'detail'] as const, - detail: (id: string) => [...wagonKeys.details(), id] as const, -}; - -export function useWagons(filters?: WagonListFilters) { - return useQuery({ - queryKey: wagonKeys.list(filters), - queryFn: () => wagonService.getAll(filters ?? {}).then((res) => res.data), - }); -} - -export const useGetWagons = useWagons; - -export function useWagonsByTrain(trainId: string) { - return useQuery({ queryKey: wagonKeys.byTrain(trainId), queryFn: () => wagonService.getByTrain(trainId).then(res => res.data), enabled: !!trainId }); -} - -export function useWagon(id: string) { - return useQuery({ queryKey: wagonKeys.detail(id), queryFn: () => wagonService.getById(id).then(res => res.data), enabled: !!id }); -} - -export const useGetWagon = useWagon; - -export function useAssignWagonToTrain() { - const qc = useQueryClient(); - return useMutation({ mutationFn: ({ wagonId, trainId, sequenceNumber }: any) => wagonService.assignToTrain(wagonId, trainId, sequenceNumber), onSuccess: (_, { trainId }) => qc.invalidateQueries({ queryKey: wagonKeys.byTrain(trainId) }) }); -} - -export function useUnassignWagon() { - const qc = useQueryClient(); - return useMutation({ mutationFn: wagonService.unassign, onSuccess: () => qc.invalidateQueries({ queryKey: wagonKeys.all }) }); -} - -export function useReorderWagons() { - const qc = useQueryClient(); - return useMutation({ mutationFn: ({ trainId, wagonIds }: any) => wagonService.reorder(trainId, wagonIds), onSuccess: (_, { trainId }) => qc.invalidateQueries({ queryKey: wagonKeys.byTrain(trainId) }) }); -} - -export function useCreateWagon() { - const qc = useQueryClient(); - return useMutation({ mutationFn: wagonService.create, onSuccess: () => qc.invalidateQueries({ queryKey: wagonKeys.all }) }); -} - -export function useUpdateWagon() { - const qc = useQueryClient(); - return useMutation({ mutationFn: ({ id, data }: any) => wagonService.update(id, data), onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: wagonKeys.all }); - qc.invalidateQueries({ queryKey: wagonKeys.detail(id) }); - } }); -} - -export function useDeleteWagon() { - const qc = useQueryClient(); - return useMutation({ mutationFn: wagonService.delete, onSuccess: () => qc.invalidateQueries({ queryKey: wagonKeys.all }) }); -} diff --git a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts deleted file mode 100644 index 5e50c151a..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; - -import { warehouseService } from '@/services/warehouse.service'; -import type { - InspectionReportPayload, - SaveAllocationRulePayload, - SaveFeeRulePayload, - WarehouseInvoiceFilter, - PayInvoicePayload, - InventoryFilter, - InventoryInquiryFilter, - LoadInventoryPayload, - MoveInventoryPayload, - ReceiveInventoryPayload, - ReleaseOrderPayload, - DeliverInventoryPayload, - BulkReceivePayload, - BulkInspectPayload, - ReserveInventoryPayload, - SaveWarehousePayload, - SaveYardPayload, - SaveZonePayload, - WarehouseFilter, -} from '@/types/warehouse'; - -export const warehouseKeys = { - all: ['warehouses'] as const, - list: (filter?: WarehouseFilter) => ['warehouses', 'list', filter ?? {}] as const, - detail: (id: string) => ['warehouses', 'detail', id] as const, - yards: (warehouseId: string) => ['warehouses', warehouseId, 'yards'] as const, - zones: (yardId: string) => ['warehouse-yards', yardId, 'zones'] as const, - inventory: (filter?: InventoryFilter) => ['warehouse-inventory', 'list', filter ?? {}] as const, - inquiry: (filter: InventoryInquiryFilter) => ['warehouse-inventory', 'inquiry', filter] as const, -}; - -// ── Warehouses ───────────────────────────────────────────────────────────── - -export function useWarehouses(filter?: WarehouseFilter) { - return useQuery({ - queryKey: warehouseKeys.list(filter), - queryFn: () => warehouseService.list(filter).then((r) => r.data), - }); -} - -export function useWarehouse(id?: string) { - return useQuery({ - queryKey: warehouseKeys.detail(id ?? ''), - queryFn: () => warehouseService.getById(id as string).then((r) => r.data), - enabled: Boolean(id), - }); -} - -export function useCreateWarehouse() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (payload: SaveWarehousePayload) => warehouseService.create(payload), - onSuccess: () => qc.invalidateQueries({ queryKey: warehouseKeys.all }), - }); -} - -export function useUpdateWarehouse() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, payload }: { id: string; payload: Partial }) => - warehouseService.update(id, payload), - onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: warehouseKeys.all }); - qc.invalidateQueries({ queryKey: warehouseKeys.detail(id) }); - }, - }); -} - -// ── Yards ──────────────────────────────────────────────────────────────── - -export function useWarehouseYards(warehouseId?: string) { - return useQuery({ - queryKey: warehouseKeys.yards(warehouseId ?? ''), - queryFn: () => warehouseService.listYards(warehouseId as string).then((r) => r.data), - enabled: Boolean(warehouseId), - }); -} - -export function useCreateYard() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ warehouseId, payload }: { warehouseId: string; payload: SaveYardPayload }) => - warehouseService.createYard(warehouseId, payload), - onSuccess: (_, { warehouseId }) => { - qc.invalidateQueries({ queryKey: warehouseKeys.yards(warehouseId) }); - qc.invalidateQueries({ queryKey: warehouseKeys.detail(warehouseId) }); - }, - }); -} - -export function useUpdateYard() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, payload }: { id: string; payload: Partial }) => - warehouseService.updateYard(id, payload), - onSuccess: () => qc.invalidateQueries({ queryKey: warehouseKeys.all }), - }); -} - -// ── Zones ────────────────────────────────────────────────────────────────── - -export function useWarehouseZones(yardId?: string) { - return useQuery({ - queryKey: warehouseKeys.zones(yardId ?? ''), - queryFn: () => warehouseService.listZones(yardId as string).then((r) => r.data), - enabled: Boolean(yardId), - }); -} - -export function useCreateZone() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ yardId, payload }: { yardId: string; payload: SaveZonePayload }) => - warehouseService.createZone(yardId, payload), - onSuccess: (_, { yardId }) => qc.invalidateQueries({ queryKey: warehouseKeys.zones(yardId) }), - }); -} - -export function useUpdateZone() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, payload }: { id: string; payload: Partial }) => - warehouseService.updateZone(id, payload), - onSuccess: () => qc.invalidateQueries({ queryKey: ['warehouse-yards'] }), - }); -} - -// ── Inventory ────────────────────────────────────────────────────────────── - -export function useWarehouseInventory(filter?: InventoryFilter) { - return useQuery({ - queryKey: warehouseKeys.inventory(filter), - queryFn: () => warehouseService.listInventory(filter).then((r) => r.data), - }); -} - -export function useReceiveInventory() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (payload: ReceiveInventoryPayload) => warehouseService.receiveInventory(payload), - onSuccess: () => { - qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); - qc.invalidateQueries({ queryKey: warehouseKeys.all }); - }, - }); -} - -function useInventoryMutation(fn: (args: TArgs) => Promise) { - const qc = useQueryClient(); - return useMutation({ - mutationFn: fn, - onSuccess: () => { - qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); - qc.invalidateQueries({ queryKey: ['warehouse-loadings'] }); - qc.invalidateQueries({ queryKey: warehouseKeys.all }); - }, - }); -} - -export const useStoreInventory = () => useInventoryMutation((id: string) => warehouseService.store(id)); -export const useReserveInventory = () => - useInventoryMutation((payload: ReserveInventoryPayload) => warehouseService.reserve(payload)); -export const useMarkReadyForLoading = () => - useInventoryMutation((id: string) => warehouseService.markReadyForLoading(id)); -export const useLoadInventory = () => - useInventoryMutation((args: { id: string; payload: LoadInventoryPayload }) => - warehouseService.load(args.id, args.payload), - ); -export const useDispatchInventory = () => useInventoryMutation((id: string) => warehouseService.dispatch(id)); -export const useMoveInventory = () => - useInventoryMutation((args: { id: string; payload: MoveInventoryPayload }) => - warehouseService.move(args.id, args.payload), - ); - -// ── Import branch (READY_FOR_PICKUP → DELIVERED) ─────────────────────────── -export const useMarkReadyForPickup = () => - useInventoryMutation((id: string) => warehouseService.markReadyForPickup(id)); -export const useReleaseInventory = () => - useInventoryMutation((args: { id: string; payload: ReleaseOrderPayload }) => - warehouseService.release(args.id, args.payload), - ); -export const useDeliverInventory = () => - useInventoryMutation((args: { id: string; payload: DeliverInventoryPayload }) => - warehouseService.deliver(args.id, args.payload), - ); - -// ── Receive (Import/Export bulk) ─────────────────────────────────────────── -/** - * All not-yet-received PAID bookings, classified IMPORT/EXPORT by route, in one call. - * Both Receive tabs share this single query (same key) — only one HTTP request fires — - * then filter client-side by direction. - */ -export function useEligibleBookings(enabled = true) { - return useQuery({ - queryKey: ['warehouse-inventory', 'eligible-bookings'], - queryFn: () => warehouseService.eligibleBookings().then((r) => r.data), - enabled, - }); -} -export const useBulkReceive = () => - useInventoryMutation((payload: BulkReceivePayload) => warehouseService.receiveBulk(payload)); -export const useLoadPassedExport = () => - useInventoryMutation(() => warehouseService.loadPassedExport()); -export const useBulkMarkInspected = () => - useInventoryMutation((payload: BulkInspectPayload) => warehouseService.bulkMarkInspected(payload)); - -export function useReadyToLoadExport(enabled = true) { - return useQuery({ - queryKey: ['warehouse-inventory', 'ready-to-load-export'], - queryFn: () => warehouseService.readyToLoadExport().then((r) => r.data), - enabled, - }); -} - -export function useLoadedExport(enabled = true) { - return useQuery({ - queryKey: ['warehouse-inventory', 'loaded-export'], - queryFn: () => warehouseService.loadedExport().then((r) => r.data), - enabled, - }); -} - -export const useBulkDispatchExport = () => - useInventoryMutation((inventoryIds: string[]) => warehouseService.bulkDispatchExport(inventoryIds)); - -/** Arrived IMPORT trains (route-derived). Read-only. */ -export function useImportArriveQueue(enabled = true) { - return useQuery({ - queryKey: ['warehouse-inventory', 'import-arrive-queue'], - queryFn: () => warehouseService.importArriveQueue().then((r) => r.data), - enabled, - }); -} - -/** Assigned bookings/items for an arrived import train. Read-only. */ -export function useImportTrainItems(scheduleId?: string) { - return useQuery({ - queryKey: ['warehouse-inventory', 'import-train-items', scheduleId], - queryFn: () => warehouseService.importTrainItems(scheduleId as string).then((r) => r.data), - enabled: Boolean(scheduleId), - }); -} - -/** Unload all eligible assigned bookings of an ARRIVED import train (→ UNLOADED). */ -export const useAutoUnloadArrivedBookings = () => - useInventoryMutation((scheduleId: string) => warehouseService.autoUnloadArrivedBookings(scheduleId)); - -/** IMPORT inventory in the Unloaded Queue (UNLOADED / destination inspection). Read-only. */ -export function useImportUnloadedQueue(enabled = true) { - return useQuery({ - queryKey: ['warehouse-inventory', 'import-unloaded-queue'], - queryFn: () => warehouseService.importUnloadedQueue().then((r) => r.data), - enabled, - }); -} - -/** IMPORT inventory that is PICKUP_READY (READY_FOR_PICKUP) awaiting pickup/dispatch. Read-only. */ -export function useImportPickupReadyQueue(enabled = true) { - return useQuery({ - queryKey: ['warehouse-inventory', 'import-pickup-ready-queue'], - queryFn: () => warehouseService.importPickupReadyQueue().then((r) => r.data), - enabled, - }); -} - -// ── Loading (Batch 3) ──────────────────────────────────────────────────────── - -export function useLoadableWagons(enabled = true) { - return useQuery({ - queryKey: ['warehouse', 'loadable-wagons'], - queryFn: () => warehouseService.loadableWagons().then((r) => r.data), - enabled, - }); -} - -export function useWarehouseLoadings(params?: { bookingId?: string; wagonId?: string }) { - return useQuery({ - queryKey: ['warehouse-loadings', params ?? {}], - queryFn: () => warehouseService.loadings(params).then((r) => r.data), - }); -} - -export function useBookingSchedule(bookingId?: string) { - return useQuery({ - queryKey: ['warehouse', 'booking-schedule', bookingId ?? ''], - queryFn: () => warehouseService.bookingSchedule(bookingId as string).then((r) => r.data), - enabled: Boolean(bookingId), - }); -} - -export function useInventoryMovements(id?: string) { - return useQuery({ - queryKey: ['warehouse-inventory', id, 'movements'], - queryFn: () => warehouseService.movements(id as string).then((r) => r.data), - enabled: Boolean(id), - }); -} - -export function useInventoryActivity(id?: string) { - return useQuery({ - queryKey: ['warehouse-inventory', id, 'activity'], - queryFn: () => warehouseService.activity(id as string).then((r) => r.data), - enabled: Boolean(id), - }); -} - -export function useWarehouseDashboard() { - return useQuery({ - queryKey: ['warehouses', 'dashboard'], - queryFn: () => warehouseService.dashboard().then((r) => r.data), - }); -} - -export function useInventoryInquiry(filter: InventoryInquiryFilter, enabled = true) { - return useQuery({ - queryKey: warehouseKeys.inquiry(filter), - queryFn: () => warehouseService.inquiry(filter).then((r) => r.data), - enabled, - }); -} - -// ── Batch 4.5: Arrival / Unload / Inspection ──────────────────────────────── - -export function useArrivalQueue() { - return useQuery({ - queryKey: ['warehouse-inventory', 'arrival-queue'], - queryFn: () => warehouseService.arrivalQueue().then((r) => r.data), - }); -} - -function useArrivalInvalidation() { - const qc = useQueryClient(); - return () => { - qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); - qc.invalidateQueries({ queryKey: warehouseKeys.all }); - }; -} - -export function useAutoUnloadArrived() { - const onSuccess = useArrivalInvalidation(); - return useMutation({ mutationFn: () => warehouseService.autoUnloadArrived(), onSuccess }); -} - -export function useAutoLoadReady() { - const onSuccess = useArrivalInvalidation(); - return useMutation({ mutationFn: () => warehouseService.autoLoadReady(), onSuccess }); -} - -export function useUnloadBooking() { - const onSuccess = useArrivalInvalidation(); - return useMutation({ - mutationFn: (args: { bookingId: string; payload?: Record }) => - warehouseService.unloadBooking(args.bookingId, args.payload), - onSuccess, - }); -} - -export function useInspectionReports(inventoryId?: string) { - return useQuery({ - queryKey: ['warehouse-inventory', inventoryId, 'inspection-reports'], - queryFn: () => warehouseService.listInspectionReports(inventoryId as string).then((r) => r.data), - enabled: Boolean(inventoryId), - }); -} - -export function useCreateInspectionReport() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ inventoryId, payload }: { inventoryId: string; payload: InspectionReportPayload }) => - warehouseService.createInspectionReport(inventoryId, payload).then((r) => r.data), - onSuccess: (_, { inventoryId }) => { - qc.invalidateQueries({ queryKey: ['warehouse-inventory', inventoryId, 'inspection-reports'] }); - qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); - }, - }); -} - -export function useUploadInspectionAttachments() { - return useMutation({ - mutationFn: ({ reportId, files }: { reportId: string; files: File[] }) => - warehouseService.uploadInspectionAttachments(reportId, files), - }); -} - -// ── Batch 5: Allocation + Fee rules / preview ─────────────────────────────── - -export function useAllocationRules() { - return useQuery({ - queryKey: ['warehouse-allocation-rules'], - queryFn: () => warehouseService.listAllocationRules().then((r) => r.data), - }); -} - -export function useFeeRules() { - return useQuery({ - queryKey: ['warehouse-fee-rules'], - queryFn: () => warehouseService.listFeeRules().then((r) => r.data), - }); -} - -function useRuleMutation(fn: (args: TArgs) => Promise, keys: string[]) { - const qc = useQueryClient(); - return useMutation({ - mutationFn: fn, - onSuccess: () => keys.forEach((k) => qc.invalidateQueries({ queryKey: [k] })), - }); -} - -export const useCreateAllocationRule = () => - useRuleMutation( - (payload: SaveAllocationRulePayload) => warehouseService.createAllocationRule(payload), - ['warehouse-allocation-rules'], - ); -export const useUpdateAllocationRule = () => - useRuleMutation( - (args: { id: string; payload: Partial }) => - warehouseService.updateAllocationRule(args.id, args.payload), - ['warehouse-allocation-rules'], - ); -export const useDeleteAllocationRule = () => - useRuleMutation((id: string) => warehouseService.deleteAllocationRule(id), ['warehouse-allocation-rules']); - -export const useCreateFeeRule = () => - useRuleMutation((payload: SaveFeeRulePayload) => warehouseService.createFeeRule(payload), ['warehouse-fee-rules']); -export const useUpdateFeeRule = () => - useRuleMutation( - (args: { id: string; payload: Partial }) => - warehouseService.updateFeeRule(args.id, args.payload), - ['warehouse-fee-rules'], - ); -export const useDeleteFeeRule = () => - useRuleMutation((id: string) => warehouseService.deleteFeeRule(id), ['warehouse-fee-rules']); - -export function useFeePreview(inventoryId?: string) { - return useQuery({ - queryKey: ['warehouse-inventory', inventoryId, 'fee-preview'], - queryFn: () => warehouseService.feePreview(inventoryId as string).then((r) => r.data), - enabled: Boolean(inventoryId), - }); -} - -// ── Batch 6: Warehouse fee invoices ───────────────────────────────────────── - -export function useWarehouseInvoices(filter?: WarehouseInvoiceFilter) { - return useQuery({ - queryKey: ['warehouse-fee-invoices', filter ?? {}], - queryFn: () => warehouseService.listInvoices(filter).then((r) => r.data), - }); -} - -export function useWarehouseInvoice(id?: string) { - return useQuery({ - queryKey: ['warehouse-fee-invoices', 'detail', id], - queryFn: () => warehouseService.getInvoice(id as string).then((r) => r.data), - enabled: Boolean(id), - }); -} - -export function useInvoicesForInventory(inventoryId?: string) { - return useQuery({ - queryKey: ['warehouse-inventory', inventoryId, 'fee-invoices'], - queryFn: () => warehouseService.invoicesForInventory(inventoryId as string).then((r) => r.data), - enabled: Boolean(inventoryId), - }); -} - -function useInvoiceInvalidation() { - const qc = useQueryClient(); - return () => { - qc.invalidateQueries({ queryKey: ['warehouse-fee-invoices'] }); - qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }); - }; -} - -export function useGenerateInvoice() { - const onSuccess = useInvoiceInvalidation(); - return useMutation({ - mutationFn: ({ inventoryId, confirmZero }: { inventoryId: string; confirmZero?: boolean }) => - warehouseService.generateInvoice(inventoryId, confirmZero).then((r) => r.data), - onSuccess, - }); -} - -export function useCancelInvoice() { - const onSuccess = useInvoiceInvalidation(); - return useMutation({ mutationFn: (id: string) => warehouseService.cancelInvoice(id), onSuccess }); -} - -export function usePayInvoice() { - const onSuccess = useInvoiceInvalidation(); - return useMutation({ - mutationFn: ({ id, payload }: { id: string; payload: PayInvoicePayload }) => - warehouseService.payInvoice(id, payload), - onSuccess, - }); -} - -export function useGateClearance() { - const onSuccess = useInvoiceInvalidation(); - return useMutation({ mutationFn: (inventoryId: string) => warehouseService.gateClearance(inventoryId), onSuccess }); -} diff --git a/apps/edr-freight-web/backoffice/src/lib/queryClient.ts b/apps/edr-freight-web/backoffice/src/lib/queryClient.ts index 9ac8402d8..32b94f325 100644 --- a/apps/edr-freight-web/backoffice/src/lib/queryClient.ts +++ b/apps/edr-freight-web/backoffice/src/lib/queryClient.ts @@ -1,7 +1,29 @@ -import { QueryClient } from "@tanstack/react-query"; +import { MutationCache, QueryClient } from "@tanstack/react-query"; -/** Single app-wide React Query client (do not nest additional providers). */ +import type { InvalidatesMeta } from "@/utils/endpoint"; + +/** + * Single app-wide React Query client (do not nest additional providers). + * + * Declarative invalidation: any mutation built via `api.*.mutationOptions()` + * (see `services/api.ts` + `utils/endpoint.ts`) carries an `invalidates` + * function in its `meta`. The shared `MutationCache` below runs it on success + * and invalidates the returned query keys — so invalidation is declared once in + * the endpoint definition rather than re-wired in every component. + */ export const queryClient = new QueryClient({ + mutationCache: new MutationCache({ + onSuccess: (data, variables, _context, mutation) => { + const invalidates = mutation.meta?.invalidates as + | InvalidatesMeta + | undefined; + if (typeof invalidates !== "function") return; + + for (const queryKey of invalidates(variables, data)) { + void queryClient.invalidateQueries({ queryKey }); + } + }, + }), defaultOptions: { queries: { retry: 1, diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx index 3f5323c7e..b3c9db35d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx @@ -1,7 +1,3 @@ -import { useEffect, useMemo, useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useNavigate } from "react-router-dom"; -import { isAxiosError } from "axios"; import { ActionIcon, Badge, @@ -23,6 +19,8 @@ import { ThemeIcon, Tooltip, } from "@mantine/core"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { isAxiosError } from "axios"; import { AlertTriangle, ArrowLeft, @@ -40,14 +38,16 @@ import { Trash2, Weight, } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; import toast from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; -import Breadcrumbs from "@/components/ui/Breadcrumbs"; -import { bookingsService } from "@/services/bookings.service"; -import { useAvailableDays } from "@/hooks/trainScheduling/useTrainScheduling"; import { api } from "@/auth/http"; -import { unwrap } from "@/utils/endpoint"; +import Breadcrumbs from "@/components/ui/Breadcrumbs"; import { URL_CONSTANTS } from "@/constants/URLS"; +import { api as appApi } from "@/services/api"; +import { bookingsService } from "@/services/bookings.service"; +import { unwrap } from "@/utils/endpoint"; interface CompanyOption { id: string; @@ -234,9 +234,11 @@ export default function NewBookingPage() { // Day-level pool: fetch only the days that have a departure on the route (no // train, no capacity). The batch engine assigns the train after booking. - const { data: availableDays, isLoading: daysLoading } = useAvailableDays( - originYardId, - destinationYardId, + const { data: availableDays, isLoading: daysLoading } = useQuery( + appApi.trainScheduling.availableDays.queryOptions({ + input: { originYardId, destinationYardId }, + enabled: Boolean(originYardId && destinationYardId), + }), ); const dayOptions = (availableDays ?? []).map((day) => ({ value: day, @@ -392,7 +394,7 @@ export default function NewBookingPage() { - + {/* LEFT — form */} @@ -453,7 +455,6 @@ export default function NewBookingPage() { value={originYardId} onChange={(v) => { setOriginYardId(v); - setTrainScheduleId(null); }} searchable disabled={isLoading} diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx index 1c1571966..f38e8c38b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx @@ -1,12 +1,584 @@ -import FeaturePlaceholder from "@/components/FeaturePlaceholder"; +import { + ActionIcon, + Box, + Button, + Card, + Center, + Container, + Group, + Loader, + SimpleGrid, + Stack, + Tabs, + Text, +} from "@mantine/core"; +import { + ArrowLeft, + ArrowRight, + Banknote, + Download, + FileText, + IdCard, + LayoutGrid, + Package, +} from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { useNavigate, useParams } from "react-router-dom"; -const CustomerDetailPage = () => { +import { + BookingStatusBadge, + CompanyStatusBadge, + CompanyTypeBadge, + PaymentStatusBadge, + ProfileApprovalActions, + ProfileChips, + ProfileStatusBadge, + ProfileTypeBadge, + TableCard, + formatBytes, + formatDate, + formatMoney, + humanize, +} from "@/components/customers"; +import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; +import { api } from "@/services/api"; +import type { + CompanyProfile, + CustomerBooking, + CustomerDocument, + CustomerPayment, +} from "@/types/customer"; +import { DataTable, type ColumnDef } from "@edr/ui-common"; + +function InfoField({ label, value }: { label: string; value?: string | null }) { return ( - + + + {label} + + + {value && value.trim() ? value : "—"} + + ); -}; +} -export default CustomerDetailPage; +function tableStatus(query: { isLoading: boolean; isError: boolean }) { + return query.isLoading ? "loading" : query.isError ? "error" : "success"; +} + +export default function CustomerDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const { data: company, isLoading } = useQuery( + api.customers.getById.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + const bookingsQuery = useQuery( + api.customers.bookings.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + const documentsQuery = useQuery( + api.customers.documents.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + const paymentsQuery = useQuery( + api.customers.payments.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + + const bookings = bookingsQuery.data ?? []; + const documents = documentsQuery.data ?? []; + const payments = paymentsQuery.data ?? []; + + const totalPaid = useMemo( + () => + payments + .filter((p) => p.status === "success") + .reduce((sum, p) => sum + p.amount, 0), + [payments], + ); + const paidCurrency = payments[0]?.currency ?? "ETB"; + + const profileColumns: ColumnDef[] = useMemo( + () => [ + { + id: "type", + header: "Role", + cell: ({ row }) => , + }, + { + id: "reference", + header: "Reference", + cell: ({ row }) => ( + + {row.original.reference} + + ), + }, + { + id: "businessLicense", + header: "Business license", + cell: ({ row }) => ( + + {row.original.businessLicense || "—"} + + ), + }, + { + id: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "createdAt", + header: "Registered", + cell: ({ row }) => ( + + {formatDate(row.original.createdAt)} + + ), + }, + { + id: "actions", + header: "", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + ), + }, + ], + [], + ); + + const bookingColumns: ColumnDef[] = useMemo( + () => [ + { + id: "reference", + header: "Booking", + cell: ({ row }) => ( + + {row.original.reference} + + ), + }, + { + id: "route", + header: "Route", + cell: ({ row }) => { + const b = row.original; + return ( + + + {b.originLabel} + + + + {b.destinationLabel} + + + ); + }, + }, + { + id: "type", + header: "Type", + cell: ({ row }) => ( + + {humanize(row.original.tradeDirection)} ·{" "} + {humanize(row.original.freightType)} + + ), + }, + { + id: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "amount", + header: "Amount", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + {formatMoney(row.original.totalAmount, row.original.currency)} + + ), + }, + { + id: "createdAt", + header: "Created", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + {formatDate(row.original.createdAt)} + + ), + }, + ], + [], + ); + + const documentColumns: ColumnDef[] = useMemo( + () => [ + { + id: "name", + header: "Document", + cell: ({ row }) => ( + + + + {row.original.name} + + + ), + }, + { + id: "code", + header: "Type", + cell: ({ row }) => ( + + {humanize(row.original.code)} + + ), + }, + { + id: "size", + header: "Size", + cell: ({ row }) => ( + + {formatBytes(row.original.size)} + + ), + }, + { + id: "uploadedAt", + header: "Uploaded", + cell: ({ row }) => ( + + {formatDate(row.original.uploadedAt)} + + ), + }, + { + id: "actions", + header: "", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + + + ), + }, + ], + [], + ); + + const paymentColumns: ColumnDef[] = useMemo( + () => [ + { + id: "reference", + header: "Payment", + cell: ({ row }) => ( + + {row.original.reference} + + ), + }, + { + id: "booking", + header: "Booking", + cell: ({ row }) => ( + + {row.original.bookingReference} + + ), + }, + { + id: "method", + header: "Method", + cell: ({ row }) => ( + + {humanize(row.original.method)} + + ), + }, + { + id: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "paidAt", + header: "Paid", + cell: ({ row }) => ( + + {formatDate(row.original.paidAt)} + + ), + }, + { + id: "amount", + header: "Amount", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + {formatMoney(row.original.amount, row.original.currency)} + + ), + }, + ], + [], + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!company) { + return ( + + + Customer not found + + + + ); + } + + return ( + + + + + + } + /> + + + + }> + Overview + + }> + Bookings + + }> + Documents + + }> + Payments + + + + {/* OVERVIEW */} + + + p.status === "pending", + ).length, + icon: IdCard, + color: "yellow", + }, + { + label: "Bookings", + value: bookings.length, + icon: Package, + color: "blue", + }, + { + label: "Total paid", + value: formatMoney(totalPaid, paidCurrency), + icon: Banknote, + color: "edr-green", + }, + ]} + /> + + + + + Company information + + + + + + + + + + + + + + + + + + + + + + + + + + Role profiles + + + + + + + + + + + + + + {/* BOOKINGS */} + + + void bookingsQuery.refetch(), + } + : undefined + } + /> + + + + {/* DOCUMENTS */} + + + void documentsQuery.refetch(), + } + : undefined + } + /> + + + + {/* PAYMENTS */} + + + void paymentsQuery.refetch(), + } + : undefined + } + /> + + + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx index 8d9158381..e06d310f4 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomersPage.tsx @@ -1,12 +1,266 @@ -import FeaturePlaceholder from "@/components/FeaturePlaceholder"; +import { + ActionIcon, + Box, + Card, + Group, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; +import { useQuery } from "@tanstack/react-query"; +import { + Building2, + CheckCircle2, + Clock, + Mail, + Phone, + RefreshCw, + Search, + ShieldOff, + Users, + X, +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; -const CustomersPage = () => { - return ( - +import { + CompanyStatusBadge, + CompanyTypeBadge, + ProfileChips, + formatDate, +} from "@/components/customers"; +import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; +import { api } from "@/services/api"; +import type { Company } from "@/types/customer"; +import { + DataTable, + DataTableFooter, + usePagination, + type ColumnDef, +} from "@edr/ui-common"; + +export default function CustomersPage() { + const navigate = useNavigate(); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); + const [query, setQuery] = useState(""); + const [debouncedQuery] = useDebouncedValue(query, 300); + + const filter = useMemo( + () => ({ + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: debouncedQuery, + }), + [pagination.pageIndex, pagination.pageSize, debouncedQuery], ); -}; -export default CustomersPage; + const { data: stats } = useQuery(api.customers.stats.queryOptions({ input: {} })); + + const { data, isLoading, isError, refetch, isFetching } = useQuery( + api.customers.list.queryOptions({ input: { filter } }), + ); + + const rows = data?.items ?? []; + const total = data?.total ?? 0; + const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize)); + + const columns: ColumnDef[] = useMemo( + () => [ + { + id: "company", + header: "Company", + cell: ({ row }) => { + const c = row.original; + return ( + + + + +
+ + + {c.name} + + + + + TIN {c.tin} + {c.country ? ` · ${c.country}` : ""} + +
+
+ ); + }, + }, + { + id: "profiles", + header: "Profiles", + cell: ({ row }) => , + }, + { + id: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "contact", + header: "Contact", + cell: ({ row }) => { + const c = row.original; + return ( + + {c.contactPersonName ? ( + + {c.contactPersonName} + + ) : null} + {c.phone ? ( + + {c.phone} + + ) : null} + {c.email ? ( + + {c.email} + + ) : null} + + ); + }, + }, + { + id: "created", + header: "Registered", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + {formatDate(row.original.createdAt)} + + ), + }, + ], + [], + ); + + return ( + + void refetch()} + > + + + } + /> + + + + + + + + } + value={query} + onChange={(e) => setQuery(e.target.value)} + rightSection={ + query ? ( + setQuery("")} + > + + + ) : null + } + style={{ flex: 1, minWidth: "240px" }} + radius="lg" + /> + + {total} record{total !== 1 ? "s" : ""} + + + + + + + navigate(`/dashboard/customers/${row.id}`)} + emptyMessage={ + debouncedQuery + ? "No companies match your search." + : "No companies yet." + } + error={ + isError + ? { + message: "Failed to load customers.", + onRetry: () => void refetch(), + } + : undefined + } + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + manualPagination: true, + pageCount, + }} + containerClassName="border-0 shadow-none bg-transparent" + footer={DataTableFooter} + /> + + + + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx b/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx index 1938443ab..4370adac1 100644 --- a/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx @@ -15,7 +15,8 @@ import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { FileUploadEntity } from "@edr/types/freight"; -import { useCreateFileUploadSetting, useUpdateFileUploadSetting } from "@/hooks/useFileUploadSettings"; +import { useMutation } from "@tanstack/react-query"; +import { api } from "@/services/api"; // import type { // FileUploadEntity, @@ -61,8 +62,8 @@ export default function EditFileUploadSettingDialog({ const [description, setDescription] = useState(setting?.description ?? ""); const [error, setError] = useState(null); - const createMutation = useCreateFileUploadSetting(); - const updateMutation = useUpdateFileUploadSetting(); + const createMutation = useMutation(api.fileUploadSettings.create.mutationOptions()); + const updateMutation = useMutation(api.fileUploadSettings.update.mutationOptions()); const pending = createMutation.isPending || updateMutation.isPending; const reset = () => { diff --git a/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx index acc9b91c0..24c2772aa 100644 --- a/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx @@ -25,12 +25,11 @@ import { Trash2, X, } from "lucide-react"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { api } from "@/services/api"; -import { useDeleteFileUploadSetting } from "@/hooks/useFileUploadSettings"; import { getMinFiles, type FileUploadSetting } from "@/types/fileUploadSettings"; import { DataTable, type ColumnDef } from "@edr/ui-common"; @@ -44,7 +43,7 @@ export default function FileUploadSettingsPage() { const { data, isLoading, isError, error, refetch } = useQuery( api.fileUploadSettings.list.queryOptions(), ); - const deleteMutation = useDeleteFileUploadSetting(); + const deleteMutation = useMutation(api.fileUploadSettings.remove.mutationOptions()); const fileUploadSettings = useMemo( () => (Array.isArray(data) ? data : []), @@ -202,7 +201,7 @@ export default function FileUploadSettingsPage() { deleteMutation.mutate(setting.id)} + onConfirm={() => deleteMutation.mutate({ id: setting.id })} > (seed); - const replaceMutation = useReplaceFileUploadFields(); + const replaceMutation = useMutation( + api.fileUploadSettings.replaceFields.mutationOptions(), + ); const update = (i: number, patch: Partial) => setFields((prev) => @@ -145,7 +148,7 @@ export default function ManageFileUploadFieldsDialog({ })); replaceMutation.mutate( - { settingId: setting.id, fields: payload }, + { id: setting.id, fields: payload }, { onSuccess: () => setOpen(false), onError: (err) => diff --git a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx index d2689c750..751b94886 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx @@ -31,9 +31,8 @@ import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import EditDropdownSettingDialog from "./EditDropdownSettingDialog"; import ManageDropdownOptionsDialog from "./ManageDropdownOptionsDialog"; import DeleteDropdownSettingDialog from "./DeleteDropdownSettingDialog"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { api } from "@/services/api"; -import { useDeleteDropdownSetting } from "@/hooks/useDropdownSettings"; import type { DropdownSetting } from "@/types/dropdownSettings"; import { DataTable, @@ -63,7 +62,7 @@ export default function DropdownSettingsPage() { const { data, isLoading, isError, error } = useQuery( api.dropdownSettings.list.queryOptions(), ); - const deleteMutation = useDeleteDropdownSetting(); + const deleteMutation = useMutation(api.dropdownSettings.remove.mutationOptions()); const dropdownSettings = useMemo( () => (Array.isArray(data) ? data : []), @@ -353,7 +352,7 @@ export default function DropdownSettingsPage() { key={`delete-${activeSetting.id}`} settingLabel={activeSetting.label} settingCode={activeSetting.code} - onConfirm={() => deleteMutation.mutate(activeSetting.id)} + onConfirm={() => deleteMutation.mutate({ id: activeSetting.id })} open={activeDialog === "delete"} onOpenChange={(next) => (next ? null : closeDialog())} /> diff --git a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/EditDropdownSettingDialog.tsx b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/EditDropdownSettingDialog.tsx index 14922446c..053a74867 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/EditDropdownSettingDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/EditDropdownSettingDialog.tsx @@ -20,10 +20,9 @@ import type { DropdownSetting, UpdateDropdownSettingDto, } from "@/types/dropdownSettings"; -import { - useCreateDropdownSetting, - useUpdateDropdownSetting, -} from "@/hooks/useDropdownSettings"; +import { useMutation } from "@tanstack/react-query"; + +import { api } from "@/services/api"; export interface EditDropdownSettingDialogProps { mode?: "create" | "edit"; @@ -76,8 +75,8 @@ export default function EditDropdownSettingDialog({ ); const [error, setError] = useState(null); - const createMutation = useCreateDropdownSetting(); - const updateMutation = useUpdateDropdownSetting(); + const createMutation = useMutation(api.dropdownSettings.create.mutationOptions()); + const updateMutation = useMutation(api.dropdownSettings.update.mutationOptions()); const pending = createMutation.isPending || updateMutation.isPending; const reset = () => { diff --git a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/ManageDropdownOptionsDialog.tsx b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/ManageDropdownOptionsDialog.tsx index 0528145ff..9f9a27057 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/ManageDropdownOptionsDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/ManageDropdownOptionsDialog.tsx @@ -18,7 +18,8 @@ import type { CreateDropdownOptionDto, DropdownSetting, } from "@/types/dropdownSettings"; -import { useReplaceDropdownOptions } from "@/hooks/useDropdownSettings"; +import { useMutation } from "@tanstack/react-query"; +import { api } from "@/services/api"; export interface ManageDropdownOptionsDialogProps { setting: DropdownSetting; @@ -84,7 +85,9 @@ export default function ManageDropdownOptionsDialog({ const [options, setOptions] = useState(seed); - const replaceMutation = useReplaceDropdownOptions(); + const replaceMutation = useMutation( + api.dropdownSettings.replaceOptions.mutationOptions(), + ); const update = (i: number, patch: Partial) => setOptions((prev) => @@ -147,7 +150,7 @@ export default function ManageDropdownOptionsDialog({ }); replaceMutation.mutate( - { settingId: setting.id, options: payload }, + { id: setting.id, options: payload }, { onSuccess: () => setOpen(false), onError: (err) => diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx index 9206372e4..cd846ada7 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx @@ -1,5 +1,8 @@ import { FormEvent, ReactNode, useMemo, useState } from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { Edit, Eye, Plus, Search, Trash2 } from 'lucide-react'; + +import { api } from '@/services/api'; import { ActionIcon, Badge as MantineBadge, @@ -33,31 +36,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { useCargoTypes } from '@/hooks/use-cargo-types'; -import { useContainerTypes } from '@/hooks/use-container-types'; -import { - useCreateWagonType, - useDeleteWagonType, - useUpdateWagonType, - useWagonTypes, -} from '@/hooks/use-wagon-types'; import { useToast } from '@/hooks/use-toast'; -import { useCreateCargo, useDeleteCargo, useCargoes, useUpdateCargo } from '@/hooks/useCargoes'; -import { - useContainers, - useCreateContainer, - useDeleteContainer, - useUpdateContainer, -} from '@/hooks/useContainers'; -import { useCreateTrain, useDeleteTrain, useTrains, useUpdateTrain } from '@/hooks/useTrains'; -import { useRouteYards } from '@/hooks/useRoutes'; -import { useCreateWagon, useDeleteWagon, useUpdateWagon, useWagons } from '@/hooks/useWagons'; -import { - useCreateLocomotive, - useDecommissionLocomotive, - useLocomotives, - useUpdateLocomotive, -} from '@/hooks/useLocomotives'; import type { Cargo } from '@/services/cargoService'; import { DeliverCargoDialog } from '@/components/cargoes/DeliverCargoDialog'; import type { Container } from '@/services/containerService'; @@ -511,7 +490,7 @@ const optionLabel = (options: { value: string; label: string }[], value?: string options.find((option) => option.value === value)?.label ?? value ?? '-'; export function TrainMasterDataPage() { - const query = useTrains(); + const query = useQuery(api.trains.list.queryOptions()); return ( title="Trains" @@ -519,9 +498,9 @@ export function TrainMasterDataPage() { addLabel="Add Train" data={query.data} isLoading={query.isLoading} - create={useCreateTrain()} - update={useUpdateTrain()} - remove={useDeleteTrain()} + create={useMutation(api.trains.create.mutationOptions())} + update={useMutation(api.trains.update.mutationOptions())} + remove={useMutation(api.trains.remove.mutationOptions())} searchText={(train) => [train.code, train.trainNumber, train.trainName, train.status].join(' ')} columns={[ { key: 'code', label: 'Code' }, @@ -546,10 +525,10 @@ export function TrainMasterDataPage() { } export function WagonTypesCrudPage() { - const query = useWagonTypes(); - const create = useCreateWagonType(); - const update = useUpdateWagonType(); - const remove = useDeleteWagonType(); + const query = useQuery(api.wagonTypes.list.queryOptions()); + const create = useMutation(api.wagonTypes.create.mutationOptions()); + const update = useMutation(api.wagonTypes.update.mutationOptions()); + const remove = useMutation(api.wagonTypes.remove.mutationOptions()); const { toast } = useToast(); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); @@ -896,9 +875,9 @@ export function WagonTypesCrudPage() { } export function WagonsCrudPage() { - const query = useWagons(); - const { data: wagonTypes = [] } = useWagonTypes(); - const { data: yards = [] } = useRouteYards(); + const query = useQuery(api.wagons.list.queryOptions({ input: {} })); + const { data: wagonTypes = [] } = useQuery(api.wagonTypes.list.queryOptions()); + const { data: yards = [] } = useQuery(api.routes.yards.queryOptions()); const wagonTypeOptions = wagonTypes.map((type: any) => ({ value: type.id, label: `${type.code} - ${type.name}`, @@ -914,9 +893,9 @@ export function WagonsCrudPage() { addLabel="Add Wagon" data={query.data} isLoading={query.isLoading} - create={useCreateWagon()} - update={useUpdateWagon()} - remove={useDeleteWagon()} + create={useMutation(api.wagons.create.mutationOptions())} + update={useMutation(api.wagons.update.mutationOptions())} + remove={useMutation(api.wagons.remove.mutationOptions())} searchText={(wagon) => [ wagon.wagonNumber, wagon.wagonTypeId, @@ -984,9 +963,11 @@ export function WagonsCrudPage() { } export function ContainersCrudPage() { - const query = useContainers(); - const { data: containerTypes = [] } = useContainerTypes(); - const { data: wagons = [] } = useWagons(); + const query = useQuery(api.containers.list.queryOptions()); + const { data: containerTypes = [] } = useQuery( + api.containerTypes.list.queryOptions({ staleTime: Infinity }), + ); + const { data: wagons = [] } = useQuery(api.wagons.list.queryOptions({ input: {} })); const containerTypeOptions = containerTypes.map((type: any) => ({ value: type.id, label: type.label ?? type.name ?? type.code, @@ -1002,9 +983,9 @@ export function ContainersCrudPage() { addLabel="Add Container" data={query.data} isLoading={query.isLoading} - create={useCreateContainer()} - update={useUpdateContainer()} - remove={useDeleteContainer()} + create={useMutation(api.containers.create.mutationOptions())} + update={useMutation(api.containers.update.mutationOptions())} + remove={useMutation(api.containers.remove.mutationOptions())} searchText={(container) => [container.containerNumber, container.containerTypeId, container.wagonId, container.status].join(' ')} columns={[ { key: 'containerNumber', label: 'Number' }, @@ -1041,9 +1022,11 @@ export function ContainersCrudPage() { } export function CargoesCrudPage() { - const query = useCargoes(); - const { data: cargoTypes = [] } = useCargoTypes(); - const { data: containers = [] } = useContainers(); + const query = useQuery(api.cargoes.list.queryOptions()); + const { data: cargoTypes = [] } = useQuery( + api.cargoTypes.list.queryOptions({ staleTime: Infinity }), + ); + const { data: containers = [] } = useQuery(api.containers.list.queryOptions()); const cargoTypeOptions = cargoTypes.map((type: any) => ({ value: type.id, label: type.cargoTypeName ?? type.cargo_type_name ?? type.name ?? type.code, @@ -1059,9 +1042,9 @@ export function CargoesCrudPage() { addLabel="Add Cargo" data={query.data} isLoading={query.isLoading} - create={useCreateCargo()} - update={useUpdateCargo()} - remove={useDeleteCargo()} + create={useMutation(api.cargoes.create.mutationOptions())} + update={useMutation(api.cargoes.update.mutationOptions())} + remove={useMutation(api.cargoes.remove.mutationOptions())} searchText={(cargo) => [cargo.cargoReference, cargo.description, cargo.containerId, cargo.status].join(' ')} columns={[ { key: 'cargoReference', label: 'Reference' }, @@ -1110,7 +1093,7 @@ export function CargoesCrudPage() { } export function LocomotivesCrudPage() { - const query = useLocomotives(); + const query = useQuery(api.locomotives.list.queryOptions()); return ( @@ -1120,9 +1103,9 @@ export function LocomotivesCrudPage() { addLabel="Add Locomotive" data={query.data} isLoading={query.isLoading} - create={useCreateLocomotive()} - update={useUpdateLocomotive()} - remove={useDecommissionLocomotive()} + create={useMutation(api.locomotives.create.mutationOptions())} + update={useMutation(api.locomotives.update.mutationOptions())} + remove={useMutation(api.locomotives.decommission.mutationOptions())} removeActionLabel="Decommission" removeConfirmMessage="Decommission this locomotive?" removeSuccessMessage="Locomotive decommissioned" diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx index 58b5dffed..6db31e931 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx @@ -1,7 +1,8 @@ import type { ColumnDef } from "@edr/ui-common"; +import { Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core"; +import { useMutation, useQuery } from "@tanstack/react-query"; -import Breadcrumbs from "@/components/ui/Breadcrumbs"; -import {Container, Title, Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core"; +import { api } from "@/services/api"; import { Archive, Circle, @@ -24,14 +25,7 @@ import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/f import { useFleetViewMode } from "@/components/fleet/useFleetViewMode"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; -import { useFleetList, useFleetMutations } from "@/hooks/fleet/useFleet"; -import { useCargoTypes } from "@/hooks/use-cargo-types"; -import { useContainerTypes } from "@/hooks/use-container-types"; import { useToast } from "@/hooks/use-toast"; -import { useWagonTypes } from "@/hooks/use-wagon-types"; -import { useContainers } from "@/hooks/useContainers"; -import { useRouteYards } from "@/hooks/useRoutes"; -import { useWagons } from "@/hooks/useWagons"; import { FLEET_SELECT_NONE, getFleetResource, @@ -94,16 +88,31 @@ const FleetResourcePage = () => { return filters; }, [slug, listFilterValues, search]); - const { data: allRows = [], isLoading, isError, error } = useFleetList(slug, serverListFilters); - const { data: drivers = [] } = useFleetList("drivers"); - const { create, update, remove } = useFleetMutations(slug); + const { data: allRows = [], isLoading, isError, error } = useQuery( + api.fleet.list.queryOptions({ input: { slug, filters: serverListFilters } }), + ); + const create = useMutation(api.fleet.create.mutationOptions()); + const update = useMutation(api.fleet.update.mutationOptions()); + const remove = useMutation(api.fleet.remove.mutationOptions()); - const { data: wagonTypes = [], isLoading: wagonTypesLoading } = useWagonTypes(); - const { data: containerTypes = [], isLoading: containerTypesLoading } = useContainerTypes(); - const { data: cargoTypes = [], isLoading: cargoTypesLoading } = useCargoTypes(); - const { data: wagons = [], isLoading: wagonsLoading } = useWagons(); - const { data: containers = [], isLoading: containersLoading } = useContainers(); - const { data: yards = [], isLoading: yardsLoading } = useRouteYards(); + const { data: wagonTypes = [], isLoading: wagonTypesLoading } = useQuery( + api.wagonTypes.list.queryOptions(), + ); + const { data: containerTypes = [], isLoading: containerTypesLoading } = useQuery( + api.containerTypes.list.queryOptions({ staleTime: Infinity }), + ); + const { data: cargoTypes = [], isLoading: cargoTypesLoading } = useQuery( + api.cargoTypes.list.queryOptions({ staleTime: Infinity }), + ); + const { data: wagons = [], isLoading: wagonsLoading } = useQuery( + api.wagons.list.queryOptions({ input: {} }), + ); + const { data: containers = [], isLoading: containersLoading } = useQuery( + api.containers.list.queryOptions(), + ); + const { data: yards = [], isLoading: yardsLoading } = useQuery( + api.routes.yards.queryOptions(), + ); useEffect(() => { setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize })); @@ -332,10 +341,10 @@ const FleetResourcePage = () => { const handleFormSubmit = async (values: Record) => { try { if (editing && "id" in editing) { - await update.mutateAsync({ id: String(editing.id), data: values }); + await update.mutateAsync({ slug, id: String(editing.id), data: values }); toast({ title: `${config.entityLabel} updated` }); } else { - await create.mutateAsync(values); + await create.mutateAsync({ slug, data: values }); toast({ title: `${config.entityLabel} created` }); } setFormOpen(false); @@ -351,7 +360,7 @@ const FleetResourcePage = () => { const handleRemove = async () => { if (!removeTarget || !("id" in removeTarget)) return; try { - await remove.mutateAsync(String(removeTarget.id)); + await remove.mutateAsync({ slug, id: String(removeTarget.id) }); toast({ title: config.removeSuccessMessage ?? `${config.entityLabel} removed`, }); diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx index 543ae26db..7d530697f 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx @@ -17,18 +17,14 @@ import { Tooltip, } from "@mantine/core"; +import { useMutation, useQuery } from "@tanstack/react-query"; + import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import FleetToolbar from "@/components/fleet/FleetToolbar"; import { useFleetViewMode } from "@/components/fleet/useFleetViewMode"; import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; -import { - useCreateRoute, - useDeactivateRoute, - useRouteYards, - useRoutes, - useUpdateRoute, -} from "@/hooks/useRoutes"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import type { RouteRecord, YardRef } from "@/services/routes.service"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; @@ -71,11 +67,11 @@ export default function RoutesPage() { const { pagination, setPagination } = usePagination({ pageSize: 10 }); const { toast } = useToast(); - const routesQuery = useRoutes(); - const yardsQuery = useRouteYards(); - const createMutation = useCreateRoute(); - const updateMutation = useUpdateRoute(); - const deactivateMutation = useDeactivateRoute(); + const routesQuery = useQuery(api.routes.list.queryOptions()); + const yardsQuery = useQuery(api.routes.yards.queryOptions()); + const createMutation = useMutation(api.routes.create.mutationOptions()); + const updateMutation = useMutation(api.routes.update.mutationOptions()); + const deactivateMutation = useMutation(api.routes.deactivate.mutationOptions()); const filteredRoutes = useMemo(() => { const query = search.trim().toLowerCase(); diff --git a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx index e5a490927..f68ac32f4 100644 --- a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx @@ -22,8 +22,10 @@ import { } from "lucide-react"; import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; + import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; -import { usePaymentList, usePaymentSummary } from "@/hooks/usePayments"; +import { api } from "@/services/api"; import type { PaymentMethod, PaymentRow } from "@/services/payments.service"; import { Badge, @@ -106,8 +108,12 @@ export default function PaymentsPage() { [query, statuses, method, pagination.pageIndex, pagination.pageSize], ); - const { data, isLoading, isError } = usePaymentList(filter); - const { data: summary, isLoading: summaryLoading } = usePaymentSummary(); + const { data, isLoading, isError } = useQuery( + api.payments.list.queryOptions({ input: { filter } }), + ); + const { data: summary, isLoading: summaryLoading } = useQuery( + api.payments.summary.queryOptions({ staleTime: 30_000 }), + ); const rows = data?.items ?? []; const total = data?.total ?? 0; diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx index bc6e5bb15..9085f2868 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchBoardPage.tsx @@ -46,7 +46,8 @@ import { import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { FREIGHT_BRAND, FREIGHT_BRAND_DARK } from "@/theme/freight-brand"; -import { useBatchBoard } from "@/hooks/trainScheduling/useTrainScheduling"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import type { BatchBoardSchedule } from "@/types/trainScheduling"; const fmtTons = (n: number) => @@ -368,7 +369,9 @@ function CardSkeleton() { export default function BatchBoardPage() { const navigate = useNavigate(); - const { data, isLoading, isError, isFetching, refetch } = useBatchBoard(); + const { data, isLoading, isError, isFetching, refetch } = useQuery( + api.trainScheduling.batchBoard.queryOptions({ refetchInterval: 30_000 }), + ); const { viewMode, setViewMode } = useFleetViewMode("batch-board"); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [search, setSearch] = useState(""); diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx index f78639359..1417d738c 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/BatchScheduleDetailPage.tsx @@ -52,11 +52,8 @@ import { WindowStatusPill, } from "@/components/trainScheduling/batchVisuals"; import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals"; -import { - useBatchBoardDetail, - useRunAllocation, - useScheduleDetail, -} from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import type { BatchBoardBookingDetail, @@ -429,8 +426,16 @@ export default function BatchScheduleDetailPage() { const { scheduleId } = useParams<{ scheduleId: string }>(); const navigate = useNavigate(); const { toast } = useToast(); - const { data, isLoading, isFetching, refetch } = useBatchBoardDetail(scheduleId); - const runAllocation = useRunAllocation(scheduleId ?? ""); + const { data, isLoading, isFetching, refetch } = useQuery( + api.trainScheduling.batchBoardDetail.queryOptions({ + input: { scheduleId: scheduleId ?? "" }, + enabled: Boolean(scheduleId), + refetchInterval: 30_000, + }), + ); + const runAllocation = useMutation( + api.trainScheduling.runAllocation.mutationOptions(), + ); const hasAssignedWagons = useMemo( () => @@ -443,7 +448,12 @@ export default function BatchScheduleDetailPage() { [data], ); - const scheduleDetailQuery = useScheduleDetail(scheduleId, "CONTAINER"); + const scheduleDetailQuery = useQuery( + api.trainScheduling.scheduleDetail.queryOptions({ + input: { id: scheduleId ?? "", freightType: "CONTAINER" }, + enabled: Boolean(scheduleId), + }), + ); // Batch bookings by state for the composition side panel (payment / expired lists). const batchBookings = useMemo(() => { @@ -550,7 +560,7 @@ export default function BatchScheduleDetailPage() { const handleRunAllocation = () => { runAllocation - .mutateAsync() + .mutateAsync({ scheduleId: scheduleId ?? "" }) .then((result) => { const failed = result.issues.filter((i) => i.status === "FAILED").length; const deferred = result.deferred.length; diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx index caed00435..7eb995ac2 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx @@ -28,7 +28,8 @@ import { PageContainer } from "@/components/page"; import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack"; import { RouteCorridor, StatusPill } from "@/components/trainScheduling/scheduleVisuals"; import { freightBrand } from "@/theme/freight-brand"; -import { useTrainTrack, useScheduleMutations } from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; const parseError = (error: unknown, fallback: string) => { @@ -81,8 +82,15 @@ function MetaStat({ export default function TrainScheduleTrackPage() { const { scheduleId } = useParams<{ scheduleId: string }>(); const { toast } = useToast(); - const trackQuery = useTrainTrack(scheduleId); - const { recordCheckpoint } = useScheduleMutations(scheduleId); + const trackQuery = useQuery( + api.trainScheduling.trainTrack.queryOptions({ + input: { id: scheduleId ?? "" }, + enabled: Boolean(scheduleId), + }), + ); + const recordCheckpoint = useMutation( + api.trainScheduling.recordCheckpoint.mutationOptions(), + ); if (trackQuery.isLoading) { return ( diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx index e2618510a..8c16b8d14 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx @@ -56,11 +56,8 @@ import { shouldShowContainerPlacementStep } from "@/components/trainScheduling/s import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram"; import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid"; import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep"; -import { - useEligibleBookings, - useScheduleDetail, - useScheduleMutations, -} from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; import type { ContainerPlacement, @@ -91,7 +88,12 @@ export default function TrainScheduleV2DetailPage() { const [maintenanceOpen, setMaintenanceOpen] = useState(false); const autoPreviewedRef = useRef(false); - const detailQuery = useScheduleDetail(scheduleId); + const detailQuery = useQuery( + api.trainScheduling.scheduleDetail.queryOptions({ + input: { id: scheduleId ?? "" }, + enabled: Boolean(scheduleId), + }), + ); const schedule = detailQuery.data; const freightType: FreightType | undefined = schedule?.freightType as FreightType | undefined; @@ -111,12 +113,17 @@ export default function TrainScheduleV2DetailPage() { const eligibleFreightType = freightType === "CONTAINER" || freightType === "BULK" ? freightType : undefined; - const eligibleQuery = useEligibleBookings( - eligibleFilters, - Boolean(schedule), - eligibleFreightType, + const eligibleQuery = useQuery( + api.trainScheduling.eligibleBookings.queryOptions({ + input: { filters: eligibleFilters, freightType: eligibleFreightType }, + enabled: Boolean(schedule), + }), ); - const { preview, assign, unassign, finalize, dispatch } = useScheduleMutations(scheduleId); + const preview = useMutation(api.trainScheduling.preview.mutationOptions()); + const assign = useMutation(api.trainScheduling.assignBookings.mutationOptions()); + const unassign = useMutation(api.trainScheduling.unassignBooking.mutationOptions()); + const finalize = useMutation(api.trainScheduling.finalizeSchedule.mutationOptions()); + const dispatch = useMutation(api.trainScheduling.dispatchSchedule.mutationOptions()); const assignedIds = useMemo( () => (schedule?.bookings ?? []).map((b) => b.id), diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx index d389009aa..193caa0a5 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx @@ -39,13 +39,9 @@ import { RouteCorridor, StatusPill, } from "@/components/trainScheduling/scheduleVisuals"; -import { - useAvailableLocomotives, - useScheduleList, - useScheduleMutations, -} from "@/hooks/trainScheduling/useTrainScheduling"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; -import { useRoutes } from "@/hooks/useRoutes"; import type { TrainScheduleListItem } from "@/types/trainScheduling"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; @@ -88,10 +84,17 @@ export default function TrainScheduleV2ListPage() { const [scheduleDate, setScheduleDate] = useState(""); const [locomotiveId, setLocomotiveId] = useState(""); - const schedulesQuery = useScheduleList(); - const routesQuery = useRoutes(); - const locomotivesQuery = useAvailableLocomotives(routeId || undefined); - const { create, cancel } = useScheduleMutations(); + const schedulesQuery = useQuery( + api.trainScheduling.scheduleList.queryOptions({ input: {} }), + ); + const routesQuery = useQuery(api.routes.list.queryOptions()); + const locomotivesQuery = useQuery( + api.trainScheduling.availableLocomotives.queryOptions({ + input: { routeId: routeId || undefined }, + }), + ); + const create = useMutation(api.trainScheduling.createSchedule.mutationOptions()); + const cancel = useMutation(api.trainScheduling.cancelSchedule.mutationOptions()); const activeRoutes = useMemo( () => (routesQuery.data ?? []).filter((r) => r.isActive), diff --git a/apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx index e6c2534db..9e336fa19 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trains/TrainDetailPage.tsx @@ -2,13 +2,17 @@ import { useParams, Link } from "react-router-dom"; import { ArrowLeft } from "lucide-react"; import { Badge, Button, Card, Group, Loader, SimpleGrid, Stack, Text } from "@mantine/core"; +import { useQuery } from "@tanstack/react-query"; + import { AssignWagonDialog } from "@/components/wagons/AssignWagonDialog"; import { WagonsTable } from "@/components/wagons/WagonsTable"; -import { useTrain } from "@/hooks/useTrains"; +import { api } from "@/services/api"; export default function TrainDetailPage() { const { id } = useParams<{ id: string }>(); - const { data: train, isLoading } = useTrain(id!); + const { data: train, isLoading } = useQuery( + api.trains.getById.queryOptions({ input: { id: id ?? "" }, enabled: !!id }), + ); if (isLoading) { return ( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx index 0b52d1388..317520f48 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx @@ -16,7 +16,9 @@ import { VisualEmptyState, formatDate, } from '@/components/warehouses'; -import { useArrivalQueue, useAutoUnloadArrived, useUnloadBooking } from '@/hooks/useWarehouses'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; import type { ArrivalQueueItem } from '@/types/warehouse'; @@ -30,17 +32,16 @@ function inspectionBadge(status: string | null) { export default function ArrivalQueuePage() { const navigate = useNavigate(); const { toast } = useToast(); - const { data, isLoading } = useArrivalQueue(); - const autoUnload = useAutoUnloadArrived(); - const unloadOne = useUnloadBooking(); + const { data, isLoading } = useQuery(api.warehouses.arrivalQueue.queryOptions()); + const autoUnload = useMutation(api.warehouses.autoUnloadArrived.mutationOptions()); + const unloadOne = useMutation(api.warehouses.unloadBooking.mutationOptions()); const [inspectInventoryId, setInspectInventoryId] = useState(null); const items = data ?? []; const handleAutoUnload = async () => { try { - const res = await autoUnload.mutateAsync(); - const r = res.data; + const r = await autoUnload.mutateAsync(); toast({ title: 'Auto-unload complete', description: `Processed ${r.processedCount}, skipped ${r.skippedCount}, failed ${r.failedCount}.`, diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx index 1776ac8e0..d2786c0b9 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/DispatchQueuePage.tsx @@ -1,12 +1,16 @@ import { Card } from '@mantine/core'; import { PageContainer, PageHeader } from '@/components/page'; +import { useQuery } from '@tanstack/react-query'; + import { InventoryWorkbench, VisualEmptyState } from '@/components/warehouses'; -import { useWarehouseInventory } from '@/hooks/useWarehouses'; +import { api } from '@/services/api'; /** Items that are LOADED and awaiting dispatch (train departure). */ export default function DispatchQueuePage() { - const { data, isLoading } = useWarehouseInventory({ status: 'LOADED' }); + const { data, isLoading } = useQuery( + api.warehouses.listInventory.queryOptions({ input: { filter: { status: 'LOADED' } } }), + ); const items = data ?? []; return ( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx index eeaaa8848..582a9cedd 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/InventoryInquiryPage.tsx @@ -3,24 +3,35 @@ import { Button, Card, Center, Group, Loader, Select, Stack, TextInput } from '@ import { Search } from 'lucide-react'; import { PageContainer, PageHeader } from '@/components/page'; +import { useQuery } from '@tanstack/react-query'; + import { VisualEmptyState, WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses'; -import { - useInventoryInquiry, - useWarehouseYards, - useWarehouseZones, - useWarehouses, -} from '@/hooks/useWarehouses'; +import { api } from '@/services/api'; import type { InventoryInquiryFilter, InventoryStatus } from '@/types/warehouse'; export default function InventoryInquiryPage() { const [draft, setDraft] = useState({}); const [applied, setApplied] = useState({}); - const warehousesQuery = useWarehouses(); - const yardsQuery = useWarehouseYards(draft.warehouseId); - const zonesQuery = useWarehouseZones(draft.yardId); + const warehousesQuery = useQuery( + api.warehouses.list.queryOptions({ input: {} }), + ); + const yardsQuery = useQuery( + api.warehouses.listYards.queryOptions({ + input: { warehouseId: draft.warehouseId ?? '' }, + enabled: Boolean(draft.warehouseId), + }), + ); + const zonesQuery = useQuery( + api.warehouses.listZones.queryOptions({ + input: { yardId: draft.yardId ?? '' }, + enabled: Boolean(draft.yardId), + }), + ); - const { data, isFetching } = useInventoryInquiry(applied); + const { data, isFetching } = useQuery( + api.warehouses.inquiry.queryOptions({ input: { filter: applied } }), + ); const results = data ?? []; const warehouseOptions = useMemo( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx index d5dbd0b10..b5deec80e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadedInventoryPage.tsx @@ -2,10 +2,13 @@ import { Badge, Card, Group, Text } from '@mantine/core'; import { DataTable, type ColumnDef } from '@edr/ui-common'; import { PageContainer, PageHeader } from '@/components/page'; -import { FreightVisual, VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses'; -import { useWarehouseLoadings } from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; -type Loading = NonNullable['data']>[number]; +import { FreightVisual, VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses'; +import { api } from '@/services/api'; +import type { WarehouseLoading } from '@/types/warehouse'; + +type Loading = WarehouseLoading; const columns: ColumnDef[] = [ { @@ -60,7 +63,9 @@ const columns: ColumnDef[] = [ /** Record of every inventory item loaded onto a wagon. */ export default function LoadedInventoryPage() { - const { data, isLoading } = useWarehouseLoadings(); + const { data, isLoading } = useQuery( + api.warehouses.loadings.queryOptions({ input: {} }), + ); const loadings = data ?? []; return ( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx index bb65d8fe6..f80db55f4 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx @@ -10,7 +10,9 @@ import { VisualEmptyState, formatNumber, } from '@/components/warehouses'; -import { useAutoLoadReady, useWarehouseInventory } from '@/hooks/useWarehouses'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; import type { WarehouseInventoryItem } from '@/types/warehouse'; @@ -27,16 +29,19 @@ const isPaid = (item: WarehouseInventoryItem) => item.booking?.status === 'PAID' export default function LoadingQueuePage() { const navigate = useNavigate(); const { toast } = useToast(); - const autoLoad = useAutoLoadReady(); - const { data: readyData, isLoading: readyLoading } = useWarehouseInventory({ - status: 'READY_FOR_LOADING', - }); - const { data: loadedData, isLoading: loadedLoading } = useWarehouseInventory({ status: 'LOADED' }); + const autoLoad = useMutation(api.warehouses.autoLoadReady.mutationOptions()); + const { data: readyData, isLoading: readyLoading } = useQuery( + api.warehouses.listInventory.queryOptions({ + input: { filter: { status: 'READY_FOR_LOADING' } }, + }), + ); + const { data: loadedData, isLoading: loadedLoading } = useQuery( + api.warehouses.listInventory.queryOptions({ input: { filter: { status: 'LOADED' } } }), + ); const handleAutoLoad = async () => { try { - const res = await autoLoad.mutateAsync(); - const r = res.data; + const r = await autoLoad.mutateAsync(); toast({ title: 'Auto-load complete', description: `Loaded ${r.loadedCount} PAID item(s); skipped ${r.skippedCount} (unpaid stay pending).`, diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx index 683baca15..60dbb13c4 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx @@ -16,8 +16,10 @@ import { } from 'lucide-react'; import { PageContainer, PageHeader } from '@/components/page'; +import { useQuery } from '@tanstack/react-query'; + import { WarehouseDashboardCharts } from '@/components/warehouses'; -import { useWarehouseDashboard } from '@/hooks/useWarehouses'; +import { api } from '@/services/api'; import type { WarehouseDashboard } from '@/types/warehouse'; interface Metric { @@ -49,7 +51,7 @@ const METRICS: Metric[] = [ export default function WarehouseDashboardPage() { const navigate = useNavigate(); - const { data, isLoading } = useWarehouseDashboard(); + const { data, isLoading } = useQuery(api.warehouses.dashboard.queryOptions()); return ( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx index 145d3579d..31509766d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDetailPage.tsx @@ -27,20 +27,27 @@ import { formatCapacity, humanizeEnum, } from '@/components/warehouses'; -import { - useWarehouse, - useWarehouseInventory, - useWarehouseYards, - useWarehouseZones, -} from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import type { WarehouseYard, WarehouseZone } from '@/types/warehouse'; export default function WarehouseDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { data: warehouse, isLoading } = useWarehouse(id); - const yardsQuery = useWarehouseYards(id); + const { data: warehouse, isLoading } = useQuery( + api.warehouses.getById.queryOptions({ + input: { id: id ?? '' }, + enabled: Boolean(id), + }), + ); + const yardsQuery = useQuery( + api.warehouses.listYards.queryOptions({ + input: { warehouseId: id ?? '' }, + enabled: Boolean(id), + }), + ); const [yardModalOpen, setYardModalOpen] = useState(false); const [editingYard, setEditingYard] = useState(null); @@ -49,9 +56,18 @@ export default function WarehouseDetailPage() { const [editingZone, setEditingZone] = useState(null); const [selectedYardId, setSelectedYardId] = useState(null); - const zonesQuery = useWarehouseZones(selectedYardId ?? undefined); + const zonesQuery = useQuery( + api.warehouses.listZones.queryOptions({ + input: { yardId: selectedYardId ?? '' }, + enabled: Boolean(selectedYardId), + }), + ); - const inventoryQuery = useWarehouseInventory(id ? { warehouseId: id } : undefined); + const inventoryQuery = useQuery( + api.warehouses.listInventory.queryOptions({ + input: { filter: id ? { warehouseId: id } : undefined }, + }), + ); const yards = yardsQuery.data ?? []; const yardOptions = useMemo( diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx index 234c37826..2ae417db1 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInventoryPage.tsx @@ -10,12 +10,9 @@ import { ReceiveInventoryModal, inventoryStatusOptions, } from '@/components/warehouses'; -import { - useWarehouseInventory, - useWarehouseYards, - useWarehouseZones, - useWarehouses, -} from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import type { InventoryFilter, InventoryStatus } from '@/types/warehouse'; export default function WarehouseInventoryPage() { @@ -33,10 +30,24 @@ export default function WarehouseInventoryPage() { [filter, debouncedSearch], ); - const warehousesQuery = useWarehouses(); - const yardsQuery = useWarehouseYards(filter.warehouseId); - const zonesQuery = useWarehouseZones(filter.yardId); - const inventoryQuery = useWarehouseInventory(queryFilter); + const warehousesQuery = useQuery( + api.warehouses.list.queryOptions({ input: {} }), + ); + const yardsQuery = useQuery( + api.warehouses.listYards.queryOptions({ + input: { warehouseId: filter.warehouseId ?? '' }, + enabled: Boolean(filter.warehouseId), + }), + ); + const zonesQuery = useQuery( + api.warehouses.listZones.queryOptions({ + input: { yardId: filter.yardId ?? '' }, + enabled: Boolean(filter.yardId), + }), + ); + const inventoryQuery = useQuery( + api.warehouses.listInventory.queryOptions({ input: { filter: queryFilter } }), + ); const warehouseOptions = useMemo( () => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })), diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx index 7b1ae4d30..3f0958a7f 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseInvoicesPage.tsx @@ -19,13 +19,10 @@ import { Ban, CreditCard, Eye, Search } from 'lucide-react'; import { DataTable, type ColumnDef } from '@edr/ui-common'; import { PageContainer, PageHeader } from '@/components/page'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { - useCancelInvoice, - usePayInvoice, - useWarehouseInvoice, - useWarehouseInvoices, -} from '@/hooks/useWarehouses'; import { WAREHOUSE_INVOICE_STATUSES, type WarehouseFeeInvoice, @@ -48,7 +45,11 @@ export default function WarehouseInvoicesPage() { const [search, setSearch] = useState(''); const [detailId, setDetailId] = useState(null); - const { data, isLoading } = useWarehouseInvoices(status ? { status } : undefined); + const { data, isLoading } = useQuery( + api.warehouses.invoices.queryOptions({ + input: { filter: status ? { status } : undefined }, + }), + ); const invoices = data ?? []; const filtered = useMemo(() => { @@ -149,9 +150,14 @@ export default function WarehouseInvoicesPage() { function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () => void }) { const { toast } = useToast(); - const { data: inv, isLoading } = useWarehouseInvoice(id ?? undefined); - const pay = usePayInvoice(); - const cancel = useCancelInvoice(); + const { data: inv, isLoading } = useQuery( + api.warehouses.invoice.queryOptions({ + input: { id: id ?? '' }, + enabled: Boolean(id), + }), + ); + const pay = useMutation(api.warehouses.payInvoice.mutationOptions()); + const cancel = useMutation(api.warehouses.cancelInvoice.mutationOptions()); const [payAmount, setPayAmount] = useState(''); const canPay = inv && (inv.status === 'ISSUED' || inv.status === 'PARTIALLY_PAID'); diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx index 3f9df44f6..93fcde493 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseListPage.tsx @@ -12,7 +12,9 @@ import { WarehouseTable, type WarehouseView, } from '@/components/warehouses'; -import { useWarehouses } from '@/hooks/useWarehouses'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import type { Warehouse, WarehouseFilter } from '@/types/warehouse'; export default function WarehouseListPage() { @@ -28,7 +30,9 @@ export default function WarehouseListPage() { [filter, debouncedSearch], ); - const { data, isLoading, isError } = useWarehouses(queryFilter); + const { data, isLoading, isError } = useQuery( + api.warehouses.list.queryOptions({ input: { filter: queryFilter } }), + ); const warehouses = data ?? []; const openCreate = () => { diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx index af5adb83d..81e998385 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseRulesPage.tsx @@ -17,15 +17,10 @@ import { Plus, Trash2 } from 'lucide-react'; import { DataTable, type ColumnDef } from '@edr/ui-common'; import { PageContainer, PageHeader } from '@/components/page'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; -import { - useAllocationRules, - useCreateAllocationRule, - useCreateFeeRule, - useDeleteAllocationRule, - useDeleteFeeRule, - useFeeRules, -} from '@/hooks/useWarehouses'; import { FEE_RULE_TYPES, type FeeRuleType } from '@/types/warehouse'; const FREIGHT = [ @@ -67,9 +62,11 @@ export default function WarehouseRulesPage() { function AllocationRules() { const { toast } = useToast(); - const { data, isLoading } = useAllocationRules(); - const create = useCreateAllocationRule(); - const remove = useDeleteAllocationRule(); + const { data, isLoading } = useQuery( + api.warehouses.allocationRules.queryOptions(), + ); + const create = useMutation(api.warehouses.createAllocationRule.mutationOptions()); + const remove = useMutation(api.warehouses.deleteAllocationRule.mutationOptions()); const [open, setOpen] = useState(false); const [form, setForm] = useState({ name: '', @@ -181,9 +178,9 @@ function AllocationRules() { function FeeRules() { const { toast } = useToast(); - const { data, isLoading } = useFeeRules(); - const create = useCreateFeeRule(); - const remove = useDeleteFeeRule(); + const { data, isLoading } = useQuery(api.warehouses.feeRules.queryOptions()); + const create = useMutation(api.warehouses.createFeeRule.mutationOptions()); + const remove = useMutation(api.warehouses.deleteFeeRule.mutationOptions()); const [open, setOpen] = useState(false); const [form, setForm] = useState({ name: '', diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index 14bf3454b..86d14ef7d 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -1,13 +1,17 @@ import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; -import { endpoint } from "@/utils/endpoint"; +import type { FleetResourceSlug } from "@/pages/fleet/config/resources"; +import type { BookingDetail } from "@/types/booking"; import type { - CreateFileUploadFieldDto, - CreateFileUploadSettingDto, - FileUploadField, - FileUploadSetting, - UpdateFileUploadFieldDto, - UpdateFileUploadSettingDto, -} from "@/types/fileUploadSettings"; + Company, + CompanyListFilter, + CompanyProfile, + CompanyStats, + CustomerBooking, + CustomerDocument, + CustomerPayment, + PaginatedCompanies, + ProfileStatus, +} from "@/types/customer"; import { CreateDropdownOptionDto, CreateDropdownSettingDto, @@ -16,29 +20,1526 @@ import { UpdateDropdownOptionDto, UpdateDropdownSettingDto, } from "@/types/dropdownSettings"; +import type { + CreateFileUploadFieldDto, + CreateFileUploadSettingDto, + FileUploadField, + FileUploadSetting, + UpdateFileUploadFieldDto, + UpdateFileUploadSettingDto, +} from "@/types/fileUploadSettings"; +import type { IOverviewDashboard, OverviewRange } from "@/types/overview"; import { RuleEngineListResult, RuleEngineRecord, RuleEngineResourceSlug, } from "@/types/rule-engine"; -import { fileUploadSettingsService } from "./fileUploadSettings.service"; -import { dropdownSettingsService } from "./dropdownSettings.service"; +import type { + AssignBookingsPayload, + BatchBoardSchedule, + BatchBoardScheduleDetail, + BookableSchedule, + CompositionRemovalEntry, + CreateTrainSchedulePayload, + EligibleContainerBookingsResponse, + FreightType, + LocomotiveRecord, + PinWagonsPayload, + RecordCheckpointPayload, + TrainScheduleDetail, + TrainScheduleFilters, + TrainScheduleListItem, + TrainSchedulePreviewPayload, + TrainSchedulePreviewResponse, + TrainTrackResponse, + UnassignedBookingsResponse, + WagonAllocationAttemptResult, + YardOption, +} from "@/types/trainScheduling"; +import type { + AllocationCriteria, + AllocationPreviewResult, + AllocationRule, + ArrivalQueueItem, + AutoLoadResult, + AutoUnloadArrivedResult, + AutoUnloadResult, + BookingScheduleView, + BulkDispatchResult, + BulkInspectPayload, + BulkInspectResult, + BulkReceivePayload, + BulkReceiveResult, + DeliverInventoryPayload, + EligibleBooking, + FeePreview, + FeeRule, + ImportTrain, + ImportTrainItem, + ImportUnloadedItem, + InspectionAttachment, + InspectionReport, + InspectionReportPayload, + InventoryFilter, + InventoryInquiryFilter, + InventoryInquiryResult, + InventoryMovement, + LoadableWagon, + LoadInventoryPayload, + LoadPassedExportResult, + MoveInventoryPayload, + PayInvoicePayload, + ReadyToLoadRow, + ReceiveInventoryPayload, + ReleaseOrderPayload, + ReserveInventoryPayload, + SaveAllocationRulePayload, + SaveFeeRulePayload, + SaveWarehousePayload, + SaveYardPayload, + SaveZonePayload, + Warehouse, + WarehouseActivityLog, + WarehouseDashboard, + WarehouseFeeInvoice, + WarehouseFilter, + WarehouseInventoryItem, + WarehouseInvoiceFilter, + WarehouseLoading, + WarehouseYard, + WarehouseZone, +} from "@/types/warehouse"; +import { endpoint } from "@/utils/endpoint"; import { - ruleEngineService, - RuleEngineListParams, -} from "./ruleEngine/ruleEngine.service"; -import { - bookingsService, BookingListFilter, + bookingsService, type ApproveStepPayload, type PaginatedBookings, type RejectStepPayload, } from "./bookings.service"; -import type { BookingDetail } from "@/types/booking"; -import type { IOverviewDashboard, OverviewRange } from "@/types/overview"; +import { cargoTypesService } from "./cargo-types.service"; +import { + cargoService, + type Cargo, + type DeliverCargoPayload, +} from "./cargoService"; +import { containerTypesService } from "./container-types.service"; +import { containerService, type Container } from "./containerService"; +import { customersService } from "./customers.service"; +import { dropdownSettingsService } from "./dropdownSettings.service"; +import { fileUploadSettingsService } from "./fileUploadSettings.service"; +import { + fleetService, + type FleetListFilters, + type FleetRecord, +} from "./fleet/fleet.service"; +import { + locomotivesService, + type Locomotive, + type SaveLocomotivePayload, +} from "./locomotives.service"; import { overviewService } from "./overview.service"; +import { + paymentsService, + type PaginatedPayments, + type PaymentListFilter, + type PaymentSummary, +} from "./payments.service"; +import { + routesService, + type RouteRecord, + type SaveRoutePayload, + type YardRef, +} from "./routes.service"; +import { + RuleEngineListParams, + ruleEngineService, +} from "./ruleEngine/ruleEngine.service"; +import { + signaturesService, + type SavedSignature, + type SaveSignaturePayload, +} from "./signatures.service"; +import { trainService, type Train } from "./trains.service"; +import { trainSchedulingService } from "./trainScheduling.service"; +import { wagonTypesService, type WagonType } from "./wagon-types.service"; +import { + wagonService, + type Wagon, + type WagonListFilters, +} from "./wagon.service"; +import { warehouseService } from "./warehouse.service"; + +/** Query keys for inventory-lifecycle mutations that ripple across views. */ +const INVENTORY_INVALIDATIONS: ReadonlyArray = [ + ["warehouse-inventory"], + ["warehouse-loadings"], + ["warehouses"], + // Singular `"warehouse"` root covers loadableWagons / bookingSchedule, which + // change when inventory is loaded/dispatched. Distinct from the `warehouse-*` + // roots above (prefix matching is element-exact, not string-prefix). + ["warehouse"], +]; + +/** + * Train-scheduling mutations broadly affect the schedule board and bookings. + * The grouped hooks invalidated TRAIN_SCHEDULING.ROOT + BOOKINGS.ROOT; since + * every train-scheduling key is prefixed with `"train-scheduling"`, the two + * roots below cover all of them via React Query's prefix matching. + */ +const TRAIN_SCHEDULING_INVALIDATIONS: ReadonlyArray = [ + QUERY_KEYS.TRAIN_SCHEDULING.ROOT, + QUERY_KEYS.BOOKINGS.ROOT, +]; export const api = { + trainScheduling: { + // ── Queries ──────────────────────────────────────────────────────────── + scheduleList: endpoint<{ freightType?: FreightType }, TrainScheduleListItem[]>( + "train-scheduling", + "schedules", + ({ freightType }) => trainSchedulingService.listSchedules(freightType), + () => QUERY_KEYS.TRAIN_SCHEDULING.schedules(), + ), + + batchBoard: endpoint( + "train-scheduling", + "batch-board", + () => trainSchedulingService.getBatchBoard(), + () => QUERY_KEYS.TRAIN_SCHEDULING.batchBoard(), + ), + + batchBoardDetail: endpoint<{ scheduleId: string }, BatchBoardScheduleDetail>( + "train-scheduling", + "batch-board-detail", + ({ scheduleId }) => trainSchedulingService.getBatchBoardDetail(scheduleId), + ({ scheduleId }) => QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(scheduleId), + ), + + scheduleDetail: endpoint< + { id: string; freightType?: FreightType }, + TrainScheduleDetail + >( + "train-scheduling", + "schedule-detail", + ({ id, freightType }) => + trainSchedulingService.getScheduleById(id, freightType), + ({ id }) => QUERY_KEYS.TRAIN_SCHEDULING.scheduleById(id), + ), + + eligibleBookings: endpoint< + { filters?: TrainScheduleFilters; freightType?: FreightType }, + EligibleContainerBookingsResponse + >( + "train-scheduling", + "eligible-bookings", + ({ filters, freightType }) => + trainSchedulingService.getEligibleBookings(filters, freightType), + ({ filters, freightType }) => + QUERY_KEYS.TRAIN_SCHEDULING.eligible(freightType, filters), + ), + + availableLocomotives: endpoint<{ routeId?: string }, LocomotiveRecord[]>( + "train-scheduling", + "locomotives", + ({ routeId }) => trainSchedulingService.getAvailableLocomotives(routeId), + ({ routeId }) => QUERY_KEYS.TRAIN_SCHEDULING.locomotives(routeId), + ), + + bookableSchedules: endpoint< + { originYardId?: string | null; destinationYardId?: string | null }, + BookableSchedule[] + >( + "train-scheduling", + "bookable", + ({ originYardId, destinationYardId }) => + trainSchedulingService.getBookableSchedules( + originYardId ?? undefined, + destinationYardId ?? undefined, + ), + ({ originYardId, destinationYardId }) => [ + ...QUERY_KEYS.TRAIN_SCHEDULING.ROOT, + "bookable", + originYardId ?? "", + destinationYardId ?? "", + ], + ), + + availableDays: endpoint< + { originYardId?: string | null; destinationYardId?: string | null }, + string[] + >( + "train-scheduling", + "available-days", + ({ originYardId, destinationYardId }) => + trainSchedulingService.getAvailableDays( + originYardId ?? undefined, + destinationYardId ?? undefined, + ), + ({ originYardId, destinationYardId }) => [ + ...QUERY_KEYS.TRAIN_SCHEDULING.ROOT, + "available-days", + originYardId ?? "", + destinationYardId ?? "", + ], + ), + + trainTrack: endpoint<{ id: string }, TrainTrackResponse>( + "train-scheduling", + "track", + ({ id }) => trainSchedulingService.getTrack(id), + ({ id }) => QUERY_KEYS.TRAIN_SCHEDULING.track(id), + ), + + unassignedBookings: endpoint< + { scheduleId: string }, + UnassignedBookingsResponse + >( + "train-scheduling", + "unassigned-bookings", + ({ scheduleId }) => + trainSchedulingService.getUnassignedBookings(scheduleId), + ({ scheduleId }) => + QUERY_KEYS.TRAIN_SCHEDULING.unassignedBookings(scheduleId), + ), + + compositionRemovals: endpoint< + { scheduleId: string }, + CompositionRemovalEntry[] + >( + "train-scheduling", + "composition-removals", + ({ scheduleId }) => + trainSchedulingService.getCompositionRemovals(scheduleId), + ({ scheduleId }) => + QUERY_KEYS.TRAIN_SCHEDULING.compositionRemovals(scheduleId), + ), + + // ── Mutations ────────────────────────────────────────────────────────── + runAllocation: endpoint<{ scheduleId: string }, WagonAllocationAttemptResult>( + "train-scheduling", + "run-allocation", + ({ scheduleId }) => trainSchedulingService.runAllocation(scheduleId), + undefined, + () => [QUERY_KEYS.TRAIN_SCHEDULING.ROOT], + ), + + runBatch: endpoint( + "train-scheduling", + "run-batch", + (id) => trainSchedulingService.runBatch(id), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + setBookingWindow: endpoint< + { id: string; status: "OPEN" | "CLOSED" }, + TrainScheduleDetail + >( + "train-scheduling", + "set-booking-window", + ({ id, status }) => trainSchedulingService.setBookingWindow(id, status), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + markBookingPaid: endpoint( + "train-scheduling", + "mark-booking-paid", + (bookingId) => trainSchedulingService.markBookingPaid(bookingId), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + expireBooking: endpoint( + "train-scheduling", + "expire-booking", + (bookingId) => trainSchedulingService.expireBooking(bookingId), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + moveBookingSchedule: endpoint< + { bookingId: string; trainScheduleId: string }, + void + >( + "train-scheduling", + "move-booking-schedule", + ({ bookingId, trainScheduleId }) => + trainSchedulingService.moveBookingSchedule(bookingId, trainScheduleId), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + createSchedule: endpoint< + { freightType?: FreightType; payload: CreateTrainSchedulePayload }, + TrainScheduleDetail + >( + "train-scheduling", + "create-schedule", + ({ freightType, payload }) => + trainSchedulingService.createSchedule(payload, freightType), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + preview: endpoint< + { freightType?: FreightType; payload: TrainSchedulePreviewPayload }, + TrainSchedulePreviewResponse + >("train-scheduling", "preview", ({ freightType, payload }) => + trainSchedulingService.preview(payload, freightType), + ), + + assignBookings: endpoint< + { id: string; freightType?: FreightType; payload: AssignBookingsPayload }, + TrainScheduleDetail + >( + "train-scheduling", + "assign-bookings", + ({ id, freightType, payload }) => + trainSchedulingService.assignBookings(id, payload, freightType), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + assignUnassignedBooking: endpoint< + { id: string; bookingId: string }, + TrainScheduleDetail + >( + "train-scheduling", + "assign-unassigned-booking", + ({ id, bookingId }) => + trainSchedulingService.assignUnassignedBooking(id, bookingId), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + unassignBooking: endpoint< + { id: string; bookingId: string }, + TrainScheduleDetail + >( + "train-scheduling", + "unassign-booking", + ({ id, bookingId }) => + trainSchedulingService.unassignBooking(id, bookingId), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + pinWagons: endpoint<{ id: string; payload: PinWagonsPayload }, TrainScheduleDetail>( + "train-scheduling", + "pin-wagons", + ({ id, payload }) => trainSchedulingService.pinWagons(id, payload), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + finalizeSchedule: endpoint( + "train-scheduling", + "finalize-schedule", + (id) => trainSchedulingService.finalizeSchedule(id), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + dispatchSchedule: endpoint( + "train-scheduling", + "dispatch-schedule", + (id) => trainSchedulingService.dispatchSchedule(id), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + cancelSchedule: endpoint< + { id: string; freightType?: FreightType }, + TrainScheduleDetail + >( + "train-scheduling", + "cancel-schedule", + ({ id, freightType }) => + trainSchedulingService.cancelSchedule(id, freightType ?? "CONTAINER"), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + recordCheckpoint: endpoint< + { id: string; payload: RecordCheckpointPayload }, + TrainTrackResponse + >( + "train-scheduling", + "record-checkpoint", + ({ id, payload }) => trainSchedulingService.recordCheckpoint(id, payload), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + arriveSchedule: endpoint( + "train-scheduling", + "arrive-schedule", + (id) => trainSchedulingService.arriveSchedule(id), + undefined, + () => TRAIN_SCHEDULING_INVALIDATIONS, + ), + + removeWagonSlot: endpoint< + { scheduleId: string; wagonId: string }, + TrainScheduleDetail + >( + "train-scheduling", + "remove-wagon-slot", + ({ scheduleId, wagonId }) => + trainSchedulingService.removeWagonSlot(scheduleId, wagonId), + undefined, + () => [QUERY_KEYS.TRAIN_SCHEDULING.ROOT], + ), + + updateContainerItem: endpoint< + { scheduleId: string; itemId: string; containerNumber: string | null }, + { id: string; containerNumber: string | null } + >( + "train-scheduling", + "update-container-item", + ({ scheduleId, itemId, containerNumber }) => + trainSchedulingService.updateContainerItem(scheduleId, itemId, { + containerNumber, + }), + undefined, + () => [QUERY_KEYS.TRAIN_SCHEDULING.ROOT], + ), + }, + + warehouses: { + // ── Warehouses ───────────────────────────────────────────────────────── + list: endpoint<{ filter?: WarehouseFilter }, Warehouse[]>( + "warehouses", + "list", + ({ filter }) => warehouseService.list(filter).then((r) => r.data), + ), + + getById: endpoint<{ id: string }, Warehouse>( + "warehouses", + "getById", + ({ id }) => warehouseService.getById(id).then((r) => r.data), + ), + + dashboard: endpoint("warehouses", "dashboard", () => + warehouseService.dashboard().then((r) => r.data), + ), + + create: endpoint( + "warehouses", + "create", + (payload) => warehouseService.create(payload).then((r) => r.data), + undefined, + () => [["warehouses"]], + ), + + update: endpoint< + { id: string; payload: Partial }, + Warehouse + >( + "warehouses", + "update", + ({ id, payload }) => + warehouseService.update(id, payload).then((r) => r.data), + undefined, + () => [["warehouses"]], + ), + + // ── Yards ────────────────────────────────────────────────────────────── + listYards: endpoint<{ warehouseId: string }, WarehouseYard[]>( + "warehouses", + "listYards", + ({ warehouseId }) => + warehouseService.listYards(warehouseId).then((r) => r.data), + ), + + createYard: endpoint< + { warehouseId: string; payload: SaveYardPayload }, + WarehouseYard + >( + "warehouses", + "createYard", + ({ warehouseId, payload }) => + warehouseService.createYard(warehouseId, payload).then((r) => r.data), + undefined, + () => [["warehouses"]], + ), + + updateYard: endpoint< + { id: string; payload: Partial }, + WarehouseYard + >( + "warehouses", + "updateYard", + ({ id, payload }) => + warehouseService.updateYard(id, payload).then((r) => r.data), + undefined, + () => [["warehouses"], ["warehouse-yards"]], + ), + + // ── Zones ────────────────────────────────────────────────────────────── + listZones: endpoint<{ yardId: string }, WarehouseZone[]>( + "warehouses", + "listZones", + ({ yardId }) => warehouseService.listZones(yardId).then((r) => r.data), + ({ yardId }) => ["warehouse-yards", yardId, "zones"], + ), + + createZone: endpoint< + { yardId: string; payload: SaveZonePayload }, + WarehouseZone + >( + "warehouses", + "createZone", + ({ yardId, payload }) => + warehouseService.createZone(yardId, payload).then((r) => r.data), + undefined, + ({ yardId }) => [["warehouse-yards", yardId, "zones"]], + ), + + updateZone: endpoint< + { id: string; payload: Partial }, + WarehouseZone + >( + "warehouses", + "updateZone", + ({ id, payload }) => + warehouseService.updateZone(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-yards"]], + ), + + // ── Inventory (queries) ──────────────────────────────────────────────── + listInventory: endpoint< + { filter?: InventoryFilter }, + WarehouseInventoryItem[] + >( + "warehouse-inventory", + "list", + ({ filter }) => warehouseService.listInventory(filter).then((r) => r.data), + ), + + inquiry: endpoint< + { filter: InventoryInquiryFilter }, + InventoryInquiryResult[] + >( + "warehouse-inventory", + "inquiry", + ({ filter }) => warehouseService.inquiry(filter).then((r) => r.data), + ({ filter }) => ["warehouse-inventory", "inquiry", filter], + ), + + eligibleBookings: endpoint( + "warehouse-inventory", + "eligible-bookings", + () => warehouseService.eligibleBookings().then((r) => r.data), + () => ["warehouse-inventory", "eligible-bookings"], + ), + + readyToLoadExport: endpoint( + "warehouse-inventory", + "ready-to-load-export", + () => warehouseService.readyToLoadExport().then((r) => r.data), + () => ["warehouse-inventory", "ready-to-load-export"], + ), + + loadedExport: endpoint( + "warehouse-inventory", + "loaded-export", + () => warehouseService.loadedExport().then((r) => r.data), + () => ["warehouse-inventory", "loaded-export"], + ), + + importArriveQueue: endpoint( + "warehouse-inventory", + "import-arrive-queue", + () => warehouseService.importArriveQueue().then((r) => r.data), + () => ["warehouse-inventory", "import-arrive-queue"], + ), + + importTrainItems: endpoint<{ scheduleId: string }, ImportTrainItem[]>( + "warehouse-inventory", + "import-train-items", + ({ scheduleId }) => + warehouseService.importTrainItems(scheduleId).then((r) => r.data), + ({ scheduleId }) => ["warehouse-inventory", "import-train-items", scheduleId], + ), + + importUnloadedQueue: endpoint( + "warehouse-inventory", + "import-unloaded-queue", + () => warehouseService.importUnloadedQueue().then((r) => r.data), + () => ["warehouse-inventory", "import-unloaded-queue"], + ), + + importPickupReadyQueue: endpoint( + "warehouse-inventory", + "import-pickup-ready-queue", + () => warehouseService.importPickupReadyQueue().then((r) => r.data), + () => ["warehouse-inventory", "import-pickup-ready-queue"], + ), + + loadableWagons: endpoint( + "warehouse", + "loadable-wagons", + () => warehouseService.loadableWagons().then((r) => r.data), + () => ["warehouse", "loadable-wagons"], + ), + + loadings: endpoint< + { params?: { bookingId?: string; wagonId?: string } }, + WarehouseLoading[] + >( + "warehouse-loadings", + "list", + ({ params }) => warehouseService.loadings(params).then((r) => r.data), + ({ params }) => ["warehouse-loadings", params ?? {}], + ), + + bookingSchedule: endpoint<{ bookingId: string }, BookingScheduleView>( + "warehouse", + "booking-schedule", + ({ bookingId }) => + warehouseService.bookingSchedule(bookingId).then((r) => r.data), + ({ bookingId }) => ["warehouse", "booking-schedule", bookingId], + ), + + movements: endpoint<{ id: string }, InventoryMovement[]>( + "warehouse-inventory", + "movements", + ({ id }) => warehouseService.movements(id).then((r) => r.data), + ({ id }) => ["warehouse-inventory", id, "movements"], + ), + + activity: endpoint<{ id: string }, WarehouseActivityLog[]>( + "warehouse-inventory", + "activity", + ({ id }) => warehouseService.activity(id).then((r) => r.data), + ({ id }) => ["warehouse-inventory", id, "activity"], + ), + + arrivalQueue: endpoint( + "warehouse-inventory", + "arrival-queue", + () => warehouseService.arrivalQueue().then((r) => r.data), + () => ["warehouse-inventory", "arrival-queue"], + ), + + inspectionReports: endpoint<{ inventoryId: string }, InspectionReport[]>( + "warehouse-inventory", + "inspection-reports", + ({ inventoryId }) => + warehouseService.listInspectionReports(inventoryId).then((r) => r.data), + ({ inventoryId }) => + ["warehouse-inventory", inventoryId, "inspection-reports"], + ), + + allocationRules: endpoint( + "warehouse-allocation-rules", + "list", + () => warehouseService.listAllocationRules().then((r) => r.data), + () => ["warehouse-allocation-rules"], + ), + + feeRules: endpoint( + "warehouse-fee-rules", + "list", + () => warehouseService.listFeeRules().then((r) => r.data), + () => ["warehouse-fee-rules"], + ), + + feePreview: endpoint<{ inventoryId: string }, FeePreview[]>( + "warehouse-inventory", + "fee-preview", + ({ inventoryId }) => + warehouseService.feePreview(inventoryId).then((r) => r.data), + ({ inventoryId }) => ["warehouse-inventory", inventoryId, "fee-preview"], + ), + + invoices: endpoint<{ filter?: WarehouseInvoiceFilter }, WarehouseFeeInvoice[]>( + "warehouse-fee-invoices", + "list", + ({ filter }) => warehouseService.listInvoices(filter).then((r) => r.data), + ({ filter }) => ["warehouse-fee-invoices", filter ?? {}], + ), + + invoice: endpoint<{ id: string }, WarehouseFeeInvoice>( + "warehouse-fee-invoices", + "detail", + ({ id }) => warehouseService.getInvoice(id).then((r) => r.data), + ({ id }) => ["warehouse-fee-invoices", "detail", id], + ), + + invoicesForInventory: endpoint< + { inventoryId: string }, + WarehouseFeeInvoice[] + >( + "warehouse-inventory", + "fee-invoices", + ({ inventoryId }) => + warehouseService.invoicesForInventory(inventoryId).then((r) => r.data), + ({ inventoryId }) => ["warehouse-inventory", inventoryId, "fee-invoices"], + ), + + // ── Inventory (mutations) ────────────────────────────────────────────── + receiveInventory: endpoint( + "warehouse-inventory", + "receive", + (payload) => warehouseService.receiveInventory(payload).then((r) => r.data), + undefined, + () => [["warehouse-inventory"], ["warehouses"]], + ), + + store: endpoint( + "warehouse-inventory", + "store", + (id) => warehouseService.store(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + reserve: endpoint( + "warehouse-inventory", + "reserve", + (payload) => warehouseService.reserve(payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + markReadyForLoading: endpoint( + "warehouse-inventory", + "mark-ready-for-loading", + (id) => warehouseService.markReadyForLoading(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + load: endpoint< + { id: string; payload: LoadInventoryPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "load", + ({ id, payload }) => warehouseService.load(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + dispatch: endpoint( + "warehouse-inventory", + "dispatch", + (id) => warehouseService.dispatch(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + move: endpoint< + { id: string; payload: MoveInventoryPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "move", + ({ id, payload }) => warehouseService.move(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + markReadyForPickup: endpoint( + "warehouse-inventory", + "mark-ready-for-pickup", + (id) => warehouseService.markReadyForPickup(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + release: endpoint< + { id: string; payload: ReleaseOrderPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "release", + ({ id, payload }) => + warehouseService.release(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + deliver: endpoint< + { id: string; payload: DeliverInventoryPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "deliver", + ({ id, payload }) => + warehouseService.deliver(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + bulkReceive: endpoint( + "warehouse-inventory", + "bulk-receive", + (payload) => warehouseService.receiveBulk(payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + loadPassedExport: endpoint( + "warehouse-inventory", + "load-passed-export", + () => warehouseService.loadPassedExport().then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + bulkMarkInspected: endpoint( + "warehouse-inventory", + "bulk-mark-inspected", + (payload) => warehouseService.bulkMarkInspected(payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + bulkDispatchExport: endpoint( + "warehouse-inventory", + "bulk-dispatch-export", + (inventoryIds) => + warehouseService.bulkDispatchExport(inventoryIds).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + autoUnloadArrivedBookings: endpoint( + "warehouse-inventory", + "auto-unload-arrived-bookings", + (scheduleId) => + warehouseService + .autoUnloadArrivedBookings(scheduleId) + .then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + autoUnloadArrived: endpoint( + "warehouse-inventory", + "auto-unload-arrived", + () => warehouseService.autoUnloadArrived().then((r) => r.data), + undefined, + () => [["warehouse-inventory"], ["warehouses"]], + ), + + autoLoadReady: endpoint( + "warehouse-inventory", + "auto-load-ready", + () => warehouseService.autoLoadReady().then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + unloadBooking: endpoint< + { bookingId: string; payload?: Record }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "unload-booking", + ({ bookingId, payload }) => + warehouseService.unloadBooking(bookingId, payload).then((r) => r.data), + undefined, + () => [["warehouse-inventory"], ["warehouses"]], + ), + + createInspectionReport: endpoint< + { inventoryId: string; payload: InspectionReportPayload }, + InspectionReport + >( + "warehouse-inventory", + "create-inspection-report", + ({ inventoryId, payload }) => + warehouseService + .createInspectionReport(inventoryId, payload) + .then((r) => r.data), + undefined, + ({ inventoryId }) => [ + ["warehouse-inventory", inventoryId, "inspection-reports"], + ["warehouse-inventory"], + ], + ), + + uploadInspectionAttachments: endpoint< + { reportId: string; files: File[] }, + InspectionAttachment[] + >( + "warehouse-inventory", + "upload-inspection-attachments", + ({ reportId, files }) => + warehouseService + .uploadInspectionAttachments(reportId, files) + .then((r) => r.data), + ), + + // ── Allocation + fee rules ───────────────────────────────────────────── + previewAllocation: endpoint( + "warehouse-allocation-rules", + "preview", + (criteria) => + warehouseService.previewAllocation(criteria).then((r) => r.data), + ), + + createAllocationRule: endpoint( + "warehouse-allocation-rules", + "create", + (payload) => + warehouseService.createAllocationRule(payload).then((r) => r.data), + undefined, + () => [["warehouse-allocation-rules"]], + ), + + updateAllocationRule: endpoint< + { id: string; payload: Partial }, + AllocationRule + >( + "warehouse-allocation-rules", + "update", + ({ id, payload }) => + warehouseService.updateAllocationRule(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-allocation-rules"]], + ), + + deleteAllocationRule: endpoint( + "warehouse-allocation-rules", + "delete", + (id) => warehouseService.deleteAllocationRule(id).then(() => undefined), + undefined, + () => [["warehouse-allocation-rules"]], + ), + + createFeeRule: endpoint( + "warehouse-fee-rules", + "create", + (payload) => warehouseService.createFeeRule(payload).then((r) => r.data), + undefined, + () => [["warehouse-fee-rules"]], + ), + + updateFeeRule: endpoint< + { id: string; payload: Partial }, + FeeRule + >( + "warehouse-fee-rules", + "update", + ({ id, payload }) => + warehouseService.updateFeeRule(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-fee-rules"]], + ), + + deleteFeeRule: endpoint( + "warehouse-fee-rules", + "delete", + (id) => warehouseService.deleteFeeRule(id).then(() => undefined), + undefined, + () => [["warehouse-fee-rules"]], + ), + + // ── Invoices ─────────────────────────────────────────────────────────── + generateInvoice: endpoint< + { inventoryId: string; confirmZero?: boolean }, + WarehouseFeeInvoice + >( + "warehouse-fee-invoices", + "generate", + ({ inventoryId, confirmZero }) => + warehouseService + .generateInvoice(inventoryId, confirmZero) + .then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + + cancelInvoice: endpoint( + "warehouse-fee-invoices", + "cancel", + (id) => warehouseService.cancelInvoice(id).then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + + payInvoice: endpoint< + { id: string; payload: PayInvoicePayload }, + WarehouseFeeInvoice + >( + "warehouse-fee-invoices", + "pay", + ({ id, payload }) => + warehouseService.payInvoice(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + + gateClearance: endpoint( + "warehouse-fee-invoices", + "gate-clearance", + (inventoryId) => + warehouseService.gateClearance(inventoryId).then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + }, + + routes: { + list: endpoint("routes", "list", () => + routesService.getAll().then((r) => r.data), + ), + + yards: endpoint( + "routes", + "yards", + () => routesService.getYards().then((r) => r.data.data), + () => ["routes", "yards"], + ), + + create: endpoint( + "routes", + "create", + (payload) => routesService.create(payload).then((r) => r.data), + undefined, + () => [["routes"]], + ), + + update: endpoint<{ id: string; data: Partial }, RouteRecord>( + "routes", + "update", + ({ id, data }) => routesService.update(id, data).then((r) => r.data), + undefined, + () => [["routes"]], + ), + + deactivate: endpoint( + "routes", + "deactivate", + (id) => routesService.deactivate(id).then(() => undefined), + undefined, + () => [["routes"]], + ), + }, + + stations: { + list: endpoint( + "train-scheduling", + "stations", + () => trainSchedulingService.getStations(), + () => QUERY_KEYS.TRAIN_SCHEDULING.stations(), + ), + }, + + containers: { + list: endpoint("containers", "list", () => + containerService.getAll().then((r) => r.data), + ), + + listByWagon: endpoint<{ wagonId: string }, Container[]>( + "containers", + "listByWagon", + ({ wagonId }) => containerService.getByWagon(wagonId).then((r) => r.data), + ({ wagonId }) => ["containers", "wagon", wagonId], + ), + + getById: endpoint<{ id: string }, Container>( + "containers", + "getById", + ({ id }) => containerService.getById(id).then((r) => r.data), + ), + + create: endpoint, Container>( + "containers", + "create", + (payload) => containerService.create(payload).then((r) => r.data), + undefined, + () => [["containers"]], + ), + + update: endpoint<{ id: string; data: Partial }, Container>( + "containers", + "update", + ({ id, data }) => containerService.update(id, data).then((r) => r.data), + undefined, + () => [["containers"]], + ), + + remove: endpoint( + "containers", + "remove", + (id) => containerService.delete(id).then(() => undefined), + undefined, + () => [["containers"]], + ), + + assignToWagon: endpoint< + { containerId: string; wagonId: string; position?: number }, + Container + >( + "containers", + "assignToWagon", + ({ containerId, wagonId, position }) => + containerService + .assignToWagon(containerId, wagonId, position) + .then((r) => r.data), + undefined, + () => [["containers"]], + ), + + unassign: endpoint( + "containers", + "unassign", + (containerId) => + containerService.unassign(containerId).then(() => undefined), + undefined, + () => [["containers"]], + ), + }, + + containerTypes: { + list: endpoint("container-types", "list", () => + containerTypesService.getContainerTypes(), + ), + }, + + wagons: { + list: endpoint<{ filters?: WagonListFilters }, Wagon[]>( + "wagons", + "list", + ({ filters }) => wagonService.getAll(filters ?? {}).then((r) => r.data), + ({ filters }) => ["wagons", "list", filters ?? {}], + ), + + listByTrain: endpoint<{ trainId: string }, Wagon[]>( + "wagons", + "listByTrain", + ({ trainId }) => wagonService.getByTrain(trainId).then((r) => r.data), + ({ trainId }) => ["wagons", "train", trainId], + ), + + getById: endpoint<{ id: string }, Wagon>( + "wagons", + "getById", + ({ id }) => wagonService.getById(id).then((r) => r.data), + ), + + assignToTrain: endpoint< + { wagonId: string; trainId: string; sequenceNumber?: number }, + Wagon + >( + "wagons", + "assignToTrain", + ({ wagonId, trainId, sequenceNumber }) => + wagonService + .assignToTrain(wagonId, trainId, sequenceNumber) + .then((r) => r.data), + undefined, + () => [["wagons"]], + ), + + unassign: endpoint( + "wagons", + "unassign", + (wagonId) => wagonService.unassign(wagonId).then(() => undefined), + undefined, + () => [["wagons"]], + ), + + reorder: endpoint<{ trainId: string; wagonIds: string[] }, Wagon[]>( + "wagons", + "reorder", + ({ trainId, wagonIds }) => + wagonService.reorder(trainId, wagonIds).then((r) => r.data), + undefined, + () => [["wagons"]], + ), + + create: endpoint, Wagon>( + "wagons", + "create", + (payload) => wagonService.create(payload).then((r) => r.data), + undefined, + () => [["wagons"]], + ), + + update: endpoint<{ id: string; data: Partial }, Wagon>( + "wagons", + "update", + ({ id, data }) => wagonService.update(id, data).then((r) => r.data), + undefined, + () => [["wagons"]], + ), + + remove: endpoint( + "wagons", + "remove", + (id) => wagonService.delete(id).then(() => undefined), + undefined, + () => [["wagons"]], + ), + }, + + trains: { + list: endpoint( + "trains", + "list", + () => trainService.getAll().then((r) => r.data), + () => ["trains", "list"], + ), + + getById: endpoint<{ id: string }, Train>( + "trains", + "getById", + ({ id }) => trainService.getById(id).then((r) => r.data), + ({ id }) => ["trains", "detail", id], + ), + + create: endpoint, Train>( + "trains", + "create", + (payload) => trainService.create(payload).then((r) => r.data), + undefined, + () => [["trains"]], + ), + + update: endpoint<{ id: string; data: Partial }, Train>( + "trains", + "update", + ({ id, data }) => trainService.update(id, data).then((r) => r.data), + undefined, + () => [["trains"]], + ), + + remove: endpoint( + "trains", + "remove", + (id) => trainService.delete(id).then(() => undefined), + undefined, + () => [["trains"]], + ), + }, + + locomotives: { + list: endpoint( + "locomotives", + "list", + () => locomotivesService.getAll().then((r) => r.data), + () => ["locomotives"], + ), + + create: endpoint, Locomotive>( + "locomotives", + "create", + (payload) => locomotivesService.create(payload).then((r) => r.data), + undefined, + () => [["locomotives"]], + ), + + update: endpoint< + { id: string; data: Partial }, + Locomotive + >( + "locomotives", + "update", + ({ id, data }) => locomotivesService.update(id, data).then((r) => r.data), + undefined, + () => [["locomotives"]], + ), + + decommission: endpoint( + "locomotives", + "decommission", + (id) => locomotivesService.decommission(id).then(() => undefined), + undefined, + () => [["locomotives"]], + ), + }, + + cargoTypes: { + list: endpoint("cargo-types", "list", () => + cargoTypesService.getCargoTypes(), + ), + }, + + payments: { + list: endpoint<{ filter?: PaymentListFilter }, PaginatedPayments>( + "payments", + "list", + ({ filter }) => paymentsService.list(filter), + ({ filter }) => ["payments", "list", filter ?? {}], + ), + + summary: endpoint( + "payments", + "summary", + () => paymentsService.getSummary(), + () => ["payments", "summary"], + ), + }, + + signatures: { + mySignature: endpoint( + "me", + "signature", + () => signaturesService.getMySignature(), + () => ["me", "signature"], + ), + + save: endpoint( + "me", + "save-signature", + (payload) => signaturesService.saveMySignature(payload), + undefined, + () => [["me", "signature"]], + ), + }, + + fleet: { + list: endpoint< + { slug: FleetResourceSlug; filters?: FleetListFilters }, + FleetRecord[] + >( + "fleet", + "list", + ({ slug, filters }) => fleetService.list(slug, filters), + ({ slug, filters }) => [...QUERY_KEYS.FLEET.list(slug), filters ?? {}], + ), + + create: endpoint< + { slug: FleetResourceSlug; data: Record }, + unknown + >( + "fleet", + "create", + ({ slug, data }) => fleetService.create(slug, data), + undefined, + ({ slug }) => [QUERY_KEYS.FLEET.list(slug)], + ), + + update: endpoint< + { slug: FleetResourceSlug; id: string; data: Record }, + unknown + >( + "fleet", + "update", + ({ slug, id, data }) => fleetService.update(slug, id, data), + undefined, + ({ slug }) => [QUERY_KEYS.FLEET.list(slug)], + ), + + remove: endpoint<{ slug: FleetResourceSlug; id: string }, unknown>( + "fleet", + "remove", + ({ slug, id }) => fleetService.remove(slug, id), + undefined, + ({ slug }) => [QUERY_KEYS.FLEET.list(slug)], + ), + }, + + wagonTypes: { + list: endpoint("wagon-types", "list", () => + wagonTypesService.getWagonTypes(), + ), + + create: endpoint, WagonType>( + "wagon-types", + "create", + (payload) => wagonTypesService.create(payload).then((r) => r.data), + undefined, + () => [["wagon-types"]], + ), + + update: endpoint<{ id: string; data: Partial }, WagonType>( + "wagon-types", + "update", + ({ id, data }) => wagonTypesService.update(id, data).then((r) => r.data), + undefined, + () => [["wagon-types"]], + ), + + remove: endpoint( + "wagon-types", + "remove", + (id) => wagonTypesService.delete(id).then(() => undefined), + undefined, + () => [["wagon-types"]], + ), + }, + + cargoes: { + list: endpoint("cargoes", "list", () => + cargoService.getAll().then((r) => r.data), + ), + + listByContainer: endpoint<{ containerId: string }, Cargo[]>( + "cargoes", + "listByContainer", + ({ containerId }) => + cargoService.getByContainer(containerId).then((r) => r.data), + ), + + getById: endpoint<{ id: string }, Cargo>("cargoes", "getById", ({ id }) => + cargoService.getById(id).then((r) => r.data), + ), + + create: endpoint, Cargo>( + "cargoes", + "create", + (payload) => cargoService.create(payload).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + update: endpoint<{ id: string; data: Partial }, Cargo>( + "cargoes", + "update", + ({ id, data }) => cargoService.update(id, data).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + remove: endpoint( + "cargoes", + "remove", + (id) => cargoService.delete(id).then(() => undefined), + undefined, + () => [["cargoes"]], + ), + + load: endpoint< + { id: string; quantity: number; weight: number; volume?: number }, + Cargo + >( + "cargoes", + "load", + ({ id, quantity, weight, volume }) => + cargoService.load(id, quantity, weight, volume).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + deliver: endpoint<{ id: string; payload?: DeliverCargoPayload }, Cargo>( + "cargoes", + "deliver", + ({ id, payload }) => + cargoService.deliver(id, payload).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + unload: endpoint<{ id: string }, Cargo>( + "cargoes", + "unload", + ({ id }) => cargoService.unload(id).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + }, + fileUploadSettings: { list: endpoint( "file-upload-settings", @@ -62,46 +1563,68 @@ export const api = { "file-upload-settings", "create", (payload) => fileUploadSettingsService.create(payload), + undefined, + () => [["file-upload-settings"]], ), update: endpoint< { id: string; dto: UpdateFileUploadSettingDto }, FileUploadSetting - >("file-upload-settings", "update", ({ id, dto }) => - fileUploadSettingsService.update(id, dto), + >( + "file-upload-settings", + "update", + ({ id, dto }) => fileUploadSettingsService.update(id, dto), + undefined, + () => [["file-upload-settings"]], ), remove: endpoint<{ id: string }, void>( "file-upload-settings", "remove", ({ id }) => fileUploadSettingsService.remove(id), + undefined, + () => [["file-upload-settings"]], ), replaceFields: endpoint< { id: string; fields: CreateFileUploadFieldDto[] }, FileUploadField[] - >("file-upload-settings", "replaceFields", ({ id, fields }) => - fileUploadSettingsService.replaceFields(id, fields), + >( + "file-upload-settings", + "replaceFields", + ({ id, fields }) => fileUploadSettingsService.replaceFields(id, fields), + undefined, + () => [["file-upload-settings"]], ), addField: endpoint< { settingId: string; dto: CreateFileUploadFieldDto }, FileUploadField - >("file-upload-settings", "addField", ({ settingId, dto }) => - fileUploadSettingsService.addField(settingId, dto), + >( + "file-upload-settings", + "addField", + ({ settingId, dto }) => fileUploadSettingsService.addField(settingId, dto), + undefined, + () => [["file-upload-settings"]], ), updateField: endpoint< { fieldId: string; dto: UpdateFileUploadFieldDto }, FileUploadField - >("file-upload-settings", "updateField", ({ fieldId, dto }) => - fileUploadSettingsService.updateField(fieldId, dto), + >( + "file-upload-settings", + "updateField", + ({ fieldId, dto }) => fileUploadSettingsService.updateField(fieldId, dto), + undefined, + () => [["file-upload-settings"]], ), removeField: endpoint<{ fieldId: string }, void>( "file-upload-settings", "removeField", ({ fieldId }) => fileUploadSettingsService.removeField(fieldId), + undefined, + () => [["file-upload-settings"]], ), }, @@ -128,46 +1651,68 @@ export const api = { "dropdown-settings", "create", (payload) => dropdownSettingsService.create(payload), + undefined, + () => [["dropdown-settings"]], ), update: endpoint< { id: string; dto: UpdateDropdownSettingDto }, DropdownSetting - >("dropdown-settings", "update", ({ id, dto }) => - dropdownSettingsService.update(id, dto), + >( + "dropdown-settings", + "update", + ({ id, dto }) => dropdownSettingsService.update(id, dto), + undefined, + () => [["dropdown-settings"]], ), remove: endpoint<{ id: string }, void>( "dropdown-settings", "remove", ({ id }) => dropdownSettingsService.remove(id), + undefined, + () => [["dropdown-settings"]], ), replaceOptions: endpoint< { id: string; options: CreateDropdownOptionDto[] }, DropdownOption[] - >("dropdown-settings", "replaceOptions", ({ id, options }) => - dropdownSettingsService.replaceOptions(id, options), + >( + "dropdown-settings", + "replaceOptions", + ({ id, options }) => dropdownSettingsService.replaceOptions(id, options), + undefined, + () => [["dropdown-settings"]], ), addOption: endpoint< { id: string; dto: CreateDropdownOptionDto }, DropdownOption - >("dropdown-settings", "addOption", ({ id, dto }) => - dropdownSettingsService.addOption(id, dto), + >( + "dropdown-settings", + "addOption", + ({ id, dto }) => dropdownSettingsService.addOption(id, dto), + undefined, + () => [["dropdown-settings"]], ), updateOption: endpoint< { optionId: string; dto: UpdateDropdownOptionDto }, DropdownOption - >("dropdown-settings", "updateOption", ({ optionId, dto }) => - dropdownSettingsService.updateOption(optionId, dto), + >( + "dropdown-settings", + "updateOption", + ({ optionId, dto }) => dropdownSettingsService.updateOption(optionId, dto), + undefined, + () => [["dropdown-settings"]], ), removeOption: endpoint<{ optionId: string }, void>( "dropdown-settings", "removeOption", ({ optionId }) => dropdownSettingsService.removeOption(optionId), + undefined, + () => [["dropdown-settings"]], ), }, @@ -346,6 +1891,64 @@ export const api = { ), }, + customers: { + stats: endpoint, CompanyStats>( + "customers", + "stats", + () => customersService.stats(), + () => QUERY_KEYS.CUSTOMERS.stats, + ), + + list: endpoint<{ filter: CompanyListFilter }, PaginatedCompanies>( + "customers", + "list", + ({ filter }) => customersService.list(filter), + ({ filter }) => QUERY_KEYS.CUSTOMERS.list(filter), + ), + + getById: endpoint<{ id: string }, Company | undefined>( + "customers", + "getById", + ({ id }) => customersService.getById(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.byId(id), + ), + + bookings: endpoint<{ id: string }, CustomerBooking[]>( + "customers", + "bookings", + ({ id }) => customersService.bookingsFor(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.bookings(id), + ), + + documents: endpoint<{ id: string }, CustomerDocument[]>( + "customers", + "documents", + ({ id }) => customersService.documentsFor(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.documents(id), + ), + + payments: endpoint<{ id: string }, CustomerPayment[]>( + "customers", + "payments", + ({ id }) => customersService.paymentsFor(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.payments(id), + ), + + setProfileStatus: endpoint< + { profileId: string; status: ProfileStatus }, + CompanyProfile + >( + "customers", + "setProfileStatus", + ({ profileId, status }) => customersService.setProfileStatus(profileId, status), + undefined, + (_input, data) => [ + QUERY_KEYS.CUSTOMERS.byId(data.companyId), + QUERY_KEYS.CUSTOMERS.ROOT, + ], + ), + }, + overview: { get: endpoint<{ range?: OverviewRange }, IOverviewDashboard>( "overview", @@ -353,4 +1956,4 @@ export const api = { ({ range }) => overviewService.getDashboard(range), ), }, -}; +}; \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/services/customers.service.ts b/apps/edr-freight-web/backoffice/src/services/customers.service.ts new file mode 100644 index 000000000..d375ad7a7 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/customers.service.ts @@ -0,0 +1,90 @@ +import { api as apiClient } from "@/auth/http"; +import { URL_CONSTANTS } from "@/constants/URLS"; +import type { + Company, + CompanyListFilter, + CompanyProfile, + CompanyStats, + CustomerBooking, + CustomerDocument, + CustomerPayment, + PaginatedCompanies, + ProfileStatus, +} from "@/types/customer"; + +const cleanParams = (params: object) => + Object.fromEntries( + Object.entries(params).filter( + ([, value]) => value !== undefined && value !== "" && value !== null, + ), + ); + +/** Lift attributes JSONB into the flat contact/manager fields the UI reads. */ +function mapCompany(dto: Record): Company { + const attrs = (dto.attributes as Record | null) ?? {}; + return { + ...(dto as unknown as Company), + contactPersonName: (attrs.contactPersonName as string | null) ?? null, + contactPersonPhone: (attrs.contactPersonPhone as string | null) ?? null, + generalManagerName: (attrs.generalManagerName as string | null) ?? null, + generalManagerEmail: (attrs.generalManagerEmail as string | null) ?? null, + generalManagerPhone: (attrs.generalManagerPhone as string | null) ?? null, + }; +} + +export const customersService = { + stats(): Promise { + return apiClient + .get(URL_CONSTANTS.COMPANIES.STATS) + .then((r) => r.data); + }, + + list(filter: CompanyListFilter): Promise { + return apiClient + .get<{ items: Record[]; total: number }>( + URL_CONSTANTS.COMPANIES.BASE, + { params: cleanParams(filter) }, + ) + .then((r) => ({ + items: r.data.items.map(mapCompany), + total: r.data.total, + })); + }, + + getById(id: string): Promise { + return apiClient + .get>(URL_CONSTANTS.COMPANIES.BY_ID(id)) + .then((r) => mapCompany(r.data)); + }, + + bookingsFor(companyId: string): Promise { + return apiClient + .get( + URL_CONSTANTS.COMPANIES.BOOKINGS_CUSTOMER_VIEW(companyId), + ) + .then((r) => r.data); + }, + + documentsFor(companyId: string): Promise { + return apiClient + .get(URL_CONSTANTS.COMPANIES.DOCUMENTS(companyId)) + .then((r) => r.data); + }, + + paymentsFor(companyId: string): Promise { + return apiClient + .get( + URL_CONSTANTS.COMPANIES.PAYMENTS_CUSTOMER_VIEW(companyId), + ) + .then((r) => r.data); + }, + + setProfileStatus(profileId: string, status: ProfileStatus): Promise { + return apiClient + .patch( + URL_CONSTANTS.COMPANIES.PROFILE_STATUS(profileId), + { status }, + ) + .then((r) => r.data); + }, +}; diff --git a/apps/edr-freight-web/backoffice/src/types/customer.ts b/apps/edr-freight-web/backoffice/src/types/customer.ts new file mode 100644 index 000000000..0cdccc10b --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/types/customer.ts @@ -0,0 +1,163 @@ +/** + * Customer-management types for the freight backoffice. + * + * These mirror the backend `Company` / `CompanyProfile` entities + * (apps/edr-freight-api/src/modules/companies/entities) plus a few lightweight + * shapes for the related data shown on the detail page (bookings / documents / + * payments). The UI is currently driven by mock data, but the shapes match the + * API so the data layer can be swapped to live endpoints with no UI changes. + */ + +/** Mirrors backend `CompanyType`. */ +export type CompanyType = + | "customer" + | "freight_forwarder" + | "dj_freight_forwarder" + | "transporter"; + +/** Mirrors backend `CompanyStatus`. */ +export type CompanyStatus = "active" | "pending" | "suspended" | "blacklisted"; + +/** Mirrors backend `ProfileType` (the role a company plays). */ +export type ProfileType = + | "importer" + | "exporter" + | "freight_forwarder" + | "dj_freight_forwarder" + | "transporter"; + +/** Mirrors backend `ProfileStatus`. */ +export type ProfileStatus = "active" | "pending" | "suspended" | "blacklisted"; + +/** A single role a company is registered for, with its reference code. */ +export interface CompanyProfile { + id: string; + companyId: string; + type: ProfileType; + reference: string; + status: ProfileStatus; + businessLicense?: string | null; + attributes?: Record | null; + createdAt: string; + updatedAt: string; +} + +/** Mirrors backend `Company` (+ its `companyProfiles`). */ +export interface Company { + id: string; + name: string; + type: CompanyType; + status: CompanyStatus; + tin: string; + vatNumber?: string | null; + fanNumber?: string | null; + country: string; + address?: string | null; + phone?: string | null; + email?: string | null; + contactPersonName?: string | null; + contactPersonPhone?: string | null; + generalManagerName?: string | null; + generalManagerEmail?: string | null; + generalManagerPhone?: string | null; + website?: string | null; + attributes?: Record | null; + companyProfiles: CompanyProfile[]; + createdAt: string; + updatedAt: string; +} + +/** Query parameters for the company list. */ +export interface CompanyListFilter { + page: number; + pageSize: number; + search?: string; + type?: CompanyType; + status?: CompanyStatus; +} + +/** Standard paginated list envelope (matches the bookings service shape). */ +export interface PaginatedCompanies { + items: Company[]; + total: number; +} + +/** KPI counts returned by GET /companies/stats. */ +export interface CompanyStats { + total: number; + active: number; + pending: number; + suspended: number; + blacklisted: number; +} + +/* ------------------------------------------------------------------ * + * Related data shown on the customer detail page (mocked for now). * + * ------------------------------------------------------------------ */ + +export type CustomerBookingStatus = + | "DRAFT" + | "SUBMITTED" + | "PENDING_APPROVAL" + | "APPROVED" + | "PAID" + | "IN_TRANSIT" + | "COMPLETED" + | "REJECTED" + | "CANCELLED"; + +export interface CustomerBooking { + id: string; + reference: string; + status: CustomerBookingStatus; + tradeDirection: "IMPORT" | "EXPORT"; + freightType: "CONTAINER" | "BULK"; + originLabel: string; + destinationLabel: string; + totalAmount: number; + currency: "ETB" | "USD"; + scheduledDate?: string | null; + createdAt: string; +} + +export interface CustomerDocument { + id: string; + name: string; + /** File-upload setting code, e.g. "business_license", "contract". */ + code: string; + mimeType: string; + /** Size in bytes. */ + size: number; + uploadedAt: string; + url?: string | null; +} + +export type CustomerPaymentStatus = + | "action-required" + | "processing" + | "success" + | "failed" + | "canceled" + | "refunded"; + +export type CustomerPaymentMethod = + | "telebirr" + | "cbe-birr" + | "ebirr" + | "waafi" + | "card" + | "dmoney" + | "cac-bank"; + +export interface CustomerPayment { + id: string; + reference: string; + /** Booking reference the payment settles. */ + bookingReference: string; + amount: number; + currency: "ETB" | "USD"; + method: CustomerPaymentMethod; + status: CustomerPaymentStatus; + paidAt?: string | null; + createdAt: string; +} diff --git a/apps/edr-freight-web/backoffice/src/utils/endpoint.ts b/apps/edr-freight-web/backoffice/src/utils/endpoint.ts index 4a4af69d6..02bc70e85 100644 --- a/apps/edr-freight-web/backoffice/src/utils/endpoint.ts +++ b/apps/edr-freight-web/backoffice/src/utils/endpoint.ts @@ -12,6 +12,27 @@ export type QueryConfig = Omit< "queryKey" | "queryFn" >; +/** + * Query keys a mutation should invalidate on success. Receives the mutation + * input and response so keys can be derived from them. Returns a list of query + * keys — each is matched as a *prefix* by React Query, so returning a service + * root (e.g. `["cargoes"]`) invalidates every query nested under it. + * + * The keys are surfaced through `mutationOptions().meta.invalidates`; the + * app-wide `MutationCache` (see `lib/queryClient.ts`) reads them and invalidates + * automatically, so components never wire `onSuccess` invalidation by hand. + */ +export type InvalidatesFn = ( + input: TInput, + data: TResponse, +) => ReadonlyArray; + +/** Shape stored in `mutation.meta.invalidates` and consumed by the MutationCache. */ +export type InvalidatesMeta = ( + variables: unknown, + data: unknown, +) => ReadonlyArray; + // --------------------------------------------------------------------------- // Endpoint interfaces // --------------------------------------------------------------------------- @@ -45,6 +66,7 @@ export function endpoint( action: string, execute: (input: TInput) => Promise, queryKeyBuilder?: (input: TInput) => readonly unknown[], + invalidates?: InvalidatesFn, ) { const buildKey = (input?: TInput): readonly unknown[] => { if (queryKeyBuilder && input !== undefined) { @@ -77,27 +99,25 @@ export function endpoint( }; const mutationOptions = ( - config?: Omit< - UseMutationOptions< - TResponse, - Error, - TInput - >, - "mutationFn" - >, -): UseMutationOptions< - TResponse, - Error, - TInput -> => { - return { - ...config, - mutationFn: ( - variables: TInput, - ): Promise => - execute(variables), + config?: Omit, "mutationFn">, + ): UseMutationOptions => { + const meta = invalidates + ? { + ...config?.meta, + invalidates: ((variables, data) => + invalidates( + variables as TInput, + data as TResponse, + )) satisfies InvalidatesMeta, + } + : config?.meta; + + return { + ...config, + meta, + mutationFn: (variables: TInput): Promise => execute(variables), + }; }; -}; return { call, diff --git a/apps/edr-freight-web/backoffice/tsconfig.app.json b/apps/edr-freight-web/backoffice/tsconfig.app.json index ff909c216..9c7064567 100644 --- a/apps/edr-freight-web/backoffice/tsconfig.app.json +++ b/apps/edr-freight-web/backoffice/tsconfig.app.json @@ -4,9 +4,8 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "useDefineForClassFields": true, "skipLibCheck": true, - "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["./src/*"] } }, "include": ["src"] diff --git a/cargo-types.service.ts b/cargo-types.service.ts deleted file mode 100644 index 7e15f8e67..000000000 --- a/cargo-types.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; - -export const cargoTypesService = { - async getCargoTypes() { - const { data } = await axios.get(`${API_URL}/api/cargo-types`); - return data; - }, -}; \ No newline at end of file diff --git a/container-types.service.ts b/container-types.service.ts deleted file mode 100644 index b636da5f2..000000000 --- a/container-types.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; - -export const containerTypesService = { - async getContainerTypes() { - const { data } = await axios.get(`${API_URL}/api/container-types`); - return data; - }, -}; \ No newline at end of file diff --git a/use-cargo-types.ts b/use-cargo-types.ts deleted file mode 100644 index 864e9732c..000000000 --- a/use-cargo-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { cargoTypesService } from '@/services/cargo-types.service'; - -export const CARGO_TYPES_QUERY_KEY = ['cargo-types']; - -export function useCargoTypes() { - return useQuery({ - queryKey: CARGO_TYPES_QUERY_KEY, - queryFn: () => cargoTypesService.getCargoTypes(), - staleTime: Infinity, - }); -} \ No newline at end of file diff --git a/use-cargoes.ts b/use-cargoes.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/use-container-types.ts b/use-container-types.ts deleted file mode 100644 index c216c2adf..000000000 --- a/use-container-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { containerTypesService } from '@/services/container-types.service'; - -export const CONTAINER_TYPES_QUERY_KEY = ['container-types']; - -export function useContainerTypes() { - return useQuery({ - queryKey: CONTAINER_TYPES_QUERY_KEY, - queryFn: () => containerTypesService.getContainerTypes(), - staleTime: Infinity, - }); -} \ No newline at end of file diff --git a/use-wagon-types.ts b/use-wagon-types.ts deleted file mode 100644 index 4b8019cc9..000000000 --- a/use-wagon-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { wagonTypesService } from '@/services/wagon-types.service'; - -export const WAGON_TYPES_QUERY_KEY = ['wagon-types']; - -export function useWagonTypes() { - return useQuery({ - queryKey: WAGON_TYPES_QUERY_KEY, - queryFn: () => wagonTypesService.getWagonTypes(), - staleTime: Infinity, - }); -} \ No newline at end of file diff --git a/wagon-type.entity.ts b/wagon-type.entity.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/wagon-types.controller.ts b/wagon-types.controller.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/wagon-types.repository.ts b/wagon-types.repository.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/wagon-types.service.ts b/wagon-types.service.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/wagon.service.ts b/wagon.service.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/wagons.controller.ts b/wagons.controller.ts deleted file mode 100644 index e69de29bb..000000000