diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..a10f75c
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,8 @@
+# Local development only (`npm run dev`). Reference for the values
+# `npm run dev` reads from .env; nothing here is used by a deployed build.
+#
+# In a deployed environment these values come from config.js, which the server
+# writes when it places the build. Do not commit environment-specific values —
+# one build is promoted across environments unchanged.
+VITE_ZINO_API_URL=http://localhost:8085
+VITE_ZINO_MOCK=false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5b08070
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+dist/
+.env
+*.local
diff --git a/README.md b/README.md
index 2d42587..2b40f0e 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
# asset-management
-asset-management — generated by frontgen
\ No newline at end of file
+Frontgen project for org 1 / app 531.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..0a97ae2
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ asset-management
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2cc4618
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "asset-management",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "typecheck": "tsc -b",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.60.5",
+ "lucide-react": "^0.441.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.26.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.5",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.41",
+ "tailwindcss": "^3.4.10",
+ "typescript": "~5.5.4",
+ "vite": "^5.4.2"
+ }
+}
\ No newline at end of file
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..176813c
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,7 @@
+export default function App() {
+ return (
+
+
Asset Management
+
+ )
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..51dc01d
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,17 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import { ZinoProvider } from './zino-sdk'
+import { requireConfigValue, basePath } from './runtimeConfig'
+import './index.css'
+import App from './App'
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+ ,
+)
diff --git a/src/runtimeConfig.ts b/src/runtimeConfig.ts
new file mode 100644
index 0000000..df0b705
--- /dev/null
+++ b/src/runtimeConfig.ts
@@ -0,0 +1,35 @@
+declare global {
+ interface Window {
+ __RUNTIME_CONFIG__?: Record
+ }
+}
+
+// SAME_ORIGIN resolves to the serving origin, for a frontend mounted on a custom
+// domain where the platform is proxied under the same host. It exists because an
+// empty string is falsy and so can never survive the || chain below, which is
+// exactly what a "use the current origin" value has to do.
+export function resolveConfigValue(key: string, fallback = ''): string {
+ const runtime = (window.__RUNTIME_CONFIG__ || {})[key]
+ const devFallback = import.meta.env.DEV ? (import.meta.env[key] as string) : ''
+ const value = runtime || devFallback || fallback
+ return value === 'SAME_ORIGIN' ? window.location.origin.replace(/\/$/, '') : value
+}
+
+// Throws rather than silently running against the wrong backend.
+export function requireConfigValue(key: string): string {
+ const value = resolveConfigValue(key)
+ if (!value) {
+ throw new Error(
+ 'Missing runtime config "' + key + '". The server writes config.js when it ' +
+ 'places the build; for local dev set ' + key + ' in .env.',
+ )
+ }
+ return value
+}
+
+// The mount path this app is served under, taken from the the server
+// writes into index.html. Drives the router basename, so one build serves any
+// mount without being rebuilt.
+export function basePath(): string {
+ return new URL(document.baseURI).pathname
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/zino-sdk/DynamicForm.tsx b/src/zino-sdk/DynamicForm.tsx
new file mode 100644
index 0000000..9ef2870
--- /dev/null
+++ b/src/zino-sdk/DynamicForm.tsx
@@ -0,0 +1,175 @@
+import React, { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { useZino } from "./provider";
+import type { FormField } from "./types";
+
+export interface DynamicFormProps {
+ /** Workflow definition ID. */
+ workflowId: string;
+ /** Activity UID — the form schema is fetched for this activity. */
+ activityId: string;
+ /** Instance ID — omit for INIT activities (start workflow). */
+ instanceId?: string;
+ /** Called after successful submission with the result. */
+ onSuccess?: (result: unknown) => void;
+ /** Called on submission error. */
+ onError?: (error: unknown) => void;
+ /** Optional CSS class for the form wrapper. */
+ className?: string;
+}
+
+/**
+ * Renders a dynamic form based on the activity's form schema.
+ *
+ * Fetches the form config from the core-service, renders fields by type,
+ * and submits via performActivity or startWorkflow.
+ *
+ * ```tsx
+ * { closeModal(); }}
+ * />
+ * ```
+ */
+export function DynamicForm({
+ workflowId,
+ activityId,
+ instanceId,
+ onSuccess,
+ onError,
+ className,
+}: DynamicFormProps) {
+ const { workflows } = useZino();
+ const queryClient = useQueryClient();
+ const [formData, setFormData] = useState>({});
+
+ // Fetch form schema
+ const { data: form, isLoading: formLoading, error: formError } = useQuery({
+ queryKey: ["form", workflowId, activityId, instanceId],
+ queryFn: () => workflows.getForm(workflowId, activityId, "desktop", instanceId),
+ });
+
+ // Submit mutation
+ const submit = useMutation({
+ mutationFn: async (data: Record) => {
+ if (instanceId) {
+ return workflows.performActivity(workflowId, instanceId, activityId, data);
+ } else {
+ return workflows.startWorkflow(workflowId, activityId, data);
+ }
+ },
+ onSuccess: (result) => {
+ queryClient.invalidateQueries({ queryKey: ["recordview"] });
+ queryClient.invalidateQueries({ queryKey: ["instance"] });
+ queryClient.invalidateQueries({ queryKey: ["detailview"] });
+ queryClient.invalidateQueries({ queryKey: ["audit"] });
+ onSuccess?.(result);
+ },
+ onError: (err) => {
+ onError?.(err);
+ },
+ });
+
+ const handleChange = (fieldId: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [fieldId]: value }));
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ // Map form field IDs to their mapped_activity_field_id
+ const mapped: Record = {};
+ for (const field of form?.form_json?.fields ?? []) {
+ const key = field.mapped_activity_field_id || field.id;
+ mapped[key] = formData[field.id] ?? "";
+ }
+ submit.mutate(mapped);
+ };
+
+ if (formLoading) {
+ return ;
+ }
+
+ if (formError) {
+ return Failed to load form: {(formError as any)?.message ?? "Unknown error"}
;
+ }
+
+ const fields: FormField[] = form?.form_json?.fields ?? [];
+
+ return (
+
+ );
+}
+
+function renderField(
+ field: FormField,
+ value: string,
+ onChange: (value: string) => void,
+) {
+ const baseClass =
+ "w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent";
+
+ switch (field.type) {
+ case "paragraph":
+ return (
+