mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 11:55:42 +00:00
feat(backoffice): guard routes and actions by permission
Mirrors the new keys in lib/permissions.ts, wraps the warehouse, overview, reports, support and booking-request routes in RequirePermission, and gates the dispatch, mark-paid, invoice pay/cancel, export and support-send actions behind their own keys. Removes duplicate route blocks.
This commit is contained in:
@@ -13,6 +13,8 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowLeft, Download } from "lucide-react";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
@@ -60,6 +62,8 @@ function InfoField({ label, value }: { label: string; value?: string | null }) {
|
||||
}
|
||||
|
||||
export default function InvoiceDetailPage() {
|
||||
const { user } = useAuth();
|
||||
const canExport = hasPermission(user, FREIGHT_PERMS.invoices.export);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
@@ -124,6 +128,7 @@ export default function InvoiceDetailPage() {
|
||||
size="lg"
|
||||
radius="md"
|
||||
aria-label="Download invoice"
|
||||
disabled={!canExport}
|
||||
loading={downloading}
|
||||
onClick={() => void downloadDocument()}
|
||||
>
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
} from "@/features/support/useSupport";
|
||||
import { useSupportSocket } from "@/features/support/useSupportSocket";
|
||||
import { customersService } from "@/services/customers.service";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
|
||||
type ReadFilter = "ALL" | "UNREAD";
|
||||
|
||||
@@ -401,6 +403,8 @@ function ConversationThread({
|
||||
fetchNextPage,
|
||||
} = useMessages(conversation.id);
|
||||
const send = useSendMessage(conversation.id);
|
||||
const { user: agentUser } = useAuth();
|
||||
const canSend = hasPermission(agentUser, FREIGHT_PERMS.support.agentSend);
|
||||
const markRead = useMarkConversationRead();
|
||||
const [draft, setDraft] = useState("");
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@@ -656,7 +660,7 @@ function ConversationThread({
|
||||
color="edr-green"
|
||||
variant="filled"
|
||||
loading={send.isPending}
|
||||
disabled={!draft.trim() && attach.attachments.length === 0}
|
||||
disabled={!canSend || (!draft.trim() && attach.attachments.length === 0)}
|
||||
onClick={submit}
|
||||
>
|
||||
<Send size={18} />
|
||||
|
||||
@@ -42,6 +42,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
import { KpiStrip, PageContainer } from "@/components/page";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import {
|
||||
autoFillPlacements,
|
||||
mergePlacementsWithSaved,
|
||||
@@ -102,6 +104,7 @@ const parseError = (error: unknown, fallback: string) => {
|
||||
};
|
||||
|
||||
export default function TrainScheduleV2DetailPage() {
|
||||
const { user: authUser } = useAuth();
|
||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||
const { toast } = useToast();
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
@@ -394,7 +397,9 @@ export default function TrainScheduleV2DetailPage() {
|
||||
: [];
|
||||
|
||||
const canEditBookings = ["DRAFT", "SCHEDULED"].includes(schedule.status);
|
||||
const canDispatch = schedule.status === "SCHEDULED";
|
||||
const canDispatch =
|
||||
schedule.status === "SCHEDULED" &&
|
||||
hasPermission(authUser, FREIGHT_PERMS.trainScheduling.dispatch);
|
||||
|
||||
// Dispatch readiness: bookings with no wagon, and wagon-loaded bookings whose
|
||||
// cargo staff never marked loaded. Both are warnings, not blockers — staff can
|
||||
|
||||
@@ -63,7 +63,7 @@ import { api } from "@/services/api";
|
||||
import { formatRouteLabel } from "@/services/routes.service";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { canCreateSchedule, canUpdateSchedule } from "@/lib/permissions";
|
||||
import { FREIGHT_PERMS, canCreateSchedule, hasPermission } from "@/lib/permissions";
|
||||
import type {
|
||||
CreateScheduleWindowRulePayload,
|
||||
FreightType,
|
||||
@@ -111,7 +111,7 @@ export default function TrainScheduleV2ListPage() {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const canCreate = canCreateSchedule(user);
|
||||
const canUpdate = canUpdateSchedule(user);
|
||||
const canDispatch = hasPermission(user, FREIGHT_PERMS.trainScheduling.dispatch);
|
||||
const { viewMode, setViewMode } = useFleetViewMode("train-scheduling-v2");
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -491,7 +491,7 @@ export default function TrainScheduleV2ListPage() {
|
||||
{/* Start the run. Same transition as the detail page's
|
||||
Dispatch button — that page also shows unassigned-wagon
|
||||
and not-loaded warnings, so it stays the fuller surface. */}
|
||||
{canUpdate && schedule.status === "SCHEDULED" ? (
|
||||
{canDispatch && schedule.status === "SCHEDULED" ? (
|
||||
<Menu.Item
|
||||
leftSection={<Play size={15} />}
|
||||
onClick={() => setDispatchTarget(schedule)}
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
import { openPdfBlob } from '@/components/warehouses/pdf';
|
||||
import { buildWarehouseExitPaperPdf } from '@/components/warehouses/warehousePdf';
|
||||
import { extractErrorMessage } from '@/components/warehouses/options';
|
||||
import { useAuth } from '@/auth/useAuth';
|
||||
import { FREIGHT_PERMS, hasPermission } from '@/lib/permissions';
|
||||
|
||||
const STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
||||
DRAFT: 'gray',
|
||||
@@ -171,6 +173,9 @@ export default function WarehouseInvoicesPage() {
|
||||
}
|
||||
|
||||
function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () => void }) {
|
||||
const { user } = useAuth();
|
||||
const mayRecordPayment = hasPermission(user, FREIGHT_PERMS.warehouseFeeInvoices.pay);
|
||||
const canCancelInvoice = hasPermission(user, FREIGHT_PERMS.warehouseFeeInvoices.cancel);
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { data: inv, isLoading } = useQuery(
|
||||
@@ -430,9 +435,11 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
||||
placeholder="Optional"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button leftSection={<CreditCard size={16} />} loading={payOnline.isPending} onClick={handleOnlinePay}>
|
||||
Pay with {gatewayMethod === 'WAAFI' ? 'Waafi' : 'Telebirr'}
|
||||
</Button>
|
||||
{mayRecordPayment && (
|
||||
<Button leftSection={<CreditCard size={16} />} loading={payOnline.isPending} onClick={handleOnlinePay}>
|
||||
Pay with {gatewayMethod === 'WAAFI' ? 'Waafi' : 'Telebirr'}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Divider label="Record manual payment" labelPosition="left" />
|
||||
@@ -456,9 +463,11 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
||||
onChange={(e) => setDriverPhone(e.currentTarget.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
|
||||
Pay
|
||||
</Button>
|
||||
{mayRecordPayment && (
|
||||
<Button leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
|
||||
Pay
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
@@ -506,7 +515,7 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
||||
Gate clearance & exit paper
|
||||
</Button>
|
||||
)}
|
||||
{inv.status !== 'PAID' && inv.status !== 'CANCELLED' && (
|
||||
{inv.status !== 'PAID' && inv.status !== 'CANCELLED' && canCancelInvoice && (
|
||||
<Button variant="light" color="red" leftSection={<Ban size={16} />} loading={cancel.isPending} onClick={handleCancel}>
|
||||
Cancel invoice
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user