mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
enhance service filtering and dashboard functionality with company profile support
This commit is contained in:
@@ -148,15 +148,10 @@ export class BookingsController {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Scope to the active operational profile (importer/exporter) when one
|
// Company-wide by default; the optional filter.companyProfileId (per-page
|
||||||
// resolves; otherwise fall back to company-level scoping.
|
// service filter) narrows within the company. The company guard always
|
||||||
const companyProfileId =
|
// applies, so a customer can only ever see their own company's bookings.
|
||||||
await this.bookingsService.resolveActiveCompanyProfileId(userId);
|
return this.bookingsService.findAll(filter, companyId);
|
||||||
return this.bookingsService.findAll(
|
|
||||||
filter,
|
|
||||||
companyId,
|
|
||||||
companyProfileId ?? undefined,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('by-company/:companyId/customer-view')
|
@Get('by-company/:companyId/customer-view')
|
||||||
|
|||||||
@@ -656,10 +656,11 @@ export class BookingsService {
|
|||||||
assignedToSchedule: filter.assignedToSchedule,
|
assignedToSchedule: filter.assignedToSchedule,
|
||||||
// A forced company scope (portal/customer) overrides any caller-provided
|
// A forced company scope (portal/customer) overrides any caller-provided
|
||||||
// companyId so a customer can only ever see their own company's bookings.
|
// companyId so a customer can only ever see their own company's bookings.
|
||||||
// When an active profile resolves, scope to it; otherwise fall back to the
|
// The company guard always applies; the optional companyProfileId filter
|
||||||
// company so nothing breaks for not-yet-onboarded customers.
|
// (from the per-page service filter) narrows WITHIN the company — the repo
|
||||||
companyId: forceCompanyProfileId ? undefined : forceCompanyId ?? filter.companyId,
|
// ANDs both, so cross-company access is impossible.
|
||||||
companyProfileId: forceCompanyProfileId,
|
companyId: forceCompanyId ?? filter.companyId,
|
||||||
|
companyProfileId: forceCompanyProfileId ?? filter.companyProfileId,
|
||||||
contractType: filter.contractType,
|
contractType: filter.contractType,
|
||||||
serviceTypeId: filter.serviceTypeId,
|
serviceTypeId: filter.serviceTypeId,
|
||||||
cargoTypeId: filter.cargoTypeId,
|
cargoTypeId: filter.cargoTypeId,
|
||||||
@@ -694,18 +695,15 @@ export class BookingsService {
|
|||||||
filter: FilterBookingDto,
|
filter: FilterBookingDto,
|
||||||
): Promise<PaginatedBookings> {
|
): Promise<PaginatedBookings> {
|
||||||
const { company } = await this.companiesService.getCompanyInfoByUserId(userId);
|
const { company } = await this.companiesService.getCompanyInfoByUserId(userId);
|
||||||
// Scope to the active operational profile when one resolves; fall back to
|
|
||||||
// company-level so not-yet-onboarded customers still see their payables.
|
|
||||||
const companyProfileId =
|
|
||||||
await this.companiesService.resolveActiveCompanyProfileId(userId);
|
|
||||||
|
|
||||||
return this.bookingsRepository.findAllPaginated({
|
return this.bookingsRepository.findAllPaginated({
|
||||||
page: filter.page ?? 1,
|
page: filter.page ?? 1,
|
||||||
pageSize: filter.pageSize ?? 20,
|
pageSize: filter.pageSize ?? 20,
|
||||||
statuses: BookingsService.PAYABLE_STATUSES,
|
statuses: BookingsService.PAYABLE_STATUSES,
|
||||||
excludePaymentStatus: 'PAID',
|
excludePaymentStatus: 'PAID',
|
||||||
companyId: companyProfileId ? undefined : company.id,
|
// Company-wide: payables span all of the customer's services.
|
||||||
companyProfileId: companyProfileId ?? undefined,
|
companyId: company.id,
|
||||||
|
companyProfileId: filter.companyProfileId,
|
||||||
sortBy: filter.sortBy,
|
sortBy: filter.sortBy,
|
||||||
sortOrder: filter.sortOrder,
|
sortOrder: filter.sortOrder,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ export class FilterBookingDto {
|
|||||||
@IsUUID()
|
@IsUUID()
|
||||||
companyId?: string;
|
companyId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
format: 'uuid',
|
||||||
|
description:
|
||||||
|
'Narrow to a single operational profile (importer/exporter/freight_forwarder) within the company.',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
companyProfileId?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
contractType?: string;
|
contractType?: string;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { CreateCompanyProfileDto } from "./dto/create-company-profile.dto";
|
|||||||
import { SetActiveModeDto } from "./dto/set-active-mode.dto";
|
import { SetActiveModeDto } from "./dto/set-active-mode.dto";
|
||||||
import { SetOnboardingStepDto } from "./dto/set-onboarding-step.dto";
|
import { SetOnboardingStepDto } from "./dto/set-onboarding-step.dto";
|
||||||
import { StartOnboardingDto } from "./dto/start-onboarding.dto";
|
import { StartOnboardingDto } from "./dto/start-onboarding.dto";
|
||||||
|
import { DashboardQueryDto } from "./dto/dashboard-query.dto";
|
||||||
import {
|
import {
|
||||||
ResponseCompanyDto,
|
ResponseCompanyDto,
|
||||||
ResponseCompanyProfileDto,
|
ResponseCompanyProfileDto,
|
||||||
@@ -86,8 +87,12 @@ export class CompaniesController {
|
|||||||
})
|
})
|
||||||
async getDashboard(
|
async getDashboard(
|
||||||
@CurrentUser() user: CurrentIamUser,
|
@CurrentUser() user: CurrentIamUser,
|
||||||
|
@Query() query: DashboardQueryDto,
|
||||||
): Promise<DashboardSummaryResponseDto> {
|
): Promise<DashboardSummaryResponseDto> {
|
||||||
return this.companiesService.getDashboardSummary(user.id);
|
return this.companiesService.getDashboardSummary(
|
||||||
|
user.id,
|
||||||
|
query.companyProfileId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("fetch-etrade-info")
|
@Post("fetch-etrade-info")
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
import { CompaniesRepository } from "./companies.repository";
|
import { CompaniesRepository } from "./companies.repository";
|
||||||
import { CompanyProfileRepository } from "./company-profile.repository";
|
import { CompanyProfileRepository } from "./company-profile.repository";
|
||||||
import { ExternalProfileRepository } from "./external-profile.repository";
|
import { ExternalProfileRepository } from "./external-profile.repository";
|
||||||
import { CompanyDashboardRepository } from "./company-dashboard.repository";
|
import {
|
||||||
|
CompanyDashboardRepository,
|
||||||
|
DashboardScope,
|
||||||
|
} from "./company-dashboard.repository";
|
||||||
import { MinioService } from "../minio/minio.service";
|
import { MinioService } from "../minio/minio.service";
|
||||||
import { ETradeService } from "./services/etrade.service";
|
import { ETradeService } from "./services/etrade.service";
|
||||||
import { normalizeE164 } from "../../common/validators/is-phone-number.validator";
|
import { normalizeE164 } from "../../common/validators/is-phone-number.validator";
|
||||||
@@ -320,6 +323,7 @@ export class CompaniesService {
|
|||||||
*/
|
*/
|
||||||
async getDashboardSummary(
|
async getDashboardSummary(
|
||||||
userId: string,
|
userId: string,
|
||||||
|
companyProfileId?: string,
|
||||||
): Promise<DashboardSummaryResponseDto> {
|
): Promise<DashboardSummaryResponseDto> {
|
||||||
// A user without a company profile has no bookings — return an empty summary
|
// A user without a company profile has no bookings — return an empty summary
|
||||||
// rather than 404, so the portal home still renders.
|
// rather than 404, so the portal home still renders.
|
||||||
@@ -327,17 +331,17 @@ export class CompaniesService {
|
|||||||
const companyId = profile?.company?.id ?? profile?.companyId ?? null;
|
const companyId = profile?.company?.id ?? profile?.companyId ?? null;
|
||||||
if (!companyId) return this.emptyDashboardSummary();
|
if (!companyId) return this.emptyDashboardSummary();
|
||||||
|
|
||||||
// Scope KPIs to the active operational profile (importer/exporter mode) when
|
// Company-wide by default (all services' data). An optional companyProfileId
|
||||||
// one resolves; otherwise aggregate across the whole company.
|
// (from the per-page service filter) narrows to one operational profile —
|
||||||
const companyProfileId = profile?.activeProfileType
|
// but only after we confirm it belongs to this user's company, since the
|
||||||
? ((await this.companyProfilesRepo.findByType(
|
// dashboard scope has no company guard at the repository layer.
|
||||||
companyId,
|
let scope: DashboardScope = { companyId };
|
||||||
profile.activeProfileType,
|
if (companyProfileId) {
|
||||||
)) ?? null)
|
const owned = await this.companyProfilesRepo.findByCompanyId(companyId);
|
||||||
: null;
|
if (owned.some((p) => p.id === companyProfileId)) {
|
||||||
const scope = companyProfileId
|
scope = { companyProfileId };
|
||||||
? { companyProfileId: companyProfileId.id }
|
}
|
||||||
: { companyId };
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const yearStart = new Date(now.getFullYear(), 0, 1);
|
const yearStart = new Date(now.getFullYear(), 0, 1);
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsOptional, IsUUID } from "class-validator";
|
||||||
|
|
||||||
|
export class DashboardQueryDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
format: "uuid",
|
||||||
|
description:
|
||||||
|
"Narrow dashboard KPIs to a single operational profile (importer/exporter/freight_forwarder) of the user's company. Omit for company-wide totals.",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
companyProfileId?: string;
|
||||||
|
}
|
||||||
@@ -228,14 +228,7 @@ const sidebarItems: SidebarItem[] = [
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const {
|
const { user, company, companyType, createProfileAndSwitch } = useAuth();
|
||||||
user,
|
|
||||||
company,
|
|
||||||
activeProfileType,
|
|
||||||
companyType,
|
|
||||||
switchMode,
|
|
||||||
createProfileAndSwitch,
|
|
||||||
} = useAuth();
|
|
||||||
|
|
||||||
const displayName = user?.name?.en || user?.username || user?.email || "User";
|
const displayName = user?.name?.en || user?.username || user?.email || "User";
|
||||||
const userEmail = user?.email;
|
const userEmail = user?.email;
|
||||||
@@ -278,8 +271,6 @@ const App = () => {
|
|||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
companyProfiles={companyProfiles}
|
companyProfiles={companyProfiles}
|
||||||
companyType={companyType}
|
companyType={companyType}
|
||||||
activeProfileType={activeProfileType}
|
|
||||||
onSwitchMode={switchMode}
|
|
||||||
onCreateProfile={createProfileAndSwitch}
|
onCreateProfile={createProfileAndSwitch}
|
||||||
>
|
>
|
||||||
<OnboardingGate />
|
<OnboardingGate />
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
ArrowLeftRight,
|
|
||||||
Bell,
|
Bell,
|
||||||
Check,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
FileSignature,
|
FileSignature,
|
||||||
LogOut,
|
LogOut,
|
||||||
@@ -61,13 +59,9 @@ export interface AppLayoutProps {
|
|||||||
userEmail?: string;
|
userEmail?: string;
|
||||||
/** Operational profiles for the company — surfaced as reference chips in the account menu. */
|
/** Operational profiles for the company — surfaced as reference chips in the account menu. */
|
||||||
companyProfiles?: { type: string; reference: string; status?: string }[];
|
companyProfiles?: { type: string; reference: string; status?: string }[];
|
||||||
/** Company type (e.g. "customer", "forwarder") — gates the importer/exporter switch. */
|
/** Company type (e.g. "customer", "forwarder") — gates the "Add service" control. */
|
||||||
companyType?: string | null;
|
companyType?: string | null;
|
||||||
/** The active operational mode (importer/exporter/...). */
|
/** Create a new service profile of the given type (with business license). */
|
||||||
activeProfileType?: string | null;
|
|
||||||
/** Switch to an existing profile of the given type. */
|
|
||||||
onSwitchMode?: (type: ServiceType) => Promise<SwitchResult> | void;
|
|
||||||
/** Create the profile of the given type (with business license) then switch. */
|
|
||||||
onCreateProfile?: (
|
onCreateProfile?: (
|
||||||
type: ServiceType,
|
type: ServiceType,
|
||||||
licenseFiles: File[],
|
licenseFiles: File[],
|
||||||
@@ -146,8 +140,6 @@ export function AppLayout({
|
|||||||
userEmail,
|
userEmail,
|
||||||
companyProfiles = [],
|
companyProfiles = [],
|
||||||
companyType,
|
companyType,
|
||||||
activeProfileType,
|
|
||||||
onSwitchMode,
|
|
||||||
onCreateProfile,
|
onCreateProfile,
|
||||||
children,
|
children,
|
||||||
}: AppLayoutProps) {
|
}: AppLayoutProps) {
|
||||||
@@ -174,16 +166,16 @@ export function AppLayout({
|
|||||||
const initials = getInitials(userName);
|
const initials = getInitials(userName);
|
||||||
const activePage = getActivePage(sidebarItems, activePath);
|
const activePage = getActivePage(sidebarItems, activePath);
|
||||||
|
|
||||||
// ── Service selection (customer companies only) ──
|
// ── Add a service (customer companies only) ──
|
||||||
// A customer can operate as importer, exporter and/or freight forwarder,
|
// A customer can operate as importer, exporter and/or freight forwarder. The
|
||||||
// and switch between whichever service profiles their company has.
|
// header lets them ADD a service they don't have yet (creating a profile with
|
||||||
|
// its business license). Data is no longer scoped by an "active" service —
|
||||||
|
// every page shows all the company's data, with an optional per-page filter.
|
||||||
const isCustomer = companyType === "customer";
|
const isCustomer = companyType === "customer";
|
||||||
const canSwitch =
|
|
||||||
isCustomer &&
|
|
||||||
CUSTOMER_SERVICES.includes(activeProfileType as ServiceType);
|
|
||||||
|
|
||||||
const profileExists = (type: ServiceType) =>
|
const profileExists = (type: ServiceType) =>
|
||||||
companyProfiles.some((p) => p.type === type);
|
companyProfiles.some((p) => p.type === type);
|
||||||
|
const addableServices = CUSTOMER_SERVICES.filter((t) => !profileExists(t));
|
||||||
|
const canAddService = isCustomer && addableServices.length > 0;
|
||||||
|
|
||||||
const [switching, setSwitching] = useState(false);
|
const [switching, setSwitching] = useState(false);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
@@ -191,22 +183,12 @@ export function AppLayout({
|
|||||||
const [licenseFiles, setLicenseFiles] = useState<File[]>([]);
|
const [licenseFiles, setLicenseFiles] = useState<File[]>([]);
|
||||||
const [createError, setCreateError] = useState<string | null>(null);
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleSelectService = async (type: ServiceType) => {
|
const handleAddService = (type: ServiceType) => {
|
||||||
if (type === activeProfileType) return;
|
// Collect a business license, then create the profile.
|
||||||
if (profileExists(type)) {
|
setCreateTarget(type);
|
||||||
setSwitching(true);
|
setLicenseFiles([]);
|
||||||
try {
|
setCreateError(null);
|
||||||
await onSwitchMode?.(type);
|
setCreateOpen(true);
|
||||||
} finally {
|
|
||||||
setSwitching(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No profile yet — collect a business license, then create + switch.
|
|
||||||
setCreateTarget(type);
|
|
||||||
setLicenseFiles([]);
|
|
||||||
setCreateError(null);
|
|
||||||
setCreateOpen(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateConfirm = async () => {
|
const handleCreateConfirm = async () => {
|
||||||
@@ -302,8 +284,8 @@ export function AppLayout({
|
|||||||
|
|
||||||
{/* Right: switch + search + bell + avatar */}
|
{/* Right: switch + search + bell + avatar */}
|
||||||
<Group gap={10} wrap="nowrap" align="center">
|
<Group gap={10} wrap="nowrap" align="center">
|
||||||
{/* Service selector (customer companies only) */}
|
{/* Add a service (customer companies that don't yet have all three) */}
|
||||||
{canSwitch && (
|
{canAddService && (
|
||||||
<Menu
|
<Menu
|
||||||
width={220}
|
width={220}
|
||||||
position="bottom-end"
|
position="bottom-end"
|
||||||
@@ -319,43 +301,25 @@ export function AppLayout({
|
|||||||
color="edr-green"
|
color="edr-green"
|
||||||
radius={999}
|
radius={999}
|
||||||
size="sm"
|
size="sm"
|
||||||
leftSection={<ArrowLeftRight size={15} strokeWidth={1.8} />}
|
leftSection={<Plus size={15} strokeWidth={1.8} />}
|
||||||
rightSection={<ChevronDown size={14} strokeWidth={1.8} />}
|
rightSection={<ChevronDown size={14} strokeWidth={1.8} />}
|
||||||
styles={{ root: { height: 36 } }}
|
styles={{ root: { height: 36 } }}
|
||||||
visibleFrom="xs"
|
visibleFrom="xs"
|
||||||
>
|
>
|
||||||
{serviceLabel(activeProfileType as ServiceType)}
|
Add service
|
||||||
</Button>
|
</Button>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Label>Select service</Menu.Label>
|
<Menu.Label>Add a service</Menu.Label>
|
||||||
{CUSTOMER_SERVICES.map((type) => {
|
{addableServices.map((type) => (
|
||||||
const isActive = type === activeProfileType;
|
<Menu.Item
|
||||||
const exists = profileExists(type);
|
key={type}
|
||||||
return (
|
onClick={() => handleAddService(type)}
|
||||||
<Menu.Item
|
leftSection={<Plus size={15} strokeWidth={1.8} />}
|
||||||
key={type}
|
>
|
||||||
onClick={() => handleSelectService(type)}
|
{serviceLabel(type)}
|
||||||
leftSection={
|
</Menu.Item>
|
||||||
isActive ? (
|
))}
|
||||||
<Check size={15} strokeWidth={2} />
|
|
||||||
) : exists ? (
|
|
||||||
<ArrowLeftRight size={15} strokeWidth={1.8} />
|
|
||||||
) : (
|
|
||||||
<Plus size={15} strokeWidth={1.8} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={isActive}
|
|
||||||
>
|
|
||||||
{serviceLabel(type)}
|
|
||||||
{!exists && (
|
|
||||||
<Text span size="xs" c="dimmed" ml={6}>
|
|
||||||
(set up)
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
@@ -468,39 +432,25 @@ export function AppLayout({
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Box px="sm" py="xs">
|
<Box px="sm" py="xs">
|
||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
{companyProfiles.map((p) => {
|
{companyProfiles.map((p) => (
|
||||||
const isActive = p.type === activeProfileType;
|
<Group
|
||||||
return (
|
key={p.reference}
|
||||||
<Group
|
justify="space-between"
|
||||||
key={p.reference}
|
gap="sm"
|
||||||
justify="space-between"
|
wrap="nowrap"
|
||||||
gap="sm"
|
>
|
||||||
wrap="nowrap"
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={600}
|
||||||
|
style={{ color: textColor }}
|
||||||
>
|
>
|
||||||
<Group gap={6} wrap="nowrap">
|
{PROFILE_TYPE_LABELS[p.type] ?? p.type}
|
||||||
{isActive && (
|
</Text>
|
||||||
<Check
|
<Text size="xs" ff="monospace" c="dimmed">
|
||||||
size={13}
|
{p.reference}
|
||||||
color={primaryDarkColor}
|
</Text>
|
||||||
strokeWidth={2.5}
|
</Group>
|
||||||
/>
|
))}
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
fw={isActive ? 700 : 600}
|
|
||||||
style={{
|
|
||||||
color: isActive ? primaryDarkColor : textColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{PROFILE_TYPE_LABELS[p.type] ?? p.type}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="xs" ff="monospace" c="dimmed">
|
|
||||||
{p.reference}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Badge, Tooltip } from "@mantine/core";
|
|
||||||
import { ArrowDownToLine, ArrowUpFromLine } from "lucide-react";
|
|
||||||
|
|
||||||
import useAuth from "@/hooks/useAuth";
|
|
||||||
import { modeDataDescription, modeDataLabel } from "@/constants/profileMode";
|
|
||||||
|
|
||||||
interface ModeIndicatorProps {
|
|
||||||
/** Mantine size token for the badge. */
|
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Small pill showing which operational mode's data is currently on screen
|
|
||||||
* (Import / Export). The data itself is scoped server-side by the active
|
|
||||||
* profile; this just makes the scope visible. Switching is done via the header
|
|
||||||
* button — this is read-only.
|
|
||||||
*
|
|
||||||
* Renders nothing for non-customer companies or when no import/export mode is
|
|
||||||
* active, so it never interferes with forwarders or not-yet-onboarded users.
|
|
||||||
*/
|
|
||||||
export function ModeIndicator({ size = "md" }: ModeIndicatorProps) {
|
|
||||||
const { companyType, activeProfileType } = useAuth();
|
|
||||||
|
|
||||||
if (companyType !== "customer") return null;
|
|
||||||
|
|
||||||
const label = modeDataLabel(activeProfileType);
|
|
||||||
if (!label) return null;
|
|
||||||
|
|
||||||
const isImport = activeProfileType === "importer";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip label={modeDataDescription(activeProfileType)} withArrow>
|
|
||||||
<Badge
|
|
||||||
size={size}
|
|
||||||
radius="sm"
|
|
||||||
variant="light"
|
|
||||||
color={isImport ? "edr-green" : "blue"}
|
|
||||||
leftSection={
|
|
||||||
isImport ? (
|
|
||||||
<ArrowDownToLine size={13} />
|
|
||||||
) : (
|
|
||||||
<ArrowUpFromLine size={13} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
styles={{
|
|
||||||
root: { textTransform: "none", letterSpacing: 0, fontWeight: 600 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Viewing: {label}
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModeIndicator;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Operational-mode (importer/exporter/…) labels and helpers, shared by the app
|
* Operational-service (importer/exporter/…) display labels, shared by the app
|
||||||
* header and the per-page mode indicator so there is a single source of truth.
|
* header and the per-page service filters so there is a single source of truth.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const PROFILE_TYPE_LABELS: Record<string, string> = {
|
export const PROFILE_TYPE_LABELS: Record<string, string> = {
|
||||||
@@ -10,21 +10,3 @@ export const PROFILE_TYPE_LABELS: Record<string, string> = {
|
|||||||
dj_freight_forwarder: "DJ Freight Forwarder",
|
dj_freight_forwarder: "DJ Freight Forwarder",
|
||||||
transporter: "Transporter",
|
transporter: "Transporter",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The data-scope label shown to the user (importer ⇒ "Import", exporter ⇒ "Export"). */
|
|
||||||
export function modeDataLabel(
|
|
||||||
activeProfileType?: string | null,
|
|
||||||
): string | null {
|
|
||||||
if (activeProfileType === "importer") return "Import";
|
|
||||||
if (activeProfileType === "exporter") return "Export";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Short helper sentence describing what the active mode scopes. */
|
|
||||||
export function modeDataDescription(
|
|
||||||
activeProfileType?: string | null,
|
|
||||||
): string {
|
|
||||||
const label = modeDataLabel(activeProfileType);
|
|
||||||
if (!label) return "";
|
|
||||||
return `Showing your ${label.toLowerCase()} data — switch in the header.`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { Currency } from "@/pages/billing/invoices.mock";
|
import type { Currency } from "@/pages/billing/invoices.mock";
|
||||||
import { formatCurrency } from "@/pages/billing/invoices.mock";
|
import { formatCurrency } from "@/pages/billing/invoices.mock";
|
||||||
import { Grid, Stack } from "@mantine/core";
|
import { Group, Grid, Select, Stack } from "@mantine/core";
|
||||||
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { PROFILE_TYPE_LABELS } from "@/constants/profileMode";
|
||||||
import {
|
import {
|
||||||
FreightVolumeSection,
|
FreightVolumeSection,
|
||||||
HelloSection,
|
HelloSection,
|
||||||
@@ -15,8 +17,12 @@ import { useMyPortalData } from "./hooks";
|
|||||||
|
|
||||||
export default function MyPortalPage() {
|
export default function MyPortalPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
customer,
|
customer,
|
||||||
|
companyProfiles,
|
||||||
bookingsQuery,
|
bookingsQuery,
|
||||||
dashboardQuery,
|
dashboardQuery,
|
||||||
allBookings,
|
allBookings,
|
||||||
@@ -30,7 +36,12 @@ export default function MyPortalPage() {
|
|||||||
dashboard,
|
dashboard,
|
||||||
volumePoints,
|
volumePoints,
|
||||||
maxVolume,
|
maxVolume,
|
||||||
} = useMyPortalData();
|
} = useMyPortalData(selectedProfileId ?? undefined);
|
||||||
|
|
||||||
|
const serviceOptions = companyProfiles.map((p) => ({
|
||||||
|
value: p.id,
|
||||||
|
label: `${PROFILE_TYPE_LABELS[p.type] ?? p.type} · ${p.reference}`,
|
||||||
|
}));
|
||||||
|
|
||||||
const handleBookingClick = (id: string) => {
|
const handleBookingClick = (id: string) => {
|
||||||
navigate(`/bookings/${id}`);
|
navigate(`/bookings/${id}`);
|
||||||
@@ -40,6 +51,22 @@ export default function MyPortalPage() {
|
|||||||
<Stack gap="lg" p={{ base: 16, sm: 24, lg: 32 }}>
|
<Stack gap="lg" p={{ base: 16, sm: 24, lg: 32 }}>
|
||||||
<HelloSection greeting={greeting} companyName={companyName} />
|
<HelloSection greeting={greeting} companyName={companyName} />
|
||||||
|
|
||||||
|
{serviceOptions.length > 1 && (
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Select
|
||||||
|
placeholder="All services"
|
||||||
|
data={serviceOptions}
|
||||||
|
value={selectedProfileId}
|
||||||
|
onChange={setSelectedProfileId}
|
||||||
|
clearable
|
||||||
|
radius="md"
|
||||||
|
comboboxProps={{ withinPortal: true }}
|
||||||
|
style={{ width: 220 }}
|
||||||
|
aria-label="Filter dashboard by service"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
|
||||||
<SetupPrompt show={!customer} />
|
<SetupPrompt show={!customer} />
|
||||||
|
|
||||||
<StatsSection
|
<StatsSection
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Box, Group, Text } from "@mantine/core";
|
|||||||
import { ArrowRight, Truck } from "lucide-react";
|
import { ArrowRight, Truck } from "lucide-react";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { ModeIndicator } from "@/components/ModeIndicator";
|
|
||||||
import { cv } from "../constants";
|
import { cv } from "../constants";
|
||||||
|
|
||||||
interface HelloSectionProps {
|
interface HelloSectionProps {
|
||||||
@@ -24,7 +23,6 @@ export const HelloSection = memo(function HelloSection({
|
|||||||
<Text fz={26} fw={800} c="edr-text" className="tracking-tight">
|
<Text fz={26} fw={800} c="edr-text" className="tracking-tight">
|
||||||
{companyName} 👋
|
{companyName} 👋
|
||||||
</Text>
|
</Text>
|
||||||
<ModeIndicator />
|
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,25 @@ import { getMyInvoices } from "@/lib/currentCustomer";
|
|||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { ACTIVE_STATUSES } from "./constants";
|
import { ACTIVE_STATUSES } from "./constants";
|
||||||
|
|
||||||
export function useMyPortalData() {
|
export function useMyPortalData(selectedProfileId?: string) {
|
||||||
const { user, customer } = useAuth();
|
const { user, customer, company } = useAuth();
|
||||||
const myInvoices = useMemo(() => getMyInvoices(), []);
|
const myInvoices = useMemo(() => getMyInvoices(), []);
|
||||||
|
|
||||||
|
const companyProfiles = company?.company?.companyProfiles ?? [];
|
||||||
|
|
||||||
const bookingsQuery = useQuery(
|
const bookingsQuery = useQuery(
|
||||||
api.bookings.list.queryOptions({
|
api.bookings.list.queryOptions({
|
||||||
input: { sortBy: "createdAt", sortOrder: "DESC" },
|
input: {
|
||||||
|
sortBy: "createdAt",
|
||||||
|
sortOrder: "DESC",
|
||||||
|
companyProfileId: selectedProfileId,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const dashboardQuery = useQuery(api.companies.getDashboard.queryOptions());
|
const dashboardQuery = useQuery(
|
||||||
|
api.companies.getDashboard.queryOptions({ input: selectedProfileId }),
|
||||||
|
);
|
||||||
|
|
||||||
const allBookings = bookingsQuery.data?.items ?? [];
|
const allBookings = bookingsQuery.data?.items ?? [];
|
||||||
const activeBookings = allBookings.filter((b) =>
|
const activeBookings = allBookings.filter((b) =>
|
||||||
@@ -56,6 +64,7 @@ export function useMyPortalData() {
|
|||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
customer,
|
customer,
|
||||||
|
companyProfiles,
|
||||||
bookingsQuery,
|
bookingsQuery,
|
||||||
dashboardQuery,
|
dashboardQuery,
|
||||||
allBookings,
|
allBookings,
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ import {
|
|||||||
|
|
||||||
import { ShipmentTrackingModal } from "./tracking/ShipmentTrackingModal";
|
import { ShipmentTrackingModal } from "./tracking/ShipmentTrackingModal";
|
||||||
import { PayNowButton } from "./payments/PayNowButton";
|
import { PayNowButton } from "./payments/PayNowButton";
|
||||||
import { ModeIndicator } from "@/components/ModeIndicator";
|
import useAuth from "@/hooks/useAuth";
|
||||||
|
import { PROFILE_TYPE_LABELS } from "@/constants/profileMode";
|
||||||
import {
|
import {
|
||||||
BookingTypeBadge,
|
BookingTypeBadge,
|
||||||
CargoModeCell,
|
CargoModeCell,
|
||||||
@@ -256,10 +257,13 @@ function fmtDate(iso?: string | null): string {
|
|||||||
// ── Main component ────────────────────────────────────────────────────────────
|
// ── Main component ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Lightweight count query for a single lifecycle filter (reads only `total`).
|
// Lightweight count query for a single lifecycle filter (reads only `total`).
|
||||||
function useStatusCount(statuses: string | undefined): number | undefined {
|
function useStatusCount(
|
||||||
|
statuses: string | undefined,
|
||||||
|
companyProfileId?: string,
|
||||||
|
): number | undefined {
|
||||||
const { data } = useQuery(
|
const { data } = useQuery(
|
||||||
api.bookings.list.queryOptions({
|
api.bookings.list.queryOptions({
|
||||||
input: { statuses, page: 1, pageSize: 1 },
|
input: { statuses, companyProfileId, page: 1, pageSize: 1 },
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -335,8 +339,18 @@ export default function MyBookings() {
|
|||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [typeFilter, setTypeFilter] = useState<string | null>(null);
|
const [typeFilter, setTypeFilter] = useState<string | null>(null);
|
||||||
const [freightFilter, setFreightFilter] = useState<string | null>(null);
|
const [freightFilter, setFreightFilter] = useState<string | null>(null);
|
||||||
|
const [serviceFilter, setServiceFilter] = useState<string | null>(null);
|
||||||
const [createdFrom, setCreatedFrom] = useState<string>("");
|
const [createdFrom, setCreatedFrom] = useState<string>("");
|
||||||
const [createdTo, setCreatedTo] = useState<string>("");
|
const [createdTo, setCreatedTo] = useState<string>("");
|
||||||
|
|
||||||
|
// Operational-service options (importer / exporter / freight forwarder) for
|
||||||
|
// the per-page filter. Empty for non-customer companies.
|
||||||
|
const { company } = useAuth();
|
||||||
|
const companyProfiles = company?.company?.companyProfiles ?? [];
|
||||||
|
const serviceOptions = companyProfiles.map((p) => ({
|
||||||
|
value: p.id,
|
||||||
|
label: `${PROFILE_TYPE_LABELS[p.type] ?? p.type} · ${p.reference}`,
|
||||||
|
}));
|
||||||
const [trackingBooking, setTrackingBooking] = useState<Freight.IBooking | null>(
|
const [trackingBooking, setTrackingBooking] = useState<Freight.IBooking | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -352,10 +366,15 @@ export default function MyBookings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasExtraFilters =
|
const hasExtraFilters =
|
||||||
!!typeFilter || !!freightFilter || !!createdFrom || !!createdTo;
|
!!typeFilter ||
|
||||||
|
!!freightFilter ||
|
||||||
|
!!serviceFilter ||
|
||||||
|
!!createdFrom ||
|
||||||
|
!!createdTo;
|
||||||
const clearExtraFilters = () => {
|
const clearExtraFilters = () => {
|
||||||
setTypeFilter(null);
|
setTypeFilter(null);
|
||||||
setFreightFilter(null);
|
setFreightFilter(null);
|
||||||
|
setServiceFilter(null);
|
||||||
setCreatedFrom("");
|
setCreatedFrom("");
|
||||||
setCreatedTo("");
|
setCreatedTo("");
|
||||||
resetPage();
|
resetPage();
|
||||||
@@ -366,6 +385,7 @@ export default function MyBookings() {
|
|||||||
statuses,
|
statuses,
|
||||||
bookingType: typeFilter ?? undefined,
|
bookingType: typeFilter ?? undefined,
|
||||||
freightType: freightFilter ?? undefined,
|
freightType: freightFilter ?? undefined,
|
||||||
|
companyProfileId: serviceFilter ?? undefined,
|
||||||
createdFrom: createdFrom || undefined,
|
createdFrom: createdFrom || undefined,
|
||||||
// include the whole selected end day
|
// include the whole selected end day
|
||||||
createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined,
|
createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined,
|
||||||
@@ -376,6 +396,7 @@ export default function MyBookings() {
|
|||||||
statuses,
|
statuses,
|
||||||
typeFilter,
|
typeFilter,
|
||||||
freightFilter,
|
freightFilter,
|
||||||
|
serviceFilter,
|
||||||
createdFrom,
|
createdFrom,
|
||||||
createdTo,
|
createdTo,
|
||||||
pagination.pageIndex,
|
pagination.pageIndex,
|
||||||
@@ -387,25 +408,33 @@ export default function MyBookings() {
|
|||||||
api.bookings.list.queryOptions({ input: filter }),
|
api.bookings.list.queryOptions({ input: filter }),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Per-card lifecycle counts (one cheap query each, total-only).
|
// Per-card lifecycle counts (one cheap query each, total-only). Scoped to the
|
||||||
const allCount = useStatusCount(undefined);
|
// selected service so the cards match the filtered table.
|
||||||
|
const svc = serviceFilter ?? undefined;
|
||||||
|
const allCount = useStatusCount(undefined, svc);
|
||||||
const activeCount = useStatusCount(
|
const activeCount = useStatusCount(
|
||||||
STATUS_FILTERS.find((f) => f.key === "active")!.statuses,
|
STATUS_FILTERS.find((f) => f.key === "active")!.statuses,
|
||||||
|
svc,
|
||||||
);
|
);
|
||||||
const paymentCount = useStatusCount(
|
const paymentCount = useStatusCount(
|
||||||
STATUS_FILTERS.find((f) => f.key === "payment")!.statuses,
|
STATUS_FILTERS.find((f) => f.key === "payment")!.statuses,
|
||||||
|
svc,
|
||||||
);
|
);
|
||||||
const draftCount = useStatusCount(
|
const draftCount = useStatusCount(
|
||||||
STATUS_FILTERS.find((f) => f.key === "draft")!.statuses,
|
STATUS_FILTERS.find((f) => f.key === "draft")!.statuses,
|
||||||
|
svc,
|
||||||
);
|
);
|
||||||
const doneCount = useStatusCount(
|
const doneCount = useStatusCount(
|
||||||
STATUS_FILTERS.find((f) => f.key === "done")!.statuses,
|
STATUS_FILTERS.find((f) => f.key === "done")!.statuses,
|
||||||
|
svc,
|
||||||
);
|
);
|
||||||
const transitCount = useStatusCount(
|
const transitCount = useStatusCount(
|
||||||
STATUS_FILTERS.find((f) => f.key === "transit")!.statuses,
|
STATUS_FILTERS.find((f) => f.key === "transit")!.statuses,
|
||||||
|
svc,
|
||||||
);
|
);
|
||||||
const closedCount = useStatusCount(
|
const closedCount = useStatusCount(
|
||||||
STATUS_FILTERS.find((f) => f.key === "closed")!.statuses,
|
STATUS_FILTERS.find((f) => f.key === "closed")!.statuses,
|
||||||
|
svc,
|
||||||
);
|
);
|
||||||
const cardCounts: Record<StatusFilterKey, number | undefined> = {
|
const cardCounts: Record<StatusFilterKey, number | undefined> = {
|
||||||
all: allCount,
|
all: allCount,
|
||||||
@@ -617,7 +646,6 @@ export default function MyBookings() {
|
|||||||
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
||||||
Bookings
|
Bookings
|
||||||
</Title>
|
</Title>
|
||||||
<ModeIndicator />
|
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" c="edr-muted" mt={4}>
|
<Text size="sm" c="edr-muted" mt={4}>
|
||||||
Track every cargo booking — from draft to delivery.
|
Track every cargo booking — from draft to delivery.
|
||||||
@@ -723,6 +751,22 @@ export default function MyBookings() {
|
|||||||
style={{ width: 150 }}
|
style={{ width: 150 }}
|
||||||
aria-label="Filter by cargo type"
|
aria-label="Filter by cargo type"
|
||||||
/>
|
/>
|
||||||
|
{serviceOptions.length > 1 && (
|
||||||
|
<Select
|
||||||
|
placeholder="All services"
|
||||||
|
data={serviceOptions}
|
||||||
|
value={serviceFilter}
|
||||||
|
onChange={(v) => {
|
||||||
|
setServiceFilter(v);
|
||||||
|
resetPage();
|
||||||
|
}}
|
||||||
|
clearable
|
||||||
|
radius="md"
|
||||||
|
comboboxProps={{ withinPortal: true }}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
aria-label="Filter by service"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
type="date"
|
type="date"
|
||||||
value={createdFrom}
|
value={createdFrom}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { ModeIndicator } from "@/components/ModeIndicator";
|
|
||||||
import { PayNowButton } from "../bookings/payments/PayNowButton";
|
import { PayNowButton } from "../bookings/payments/PayNowButton";
|
||||||
import {
|
import {
|
||||||
BORDER,
|
BORDER,
|
||||||
@@ -121,7 +120,6 @@ export default function ContractDetailPage() {
|
|||||||
{contract.reference}
|
{contract.reference}
|
||||||
</Title>
|
</Title>
|
||||||
<ContractStatusBadge status={contract.status} />
|
<ContractStatusBadge status={contract.status} />
|
||||||
<ModeIndicator size="sm" />
|
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" c="dimmed" mt={2}>
|
<Text size="sm" c="dimmed" mt={2}>
|
||||||
General contract · {isContainer ? "Containerised" : "Bulk"}
|
General contract · {isContainer ? "Containerised" : "Bulk"}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
usePagination,
|
usePagination,
|
||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
import { ModeIndicator } from "@/components/ModeIndicator";
|
|
||||||
import { CargoModeCell, PaymentBadge } from "../bookings/booking-display";
|
import { CargoModeCell, PaymentBadge } from "../bookings/booking-display";
|
||||||
import { ContractStatusBadge, GREEN, INK, MUTED } from "./contract-ui";
|
import { ContractStatusBadge, GREEN, INK, MUTED } from "./contract-ui";
|
||||||
|
|
||||||
@@ -177,7 +176,6 @@ export default function ContractsList() {
|
|||||||
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
||||||
General Contracts
|
General Contracts
|
||||||
</Title>
|
</Title>
|
||||||
<ModeIndicator />
|
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" c="edr-muted" mt={4}>
|
<Text size="sm" c="edr-muted" mt={4}>
|
||||||
Reserve a quantity once, then place orders against it until the
|
Reserve a quantity once, then place orders against it until the
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const api = {
|
|||||||
companiesService.updateProfile,
|
companiesService.updateProfile,
|
||||||
),
|
),
|
||||||
|
|
||||||
getDashboard: endpoint<void, DashboardSummary>(
|
getDashboard: endpoint<string | void, DashboardSummary>(
|
||||||
"companies",
|
"companies",
|
||||||
"getDashboard",
|
"getDashboard",
|
||||||
companiesService.getDashboard,
|
companiesService.getDashboard,
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export interface BookingListFilter {
|
|||||||
freightType?: string;
|
freightType?: string;
|
||||||
/** IMPORT / EXPORT / DOMESTIC. */
|
/** IMPORT / EXPORT / DOMESTIC. */
|
||||||
tradeDirection?: string;
|
tradeDirection?: string;
|
||||||
|
/** Narrow to a single operational profile (importer/exporter/freight_forwarder). */
|
||||||
|
companyProfileId?: string;
|
||||||
/** Created-date range (ISO). */
|
/** Created-date range (ISO). */
|
||||||
createdFrom?: string;
|
createdFrom?: string;
|
||||||
createdTo?: string;
|
createdTo?: string;
|
||||||
|
|||||||
@@ -162,9 +162,12 @@ export const companiesService = {
|
|||||||
return unwrap(response.data);
|
return unwrap(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
getDashboard: async (): Promise<DashboardSummary> => {
|
getDashboard: async (
|
||||||
|
companyProfileId?: string,
|
||||||
|
): Promise<DashboardSummary> => {
|
||||||
const response = await client.get<ApiResponse<DashboardSummary>>(
|
const response = await client.get<ApiResponse<DashboardSummary>>(
|
||||||
URL_CONSTANTS.COMPANIES_API.DASHBOARD,
|
URL_CONSTANTS.COMPANIES_API.DASHBOARD,
|
||||||
|
{ params: companyProfileId ? { companyProfileId } : undefined },
|
||||||
);
|
);
|
||||||
return unwrap(response.data);
|
return unwrap(response.data);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user