@tailor-platform/erp-kit 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -3
  3. package/package.json +1 -1
  4. package/schemas/app-compose/business-flow.yml +3 -0
  5. package/schemas/app-compose/story.yml +1 -1
  6. package/skills/erp-kit-app-1-requirements/SKILL.md +6 -12
  7. package/skills/erp-kit-app-2-breakdown/SKILL.md +3 -10
  8. package/skills/erp-kit-app-3-doc-review/SKILL.md +1 -5
  9. package/skills/{erp-kit-app-6-impl-spec → erp-kit-app-4-impl-spec}/SKILL.md +10 -21
  10. package/skills/erp-kit-app-5-implementation/SKILL.md +149 -0
  11. package/skills/erp-kit-app-5-implementation/references/backend.md +232 -0
  12. package/skills/erp-kit-app-5-implementation/references/frontend.md +242 -0
  13. package/src/module.ts +38 -0
  14. package/skills/erp-kit-app-1-requirements/references/structure.md +0 -27
  15. package/skills/erp-kit-app-2-breakdown/references/screen-detailview.md +0 -106
  16. package/skills/erp-kit-app-2-breakdown/references/screen-form.md +0 -139
  17. package/skills/erp-kit-app-2-breakdown/references/screen-listview.md +0 -153
  18. package/skills/erp-kit-app-2-breakdown/references/structure.md +0 -27
  19. package/skills/erp-kit-app-3-doc-review/references/structure.md +0 -27
  20. package/skills/erp-kit-app-4-design/SKILL.md +0 -256
  21. package/skills/erp-kit-app-4-design/references/component.md +0 -50
  22. package/skills/erp-kit-app-4-design/references/screen-detailview.md +0 -106
  23. package/skills/erp-kit-app-4-design/references/screen-form.md +0 -139
  24. package/skills/erp-kit-app-4-design/references/screen-listview.md +0 -153
  25. package/skills/erp-kit-app-4-design/references/structure.md +0 -27
  26. package/skills/erp-kit-app-5-design-review/SKILL.md +0 -290
  27. package/skills/erp-kit-app-5-design-review/references/component.md +0 -50
  28. package/skills/erp-kit-app-5-design-review/references/screen-detailview.md +0 -106
  29. package/skills/erp-kit-app-5-design-review/references/screen-form.md +0 -139
  30. package/skills/erp-kit-app-5-design-review/references/screen-listview.md +0 -153
  31. package/skills/erp-kit-app-6-impl-spec/references/auth.md +0 -72
  32. package/skills/erp-kit-app-6-impl-spec/references/structure.md +0 -27
