import { useEffect, useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { createEmojiPack, createGift, createResource, createResourceGroup, disableGift, disableResource, disableResourceGroup, enableGift, enableResource, enableResourceGroup, grantResource, grantResourceGroup, listEmojiPackCategories, listEmojiPacks, listGifts, listResourceGrants, listResourceGroups, listResources, updateGift, updateResource, updateResourceGroup, } from "@/features/resources/api"; import { useResourceAbilities } from "@/features/resources/permissions.js"; import { emojiPackFormSchema, giftFormSchema, resourceFormSchema, resourceGrantFormSchema, resourceGroupCreateFormSchema, } from "@/features/resources/schema.js"; const pageSize = 10; const dayMillis = 24 * 60 * 60 * 1000; const emptyData = { items: [], page: 1, pageSize, total: 0 }; const emptyResourceForm = (resource = {}) => ({ animationUrl: resource.animationUrl || resource.assetUrl || "", enabled: resource.status ? resource.status === "active" : true, name: resource.name || "", previewUrl: resource.previewUrl || "", resourceCode: resource.resourceCode || "", resourceType: resource.resourceType || "avatar_frame", walletAssetAmount: resource.walletAssetAmount ? String(resource.walletAssetAmount) : "", }); const emptyEmojiPackForm = () => ({ animationUrl: "", category: "默认", coverUrl: "", name: "", pricingType: "free", regionIds: ["0"], sortOrder: "0", }); const emptyResourceGroupForm = (group = {}) => ({ description: group.description || "", enabled: group.status ? group.status === "active" : true, groupCode: group.groupCode || "", items: group.items?.length ? group.items.map(groupItemToForm) : [emptyGroupResourceItem()], name: group.name || "", sortOrder: group.sortOrder === 0 || group.sortOrder ? String(group.sortOrder) : "0", }); const emptyGroupResourceItem = () => ({ durationDays: "1", itemType: "resource", resourceId: "", sortOrder: "0", walletAssetAmount: "", walletAssetType: "", }); const emptyGroupWalletAssetItem = (walletAssetType) => ({ durationDays: "", itemType: "wallet_asset", resourceId: "", sortOrder: "0", walletAssetAmount: "", walletAssetType, }); const emptyGiftForm = (gift = {}) => ({ chargeAssetType: gift.chargeAssetType || "COIN", coinPrice: gift.coinPrice === 0 || gift.coinPrice ? String(gift.coinPrice) : "", effectTypes: gift.effectTypes || [], effectiveFrom: msToDatetimeLocal(gift.effectiveFromMs), effectiveTo: msToDatetimeLocal(gift.effectiveToMs), enabled: gift.status ? gift.status === "active" : true, giftId: gift.giftId || "", giftPointAmount: gift.giftPointAmount === 0 || gift.giftPointAmount ? String(gift.giftPointAmount) : "0", giftTypeCode: gift.giftTypeCode || "normal", heatValue: gift.heatValue === 0 || gift.heatValue ? String(gift.heatValue) : "0", name: gift.name || "", presentationJson: gift.presentationJson || "{}", priceVersion: gift.priceVersion || "default", regionIds: (gift.regionIds?.length ? gift.regionIds : [0]).map(String), resourceId: gift.resourceId ? String(gift.resourceId) : "", sortOrder: gift.sortOrder === 0 || gift.sortOrder ? String(gift.sortOrder) : "0", }); export function applyGiftResourceSelection(form, resource) { const resourceId = resource?.resourceId ? String(resource.resourceId) : ""; return { ...form, giftId: resourceId, name: resource?.name || resource?.resourceCode || resourceId, resourceId, }; } export function applyGiftPriceDefaults(form, coinPrice) { const priceValue = String(coinPrice ?? ""); const priceNumber = Number(priceValue); return { ...form, coinPrice: priceValue, giftPointAmount: priceValue, heatValue: priceValue.trim() && Number.isFinite(priceNumber) ? String(Math.ceil(priceNumber / 100)) : "", }; } const emptyGrantForm = () => ({ durationDays: "0", groupId: "", quantity: "1", reason: "", resourceId: "", subjectType: "resource", targetUserId: "", }); function groupItemToForm(item) { const sortOrder = item.sortOrder === 0 || item.sortOrder ? String(item.sortOrder) : "0"; if (item.itemType === "wallet_asset") { return { durationDays: "", itemType: "wallet_asset", resourceId: "", sortOrder, walletAssetAmount: item.walletAssetAmount === 0 || item.walletAssetAmount ? String(item.walletAssetAmount) : "", walletAssetType: item.walletAssetType || "", }; } return { durationDays: item.durationMs ? formatDurationDays(item.durationMs) : "1", itemType: "resource", resourceId: item.resourceId ? String(item.resourceId) : "", sortOrder, walletAssetAmount: "", walletAssetType: "", }; } export function useResourceListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [resourceType, setResourceType] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyResourceForm); const [selectedResource, setSelectedResource] = useState(null); const [loadingAction, setLoadingAction] = useState(""); const filters = useMemo( () => ({ keyword: query, resource_type: resourceType, status, }), [query, resourceType, status], ); const result = usePaginatedQuery({ errorMessage: "加载资源列表失败", fetcher: listResources, filters, page, pageSize, queryKey: ["resources", filters, page], }); const openCreateResource = () => { setForm(emptyResourceForm()); setSelectedResource(null); setActiveAction("create"); }; const openEditResource = (resource) => { if (!resource?.resourceId) { return; } setForm(emptyResourceForm(resource)); setSelectedResource(resource); setActiveAction("edit"); }; const closeAction = () => { setActiveAction(""); setSelectedResource(null); }; const submitResource = async (event) => { event.preventDefault(); const editing = activeAction === "edit"; await runAction( editing ? "resource-edit" : "resource-create", editing ? "资源已更新" : "资源已创建", async () => { const payload = buildResourcePayload(parseForm(resourceFormSchema, form)); if (editing) { await updateResource(selectedResource.resourceId, payload); } else { await createResource(payload); } setForm(emptyResourceForm()); closeAction(); await result.reload(); }, ); }; const toggleResource = async (resource, nextEnabled = resource.status !== "active") => { if (!abilities.canUpdate || !resource?.resourceId) { return; } const nextStatus = nextEnabled ? "active" : "disabled"; if ((resource.status === "active") === nextEnabled) { return; } await runAction( `resource-status-${resource.resourceId}`, nextEnabled ? "资源已启用" : "资源已禁用", async () => { if (nextStatus === "active") { await enableResource(resource.resourceId); } else { await disableResource(resource.resourceId); } await result.reload(); }, ); }; const runAction = async (action, successMessage, submitter) => { setLoadingAction(action); try { await submitter(); showToast(successMessage, "success"); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setResourceType(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeResourceType: resetSetter(setResourceType, setPage), changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, openCreateResource, openEditResource, resourceType, selectedResource, resetFilters, setForm, status, submitResource, toggleResource, }; } export function useResourceGroupListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyResourceGroupForm); const [selectedGroup, setSelectedGroup] = useState(null); const [loadingAction, setLoadingAction] = useState(""); const [resourceOptions, setResourceOptions] = useState([]); const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false); const filters = useMemo( () => ({ keyword: query, status, }), [query, status], ); const result = usePaginatedQuery({ errorMessage: "加载资源组列表失败", fetcher: listResourceGroups, filters, page, pageSize, queryKey: ["resource-groups", filters, page], }); useEffect(() => { if (activeAction !== "create" && activeAction !== "edit") { return undefined; } let ignore = false; setResourceOptionsLoading(true); listResources({ page: 1, page_size: 100, status: "active" }) .then((data) => { if (!ignore) { setResourceOptions(toGroupResourceOptions(data.items || [], selectedGroup)); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载资源选项失败", "error"); } }) .finally(() => { if (!ignore) { setResourceOptionsLoading(false); } }); return () => { ignore = true; }; }, [activeAction, selectedGroup, showToast]); const openCreateGroup = () => { setForm(emptyResourceGroupForm()); setSelectedGroup(null); setActiveAction("create"); }; const openEditGroup = (group) => { if (!group?.groupId) { return; } setForm(emptyResourceGroupForm(group)); setSelectedGroup(group); setActiveAction("edit"); }; const closeAction = () => { setActiveAction(""); setSelectedGroup(null); }; const addResourceItem = () => { setForm((current) => ({ ...current, items: [...current.items, emptyGroupResourceItem()] })); }; const addWalletAssetItem = (walletAssetType) => { setForm((current) => ({ ...current, items: [...current.items, emptyGroupWalletAssetItem(walletAssetType)] })); }; const updateGroupItem = (index, patch) => { setForm((current) => ({ ...current, items: current.items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)), })); }; const removeGroupItem = (index) => { setForm((current) => ({ ...current, items: current.items.filter((_, itemIndex) => itemIndex !== index), })); }; const submitGroup = async (event) => { event.preventDefault(); const editing = activeAction === "edit"; const action = editing ? "group-edit" : "group-create"; const successMessage = editing ? "资源组已更新" : "资源组已创建"; setLoadingAction(action); try { const payload = buildResourceGroupPayload(parseForm(resourceGroupCreateFormSchema, form)); if (editing) { await updateResourceGroup(selectedGroup.groupId, payload); } else { await createResourceGroup(payload); } showToast(successMessage, "success"); setForm(emptyResourceGroupForm()); closeAction(); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const toggleGroup = async (group, nextEnabled = group.status !== "active") => { if (!abilities.canUpdateGroup || !group?.groupId) { return; } if ((group.status === "active") === nextEnabled) { return; } setLoadingAction(`group-status-${group.groupId}`); try { if (nextEnabled) { await enableResourceGroup(group.groupId); } else { await disableResourceGroup(group.groupId); } showToast(nextEnabled ? "资源组已启用" : "资源组已禁用", "success"); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, addResourceItem, addWalletAssetItem, changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, openCreateGroup, openEditGroup, removeGroupItem, resourceOptions, resourceOptionsLoading, selectedGroup, resetFilters, setForm, status, submitGroup, toggleGroup, updateGroupItem, }; } export function useGiftListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [regionId, setRegionId] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyGiftForm); const [selectedGift, setSelectedGift] = useState(null); const [loadingAction, setLoadingAction] = useState(""); const [resourceOptions, setResourceOptions] = useState([]); const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false); const { loadingRegions, regionOptions } = useRegionOptions(); const filters = useMemo( () => ({ keyword: query, region_id: regionId, status, }), [query, regionId, status], ); const result = usePaginatedQuery({ errorMessage: "加载礼物列表失败", fetcher: listGifts, filters, page, pageSize, queryKey: ["gifts", filters, page], }); useEffect(() => { if (activeAction !== "create" && activeAction !== "edit") { return undefined; } let ignore = false; setResourceOptionsLoading(true); listResources({ page: 1, page_size: 100, resource_type: "gift" }) .then((data) => { if (!ignore) { setResourceOptions(toGiftResourceOptions(data.items || [], selectedGift)); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载礼物资源失败", "error"); } }) .finally(() => { if (!ignore) { setResourceOptionsLoading(false); } }); return () => { ignore = true; }; }, [activeAction, selectedGift, showToast]); const openCreateGift = () => { setForm(emptyGiftForm()); setSelectedGift(null); setActiveAction("create"); }; const openEditGift = (gift) => { if (!gift?.giftId) { return; } setForm(emptyGiftForm(gift)); setSelectedGift(gift); setActiveAction("edit"); }; const closeAction = () => { setActiveAction(""); setSelectedGift(null); }; const selectGiftResource = (resourceId) => { const selectedResource = resourceOptions.find((resource) => String(resource.resourceId) === String(resourceId)); setForm((currentForm) => { if (activeAction !== "create") { return { ...currentForm, resourceId }; } return applyGiftResourceSelection(currentForm, selectedResource || { resourceId }); }); }; const changeGiftPrice = (coinPrice) => { setForm((currentForm) => applyGiftPriceDefaults(currentForm, coinPrice)); }; const submitGift = async (event) => { event.preventDefault(); const editing = activeAction === "edit"; if (editing && !selectedGift?.giftId) { return; } setLoadingAction(editing ? "gift-edit" : "gift-create"); try { const payload = buildGiftPayload(parseForm(giftFormSchema, form)); if (editing) { await updateGift(selectedGift.giftId, payload); } else { await createGift(payload); } showToast(editing ? "礼物已更新" : "礼物已创建", "success"); closeAction(); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const toggleGift = async (gift, nextEnabled = gift.status !== "active") => { if (!abilities.canStatusGift || !gift?.giftId) { return; } if ((gift.status === "active") === nextEnabled) { return; } setLoadingAction(`gift-status-${gift.giftId}`); try { if (nextEnabled) { await enableGift(gift.giftId); } else { await disableGift(gift.giftId); } showToast(nextEnabled ? "礼物已启用" : "礼物已禁用", "success"); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setRegionId(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeGiftPrice, changeRegionId: resetSetter(setRegionId, setPage), changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, loadingRegions, openCreateGift, openEditGift, regionId, regionOptions, resourceOptions, resourceOptionsLoading, selectedGift, resetFilters, selectGiftResource, setForm, status, submitGift, toggleGift, }; } export function useEmojiPackListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyEmojiPackForm); const [loadingAction, setLoadingAction] = useState(""); const [categoryOptions, setCategoryOptions] = useState([]); const [categoryOptionsLoading, setCategoryOptionsLoading] = useState(false); const { loadingRegions, regionOptions } = useRegionOptions(); const filters = useMemo( () => ({ keyword: query, status, }), [query, status], ); const result = usePaginatedQuery({ errorMessage: "加载表情包列表失败", fetcher: listEmojiPacks, filters, page, pageSize, queryKey: ["emoji-packs", filters, page], }); useEffect(() => { let ignore = false; setCategoryOptionsLoading(true); listEmojiPackCategories() .then((options) => { if (!ignore) { setCategoryOptions(mergeEmojiPackCategoryOptions(options)); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载表情包分类失败", "error"); } }) .finally(() => { if (!ignore) { setCategoryOptionsLoading(false); } }); return () => { ignore = true; }; }, [showToast]); const openCreateEmojiPack = () => { setForm(emptyEmojiPackForm()); setActiveAction("create"); }; const closeAction = () => { setActiveAction(""); }; const submitEmojiPack = async (event) => { event.preventDefault(); if (!abilities.canCreateEmojiPack) { return; } setLoadingAction("emoji-pack-create"); try { const payload = buildEmojiPackPayload(parseForm(emojiPackFormSchema, form)); await createEmojiPack(payload); setCategoryOptions((options) => mergeEmojiPackCategoryOptions(options, payload.category)); showToast("表情包已创建", "success"); closeAction(); setForm(emptyEmojiPackForm()); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const toggleEmojiPack = async (emojiPack, nextEnabled = emojiPack.status !== "active") => { if (!abilities.canUpdate || !emojiPack?.resourceId) { return; } if ((emojiPack.status === "active") === nextEnabled) { return; } setLoadingAction(`emoji-pack-status-${emojiPack.resourceId}`); try { if (nextEnabled) { await enableResource(emojiPack.resourceId); } else { await disableResource(emojiPack.resourceId); } showToast(nextEnabled ? "表情包已启用" : "表情包已禁用", "success"); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, categoryOptions, categoryOptionsLoading, changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, loadingRegions, openCreateEmojiPack, regionOptions, resetFilters, setForm, status, submitEmojiPack, toggleEmojiPack, }; } export function useResourceGrantListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyGrantForm); const [resourceOptions, setResourceOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]); const [optionsLoading, setOptionsLoading] = useState(false); const [loadingAction, setLoadingAction] = useState(""); const filters = useMemo( () => ({ status, target_user_id: query.trim(), }), [query, status], ); const result = usePaginatedQuery({ errorMessage: "加载资源赠送记录失败", fetcher: listResourceGrants, filters, page, pageSize, queryKey: ["resource-grants", filters, page], }); useEffect(() => { if (activeAction !== "create") { return undefined; } let ignore = false; setOptionsLoading(true); Promise.all([ listResources({ page: 1, page_size: 100, status: "active" }), listResourceGroups({ page: 1, page_size: 100, status: "active" }), ]) .then(([resources, groups]) => { if (!ignore) { setResourceOptions((resources.items || []).filter((resource) => resource.grantable !== false)); setGroupOptions(groups.items || []); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载资源选项失败", "error"); } }) .finally(() => { if (!ignore) { setOptionsLoading(false); } }); return () => { ignore = true; }; }, [activeAction, showToast]); const openCreateGrant = () => { setForm(emptyGrantForm()); setActiveAction("create"); }; const closeAction = () => { setActiveAction(""); }; const submitGrant = async (event) => { event.preventDefault(); if (!abilities.canCreateGrant) { return; } setLoadingAction("grant-create"); try { const payload = buildGrantPayload(parseForm(resourceGrantFormSchema, form)); if (payload.subjectType === "resource") { await grantResource(payload.resource); } else { await grantResourceGroup(payload.group); } showToast("资源已赠送", "success"); closeAction(); setForm(emptyGrantForm()); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeStatus: resetSetter(setStatus, setPage), closeAction, form, groupOptions, loadingAction, openCreateGrant, optionsLoading, resourceOptions, resetFilters, setForm, status, submitGrant, }; } function sharedPageState({ page, query, result, setPage, setQuery }) { return { data: result.data || emptyData, error: result.error, loading: result.loading, page, query, reload: result.reload, setPage, changeQuery: resetSetter(setQuery, setPage), }; } function resetSetter(setValue, setPage) { return (value) => { setValue(value); setPage(1); }; } function buildResourcePayload(form) { const resourceType = form.resourceType; const isCoin = resourceType === "coin"; const animationUrl = form.animationUrl.trim(); return { amount: isCoin ? Number(form.walletAssetAmount) : 0, animationUrl, assetUrl: animationUrl, name: form.name.trim(), previewUrl: form.previewUrl.trim(), resourceCode: form.resourceCode.trim(), resourceType, sortOrder: 0, status: form.enabled ? "active" : "disabled", }; } function buildEmojiPackPayload(form) { const regionIds = (form.regionIds || []) .map(Number) .filter((regionId) => Number.isInteger(regionId) && regionId >= 0); return { animationUrl: form.animationUrl.trim(), category: form.category.trim(), coverUrl: form.coverUrl.trim(), name: form.name.trim(), pricingType: form.pricingType, regionIds: regionIds.length ? regionIds : [0], sortOrder: Number(form.sortOrder || 0), }; } function mergeEmojiPackCategoryOptions(options = [], value = "默认") { const merged = new Set(["默认"]); options.forEach((option) => { const category = String(option || "").trim(); if (category) { merged.add(category); } }); const category = String(value || "").trim(); if (category) { merged.add(category); } return Array.from(merged).sort((left, right) => left.localeCompare(right, "zh-Hans-CN")); } function buildResourceGroupPayload(form) { return { description: (form.description || "").trim(), groupCode: form.groupCode.trim(), items: form.items.map((item, index) => { const sortOrder = Number(item.sortOrder || index); if (item.itemType === "wallet_asset") { return { amount: Number(item.walletAssetAmount), assetType: item.walletAssetType, itemType: "wallet_asset", sortOrder: Number.isInteger(sortOrder) ? sortOrder : index, }; } return { durationDays: Number(item.durationDays), itemType: "resource", resourceId: Number(item.resourceId), sortOrder: Number.isInteger(sortOrder) ? sortOrder : index, }; }), name: form.name.trim(), sortOrder: Number(form.sortOrder || 0), status: form.enabled ? "active" : "disabled", }; } function buildGiftPayload(form) { return { chargeAssetType: form.chargeAssetType, coinPrice: Number(form.coinPrice), effectiveAtMs: Date.now(), effectiveFromMs: datetimeLocalToMs(form.effectiveFrom), effectiveToMs: datetimeLocalToMs(form.effectiveTo), effectTypes: form.effectTypes || [], giftId: form.giftId.trim(), giftPointAmount: Number(form.giftPointAmount || 0), giftTypeCode: form.giftTypeCode, heatValue: Number(form.heatValue || 0), name: form.name.trim(), presentationJson: form.presentationJson?.trim() || "{}", priceVersion: form.priceVersion.trim(), regionIds: form.regionIds.map(Number), resourceId: Number(form.resourceId), sortOrder: Number(form.sortOrder || 0), status: form.enabled ? "active" : "disabled", }; } function msToDatetimeLocal(value) { if (!value) { return ""; } const date = new Date(Number(value)); if (Number.isNaN(date.getTime())) { return ""; } const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000); return local.toISOString().slice(0, 16); } function datetimeLocalToMs(value) { const trimmed = String(value || "").trim(); if (!trimmed) { return 0; } const parsed = new Date(trimmed).getTime(); return Number.isFinite(parsed) ? parsed : 0; } function buildGrantPayload(form) { const targetUserId = Number(form.targetUserId); const reason = form.reason.trim(); const commandId = makeCommandId(form.subjectType === "resource" ? "resource-grant" : "resource-group-grant"); if (form.subjectType === "resource") { return { resource: { commandId, durationMs: Math.round(Number(form.durationDays || 0) * dayMillis), quantity: Number(form.quantity || 1), reason, resourceId: Number(form.resourceId), targetUserId, }, subjectType: "resource", }; } return { group: { commandId, groupId: Number(form.groupId), reason, targetUserId, }, subjectType: "resource_group", }; } function makeCommandId(prefix) { return `${prefix}-${Date.now()}`; } function toGroupResourceOptions(items, selectedGroup) { const selectedResources = (selectedGroup?.items || []) .map((item) => item.resource) .filter((resource) => resource?.resourceId); return uniqueResources([...items, ...selectedResources]).filter( (resource) => resource.resourceType !== "coin" && resource.status === "active" && resource.grantable !== false, ); } function toGiftResourceOptions(items, selectedGift) { const selectedResource = selectedGift?.resource?.resourceId ? [selectedGift.resource] : selectedGift?.resourceId ? [{ name: String(selectedGift.resourceId), resourceId: selectedGift.resourceId, resourceType: "gift" }] : []; return uniqueResources([...items, ...selectedResource]).filter( (resource) => resource.resourceType === "gift" || resource.resourceId === selectedGift?.resourceId, ); } function uniqueResources(resources) { const seen = new Set(); return resources.filter((resource) => { if (!resource?.resourceId || seen.has(resource.resourceId)) { return false; } seen.add(resource.resourceId); return true; }); } function formatDurationDays(durationMs) { const days = Number(durationMs || 0) / dayMillis; return Number.isInteger(days) ? String(days) : String(days.toFixed(1)); }