mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
Merge pull request #246 from Tria-plc/freight/style/ui-sync
Feat: add the company profile management to the backoffice
This commit is contained in:
@@ -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')
|
@Get('list-summary')
|
||||||
@ApiOperation({ summary: 'Booking list metrics and tab counts (backoffice)' })
|
@ApiOperation({ summary: 'Booking list metrics and tab counts (backoffice)' })
|
||||||
@ApiOkResponse({ type: BookingListSummaryDto })
|
@ApiOkResponse({ type: BookingListSummaryDto })
|
||||||
|
|||||||
@@ -1118,4 +1118,37 @@ export class BookingsService {
|
|||||||
|
|
||||||
return this.findById(id);
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ import { CompanyInfoResponseDto } from "./dto/company-info-response.dto";
|
|||||||
import { UpdateProfileDto } from "./dto/update-profile.dto";
|
import { UpdateProfileDto } from "./dto/update-profile.dto";
|
||||||
import { ProfileResponseDto } from "./dto/profile-response.dto";
|
import { ProfileResponseDto } from "./dto/profile-response.dto";
|
||||||
import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-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 { FetchETradeDto } from "./dto/fetch-etrade.dto";
|
||||||
import { ETradeResponseDto } from "./dto/etrade-response.dto";
|
import { ETradeResponseDto } from "./dto/etrade-response.dto";
|
||||||
|
|
||||||
@@ -265,29 +268,19 @@ export class CompaniesController {
|
|||||||
return new ResponseCompanyDto(company);
|
return new ResponseCompanyDto(company);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("stats")
|
||||||
|
@ApiOperation({ summary: "Company counts by status (KPI strip)" })
|
||||||
|
async getStats(): Promise<CompanyStatsResponseDto> {
|
||||||
|
return this.companiesService.getCompanyStats();
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: "List all companies" })
|
@ApiOperation({ summary: "List companies (paginated, filterable)" })
|
||||||
async findAll(): Promise<ResponseCompanyDto[]> {
|
async findAll(
|
||||||
const companies = await this.companiesService.findAllCompanies();
|
@Query() query: ListCompaniesQueryDto,
|
||||||
return companies.map((c) => new ResponseCompanyDto(c));
|
): Promise<{ items: ResponseCompanyDto[]; total: number }> {
|
||||||
}
|
const { items, total } = await this.companiesService.listCompanies(query);
|
||||||
|
return { items: items.map((c) => new ResponseCompanyDto(c)), total };
|
||||||
@Get("type/:type")
|
|
||||||
@ApiOperation({ summary: "Find companies by type" })
|
|
||||||
async findByType(@Param("type") type: string): Promise<ResponseCompanyDto[]> {
|
|
||||||
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<ResponseCompanyDto[]> {
|
|
||||||
const companies = await this.companiesService.findAllCompanies();
|
|
||||||
return companies
|
|
||||||
.filter((c) => c.name.toLowerCase().includes(name.toLowerCase()))
|
|
||||||
.map((c) => new ResponseCompanyDto(c));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
@@ -318,6 +311,23 @@ export class CompaniesController {
|
|||||||
await this.companiesService.deleteCompany(id);
|
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")
|
@Post(":companyId/documents")
|
||||||
@UseInterceptors(AnyFilesInterceptor())
|
@UseInterceptors(AnyFilesInterceptor())
|
||||||
@ApiConsumes("multipart/form-data")
|
@ApiConsumes("multipart/form-data")
|
||||||
@@ -329,6 +339,20 @@ export class CompaniesController {
|
|||||||
return this.filesService.uploadMany(companyId, "companies", files);
|
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<ResponseCompanyProfileDto> {
|
||||||
|
const profile = await this.companiesService.setCompanyProfileStatus(
|
||||||
|
profileId,
|
||||||
|
dto.status,
|
||||||
|
);
|
||||||
|
return new ResponseCompanyProfileDto(profile);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(":companyId/profiles")
|
@Post(":companyId/profiles")
|
||||||
@FreightAdmin()
|
@FreightAdmin()
|
||||||
@ApiOperation({ summary: "Add a profile (employee) to a company" })
|
@ApiOperation({ summary: "Add a profile (employee) to a company" })
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { BaseRepository } from '@edr/api-common';
|
import { BaseRepository } from '@edr/api-common';
|
||||||
import { Company } from './entities/company.entity';
|
import { Company } from './entities/company.entity';
|
||||||
|
import { ListCompaniesQueryDto } from './dto/list-companies-query.dto';
|
||||||
|
import { CompanyStatsResponseDto } from './dto/company-stats-response.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CompaniesRepository extends BaseRepository<Company> {
|
export class CompaniesRepository extends BaseRepository<Company> {
|
||||||
@@ -32,4 +34,68 @@ export class CompaniesRepository extends BaseRepository<Company> {
|
|||||||
const count = await this.repository.count({ where: { tin } as any });
|
const count = await this.repository.count({ where: { tin } as any });
|
||||||
return count > 0;
|
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<CompanyStatsResponseDto> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { CreateCompanyWithProfileDto } from "./dto/create-company-with-profile.d
|
|||||||
import { UpdateProfileDto } from "./dto/update-profile.dto";
|
import { UpdateProfileDto } from "./dto/update-profile.dto";
|
||||||
import { ProfileResponseDto } from "./dto/profile-response.dto";
|
import { ProfileResponseDto } from "./dto/profile-response.dto";
|
||||||
import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-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 {
|
import {
|
||||||
Company,
|
Company,
|
||||||
CompanyNationality,
|
CompanyNationality,
|
||||||
@@ -148,6 +150,14 @@ export class CompaniesService {
|
|||||||
return { company, profile };
|
return { company, profile };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listCompanies(
|
||||||
|
query: ListCompaniesQueryDto,
|
||||||
|
): Promise<{ items: Company[]; total: number }> {
|
||||||
|
return this.companiesRepo.findPaginated(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCompanyStats(): Promise<CompanyStatsResponseDto> {
|
||||||
|
return this.companiesRepo.getStats();
|
||||||
/**
|
/**
|
||||||
* Begin onboarding: create a DRAFT company + the user's external profile + the
|
* Begin onboarding: create a DRAFT company + the user's external profile + the
|
||||||
* chosen operational role(s) up front, so every subsequent wizard step can
|
* 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<CompanyProfile> {
|
||||||
|
const updated = await this.companyProfilesRepo.updateStatus(profileId, status);
|
||||||
|
if (!updated)
|
||||||
|
throw new NotFoundException(`Company profile ${profileId} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
async createCompanyProfile(
|
async createCompanyProfile(
|
||||||
companyId: string,
|
companyId: string,
|
||||||
profileType?: ProfileType,
|
profileType?: ProfileType,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common";
|
|||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseRepository } from "@edr/api-common";
|
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, string> = {
|
const SEQUENCE_MAP: Record<ProfileType, string> = {
|
||||||
[ProfileType.exporter]: "seq_company_profile_ex",
|
[ProfileType.exporter]: "seq_company_profile_ex",
|
||||||
@@ -58,4 +58,16 @@ export class CompanyProfileRepository extends BaseRepository<CompanyProfile> {
|
|||||||
async findByReference(reference: string): Promise<CompanyProfile | null> {
|
async findByReference(reference: string): Promise<CompanyProfile | null> {
|
||||||
return this.repository.findOne({ where: { reference } });
|
return this.repository.findOne({ where: { reference } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<CompanyProfile | null> {
|
||||||
|
return this.repository.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStatus(
|
||||||
|
id: string,
|
||||||
|
status: ProfileStatus,
|
||||||
|
): Promise<CompanyProfile | null> {
|
||||||
|
await this.repository.update({ id }, { status });
|
||||||
|
return this.repository.findOne({ where: { id } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class CompanyStatsResponseDto {
|
||||||
|
total!: number;
|
||||||
|
active!: number;
|
||||||
|
pending!: number;
|
||||||
|
suspended!: number;
|
||||||
|
blacklisted!: number;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { ResponseExternalProfileDto } from './response-external-profile.dto';
|
|||||||
|
|
||||||
export class ResponseCompanyProfileDto {
|
export class ResponseCompanyProfileDto {
|
||||||
id: string;
|
id: string;
|
||||||
|
companyId: string;
|
||||||
type: string;
|
type: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -25,6 +26,7 @@ export class ResponseCompanyProfileDto {
|
|||||||
|
|
||||||
constructor(profile: CompanyProfile) {
|
constructor(profile: CompanyProfile) {
|
||||||
this.id = profile.id;
|
this.id = profile.id;
|
||||||
|
this.companyId = profile.companyId;
|
||||||
this.type = profile.type;
|
this.type = profile.type;
|
||||||
this.reference = profile.reference;
|
this.reference = profile.reference;
|
||||||
this.status = profile.status;
|
this.status = profile.status;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Param,
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Res,
|
Res,
|
||||||
@@ -33,6 +34,14 @@ import {
|
|||||||
export class PaymentController {
|
export class PaymentController {
|
||||||
constructor(private readonly paymentService: PaymentService) { }
|
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")
|
@Get("summary")
|
||||||
@BookingView()
|
@BookingView()
|
||||||
@ApiOperation({ summary: "Payment count/amount summary for dashboard cards" })
|
@ApiOperation({ summary: "Payment count/amount summary for dashboard cards" })
|
||||||
|
|||||||
@@ -61,4 +61,56 @@ export class PaymentRepository {
|
|||||||
return this.paymentRepo.createQueryBuilder(alias);
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -477,4 +477,8 @@ export class PaymentService {
|
|||||||
default: return "action-required";
|
default: return "action-required";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findByCompanyId(companyId: string) {
|
||||||
|
return this.paymentRepo.findByCompanyId(companyId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Boxes,
|
Boxes,
|
||||||
|
Building2,
|
||||||
Container,
|
Container,
|
||||||
FileText,
|
FileText,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@@ -27,6 +28,8 @@ import BookingContractPage from "./pages/bookings/BookingContractPage";
|
|||||||
import BookingRequestDetailPage from "./pages/bookings/BookingRequestDetailPage";
|
import BookingRequestDetailPage from "./pages/bookings/BookingRequestDetailPage";
|
||||||
import BookingRequestsPage from "./pages/bookings/BookingRequestsPage";
|
import BookingRequestsPage from "./pages/bookings/BookingRequestsPage";
|
||||||
import NewBookingPage from "./pages/bookings/NewBookingPage";
|
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 DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page";
|
||||||
import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page";
|
import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page";
|
||||||
import MyProfilePage from "./pages/dashboard/MyProfilePage";
|
import MyProfilePage from "./pages/dashboard/MyProfilePage";
|
||||||
@@ -89,6 +92,11 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
href: "/dashboard/booking-requests",
|
href: "/dashboard/booking-requests",
|
||||||
icon: <FileText />,
|
icon: <FileText />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Customers",
|
||||||
|
href: "/dashboard/customers",
|
||||||
|
icon: <Building2 />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Payments",
|
label: "Payments",
|
||||||
href: "/dashboard/payments",
|
href: "/dashboard/payments",
|
||||||
@@ -360,6 +368,8 @@ const App = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route path="customers" element={<CustomersPage />} />
|
||||||
|
<Route path="customers/:id" element={<CustomerDetailPage />} />
|
||||||
<Route path="booking-requests/new" element={<NewBookingPage />} />
|
<Route path="booking-requests/new" element={<NewBookingPage />} />
|
||||||
<Route path="booking-requests/:id" element={<BookingRequestDetailPage />} />
|
<Route path="booking-requests/:id" element={<BookingRequestDetailPage />} />
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCargoesByContainer, useDeliverCargo, useUnloadCargo } from '@/hooks/useCargoes';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -6,9 +7,14 @@ import { LoadCargoDialog } from './LoadCargoDialog';
|
|||||||
import type { Cargo } from '@/services/cargoService';
|
import type { Cargo } from '@/services/cargoService';
|
||||||
|
|
||||||
export function CargoesTable({ containerId }: { containerId: string }) {
|
export function CargoesTable({ containerId }: { containerId: string }) {
|
||||||
const { data: cargoes, refetch } = useCargoesByContainer(containerId);
|
const { data: cargoes, refetch } = useQuery(
|
||||||
const deliver = useDeliverCargo();
|
api.cargoes.listByContainer.queryOptions({
|
||||||
const unload = useUnloadCargo();
|
input: { containerId },
|
||||||
|
enabled: !!containerId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const deliver = useMutation(api.cargoes.deliver.mutationOptions());
|
||||||
|
const unload = useMutation(api.cargoes.unload.mutationOptions());
|
||||||
|
|
||||||
if (!cargoes?.length) return <div className="text-muted-foreground">No cargoes for this container.</div>;
|
if (!cargoes?.length) return <div className="text-muted-foreground">No cargoes for this container.</div>;
|
||||||
|
|
||||||
@@ -34,8 +40,8 @@ export function CargoesTable({ containerId }: { containerId: string }) {
|
|||||||
<TableCell><Badge variant="outline">{cargo.status}</Badge></TableCell>
|
<TableCell><Badge variant="outline">{cargo.status}</Badge></TableCell>
|
||||||
<TableCell className="space-x-2">
|
<TableCell className="space-x-2">
|
||||||
{cargo.status === 'PENDING' && <LoadCargoDialog cargoId={cargo.id} onSuccess={() => refetch()} />}
|
{cargo.status === 'PENDING' && <LoadCargoDialog cargoId={cargo.id} onSuccess={() => refetch()} />}
|
||||||
{cargo.status === 'LOADED' && <Button size="sm" onClick={() => deliver.mutateAsync(cargo.id).then(() => refetch())}>Deliver</Button>}
|
{cargo.status === 'LOADED' && <Button size="sm" onClick={() => deliver.mutateAsync({ id: cargo.id }).then(() => refetch())}>Deliver</Button>}
|
||||||
{cargo.status === 'LOADED' && <Button size="sm" variant="outline" onClick={() => unload.mutateAsync(cargo.id).then(() => refetch())}>Unload</Button>}
|
{cargo.status === 'LOADED' && <Button size="sm" variant="outline" onClick={() => unload.mutateAsync({ id: cargo.id }).then(() => refetch())}>Unload</Button>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
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';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +19,7 @@ export function DeliverCargoDialog({ cargoId, onSuccess }: { cargoId: string; on
|
|||||||
const [receiverName, setReceiverName] = useState('');
|
const [receiverName, setReceiverName] = useState('');
|
||||||
const [pickupDate, setPickupDate] = useState('');
|
const [pickupDate, setPickupDate] = useState('');
|
||||||
const [deliveryRemarks, setDeliveryRemarks] = useState('');
|
const [deliveryRemarks, setDeliveryRemarks] = useState('');
|
||||||
const deliver = useDeliverCargo();
|
const deliver = useMutation(api.cargoes.deliver.mutationOptions());
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const handleDeliver = async () => {
|
const handleDeliver = async () => {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
export function LoadCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuccess?: () => void }) {
|
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 [quantity, setQuantity] = useState(0);
|
||||||
const [weight, setWeight] = useState(0);
|
const [weight, setWeight] = useState(0);
|
||||||
const [volume, setVolume] = useState<number>();
|
const [volume, setVolume] = useState<number>();
|
||||||
const load = useLoadCargo();
|
const load = useMutation(api.cargoes.load.mutationOptions());
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const handleLoad = async () => {
|
const handleLoad = async () => {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Card p={0}>
|
||||||
|
<Box style={{ overflowX: "auto" }} w="100%">
|
||||||
|
<Box miw={minWidth}>{children}</Box>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableCard;
|
||||||
@@ -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<CompanyStatus | ProfileStatus, string> = {
|
||||||
|
active: "edr-green",
|
||||||
|
pending: "yellow",
|
||||||
|
suspended: "orange",
|
||||||
|
blacklisted: "red",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPANY_TYPE_COLOR: Record<CompanyType, string> = {
|
||||||
|
customer: "edr-green",
|
||||||
|
freight_forwarder: "blue",
|
||||||
|
dj_freight_forwarder: "indigo",
|
||||||
|
transporter: "grape",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROFILE_TYPE_COLOR: Record<ProfileType, string> = {
|
||||||
|
importer: "teal",
|
||||||
|
exporter: "cyan",
|
||||||
|
freight_forwarder: "blue",
|
||||||
|
dj_freight_forwarder: "indigo",
|
||||||
|
transporter: "grape",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyStatusBadge({ status }: { status: CompanyStatus }) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color={STATUS_COLOR[status] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
tt="capitalize"
|
||||||
|
fw={600}
|
||||||
|
style={badgeStyle}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompanyTypeBadge({ type }: { type: CompanyType }) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color={COMPANY_TYPE_COLOR[type] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
fw={600}
|
||||||
|
style={badgeStyle}
|
||||||
|
>
|
||||||
|
{humanize(type)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Badge color="gray" variant="light" size="sm" radius="md" style={badgeStyle}>
|
||||||
|
No profiles
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shown = profiles.slice(0, max);
|
||||||
|
const extra = profiles.length - shown.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group gap={6} wrap="wrap">
|
||||||
|
{shown.map((profile) => (
|
||||||
|
<Tooltip
|
||||||
|
key={profile.id}
|
||||||
|
label={`${humanize(profile.type)} · ${humanize(profile.status)}`}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
color={PROFILE_TYPE_COLOR[profile.type] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
fw={600}
|
||||||
|
style={badgeStyle}
|
||||||
|
>
|
||||||
|
{humanize(profile.type)} · {profile.reference}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
{extra > 0 ? (
|
||||||
|
<Badge color="gray" variant="light" size="sm" radius="md" style={badgeStyle}>
|
||||||
|
+{extra}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileTypeBadge({ type }: { type: ProfileType }) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color={PROFILE_TYPE_COLOR[type] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
fw={600}
|
||||||
|
style={badgeStyle}
|
||||||
|
>
|
||||||
|
{humanize(type)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileStatusBadge({ status }: { status: ProfileStatus }) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color={STATUS_COLOR[status] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
tt="capitalize"
|
||||||
|
fw={600}
|
||||||
|
style={badgeStyle}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOOKING_STATUS_COLOR: Record<CustomerBookingStatus, string> = {
|
||||||
|
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 (
|
||||||
|
<Badge
|
||||||
|
color={BOOKING_STATUS_COLOR[status] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
tt="uppercase"
|
||||||
|
fw={600}
|
||||||
|
style={badgeStyle}
|
||||||
|
>
|
||||||
|
{humanize(status)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAYMENT_STATUS_COLOR: Record<CustomerPaymentStatus, string> = {
|
||||||
|
"action-required": "orange",
|
||||||
|
processing: "yellow",
|
||||||
|
success: "edr-green",
|
||||||
|
failed: "red",
|
||||||
|
canceled: "gray",
|
||||||
|
refunded: "grape",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PaymentStatusBadge({ status }: { status: CustomerPaymentStatus }) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color={PAYMENT_STATUS_COLOR[status] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
tt="capitalize"
|
||||||
|
fw={600}
|
||||||
|
style={badgeStyle}
|
||||||
|
>
|
||||||
|
{humanize(status)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Group gap={6} wrap="nowrap">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="edr-green"
|
||||||
|
radius="md"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => act("active")}
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
radius="md"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => act("blacklisted")}
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "active") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="orange"
|
||||||
|
radius="md"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => act("suspended")}
|
||||||
|
>
|
||||||
|
Suspend
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "suspended") {
|
||||||
|
return (
|
||||||
|
<Group gap={6} wrap="nowrap">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="edr-green"
|
||||||
|
radius="md"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => act("active")}
|
||||||
|
>
|
||||||
|
Reactivate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
radius="md"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => act("blacklisted")}
|
||||||
|
>
|
||||||
|
Blacklist
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "blacklisted") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => act("pending")}
|
||||||
|
>
|
||||||
|
Reinstate
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -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]}`;
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FileSignature, Loader2 } from "lucide-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 { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -10,10 +13,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { useAuth } from "@/auth/useAuth";
|
import { useAuth } from "@/auth/useAuth";
|
||||||
import {
|
|
||||||
useMySignature,
|
|
||||||
useSaveSignature,
|
|
||||||
} from "@/hooks/useSavedSignature";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -33,8 +32,10 @@ import {
|
|||||||
*/
|
*/
|
||||||
export function MySignatureCard() {
|
export function MySignatureCard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { data: saved, isLoading } = useMySignature();
|
const { data: saved, isLoading } = useQuery(
|
||||||
const saveMutation = useSaveSignature();
|
api.signatures.mySignature.queryOptions({ staleTime: 60_000 }),
|
||||||
|
);
|
||||||
|
const saveMutation = useMutation(api.signatures.save.mutationOptions());
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [signerName, setSignerName] = useState("");
|
const [signerName, setSignerName] = useState("");
|
||||||
@@ -56,7 +57,13 @@ export function MySignatureCard() {
|
|||||||
signerDisplayName: signerName.trim(),
|
signerDisplayName: signerName.trim(),
|
||||||
signatureImageBase64: signatureData,
|
signatureImageBase64: signatureData,
|
||||||
},
|
},
|
||||||
{ onSuccess: () => setOpen(false) },
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Signature saved");
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
onError: () => toast.error("Failed to save signature"),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,13 +31,8 @@ import {
|
|||||||
Weight,
|
Weight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
useAvailableLocomotives,
|
import { api } from "@/services/api";
|
||||||
useEligibleBookings,
|
|
||||||
useScheduleList,
|
|
||||||
useScheduleMutations,
|
|
||||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
|
||||||
import { useRoutes } from "@/hooks/useRoutes";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { trainSchedulingService } from "@/services/trainScheduling.service";
|
import { trainSchedulingService } from "@/services/trainScheduling.service";
|
||||||
import type { BookingDetail } from "@/types/booking";
|
import type { BookingDetail } from "@/types/booking";
|
||||||
@@ -133,13 +128,27 @@ export function AllocateBookingWizard({
|
|||||||
[originId, destinationId],
|
[originId, destinationId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const eligibleQuery = useEligibleBookings(eligibleFilters, opened);
|
const eligibleQuery = useQuery(
|
||||||
const schedulesQuery = useScheduleList();
|
api.trainScheduling.eligibleBookings.queryOptions({
|
||||||
const routesQuery = useRoutes();
|
input: { filters: eligibleFilters },
|
||||||
const locomotivesQuery = useAvailableLocomotives(
|
enabled: opened,
|
||||||
scheduleMode === "new" && routeId ? routeId : undefined,
|
}),
|
||||||
);
|
);
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (scheduleMode === "new") {
|
if (scheduleMode === "new") {
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { CheckCircle2, Layers, Lock, LockOpen, PlayCircle, Repeat, XCircle } from "lucide-react";
|
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 { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
||||||
import {
|
import { api } from "@/services/api";
|
||||||
useBatchActions,
|
|
||||||
useBookableSchedules,
|
|
||||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { TrainScheduleDetail } from "@/types/trainScheduling";
|
import type { TrainScheduleDetail } from "@/types/trainScheduling";
|
||||||
|
|
||||||
@@ -33,16 +32,31 @@ const windowColor: Record<string, string> = {
|
|||||||
|
|
||||||
export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) {
|
export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) {
|
||||||
const { toast } = useToast();
|
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 windowStatus = (schedule as { bookingWindowStatus?: string }).bookingWindowStatus ?? "OPEN";
|
||||||
const locked = schedule.status === "DISPATCHED" || schedule.status === "ARRIVED";
|
const locked = schedule.status === "DISPATCHED" || schedule.status === "ARRIVED";
|
||||||
|
|
||||||
const [moveBookingId, setMoveBookingId] = useState<string | null>(null);
|
const [moveBookingId, setMoveBookingId] = useState<string | null>(null);
|
||||||
const [moveTarget, setMoveTarget] = useState<string | null>(null);
|
const [moveTarget, setMoveTarget] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: targets } = useBookableSchedules(
|
const { data: targets } = useQuery(
|
||||||
schedule.originStation?.id,
|
api.trainScheduling.bookableSchedules.queryOptions({
|
||||||
schedule.destinationStation?.id,
|
input: {
|
||||||
|
originYardId: schedule.originStation?.id,
|
||||||
|
destinationYardId: schedule.destinationStation?.id,
|
||||||
|
},
|
||||||
|
enabled: Boolean(
|
||||||
|
schedule.originStation?.id && schedule.destinationStation?.id,
|
||||||
|
),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const moveOptions = useMemo(
|
const moveOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { Building2, Package, TrainFront, Weight, X } from "lucide-react";
|
|||||||
import type { TrainScheduleDetail } from "@/types/trainScheduling";
|
import type { TrainScheduleDetail } from "@/types/trainScheduling";
|
||||||
import type { BookingDetailData } from "./BookingDetailModal";
|
import type { BookingDetailData } from "./BookingDetailModal";
|
||||||
import { RemoveBookingConfirmModal, type RemovalTarget } from "./RemoveBookingConfirmModal";
|
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 { useToast } from "@/hooks/use-toast";
|
||||||
import { freightBrand } from "@/theme/freight-brand";
|
import { freightBrand } from "@/theme/freight-brand";
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export const AssignedBookingsPanel = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: AssignedBookingsPanelProps) => {
|
}: AssignedBookingsPanelProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const unassign = useScheduleMutations(scheduleId).unassign;
|
const unassign = useMutation(api.trainScheduling.unassignBooking.mutationOptions());
|
||||||
const isDispatched = scheduleDetail.status === "DISPATCHED";
|
const isDispatched = scheduleDetail.status === "DISPATCHED";
|
||||||
const [removalTarget, setRemovalTarget] = useState<RemovalTarget | null>(null);
|
const [removalTarget, setRemovalTarget] = useState<RemovalTarget | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import { UnassignedBookingsPanel } from "./UnassignedBookingsPanel";
|
|||||||
import { RemovalLogPanel } from "./RemovalLogPanel";
|
import { RemovalLogPanel } from "./RemovalLogPanel";
|
||||||
import { BatchBookingList } from "./BatchBookingList";
|
import { BatchBookingList } from "./BatchBookingList";
|
||||||
import { BookingDetailModal, type BookingDetailData } from "./BookingDetailModal";
|
import { BookingDetailModal, type BookingDetailData } from "./BookingDetailModal";
|
||||||
import {
|
import { useQuery } from "@tanstack/react-query";
|
||||||
useCompositionRemovals,
|
import { api } from "@/services/api";
|
||||||
useUnassignedBookings,
|
|
||||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
|
||||||
import { freightBrand } from "@/theme/freight-brand";
|
import { freightBrand } from "@/theme/freight-brand";
|
||||||
|
|
||||||
interface CompositionBookingTabsProps {
|
interface CompositionBookingTabsProps {
|
||||||
@@ -47,8 +45,18 @@ export const CompositionBookingTabs = ({
|
|||||||
const [detailBooking, setDetailBooking] = useState<BookingDetailData | null>(null);
|
const [detailBooking, setDetailBooking] = useState<BookingDetailData | null>(null);
|
||||||
const [tab, setTab] = useState<TabKey>("assigned");
|
const [tab, setTab] = useState<TabKey>("assigned");
|
||||||
|
|
||||||
const unassignedQuery = useUnassignedBookings(scheduleId);
|
const unassignedQuery = useQuery(
|
||||||
const removalsQuery = useCompositionRemovals(scheduleId);
|
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 { assignedCount } = useMemo(() => {
|
||||||
const wagons = scheduleDetail.trainSet?.wagons ?? [];
|
const wagons = scheduleDetail.trainSet?.wagons ?? [];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Group, TextInput, Text } from "@mantine/core";
|
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 {
|
interface ContainerNumberInputProps {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@@ -19,13 +20,16 @@ export const ContainerNumberInput = ({
|
|||||||
const [inputValue, setInputValue] = useState(value ?? "");
|
const [inputValue, setInputValue] = useState(value ?? "");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const updateMutation = useUpdateContainerItem(scheduleId);
|
const updateMutation = useMutation(
|
||||||
|
api.trainScheduling.updateContainerItem.mutationOptions(),
|
||||||
|
);
|
||||||
const isLoading = updateMutation.isPending;
|
const isLoading = updateMutation.isPending;
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
await updateMutation.mutateAsync({
|
await updateMutation.mutateAsync({
|
||||||
|
scheduleId,
|
||||||
itemId,
|
itemId,
|
||||||
containerNumber: inputValue || null,
|
containerNumber: inputValue || null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { Box, Card, Group, Stack, Text, ThemeIcon } from "@mantine/core";
|
import { Box, Card, Group, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||||
import { History, PackageX } from "lucide-react";
|
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 {
|
interface RemovalLogPanelProps {
|
||||||
scheduleId: string;
|
scheduleId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RemovalLogPanel = ({ scheduleId }: RemovalLogPanelProps) => {
|
export const RemovalLogPanel = ({ scheduleId }: RemovalLogPanelProps) => {
|
||||||
const removalQuery = useCompositionRemovals(scheduleId);
|
const removalQuery = useQuery(
|
||||||
|
api.trainScheduling.compositionRemovals.queryOptions({
|
||||||
|
input: { scheduleId },
|
||||||
|
enabled: Boolean(scheduleId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (removalQuery.isLoading) {
|
if (removalQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { TrainStatsBar } from "./TrainStatsBar";
|
|||||||
import { WagonCard } from "./WagonCard";
|
import { WagonCard } from "./WagonCard";
|
||||||
import { InteractiveTrainConsist } from "./InteractiveTrainConsist";
|
import { InteractiveTrainConsist } from "./InteractiveTrainConsist";
|
||||||
import { RemoveBookingModal } from "./RemoveBookingModal";
|
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";
|
import { freightBrand } from "@/theme/freight-brand";
|
||||||
|
|
||||||
type Wagon = TrainScheduleDetail["trainSet"]["wagons"][number];
|
type Wagon = TrainScheduleDetail["trainSet"]["wagons"][number];
|
||||||
@@ -47,8 +48,12 @@ export const TrainConsistView = ({
|
|||||||
const [selectedWagonId, setSelectedWagonId] = useState<string | null>(null);
|
const [selectedWagonId, setSelectedWagonId] = useState<string | null>(null);
|
||||||
const [removeModalOpen, setRemoveModalOpen] = useState(false);
|
const [removeModalOpen, setRemoveModalOpen] = useState(false);
|
||||||
|
|
||||||
const unassignMutation = useScheduleMutations(scheduleId).unassign;
|
const unassignMutation = useMutation(
|
||||||
const removeWagonMutation = useRemoveWagonSlot(scheduleId);
|
api.trainScheduling.unassignBooking.mutationOptions(),
|
||||||
|
);
|
||||||
|
const removeWagonMutation = useMutation(
|
||||||
|
api.trainScheduling.removeWagonSlot.mutationOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
const trainSet = scheduleDetail.trainSet;
|
const trainSet = scheduleDetail.trainSet;
|
||||||
const wagons = trainSet?.wagons ?? [];
|
const wagons = trainSet?.wagons ?? [];
|
||||||
@@ -83,7 +88,7 @@ export const TrainConsistView = ({
|
|||||||
|
|
||||||
const handleRemoveWagon = async (wagonId: string) => {
|
const handleRemoveWagon = async (wagonId: string) => {
|
||||||
if (confirm("Are you sure you want to remove this wagon slot?")) {
|
if (confirm("Are you sure you want to remove this wagon slot?")) {
|
||||||
await removeWagonMutation.mutateAsync(wagonId);
|
await removeWagonMutation.mutateAsync({ scheduleId, wagonId });
|
||||||
setSelectedWagonId(null);
|
setSelectedWagonId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Badge, Box, Button, Card, Group, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core";
|
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 { AlertTriangle, Container as ContainerIcon, MapPin, Plus, TrainFront } from "lucide-react";
|
||||||
import {
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
useUnassignedBookings,
|
import { api } from "@/services/api";
|
||||||
useScheduleMutations,
|
|
||||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { FleetAvailabilityRow } from "@/types/trainScheduling";
|
import type { FleetAvailabilityRow } from "@/types/trainScheduling";
|
||||||
import type { BookingDetailData } from "./BookingDetailModal";
|
import type { BookingDetailData } from "./BookingDetailModal";
|
||||||
@@ -73,8 +71,15 @@ export const UnassignedBookingsPanel = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: UnassignedBookingsPanelProps) => {
|
}: UnassignedBookingsPanelProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const unassignedQuery = useUnassignedBookings(scheduleId);
|
const unassignedQuery = useQuery(
|
||||||
const assignMutation = useScheduleMutations(scheduleId).assignUnassigned;
|
api.trainScheduling.unassignedBookings.queryOptions({
|
||||||
|
input: { scheduleId },
|
||||||
|
enabled: Boolean(scheduleId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const assignMutation = useMutation(
|
||||||
|
api.trainScheduling.assignUnassignedBooking.mutationOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
const handleAssign = async (bookingId: string, reference: string | null) => {
|
const handleAssign = async (bookingId: string, reference: string | null) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { Button, Group, Modal, NumberInput, Select, Stack, Text } from "@mantine
|
|||||||
|
|
||||||
import { Freight } from "@edr/types";
|
import { Freight } from "@edr/types";
|
||||||
|
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { api } from "@/services/api";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useRouteYards } from "@/hooks/useRoutes";
|
|
||||||
import { useAssignWagonToTrain, useWagons } from "@/hooks/useWagons";
|
|
||||||
|
|
||||||
export function AssignWagonDialog({ trainId }: { trainId: string }) {
|
export function AssignWagonDialog({ trainId }: { trainId: string }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [wagonId, setWagonId] = useState<string | null>(null);
|
const [wagonId, setWagonId] = useState<string | null>(null);
|
||||||
const [sequence, setSequence] = useState<number | "">("");
|
const [sequence, setSequence] = useState<number | "">("");
|
||||||
const { data: wagons } = useWagons();
|
const { data: wagons } = useQuery(api.wagons.list.queryOptions({ input: {} }));
|
||||||
const { data: yards = [] } = useRouteYards();
|
const { data: yards = [] } = useQuery(api.routes.yards.queryOptions());
|
||||||
const assign = useAssignWagonToTrain();
|
const assign = useMutation(api.wagons.assignToTrain.mutationOptions());
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const available = (wagons ?? []).filter(
|
const available = (wagons ?? []).filter(
|
||||||
|
|||||||
@@ -3,15 +3,22 @@ import { Trash2 } from "lucide-react";
|
|||||||
import type { ColumnDef } from "@edr/ui-common";
|
import type { ColumnDef } from "@edr/ui-common";
|
||||||
import { ActionIcon, Badge, Group, Text, Tooltip } from "@mantine/core";
|
import { ActionIcon, Badge, Group, Text, Tooltip } from "@mantine/core";
|
||||||
|
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
|
import { api } from "@/services/api";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useUnassignWagon, useWagonsByTrain } from "@/hooks/useWagons";
|
|
||||||
import type { Wagon } from "@/services/wagon.service";
|
import type { Wagon } from "@/services/wagon.service";
|
||||||
import { DataTable } from "@edr/ui-common";
|
import { DataTable } from "@edr/ui-common";
|
||||||
|
|
||||||
export function WagonsTable({ trainId }: { trainId: string }) {
|
export function WagonsTable({ trainId }: { trainId: string }) {
|
||||||
const { data: wagons = [], isLoading, refetch } = useWagonsByTrain(trainId);
|
const { data: wagons = [], isLoading, refetch } = useQuery(
|
||||||
const unassign = useUnassignWagon();
|
api.wagons.listByTrain.queryOptions({
|
||||||
|
input: { trainId },
|
||||||
|
enabled: !!trainId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const unassign = useMutation(api.wagons.unassign.mutationOptions());
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const columns = useMemo((): ColumnDef<Wagon>[] => {
|
const columns = useMemo((): ColumnDef<Wagon>[] => {
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import {
|
|||||||
Warehouse,
|
Warehouse,
|
||||||
} from 'lucide-react';
|
} 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 type { ActivityType } from '@/types/warehouse';
|
||||||
import { formatDate, humanizeEnum } from './options';
|
import { formatDate, humanizeEnum } from './options';
|
||||||
|
|
||||||
@@ -24,7 +26,12 @@ const activityIcon: Record<ActivityType, React.ReactNode> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ActivityTimeline({ inventoryId }: { inventoryId: string }) {
|
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 ?? [];
|
const items = data ?? [];
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
|
||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
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 type { SaveWarehousePayload, Warehouse, WarehouseType } from '@/types/warehouse';
|
||||||
import { extractErrorMessage, statusOptions, warehouseTypeOptions } from './options';
|
import { extractErrorMessage, statusOptions, warehouseTypeOptions } from './options';
|
||||||
|
|
||||||
@@ -48,9 +49,11 @@ const emptyForm = (): FormState => ({
|
|||||||
export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWarehouseModalProps) {
|
export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWarehouseModalProps) {
|
||||||
const isEdit = Boolean(warehouse);
|
const isEdit = Boolean(warehouse);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const createMutation = useCreateWarehouse();
|
const createMutation = useMutation(api.warehouses.create.mutationOptions());
|
||||||
const updateMutation = useUpdateWarehouse();
|
const updateMutation = useMutation(api.warehouses.update.mutationOptions());
|
||||||
const { data: stations } = useStations();
|
const { data: stations } = useQuery(
|
||||||
|
api.stations.list.queryOptions({ staleTime: 5 * 60 * 1000 }),
|
||||||
|
);
|
||||||
const [form, setForm] = useState<FormState>(emptyForm());
|
const [form, setForm] = useState<FormState>(emptyForm());
|
||||||
|
|
||||||
const stationOptions = (stations ?? []).map((s) => ({ value: s.id, label: `${s.name} (${s.code})` }));
|
const stationOptions = (stations ?? []).map((s) => ({ value: s.id, label: `${s.name} (${s.code})` }));
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button, Group, Modal, NumberInput, Select, Stack, TextInput } from '@mantine/core';
|
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 { useToast } from '@/hooks/use-toast';
|
||||||
import { useCreateYard, useUpdateYard } from '@/hooks/useWarehouses';
|
|
||||||
import type { SaveYardPayload, WarehouseYard, WarehouseYardType } from '@/types/warehouse';
|
import type { SaveYardPayload, WarehouseYard, WarehouseYardType } from '@/types/warehouse';
|
||||||
import { extractErrorMessage, statusOptions, yardTypeOptions } from './options';
|
import { extractErrorMessage, statusOptions, yardTypeOptions } from './options';
|
||||||
|
|
||||||
@@ -36,8 +38,8 @@ const emptyForm = (): FormState => ({
|
|||||||
export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYardModalProps) {
|
export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYardModalProps) {
|
||||||
const isEdit = Boolean(yard);
|
const isEdit = Boolean(yard);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const createMutation = useCreateYard();
|
const createMutation = useMutation(api.warehouses.createYard.mutationOptions());
|
||||||
const updateMutation = useUpdateYard();
|
const updateMutation = useMutation(api.warehouses.updateYard.mutationOptions());
|
||||||
const [form, setForm] = useState<FormState>(emptyForm());
|
const [form, setForm] = useState<FormState>(emptyForm());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button, Group, Modal, NumberInput, Select, Stack, TextInput } from '@mantine/core';
|
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 { useToast } from '@/hooks/use-toast';
|
||||||
import { useCreateZone, useUpdateZone } from '@/hooks/useWarehouses';
|
|
||||||
import type { SaveZonePayload, WarehouseZone, WarehouseZoneType } from '@/types/warehouse';
|
import type { SaveZonePayload, WarehouseZone, WarehouseZoneType } from '@/types/warehouse';
|
||||||
import { extractErrorMessage, statusOptions, zoneTypeOptions } from './options';
|
import { extractErrorMessage, statusOptions, zoneTypeOptions } from './options';
|
||||||
|
|
||||||
@@ -36,8 +38,8 @@ const emptyForm = (): FormState => ({
|
|||||||
export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneModalProps) {
|
export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneModalProps) {
|
||||||
const isEdit = Boolean(zone);
|
const isEdit = Boolean(zone);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const createMutation = useCreateZone();
|
const createMutation = useMutation(api.warehouses.createZone.mutationOptions());
|
||||||
const updateMutation = useUpdateZone();
|
const updateMutation = useMutation(api.warehouses.updateZone.mutationOptions());
|
||||||
const [form, setForm] = useState<FormState>(emptyForm());
|
const [form, setForm] = useState<FormState>(emptyForm());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Alert, Button, Group, Modal, Stack, Text, Textarea, TextInput } from '@mantine/core';
|
import { Alert, Button, Group, Modal, Stack, Text, Textarea, TextInput } from '@mantine/core';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useDeliverInventory } from '@/hooks/useWarehouses';
|
|
||||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ interface DeliverInventoryModalProps {
|
|||||||
|
|
||||||
export function DeliverInventoryModal({ opened, onClose, item }: DeliverInventoryModalProps) {
|
export function DeliverInventoryModal({ opened, onClose, item }: DeliverInventoryModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const deliverMutation = useDeliverInventory();
|
const deliverMutation = useMutation(api.warehouses.deliver.mutationOptions());
|
||||||
const [receiverName, setReceiverName] = useState('');
|
const [receiverName, setReceiverName] = useState('');
|
||||||
const [remarks, setRemarks] = useState('');
|
const [remarks, setRemarks] = useState('');
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { Badge, Button, Card, Divider, Group, Loader, Modal, Stack, Text } from '@mantine/core';
|
import { Badge, Button, Card, Divider, Group, Loader, Modal, Stack, Text } from '@mantine/core';
|
||||||
import { CalendarClock, Coins, DoorOpen, FileText } from 'lucide-react';
|
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 { useToast } from '@/hooks/use-toast';
|
||||||
import {
|
|
||||||
useFeePreview,
|
|
||||||
useGateClearance,
|
|
||||||
useGenerateInvoice,
|
|
||||||
useInvoicesForInventory,
|
|
||||||
} from '@/hooks/useWarehouses';
|
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
import type { FeePreview, WarehouseInvoiceStatus } from '@/types/warehouse';
|
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) {
|
export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const enabledId = opened ? inventoryId ?? undefined : undefined;
|
const enabledId = opened ? inventoryId ?? undefined : undefined;
|
||||||
const { data, isLoading } = useFeePreview(enabledId);
|
const { data, isLoading } = useQuery(
|
||||||
const { data: invoices } = useInvoicesForInventory(enabledId);
|
api.warehouses.feePreview.queryOptions({
|
||||||
const generate = useGenerateInvoice();
|
input: { inventoryId: enabledId ?? '' },
|
||||||
const gateClear = useGateClearance();
|
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');
|
const activeInvoice = (invoices ?? []).find((i) => i.status !== 'CANCELLED');
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Upload } from 'lucide-react';
|
import { Upload } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useCreateInspectionReport, useUploadInspectionAttachments } from '@/hooks/useWarehouses';
|
|
||||||
import {
|
import {
|
||||||
INSPECTION_REPORT_TYPES,
|
INSPECTION_REPORT_TYPES,
|
||||||
INSPECTION_STATUSES,
|
INSPECTION_STATUSES,
|
||||||
@@ -45,8 +47,12 @@ const STATUS_LABELS: Record<InspectionResultStatus, string> = {
|
|||||||
/** Batch 4.5 — record an inspection / damage report with optional image upload. */
|
/** Batch 4.5 — record an inspection / damage report with optional image upload. */
|
||||||
export function InspectionReportModal({ opened, onClose, inventoryId }: InspectionReportModalProps) {
|
export function InspectionReportModal({ opened, onClose, inventoryId }: InspectionReportModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const createReport = useCreateInspectionReport();
|
const createReport = useMutation(
|
||||||
const uploadAttachments = useUploadInspectionAttachments();
|
api.warehouses.createInspectionReport.mutationOptions(),
|
||||||
|
);
|
||||||
|
const uploadAttachments = useMutation(
|
||||||
|
api.warehouses.uploadInspectionAttachments.mutationOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
const [reportType, setReportType] = useState<InspectionReportType>('INSPECTION');
|
const [reportType, setReportType] = useState<InspectionReportType>('INSPECTION');
|
||||||
const [inspectionStatus, setInspectionStatus] = useState<InspectionResultStatus>('PASSED');
|
const [inspectionStatus, setInspectionStatus] = useState<InspectionResultStatus>('PASSED');
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { Center, Loader, Table, Text } from '@mantine/core';
|
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';
|
import { formatDate } from './options';
|
||||||
|
|
||||||
const shortId = (id?: string | null) => (id ? `${id.slice(0, 8)}…` : '—');
|
const shortId = (id?: string | null) => (id ? `${id.slice(0, 8)}…` : '—');
|
||||||
|
|
||||||
export function InventoryMovementHistoryTable({ inventoryId }: { inventoryId: string }) {
|
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 ?? [];
|
const movements = data ?? [];
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ import { useState } from 'react';
|
|||||||
import { Button, Center, Group, Loader, Stack, Text } from '@mantine/core';
|
import { Button, Center, Group, Loader, Stack, Text } from '@mantine/core';
|
||||||
import { ClipboardCheck } from 'lucide-react';
|
import { ClipboardCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import {
|
|
||||||
useBulkMarkInspected,
|
|
||||||
useDispatchInventory,
|
|
||||||
useMarkReadyForLoading,
|
|
||||||
useMarkReadyForPickup,
|
|
||||||
useStoreInventory,
|
|
||||||
} from '@/hooks/useWarehouses';
|
|
||||||
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
import { DeliverInventoryModal } from './DeliverInventoryModal';
|
import { DeliverInventoryModal } from './DeliverInventoryModal';
|
||||||
import { FeePreviewModal } from './FeePreviewModal';
|
import { FeePreviewModal } from './FeePreviewModal';
|
||||||
@@ -42,11 +38,17 @@ export function InventoryWorkbench({ items, isLoading, onLastMile }: InventoryWo
|
|||||||
const [releaseItem, setReleaseItem] = useState<WarehouseInventoryItem | null>(null);
|
const [releaseItem, setReleaseItem] = useState<WarehouseInventoryItem | null>(null);
|
||||||
const [deliverItem, setDeliverItem] = useState<WarehouseInventoryItem | null>(null);
|
const [deliverItem, setDeliverItem] = useState<WarehouseInventoryItem | null>(null);
|
||||||
|
|
||||||
const storeMutation = useStoreInventory();
|
const storeMutation = useMutation(api.warehouses.store.mutationOptions());
|
||||||
const readyMutation = useMarkReadyForLoading();
|
const readyMutation = useMutation(
|
||||||
const pickupMutation = useMarkReadyForPickup();
|
api.warehouses.markReadyForLoading.mutationOptions(),
|
||||||
const dispatchMutation = useDispatchInventory();
|
);
|
||||||
const inspectMutation = useBulkMarkInspected();
|
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<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
const allSelected = items.length > 0 && selected.size === items.length;
|
const allSelected = items.length > 0 && selected.size === items.length;
|
||||||
@@ -66,10 +68,7 @@ export function InventoryWorkbench({ items, isLoading, onLastMile }: InventoryWo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = (await inspectMutation.mutateAsync({ inventoryIds: [...selected] })) as {
|
const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] });
|
||||||
data: { inspectedCount: number; skippedCount: number };
|
|
||||||
};
|
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: `${r.inspectedCount} marked inspected`,
|
title: `${r.inspectedCount} marked inspected`,
|
||||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Alert, Button, Group, Modal, NumberInput, Stack, Text, Textarea } from '@mantine/core';
|
import { Alert, Button, Group, Modal, NumberInput, Stack, Text, Textarea } from '@mantine/core';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useLoadInventory } from '@/hooks/useWarehouses';
|
|
||||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
import { WagonSelect } from './WagonSelect';
|
import { WagonSelect } from './WagonSelect';
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
@@ -17,7 +19,7 @@ interface LoadInventoryModalProps {
|
|||||||
/** Load READY_FOR_LOADING inventory onto a wagon (creates a loading record). */
|
/** Load READY_FOR_LOADING inventory onto a wagon (creates a loading record). */
|
||||||
export function LoadInventoryModal({ opened, onClose, item }: LoadInventoryModalProps) {
|
export function LoadInventoryModal({ opened, onClose, item }: LoadInventoryModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const loadMutation = useLoadInventory();
|
const loadMutation = useMutation(api.warehouses.load.mutationOptions());
|
||||||
const [wagonId, setWagonId] = useState('');
|
const [wagonId, setWagonId] = useState('');
|
||||||
const [loadedWeight, setLoadedWeight] = useState<number | ''>('');
|
const [loadedWeight, setLoadedWeight] = useState<number | ''>('');
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Button, Group, Modal, Select, Stack, Textarea } from '@mantine/core';
|
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 { useToast } from '@/hooks/use-toast';
|
||||||
import { useMoveInventory, useWarehouseYards, useWarehouseZones, useWarehouses } from '@/hooks/useWarehouses';
|
|
||||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ interface MoveInventoryModalProps {
|
|||||||
|
|
||||||
export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModalProps) {
|
export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const moveMutation = useMoveInventory();
|
const moveMutation = useMutation(api.warehouses.move.mutationOptions());
|
||||||
const [warehouseId, setWarehouseId] = useState('');
|
const [warehouseId, setWarehouseId] = useState('');
|
||||||
const [yardId, setYardId] = useState('');
|
const [yardId, setYardId] = useState('');
|
||||||
const [zoneId, setZoneId] = useState('');
|
const [zoneId, setZoneId] = useState('');
|
||||||
@@ -29,9 +31,21 @@ export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModal
|
|||||||
}
|
}
|
||||||
}, [opened]);
|
}, [opened]);
|
||||||
|
|
||||||
const warehousesQuery = useWarehouses({ status: 'ACTIVE' });
|
const warehousesQuery = useQuery(
|
||||||
const yardsQuery = useWarehouseYards(warehouseId || undefined);
|
api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } } }),
|
||||||
const zonesQuery = useWarehouseZones(yardId || undefined);
|
);
|
||||||
|
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(
|
const warehouseOptions = useMemo(
|
||||||
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
||||||
|
|||||||
@@ -18,34 +18,14 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { ChevronDown, ChevronRight, ClipboardCheck, Info, PackageSearch, Train, Truck } from 'lucide-react';
|
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 { 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 {
|
import type {
|
||||||
AutoUnloadArrivedResult,
|
|
||||||
BulkDispatchResult,
|
|
||||||
BulkInspectResult,
|
|
||||||
BulkReceiveResult,
|
|
||||||
ImportTrain,
|
ImportTrain,
|
||||||
ImportTrainItem,
|
ImportTrainItem,
|
||||||
ImportUnloadedItem,
|
ImportUnloadedItem,
|
||||||
LoadPassedExportResult,
|
|
||||||
ReadyToLoadRow,
|
ReadyToLoadRow,
|
||||||
ReceiveInventoryPayload,
|
ReceiveInventoryPayload,
|
||||||
} from '@/types/warehouse';
|
} from '@/types/warehouse';
|
||||||
@@ -77,9 +57,21 @@ function LocationSelects({
|
|||||||
value: Location;
|
value: Location;
|
||||||
onChange: (next: Location) => void;
|
onChange: (next: Location) => void;
|
||||||
}) {
|
}) {
|
||||||
const warehousesQuery = useWarehouses({ status: 'ACTIVE' });
|
const warehousesQuery = useQuery(
|
||||||
const yardsQuery = useWarehouseYards(value.warehouseId || undefined);
|
api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } } }),
|
||||||
const zonesQuery = useWarehouseZones(value.yardId || undefined);
|
);
|
||||||
|
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(
|
const warehouseOptions = useMemo(
|
||||||
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
||||||
@@ -148,10 +140,12 @@ function EligibleTab({
|
|||||||
onChanged?: () => void;
|
onChanged?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { toast } = useToast();
|
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 rows = useMemo(() => allRows.filter((r) => r.direction === direction), [allRows, direction]);
|
||||||
const bulkReceive = useBulkReceive();
|
const bulkReceive = useMutation(api.warehouses.bulkReceive.mutationOptions());
|
||||||
const loadPassed = useLoadPassedExport();
|
const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions());
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const locationReady = Boolean(location.warehouseId && location.yardId && location.zoneId);
|
const locationReady = Boolean(location.warehouseId && location.yardId && location.zoneId);
|
||||||
@@ -177,10 +171,7 @@ function EligibleTab({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = (await bulkReceive.mutateAsync({ direction, ...location, bookingIds })) as {
|
const r = await bulkReceive.mutateAsync({ direction, ...location, bookingIds });
|
||||||
data: BulkReceiveResult;
|
|
||||||
};
|
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: `${r.receivedCount} received at ${direction === 'EXPORT' ? 'facility' : 'warehouse'}`,
|
title: `${r.receivedCount} received at ${direction === 'EXPORT' ? 'facility' : 'warehouse'}`,
|
||||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||||
@@ -194,8 +185,7 @@ function EligibleTab({
|
|||||||
|
|
||||||
const loadPassedExport = async () => {
|
const loadPassedExport = async () => {
|
||||||
try {
|
try {
|
||||||
const res = (await loadPassed.mutateAsync(undefined)) as { data: LoadPassedExportResult };
|
const r = await loadPassed.mutateAsync(undefined);
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: `${r.loadedCount} loaded`,
|
title: `${r.loadedCount} loaded`,
|
||||||
description: r.skippedCount ? `${r.skippedCount} skipped — inspection not passed` : undefined,
|
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. */
|
/** Export items that passed inspection and are queued to be loaded onto a train. */
|
||||||
function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) {
|
function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data: rows = [], isLoading } = useReadyToLoadExport(enabled);
|
const { data: rows = [], isLoading } = useQuery(
|
||||||
const loadPassed = useLoadPassedExport();
|
api.warehouses.readyToLoadExport.queryOptions({ enabled }),
|
||||||
|
);
|
||||||
|
const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions());
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const allSelected = rows.length > 0 && selected.size === rows.length;
|
const allSelected = rows.length > 0 && selected.size === rows.length;
|
||||||
@@ -369,8 +361,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
|||||||
|
|
||||||
const autoLoad = async () => {
|
const autoLoad = async () => {
|
||||||
try {
|
try {
|
||||||
const res = (await loadPassed.mutateAsync(undefined)) as { data: LoadPassedExportResult };
|
const r = await loadPassed.mutateAsync(undefined);
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: `${r.loadedCount} items loaded`,
|
title: `${r.loadedCount} items loaded`,
|
||||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||||
@@ -494,8 +485,12 @@ function LoadedExportTab({
|
|||||||
onChanged?: () => void;
|
onChanged?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data: rows = [], isLoading } = useLoadedExport(enabled);
|
const { data: rows = [], isLoading } = useQuery(
|
||||||
const bulkDispatch = useBulkDispatchExport();
|
api.warehouses.loadedExport.queryOptions({ enabled }),
|
||||||
|
);
|
||||||
|
const bulkDispatch = useMutation(
|
||||||
|
api.warehouses.bulkDispatchExport.mutationOptions(),
|
||||||
|
);
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const allSelected = rows.length > 0 && selected.size === rows.length;
|
const allSelected = rows.length > 0 && selected.size === rows.length;
|
||||||
@@ -514,8 +509,7 @@ function LoadedExportTab({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = (await bulkDispatch.mutateAsync(inventoryIds)) as { data: BulkDispatchResult };
|
const r = await bulkDispatch.mutateAsync(inventoryIds);
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: `${r.dispatchedCount} dispatched`,
|
title: `${r.dispatchedCount} dispatched`,
|
||||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
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). */
|
/** Assigned bookings/items for an arrived import train (read-only detail view). */
|
||||||
function ImportTrainDetailTable({ scheduleId }: { scheduleId: string }) {
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -735,18 +734,19 @@ function ImportArriveQueueTab({
|
|||||||
onChanged?: () => void;
|
onChanged?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data: trains = [], isLoading } = useImportArriveQueue(enabled);
|
const { data: trains = [], isLoading } = useQuery(
|
||||||
const autoUnloadMutation = useAutoUnloadArrivedBookings();
|
api.warehouses.importArriveQueue.queryOptions({ enabled }),
|
||||||
|
);
|
||||||
|
const autoUnloadMutation = useMutation(
|
||||||
|
api.warehouses.autoUnloadArrivedBookings.mutationOptions(),
|
||||||
|
);
|
||||||
const [openId, setOpenId] = useState<string | null>(null);
|
const [openId, setOpenId] = useState<string | null>(null);
|
||||||
const [busyId, setBusyId] = useState<string | null>(null);
|
const [busyId, setBusyId] = useState<string | null>(null);
|
||||||
|
|
||||||
const autoUnload = async (train: ImportTrain) => {
|
const autoUnload = async (train: ImportTrain) => {
|
||||||
setBusyId(train.scheduleId);
|
setBusyId(train.scheduleId);
|
||||||
try {
|
try {
|
||||||
const res = (await autoUnloadMutation.mutateAsync(train.scheduleId)) as {
|
const r = await autoUnloadMutation.mutateAsync(train.scheduleId);
|
||||||
data: AutoUnloadArrivedResult;
|
|
||||||
};
|
|
||||||
const r = res.data;
|
|
||||||
const extra = [
|
const extra = [
|
||||||
r.skippedCount ? `${r.skippedCount} skipped` : '',
|
r.skippedCount ? `${r.skippedCount} skipped` : '',
|
||||||
r.failedCount ? `${r.failedCount} failed` : '',
|
r.failedCount ? `${r.failedCount} failed` : '',
|
||||||
@@ -866,8 +866,12 @@ function ImportArriveQueueTab({
|
|||||||
*/
|
*/
|
||||||
function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data: rows = [], isLoading } = useImportUnloadedQueue(enabled);
|
const { data: rows = [], isLoading } = useQuery(
|
||||||
const inspectMutation = useBulkMarkInspected();
|
api.warehouses.importUnloadedQueue.queryOptions({ enabled }),
|
||||||
|
);
|
||||||
|
const inspectMutation = useMutation(
|
||||||
|
api.warehouses.bulkMarkInspected.mutationOptions(),
|
||||||
|
);
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
const [inspectId, setInspectId] = useState<string | null>(null);
|
const [inspectId, setInspectId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -888,10 +892,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = (await inspectMutation.mutateAsync({ inventoryIds: [...selected] })) as {
|
const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] });
|
||||||
data: BulkInspectResult;
|
|
||||||
};
|
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: `${r.inspectedCount} marked inspected`,
|
title: `${r.inspectedCount} marked inspected`,
|
||||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||||
@@ -1033,8 +1034,10 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
|||||||
*/
|
*/
|
||||||
function ImportDispatchQueueTab({ enabled }: { enabled: boolean }) {
|
function ImportDispatchQueueTab({ enabled }: { enabled: boolean }) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data: items = [], isLoading } = useWarehouseInventory(
|
const { data: items = [], isLoading } = useQuery(
|
||||||
enabled ? { status: 'READY_FOR_PICKUP' } : undefined,
|
api.warehouses.listInventory.queryOptions({
|
||||||
|
input: { filter: enabled ? { status: 'READY_FOR_PICKUP' } : undefined },
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1155,7 +1158,9 @@ function SingleBookingReceiveModal({
|
|||||||
onReceived,
|
onReceived,
|
||||||
}: ReceiveInventoryModalProps) {
|
}: ReceiveInventoryModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const receiveMutation = useReceiveInventory();
|
const receiveMutation = useMutation(
|
||||||
|
api.warehouses.receiveInventory.mutationOptions(),
|
||||||
|
);
|
||||||
const [selectedBooking, setSelectedBooking] = useState(bookingId ?? '');
|
const [selectedBooking, setSelectedBooking] = useState(bookingId ?? '');
|
||||||
const [form, setForm] = useState<SingleFormState>({
|
const [form, setForm] = useState<SingleFormState>({
|
||||||
warehouseId: '',
|
warehouseId: '',
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Alert, Button, Group, Modal, Stack, Text, TextInput } from '@mantine/core';
|
import { Alert, Button, Group, Modal, Stack, Text, TextInput } from '@mantine/core';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useReleaseInventory } from '@/hooks/useWarehouses';
|
|
||||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ interface ReleaseOrderModalProps {
|
|||||||
|
|
||||||
export function ReleaseOrderModal({ opened, onClose, item }: ReleaseOrderModalProps) {
|
export function ReleaseOrderModal({ opened, onClose, item }: ReleaseOrderModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const releaseMutation = useReleaseInventory();
|
const releaseMutation = useMutation(api.warehouses.release.mutationOptions());
|
||||||
const [reference, setReference] = useState('');
|
const [reference, setReference] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Alert, Button, Group, Modal, Stack, Text } from '@mantine/core';
|
import { Alert, Button, Group, Modal, Stack, Text } from '@mantine/core';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useReserveInventory } from '@/hooks/useWarehouses';
|
|
||||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
import { BookingSelect } from './BookingSelect';
|
import { BookingSelect } from './BookingSelect';
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
@@ -16,7 +18,7 @@ interface ReserveInventoryModalProps {
|
|||||||
|
|
||||||
export function ReserveInventoryModal({ opened, onClose, item }: ReserveInventoryModalProps) {
|
export function ReserveInventoryModal({ opened, onClose, item }: ReserveInventoryModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const reserveMutation = useReserveInventory();
|
const reserveMutation = useMutation(api.warehouses.reserve.mutationOptions());
|
||||||
const [bookingId, setBookingId] = useState('');
|
const [bookingId, setBookingId] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Select } from '@mantine/core';
|
import { Select } from '@mantine/core';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useLoadableWagons } from '@/hooks/useWarehouses';
|
import { api } from '@/services/api';
|
||||||
|
|
||||||
interface WagonSelectProps {
|
interface WagonSelectProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -11,7 +12,9 @@ interface WagonSelectProps {
|
|||||||
|
|
||||||
/** Searchable wagon picker. Lists wagons that are loadable (read-only from scheduling). */
|
/** Searchable wagon picker. Lists wagons that are loadable (read-only from scheduling). */
|
||||||
export function WagonSelect({ value, onChange, label = 'Wagon', required }: WagonSelectProps) {
|
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) => ({
|
const options = (data ?? []).map((w) => ({
|
||||||
value: w.id,
|
value: w.id,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { useMemo } from 'react';
|
|||||||
import { ActionIcon, Card, Group, SimpleGrid, Stack, Text } from '@mantine/core';
|
import { ActionIcon, Card, Group, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||||
import { Building2, Eye, MapPin, Pencil } from 'lucide-react';
|
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 type { Warehouse } from '@/types/warehouse';
|
||||||
import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges';
|
import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges';
|
||||||
import { formatCapacity } from './options';
|
import { formatCapacity } from './options';
|
||||||
@@ -14,7 +16,9 @@ interface WarehouseCardViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WarehouseCardView({ warehouses, onView, onEdit }: 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(
|
const stationNameById = useMemo(
|
||||||
() => new Map((stations ?? []).map((s) => [s.id, s.name])),
|
() => new Map((stations ?? []).map((s) => [s.id, s.name])),
|
||||||
[stations],
|
[stations],
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} 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';
|
import type { WarehouseDashboard, WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
|
|
||||||
interface WarehouseDashboardChartsProps {
|
interface WarehouseDashboardChartsProps {
|
||||||
@@ -38,7 +40,9 @@ type Granularity = 'week' | 'month' | 'year';
|
|||||||
|
|
||||||
export function WarehouseDashboardCharts({ data }: WarehouseDashboardChartsProps) {
|
export function WarehouseDashboardCharts({ data }: WarehouseDashboardChartsProps) {
|
||||||
const [granularity, setGranularity] = useState<Granularity>('month');
|
const [granularity, setGranularity] = useState<Granularity>('month');
|
||||||
const { data: inventory } = useWarehouseInventory();
|
const { data: inventory } = useQuery(
|
||||||
|
api.warehouses.listInventory.queryOptions({ input: {} }),
|
||||||
|
);
|
||||||
|
|
||||||
const statusData = STATUS_SERIES.map((s) => ({
|
const statusData = STATUS_SERIES.map((s) => ({
|
||||||
name: s.label,
|
name: s.label,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { useState } from 'react';
|
|||||||
import { Badge, Button, Card, Divider, Group, Stack, Text } from '@mantine/core';
|
import { Badge, Button, Card, Divider, Group, Stack, Text } from '@mantine/core';
|
||||||
import { PackagePlus, Train as TrainIcon, Warehouse as WarehouseIcon } from 'lucide-react';
|
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 { InventoryStatusBadge } from './badges';
|
||||||
import { FreightVisual } from './FreightVisual';
|
import { FreightVisual } from './FreightVisual';
|
||||||
import { formatDate } from './options';
|
import { formatDate } from './options';
|
||||||
@@ -28,8 +30,15 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) {
|
|||||||
|
|
||||||
export function WarehouseInfoCard({ bookingId, bookingReference }: WarehouseInfoCardProps) {
|
export function WarehouseInfoCard({ bookingId, bookingReference }: WarehouseInfoCardProps) {
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const { data, isLoading } = useWarehouseInventory({ bookingId });
|
const { data, isLoading } = useQuery(
|
||||||
const { data: scheduleView } = useBookingSchedule(bookingId);
|
api.warehouses.listInventory.queryOptions({ input: { filter: { bookingId } } }),
|
||||||
|
);
|
||||||
|
const { data: scheduleView } = useQuery(
|
||||||
|
api.warehouses.bookingSchedule.queryOptions({
|
||||||
|
input: { bookingId },
|
||||||
|
enabled: Boolean(bookingId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const items = data ?? [];
|
const items = data ?? [];
|
||||||
const latest = items[0];
|
const latest = items[0];
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { ActionIcon, Group, Text } from '@mantine/core';
|
|||||||
import { Eye, Pencil } from 'lucide-react';
|
import { Eye, Pencil } from 'lucide-react';
|
||||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
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 type { Warehouse } from '@/types/warehouse';
|
||||||
import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges';
|
import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges';
|
||||||
import { formatCapacity } from './options';
|
import { formatCapacity } from './options';
|
||||||
@@ -15,7 +17,9 @@ interface WarehouseTableProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WarehouseTable({ warehouses, onView, onEdit }: 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(
|
const stationNameById = useMemo(
|
||||||
() => new Map((stations ?? []).map((s) => [s.id, s.name])),
|
() => new Map((stations ?? []).map((s) => [s.id, s.name])),
|
||||||
[stations],
|
[stations],
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import type { FleetResourceSlug } from "@/pages/fleet/config/resources";
|
||||||
import type { BookingListFilter } from "@/services/bookings.service";
|
import type { BookingListFilter } from "@/services/bookings.service";
|
||||||
import type { RuleEngineListParams } from "@/services/ruleEngine/ruleEngine.service";
|
import type { RuleEngineListParams } from "@/services/ruleEngine/ruleEngine.service";
|
||||||
import type { TrainScheduleFilters } from "@/types/trainScheduling";
|
import type { CompanyListFilter } from "@/types/customer";
|
||||||
import type { FleetResourceSlug } from "@/pages/fleet/config/resources";
|
|
||||||
import type { RuleEngineResourceSlug } from "@/types/rule-engine";
|
import type { RuleEngineResourceSlug } from "@/types/rule-engine";
|
||||||
|
import type { TrainScheduleFilters } from "@/types/trainScheduling";
|
||||||
|
|
||||||
export const QUERY_KEYS = {
|
export const QUERY_KEYS = {
|
||||||
USERS: {
|
USERS: {
|
||||||
@@ -26,8 +27,13 @@ export const QUERY_KEYS = {
|
|||||||
|
|
||||||
CUSTOMERS: {
|
CUSTOMERS: {
|
||||||
ROOT: ["customers"] as const,
|
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,
|
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: {
|
BOOKINGS: {
|
||||||
|
|||||||
@@ -71,7 +71,12 @@ export const URL_CONSTANTS = {
|
|||||||
|
|
||||||
COMPANIES: {
|
COMPANIES: {
|
||||||
BASE: "/companies",
|
BASE: "/companies",
|
||||||
|
STATS: "/companies/stats",
|
||||||
BY_ID: (id: string | number) => `/companies/${id}`,
|
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: {
|
CUSTOMERS_API: {
|
||||||
|
|||||||
@@ -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<string, unknown>) => fleetService.create(slug, data),
|
|
||||||
onSuccess: invalidate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const update = useMutation({
|
|
||||||
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) =>
|
|
||||||
fleetService.update(slug, id, data),
|
|
||||||
onSuccess: invalidate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const remove = useMutation({
|
|
||||||
mutationFn: (id: string) => fleetService.remove(slug, id),
|
|
||||||
onSuccess: invalidate,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { create, update, remove };
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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<string, unknown> }) =>
|
|
||||||
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 }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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 })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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 })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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() }),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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<string, unknown> }) =>
|
|
||||||
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) });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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<string, unknown> }) =>
|
|
||||||
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) });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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() }) });
|
|
||||||
}
|
|
||||||
@@ -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 }) });
|
|
||||||
}
|
|
||||||
@@ -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<SaveWarehousePayload> }) =>
|
|
||||||
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<SaveYardPayload> }) =>
|
|
||||||
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<SaveZonePayload> }) =>
|
|
||||||
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<TArgs>(fn: (args: TArgs) => Promise<unknown>) {
|
|
||||||
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<string, unknown> }) =>
|
|
||||||
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<TArgs>(fn: (args: TArgs) => Promise<unknown>, 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<SaveAllocationRulePayload> }) =>
|
|
||||||
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<SaveFeeRulePayload> }) =>
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
@@ -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({
|
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: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
retry: 1,
|
retry: 1,
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -23,6 +19,8 @@ import {
|
|||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { isAxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -40,14 +38,16 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Weight,
|
Weight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
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 { api } from "@/auth/http";
|
||||||
import { unwrap } from "@/utils/endpoint";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import { URL_CONSTANTS } from "@/constants/URLS";
|
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 {
|
interface CompanyOption {
|
||||||
id: string;
|
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
|
// 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.
|
// train, no capacity). The batch engine assigns the train after booking.
|
||||||
const { data: availableDays, isLoading: daysLoading } = useAvailableDays(
|
const { data: availableDays, isLoading: daysLoading } = useQuery(
|
||||||
originYardId,
|
appApi.trainScheduling.availableDays.queryOptions({
|
||||||
destinationYardId,
|
input: { originYardId, destinationYardId },
|
||||||
|
enabled: Boolean(originYardId && destinationYardId),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const dayOptions = (availableDays ?? []).map((day) => ({
|
const dayOptions = (availableDays ?? []).map((day) => ({
|
||||||
value: day,
|
value: day,
|
||||||
@@ -392,7 +394,7 @@ export default function NewBookingPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Grid gutter="lg" mt="lg">
|
<Grid gap="lg" mt="lg">
|
||||||
{/* LEFT — form */}
|
{/* LEFT — form */}
|
||||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
@@ -453,7 +455,6 @@ export default function NewBookingPage() {
|
|||||||
value={originYardId}
|
value={originYardId}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
setOriginYardId(v);
|
setOriginYardId(v);
|
||||||
setTrainScheduleId(null);
|
|
||||||
}}
|
}}
|
||||||
searchable
|
searchable
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|||||||
@@ -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 (
|
return (
|
||||||
<FeaturePlaceholder
|
<Stack gap={2}>
|
||||||
title="Customer Detail"
|
<Text
|
||||||
description="View customer profile details, active shipments, and internal account notes."
|
size="xs"
|
||||||
/>
|
fw={600}
|
||||||
|
c="edr-muted"
|
||||||
|
tt="uppercase"
|
||||||
|
style={{ letterSpacing: "0.04em" }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="edr-text">
|
||||||
|
{value && value.trim() ? value : "—"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
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<CompanyProfile>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "type",
|
||||||
|
header: "Role",
|
||||||
|
cell: ({ row }) => <ProfileTypeBadge type={row.original.type} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "reference",
|
||||||
|
header: "Reference",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" fw={600} c="edr-text">
|
||||||
|
{row.original.reference}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "businessLicense",
|
||||||
|
header: "Business license",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{row.original.businessLicense || "—"}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => <ProfileStatusBadge status={row.original.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "createdAt",
|
||||||
|
header: "Registered",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{formatDate(row.original.createdAt)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "",
|
||||||
|
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ProfileApprovalActions
|
||||||
|
profileId={row.original.id}
|
||||||
|
status={row.original.status}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const bookingColumns: ColumnDef<CustomerBooking>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "reference",
|
||||||
|
header: "Booking",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" fw={600} c="edr-text">
|
||||||
|
{row.original.reference}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "route",
|
||||||
|
header: "Route",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const b = row.original;
|
||||||
|
return (
|
||||||
|
<Group gap={6} wrap="nowrap">
|
||||||
|
<Text size="sm" c="edr-text">
|
||||||
|
{b.originLabel}
|
||||||
|
</Text>
|
||||||
|
<ArrowRight size={14} className="shrink-0 text-edr-muted" />
|
||||||
|
<Text size="sm" c="edr-text">
|
||||||
|
{b.destinationLabel}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "type",
|
||||||
|
header: "Type",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{humanize(row.original.tradeDirection)} ·{" "}
|
||||||
|
{humanize(row.original.freightType)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => <BookingStatusBadge status={row.original.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "amount",
|
||||||
|
header: "Amount",
|
||||||
|
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" fw={600} c="edr-text">
|
||||||
|
{formatMoney(row.original.totalAmount, row.original.currency)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "createdAt",
|
||||||
|
header: "Created",
|
||||||
|
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{formatDate(row.original.createdAt)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const documentColumns: ColumnDef<CustomerDocument>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "name",
|
||||||
|
header: "Document",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<FileText size={16} className="shrink-0 text-edr-muted" />
|
||||||
|
<Text size="sm" c="edr-text" truncate>
|
||||||
|
{row.original.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "code",
|
||||||
|
header: "Type",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{humanize(row.original.code)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "size",
|
||||||
|
header: "Size",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{formatBytes(row.original.size)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "uploadedAt",
|
||||||
|
header: "Uploaded",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{formatDate(row.original.uploadedAt)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "",
|
||||||
|
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ActionIcon
|
||||||
|
component="a"
|
||||||
|
href={row.original.url ?? "#"}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Download"
|
||||||
|
data-stop-row-click
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const paymentColumns: ColumnDef<CustomerPayment>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "reference",
|
||||||
|
header: "Payment",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" fw={600} c="edr-text">
|
||||||
|
{row.original.reference}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "booking",
|
||||||
|
header: "Booking",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{row.original.bookingReference}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "method",
|
||||||
|
header: "Method",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{humanize(row.original.method)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => <PaymentStatusBadge status={row.original.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "paidAt",
|
||||||
|
header: "Paid",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{formatDate(row.original.paidAt)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "amount",
|
||||||
|
header: "Amount",
|
||||||
|
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" fw={600} c="edr-text">
|
||||||
|
{formatMoney(row.original.amount, row.original.currency)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Center mih="60vh">
|
||||||
|
<Loader />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!company) {
|
||||||
|
return (
|
||||||
|
<Container size="sm" py="xl">
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<Text fw={700}>Customer not found</Text>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<ArrowLeft size={16} />}
|
||||||
|
onClick={() => navigate("/dashboard/customers")}
|
||||||
|
>
|
||||||
|
Back to customers
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: "Customers", href: "/dashboard/customers" },
|
||||||
|
{ label: company.name },
|
||||||
|
]}
|
||||||
|
backTo="/dashboard/customers"
|
||||||
|
title={company.name}
|
||||||
|
subtitle={`TIN ${company.tin}${
|
||||||
|
company.country ? ` · ${company.country}` : ""
|
||||||
|
}`}
|
||||||
|
meta={
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<CompanyTypeBadge type={company.type} />
|
||||||
|
<CompanyStatusBadge status={company.status} />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs defaultValue="overview">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="overview" leftSection={<LayoutGrid size={16} />}>
|
||||||
|
Overview
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="bookings" leftSection={<Package size={16} />}>
|
||||||
|
Bookings
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="documents" leftSection={<FileText size={16} />}>
|
||||||
|
Documents
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="payments" leftSection={<Banknote size={16} />}>
|
||||||
|
Payments
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
{/* OVERVIEW */}
|
||||||
|
<Tabs.Panel value="overview" pt="lg">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<KpiStrip
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Profiles",
|
||||||
|
value: company.companyProfiles.length,
|
||||||
|
icon: IdCard,
|
||||||
|
color: "edr-green",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pending approval",
|
||||||
|
value: company.companyProfiles.filter(
|
||||||
|
(p) => 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",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Text fw={600} c="edr-text">
|
||||||
|
Company information
|
||||||
|
</Text>
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||||
|
<InfoField label="TIN" value={company.tin} />
|
||||||
|
<InfoField label="VAT number" value={company.vatNumber} />
|
||||||
|
<InfoField label="FAN number" value={company.fanNumber} />
|
||||||
|
<InfoField label="Country" value={company.country} />
|
||||||
|
<InfoField label="Address" value={company.address} />
|
||||||
|
<InfoField label="Website" value={company.website} />
|
||||||
|
<InfoField label="Email" value={company.email} />
|
||||||
|
<InfoField label="Phone" value={company.phone} />
|
||||||
|
<Box />
|
||||||
|
<InfoField
|
||||||
|
label="Contact person"
|
||||||
|
value={company.contactPersonName}
|
||||||
|
/>
|
||||||
|
<InfoField
|
||||||
|
label="Contact phone"
|
||||||
|
value={company.contactPersonPhone}
|
||||||
|
/>
|
||||||
|
<Box />
|
||||||
|
<InfoField
|
||||||
|
label="General manager"
|
||||||
|
value={company.generalManagerName}
|
||||||
|
/>
|
||||||
|
<InfoField
|
||||||
|
label="GM email"
|
||||||
|
value={company.generalManagerEmail}
|
||||||
|
/>
|
||||||
|
<InfoField
|
||||||
|
label="GM phone"
|
||||||
|
value={company.generalManagerPhone}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={600} c="edr-text">
|
||||||
|
Role profiles
|
||||||
|
</Text>
|
||||||
|
<ProfileChips profiles={company.companyProfiles} />
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: "auto" }} w="100%">
|
||||||
|
<Box miw={860}>
|
||||||
|
<DataTable
|
||||||
|
columns={profileColumns}
|
||||||
|
data={company.companyProfiles}
|
||||||
|
status="success"
|
||||||
|
emptyMessage="No profiles registered."
|
||||||
|
containerClassName="border-0 shadow-none bg-transparent"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
{/* BOOKINGS */}
|
||||||
|
<Tabs.Panel value="bookings" pt="lg">
|
||||||
|
<TableCard minWidth={900}>
|
||||||
|
<DataTable
|
||||||
|
columns={bookingColumns}
|
||||||
|
data={bookings}
|
||||||
|
status={tableStatus(bookingsQuery)}
|
||||||
|
emptyMessage="No bookings for this customer."
|
||||||
|
containerClassName="border-0 shadow-none bg-transparent"
|
||||||
|
error={
|
||||||
|
bookingsQuery.isError
|
||||||
|
? {
|
||||||
|
message: "Failed to load bookings.",
|
||||||
|
onRetry: () => void bookingsQuery.refetch(),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCard>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
{/* DOCUMENTS */}
|
||||||
|
<Tabs.Panel value="documents" pt="lg">
|
||||||
|
<TableCard minWidth={760}>
|
||||||
|
<DataTable
|
||||||
|
columns={documentColumns}
|
||||||
|
data={documents}
|
||||||
|
status={tableStatus(documentsQuery)}
|
||||||
|
emptyMessage="No documents uploaded."
|
||||||
|
containerClassName="border-0 shadow-none bg-transparent"
|
||||||
|
error={
|
||||||
|
documentsQuery.isError
|
||||||
|
? {
|
||||||
|
message: "Failed to load documents.",
|
||||||
|
onRetry: () => void documentsQuery.refetch(),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCard>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
{/* PAYMENTS */}
|
||||||
|
<Tabs.Panel value="payments" pt="lg">
|
||||||
|
<TableCard minWidth={880}>
|
||||||
|
<DataTable
|
||||||
|
columns={paymentColumns}
|
||||||
|
data={payments}
|
||||||
|
status={tableStatus(paymentsQuery)}
|
||||||
|
emptyMessage="No payments recorded."
|
||||||
|
containerClassName="border-0 shadow-none bg-transparent"
|
||||||
|
error={
|
||||||
|
paymentsQuery.isError
|
||||||
|
? {
|
||||||
|
message: "Failed to load payments.",
|
||||||
|
onRetry: () => void paymentsQuery.refetch(),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCard>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 = () => {
|
import {
|
||||||
return (
|
CompanyStatusBadge,
|
||||||
<FeaturePlaceholder
|
CompanyTypeBadge,
|
||||||
title="Customers"
|
ProfileChips,
|
||||||
description="Review and maintain customer records, service status, and operational context."
|
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<Company>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "company",
|
||||||
|
header: "Company",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const c = row.original;
|
||||||
|
return (
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
className="flex size-9 shrink-0 items-center justify-center rounded-lg"
|
||||||
|
style={{
|
||||||
|
background: "var(--mantine-color-edr-green-1)",
|
||||||
|
color: "var(--mantine-color-edr-green-7)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Building2 size={18} strokeWidth={1.9} />
|
||||||
|
</Box>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<Group gap={8} wrap="nowrap">
|
||||||
|
<Text fw={600} c="edr-text" truncate>
|
||||||
|
{c.name}
|
||||||
|
</Text>
|
||||||
|
<CompanyTypeBadge type={c.type} />
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
TIN {c.tin}
|
||||||
|
{c.country ? ` · ${c.country}` : ""}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "profiles",
|
||||||
|
header: "Profiles",
|
||||||
|
cell: ({ row }) => <ProfileChips profiles={row.original.companyProfiles} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => <CompanyStatusBadge status={row.original.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "contact",
|
||||||
|
header: "Contact",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const c = row.original;
|
||||||
|
return (
|
||||||
|
<Stack gap={2}>
|
||||||
|
{c.contactPersonName ? (
|
||||||
|
<Text size="sm" c="edr-text">
|
||||||
|
{c.contactPersonName}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{c.phone ? (
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="dimmed"
|
||||||
|
className="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Phone size={12} /> {c.phone}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{c.email ? (
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="dimmed"
|
||||||
|
className="inline-flex items-center gap-1"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
<Mail size={12} /> {c.email}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "created",
|
||||||
|
header: "Registered",
|
||||||
|
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{formatDate(row.original.createdAt)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader
|
||||||
|
title="Customers"
|
||||||
|
subtitle="Companies registered for freight services, with their role profiles."
|
||||||
|
action={
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
radius="md"
|
||||||
|
aria-label="Refresh"
|
||||||
|
loading={isFetching}
|
||||||
|
onClick={() => void refetch()}
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KpiStrip
|
||||||
|
items={[
|
||||||
|
{ label: "Companies", value: stats?.total ?? "—", icon: Users, color: "edr-green" },
|
||||||
|
{ label: "Active", value: stats?.active ?? "—", icon: CheckCircle2, color: "edr-green" },
|
||||||
|
{ label: "Pending", value: stats?.pending ?? "—", icon: Clock, color: "yellow" },
|
||||||
|
{
|
||||||
|
label: "Blacklisted",
|
||||||
|
value: stats?.blacklisted ?? "—",
|
||||||
|
icon: ShieldOff,
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card p={0}>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Box px="md" pt="md" pb="sm" w="100%">
|
||||||
|
<Group justify="space-between" gap="md" wrap="wrap">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search by company, TIN, email or profile reference…"
|
||||||
|
leftSection={<Search size={18} />}
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
rightSection={
|
||||||
|
query ? (
|
||||||
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={() => setQuery("")}
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
style={{ flex: 1, minWidth: "240px" }}
|
||||||
|
radius="lg"
|
||||||
|
/>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{total} record{total !== 1 ? "s" : ""}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box style={{ overflowX: "auto" }} w="100%">
|
||||||
|
<Box miw={980}>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={rows}
|
||||||
|
status={isLoading ? "loading" : isError ? "error" : "success"}
|
||||||
|
onRowClick={(row) => 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}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { FileUploadEntity } from "@edr/types/freight";
|
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 {
|
// import type {
|
||||||
// FileUploadEntity,
|
// FileUploadEntity,
|
||||||
@@ -61,8 +62,8 @@ export default function EditFileUploadSettingDialog({
|
|||||||
const [description, setDescription] = useState(setting?.description ?? "");
|
const [description, setDescription] = useState(setting?.description ?? "");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const createMutation = useCreateFileUploadSetting();
|
const createMutation = useMutation(api.fileUploadSettings.create.mutationOptions());
|
||||||
const updateMutation = useUpdateFileUploadSetting();
|
const updateMutation = useMutation(api.fileUploadSettings.update.mutationOptions());
|
||||||
const pending = createMutation.isPending || updateMutation.isPending;
|
const pending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { useDeleteFileUploadSetting } from "@/hooks/useFileUploadSettings";
|
|
||||||
import { getMinFiles, type FileUploadSetting } from "@/types/fileUploadSettings";
|
import { getMinFiles, type FileUploadSetting } from "@/types/fileUploadSettings";
|
||||||
import { DataTable, type ColumnDef } from "@edr/ui-common";
|
import { DataTable, type ColumnDef } from "@edr/ui-common";
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ export default function FileUploadSettingsPage() {
|
|||||||
const { data, isLoading, isError, error, refetch } = useQuery(
|
const { data, isLoading, isError, error, refetch } = useQuery(
|
||||||
api.fileUploadSettings.list.queryOptions(),
|
api.fileUploadSettings.list.queryOptions(),
|
||||||
);
|
);
|
||||||
const deleteMutation = useDeleteFileUploadSetting();
|
const deleteMutation = useMutation(api.fileUploadSettings.remove.mutationOptions());
|
||||||
|
|
||||||
const fileUploadSettings = useMemo(
|
const fileUploadSettings = useMemo(
|
||||||
() => (Array.isArray(data) ? data : []),
|
() => (Array.isArray(data) ? data : []),
|
||||||
@@ -202,7 +201,7 @@ export default function FileUploadSettingsPage() {
|
|||||||
<DeleteFileUploadSettingDialog
|
<DeleteFileUploadSettingDialog
|
||||||
settingLabel={setting.label}
|
settingLabel={setting.label}
|
||||||
settingCode={setting.code}
|
settingCode={setting.code}
|
||||||
onConfirm={() => deleteMutation.mutate(setting.id)}
|
onConfirm={() => deleteMutation.mutate({ id: setting.id })}
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import type {
|
|||||||
FileUploadSetting,
|
FileUploadSetting,
|
||||||
} from "@/types/fileUploadSettings";
|
} from "@/types/fileUploadSettings";
|
||||||
import { getMinFiles } from "@/types/fileUploadSettings";
|
import { getMinFiles } from "@/types/fileUploadSettings";
|
||||||
import { useReplaceFileUploadFields } from "@/hooks/useFileUploadSettings";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { api } from "@/services/api";
|
||||||
|
|
||||||
export interface ManageFileUploadFieldsDialogProps {
|
export interface ManageFileUploadFieldsDialogProps {
|
||||||
setting: FileUploadSetting;
|
setting: FileUploadSetting;
|
||||||
@@ -77,7 +78,9 @@ export default function ManageFileUploadFieldsDialog({
|
|||||||
|
|
||||||
const [fields, setFields] = useState<DraftField[]>(seed);
|
const [fields, setFields] = useState<DraftField[]>(seed);
|
||||||
|
|
||||||
const replaceMutation = useReplaceFileUploadFields();
|
const replaceMutation = useMutation(
|
||||||
|
api.fileUploadSettings.replaceFields.mutationOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
const update = (i: number, patch: Partial<DraftField>) =>
|
const update = (i: number, patch: Partial<DraftField>) =>
|
||||||
setFields((prev) =>
|
setFields((prev) =>
|
||||||
@@ -145,7 +148,7 @@ export default function ManageFileUploadFieldsDialog({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
replaceMutation.mutate(
|
replaceMutation.mutate(
|
||||||
{ settingId: setting.id, fields: payload },
|
{ id: setting.id, fields: payload },
|
||||||
{
|
{
|
||||||
onSuccess: () => setOpen(false),
|
onSuccess: () => setOpen(false),
|
||||||
onError: (err) =>
|
onError: (err) =>
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
|||||||
import EditDropdownSettingDialog from "./EditDropdownSettingDialog";
|
import EditDropdownSettingDialog from "./EditDropdownSettingDialog";
|
||||||
import ManageDropdownOptionsDialog from "./ManageDropdownOptionsDialog";
|
import ManageDropdownOptionsDialog from "./ManageDropdownOptionsDialog";
|
||||||
import DeleteDropdownSettingDialog from "./DeleteDropdownSettingDialog";
|
import DeleteDropdownSettingDialog from "./DeleteDropdownSettingDialog";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { useDeleteDropdownSetting } from "@/hooks/useDropdownSettings";
|
|
||||||
import type { DropdownSetting } from "@/types/dropdownSettings";
|
import type { DropdownSetting } from "@/types/dropdownSettings";
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
@@ -63,7 +62,7 @@ export default function DropdownSettingsPage() {
|
|||||||
const { data, isLoading, isError, error } = useQuery(
|
const { data, isLoading, isError, error } = useQuery(
|
||||||
api.dropdownSettings.list.queryOptions(),
|
api.dropdownSettings.list.queryOptions(),
|
||||||
);
|
);
|
||||||
const deleteMutation = useDeleteDropdownSetting();
|
const deleteMutation = useMutation(api.dropdownSettings.remove.mutationOptions());
|
||||||
|
|
||||||
const dropdownSettings = useMemo<DropdownSetting[]>(
|
const dropdownSettings = useMemo<DropdownSetting[]>(
|
||||||
() => (Array.isArray(data) ? data : []),
|
() => (Array.isArray(data) ? data : []),
|
||||||
@@ -353,7 +352,7 @@ export default function DropdownSettingsPage() {
|
|||||||
key={`delete-${activeSetting.id}`}
|
key={`delete-${activeSetting.id}`}
|
||||||
settingLabel={activeSetting.label}
|
settingLabel={activeSetting.label}
|
||||||
settingCode={activeSetting.code}
|
settingCode={activeSetting.code}
|
||||||
onConfirm={() => deleteMutation.mutate(activeSetting.id)}
|
onConfirm={() => deleteMutation.mutate({ id: activeSetting.id })}
|
||||||
open={activeDialog === "delete"}
|
open={activeDialog === "delete"}
|
||||||
onOpenChange={(next) => (next ? null : closeDialog())}
|
onOpenChange={(next) => (next ? null : closeDialog())}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import type {
|
|||||||
DropdownSetting,
|
DropdownSetting,
|
||||||
UpdateDropdownSettingDto,
|
UpdateDropdownSettingDto,
|
||||||
} from "@/types/dropdownSettings";
|
} from "@/types/dropdownSettings";
|
||||||
import {
|
import { useMutation } from "@tanstack/react-query";
|
||||||
useCreateDropdownSetting,
|
|
||||||
useUpdateDropdownSetting,
|
import { api } from "@/services/api";
|
||||||
} from "@/hooks/useDropdownSettings";
|
|
||||||
|
|
||||||
export interface EditDropdownSettingDialogProps {
|
export interface EditDropdownSettingDialogProps {
|
||||||
mode?: "create" | "edit";
|
mode?: "create" | "edit";
|
||||||
@@ -76,8 +75,8 @@ export default function EditDropdownSettingDialog({
|
|||||||
);
|
);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const createMutation = useCreateDropdownSetting();
|
const createMutation = useMutation(api.dropdownSettings.create.mutationOptions());
|
||||||
const updateMutation = useUpdateDropdownSetting();
|
const updateMutation = useMutation(api.dropdownSettings.update.mutationOptions());
|
||||||
const pending = createMutation.isPending || updateMutation.isPending;
|
const pending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import type {
|
|||||||
CreateDropdownOptionDto,
|
CreateDropdownOptionDto,
|
||||||
DropdownSetting,
|
DropdownSetting,
|
||||||
} from "@/types/dropdownSettings";
|
} from "@/types/dropdownSettings";
|
||||||
import { useReplaceDropdownOptions } from "@/hooks/useDropdownSettings";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { api } from "@/services/api";
|
||||||
|
|
||||||
export interface ManageDropdownOptionsDialogProps {
|
export interface ManageDropdownOptionsDialogProps {
|
||||||
setting: DropdownSetting;
|
setting: DropdownSetting;
|
||||||
@@ -84,7 +85,9 @@ export default function ManageDropdownOptionsDialog({
|
|||||||
|
|
||||||
const [options, setOptions] = useState<DraftOption[]>(seed);
|
const [options, setOptions] = useState<DraftOption[]>(seed);
|
||||||
|
|
||||||
const replaceMutation = useReplaceDropdownOptions();
|
const replaceMutation = useMutation(
|
||||||
|
api.dropdownSettings.replaceOptions.mutationOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
const update = (i: number, patch: Partial<DraftOption>) =>
|
const update = (i: number, patch: Partial<DraftOption>) =>
|
||||||
setOptions((prev) =>
|
setOptions((prev) =>
|
||||||
@@ -147,7 +150,7 @@ export default function ManageDropdownOptionsDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
replaceMutation.mutate(
|
replaceMutation.mutate(
|
||||||
{ settingId: setting.id, options: payload },
|
{ id: setting.id, options: payload },
|
||||||
{
|
{
|
||||||
onSuccess: () => setOpen(false),
|
onSuccess: () => setOpen(false),
|
||||||
onError: (err) =>
|
onError: (err) =>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { FormEvent, ReactNode, useMemo, useState } from 'react';
|
import { FormEvent, ReactNode, useMemo, useState } from 'react';
|
||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { Edit, Eye, Plus, Search, Trash2 } from 'lucide-react';
|
import { Edit, Eye, Plus, Search, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
import { api } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Badge as MantineBadge,
|
Badge as MantineBadge,
|
||||||
@@ -33,31 +36,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
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 { 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 type { Cargo } from '@/services/cargoService';
|
||||||
import { DeliverCargoDialog } from '@/components/cargoes/DeliverCargoDialog';
|
import { DeliverCargoDialog } from '@/components/cargoes/DeliverCargoDialog';
|
||||||
import type { Container } from '@/services/containerService';
|
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 ?? '-';
|
options.find((option) => option.value === value)?.label ?? value ?? '-';
|
||||||
|
|
||||||
export function TrainMasterDataPage() {
|
export function TrainMasterDataPage() {
|
||||||
const query = useTrains();
|
const query = useQuery(api.trains.list.queryOptions());
|
||||||
return (
|
return (
|
||||||
<FleetCrudPage<Train>
|
<FleetCrudPage<Train>
|
||||||
title="Trains"
|
title="Trains"
|
||||||
@@ -519,9 +498,9 @@ export function TrainMasterDataPage() {
|
|||||||
addLabel="Add Train"
|
addLabel="Add Train"
|
||||||
data={query.data}
|
data={query.data}
|
||||||
isLoading={query.isLoading}
|
isLoading={query.isLoading}
|
||||||
create={useCreateTrain()}
|
create={useMutation(api.trains.create.mutationOptions())}
|
||||||
update={useUpdateTrain()}
|
update={useMutation(api.trains.update.mutationOptions())}
|
||||||
remove={useDeleteTrain()}
|
remove={useMutation(api.trains.remove.mutationOptions())}
|
||||||
searchText={(train) => [train.code, train.trainNumber, train.trainName, train.status].join(' ')}
|
searchText={(train) => [train.code, train.trainNumber, train.trainName, train.status].join(' ')}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'code', label: 'Code' },
|
{ key: 'code', label: 'Code' },
|
||||||
@@ -546,10 +525,10 @@ export function TrainMasterDataPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WagonTypesCrudPage() {
|
export function WagonTypesCrudPage() {
|
||||||
const query = useWagonTypes();
|
const query = useQuery(api.wagonTypes.list.queryOptions());
|
||||||
const create = useCreateWagonType();
|
const create = useMutation(api.wagonTypes.create.mutationOptions());
|
||||||
const update = useUpdateWagonType();
|
const update = useMutation(api.wagonTypes.update.mutationOptions());
|
||||||
const remove = useDeleteWagonType();
|
const remove = useMutation(api.wagonTypes.remove.mutationOptions());
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -896,9 +875,9 @@ export function WagonTypesCrudPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WagonsCrudPage() {
|
export function WagonsCrudPage() {
|
||||||
const query = useWagons();
|
const query = useQuery(api.wagons.list.queryOptions({ input: {} }));
|
||||||
const { data: wagonTypes = [] } = useWagonTypes();
|
const { data: wagonTypes = [] } = useQuery(api.wagonTypes.list.queryOptions());
|
||||||
const { data: yards = [] } = useRouteYards();
|
const { data: yards = [] } = useQuery(api.routes.yards.queryOptions());
|
||||||
const wagonTypeOptions = wagonTypes.map((type: any) => ({
|
const wagonTypeOptions = wagonTypes.map((type: any) => ({
|
||||||
value: type.id,
|
value: type.id,
|
||||||
label: `${type.code} - ${type.name}`,
|
label: `${type.code} - ${type.name}`,
|
||||||
@@ -914,9 +893,9 @@ export function WagonsCrudPage() {
|
|||||||
addLabel="Add Wagon"
|
addLabel="Add Wagon"
|
||||||
data={query.data}
|
data={query.data}
|
||||||
isLoading={query.isLoading}
|
isLoading={query.isLoading}
|
||||||
create={useCreateWagon()}
|
create={useMutation(api.wagons.create.mutationOptions())}
|
||||||
update={useUpdateWagon()}
|
update={useMutation(api.wagons.update.mutationOptions())}
|
||||||
remove={useDeleteWagon()}
|
remove={useMutation(api.wagons.remove.mutationOptions())}
|
||||||
searchText={(wagon) => [
|
searchText={(wagon) => [
|
||||||
wagon.wagonNumber,
|
wagon.wagonNumber,
|
||||||
wagon.wagonTypeId,
|
wagon.wagonTypeId,
|
||||||
@@ -984,9 +963,11 @@ export function WagonsCrudPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ContainersCrudPage() {
|
export function ContainersCrudPage() {
|
||||||
const query = useContainers();
|
const query = useQuery(api.containers.list.queryOptions());
|
||||||
const { data: containerTypes = [] } = useContainerTypes();
|
const { data: containerTypes = [] } = useQuery(
|
||||||
const { data: wagons = [] } = useWagons();
|
api.containerTypes.list.queryOptions({ staleTime: Infinity }),
|
||||||
|
);
|
||||||
|
const { data: wagons = [] } = useQuery(api.wagons.list.queryOptions({ input: {} }));
|
||||||
const containerTypeOptions = containerTypes.map((type: any) => ({
|
const containerTypeOptions = containerTypes.map((type: any) => ({
|
||||||
value: type.id,
|
value: type.id,
|
||||||
label: type.label ?? type.name ?? type.code,
|
label: type.label ?? type.name ?? type.code,
|
||||||
@@ -1002,9 +983,9 @@ export function ContainersCrudPage() {
|
|||||||
addLabel="Add Container"
|
addLabel="Add Container"
|
||||||
data={query.data}
|
data={query.data}
|
||||||
isLoading={query.isLoading}
|
isLoading={query.isLoading}
|
||||||
create={useCreateContainer()}
|
create={useMutation(api.containers.create.mutationOptions())}
|
||||||
update={useUpdateContainer()}
|
update={useMutation(api.containers.update.mutationOptions())}
|
||||||
remove={useDeleteContainer()}
|
remove={useMutation(api.containers.remove.mutationOptions())}
|
||||||
searchText={(container) => [container.containerNumber, container.containerTypeId, container.wagonId, container.status].join(' ')}
|
searchText={(container) => [container.containerNumber, container.containerTypeId, container.wagonId, container.status].join(' ')}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'containerNumber', label: 'Number' },
|
{ key: 'containerNumber', label: 'Number' },
|
||||||
@@ -1041,9 +1022,11 @@ export function ContainersCrudPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CargoesCrudPage() {
|
export function CargoesCrudPage() {
|
||||||
const query = useCargoes();
|
const query = useQuery(api.cargoes.list.queryOptions());
|
||||||
const { data: cargoTypes = [] } = useCargoTypes();
|
const { data: cargoTypes = [] } = useQuery(
|
||||||
const { data: containers = [] } = useContainers();
|
api.cargoTypes.list.queryOptions({ staleTime: Infinity }),
|
||||||
|
);
|
||||||
|
const { data: containers = [] } = useQuery(api.containers.list.queryOptions());
|
||||||
const cargoTypeOptions = cargoTypes.map((type: any) => ({
|
const cargoTypeOptions = cargoTypes.map((type: any) => ({
|
||||||
value: type.id,
|
value: type.id,
|
||||||
label: type.cargoTypeName ?? type.cargo_type_name ?? type.name ?? type.code,
|
label: type.cargoTypeName ?? type.cargo_type_name ?? type.name ?? type.code,
|
||||||
@@ -1059,9 +1042,9 @@ export function CargoesCrudPage() {
|
|||||||
addLabel="Add Cargo"
|
addLabel="Add Cargo"
|
||||||
data={query.data}
|
data={query.data}
|
||||||
isLoading={query.isLoading}
|
isLoading={query.isLoading}
|
||||||
create={useCreateCargo()}
|
create={useMutation(api.cargoes.create.mutationOptions())}
|
||||||
update={useUpdateCargo()}
|
update={useMutation(api.cargoes.update.mutationOptions())}
|
||||||
remove={useDeleteCargo()}
|
remove={useMutation(api.cargoes.remove.mutationOptions())}
|
||||||
searchText={(cargo) => [cargo.cargoReference, cargo.description, cargo.containerId, cargo.status].join(' ')}
|
searchText={(cargo) => [cargo.cargoReference, cargo.description, cargo.containerId, cargo.status].join(' ')}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'cargoReference', label: 'Reference' },
|
{ key: 'cargoReference', label: 'Reference' },
|
||||||
@@ -1110,7 +1093,7 @@ export function CargoesCrudPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LocomotivesCrudPage() {
|
export function LocomotivesCrudPage() {
|
||||||
const query = useLocomotives();
|
const query = useQuery(api.locomotives.list.queryOptions());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FleetCrudPage<Locomotive>
|
<FleetCrudPage<Locomotive>
|
||||||
@@ -1120,9 +1103,9 @@ export function LocomotivesCrudPage() {
|
|||||||
addLabel="Add Locomotive"
|
addLabel="Add Locomotive"
|
||||||
data={query.data}
|
data={query.data}
|
||||||
isLoading={query.isLoading}
|
isLoading={query.isLoading}
|
||||||
create={useCreateLocomotive()}
|
create={useMutation(api.locomotives.create.mutationOptions())}
|
||||||
update={useUpdateLocomotive()}
|
update={useMutation(api.locomotives.update.mutationOptions())}
|
||||||
remove={useDecommissionLocomotive()}
|
remove={useMutation(api.locomotives.decommission.mutationOptions())}
|
||||||
removeActionLabel="Decommission"
|
removeActionLabel="Decommission"
|
||||||
removeConfirmMessage="Decommission this locomotive?"
|
removeConfirmMessage="Decommission this locomotive?"
|
||||||
removeSuccessMessage="Locomotive decommissioned"
|
removeSuccessMessage="Locomotive decommissioned"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ColumnDef } from "@edr/ui-common";
|
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 { api } from "@/services/api";
|
||||||
import {Container, Title, Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core";
|
|
||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
Circle,
|
Circle,
|
||||||
@@ -24,14 +25,7 @@ import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/f
|
|||||||
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
|
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
|
||||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
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 { 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 {
|
import {
|
||||||
FLEET_SELECT_NONE,
|
FLEET_SELECT_NONE,
|
||||||
getFleetResource,
|
getFleetResource,
|
||||||
@@ -94,16 +88,31 @@ const FleetResourcePage = () => {
|
|||||||
return filters;
|
return filters;
|
||||||
}, [slug, listFilterValues, search]);
|
}, [slug, listFilterValues, search]);
|
||||||
|
|
||||||
const { data: allRows = [], isLoading, isError, error } = useFleetList(slug, serverListFilters);
|
const { data: allRows = [], isLoading, isError, error } = useQuery(
|
||||||
const { data: drivers = [] } = useFleetList("drivers");
|
api.fleet.list.queryOptions({ input: { slug, filters: serverListFilters } }),
|
||||||
const { create, update, remove } = useFleetMutations(slug);
|
);
|
||||||
|
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: wagonTypes = [], isLoading: wagonTypesLoading } = useQuery(
|
||||||
const { data: containerTypes = [], isLoading: containerTypesLoading } = useContainerTypes();
|
api.wagonTypes.list.queryOptions(),
|
||||||
const { data: cargoTypes = [], isLoading: cargoTypesLoading } = useCargoTypes();
|
);
|
||||||
const { data: wagons = [], isLoading: wagonsLoading } = useWagons();
|
const { data: containerTypes = [], isLoading: containerTypesLoading } = useQuery(
|
||||||
const { data: containers = [], isLoading: containersLoading } = useContainers();
|
api.containerTypes.list.queryOptions({ staleTime: Infinity }),
|
||||||
const { data: yards = [], isLoading: yardsLoading } = useRouteYards();
|
);
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize }));
|
setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize }));
|
||||||
@@ -332,10 +341,10 @@ const FleetResourcePage = () => {
|
|||||||
const handleFormSubmit = async (values: Record<string, unknown>) => {
|
const handleFormSubmit = async (values: Record<string, unknown>) => {
|
||||||
try {
|
try {
|
||||||
if (editing && "id" in editing) {
|
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` });
|
toast({ title: `${config.entityLabel} updated` });
|
||||||
} else {
|
} else {
|
||||||
await create.mutateAsync(values);
|
await create.mutateAsync({ slug, data: values });
|
||||||
toast({ title: `${config.entityLabel} created` });
|
toast({ title: `${config.entityLabel} created` });
|
||||||
}
|
}
|
||||||
setFormOpen(false);
|
setFormOpen(false);
|
||||||
@@ -351,7 +360,7 @@ const FleetResourcePage = () => {
|
|||||||
const handleRemove = async () => {
|
const handleRemove = async () => {
|
||||||
if (!removeTarget || !("id" in removeTarget)) return;
|
if (!removeTarget || !("id" in removeTarget)) return;
|
||||||
try {
|
try {
|
||||||
await remove.mutateAsync(String(removeTarget.id));
|
await remove.mutateAsync({ slug, id: String(removeTarget.id) });
|
||||||
toast({
|
toast({
|
||||||
title: config.removeSuccessMessage ?? `${config.entityLabel} removed`,
|
title: config.removeSuccessMessage ?? `${config.entityLabel} removed`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,18 +17,14 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||||
import FleetToolbar from "@/components/fleet/FleetToolbar";
|
import FleetToolbar from "@/components/fleet/FleetToolbar";
|
||||||
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
|
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
import {
|
import { api } from "@/services/api";
|
||||||
useCreateRoute,
|
|
||||||
useDeactivateRoute,
|
|
||||||
useRouteYards,
|
|
||||||
useRoutes,
|
|
||||||
useUpdateRoute,
|
|
||||||
} from "@/hooks/useRoutes";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { RouteRecord, YardRef } from "@/services/routes.service";
|
import type { RouteRecord, YardRef } from "@/services/routes.service";
|
||||||
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
||||||
@@ -71,11 +67,11 @@ export default function RoutesPage() {
|
|||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const routesQuery = useRoutes();
|
const routesQuery = useQuery(api.routes.list.queryOptions());
|
||||||
const yardsQuery = useRouteYards();
|
const yardsQuery = useQuery(api.routes.yards.queryOptions());
|
||||||
const createMutation = useCreateRoute();
|
const createMutation = useMutation(api.routes.create.mutationOptions());
|
||||||
const updateMutation = useUpdateRoute();
|
const updateMutation = useMutation(api.routes.update.mutationOptions());
|
||||||
const deactivateMutation = useDeactivateRoute();
|
const deactivateMutation = useMutation(api.routes.deactivate.mutationOptions());
|
||||||
|
|
||||||
const filteredRoutes = useMemo(() => {
|
const filteredRoutes = useMemo(() => {
|
||||||
const query = search.trim().toLowerCase();
|
const query = search.trim().toLowerCase();
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
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 type { PaymentMethod, PaymentRow } from "@/services/payments.service";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -106,8 +108,12 @@ export default function PaymentsPage() {
|
|||||||
[query, statuses, method, pagination.pageIndex, pagination.pageSize],
|
[query, statuses, method, pagination.pageIndex, pagination.pageSize],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isLoading, isError } = usePaymentList(filter);
|
const { data, isLoading, isError } = useQuery(
|
||||||
const { data: summary, isLoading: summaryLoading } = usePaymentSummary();
|
api.payments.list.queryOptions({ input: { filter } }),
|
||||||
|
);
|
||||||
|
const { data: summary, isLoading: summaryLoading } = useQuery(
|
||||||
|
api.payments.summary.queryOptions({ staleTime: 30_000 }),
|
||||||
|
);
|
||||||
|
|
||||||
const rows = data?.items ?? [];
|
const rows = data?.items ?? [];
|
||||||
const total = data?.total ?? 0;
|
const total = data?.total ?? 0;
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ import {
|
|||||||
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
|
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
import { FREIGHT_BRAND, FREIGHT_BRAND_DARK } from "@/theme/freight-brand";
|
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";
|
import type { BatchBoardSchedule } from "@/types/trainScheduling";
|
||||||
|
|
||||||
const fmtTons = (n: number) =>
|
const fmtTons = (n: number) =>
|
||||||
@@ -368,7 +369,9 @@ function CardSkeleton() {
|
|||||||
|
|
||||||
export default function BatchBoardPage() {
|
export default function BatchBoardPage() {
|
||||||
const navigate = useNavigate();
|
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 { viewMode, setViewMode } = useFleetViewMode("batch-board");
|
||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|||||||
@@ -52,11 +52,8 @@ import {
|
|||||||
WindowStatusPill,
|
WindowStatusPill,
|
||||||
} from "@/components/trainScheduling/batchVisuals";
|
} from "@/components/trainScheduling/batchVisuals";
|
||||||
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
|
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import {
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
useBatchBoardDetail,
|
import { api } from "@/services/api";
|
||||||
useRunAllocation,
|
|
||||||
useScheduleDetail,
|
|
||||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type {
|
import type {
|
||||||
BatchBoardBookingDetail,
|
BatchBoardBookingDetail,
|
||||||
@@ -429,8 +426,16 @@ export default function BatchScheduleDetailPage() {
|
|||||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data, isLoading, isFetching, refetch } = useBatchBoardDetail(scheduleId);
|
const { data, isLoading, isFetching, refetch } = useQuery(
|
||||||
const runAllocation = useRunAllocation(scheduleId ?? "");
|
api.trainScheduling.batchBoardDetail.queryOptions({
|
||||||
|
input: { scheduleId: scheduleId ?? "" },
|
||||||
|
enabled: Boolean(scheduleId),
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const runAllocation = useMutation(
|
||||||
|
api.trainScheduling.runAllocation.mutationOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
const hasAssignedWagons = useMemo(
|
const hasAssignedWagons = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -443,7 +448,12 @@ export default function BatchScheduleDetailPage() {
|
|||||||
[data],
|
[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).
|
// Batch bookings by state for the composition side panel (payment / expired lists).
|
||||||
const batchBookings = useMemo(() => {
|
const batchBookings = useMemo(() => {
|
||||||
@@ -550,7 +560,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
|
|
||||||
const handleRunAllocation = () => {
|
const handleRunAllocation = () => {
|
||||||
runAllocation
|
runAllocation
|
||||||
.mutateAsync()
|
.mutateAsync({ scheduleId: scheduleId ?? "" })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const failed = result.issues.filter((i) => i.status === "FAILED").length;
|
const failed = result.issues.filter((i) => i.status === "FAILED").length;
|
||||||
const deferred = result.deferred.length;
|
const deferred = result.deferred.length;
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ import { PageContainer } from "@/components/page";
|
|||||||
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
|
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
|
||||||
import { RouteCorridor, StatusPill } from "@/components/trainScheduling/scheduleVisuals";
|
import { RouteCorridor, StatusPill } from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import { freightBrand } from "@/theme/freight-brand";
|
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";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
const parseError = (error: unknown, fallback: string) => {
|
const parseError = (error: unknown, fallback: string) => {
|
||||||
@@ -81,8 +82,15 @@ function MetaStat({
|
|||||||
export default function TrainScheduleTrackPage() {
|
export default function TrainScheduleTrackPage() {
|
||||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const trackQuery = useTrainTrack(scheduleId);
|
const trackQuery = useQuery(
|
||||||
const { recordCheckpoint } = useScheduleMutations(scheduleId);
|
api.trainScheduling.trainTrack.queryOptions({
|
||||||
|
input: { id: scheduleId ?? "" },
|
||||||
|
enabled: Boolean(scheduleId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const recordCheckpoint = useMutation(
|
||||||
|
api.trainScheduling.recordCheckpoint.mutationOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
if (trackQuery.isLoading) {
|
if (trackQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -56,11 +56,8 @@ import { shouldShowContainerPlacementStep } from "@/components/trainScheduling/s
|
|||||||
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
||||||
import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
|
import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
|
||||||
import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
|
import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
|
||||||
import {
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
useEligibleBookings,
|
import { api } from "@/services/api";
|
||||||
useScheduleDetail,
|
|
||||||
useScheduleMutations,
|
|
||||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type {
|
import type {
|
||||||
ContainerPlacement,
|
ContainerPlacement,
|
||||||
@@ -91,7 +88,12 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
const [maintenanceOpen, setMaintenanceOpen] = useState(false);
|
const [maintenanceOpen, setMaintenanceOpen] = useState(false);
|
||||||
const autoPreviewedRef = useRef(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 schedule = detailQuery.data;
|
||||||
const freightType: FreightType | undefined = schedule?.freightType as FreightType | undefined;
|
const freightType: FreightType | undefined = schedule?.freightType as FreightType | undefined;
|
||||||
|
|
||||||
@@ -111,12 +113,17 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
const eligibleFreightType =
|
const eligibleFreightType =
|
||||||
freightType === "CONTAINER" || freightType === "BULK" ? freightType : undefined;
|
freightType === "CONTAINER" || freightType === "BULK" ? freightType : undefined;
|
||||||
|
|
||||||
const eligibleQuery = useEligibleBookings(
|
const eligibleQuery = useQuery(
|
||||||
eligibleFilters,
|
api.trainScheduling.eligibleBookings.queryOptions({
|
||||||
Boolean(schedule),
|
input: { filters: eligibleFilters, freightType: eligibleFreightType },
|
||||||
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(
|
const assignedIds = useMemo(
|
||||||
() => (schedule?.bookings ?? []).map((b) => b.id),
|
() => (schedule?.bookings ?? []).map((b) => b.id),
|
||||||
|
|||||||
@@ -39,13 +39,9 @@ import {
|
|||||||
RouteCorridor,
|
RouteCorridor,
|
||||||
StatusPill,
|
StatusPill,
|
||||||
} from "@/components/trainScheduling/scheduleVisuals";
|
} from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import {
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
useAvailableLocomotives,
|
import { api } from "@/services/api";
|
||||||
useScheduleList,
|
|
||||||
useScheduleMutations,
|
|
||||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useRoutes } from "@/hooks/useRoutes";
|
|
||||||
import type { TrainScheduleListItem } from "@/types/trainScheduling";
|
import type { TrainScheduleListItem } from "@/types/trainScheduling";
|
||||||
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
||||||
|
|
||||||
@@ -88,10 +84,17 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
const [scheduleDate, setScheduleDate] = useState("");
|
const [scheduleDate, setScheduleDate] = useState("");
|
||||||
const [locomotiveId, setLocomotiveId] = useState("");
|
const [locomotiveId, setLocomotiveId] = useState("");
|
||||||
|
|
||||||
const schedulesQuery = useScheduleList();
|
const schedulesQuery = useQuery(
|
||||||
const routesQuery = useRoutes();
|
api.trainScheduling.scheduleList.queryOptions({ input: {} }),
|
||||||
const locomotivesQuery = useAvailableLocomotives(routeId || undefined);
|
);
|
||||||
const { create, cancel } = useScheduleMutations();
|
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(
|
const activeRoutes = useMemo(
|
||||||
() => (routesQuery.data ?? []).filter((r) => r.isActive),
|
() => (routesQuery.data ?? []).filter((r) => r.isActive),
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import { useParams, Link } from "react-router-dom";
|
|||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { Badge, Button, Card, Group, Loader, SimpleGrid, Stack, Text } from "@mantine/core";
|
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 { AssignWagonDialog } from "@/components/wagons/AssignWagonDialog";
|
||||||
import { WagonsTable } from "@/components/wagons/WagonsTable";
|
import { WagonsTable } from "@/components/wagons/WagonsTable";
|
||||||
import { useTrain } from "@/hooks/useTrains";
|
import { api } from "@/services/api";
|
||||||
|
|
||||||
export default function TrainDetailPage() {
|
export default function TrainDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import {
|
|||||||
VisualEmptyState,
|
VisualEmptyState,
|
||||||
formatDate,
|
formatDate,
|
||||||
} from '@/components/warehouses';
|
} 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 { useToast } from '@/hooks/use-toast';
|
||||||
import type { ArrivalQueueItem } from '@/types/warehouse';
|
import type { ArrivalQueueItem } from '@/types/warehouse';
|
||||||
|
|
||||||
@@ -30,17 +32,16 @@ function inspectionBadge(status: string | null) {
|
|||||||
export default function ArrivalQueuePage() {
|
export default function ArrivalQueuePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data, isLoading } = useArrivalQueue();
|
const { data, isLoading } = useQuery(api.warehouses.arrivalQueue.queryOptions());
|
||||||
const autoUnload = useAutoUnloadArrived();
|
const autoUnload = useMutation(api.warehouses.autoUnloadArrived.mutationOptions());
|
||||||
const unloadOne = useUnloadBooking();
|
const unloadOne = useMutation(api.warehouses.unloadBooking.mutationOptions());
|
||||||
const [inspectInventoryId, setInspectInventoryId] = useState<string | null>(null);
|
const [inspectInventoryId, setInspectInventoryId] = useState<string | null>(null);
|
||||||
|
|
||||||
const items = data ?? [];
|
const items = data ?? [];
|
||||||
|
|
||||||
const handleAutoUnload = async () => {
|
const handleAutoUnload = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await autoUnload.mutateAsync();
|
const r = await autoUnload.mutateAsync();
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Auto-unload complete',
|
title: 'Auto-unload complete',
|
||||||
description: `Processed ${r.processedCount}, skipped ${r.skippedCount}, failed ${r.failedCount}.`,
|
description: `Processed ${r.processedCount}, skipped ${r.skippedCount}, failed ${r.failedCount}.`,
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Card } from '@mantine/core';
|
import { Card } from '@mantine/core';
|
||||||
|
|
||||||
import { PageContainer, PageHeader } from '@/components/page';
|
import { PageContainer, PageHeader } from '@/components/page';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { InventoryWorkbench, VisualEmptyState } from '@/components/warehouses';
|
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). */
|
/** Items that are LOADED and awaiting dispatch (train departure). */
|
||||||
export default function DispatchQueuePage() {
|
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 ?? [];
|
const items = data ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,24 +3,35 @@ import { Button, Card, Center, Group, Loader, Select, Stack, TextInput } from '@
|
|||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
import { PageContainer, PageHeader } from '@/components/page';
|
import { PageContainer, PageHeader } from '@/components/page';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { VisualEmptyState, WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses';
|
import { VisualEmptyState, WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses';
|
||||||
import {
|
import { api } from '@/services/api';
|
||||||
useInventoryInquiry,
|
|
||||||
useWarehouseYards,
|
|
||||||
useWarehouseZones,
|
|
||||||
useWarehouses,
|
|
||||||
} from '@/hooks/useWarehouses';
|
|
||||||
import type { InventoryInquiryFilter, InventoryStatus } from '@/types/warehouse';
|
import type { InventoryInquiryFilter, InventoryStatus } from '@/types/warehouse';
|
||||||
|
|
||||||
export default function InventoryInquiryPage() {
|
export default function InventoryInquiryPage() {
|
||||||
const [draft, setDraft] = useState<InventoryInquiryFilter>({});
|
const [draft, setDraft] = useState<InventoryInquiryFilter>({});
|
||||||
const [applied, setApplied] = useState<InventoryInquiryFilter>({});
|
const [applied, setApplied] = useState<InventoryInquiryFilter>({});
|
||||||
|
|
||||||
const warehousesQuery = useWarehouses();
|
const warehousesQuery = useQuery(
|
||||||
const yardsQuery = useWarehouseYards(draft.warehouseId);
|
api.warehouses.list.queryOptions({ input: {} }),
|
||||||
const zonesQuery = useWarehouseZones(draft.yardId);
|
);
|
||||||
|
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 results = data ?? [];
|
||||||
|
|
||||||
const warehouseOptions = useMemo(
|
const warehouseOptions = useMemo(
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { Badge, Card, Group, Text } from '@mantine/core';
|
|||||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||||
|
|
||||||
import { PageContainer, PageHeader } from '@/components/page';
|
import { PageContainer, PageHeader } from '@/components/page';
|
||||||
import { FreightVisual, VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useWarehouseLoadings } from '@/hooks/useWarehouses';
|
|
||||||
|
|
||||||
type Loading = NonNullable<ReturnType<typeof useWarehouseLoadings>['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<Loading>[] = [
|
const columns: ColumnDef<Loading>[] = [
|
||||||
{
|
{
|
||||||
@@ -60,7 +63,9 @@ const columns: ColumnDef<Loading>[] = [
|
|||||||
|
|
||||||
/** Record of every inventory item loaded onto a wagon. */
|
/** Record of every inventory item loaded onto a wagon. */
|
||||||
export default function LoadedInventoryPage() {
|
export default function LoadedInventoryPage() {
|
||||||
const { data, isLoading } = useWarehouseLoadings();
|
const { data, isLoading } = useQuery(
|
||||||
|
api.warehouses.loadings.queryOptions({ input: {} }),
|
||||||
|
);
|
||||||
const loadings = data ?? [];
|
const loadings = data ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
VisualEmptyState,
|
VisualEmptyState,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
} from '@/components/warehouses';
|
} 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 { useToast } from '@/hooks/use-toast';
|
||||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
|
|
||||||
@@ -27,16 +29,19 @@ const isPaid = (item: WarehouseInventoryItem) => item.booking?.status === 'PAID'
|
|||||||
export default function LoadingQueuePage() {
|
export default function LoadingQueuePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const autoLoad = useAutoLoadReady();
|
const autoLoad = useMutation(api.warehouses.autoLoadReady.mutationOptions());
|
||||||
const { data: readyData, isLoading: readyLoading } = useWarehouseInventory({
|
const { data: readyData, isLoading: readyLoading } = useQuery(
|
||||||
status: 'READY_FOR_LOADING',
|
api.warehouses.listInventory.queryOptions({
|
||||||
});
|
input: { filter: { status: 'READY_FOR_LOADING' } },
|
||||||
const { data: loadedData, isLoading: loadedLoading } = useWarehouseInventory({ status: 'LOADED' });
|
}),
|
||||||
|
);
|
||||||
|
const { data: loadedData, isLoading: loadedLoading } = useQuery(
|
||||||
|
api.warehouses.listInventory.queryOptions({ input: { filter: { status: 'LOADED' } } }),
|
||||||
|
);
|
||||||
|
|
||||||
const handleAutoLoad = async () => {
|
const handleAutoLoad = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await autoLoad.mutateAsync();
|
const r = await autoLoad.mutateAsync();
|
||||||
const r = res.data;
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Auto-load complete',
|
title: 'Auto-load complete',
|
||||||
description: `Loaded ${r.loadedCount} PAID item(s); skipped ${r.skippedCount} (unpaid stay pending).`,
|
description: `Loaded ${r.loadedCount} PAID item(s); skipped ${r.skippedCount} (unpaid stay pending).`,
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { PageContainer, PageHeader } from '@/components/page';
|
import { PageContainer, PageHeader } from '@/components/page';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { WarehouseDashboardCharts } from '@/components/warehouses';
|
import { WarehouseDashboardCharts } from '@/components/warehouses';
|
||||||
import { useWarehouseDashboard } from '@/hooks/useWarehouses';
|
import { api } from '@/services/api';
|
||||||
import type { WarehouseDashboard } from '@/types/warehouse';
|
import type { WarehouseDashboard } from '@/types/warehouse';
|
||||||
|
|
||||||
interface Metric {
|
interface Metric {
|
||||||
@@ -49,7 +51,7 @@ const METRICS: Metric[] = [
|
|||||||
|
|
||||||
export default function WarehouseDashboardPage() {
|
export default function WarehouseDashboardPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, isLoading } = useWarehouseDashboard();
|
const { data, isLoading } = useQuery(api.warehouses.dashboard.queryOptions());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
|
|||||||
@@ -27,20 +27,27 @@ import {
|
|||||||
formatCapacity,
|
formatCapacity,
|
||||||
humanizeEnum,
|
humanizeEnum,
|
||||||
} from '@/components/warehouses';
|
} from '@/components/warehouses';
|
||||||
import {
|
import { useQuery } from '@tanstack/react-query';
|
||||||
useWarehouse,
|
|
||||||
useWarehouseInventory,
|
import { api } from '@/services/api';
|
||||||
useWarehouseYards,
|
|
||||||
useWarehouseZones,
|
|
||||||
} from '@/hooks/useWarehouses';
|
|
||||||
import type { WarehouseYard, WarehouseZone } from '@/types/warehouse';
|
import type { WarehouseYard, WarehouseZone } from '@/types/warehouse';
|
||||||
|
|
||||||
export default function WarehouseDetailPage() {
|
export default function WarehouseDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: warehouse, isLoading } = useWarehouse(id);
|
const { data: warehouse, isLoading } = useQuery(
|
||||||
const yardsQuery = useWarehouseYards(id);
|
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 [yardModalOpen, setYardModalOpen] = useState(false);
|
||||||
const [editingYard, setEditingYard] = useState<WarehouseYard | null>(null);
|
const [editingYard, setEditingYard] = useState<WarehouseYard | null>(null);
|
||||||
@@ -49,9 +56,18 @@ export default function WarehouseDetailPage() {
|
|||||||
const [editingZone, setEditingZone] = useState<WarehouseZone | null>(null);
|
const [editingZone, setEditingZone] = useState<WarehouseZone | null>(null);
|
||||||
const [selectedYardId, setSelectedYardId] = useState<string | null>(null);
|
const [selectedYardId, setSelectedYardId] = useState<string | null>(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 yards = yardsQuery.data ?? [];
|
||||||
const yardOptions = useMemo(
|
const yardOptions = useMemo(
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ import {
|
|||||||
ReceiveInventoryModal,
|
ReceiveInventoryModal,
|
||||||
inventoryStatusOptions,
|
inventoryStatusOptions,
|
||||||
} from '@/components/warehouses';
|
} from '@/components/warehouses';
|
||||||
import {
|
import { useQuery } from '@tanstack/react-query';
|
||||||
useWarehouseInventory,
|
|
||||||
useWarehouseYards,
|
import { api } from '@/services/api';
|
||||||
useWarehouseZones,
|
|
||||||
useWarehouses,
|
|
||||||
} from '@/hooks/useWarehouses';
|
|
||||||
import type { InventoryFilter, InventoryStatus } from '@/types/warehouse';
|
import type { InventoryFilter, InventoryStatus } from '@/types/warehouse';
|
||||||
|
|
||||||
export default function WarehouseInventoryPage() {
|
export default function WarehouseInventoryPage() {
|
||||||
@@ -33,10 +30,24 @@ export default function WarehouseInventoryPage() {
|
|||||||
[filter, debouncedSearch],
|
[filter, debouncedSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const warehousesQuery = useWarehouses();
|
const warehousesQuery = useQuery(
|
||||||
const yardsQuery = useWarehouseYards(filter.warehouseId);
|
api.warehouses.list.queryOptions({ input: {} }),
|
||||||
const zonesQuery = useWarehouseZones(filter.yardId);
|
);
|
||||||
const inventoryQuery = useWarehouseInventory(queryFilter);
|
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(
|
const warehouseOptions = useMemo(
|
||||||
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user