actions.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import { toast } from "sonner";
  2. import { findFormValue } from "@json-render/core";
  3. import {
  4. type Actions,
  5. type ActionFn,
  6. type SetData,
  7. type DataModel,
  8. } from "@json-render/react";
  9. import { dashboardCatalog } from "../catalog";
  10. // =============================================================================
  11. // Action Handlers - Type-safe with Catalog
  12. // =============================================================================
  13. export const actionHandlers: Actions<typeof dashboardCatalog> = {
  14. viewCustomers: async (params, setData) => {
  15. const queryParams = new URLSearchParams();
  16. if (params?.limit) queryParams.set("limit", String(params.limit));
  17. if (params?.sort) queryParams.set("sort", String(params.sort));
  18. if (params?.status) queryParams.set("status", String(params.status));
  19. const url = `/api/v1/customers${queryParams.toString() ? `?${queryParams}` : ""}`;
  20. const res = await fetch(url);
  21. const customers = await res.json();
  22. setData((prev) => ({ ...prev, customers }));
  23. },
  24. refreshCustomers: async (params, setData) => {
  25. const queryParams = new URLSearchParams();
  26. if (params?.limit) queryParams.set("limit", String(params.limit));
  27. if (params?.sort) queryParams.set("sort", String(params.sort));
  28. const url = `/api/v1/customers${queryParams.toString() ? `?${queryParams}` : ""}`;
  29. const res = await fetch(url);
  30. const customers = await res.json();
  31. setData((prev) => ({ ...prev, customers }));
  32. },
  33. createCustomer: async (params, setData, data) => {
  34. const name = findFormValue("name", params, data) as string;
  35. const email =
  36. (findFormValue("email", params, data) as string) ||
  37. `${name?.toLowerCase().replace(/\s+/g, ".")}@example.com`;
  38. const phone = findFormValue("phone", params, data) as string | undefined;
  39. if (!name) {
  40. toast.error("Customer name is required");
  41. return;
  42. }
  43. try {
  44. const res = await fetch("/api/v1/customers", {
  45. method: "POST",
  46. headers: { "Content-Type": "application/json" },
  47. body: JSON.stringify({ name, email, phone }),
  48. });
  49. const customer = await res.json();
  50. if (res.ok) {
  51. toast.success(`Customer "${customer.name}" created`);
  52. const listRes = await fetch("/api/v1/customers");
  53. const customers = await listRes.json();
  54. setData((prev) => ({ ...prev, customers }));
  55. } else {
  56. toast.error(customer.error || "Failed to create customer");
  57. }
  58. } catch (err) {
  59. console.error("Failed to create customer:", err);
  60. toast.error("Failed to create customer");
  61. }
  62. },
  63. deleteCustomer: async (params, setData, data) => {
  64. const customerId =
  65. findFormValue("customerId", params, data) ||
  66. findFormValue("id", params, data);
  67. if (!customerId) {
  68. toast.error("Customer ID required");
  69. return;
  70. }
  71. try {
  72. const res = await fetch(`/api/v1/customers/${customerId}`, {
  73. method: "DELETE",
  74. });
  75. if (res.ok) {
  76. toast.success("Customer deleted");
  77. const listRes = await fetch("/api/v1/customers");
  78. const customers = await listRes.json();
  79. setData((prev) => ({ ...prev, customers }));
  80. } else {
  81. const err = await res.json();
  82. toast.error(err.error || "Failed to delete customer");
  83. }
  84. } catch (err) {
  85. console.error("Failed to delete customer:", err);
  86. toast.error("Failed to delete customer");
  87. }
  88. },
  89. viewInvoices: async (params, setData) => {
  90. const queryParams = new URLSearchParams();
  91. if (params?.status) queryParams.set("status", String(params.status));
  92. const url = `/api/v1/invoices${queryParams.toString() ? `?${queryParams}` : ""}`;
  93. const res = await fetch(url);
  94. const invoices = await res.json();
  95. setData((prev) => ({ ...prev, invoices }));
  96. },
  97. refreshInvoices: async (_params, setData) => {
  98. const res = await fetch("/api/v1/invoices");
  99. const invoices = await res.json();
  100. setData((prev) => ({ ...prev, invoices }));
  101. },
  102. createInvoice: async (params, setData) => {
  103. if (!params?.customerId || !params?.dueDate) {
  104. toast.error("Customer ID and due date required");
  105. return;
  106. }
  107. try {
  108. const res = await fetch("/api/v1/invoices", {
  109. method: "POST",
  110. headers: { "Content-Type": "application/json" },
  111. body: JSON.stringify(params),
  112. });
  113. const invoice = await res.json();
  114. if (res.ok) {
  115. toast.success("Invoice created");
  116. const listRes = await fetch("/api/v1/invoices");
  117. const invoices = await listRes.json();
  118. setData((prev) => ({ ...prev, invoices }));
  119. } else {
  120. toast.error(invoice.error || "Failed to create invoice");
  121. }
  122. } catch (err) {
  123. console.error("Failed to create invoice:", err);
  124. toast.error("Failed to create invoice");
  125. }
  126. },
  127. sendInvoice: async (params) => {
  128. if (!params?.invoiceId) {
  129. toast.error("Invoice ID required");
  130. return;
  131. }
  132. const res = await fetch(`/api/v1/invoices/${params.invoiceId}/send`, {
  133. method: "POST",
  134. });
  135. const result = await res.json();
  136. if (res.ok) {
  137. toast.success(result.message || "Invoice sent");
  138. } else {
  139. toast.error(result.error || "Failed to send invoice");
  140. }
  141. },
  142. markInvoicePaid: async (params) => {
  143. if (!params?.invoiceId) {
  144. toast.error("Invoice ID required");
  145. return;
  146. }
  147. const res = await fetch(`/api/v1/invoices/${params.invoiceId}/mark-paid`, {
  148. method: "POST",
  149. });
  150. const result = await res.json();
  151. if (res.ok) {
  152. toast.success(result.message || "Invoice marked paid");
  153. } else {
  154. toast.error(result.error || "Failed to mark invoice paid");
  155. }
  156. },
  157. viewExpenses: async (params, setData) => {
  158. const queryParams = new URLSearchParams();
  159. if (params?.status) queryParams.set("status", String(params.status));
  160. const url = `/api/v1/expenses${queryParams.toString() ? `?${queryParams}` : ""}`;
  161. const res = await fetch(url);
  162. const expenses = await res.json();
  163. setData((prev) => ({ ...prev, expenses }));
  164. },
  165. refreshExpenses: async (_params, setData) => {
  166. const res = await fetch("/api/v1/expenses");
  167. const expenses = await res.json();
  168. setData((prev) => ({ ...prev, expenses }));
  169. },
  170. createExpense: async (params, setData) => {
  171. if (!params?.vendor || !params?.category || params?.amount === undefined) {
  172. toast.error("Vendor, category, and amount required");
  173. return;
  174. }
  175. try {
  176. const res = await fetch("/api/v1/expenses", {
  177. method: "POST",
  178. headers: { "Content-Type": "application/json" },
  179. body: JSON.stringify(params),
  180. });
  181. const expense = await res.json();
  182. if (res.ok) {
  183. toast.success("Expense created");
  184. const listRes = await fetch("/api/v1/expenses");
  185. const expenses = await listRes.json();
  186. setData((prev) => ({ ...prev, expenses }));
  187. } else {
  188. toast.error(expense.error || "Failed to create expense");
  189. }
  190. } catch (err) {
  191. console.error("Failed to create expense:", err);
  192. toast.error("Failed to create expense");
  193. }
  194. },
  195. approveExpense: async (params) => {
  196. if (!params?.expenseId) {
  197. toast.error("Expense ID required");
  198. return;
  199. }
  200. const res = await fetch(`/api/v1/expenses/${params.expenseId}/approve`, {
  201. method: "POST",
  202. });
  203. const result = await res.json();
  204. if (res.ok) {
  205. toast.success(result.message || "Expense approved");
  206. } else {
  207. toast.error(result.error || "Failed to approve expense");
  208. }
  209. },
  210. };
  211. // =============================================================================
  212. // Execute Action
  213. // =============================================================================
  214. type Catalog = typeof dashboardCatalog;
  215. type CatalogActions = Catalog["data"]["actions"];
  216. /**
  217. * Execute an action by name with the given parameters.
  218. */
  219. export async function executeAction(
  220. actionName: string,
  221. params: Record<string, unknown> | undefined,
  222. setData: SetData,
  223. data: DataModel = {},
  224. ): Promise<void> {
  225. const handler = actionHandlers[actionName as keyof CatalogActions];
  226. if (handler) {
  227. await (handler as ActionFn<Catalog, keyof CatalogActions>)(
  228. params as never,
  229. setData,
  230. data,
  231. );
  232. } else {
  233. console.log("Unknown action:", actionName, params);
  234. toast(`Action: ${actionName}`);
  235. }
  236. }