style(app): format app routes, pages, and server actions
This commit is contained in:
parent
05c7c83799
commit
7db1f648bf
32 changed files with 517 additions and 579 deletions
|
|
@ -9,12 +9,8 @@ import { EventTypes } from "@/utils/event-log/eventTypes";
|
|||
import { notifyUser } from "@/lib/notifications";
|
||||
import { plaintextToLexical } from "@/lib/tickets/lexical";
|
||||
import { getStaffUserIds } from "@/lib/tickets/staff";
|
||||
import {
|
||||
CLOSING_STATUSES,
|
||||
STATUS_LABEL,
|
||||
TICKET_STATUSES,
|
||||
} from "@/lib/tickets/ticketMeta";
|
||||
import type { TicketCategory, TicketPriority, TicketStatus } from "@/lib/tickets/ticketMeta";
|
||||
import { CLOSING_STATUSES, STATUS_LABEL, TICKET_STATUSES } from "@/lib/tickets/ticketMeta";
|
||||
|
||||
export interface ActionResult<T = undefined> {
|
||||
success: boolean;
|
||||
|
|
@ -36,7 +32,10 @@ async function authenticate() {
|
|||
return { payload, user };
|
||||
}
|
||||
|
||||
function displayNameOf(user: { payloadDisplayName?: string | null; username?: string | null }): string {
|
||||
function displayNameOf(user: {
|
||||
payloadDisplayName?: string | null;
|
||||
username?: string | null;
|
||||
}): string {
|
||||
return user.payloadDisplayName || user.username || "A member";
|
||||
}
|
||||
|
||||
|
|
@ -169,9 +168,7 @@ export async function replyToTicket(ticketId: number, content: string): Promise<
|
|||
await payload.update({
|
||||
collection: "tickets",
|
||||
id: ticketId,
|
||||
data: reopen
|
||||
? { activity: newActivity, status: "open" }
|
||||
: { activity: newActivity },
|
||||
data: reopen ? { activity: newActivity, status: "open" } : { activity: newActivity },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
|
@ -229,7 +226,10 @@ export async function replyToTicket(ticketId: number, content: string): Promise<
|
|||
}
|
||||
}
|
||||
|
||||
export async function updateTicketStatus(ticketId: number, status: TicketStatus): Promise<ActionResult> {
|
||||
export async function updateTicketStatus(
|
||||
ticketId: number,
|
||||
status: TicketStatus,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const userId = user.id as number;
|
||||
|
|
@ -245,9 +245,7 @@ export async function updateTicketStatus(ticketId: number, status: TicketStatus)
|
|||
const reporterId = reporterIdOf(ticket);
|
||||
if (!isStaff) {
|
||||
const reporterCancel =
|
||||
reporterId === userId &&
|
||||
status === "canceled" &&
|
||||
!CLOSING_STATUSES.includes(ticket.status);
|
||||
reporterId === userId && status === "canceled" && !CLOSING_STATUSES.includes(ticket.status);
|
||||
if (!reporterCancel) throw new Error("Only staff can change ticket status.");
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +291,10 @@ export async function updateTicketStatus(ticketId: number, status: TicketStatus)
|
|||
}
|
||||
}
|
||||
|
||||
export async function assignTicket(ticketId: number, assigneeId: number | null): Promise<ActionResult> {
|
||||
export async function assignTicket(
|
||||
ticketId: number,
|
||||
assigneeId: number | null,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const userId = user.id as number;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { Campaign, Faction, Media, Mission } from "@/payload-types";
|
||||
import type { Faction, Media, Mission } from "@/payload-types";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CalendarDaysIcon,
|
||||
ChevronRightIcon,
|
||||
CrosshairIcon,
|
||||
ShieldIcon,
|
||||
TargetIcon,
|
||||
UsersIcon,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { Faction, Media, Mission, BankAccount, Resource, Campaign } from "@/payload-types";
|
||||
import type { BankAccount, Campaign, Faction, Media, Mission, Resource } from "@/payload-types";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
|
||||
import {
|
||||
|
|
@ -10,8 +10,8 @@ import {
|
|||
ChevronRightIcon,
|
||||
FlagIcon,
|
||||
LandmarkIcon,
|
||||
ShieldIcon,
|
||||
ScaleIcon,
|
||||
ShieldIcon,
|
||||
} from "lucide-react";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { formatAmount } from "@/lib/banking/format";
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ export default async function FactionsPage() {
|
|||
<h1 className="text-lg font-semibold">Factions</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{factions.length} faction{factions.length !== 1 ? "s" : ""} tracked in
|
||||
the theater of operations.
|
||||
{factions.length} faction{factions.length !== 1 ? "s" : ""} tracked in the theater of
|
||||
operations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,35 +3,18 @@ import { getPayload } from "payload";
|
|||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import type {
|
||||
Mission,
|
||||
Campaign,
|
||||
Map,
|
||||
User,
|
||||
Faction,
|
||||
Qualification,
|
||||
Rank,
|
||||
Media,
|
||||
} from "@/payload-types";
|
||||
import type { Campaign, Map, Media, Mission } from "@/payload-types";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
CalendarDaysIcon,
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
MapPinIcon,
|
||||
CrosshairIcon,
|
||||
ServerIcon,
|
||||
FlagIcon,
|
||||
MapPinIcon,
|
||||
ServerIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemHeader,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/components/ui/item";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { MissionBriefingSection } from "@/components/frontend/intelligence/MissionBriefingSection";
|
||||
import { MissionObjectives } from "@/components/frontend/intelligence/MissionObjectives";
|
||||
import { MissionFactions } from "@/components/frontend/intelligence/MissionFactions";
|
||||
|
|
@ -40,10 +23,10 @@ import { MissionLogistics } from "@/components/frontend/intelligence/MissionLogi
|
|||
import { MissionComms } from "@/components/frontend/intelligence/MissionComms";
|
||||
import { MissionAttendance } from "@/components/frontend/intelligence/MissionAttendance";
|
||||
import {
|
||||
type AttendanceResponse,
|
||||
getMissionAttendance,
|
||||
getUserAttendance,
|
||||
isMissionPast,
|
||||
type AttendanceResponse,
|
||||
} from "@/lib/attendance";
|
||||
|
||||
interface MissionDetailPageProps {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import { setMissionAttendance as setAttendance } from "@/lib/attendance";
|
||||
import type { AttendanceResponse } from "@/lib/attendance";
|
||||
import { setMissionAttendance as setAttendance } from "@/lib/attendance";
|
||||
|
||||
export interface ActionResult<T = undefined> {
|
||||
success: boolean;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import type { Mission, Campaign, Map } from "@/payload-types";
|
||||
import type { Campaign, Mission } from "@/payload-types";
|
||||
import { CrosshairIcon } from "lucide-react";
|
||||
import { MissionsList } from "@/components/frontend/intelligence/MissionsList";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,10 @@ import { getPayload } from "payload";
|
|||
import { headers as nextHeaders } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { Campaign, Mission, Faction } from "@/payload-types";
|
||||
import type { Campaign, Faction } from "@/payload-types";
|
||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
import { CrosshairIcon, ArrowRightIcon } from "lucide-react";
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemHeader,
|
||||
ItemTitle,
|
||||
} from "@/components/ui/item";
|
||||
import { ArrowRightIcon, CrosshairIcon } from "lucide-react";
|
||||
import { Item, ItemContent, ItemDescription, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { CampaignCard } from "@/components/frontend/intelligence/CampaignCard";
|
||||
|
||||
export const metadata = {
|
||||
|
|
@ -55,8 +49,7 @@ export default async function IntelligenceOverviewPage() {
|
|||
<h1 className="text-lg font-semibold">Intelligence</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Campaigns, missions, and faction intelligence for the theater of
|
||||
operations.
|
||||
Campaigns, missions, and faction intelligence for the theater of operations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -71,9 +64,7 @@ export default async function IntelligenceOverviewPage() {
|
|||
<ArrowRightIcon className="size-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<ItemDescription>
|
||||
Active and completed campaigns across all theaters.
|
||||
</ItemDescription>
|
||||
<ItemDescription>Active and completed campaigns across all theaters.</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</Link>
|
||||
|
|
@ -87,9 +78,7 @@ export default async function IntelligenceOverviewPage() {
|
|||
<ArrowRightIcon className="size-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<ItemDescription>
|
||||
Operation briefings, objectives, and schedules.
|
||||
</ItemDescription>
|
||||
<ItemDescription>Operation briefings, objectives, and schedules.</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</Link>
|
||||
|
|
@ -104,8 +93,7 @@ export default async function IntelligenceOverviewPage() {
|
|||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<ItemDescription>
|
||||
{factions.length} faction{factions.length !== 1 ? "s" : ""}{" "}
|
||||
tracked in the AO.
|
||||
{factions.length} faction{factions.length !== 1 ? "s" : ""} tracked in the AO.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import {
|
|||
autoArrangeLockerItems,
|
||||
ensureLockerStorage,
|
||||
findLockerEmptySpot,
|
||||
getLoadoutSlot,
|
||||
getLockerGridDimensions,
|
||||
getLockerItemFootprint,
|
||||
getLoadoutSlot,
|
||||
isAttachmentCompatible,
|
||||
isLockerPlacementValid,
|
||||
isSkinAsset,
|
||||
|
|
@ -23,7 +23,6 @@ import {
|
|||
resolveAssetId,
|
||||
skinAppliesTo,
|
||||
toLockerGridItems,
|
||||
type LockerGridItem,
|
||||
} from "@/lib/locker";
|
||||
|
||||
export interface ActionResult<T = undefined> {
|
||||
|
|
@ -219,7 +218,8 @@ export async function removeLockerItem(entryId: string): Promise<ActionResult> {
|
|||
|
||||
const removed = items[idx];
|
||||
const asset = removed.asset as Asset;
|
||||
const removedName = typeof asset === "object" && asset ? asset.name : `asset #${resolveAssetId(removed.asset)}`;
|
||||
const removedName =
|
||||
typeof asset === "object" && asset ? asset.name : `asset #${resolveAssetId(removed.asset)}`;
|
||||
|
||||
items.splice(idx, 1);
|
||||
await payload.update({
|
||||
|
|
@ -236,7 +236,12 @@ export async function removeLockerItem(entryId: string): Promise<ActionResult> {
|
|||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { entryId, assetId: resolveAssetId(removed.asset), assetName: removedName, quantity: removed.quantity },
|
||||
data: {
|
||||
entryId,
|
||||
assetId: resolveAssetId(removed.asset),
|
||||
assetName: removedName,
|
||||
quantity: removed.quantity,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -269,19 +274,33 @@ export async function moveLockerItem(
|
|||
const effectiveRotated = rotated ?? entry.rotated ?? false;
|
||||
const fp = getLockerItemFootprint(asset, effectiveRotated);
|
||||
|
||||
if (gridX < 0 || gridY < 0 || gridX + fp.width > grid.width || gridY + fp.height > grid.height) {
|
||||
if (
|
||||
gridX < 0 ||
|
||||
gridY < 0 ||
|
||||
gridX + fp.width > grid.width ||
|
||||
gridY + fp.height > grid.height
|
||||
) {
|
||||
return { success: false, error: "Placement is outside grid bounds." };
|
||||
}
|
||||
|
||||
const gridItems = toLockerGridItems(items);
|
||||
if (!isLockerPlacementValid(gridItems, entryId, gridX, gridY, fp.width, fp.height, grid.width, grid.height)) {
|
||||
if (
|
||||
!isLockerPlacementValid(
|
||||
gridItems,
|
||||
entryId,
|
||||
gridX,
|
||||
gridY,
|
||||
fp.width,
|
||||
fp.height,
|
||||
grid.width,
|
||||
grid.height,
|
||||
)
|
||||
) {
|
||||
return { success: false, error: "Cannot move here — overlaps with another item." };
|
||||
}
|
||||
|
||||
items[idx] =
|
||||
rotated !== undefined
|
||||
? { ...entry, gridX, gridY, rotated }
|
||||
: { ...entry, gridX, gridY };
|
||||
rotated !== undefined ? { ...entry, gridX, gridY, rotated } : { ...entry, gridX, gridY };
|
||||
|
||||
await payload.update({
|
||||
collection: "locker-storages",
|
||||
|
|
@ -297,7 +316,14 @@ export async function moveLockerItem(
|
|||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { entryId, assetId: asset.id, assetName: asset.name, gridX, gridY, rotated: effectiveRotated },
|
||||
data: {
|
||||
entryId,
|
||||
assetId: asset.id,
|
||||
assetName: asset.name,
|
||||
gridX,
|
||||
gridY,
|
||||
rotated: effectiveRotated,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -321,7 +347,8 @@ export async function rotateLockerItem(entryId: string): Promise<ActionResult> {
|
|||
const entry = items[idx];
|
||||
const asset = entry.asset as Asset;
|
||||
if (!asset || typeof asset === "number") return { success: false, error: "Invalid asset." };
|
||||
if (!lockerIsRotatable(asset)) return { success: false, error: "This asset cannot be rotated." };
|
||||
if (!lockerIsRotatable(asset))
|
||||
return { success: false, error: "This asset cannot be rotated." };
|
||||
|
||||
const newRotated = !(entry.rotated ?? false);
|
||||
const fp = getLockerItemFootprint(asset, newRotated);
|
||||
|
|
@ -331,7 +358,18 @@ export async function rotateLockerItem(entryId: string): Promise<ActionResult> {
|
|||
}
|
||||
|
||||
const gridItems = toLockerGridItems(items);
|
||||
if (!isLockerPlacementValid(gridItems, entryId, entry.gridX, entry.gridY, fp.width, fp.height, grid.width, grid.height)) {
|
||||
if (
|
||||
!isLockerPlacementValid(
|
||||
gridItems,
|
||||
entryId,
|
||||
entry.gridX,
|
||||
entry.gridY,
|
||||
fp.width,
|
||||
fp.height,
|
||||
grid.width,
|
||||
grid.height,
|
||||
)
|
||||
) {
|
||||
return { success: false, error: "Rotation would overlap with another item." };
|
||||
}
|
||||
|
||||
|
|
@ -362,10 +400,7 @@ export async function rotateLockerItem(entryId: string): Promise<ActionResult> {
|
|||
}
|
||||
}
|
||||
|
||||
export async function mergeLockerStacks(
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
): Promise<ActionResult> {
|
||||
export async function mergeLockerStacks(sourceId: string, targetId: string): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
|
|
@ -385,7 +420,8 @@ export async function mergeLockerStacks(
|
|||
if (srcAsset.id !== tgtAsset.id) {
|
||||
return { success: false, error: "Cannot merge different asset types." };
|
||||
}
|
||||
if (!lockerIsStackable(tgtAsset)) return { success: false, error: "This asset cannot be stacked." };
|
||||
if (!lockerIsStackable(tgtAsset))
|
||||
return { success: false, error: "This asset cannot be stacked." };
|
||||
|
||||
const maxStack = lockerMaxStack(tgtAsset);
|
||||
const spaceInTarget = maxStack === Infinity ? Infinity : Math.max(0, maxStack - tgt.quantity);
|
||||
|
|
@ -426,7 +462,10 @@ export async function mergeLockerStacks(
|
|||
}
|
||||
}
|
||||
|
||||
export async function splitLockerStack(entryId: string, splitAmount: number): Promise<ActionResult> {
|
||||
export async function splitLockerStack(
|
||||
entryId: string,
|
||||
splitAmount: number,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!Number.isInteger(splitAmount) || splitAmount <= 0) {
|
||||
|
|
@ -445,7 +484,8 @@ export async function splitLockerStack(entryId: string, splitAmount: number): Pr
|
|||
}
|
||||
const asset = entry.asset as Asset;
|
||||
if (!asset || typeof asset === "number") return { success: false, error: "Invalid asset." };
|
||||
if (!lockerIsStackable(asset)) return { success: false, error: "This asset cannot be stacked." };
|
||||
if (!lockerIsStackable(asset))
|
||||
return { success: false, error: "This asset cannot be stacked." };
|
||||
|
||||
const gridItems = toLockerGridItems(items);
|
||||
const fp = getLockerItemFootprint(asset, entry.rotated ?? false);
|
||||
|
|
@ -475,7 +515,13 @@ export async function splitLockerStack(entryId: string, splitAmount: number): Pr
|
|||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { entryId, assetId: asset.id, assetName: asset.name, splitAmount, remainingAmount: entry.quantity - splitAmount },
|
||||
data: {
|
||||
entryId,
|
||||
assetId: asset.id,
|
||||
assetName: asset.name,
|
||||
splitAmount,
|
||||
remainingAmount: entry.quantity - splitAmount,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -674,7 +720,7 @@ export async function setLoadoutSlot(
|
|||
});
|
||||
|
||||
const equippedName = assetId
|
||||
? (await getAssetOrThrow(payload, assetId).catch(() => null))?.name ?? `asset #${assetId}`
|
||||
? ((await getAssetOrThrow(payload, assetId).catch(() => null))?.name ?? `asset #${assetId}`)
|
||||
: null;
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
|
|
@ -778,10 +824,7 @@ export async function equipLoadout(loadoutId: number, equipped = true): Promise<
|
|||
|
||||
// --- Equipment customization (attachments & skins) ---
|
||||
|
||||
export async function setItemAttachment(
|
||||
entryId: string,
|
||||
assetId: number,
|
||||
): Promise<ActionResult> {
|
||||
export async function setItemAttachment(entryId: string, assetId: number): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
|
|
@ -954,7 +997,8 @@ export async function setItemSkin(entryId: string, assetId: number): Promise<Act
|
|||
|
||||
const target = items[targetIdx];
|
||||
const equipment = target.asset as Asset;
|
||||
if (!equipment || typeof equipment === "number") return { success: false, error: "Invalid asset." };
|
||||
if (!equipment || typeof equipment === "number")
|
||||
return { success: false, error: "Invalid asset." };
|
||||
|
||||
const skin = await getAssetOrThrow(payload, assetId);
|
||||
if (!isSkinAsset(skin)) return { success: false, error: `"${skin.name}" is not a skin.` };
|
||||
|
|
@ -1056,7 +1100,13 @@ export async function removeItemSkin(entryId: string): Promise<ActionResult> {
|
|||
|
||||
const skin = await getAssetOrThrow(payload, skinId);
|
||||
const fp = getLockerItemFootprint(skin, false);
|
||||
const spot = findLockerEmptySpot(toLockerGridItems(items), fp.width, fp.height, grid.width, grid.height);
|
||||
const spot = findLockerEmptySpot(
|
||||
toLockerGridItems(items),
|
||||
fp.width,
|
||||
fp.height,
|
||||
grid.width,
|
||||
grid.height,
|
||||
);
|
||||
if (!spot) return { success: false, error: `No space in locker to return ${skin.name}.` };
|
||||
|
||||
const updatedItems = [...items];
|
||||
|
|
@ -1092,10 +1142,7 @@ export async function removeItemSkin(entryId: string): Promise<ActionResult> {
|
|||
}
|
||||
}
|
||||
|
||||
export async function renameLockerItem(
|
||||
entryId: string,
|
||||
customName: string,
|
||||
): Promise<ActionResult> {
|
||||
export async function renameLockerItem(entryId: string, customName: string): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
|
|
@ -1121,9 +1168,7 @@ export async function renameLockerItem(
|
|||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerItemRename,
|
||||
message: name
|
||||
? `Renamed ${asset.name} to "${name}"`
|
||||
: `Cleared custom name on ${asset.name}`,
|
||||
message: name ? `Renamed ${asset.name} to "${name}"` : `Cleared custom name on ${asset.name}`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
|
|
|
|||
|
|
@ -47,10 +47,7 @@ export default async function AccountPage({ params }: AccountPageProps) {
|
|||
? typedAccount.ownerUser?.id
|
||||
: typedAccount.ownerUser;
|
||||
|
||||
const isOwner =
|
||||
typedAccount.accountType === "personal" &&
|
||||
user !== null &&
|
||||
ownerId === user.id;
|
||||
const isOwner = typedAccount.accountType === "personal" && user !== null && ownerId === user.id;
|
||||
|
||||
if (!isManager && !isOwner) {
|
||||
notFound();
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type {
|
||||
GameVehicle,
|
||||
Vehicle,
|
||||
GameStructure,
|
||||
Structure,
|
||||
Resource,
|
||||
} from "@/payload-types";
|
||||
import type { GameStructure, GameVehicle, Resource, Structure, Vehicle } from "@/payload-types";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
FuelIcon,
|
||||
|
|
@ -38,9 +32,7 @@ const statusColors: Record<string, string> = {
|
|||
destroyed: "bg-red-500/20 text-red-400",
|
||||
};
|
||||
|
||||
export default async function GameVehicleDetailPage({
|
||||
params,
|
||||
}: GameVehicleDetailPageProps) {
|
||||
export default async function GameVehicleDetailPage({ params }: GameVehicleDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
|
|
@ -59,11 +51,9 @@ export default async function GameVehicleDetailPage({
|
|||
const vehicleData = vehicle as unknown as GameVehicle & {
|
||||
type: Vehicle;
|
||||
};
|
||||
const vehicleType =
|
||||
typeof vehicleData.type === "object" ? vehicleData.type : null;
|
||||
const vehicleType = typeof vehicleData.type === "object" ? vehicleData.type : null;
|
||||
const deployedAt =
|
||||
typeof vehicleData.deployedAt === "object" &&
|
||||
vehicleData.deployedAt !== null
|
||||
typeof vehicleData.deployedAt === "object" && vehicleData.deployedAt !== null
|
||||
? (vehicleData.deployedAt as GameStructure & { type: Structure })
|
||||
: null;
|
||||
const faction =
|
||||
|
|
@ -100,8 +90,7 @@ export default async function GameVehicleDetailPage({
|
|||
>
|
||||
{vehicleData.status === "in_transit"
|
||||
? "In Transit"
|
||||
: vehicleData.status.charAt(0).toUpperCase() +
|
||||
vehicleData.status.slice(1)}
|
||||
: vehicleData.status.charAt(0).toUpperCase() + vehicleData.status.slice(1)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{vehicleType?.name ?? "Unknown type"}
|
||||
|
|
@ -118,8 +107,7 @@ export default async function GameVehicleDetailPage({
|
|||
<ItemContent>
|
||||
<ItemTitle>Fuel</ItemTitle>
|
||||
<ItemDescription>
|
||||
{vehicleData.currentFuel?.toFixed(1)} /{" "}
|
||||
{vehicleType?.fuel?.fuelCapacity ?? "?"}
|
||||
{vehicleData.currentFuel?.toFixed(1)} / {vehicleType?.fuel?.fuelCapacity ?? "?"}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
|
@ -129,9 +117,7 @@ export default async function GameVehicleDetailPage({
|
|||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Health</ItemTitle>
|
||||
<ItemDescription>
|
||||
{vehicleData.currentHealth}
|
||||
</ItemDescription>
|
||||
<ItemDescription>{vehicleData.currentHealth}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" size="sm">
|
||||
|
|
@ -140,9 +126,7 @@ export default async function GameVehicleDetailPage({
|
|||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Location</ItemTitle>
|
||||
<ItemDescription>
|
||||
{deployedAt?.name ?? "In transit"}
|
||||
</ItemDescription>
|
||||
<ItemDescription>{deployedAt?.name ?? "In transit"}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" size="sm">
|
||||
|
|
@ -151,9 +135,7 @@ export default async function GameVehicleDetailPage({
|
|||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Faction</ItemTitle>
|
||||
<ItemDescription>
|
||||
{faction?.name ?? "Unassigned"}
|
||||
</ItemDescription>
|
||||
<ItemDescription>{faction?.name ?? "Unassigned"}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</div>
|
||||
|
|
@ -180,25 +162,18 @@ export default async function GameVehicleDetailPage({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-mono font-semibold">
|
||||
{fuelPercent.toFixed(0)}%
|
||||
</span>
|
||||
<span className="text-sm font-mono font-semibold">{fuelPercent.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>
|
||||
{vehicleData.currentFuel?.toFixed(1)} units remaining
|
||||
</span>
|
||||
<span>
|
||||
Capacity: {vehicleType?.fuel?.fuelCapacity ?? "?"} units
|
||||
</span>
|
||||
<span>{vehicleData.currentFuel?.toFixed(1)} units remaining</span>
|
||||
<span>Capacity: {vehicleType?.fuel?.fuelCapacity ?? "?"} units</span>
|
||||
</div>
|
||||
{vehicleType?.fuel?.fuelType &&
|
||||
typeof vehicleType.fuel.fuelType === "object" && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Fuel type: {vehicleType.fuel.fuelType.name} · Consumption
|
||||
rate: {vehicleType.fuel.fuelConsumptionRate}/unit
|
||||
</div>
|
||||
)}
|
||||
{vehicleType?.fuel?.fuelType && typeof vehicleType.fuel.fuelType === "object" && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Fuel type: {vehicleType.fuel.fuelType.name} · Consumption rate:{" "}
|
||||
{vehicleType.fuel.fuelConsumptionRate}/unit
|
||||
</div>
|
||||
)}
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
||||
|
|
@ -227,14 +202,9 @@ export default async function GameVehicleDetailPage({
|
|||
<tbody>
|
||||
{storedResources.map((entry, index) => {
|
||||
const resource =
|
||||
typeof entry.resource === "object"
|
||||
? (entry.resource as Resource)
|
||||
: null;
|
||||
typeof entry.resource === "object" ? (entry.resource as Resource) : null;
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b border-border/50"
|
||||
>
|
||||
<tr key={index} className="border-b border-border/50">
|
||||
<td className="px-4 py-2 font-medium">
|
||||
{resource?.name ?? `Resource #${entry.resource}`}
|
||||
</td>
|
||||
|
|
@ -248,9 +218,7 @@ export default async function GameVehicleDetailPage({
|
|||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
Vehicle cargo is empty.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground italic">Vehicle cargo is empty.</p>
|
||||
)}
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
|
@ -266,28 +234,25 @@ export default async function GameVehicleDetailPage({
|
|||
<ItemContent>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<div>
|
||||
<p className="font-semibold">
|
||||
{vehicleType?.name ?? "Unknown"}
|
||||
</p>
|
||||
<p className="font-semibold">{vehicleType?.name ?? "Unknown"}</p>
|
||||
{vehicleType?.classname && (
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{vehicleType.classname}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{vehicleType?.category &&
|
||||
vehicleType.category.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{vehicleType.category.map((cat) => (
|
||||
<span
|
||||
key={cat}
|
||||
className="text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted"
|
||||
>
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{vehicleType?.category && vehicleType.category.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{vehicleType.category.map((cat) => (
|
||||
<span
|
||||
key={cat}
|
||||
className="text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted"
|
||||
>
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
|
|
|||
|
|
@ -52,17 +52,13 @@ export default async function GameVehiclesPage() {
|
|||
{vehicles.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{vehicles.map((vehicle) => {
|
||||
const vehicleType =
|
||||
typeof vehicle.type === "object" ? vehicle.type : null;
|
||||
const vehicleType = typeof vehicle.type === "object" ? vehicle.type : null;
|
||||
const fuelPercent = vehicleType?.fuel?.fuelCapacity
|
||||
? (vehicle.currentFuel / vehicleType.fuel.fuelCapacity) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={vehicle.id}
|
||||
href={`/logistics/game-vehicles/${vehicle.id}`}
|
||||
>
|
||||
<Link key={vehicle.id} href={`/logistics/game-vehicles/${vehicle.id}`}>
|
||||
<Item
|
||||
variant="outline"
|
||||
className="cursor-pointer group transition-all duration-200 hover:scale-[1.01] hover:[box-shadow:inset_0_2px_8px_rgba(255,255,255,0.33)]"
|
||||
|
|
@ -84,8 +80,7 @@ export default async function GameVehiclesPage() {
|
|||
>
|
||||
{vehicle.status === "in_transit"
|
||||
? "In Transit"
|
||||
: vehicle.status.charAt(0).toUpperCase() +
|
||||
vehicle.status.slice(1)}
|
||||
: vehicle.status.charAt(0).toUpperCase() + vehicle.status.slice(1)}
|
||||
</span>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
|
|
@ -94,9 +89,7 @@ export default async function GameVehiclesPage() {
|
|||
<div className="flex-1 h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${
|
||||
fuelPercent > 20
|
||||
? "bg-green-500"
|
||||
: "bg-red-500"
|
||||
fuelPercent > 20 ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(100, fuelPercent)}%`,
|
||||
|
|
@ -112,16 +105,12 @@ export default async function GameVehiclesPage() {
|
|||
<span>HP: {vehicle.currentHealth}</span>
|
||||
{vehicleType?.fuel?.fuelType &&
|
||||
typeof vehicleType.fuel.fuelType === "object" && (
|
||||
<span>
|
||||
{vehicleType.fuel.fuelType.name}
|
||||
</span>
|
||||
<span>{vehicleType.fuel.fuelType.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ItemContent>
|
||||
<ItemDescription>
|
||||
{vehicleType?.name ?? "Unknown type"}
|
||||
</ItemDescription>
|
||||
<ItemDescription>{vehicleType?.name ?? "Unknown type"}</ItemDescription>
|
||||
</Item>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,17 +7,22 @@ import hasRoles from "@/utils/access-control/hasRoles";
|
|||
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||
import {
|
||||
buyListing,
|
||||
countCleanQuantity,
|
||||
creditLockerQuantity,
|
||||
deductLockerQuantity,
|
||||
buyListing,
|
||||
negotiationRange,
|
||||
USER_LISTING_DURATION_MS,
|
||||
} from "@/lib/market";
|
||||
import { ensureLockerStorage, resolveAssetId } from "@/lib/locker";
|
||||
import { getMainCurrencyId } from "@/lib/banking";
|
||||
import { notifyUser } from "@/lib/notifications";
|
||||
import { expireNegotiationsForListing, npcDialogue, npcResponseToOffer, type NpcState } from "@/lib/market/negotiations";
|
||||
import {
|
||||
expireNegotiationsForListing,
|
||||
npcDialogue,
|
||||
npcResponseToOffer,
|
||||
type NpcState,
|
||||
} from "@/lib/market/negotiations";
|
||||
|
||||
export interface ActionResult<T = undefined> {
|
||||
success: boolean;
|
||||
|
|
@ -86,7 +91,9 @@ async function getListingOrThrow(
|
|||
|
||||
function assetNameOf(listing: MarketListing): string {
|
||||
const asset = listing.asset as Asset;
|
||||
return typeof asset === "object" && asset ? asset.name : `asset #${resolveAssetId(listing.asset)}`;
|
||||
return typeof asset === "object" && asset
|
||||
? asset.name
|
||||
: `asset #${resolveAssetId(listing.asset)}`;
|
||||
}
|
||||
|
||||
async function getNegotiationOrThrow(
|
||||
|
|
@ -94,7 +101,12 @@ async function getNegotiationOrThrow(
|
|||
negotiationId: number,
|
||||
): Promise<MarketNegotiation> {
|
||||
const negotiation = (await payload
|
||||
.findByID({ collection: "market-negotiations", id: negotiationId, depth: 2, overrideAccess: true })
|
||||
.findByID({
|
||||
collection: "market-negotiations",
|
||||
id: negotiationId,
|
||||
depth: 2,
|
||||
overrideAccess: true,
|
||||
})
|
||||
.catch(() => null)) as unknown as MarketNegotiation | null;
|
||||
if (!negotiation) throw new Error("Negotiation not found.");
|
||||
return negotiation;
|
||||
|
|
@ -146,10 +158,7 @@ function enforceHigherBuyerOffer(negotiation: MarketNegotiation, amount: number)
|
|||
}
|
||||
|
||||
/** Role of the acting user in a negotiation: "buyer", "seller", or null. */
|
||||
function partyRoleOf(
|
||||
negotiation: MarketNegotiation,
|
||||
userId: number,
|
||||
): "buyer" | "seller" | null {
|
||||
function partyRoleOf(negotiation: MarketNegotiation, userId: number): "buyer" | "seller" | null {
|
||||
if (negotiationBuyerId(negotiation) === userId) return "buyer";
|
||||
const listing = negotiationListing(negotiation);
|
||||
if (typeof listing === "object" && listing) {
|
||||
|
|
@ -175,7 +184,7 @@ async function finalizeNegotiation(
|
|||
const buyerId = buyer.id as number;
|
||||
const sellerId = sellerIdOf(listing);
|
||||
const assetName = assetNameOf(listing);
|
||||
const quantity = negotiation.quantity ?? (listing.quantity ?? 1);
|
||||
const quantity = negotiation.quantity ?? listing.quantity ?? 1;
|
||||
const total = agreedPrice * quantity;
|
||||
|
||||
const result = await buyListing(payload, listing, buyer, { unitPrice: agreedPrice, quantity });
|
||||
|
|
@ -319,7 +328,7 @@ async function resolveNpcOffer(
|
|||
targetId: negotiation.id,
|
||||
data: {
|
||||
listingId: listing.id,
|
||||
quantity: negotiation.quantity ?? (listing.quantity ?? 1),
|
||||
quantity: negotiation.quantity ?? listing.quantity ?? 1,
|
||||
amount,
|
||||
counter: response.amount,
|
||||
patience: response.patience,
|
||||
|
|
@ -343,7 +352,7 @@ async function resolveNpcOffer(
|
|||
targetId: negotiation.id,
|
||||
data: {
|
||||
listingId: listing.id,
|
||||
quantity: negotiation.quantity ?? (listing.quantity ?? 1),
|
||||
quantity: negotiation.quantity ?? listing.quantity ?? 1,
|
||||
amount,
|
||||
counter: response.amount,
|
||||
patience: response.patience,
|
||||
|
|
@ -472,7 +481,12 @@ export async function cancelMarketListing(listingId: number): Promise<ActionResu
|
|||
const assetName =
|
||||
typeof asset === "object" && asset ? asset.name : `asset #${resolveAssetId(listing.asset)}`;
|
||||
|
||||
await creditLockerQuantity(payload, sellerId, resolveAssetId(listing.asset) as number, listing.quantity);
|
||||
await creditLockerQuantity(
|
||||
payload,
|
||||
sellerId,
|
||||
resolveAssetId(listing.asset) as number,
|
||||
listing.quantity,
|
||||
);
|
||||
|
||||
await payload.update({
|
||||
collection: "market-listings",
|
||||
|
|
@ -517,7 +531,7 @@ export async function buyMarketListing(
|
|||
return { success: false, error: "This listing is no longer active." };
|
||||
}
|
||||
|
||||
const qty = quantity ?? (listing.quantity ?? 1);
|
||||
const qty = quantity ?? listing.quantity ?? 1;
|
||||
if (!Number.isInteger(qty) || qty <= 0 || qty > (listing.quantity ?? 1)) {
|
||||
return { success: false, error: `Quantity must be between 1 and ${listing.quantity}.` };
|
||||
}
|
||||
|
|
@ -580,7 +594,7 @@ export async function makeMarketOffer(
|
|||
return { success: false, error: "Offer must be greater than zero." };
|
||||
}
|
||||
|
||||
const qty = quantity ?? (listing.quantity ?? 1);
|
||||
const qty = quantity ?? listing.quantity ?? 1;
|
||||
if (!Number.isInteger(qty) || qty <= 0 || qty > (listing.quantity ?? 1)) {
|
||||
return { success: false, error: `Quantity must be between 1 and ${listing.quantity}.` };
|
||||
}
|
||||
|
|
@ -642,7 +656,9 @@ export async function makeMarketOffer(
|
|||
amount,
|
||||
proposedBy: "buyer",
|
||||
quantity: qty,
|
||||
history: [{ amount, from: "buyer", note: "Initial offer", at: new Date().toISOString() }],
|
||||
history: [
|
||||
{ amount, from: "buyer", note: "Initial offer", at: new Date().toISOString() },
|
||||
],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 1,
|
||||
|
|
@ -728,7 +744,9 @@ export async function makeMarketOffer(
|
|||
* buyer accepts the seller's counteroffer). Completes the sale at the
|
||||
* agreed price.
|
||||
*/
|
||||
export async function acceptMarketOffer(negotiationId: number): Promise<ActionResult<MarketListing>> {
|
||||
export async function acceptMarketOffer(
|
||||
negotiationId: number,
|
||||
): Promise<ActionResult<MarketListing>> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const userId = user.id as number;
|
||||
|
|
@ -789,8 +807,12 @@ export async function rejectMarketOffer(negotiationId: number): Promise<ActionRe
|
|||
}
|
||||
|
||||
const listing = negotiationListing(negotiation);
|
||||
const listingId = typeof listing === "object" && listing ? (listing as MarketListing).id : (listing as number);
|
||||
const assetName = typeof listing === "object" && listing ? assetNameOf(listing as MarketListing) : "the listing";
|
||||
const listingId =
|
||||
typeof listing === "object" && listing ? (listing as MarketListing).id : (listing as number);
|
||||
const assetName =
|
||||
typeof listing === "object" && listing
|
||||
? assetNameOf(listing as MarketListing)
|
||||
: "the listing";
|
||||
|
||||
await payload.update({
|
||||
collection: "market-negotiations",
|
||||
|
|
@ -868,9 +890,7 @@ export async function counterMarketOffer(
|
|||
return { success: false, error: "This listing is no longer active." };
|
||||
}
|
||||
|
||||
const history = [
|
||||
...((negotiation.history ?? []) as NonNullable<MarketNegotiation["history"]>),
|
||||
];
|
||||
const history = [...((negotiation.history ?? []) as NonNullable<MarketNegotiation["history"]>)];
|
||||
history.push({ amount, from: role, note: "Counteroffer", at: new Date().toISOString() });
|
||||
|
||||
const updated = (await payload.update({
|
||||
|
|
@ -900,7 +920,8 @@ export async function counterMarketOffer(
|
|||
return { success: true, data: refreshed };
|
||||
}
|
||||
|
||||
const counterpartyId = role === "buyer" ? sellerId : (negotiationBuyerId(negotiation) as number);
|
||||
const counterpartyId =
|
||||
role === "buyer" ? sellerId : (negotiationBuyerId(negotiation) as number);
|
||||
await notifyUser(payload, {
|
||||
userId: counterpartyId,
|
||||
type: "market:counter",
|
||||
|
|
@ -933,9 +954,14 @@ export async function cancelMarketOffer(negotiationId: number): Promise<ActionRe
|
|||
if (!role) return { success: false, error: "You are not part of this negotiation." };
|
||||
|
||||
const listing = negotiationListing(negotiation);
|
||||
const listingId = typeof listing === "object" && listing ? (listing as MarketListing).id : (listing as number);
|
||||
const assetName = typeof listing === "object" && listing ? assetNameOf(listing as MarketListing) : "the listing";
|
||||
const sellerId = typeof listing === "object" && listing ? sellerIdOf(listing as MarketListing) : null;
|
||||
const listingId =
|
||||
typeof listing === "object" && listing ? (listing as MarketListing).id : (listing as number);
|
||||
const assetName =
|
||||
typeof listing === "object" && listing
|
||||
? assetNameOf(listing as MarketListing)
|
||||
: "the listing";
|
||||
const sellerId =
|
||||
typeof listing === "object" && listing ? sellerIdOf(listing as MarketListing) : null;
|
||||
|
||||
await payload.update({
|
||||
collection: "market-negotiations",
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ export default async function MarketPage() {
|
|||
and: [
|
||||
{ status: { equals: "active" } },
|
||||
{
|
||||
or: [
|
||||
{ expiresAt: { exists: false } },
|
||||
{ expiresAt: { greater_than: dayAgo } },
|
||||
],
|
||||
or: [{ expiresAt: { exists: false } }, { expiresAt: { greater_than: dayAgo } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -50,7 +47,9 @@ export default async function MarketPage() {
|
|||
|
||||
const myListings = listings.filter((l) => {
|
||||
const sellerId =
|
||||
typeof l.seller === "object" && l.seller ? (l.seller as { id: number }).id : (l.seller as number | null);
|
||||
typeof l.seller === "object" && l.seller
|
||||
? (l.seller as { id: number }).id
|
||||
: (l.seller as number | null);
|
||||
return sellerId === userId;
|
||||
});
|
||||
|
||||
|
|
@ -62,7 +61,9 @@ export default async function MarketPage() {
|
|||
});
|
||||
const negotiations = (negotiationsRes.docs as unknown as MarketNegotiation[]).filter((n) => {
|
||||
const buyerId =
|
||||
typeof n.buyer === "object" && n.buyer ? (n.buyer as { id: number }).id : (n.buyer as number | null);
|
||||
typeof n.buyer === "object" && n.buyer
|
||||
? (n.buyer as { id: number }).id
|
||||
: (n.buyer as number | null);
|
||||
if (buyerId === userId) return true;
|
||||
const listing = n.listing as unknown as MarketListing | number | null;
|
||||
if (typeof listing === "object" && listing) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import { GameStructure, Faction, Structure } from "@/payload-types";
|
||||
import { Faction, GameStructure, Structure } from "@/payload-types";
|
||||
import { StorageGrid } from "@/components/frontend/storage/StorageGrid";
|
||||
import { Building2Icon } from "lucide-react";
|
||||
|
||||
|
|
@ -40,11 +40,7 @@ export default async function LogisticsPage() {
|
|||
Overview of all structures and their stored resources across the theater of operations.
|
||||
</p>
|
||||
</div>
|
||||
<StorageGrid
|
||||
structures={structures}
|
||||
categories={categories}
|
||||
factions={factions}
|
||||
/>
|
||||
<StorageGrid structures={structures} categories={categories} factions={factions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { Resource, Structure, Asset } from "@/payload-types";
|
||||
import type { Asset, Structure } from "@/payload-types";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
|
||||
import {
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
CoinsIcon,
|
||||
Grid3x3Icon,
|
||||
LayersIcon,
|
||||
MoveIcon,
|
||||
RotateCcwIcon,
|
||||
ScaleIcon,
|
||||
TagIcon,
|
||||
|
|
@ -48,9 +47,7 @@ const typeColors: Record<string, string> = {
|
|||
abstract: "bg-purple-500/20 text-purple-400",
|
||||
};
|
||||
|
||||
export default async function ResourceDetailPage({
|
||||
params,
|
||||
}: ResourceDetailPageProps) {
|
||||
export default async function ResourceDetailPage({ params }: ResourceDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
|
|
@ -105,9 +102,7 @@ export default async function ResourceDetailPage({
|
|||
|
||||
const allRelatedStructures = [
|
||||
...producers,
|
||||
...autoGenStructures.filter(
|
||||
(s) => !producers.some((p) => p.id === s.id),
|
||||
),
|
||||
...autoGenStructures.filter((s) => !producers.some((p) => p.id === s.id)),
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -124,10 +119,7 @@ export default async function ResourceDetailPage({
|
|||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{resource.color ? (
|
||||
<div
|
||||
className="size-5 rounded-sm"
|
||||
style={{ backgroundColor: resource.color }}
|
||||
/>
|
||||
<div className="size-5 rounded-sm" style={{ backgroundColor: resource.color }} />
|
||||
) : (
|
||||
<BoxIcon className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
|
|
@ -243,10 +235,7 @@ export default async function ResourceDetailPage({
|
|||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{allRelatedStructures.map((structure) => (
|
||||
<Link
|
||||
key={structure.id}
|
||||
href={`/logistics/structures/${structure.id}`}
|
||||
>
|
||||
<Link key={structure.id} href={`/logistics/structures/${structure.id}`}>
|
||||
<Item
|
||||
variant="outline"
|
||||
className="cursor-pointer group transition-all duration-200 hover:scale-[1.01]"
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ export default async function ResourcesPage() {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No resources available.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground italic">No resources available.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,30 +2,16 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type {
|
||||
Shipment,
|
||||
GameStructure,
|
||||
GameVehicle,
|
||||
Vehicle,
|
||||
Resource,
|
||||
} from "@/payload-types";
|
||||
import type { GameStructure, GameVehicle, Vehicle } from "@/payload-types";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
ClockIcon,
|
||||
FuelIcon,
|
||||
MapPinIcon,
|
||||
PackageIcon,
|
||||
TruckIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemHeader,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/components/ui/item";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { ShipmentActions } from "@/components/frontend/logistics/ShipmentActions";
|
||||
import { ShipmentProgressBar } from "@/components/frontend/logistics/ShipmentProgressBar";
|
||||
|
||||
|
|
@ -44,9 +30,7 @@ const statusColors: Record<string, string> = {
|
|||
stranded: "bg-red-500/20 text-red-400",
|
||||
};
|
||||
|
||||
export default async function ShipmentDetailPage({
|
||||
params,
|
||||
}: ShipmentDetailPageProps) {
|
||||
export default async function ShipmentDetailPage({ params }: ShipmentDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
|
|
@ -71,31 +55,20 @@ export default async function ShipmentDetailPage({
|
|||
? (shipment.destination as GameStructure)
|
||||
: null;
|
||||
const vehicle =
|
||||
typeof shipment.transportVehicle === "object" &&
|
||||
shipment.transportVehicle !== null
|
||||
typeof shipment.transportVehicle === "object" && shipment.transportVehicle !== null
|
||||
? (shipment.transportVehicle as GameVehicle & { type: Vehicle })
|
||||
: null;
|
||||
const vehicleType =
|
||||
vehicle && typeof vehicle.type === "object" ? vehicle.type : null;
|
||||
const vehicleType = vehicle && typeof vehicle.type === "object" ? vehicle.type : null;
|
||||
|
||||
const dispatchedAt = shipment.dispatchedAt
|
||||
? new Date(shipment.dispatchedAt)
|
||||
: null;
|
||||
const estimatedArrival = shipment.estimatedArrival
|
||||
? new Date(shipment.estimatedArrival)
|
||||
: null;
|
||||
const completedAt = shipment.completedAt
|
||||
? new Date(shipment.completedAt)
|
||||
: null;
|
||||
const dispatchedAt = shipment.dispatchedAt ? new Date(shipment.dispatchedAt) : null;
|
||||
const estimatedArrival = shipment.estimatedArrival ? new Date(shipment.estimatedArrival) : null;
|
||||
const completedAt = shipment.completedAt ? new Date(shipment.completedAt) : null;
|
||||
|
||||
const now = new Date();
|
||||
const isActive =
|
||||
shipment.status === "dispatched" || shipment.status === "in_transit";
|
||||
const isActive = shipment.status === "dispatched" || shipment.status === "in_transit";
|
||||
|
||||
const fuelPercent =
|
||||
shipment.fuelCost > 0
|
||||
? ((shipment.fuelConsumed ?? 0) / shipment.fuelCost) * 100
|
||||
: 0;
|
||||
shipment.fuelCost > 0 ? ((shipment.fuelConsumed ?? 0) / shipment.fuelCost) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="p-5 flex flex-col gap-6 max-w-7xl">
|
||||
|
|
@ -111,9 +84,7 @@ export default async function ShipmentDetailPage({
|
|||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<TruckIcon className="size-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">
|
||||
{shipment.name ?? `SHP-${shipment.id}`}
|
||||
</h1>
|
||||
<h1 className="text-lg font-semibold">{shipment.name ?? `SHP-${shipment.id}`}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
|
|
@ -121,8 +92,7 @@ export default async function ShipmentDetailPage({
|
|||
>
|
||||
{shipment.status === "in_transit"
|
||||
? "In Transit"
|
||||
: shipment.status.charAt(0).toUpperCase() +
|
||||
shipment.status.slice(1)}
|
||||
: shipment.status.charAt(0).toUpperCase() + shipment.status.slice(1)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{shipment.distance?.toFixed(1)} units distance
|
||||
|
|
@ -143,9 +113,7 @@ export default async function ShipmentDetailPage({
|
|||
<ItemContent>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 text-center">
|
||||
<p className="text-sm font-semibold">
|
||||
{origin?.name ?? "Unknown"}
|
||||
</p>
|
||||
<p className="text-sm font-semibold">{origin?.name ?? "Unknown"}</p>
|
||||
<p className="text-xs text-muted-foreground">Origin</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
|
|
@ -155,9 +123,7 @@ export default async function ShipmentDetailPage({
|
|||
</span>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<p className="text-sm font-semibold">
|
||||
{destination?.name ?? "Unknown"}
|
||||
</p>
|
||||
<p className="text-sm font-semibold">{destination?.name ?? "Unknown"}</p>
|
||||
<p className="text-xs text-muted-foreground">Destination</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -220,23 +186,14 @@ export default async function ShipmentDetailPage({
|
|||
value: number | { name?: string };
|
||||
};
|
||||
itemType = polymorphic.relationTo;
|
||||
if (
|
||||
typeof polymorphic.value === "object" &&
|
||||
polymorphic.value !== null
|
||||
) {
|
||||
itemName =
|
||||
polymorphic.value.name ?? "Unknown";
|
||||
if (typeof polymorphic.value === "object" && polymorphic.value !== null) {
|
||||
itemName = polymorphic.value.name ?? "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b border-border/50"
|
||||
>
|
||||
<td className="px-4 py-2 font-medium">
|
||||
{itemName}
|
||||
</td>
|
||||
<tr key={index} className="border-b border-border/50">
|
||||
<td className="px-4 py-2 font-medium">{itemName}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground capitalize">
|
||||
{itemType}
|
||||
</td>
|
||||
|
|
@ -250,9 +207,7 @@ export default async function ShipmentDetailPage({
|
|||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No cargo on this shipment.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground italic">No cargo on this shipment.</p>
|
||||
)}
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
|
@ -271,9 +226,7 @@ export default async function ShipmentDetailPage({
|
|||
<ItemContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
{vehicle?.name ?? "Unknown"}
|
||||
</p>
|
||||
<p className="text-sm font-semibold">{vehicle?.name ?? "Unknown"}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{vehicleType?.name ?? "Unknown type"}
|
||||
</p>
|
||||
|
|
@ -283,24 +236,19 @@ export default async function ShipmentDetailPage({
|
|||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Fuel</span>
|
||||
<span className="font-mono">
|
||||
{vehicle.currentFuel?.toFixed(1)} /{" "}
|
||||
{vehicleType?.fuel?.fuelCapacity ?? "?"}
|
||||
{vehicle.currentFuel?.toFixed(1)} / {vehicleType?.fuel?.fuelCapacity ?? "?"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${
|
||||
vehicle.currentFuel > 0
|
||||
? "bg-green-500"
|
||||
: "bg-red-500"
|
||||
vehicle.currentFuel > 0 ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(
|
||||
100,
|
||||
vehicleType?.fuel?.fuelCapacity
|
||||
? (vehicle.currentFuel /
|
||||
vehicleType.fuel.fuelCapacity) *
|
||||
100
|
||||
? (vehicle.currentFuel / vehicleType.fuel.fuelCapacity) * 100
|
||||
: 0,
|
||||
)}%`,
|
||||
}}
|
||||
|
|
@ -308,9 +256,7 @@ export default async function ShipmentDetailPage({
|
|||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Health</span>
|
||||
<span className="font-mono">
|
||||
{vehicle.currentHealth}
|
||||
</span>
|
||||
<span className="font-mono">{vehicle.currentHealth}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -334,9 +280,7 @@ export default async function ShipmentDetailPage({
|
|||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Consumed</span>
|
||||
<span className="font-mono">
|
||||
{(shipment.fuelConsumed ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<span className="font-mono">{(shipment.fuelConsumed ?? 0).toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
|
|
@ -375,9 +319,7 @@ export default async function ShipmentDetailPage({
|
|||
{estimatedArrival && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{shipment.status === "completed"
|
||||
? "Arrived"
|
||||
: "Est. Arrival"}
|
||||
{shipment.status === "completed" ? "Arrived" : "Est. Arrival"}
|
||||
</span>
|
||||
<span>
|
||||
{estimatedArrival.toLocaleDateString()}{" "}
|
||||
|
|
@ -411,9 +353,7 @@ export default async function ShipmentDetailPage({
|
|||
<ItemTitle>Failure Reason</ItemTitle>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<p className="text-sm text-red-400">
|
||||
{shipment.failureReason}
|
||||
</p>
|
||||
<p className="text-sm text-red-400">{shipment.failureReason}</p>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,13 @@
|
|||
|
||||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import type {
|
||||
GameStructure,
|
||||
GameVehicle,
|
||||
Structure,
|
||||
Vehicle,
|
||||
Resource,
|
||||
} from "@/payload-types";
|
||||
import type { GameStructure, GameVehicle, Resource, Structure, Vehicle } from "@/payload-types";
|
||||
import hasRoles from "@/utils/access-control/hasRoles";
|
||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||
import { calculateDistance } from "@/lib/distance";
|
||||
import {
|
||||
calculateFuelCost,
|
||||
calculateTransitTime,
|
||||
getVehicleEffectiveSpeed,
|
||||
} from "@/lib/shipping";
|
||||
import { processShipmentTick } from "@/scripts/processShipmentTick";
|
||||
import {
|
||||
checkStorageDeposit,
|
||||
storageViolationMessage,
|
||||
} from "@/lib/storageRules";
|
||||
import { calculateFuelCost, calculateTransitTime, getVehicleEffectiveSpeed } from "@/lib/shipping";
|
||||
import { checkStorageDeposit, storageViolationMessage } from "@/lib/storageRules";
|
||||
|
||||
interface ActionResult {
|
||||
success: boolean;
|
||||
|
|
@ -105,8 +91,7 @@ export async function createShipment(params: {
|
|||
if (!originCoords || !destCoords) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Both origin and destination must have coordinates set.",
|
||||
error: "Both origin and destination must have coordinates set.",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -146,9 +131,7 @@ export async function createShipment(params: {
|
|||
}
|
||||
|
||||
const vehicleAtId =
|
||||
typeof vehicle.deployedAt === "object"
|
||||
? vehicle.deployedAt?.id
|
||||
: vehicle.deployedAt;
|
||||
typeof vehicle.deployedAt === "object" ? vehicle.deployedAt?.id : vehicle.deployedAt;
|
||||
|
||||
if (vehicleAtId !== originId) {
|
||||
return {
|
||||
|
|
@ -158,8 +141,7 @@ export async function createShipment(params: {
|
|||
}
|
||||
|
||||
// Get vehicle type details
|
||||
const vehicleType =
|
||||
typeof vehicle.type === "object" ? vehicle.type : null;
|
||||
const vehicleType = typeof vehicle.type === "object" ? vehicle.type : null;
|
||||
if (!vehicleType) {
|
||||
return { success: false, error: "Vehicle type not found." };
|
||||
}
|
||||
|
|
@ -182,8 +164,7 @@ export async function createShipment(params: {
|
|||
if (cargoItem.relationTo === "resources") {
|
||||
const available = storedResources
|
||||
.filter((r) => {
|
||||
const resId =
|
||||
typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
const resId = typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
return resId === cargoItem.value;
|
||||
})
|
||||
.reduce((sum, r) => sum + r.amount, 0);
|
||||
|
|
@ -191,18 +172,12 @@ export async function createShipment(params: {
|
|||
if (available < cargoItem.amount) {
|
||||
const resName =
|
||||
typeof storedResources.find((r) => {
|
||||
const resId =
|
||||
typeof r.resource === "object"
|
||||
? r.resource.id
|
||||
: r.resource;
|
||||
const resId = typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
return resId === cargoItem.value;
|
||||
})?.resource === "object"
|
||||
? (
|
||||
storedResources.find((r) => {
|
||||
const resId =
|
||||
typeof r.resource === "object"
|
||||
? r.resource.id
|
||||
: r.resource;
|
||||
const resId = typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
return resId === cargoItem.value;
|
||||
})?.resource as Resource
|
||||
).name
|
||||
|
|
@ -217,8 +192,7 @@ export async function createShipment(params: {
|
|||
}
|
||||
|
||||
// Validate destination can accept the cargo (storage rules + mass capacity)
|
||||
const destType =
|
||||
typeof destination.type === "object" ? destination.type : null;
|
||||
const destType = typeof destination.type === "object" ? destination.type : null;
|
||||
if (destType) {
|
||||
const destStored = destination.storedResources ?? [];
|
||||
const destVoid = (destination as GameStructure).voidStorage ?? [];
|
||||
|
|
@ -233,8 +207,7 @@ export async function createShipment(params: {
|
|||
itemId: number,
|
||||
) =>
|
||||
entries.reduce((sum, r) => {
|
||||
const resId =
|
||||
typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
const resId = typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
return sum + (resId === itemId ? r.amount : 0);
|
||||
}, 0);
|
||||
|
||||
|
|
@ -257,8 +230,7 @@ export async function createShipment(params: {
|
|||
}
|
||||
|
||||
const currentStored =
|
||||
storedAmountIn(destStored, cargoItem.value) +
|
||||
storedAmountIn(destVoid, cargoItem.value);
|
||||
storedAmountIn(destStored, cargoItem.value) + storedAmountIn(destVoid, cargoItem.value);
|
||||
|
||||
const ruleCheck = checkStorageDeposit(
|
||||
destType,
|
||||
|
|
@ -278,10 +250,7 @@ export async function createShipment(params: {
|
|||
}
|
||||
|
||||
destMass += (resource.massPerUnit ?? 0) * cargoItem.amount;
|
||||
if (
|
||||
destType.maxCapacityMass &&
|
||||
destMass > destType.maxCapacityMass
|
||||
) {
|
||||
if (destType.maxCapacityMass && destMass > destType.maxCapacityMass) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Destination "${destination.name}" would exceed its mass capacity of ${destType.maxCapacityMass.toLocaleString()} kg.`,
|
||||
|
|
@ -327,8 +296,7 @@ export async function createShipment(params: {
|
|||
let remaining = cargoItem.amount;
|
||||
updatedResources = updatedResources
|
||||
.map((r) => {
|
||||
const resId =
|
||||
typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
const resId = typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
if (resId === cargoItem.value && remaining > 0) {
|
||||
const deduct = Math.min(r.amount, remaining);
|
||||
remaining -= deduct;
|
||||
|
|
@ -377,9 +345,7 @@ export async function createShipment(params: {
|
|||
fuelConsumed: 0,
|
||||
status: "dispatched",
|
||||
dispatchedAt: now.toISOString(),
|
||||
estimatedArrival: new Date(
|
||||
now.getTime() + transitTimeMs,
|
||||
).toISOString(),
|
||||
estimatedArrival: new Date(now.getTime() + transitTimeMs).toISOString(),
|
||||
autoReturn: false,
|
||||
createdBy: user.id,
|
||||
},
|
||||
|
|
@ -402,8 +368,7 @@ export async function createShipment(params: {
|
|||
|
||||
return { success: true, data: shipment };
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
if (message === "Unauthorized") {
|
||||
return { success: false, error: "You must be logged in." };
|
||||
}
|
||||
|
|
@ -411,9 +376,7 @@ export async function createShipment(params: {
|
|||
}
|
||||
}
|
||||
|
||||
export async function cancelShipment(
|
||||
shipmentId: number,
|
||||
): Promise<ActionResult> {
|
||||
export async function cancelShipment(shipmentId: number): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
|
||||
|
|
@ -433,10 +396,7 @@ export async function cancelShipment(
|
|||
return { success: false, error: "Shipment not found." };
|
||||
}
|
||||
|
||||
if (
|
||||
shipment.status !== "dispatched" &&
|
||||
shipment.status !== "in_transit"
|
||||
) {
|
||||
if (shipment.status !== "dispatched" && shipment.status !== "in_transit") {
|
||||
return {
|
||||
success: false,
|
||||
error: `Cannot cancel shipment with status "${shipment.status}".`,
|
||||
|
|
@ -444,9 +404,7 @@ export async function cancelShipment(
|
|||
}
|
||||
|
||||
const originId =
|
||||
typeof shipment.origin === "object"
|
||||
? shipment.origin.id
|
||||
: (shipment.origin as number);
|
||||
typeof shipment.origin === "object" ? shipment.origin.id : (shipment.origin as number);
|
||||
const vehicleId =
|
||||
typeof shipment.transportVehicle === "object"
|
||||
? shipment.transportVehicle.id
|
||||
|
|
@ -464,9 +422,7 @@ export async function cancelShipment(
|
|||
if (polymorphic.relationTo !== "resources") continue;
|
||||
|
||||
const itemId =
|
||||
typeof polymorphic.value === "object"
|
||||
? polymorphic.value.id
|
||||
: polymorphic.value;
|
||||
typeof polymorphic.value === "object" ? polymorphic.value.id : polymorphic.value;
|
||||
|
||||
const origin = await payload.findByID({
|
||||
collection: "game-structures",
|
||||
|
|
@ -476,8 +432,7 @@ export async function cancelShipment(
|
|||
|
||||
const existingResources = origin.storedResources ?? [];
|
||||
const existingEntry = existingResources.find((r) => {
|
||||
const resId =
|
||||
typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
const resId = typeof r.resource === "object" ? r.resource.id : r.resource;
|
||||
return resId === itemId;
|
||||
});
|
||||
|
||||
|
|
@ -531,8 +486,7 @@ export async function cancelShipment(
|
|||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
if (message === "Unauthorized") {
|
||||
return { success: false, error: "You must be logged in." };
|
||||
}
|
||||
|
|
@ -574,8 +528,7 @@ export async function toggleAutoReturn(
|
|||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import type { Shipment, GameStructure, GameVehicle, Vehicle } from "@/payload-types";
|
||||
import type { Shipment } from "@/payload-types";
|
||||
import { TruckIcon } from "lucide-react";
|
||||
import { ShipmentsList } from "@/components/frontend/logistics/ShipmentsList";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { getPayload } from "payload";
|
|||
import { notFound } from "next/navigation";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { GameStructure, Structure, Faction, Resource } from "@/payload-types";
|
||||
import { Faction, GameStructure, Resource, Structure } from "@/payload-types";
|
||||
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
|
||||
import { CapacityBar } from "@/components/frontend/storage/CapacityBar";
|
||||
import { StorageRules } from "@/components/frontend/storage/StorageRules";
|
||||
|
|
@ -45,11 +45,13 @@ export default async function StructureDetailPage({ params }: StructureDetailPag
|
|||
const headers = await nextHeaders();
|
||||
const { user } = await payload.auth({ headers, canSetHeaders: false });
|
||||
|
||||
const structureRes = await payload.findByID({
|
||||
collection: "game-structures",
|
||||
id: Number(id),
|
||||
depth: 2,
|
||||
}).catch(() => null);
|
||||
const structureRes = await payload
|
||||
.findByID({
|
||||
collection: "game-structures",
|
||||
id: Number(id),
|
||||
depth: 2,
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!structureRes) {
|
||||
notFound();
|
||||
|
|
@ -116,9 +118,7 @@ export default async function StructureDetailPage({ params }: StructureDetailPag
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Item variant="outline">
|
||||
|
|
@ -187,7 +187,9 @@ export default async function StructureDetailPage({ params }: StructureDetailPag
|
|||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Max Capacity</ItemTitle>
|
||||
<ItemDescription>{structureType.maxCapacityMass.toLocaleString()} kg (mass)</ItemDescription>
|
||||
<ItemDescription>
|
||||
{structureType.maxCapacityMass.toLocaleString()} kg (mass)
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)}
|
||||
|
|
@ -204,9 +206,7 @@ export default async function StructureDetailPage({ params }: StructureDetailPag
|
|||
)}
|
||||
</div>
|
||||
|
||||
<StructureStorageView
|
||||
structure={structure}
|
||||
/>
|
||||
<StructureStorageView structure={structure} />
|
||||
|
||||
<StorageRules
|
||||
allowedStorage={structureType.allowedStorage}
|
||||
|
|
@ -229,8 +229,12 @@ export default async function StructureDetailPage({ params }: StructureDetailPag
|
|||
const resource = upkeep.resource as Resource;
|
||||
if (!resource || typeof resource === "number") return null;
|
||||
return (
|
||||
<span key={upkeep.id ?? index} className="text-sm bg-muted px-2.5 py-1 rounded-md">
|
||||
{resource.name}: {upkeep.amount?.toLocaleString()} {pluralize(upkeep.amount ?? 0, resource.unitOfMeasure)}/tick
|
||||
<span
|
||||
key={upkeep.id ?? index}
|
||||
className="text-sm bg-muted px-2.5 py-1 rounded-md"
|
||||
>
|
||||
{resource.name}: {upkeep.amount?.toLocaleString()}{" "}
|
||||
{pluralize(upkeep.amount ?? 0, resource.unitOfMeasure)}/tick
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import { GameStructure, Resource, Structure } from "@/payload-types";
|
|||
import hasRoles from "@/utils/access-control/hasRoles";
|
||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||
import {
|
||||
checkStorageDeposit,
|
||||
storageViolationMessage,
|
||||
} from "@/lib/storageRules";
|
||||
import { checkStorageDeposit, storageViolationMessage } from "@/lib/storageRules";
|
||||
|
||||
interface ActionResult {
|
||||
success: boolean;
|
||||
|
|
@ -32,11 +29,13 @@ async function authenticate() {
|
|||
}
|
||||
|
||||
async function getStructure(payload: Awaited<ReturnType<typeof getPayload>>, id: number) {
|
||||
const structure = await payload.findByID({
|
||||
collection: "game-structures",
|
||||
id,
|
||||
depth: 2,
|
||||
}).catch(() => null);
|
||||
const structure = await payload
|
||||
.findByID({
|
||||
collection: "game-structures",
|
||||
id,
|
||||
depth: 2,
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!structure) {
|
||||
throw new Error("Structure not found");
|
||||
|
|
@ -45,7 +44,10 @@ async function getStructure(payload: Awaited<ReturnType<typeof getPayload>>, id:
|
|||
return structure as unknown as GameStructure & { type: Structure };
|
||||
}
|
||||
|
||||
function getStoredAmount(storedResources: GameStructure["storedResources"], resourceId: number): number {
|
||||
function getStoredAmount(
|
||||
storedResources: GameStructure["storedResources"],
|
||||
resourceId: number,
|
||||
): number {
|
||||
if (!storedResources) return 0;
|
||||
const entry = storedResources.find((e) => {
|
||||
const res = e.resource;
|
||||
|
|
@ -73,7 +75,10 @@ export async function addResource(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. Admin or Developer role required for deposit." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. Admin or Developer role required for deposit.",
|
||||
};
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
|
|
@ -84,11 +89,13 @@ export async function addResource(
|
|||
const structureType = structure.type;
|
||||
|
||||
// Fetch the resource to check rules + mass
|
||||
const resource = await payload.findByID({
|
||||
collection: "resources",
|
||||
id: resourceId,
|
||||
depth: 0,
|
||||
}).catch(() => null);
|
||||
const resource = await payload
|
||||
.findByID({
|
||||
collection: "resources",
|
||||
id: resourceId,
|
||||
depth: 0,
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!resource) {
|
||||
return { success: false, error: "Resource not found." };
|
||||
|
|
@ -99,12 +106,7 @@ export async function addResource(
|
|||
getStoredAmount(structure.storedResources, resourceId) +
|
||||
getStoredAmount(structure.voidStorage ?? [], resourceId);
|
||||
|
||||
const ruleCheck = checkStorageDeposit(
|
||||
structureType,
|
||||
resourceId,
|
||||
amount,
|
||||
currentStored,
|
||||
);
|
||||
const ruleCheck = checkStorageDeposit(structureType, resourceId, amount, currentStored);
|
||||
|
||||
if (!ruleCheck.allowed) {
|
||||
return {
|
||||
|
|
@ -136,9 +138,7 @@ export async function addResource(
|
|||
const resH = resource.gridHeight ?? 1;
|
||||
const ew = resW;
|
||||
const eh = resH;
|
||||
const maxStack = resource.stackable === false
|
||||
? 1
|
||||
: resource.maxStackSize ?? Infinity;
|
||||
const maxStack = resource.stackable === false ? 1 : (resource.maxStackSize ?? Infinity);
|
||||
|
||||
// 1. Fill existing stacks of the same resource that have space
|
||||
let remaining = amount;
|
||||
|
|
@ -151,9 +151,8 @@ export async function addResource(
|
|||
|
||||
for (const entry of sameType) {
|
||||
if (remaining <= 0) break;
|
||||
const space = maxStack === Infinity
|
||||
? remaining
|
||||
: Math.min(maxStack - entry.amount, remaining);
|
||||
const space =
|
||||
maxStack === Infinity ? remaining : Math.min(maxStack - entry.amount, remaining);
|
||||
if (space <= 0) continue;
|
||||
const idx = existing.findIndex((e) => e.id === entry.id);
|
||||
if (idx >= 0) {
|
||||
|
|
@ -204,7 +203,13 @@ export async function addResource(
|
|||
}
|
||||
|
||||
const stackAmount = Math.min(remaining, maxStack === Infinity ? remaining : maxStack);
|
||||
existing.push({ resource: resourceId, amount: stackAmount, gridX: spot.x, gridY: spot.y, rotated: false });
|
||||
existing.push({
|
||||
resource: resourceId,
|
||||
amount: stackAmount,
|
||||
gridX: spot.x,
|
||||
gridY: spot.y,
|
||||
rotated: false,
|
||||
});
|
||||
remaining -= stackAmount;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -260,7 +265,10 @@ export async function removeResource(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. Admin or Developer role required for withdrawal." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. Admin or Developer role required for withdrawal.",
|
||||
};
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
|
|
@ -331,7 +339,12 @@ export async function removeResource(
|
|||
data: { storedResources, voidStorage },
|
||||
});
|
||||
|
||||
const resName = (await payload.findByID({ collection: "resources", id: resourceId, depth: 0 }).catch(() => null))?.name ?? `resource #${resourceId}`;
|
||||
const resName =
|
||||
(
|
||||
await payload
|
||||
.findByID({ collection: "resources", id: resourceId, depth: 0 })
|
||||
.catch(() => null)
|
||||
)?.name ?? `resource #${resourceId}`;
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.StorageWithdraw,
|
||||
message: `Withdrew ${amount.toLocaleString()}× ${resName} from "${structure.name}"`,
|
||||
|
|
@ -361,7 +374,10 @@ export async function transferResource(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. User, Admin, or Developer role required for transfer." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. User, Admin, or Developer role required for transfer.",
|
||||
};
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
|
|
@ -405,11 +421,13 @@ export async function transferResource(
|
|||
}
|
||||
|
||||
// Fetch the resource for rule + capacity checks
|
||||
const resource = await payload.findByID({
|
||||
collection: "resources",
|
||||
id: resourceId,
|
||||
depth: 0,
|
||||
}).catch(() => null);
|
||||
const resource = await payload
|
||||
.findByID({
|
||||
collection: "resources",
|
||||
id: resourceId,
|
||||
depth: 0,
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!resource) {
|
||||
return { success: false, error: "Resource not found." };
|
||||
|
|
@ -468,7 +486,15 @@ export async function transferResource(
|
|||
structure: toStructureId,
|
||||
targetCollection: "game-structures",
|
||||
targetId: toStructureId,
|
||||
data: { resourceId, amount, resourceName: resName, fromStructureId, toStructureId, fromStructureName: fromStructure.name, toStructureName: toStructure.name },
|
||||
data: {
|
||||
resourceId,
|
||||
amount,
|
||||
resourceName: resName,
|
||||
fromStructureId,
|
||||
toStructureId,
|
||||
fromStructureName: fromStructure.name,
|
||||
toStructureName: toStructure.name,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, data: addResult.data };
|
||||
|
|
@ -499,7 +525,10 @@ export async function placeResourceOnGrid(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. Admin or Developer role required for placement." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. Admin or Developer role required for placement.",
|
||||
};
|
||||
}
|
||||
|
||||
if (amount <= 0) return { success: false, error: "Amount must be greater than zero." };
|
||||
|
|
@ -507,7 +536,9 @@ export async function placeResourceOnGrid(
|
|||
const structure = await getStructure(payload, structureId);
|
||||
const structureType = structure.type;
|
||||
|
||||
const resource = await payload.findByID({ collection: "resources", id: resourceId, depth: 0 }).catch(() => null);
|
||||
const resource = await payload
|
||||
.findByID({ collection: "resources", id: resourceId, depth: 0 })
|
||||
.catch(() => null);
|
||||
if (!resource) return { success: false, error: "Resource not found." };
|
||||
|
||||
// Check storage rules (prohibited → whitelist → per-item cap)
|
||||
|
|
@ -515,12 +546,7 @@ export async function placeResourceOnGrid(
|
|||
getStoredAmount(structure.storedResources, resourceId) +
|
||||
getStoredAmount(structure.voidStorage ?? [], resourceId);
|
||||
|
||||
const ruleCheck = checkStorageDeposit(
|
||||
structureType,
|
||||
resourceId,
|
||||
amount,
|
||||
currentStored,
|
||||
);
|
||||
const ruleCheck = checkStorageDeposit(structureType, resourceId, amount, currentStored);
|
||||
|
||||
if (!ruleCheck.allowed) {
|
||||
return { success: false, error: storageViolationMessage(resource.name, ruleCheck) };
|
||||
|
|
@ -534,7 +560,10 @@ export async function placeResourceOnGrid(
|
|||
const addedMass = (resource.massPerUnit ?? 0) * amount;
|
||||
const maxMass = structureType.maxCapacityMass;
|
||||
if (maxMass && currentMass + addedMass > maxMass) {
|
||||
return { success: false, error: `Exceeds mass capacity. Available: ${(maxMass - currentMass).toLocaleString()} kg.` };
|
||||
return {
|
||||
success: false,
|
||||
error: `Exceeds mass capacity. Available: ${(maxMass - currentMass).toLocaleString()} kg.`,
|
||||
};
|
||||
}
|
||||
|
||||
const gridW = structureType.xSize ?? 0;
|
||||
|
|
@ -543,8 +572,8 @@ export async function placeResourceOnGrid(
|
|||
return { success: false, error: "This structure does not have grid dimensions configured." };
|
||||
}
|
||||
|
||||
const resW = (resource.gridWidth ?? 1);
|
||||
const resH = (resource.gridHeight ?? 1);
|
||||
const resW = resource.gridWidth ?? 1;
|
||||
const resH = resource.gridHeight ?? 1;
|
||||
const effectiveW = rotated ? resH : resW;
|
||||
const effectiveH = rotated ? resW : resH;
|
||||
|
||||
|
|
@ -578,7 +607,11 @@ export async function placeResourceOnGrid(
|
|||
|
||||
existing.push({ resource: resourceId, amount, gridX, gridY, rotated });
|
||||
|
||||
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
|
||||
await payload.update({
|
||||
collection: "game-structures",
|
||||
id: structureId,
|
||||
data: { storedResources: existing },
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.GridPlace,
|
||||
|
|
@ -609,7 +642,10 @@ export async function moveGridItem(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. User, Admin, or Developer role required." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||
};
|
||||
}
|
||||
const structure = await getStructure(payload, structureId);
|
||||
const existing = [...(structure.storedResources ?? [])];
|
||||
|
|
@ -653,10 +689,13 @@ export async function moveGridItem(
|
|||
}
|
||||
}
|
||||
|
||||
existing[idx] = rotated !== undefined
|
||||
? { ...entry, gridX, gridY, rotated }
|
||||
: { ...entry, gridX, gridY };
|
||||
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
|
||||
existing[idx] =
|
||||
rotated !== undefined ? { ...entry, gridX, gridY, rotated } : { ...entry, gridX, gridY };
|
||||
await payload.update({
|
||||
collection: "game-structures",
|
||||
id: structureId,
|
||||
data: { storedResources: existing },
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.GridMove,
|
||||
|
|
@ -665,7 +704,14 @@ export async function moveGridItem(
|
|||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
targetId: structureId,
|
||||
data: { entryId, resourceId: (typeof res === "object" ? res.id : res), resourceName: res.name, gridX, gridY, rotated },
|
||||
data: {
|
||||
entryId,
|
||||
resourceId: typeof res === "object" ? res.id : res,
|
||||
resourceName: res.name,
|
||||
gridX,
|
||||
gridY,
|
||||
rotated,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -685,7 +731,10 @@ export async function mergeGridStacks(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. User, Admin, or Developer role required." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||
};
|
||||
}
|
||||
const structure = await getStructure(payload, structureId);
|
||||
const existing = [...(structure.storedResources ?? [])];
|
||||
|
|
@ -705,9 +754,11 @@ export async function mergeGridStacks(
|
|||
|
||||
const srcResId = typeof srcRes === "object" ? srcRes.id : srcRes;
|
||||
const tgtResId = typeof tgtRes === "object" ? tgtRes.id : tgtRes;
|
||||
if (srcResId !== tgtResId) return { success: false, error: "Cannot merge different resource types." };
|
||||
if (srcResId !== tgtResId)
|
||||
return { success: false, error: "Cannot merge different resource types." };
|
||||
|
||||
if (tgtRes.stackable === false) return { success: false, error: "This resource cannot be stacked." };
|
||||
if (tgtRes.stackable === false)
|
||||
return { success: false, error: "This resource cannot be stacked." };
|
||||
|
||||
const maxStack = tgtRes.maxStackSize;
|
||||
const spaceInTarget = maxStack ? maxStack - tgt.amount : Infinity;
|
||||
|
|
@ -723,7 +774,11 @@ export async function mergeGridStacks(
|
|||
existing[srcIdx] = { ...src, amount: src.amount - transferAmount };
|
||||
}
|
||||
|
||||
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
|
||||
await payload.update({
|
||||
collection: "game-structures",
|
||||
id: structureId,
|
||||
data: { storedResources: existing },
|
||||
});
|
||||
|
||||
const mergeResName = typeof srcRes === "object" ? srcRes.name : `resource #${srcResId}`;
|
||||
await emitGameEvent(payload, {
|
||||
|
|
@ -733,7 +788,13 @@ export async function mergeGridStacks(
|
|||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
targetId: structureId,
|
||||
data: { sourceId, targetId, resourceId: srcResId, resourceName: mergeResName, transferAmount },
|
||||
data: {
|
||||
sourceId,
|
||||
targetId,
|
||||
resourceId: srcResId,
|
||||
resourceName: mergeResName,
|
||||
transferAmount,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -752,7 +813,10 @@ export async function rotateGridItem(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. User, Admin, or Developer role required." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||
};
|
||||
}
|
||||
const structure = await getStructure(payload, structureId);
|
||||
const existing = [...(structure.storedResources ?? [])];
|
||||
|
|
@ -762,7 +826,8 @@ export async function rotateGridItem(
|
|||
const entry = existing[idx];
|
||||
const res = entry.resource as Resource;
|
||||
if (!res || typeof res === "number") return { success: false, error: "Invalid resource." };
|
||||
if (res.rotatable === false) return { success: false, error: "This resource cannot be rotated." };
|
||||
if (res.rotatable === false)
|
||||
return { success: false, error: "This resource cannot be rotated." };
|
||||
|
||||
const newRotated = !(entry.rotated ?? false);
|
||||
const ew = newRotated ? (res.gridHeight ?? 1) : (res.gridWidth ?? 1);
|
||||
|
|
@ -798,7 +863,11 @@ export async function rotateGridItem(
|
|||
}
|
||||
|
||||
existing[idx] = { ...entry, rotated: newRotated };
|
||||
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
|
||||
await payload.update({
|
||||
collection: "game-structures",
|
||||
id: structureId,
|
||||
data: { storedResources: existing },
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.GridRotate,
|
||||
|
|
@ -807,7 +876,12 @@ export async function rotateGridItem(
|
|||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
targetId: structureId,
|
||||
data: { entryId, resourceId: (typeof res === "object" ? res.id : res), resourceName: res.name, rotated: newRotated },
|
||||
data: {
|
||||
entryId,
|
||||
resourceId: typeof res === "object" ? res.id : res,
|
||||
resourceName: res.name,
|
||||
rotated: newRotated,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -826,7 +900,10 @@ export async function removeGridItem(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. Admin or Developer role required for removal." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. Admin or Developer role required for removal.",
|
||||
};
|
||||
}
|
||||
const structure = await getStructure(payload, structureId);
|
||||
const existing = [...(structure.storedResources ?? [])];
|
||||
|
|
@ -836,9 +913,15 @@ export async function removeGridItem(
|
|||
const removedEntry = existing[idx];
|
||||
const removedRes = removedEntry.resource as Resource;
|
||||
existing.splice(idx, 1);
|
||||
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
|
||||
await payload.update({
|
||||
collection: "game-structures",
|
||||
id: structureId,
|
||||
data: { storedResources: existing },
|
||||
});
|
||||
|
||||
const removedName = removedRes?.name ?? `resource #${typeof removedRes === "object" ? removedRes.id : removedRes}`;
|
||||
const removedName =
|
||||
removedRes?.name ??
|
||||
`resource #${typeof removedRes === "object" ? removedRes.id : removedRes}`;
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.StorageWithdraw,
|
||||
message: `Removed ${removedEntry.amount}× ${removedName} from "${structure.name}"`,
|
||||
|
|
@ -846,7 +929,12 @@ export async function removeGridItem(
|
|||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
targetId: structureId,
|
||||
data: { entryId, resourceId: typeof removedRes === "object" ? removedRes.id : removedRes, resourceName: removedName, amount: removedEntry.amount },
|
||||
data: {
|
||||
entryId,
|
||||
resourceId: typeof removedRes === "object" ? removedRes.id : removedRes,
|
||||
resourceName: removedName,
|
||||
amount: removedEntry.amount,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -866,7 +954,10 @@ export async function splitGridStack(
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. User, Admin, or Developer role required." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||
};
|
||||
}
|
||||
|
||||
if (splitAmount <= 0) return { success: false, error: "Split amount must be positive." };
|
||||
|
|
@ -877,7 +968,8 @@ export async function splitGridStack(
|
|||
if (idx < 0) return { success: false, error: "Item not found in grid." };
|
||||
|
||||
const entry = existing[idx];
|
||||
if (splitAmount >= entry.amount) return { success: false, error: "Split amount must be less than the stack size." };
|
||||
if (splitAmount >= entry.amount)
|
||||
return { success: false, error: "Split amount must be less than the stack size." };
|
||||
|
||||
const res = entry.resource as Resource;
|
||||
if (!res || typeof res === "number") return { success: false, error: "Invalid resource." };
|
||||
|
|
@ -934,7 +1026,11 @@ export async function splitGridStack(
|
|||
|
||||
if (!placed) return { success: false, error: "No empty spot available for the split stack." };
|
||||
|
||||
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
|
||||
await payload.update({
|
||||
collection: "game-structures",
|
||||
id: structureId,
|
||||
data: { storedResources: existing },
|
||||
});
|
||||
|
||||
const splitResName = typeof res === "object" ? res.name : `resource #${res}`;
|
||||
await emitGameEvent(payload, {
|
||||
|
|
@ -944,7 +1040,13 @@ export async function splitGridStack(
|
|||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
targetId: structureId,
|
||||
data: { entryId, resourceId: typeof res === "object" ? res.id : res, resourceName: splitResName, splitAmount, remainingAmount: entry.amount - splitAmount },
|
||||
data: {
|
||||
entryId,
|
||||
resourceId: typeof res === "object" ? res.id : res,
|
||||
resourceName: splitResName,
|
||||
splitAmount,
|
||||
remainingAmount: entry.amount - splitAmount,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -1029,9 +1131,10 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
|
|||
const recovered = voidStorage.length - remainingVoid.length;
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.StorageRecover,
|
||||
message: recovered > 0
|
||||
? `Retrieved ${recovered} item(s) from void in "${structure.name}"`
|
||||
: `No void items could be recovered in "${structure.name}" — grid full`,
|
||||
message:
|
||||
recovered > 0
|
||||
? `Retrieved ${recovered} item(s) from void in "${structure.name}"`
|
||||
: `No void items could be recovered in "${structure.name}" — grid full`,
|
||||
actor: user.id,
|
||||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
|
|
@ -1040,9 +1143,10 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
|
|||
});
|
||||
return {
|
||||
success: true,
|
||||
error: recovered > 0
|
||||
? `${recovered} item(s) retrieved from void storage.`
|
||||
: "No items could be retrieved — grid is full.",
|
||||
error:
|
||||
recovered > 0
|
||||
? `${recovered} item(s) retrieved from void storage.`
|
||||
: "No items could be retrieved — grid is full.",
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
|
|
@ -1056,7 +1160,10 @@ export async function autoArrangeGrid(structureId: number): Promise<GridActionRe
|
|||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
||||
return { success: false, error: "Insufficient permissions. User, Admin, or Developer role required." };
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||
};
|
||||
}
|
||||
const structure = await getStructure(payload, structureId);
|
||||
const gridW = structure.type.xSize ?? 0;
|
||||
|
|
|
|||
|
|
@ -1,34 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { Vehicle } from "@/payload-types";
|
||||
import { VehicleCard } from "@/components/frontend/logistics/VehicleCard";
|
||||
import { VehicleFilters } from "@/components/frontend/logistics/VehicleFilters";
|
||||
|
||||
const categories = [
|
||||
"civilian",
|
||||
"utility",
|
||||
"transport",
|
||||
"weaponized",
|
||||
"military",
|
||||
];
|
||||
const categories = ["civilian", "utility", "transport", "weaponized", "military"];
|
||||
|
||||
export function VehiclesList({ vehicles }: { vehicles: Vehicle[] }) {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!selectedCategory) return vehicles;
|
||||
return vehicles.filter(
|
||||
(v) => v.category?.includes(selectedCategory as NonNullable<Vehicle["category"]>[number]),
|
||||
return vehicles.filter((v) =>
|
||||
v.category?.includes(selectedCategory as NonNullable<Vehicle["category"]>[number]),
|
||||
);
|
||||
}, [vehicles, selectedCategory]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VehicleFilters
|
||||
categories={categories}
|
||||
onFilterChange={setSelectedCategory}
|
||||
/>
|
||||
<VehicleFilters categories={categories} onFilterChange={setSelectedCategory} />
|
||||
{filtered.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{filtered.map((vehicle) => (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { Vehicle, Resource, Technology, Structure } from "@/payload-types";
|
||||
import type { Resource, Structure, Technology } from "@/payload-types";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
|
||||
import {
|
||||
|
|
@ -10,7 +10,6 @@ import {
|
|||
BoxIcon,
|
||||
Building2Icon,
|
||||
ClockIcon,
|
||||
CrosshairIcon,
|
||||
GaugeIcon,
|
||||
HeartIcon,
|
||||
ShieldIcon,
|
||||
|
|
@ -46,9 +45,7 @@ const armorColors: Record<string, string> = {
|
|||
composite: "bg-purple-500/20 text-purple-400",
|
||||
};
|
||||
|
||||
export default async function VehicleDetailPage({
|
||||
params,
|
||||
}: VehicleDetailPageProps) {
|
||||
export default async function VehicleDetailPage({ params }: VehicleDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
|
|
@ -144,10 +141,7 @@ export default async function VehicleDetailPage({
|
|||
<ItemContent>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{materials.map((mat, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="text-sm bg-muted px-2.5 py-1 rounded-md"
|
||||
>
|
||||
<span key={index} className="text-sm bg-muted px-2.5 py-1 rounded-md">
|
||||
{mat.resource.name}: {mat.amount.toLocaleString()}
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -203,9 +197,7 @@ export default async function VehicleDetailPage({
|
|||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Armor Rating</ItemTitle>
|
||||
<ItemDescription className="capitalize">
|
||||
{vehicle.armorRating}
|
||||
</ItemDescription>
|
||||
<ItemDescription className="capitalize">{vehicle.armorRating}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)}
|
||||
|
|
@ -319,15 +311,10 @@ export default async function VehicleDetailPage({
|
|||
<ItemContent>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{scrapReturns.map((scrap, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="text-sm bg-muted px-2.5 py-1 rounded-md"
|
||||
>
|
||||
<span key={index} className="text-sm bg-muted px-2.5 py-1 rounded-md">
|
||||
{scrap.resource.name}: {scrap.amount.toLocaleString()}
|
||||
{scrap.chance < 100 && (
|
||||
<span className="text-muted-foreground ml-1">
|
||||
({scrap.chance}%)
|
||||
</span>
|
||||
<span className="text-muted-foreground ml-1">({scrap.chance}%)</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -348,10 +335,7 @@ export default async function VehicleDetailPage({
|
|||
<ItemContent>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{upkeepCosts.map((upkeep, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="text-sm bg-muted px-2.5 py-1 rounded-md"
|
||||
>
|
||||
<span key={index} className="text-sm bg-muted px-2.5 py-1 rounded-md">
|
||||
{upkeep.resource.name}: {upkeep.amount.toLocaleString()}/tick
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -369,10 +353,7 @@ export default async function VehicleDetailPage({
|
|||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{producingStructures.map((structure) => (
|
||||
<Link
|
||||
key={structure.id}
|
||||
href={`/logistics/structures/${structure.id}`}
|
||||
>
|
||||
<Link key={structure.id} href={`/logistics/structures/${structure.id}`}>
|
||||
<Item
|
||||
variant="outline"
|
||||
className="cursor-pointer group transition-all duration-200 hover:scale-[1.01]"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import type { User, Rank, Media, Assignment, Campaign, Map, GameStructure } from "@/payload-types";
|
||||
import type { Assignment, Campaign, GameStructure, Map, Media, Rank, User } from "@/payload-types";
|
||||
import { resolveLevel } from "@/utils/xp/resolveLevel";
|
||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
import { ProfileSummary } from "@/components/frontend/dashboard/ProfileSummary";
|
||||
|
|
|
|||
|
|
@ -2,27 +2,27 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { User, Rank, Award, Media, Assignment, Qualification } from "@/payload-types";
|
||||
import type { Assignment, Award, Media, Qualification, Rank, User } from "@/payload-types";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
|
||||
import { resolveLevel } from "@/utils/xp/resolveLevel";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ShieldIcon,
|
||||
SwordsIcon,
|
||||
UserIcon,
|
||||
FileTextIcon,
|
||||
CrosshairIcon,
|
||||
AwardIcon,
|
||||
SparkleIcon,
|
||||
CalendarDaysIcon,
|
||||
HashIcon,
|
||||
SkullIcon,
|
||||
TruckIcon,
|
||||
PlaneIcon,
|
||||
TargetIcon,
|
||||
BadgeCheckIcon,
|
||||
CalendarDaysIcon,
|
||||
CrosshairIcon,
|
||||
FileTextIcon,
|
||||
HashIcon,
|
||||
PlaneIcon,
|
||||
ShieldCheck,
|
||||
ShieldIcon,
|
||||
SkullIcon,
|
||||
SparkleIcon,
|
||||
SwordsIcon,
|
||||
TargetIcon,
|
||||
TruckIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Item,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import type { User, Rank, Media } from "@/payload-types";
|
||||
import type { Media, Rank, User } from "@/payload-types";
|
||||
import { UsersIcon } from "lucide-react";
|
||||
import { RosterView } from "@/components/frontend/roster/RosterView";
|
||||
|
||||
|
|
@ -64,9 +64,7 @@ export default async function RosterPage() {
|
|||
const user = profile.user as User;
|
||||
const rank = profile.rank as Rank;
|
||||
const insigniaUrl =
|
||||
rank.insignia && typeof rank.insignia === "object"
|
||||
? (rank.insignia as Media).url
|
||||
: null;
|
||||
rank.insignia && typeof rank.insignia === "object" ? (rank.insignia as Media).url : null;
|
||||
|
||||
let assignmentName: string | undefined;
|
||||
let assignmentType: string | undefined;
|
||||
|
|
@ -125,15 +123,12 @@ export default async function RosterPage() {
|
|||
<h1 className="text-lg font-semibold">Unit Roster</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{members.length} operator{members.length !== 1 ? "s" : ""} across{" "}
|
||||
{assignments.length} assignment{assignments.length !== 1 ? "s" : ""}
|
||||
{members.length} operator{members.length !== 1 ? "s" : ""} across {assignments.length}{" "}
|
||||
assignment{assignments.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RosterView
|
||||
members={members}
|
||||
assignments={assignments}
|
||||
/>
|
||||
<RosterView members={members} assignments={assignments} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
import config from "@payload-config";
|
||||
import "@payloadcms/next/css";
|
||||
import { GRAPHQL_PLAYGROUND_GET } from "@payloadcms/next/routes";
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
import config from "@payload-config";
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from "@payloadcms/next/routes";
|
||||
|
||||
export const POST = GRAPHQL_POST(config);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
type SSEEventClient,
|
||||
} from "@/lib/realtime/bus";
|
||||
import { type SSEEventClient, subscribe, unsubscribe } from "@/lib/realtime/bus";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ import type { VersionInfo } from "@/lib/versionInfo";
|
|||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const VERSION_INFO_PATH = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"generated",
|
||||
"versionInfo.json",
|
||||
);
|
||||
const VERSION_INFO_PATH = path.join(process.cwd(), "src", "generated", "versionInfo.json");
|
||||
|
||||
const EMPTY: VersionInfo = {
|
||||
version: null,
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ export default async function LoginPage() {
|
|||
<AppLogo />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Log in to access the operations dashboard, intelligence, and
|
||||
logistics.
|
||||
Log in to access the operations dashboard, intelligence, and logistics.
|
||||
</p>
|
||||
</div>
|
||||
<LoginForm />
|
||||
|
|
|
|||
Loading…
Reference in a new issue