@@ -0,0 +1,242 @@
1
+ # Frontend Implementation Patterns
2
+
3
+ ## Table of Contents
4
+
5
+ - [Frontend Scaffold](#frontend-scaffold)
6
+ - [Frontend Pages](#frontend-pages)
7
+
8
+ ---
9
+
10
+ ## Frontend Scaffold
11
+
12
+ ```
13
+ frontend/
14
+ ├── src/
15
+ │ ├── main.tsx, App.tsx, index.css
16
+ │ ├── graphql/
17
+ │ │ ├── index.ts # gql.tada setup
18
+ │ │ └── generated/ # Placeholder until backend deploy
19
+ │ ├── lib/
20
+ │ │ ├── auth-client.ts
21
+ │ │ └── utils.ts # cn() utility
22
+ │ ├── providers/
23
+ │ │ └── graphql-provider.tsx # urql client with DPoP auth
24
+ │ ├── components/
25
+ │ │ ├── ui/ # shadcn/Radix primitives
26
+ │ │ └── composed/ # empty-state, error-fallback, loading
27
+ │ └── pages/
28
+ │ └── <domain>/
29
+ │ ├── page.tsx # Module redirect (REQUIRED)
30
+ │ └── <entity>/
31
+ │ ├── page.tsx # ListView
32
+ │ ├── components/<entities>-table.tsx
33
+ │ ├── create/page.tsx + components/create-<entity>-form.tsx
34
+ │ └── [id]/
35
+ │ ├── page.tsx # DetailView
36
+ │ ├── components/<entity>-detail.tsx, <entity>-actions.tsx
37
+ │ └── edit/page.tsx + components/edit-<entity>-form.tsx
38
+ ├── scripts/generate-graphql.mjs
39
+ ├── package.json, vite.config.ts, tsconfig.json, eslint.config.js
40
+ └── components.json
41
+ ```
42
+
43
+ `package.json` key dependencies and scripts:
44
+
45
+ ```json
46
+ {
47
+ "type": "module",
48
+ "scripts": {
49
+ "dev": "vite",
50
+ "build": "tsc -b && vite build",
51
+ "generate": "node --env-file=.env ./scripts/generate-graphql.mjs",
52
+ "lint": "eslint .",
53
+ "typecheck": "tsc --noEmit"
54
+ },
55
+ "dependencies": {
56
+ "@tailor-platform/app-shell": "...",
57
+ "gql.tada": "...",
58
+ "urql": "...",
59
+ "react-hook-form": "...",
60
+ "@hookform/resolvers": "...",
61
+ "zod": "...",
62
+ "tailwindcss": "...",
63
+ "lucide-react": "..."
64
+ },
65
+ "devDependencies": {
66
+ "@tailor-platform/app-shell-vite-plugin": "...",
67
+ "@tailor-platform/sdk": "...",
68
+ "@tailwindcss/vite": "...",
69
+ "vite": "...",
70
+ "typescript": "..."
71
+ }
72
+ }
73
+ ```
74
+
75
+ `scripts/generate-graphql.mjs` — fetches the GraphQL schema from the deployed backend:
76
+
77
+ ```js
78
+ import { execSync } from "node:child_process";
79
+ const url = process.env.VITE_TAILOR_APP_URL;
80
+ execSync(`pnpm gql-tada generate schema "${url}/query"`);
81
+ execSync("pnpm gql-tada generate output");
82
+ ```
83
+
84
+ Before deployment, create empty placeholders for `src/graphql/generated/schema.graphql` and `graphql-env.d.ts` so the project compiles. After backend deployment, configure `.env` with `VITE_TAILOR_APP_URL` and `VITE_TAILOR_CLIENT_ID`, then run `pnpm generate` to regenerate them.
85
+
86
+ ---
87
+
88
+ ## Frontend Pages
89
+
90
+ ### GraphQL Setup
91
+
92
+ All operations use gql.tada for type safety:
93
+
94
+ ```ts
95
+ import { graphql, type FragmentOf, readFragment } from "@/graphql";
96
+ ```
97
+
98
+ Built-in queries (`gqlOperations: "query"`) generate:
99
+
100
+ - `<Entity>(id: ID!)` — get by ID
101
+ - `<entities>` — list with connection pattern (`edges { node { ... } }`)
102
+
103
+ Mutations are defined by custom resolvers.
104
+
105
+ ### ListView
106
+
107
+ Key points:
108
+
109
+ - Query uses connection pattern: `edges { node { ...Fragment } }`
110
+ - Two-level fragments: row fragment for items, table fragment for the connection
111
+ - `EmptyState` when `edges.length === 0`
112
+ - Columns map to screen spec's "Required Columns"
113
+ - `Layout` with title and create action button
114
+
115
+ ### DetailView
116
+
117
+ Key points:
118
+
119
+ - Two-column `Layout`: detail on left, actions on right
120
+ - `useParams()` to get entity ID from route
121
+ - `DescriptionCard` from `@tailor-platform/app-shell` for key-value fields:
122
+
123
+ ```tsx
124
+ <DescriptionCard
125
+ data={resource}
126
+ title="Overview"
127
+ columns={3}
128
+ fields={[
129
+ { key: "name", label: "Name", meta: { copyable: true } },
130
+ {
131
+ key: "status",
132
+ label: "Status",
133
+ type: "badge",
134
+ meta: { badgeVariantMap: { ACTIVE: "success" } },
135
+ },
136
+ { key: "createdAt", label: "Created At", type: "date", meta: { dateFormat: "medium" } },
137
+ ]}
138
+ />
139
+ ```
140
+
141
+ Field types: `"text"` (default), `"badge"`, `"money"`, `"date"`, `"link"`, `"address"`, `"reference"`, `"divider"`
142
+
143
+ ### Form Pattern
144
+
145
+ Create and edit forms share the same structure:
146
+
147
+ - **React Hook Form + Zod** for validation with `zodResolver`
148
+ - **urql `useMutation`** for GraphQL mutations
149
+ - **Navigate `".."`** to go back to list after success
150
+ - Edit form pre-fills `defaultValues` from existing entity via fragment
151
+
152
+ #### Field Type Mapping
153
+
154
+ | Field Type | Component | Zod Schema |
155
+ | ---------- | ------------------------- | ------------------------------- |
156
+ | Text | `<Input />` | `z.string()` |
157
+ | Textarea | `<textarea />` | `z.string()` |
158
+ | Dropdown | `<Select />` | `z.string()` or `z.enum([...])` |
159
+ | Date | `<Input type="date" />` | `z.string()` (ISO format) |
160
+ | Number | `<Input type="number" />` | `z.coerce.number()` |
161
+ | Checkbox | `<Checkbox />` | `z.boolean()` |
162
+
163
+ #### Validation
164
+
165
+ - **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
166
+ - **Required: No** → `.optional()`
167
+
168
+ ### Shared Component Patterns
169
+
170
+ #### Fragment Collocation
171
+
172
+ Each component defines and exports its own GraphQL fragment. The parent page imports it and includes it in the query:
173
+
174
+ ```tsx
175
+ // components/user-card.tsx
176
+ export const UserCardFragment = graphql(`
177
+ fragment UserCard on User {
178
+ id
179
+ name
180
+ email
181
+ }
182
+ `);
183
+
184
+ export const UserCard = ({ user }: { user: FragmentOf<typeof UserCardFragment> }) => {
185
+ const data = readFragment(UserCardFragment, user);
186
+ return <div>{data.name}</div>;
187
+ };
188
+
189
+ // page.tsx
190
+ const UserQuery = graphql(
191
+ `
192
+ query User($id: ID!) {
193
+ user(id: $id) {
194
+ ...UserCard
195
+ }
196
+ }
197
+ `,
198
+ [UserCardFragment],
199
+ );
200
+ ```
201
+
202
+ #### App.tsx
203
+
204
+ - `AuthGuard` is not exported from app-shell — implement it yourself, pass via `guardComponent` prop
205
+ - `GraphQLProvider` needs `authClient` prop to attach DPoP auth headers
206
+ - Required env vars: `VITE_TAILOR_APP_URL`, `VITE_TAILOR_CLIENT_ID`
207
+ - Auth client: create once in `src/lib/auth-client.ts` using `createAuthClient({ appUri, clientId })`
208
+ - Sidebar: use `SidebarGroup` + `SidebarItem` for custom ordering
209
+
210
+ #### Module-level page.tsx (required)
211
+
212
+ Every module directory must have a `page.tsx` that redirects to the first child. Use **absolute paths** to avoid double-path bugs:
213
+
214
+ ```tsx
215
+ useEffect(() => {
216
+ void navigate("/item-management/item", { replace: true });
217
+ }, [navigate]);
218
+ ```
219
+
220
+ #### Page conventions
221
+
222
+ Every page component must:
223
+
224
+ 1. Be the **default export** of `page.tsx`
225
+ 2. Set `appShellPageProps` with at least `meta.title`
226
+
227
+ #### Common imports
228
+
229
+ ```tsx
230
+ import {
231
+ Layout,
232
+ Link,
233
+ useParams,
234
+ useNavigate,
235
+ type AppShellPageProps,
236
+ } from "@tailor-platform/app-shell";
237
+ import { useQuery, useMutation } from "urql";
238
+ import { graphql, type FragmentOf, readFragment } from "@/graphql";
239
+ import { useForm } from "react-hook-form";
240
+ import { zodResolver } from "@hookform/resolvers/zod";
241
+ import { z } from "zod";
242
+ ```
package/src/module.ts CHANGED
@@ -85,3 +85,41 @@ export { type RevokePermissionFromRoleInput } from "./modules/user-management/co
85
85
  export { type AssignRoleToUserInput } from "./modules/user-management/command/assignRoleToUser";
86
86
  export { type RevokeRoleFromUserInput } from "./modules/user-management/command/revokeRoleFromUser";
87
87
  export { type LogAuditEventInput } from "./modules/user-management/command/logAuditEvent";
88
+ // item-management
89
+ export { defineModule as defineItemManagementModule } from "./modules/item-management/module";
90
+ export {
91
+ permissions as itemManagementPermissions,
92
+ own as itemManagementOwn,
93
+ all as itemManagementAll,
94
+ } from "./modules/item-management/lib/permissions.generated";
95
+ export { ItemStatus } from "./modules/item-management/generated/enums";
96
+ export {
97
+ ItemNotFoundError,
98
+ DuplicateSkuError,
99
+ DuplicateBarcodeError,
100
+ UnitNotFoundError as ItemUnitNotFoundError,
101
+ SkuImmutableError,
102
+ UomLockedError,
103
+ NoFieldsToUpdateError,
104
+ InvalidStateTransitionError,
105
+ DeleteNonDraftError,
106
+ NodeNotFoundError,
107
+ DuplicateNodeCodeError,
108
+ ParentNodeNotFoundError,
109
+ MaxDepthExceededError,
110
+ CodeImmutableError,
111
+ MissingRequiredFieldsError,
112
+ CircularReferenceError,
113
+ NodeHasChildrenError,
114
+ NodeHasAssignmentsError,
115
+ DuplicateAssignmentError,
116
+ AssignmentNotFoundError as ItemAssignmentNotFoundError,
117
+ } from "./modules/item-management/lib/errors.generated";
118
+ export { type GetItemInput } from "./modules/item-management/query/getItem";
119
+ export { type GetTaxonomyNodeInput } from "./modules/item-management/query/getTaxonomyNode";
120
+ export { type CreateItemInput } from "./modules/item-management/command/createItem";
121
+ export { type UpdateItemInput } from "./modules/item-management/command/updateItem";
122
+ export { type ActivateItemInput } from "./modules/item-management/command/activateItem";
123
+ export { type DeactivateItemInput } from "./modules/item-management/command/deactivateItem";
124
+ export { type CreateTaxonomyNodeInput } from "./modules/item-management/command/createTaxonomyNode";
125
+ export { type AssignItemToTaxonomyInput } from "./modules/item-management/command/assignItemToTaxonomy";
@@ -1,27 +0,0 @@
1
- # Application Directory Structure
2
-
3
- ```
4
- {app_name}/
5
- ├── backend/
6
- │ ├── src/
7
- │ │ ├── modules.ts # Declaring module usage
8
- │ │ ├── modules/
9
- │ │ │ └── {module-name}/ # Module-specific directory
10
- │ │ │ ├── resolvers/ # API Definition to expose graphql apis
11
- │ │ │ └── executors/ # PubSub Automation (one file per declaration)
12
- │ │ └── generated/ # Auto-generated code (do not edit)
13
- │ └── tailor.config.ts # tailor application config
14
-
15
- └── frontend/
16
- └── src/
17
- ├── pages/ # File-based routing (auto-discovered by Vite plugin)
18
- │ └── {page-path}/
19
- │ ├── page.tsx
20
- │ └── {page-path}/
21
- │ ├── components/
22
- │ └── page.tsx
23
- ├── components/
24
- │ └── ui/ # Generic UI components
25
- ├── graphql/ # gql.tada settings
26
- └── providers/ # react providers
27
- ```
@@ -1,106 +0,0 @@
1
- # DetailView Screen Implementation
2
-
3
- Implementation pattern for screens with `Screen Type: DetailView`.
4
- Assumes `page.md` and `component.md` rules.
5
-
6
- ## File Structure
7
-
8
- ```
9
- {screen-path}/[id]/
10
- ├── components/
11
- │ ├── {screen-name}-detail.tsx # Main content (left column)
12
- │ └── {screen-name}-actions.tsx # Action sidebar (right column)
13
- ├── edit/
14
- │ ├── components/
15
- │ │ └── edit-{screen-name}-form.tsx
16
- │ └── page.tsx
17
- └── page.tsx
18
- ```
19
-
20
- ## Layout
21
-
22
- - Two-column layout: main content on the left, actions on the right.
23
-
24
- ```tsx
25
- const ResourcePage = () => {
26
- const { id } = useParams();
27
- const [{ data, error, fetching }, reexecuteQuery] = useQuery({
28
- query: ResourceQuery,
29
- variables: { id: id! },
30
- });
31
-
32
- if (fetching) return <Loading />;
33
- if (error || !data?.resource) return <ErrorFallback ... />;
34
-
35
- return (
36
- <Layout columns={2} title="Resource Detail">
37
- <Layout.Column>
38
- <ResourceDetail resource={data.resource} />
39
- </Layout.Column>
40
- <Layout.Column>
41
- <ResourceActions resource={data.resource} />
42
- </Layout.Column>
43
- </Layout>
44
- );
45
- };
46
- ```
47
-
48
- ## Left Column: Detail Component
49
-
50
- Stack `DescriptionCard` and related tables vertically with `space-y-6`.
51
-
52
- - `DescriptionCard` (`@tailor-platform/app-shell`): renders key-value fields declaratively.
53
- - Complex content (tables, timelines): wrap in `<div className="rounded-lg border bg-card p-6">`.
54
-
55
- ### DescriptionCard
56
-
57
- ```tsx
58
- <DescriptionCard
59
- data={resource}
60
- title="Overview"
61
- columns={3}
62
- fields={[
63
- { key: "name", label: "Name", meta: { copyable: true } },
64
- {
65
- key: "status",
66
- label: "Status",
67
- type: "badge",
68
- meta: { badgeVariantMap: { ACTIVE: "success", PENDING: "warning" } },
69
- },
70
- { type: "divider" },
71
- {
72
- key: "createdAt",
73
- label: "Created At",
74
- type: "date",
75
- meta: { dateFormat: "medium" },
76
- },
77
- ]}
78
- />
79
- ```
80
-
81
- Field types: `"text"` (default), `"badge"`, `"money"`, `"date"`, `"link"`, `"address"`, `"reference"`, `"divider"`
82
-
83
- ## Right Column: Actions Component
84
-
85
- Wrap in a `Card` component. Use `Button variant="ghost"` for each action item.
86
-
87
- ```tsx
88
- <Card>
89
- <CardHeader>
90
- <CardTitle>Actions</CardTitle>
91
- </CardHeader>
92
- <CardContent className="space-y-2">
93
- <Button variant="ghost" className="w-full justify-start gap-2" asChild>
94
- <Link to="edit">✎ Edit</Link>
95
- </Button>
96
- <Button variant="ghost" className="w-full justify-start gap-2" onClick={handler}>
97
- ✓ Approve
98
- </Button>
99
- </CardContent>
100
- </Card>
101
- ```
102
-
103
- - Navigation: `<Button variant="ghost" asChild><Link to="...">`
104
- - Mutation: `<Button variant="ghost" onClick={handler}>` with custom resolvers (see `backend/resolvers.md`)
105
- - Conditional: show/hide based on status
106
- - Multiple cards: stack with `<div className="space-y-6">`
@@ -1,139 +0,0 @@
1
- # Form Screen Implementation
2
-
3
- Implementation pattern for screens with `Screen Type: Form`.
4
- Assumes `page.md` and `component.md` rules.
5
-
6
- ## File Structure
7
-
8
- ```
9
- {screen-path}/
10
- ├── components/
11
- │ └── {screen-name}-form.tsx # Form component with validation
12
- └── page.tsx
13
- ```
14
-
15
- ## Page Component (page.tsx)
16
-
17
- Form pages delegate mutation logic to the form component.
18
-
19
- ```tsx
20
- const ScreenNamePage = () => (
21
- <Layout columns={1} title="Screen Title">
22
- <Layout.Column>
23
- <ScreenNameForm />
24
- </Layout.Column>
25
- </Layout>
26
- );
27
- ```
28
-
29
- For edit forms that need existing data, co-locate data fetching in the page component:
30
-
31
- ```tsx
32
- const EditPage = () => {
33
- const { id } = useParams();
34
- const [{ data, error, fetching }] = useQuery({
35
- query: ResourceQuery,
36
- variables: { id: id! },
37
- });
38
-
39
- if (fetching) return <Loading />;
40
- if (error || !data?.resource) return <ErrorFallback ... />;
41
-
42
- return (
43
- <Layout columns={1} title="Edit Resource">
44
- <Layout.Column>
45
- <EditResourceForm resource={data.resource} />
46
- </Layout.Column>
47
- </Layout>
48
- );
49
- };
50
- ```
51
-
52
- ## Form Component (components/{screen-name}-form.tsx)
53
-
54
- ### Technology Stack
55
-
56
- - `react-hook-form` — form state management
57
- - `zod` + `@hookform/resolvers/zod` — validation
58
- - `useMutation` (urql) — GraphQL mutation
59
- - `useNavigate` (@tailor-platform/app-shell) — post-submit navigation
60
-
61
- ### Pattern
62
-
63
- ```tsx
64
- const formSchema = z.object({
65
- title: z.string().min(1, "Title is required"),
66
- description: z.string().optional(),
67
- });
68
-
69
- type FormValues = z.infer<typeof formSchema>;
70
-
71
- export const ScreenNameForm = () => {
72
- const navigate = useNavigate();
73
- const [, createResource] = useMutation(CreateMutation);
74
-
75
- const form = useForm<FormValues>({
76
- resolver: zodResolver(formSchema),
77
- defaultValues: { title: "", description: "" },
78
- });
79
-
80
- const onSubmit = (values: FormValues) => {
81
- void createResource({ input: values }).then((result) => {
82
- if (!result.error) {
83
- void navigate("..");
84
- }
85
- });
86
- };
87
-
88
- return (
89
- <Form {...form}>
90
- <form onSubmit={(e) => void form.handleSubmit(onSubmit)(e)} className="max-w-md space-y-4">
91
- <FormField
92
- control={form.control}
93
- name="title"
94
- render={({ field }) => (
95
- <FormItem>
96
- <FormLabel>Title</FormLabel>
97
- <FormControl>
98
- <Input placeholder="Enter title" {...field} />
99
- </FormControl>
100
- <FormMessage />
101
- </FormItem>
102
- )}
103
- />
104
- <div className="flex gap-2">
105
- <Button type="submit">Create</Button>
106
- <Button type="button" variant="outline" onClick={() => void navigate("..")}>
107
- Cancel
108
- </Button>
109
- </div>
110
- </form>
111
- </Form>
112
- );
113
- };
114
- ```
115
-
116
- ## Field Type Mapping
117
-
118
- | Field Type | Component | Zod Schema |
119
- | ---------- | ------------------------------ | ------------------------------- |
120
- | Text | `<Input />` | `z.string()` |
121
- | Textarea | `<textarea className="..." />` | `z.string()` |
122
- | Dropdown | `<Select />` | `z.string()` or `z.enum([...])` |
123
- | Date | `<Input type="date" />` | `z.string()` (ISO format) |
124
- | Number | `<Input type="number" />` | `z.coerce.number()` |
125
- | Email | `<Input type="email" />` | `z.string().email()` |
126
- | Checkbox | `<Checkbox />` | `z.boolean()` |
127
- | Radio | `<RadioGroup />` | `z.enum([...])` |
128
-
129
- ## Validation Mapping
130
-
131
- - **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
132
- - **Required: No** → `.optional()`
133
-
134
- ## Key Points
135
-
136
- - Set `defaultValues` for all fields (empty string, false, etc.)
137
- - Navigate to `".."` after successful mutation
138
- - Cancel button must use `type="button"` to prevent form submit
139
- - For edit forms, accept fragment data as props and pre-fill `defaultValues`