313 lines
10 KiB
JavaScript
313 lines
10 KiB
JavaScript
import { makeRecoveredPage } from "../../runtime/makeRecoveredPage.js";
|
|
|
|
export default makeRecoveredPage({
|
|
title: "Finance Plan",
|
|
routePath: "/finance/plan",
|
|
moduleId: "p3t",
|
|
featureKey: "finance.plan",
|
|
translationNamespaces: ["subscribe", "common"],
|
|
summary:
|
|
"The plan management page is already fully visible in the compiled bundle: it loads the complete plan table, supports inline visibility/sale/renew toggles, drag-sort persistence, modal add/edit, delete confirmation, markdown content editing, and server-group binding.",
|
|
api: [
|
|
{
|
|
name: "plan.fetch",
|
|
method: "GET",
|
|
path: "{secure}/plan/fetch",
|
|
queryKey: ["planList"],
|
|
purpose: "Loads the entire plan table used by desktop and mobile layouts.",
|
|
responseShape: [
|
|
{
|
|
id: "number",
|
|
show: "boolean",
|
|
sell: "boolean",
|
|
renew: "boolean",
|
|
name: "string",
|
|
tags: ["string"],
|
|
group: {
|
|
id: "number",
|
|
name: "string",
|
|
},
|
|
users_count: "number",
|
|
active_users_count: "number",
|
|
transfer_enable: "number",
|
|
speed_limit: "number | null",
|
|
capacity_limit: "number | null",
|
|
device_limit: "number | null",
|
|
reset_traffic_method: "number | null",
|
|
content: "string",
|
|
prices: {
|
|
monthly: "number | null",
|
|
quarterly: "number | null",
|
|
half_yearly: "number | null",
|
|
yearly: "number | null",
|
|
two_yearly: "number | null",
|
|
three_yearly: "number | null",
|
|
onetime: "number | null",
|
|
reset_traffic: "number | null",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "plan.save",
|
|
method: "POST",
|
|
path: "{secure}/plan/save",
|
|
purpose: "Submits both add and edit modal payloads.",
|
|
notes: [
|
|
"The compiled dialog uses the same save endpoint for create and edit.",
|
|
"The currently edited row is distinguished by the presence of id in the form payload.",
|
|
],
|
|
},
|
|
{
|
|
name: "plan.update",
|
|
method: "POST",
|
|
path: "{secure}/plan/update",
|
|
purpose: "Inline boolean updates for show/sell/renew toggles.",
|
|
payloadExamples: [{ id: 1, show: true }, { id: 1, sell: false }, { id: 1, renew: true }],
|
|
},
|
|
{
|
|
name: "plan.drop",
|
|
method: "POST",
|
|
path: "{secure}/plan/drop",
|
|
purpose: "Deletes a plan after confirmation.",
|
|
payloadExamples: [{ id: 1 }],
|
|
},
|
|
{
|
|
name: "plan.sort",
|
|
method: "POST",
|
|
path: "{secure}/plan/sort",
|
|
purpose: "Persists drag-and-drop ordering when sort mode is saved.",
|
|
payloadExamples: [{ ids: [1, 3, 2] }],
|
|
},
|
|
{
|
|
name: "server.group.fetch",
|
|
method: "GET",
|
|
path: "{secure}/server/group/fetch",
|
|
purpose: "Populates the group selector inside the add/edit dialog.",
|
|
notes: ["The group list is refreshed every time the plan modal opens."],
|
|
},
|
|
],
|
|
stateModel: {
|
|
list: {
|
|
sorting: [],
|
|
columnVisibility: {
|
|
"drag-handle": false,
|
|
},
|
|
rowSelection: {},
|
|
columnFilters: [],
|
|
pagination: {
|
|
pageSize: 20,
|
|
pageIndex: 0,
|
|
},
|
|
draggableRows: false,
|
|
localSortedRowsMirror: "A local copy of the fetched plan list is mutated while drag-sort mode is active.",
|
|
},
|
|
modal: {
|
|
isOpen: false,
|
|
editingPlan: null,
|
|
isSubmitting: false,
|
|
previewMarkdown: false,
|
|
groupOptions: [],
|
|
},
|
|
},
|
|
tableModel: {
|
|
toolbar: {
|
|
actions: [
|
|
"Add plan button opens the plan dialog through context state.",
|
|
"Name search writes directly into the table filter for the name column.",
|
|
"Sort button toggles drag-sort mode; a second click persists order through plan.sort.",
|
|
],
|
|
},
|
|
columns: [
|
|
{
|
|
key: "drag-handle",
|
|
visibleWhen: "sort mode only",
|
|
role: "Drag handle used by the generic table component callbacks.",
|
|
},
|
|
{
|
|
key: "id",
|
|
titleKey: "plan.columns.id",
|
|
renderer: "Outline badge",
|
|
},
|
|
{
|
|
key: "show",
|
|
titleKey: "plan.columns.show",
|
|
renderer: "Boolean switch",
|
|
updateApi: "plan.update",
|
|
},
|
|
{
|
|
key: "sell",
|
|
titleKey: "plan.columns.sell",
|
|
renderer: "Boolean switch",
|
|
updateApi: "plan.update",
|
|
},
|
|
{
|
|
key: "renew",
|
|
titleKey: "plan.columns.renew",
|
|
tooltipKey: "plan.columns.renew_tooltip",
|
|
renderer: "Boolean switch",
|
|
updateApi: "plan.update",
|
|
},
|
|
{
|
|
key: "name",
|
|
titleKey: "plan.columns.name",
|
|
renderer: "Long truncated text cell",
|
|
},
|
|
{
|
|
key: "users_count",
|
|
titleKey: "plan.columns.stats",
|
|
renderer:
|
|
"Two compact stat pills: total users_count and active_users_count, with active-rate percentage when users_count > 0.",
|
|
},
|
|
{
|
|
key: "group",
|
|
titleKey: "plan.columns.group",
|
|
renderer: "Single badge using group.name",
|
|
},
|
|
{
|
|
key: "prices",
|
|
titleKey: "plan.columns.price",
|
|
renderer:
|
|
"Dynamic period badges for each non-null price; shows a dashed no-price badge when no prices exist.",
|
|
supportedPeriods: [
|
|
"monthly",
|
|
"quarterly",
|
|
"half_yearly",
|
|
"yearly",
|
|
"two_yearly",
|
|
"three_yearly",
|
|
"onetime",
|
|
"reset_traffic",
|
|
],
|
|
},
|
|
{
|
|
key: "actions",
|
|
titleKey: "plan.columns.actions",
|
|
renderer: "Edit action + delete confirmation action",
|
|
},
|
|
],
|
|
mobileLayout: {
|
|
primaryField: "name",
|
|
gridFields: ["prices", "users_count", "group"],
|
|
},
|
|
},
|
|
formModel: {
|
|
schema: {
|
|
id: "number | null",
|
|
group_id: "number | string | null",
|
|
name: "string, required, max 250",
|
|
tags: "string[] | null",
|
|
content: "string | null",
|
|
transfer_enable: "number | string, required",
|
|
prices: {
|
|
monthly: "number | string | null",
|
|
quarterly: "number | string | null",
|
|
half_yearly: "number | string | null",
|
|
yearly: "number | string | null",
|
|
two_yearly: "number | string | null",
|
|
three_yearly: "number | string | null",
|
|
onetime: "number | string | null",
|
|
reset_traffic: "number | string | null",
|
|
},
|
|
speed_limit: "number | string | null",
|
|
capacity_limit: "number | string | null",
|
|
device_limit: "number | string | null",
|
|
force_update: "boolean",
|
|
reset_traffic_method: "number | null",
|
|
users_count: "number | undefined",
|
|
active_users_count: "number | undefined",
|
|
},
|
|
defaults: {
|
|
id: null,
|
|
group_id: null,
|
|
name: "",
|
|
tags: [],
|
|
content: "",
|
|
transfer_enable: "",
|
|
prices: {
|
|
monthly: "",
|
|
quarterly: "",
|
|
half_yearly: "",
|
|
yearly: "",
|
|
two_yearly: "",
|
|
three_yearly: "",
|
|
onetime: "",
|
|
reset_traffic: "",
|
|
},
|
|
speed_limit: "",
|
|
capacity_limit: "",
|
|
device_limit: "",
|
|
force_update: false,
|
|
reset_traffic_method: null,
|
|
},
|
|
fields: [
|
|
"name",
|
|
"tags",
|
|
"group_id",
|
|
"transfer_enable",
|
|
"speed_limit",
|
|
"device_limit",
|
|
"capacity_limit",
|
|
"reset_traffic_method",
|
|
"prices.*",
|
|
"content",
|
|
],
|
|
editOnlyControls: ["force_update"],
|
|
resetTrafficMethodOptions: [
|
|
{ value: null, labelKey: "plan.form.reset_method.options.follow_system" },
|
|
{ value: 0, labelKey: "plan.form.reset_method.options.monthly_first" },
|
|
{ value: 1, labelKey: "plan.form.reset_method.options.monthly_reset" },
|
|
{ value: 2, labelKey: "plan.form.reset_method.options.no_reset" },
|
|
{ value: 3, labelKey: "plan.form.reset_method.options.yearly_first" },
|
|
{ value: 4, labelKey: "plan.form.reset_method.options.yearly_reset" },
|
|
],
|
|
markdownEditor: {
|
|
templateButton: "plan.form.content.template.button",
|
|
templateContentKey: "plan.form.content.template.content",
|
|
previewToggleKeys: [
|
|
"plan.form.content.preview_button.show",
|
|
"plan.form.content.preview_button.hide",
|
|
],
|
|
notes: [
|
|
"The editor is configured for markdown view with menu enabled and HTML view disabled.",
|
|
"Preview is rendered with markdown-it using html:true.",
|
|
],
|
|
},
|
|
priceAutoFill: {
|
|
trigger: "Base price input",
|
|
formula: "basePrice * months * discount, rounded to 2 decimals and applied to all price slots.",
|
|
matrix: {
|
|
monthly: { months: 1, discount: 1 },
|
|
quarterly: { months: 3, discount: 0.95 },
|
|
half_yearly: { months: 6, discount: 0.9 },
|
|
yearly: { months: 12, discount: 0.85 },
|
|
two_yearly: { months: 24, discount: 0.8 },
|
|
three_yearly: { months: 36, discount: 0.75 },
|
|
onetime: { months: 1, discount: 1 },
|
|
reset_traffic: { months: 1, discount: 1 },
|
|
},
|
|
},
|
|
},
|
|
interactions: [
|
|
"Entering sort mode exposes the drag handle column, disables normal pagination, and inflates pageSize so the full list can be reordered at once.",
|
|
"Saving sort mode posts the current local row id order to plan.sort and exits drag mode after success.",
|
|
"Deleting a row asks for confirmation and refetches the plan list after a success toast.",
|
|
"Opening the edit dialog preloads the selected row into react-hook-form and keeps id in the payload.",
|
|
"The group field includes an inline 'add group' dialog trigger and refreshes the available groups afterwards.",
|
|
"Validation errors are aggregated into a toast message by joining field error messages with line breaks.",
|
|
],
|
|
notes: [
|
|
"The plan page uses the subscribe translation namespace even though it belongs to finance routing.",
|
|
"The table is pinned with the actions column on the right in its initial state.",
|
|
"Desktop sorting/search/filter behavior is driven through the shared table abstraction rather than page-local DOM code.",
|
|
],
|
|
sourceRefs: [
|
|
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:27357",
|
|
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:295538",
|
|
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:295602",
|
|
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:295972",
|
|
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:296385",
|
|
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:296492",
|
|
],
|
|
});
|