Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/actions/contract-backfill.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export const ExecutedUploadAction: Action = {
params: [
// Field-backed where a picker is wanted: the param inherits the field's
// type, its label and its `lookupFilters`, so inactive types, blocked
// counterparties and non-company business units are already out of the
// counterparties, and business units of the wrong kind (a non-company as
// the entity, a non-department as the department) are already out of the
// pickers.
{ field: 'contract_type', required: true, helpText: 'The workflow this contract would have run. It still stamps the number, the category and the execution formalities.' },
{ field: 'party', required: true, helpText: 'The counterparty on the executed document.' },
Expand Down
14 changes: 9 additions & 5 deletions src/actions/contract.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { P } from '@objectstack/spec';
* ## Why the action carries params at all
*
* The flat screens of a flow have no record picker (measured on the pinned
* console — see the flow's header), so the three pickers the intake needs are
* console — see the flow's header), so the four pickers the intake needs are
* collected HERE, by the action's param dialog: field-backed lookup params
* render the real pickers and inherit the fields' `lookupFilters` (active
* types only; blocked parties hidden; company-kind business units only). The console posts them as the flow's
* `params`, which the engine seeds into the input variables of the same name.
* types only; blocked parties hidden; company-kind business units for the
* entity, department-kind ones for the department). The console posts them
* as the flow's `params`, which the engine seeds into the input variables of
* the same name.
*
* ## The headless half
*
Expand Down Expand Up @@ -55,16 +57,18 @@ export const LaunchContractAction: Action = {
target: 'contract_intake',
locations: ['list_toolbar'],
params: [
// The three pickers a flat screen cannot render.
// The four pickers a flat screen cannot render.
{ field: 'contract_type', label: 'Contract type', required: true, helpText: 'The workflow this contract runs: which fields are asked, whether legal reviews it, how it is executed.' },
{ field: 'party', label: 'Counterparty', required: false, helpText: 'Pick an existing counterparty, or leave empty to create one on the next step.' },
// Required here because it is required on the contract: a signing entity
// is a business-unit row, so the action has no default to fall back to.
{ field: 'our_entity', label: 'Our signing entity', required: true, helpText: 'Which of our legal entities signs.' },
// Optional, as the field is: a business-unit row of kind department
// (#109). Before #109 it was a select on the flow's core screen.
{ field: 'department', label: 'Requesting department', required: false, helpText: 'The department launching this contract.' },
// Everything else is answered on the flow's screens; declared here for
// the headless doors only.
headless('title', 'text', 'Title'),
headless('department', 'text', 'Requesting department'),
headless('amount', 'number', 'Contract amount'),
headless('currency_code', 'text', 'Currency (ISO 4217, lowercase)'),
headless('is_amount_estimated', 'boolean', 'Amount is an estimate'),
Expand Down
14 changes: 7 additions & 7 deletions src/apps/clm.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ import { App } from '@objectstack/spec/ui';
* switcher builds a tab per named `listViews` entry, so `status_kanban` is
* one click from 全部合同 under its own label and zero clicks further than a
* second sidebar row would be.
* - **签约主体与部门** (管理) is not a row here yet. The signing entities are
* rows now — `our_entity` is a lookup to the platform's `sys_business_unit`
* (kind `company`, #20 ruling C), maintained in Setup → Business Units, the
* platform's own org-chart surface — but `department` is still a
* `Field.select` option list on `clm_contract` (schema, not rows) until #109
* moves it onto the same object. The sidebar row for the pair is decided
* once both halves are data, rather than half a row now.
* - **签约主体与部门** (管理) is not a row here yet. Both halves are rows of
* the platform's `sys_business_unit` now: `our_entity` is a lookup to the
* company-kind units (#20 ruling C) and `department` to the
* department-kind ones (#109). Both are maintained in Setup → Business
* Units, the platform's own org-chart surface. Whether the 管理 group also
* gets a row pointing there is a §05 navigation decision of its own, not
* part of either card.
* - **报表** (管理) is still absent. Card 10 landed §09's four datasets and
* three DASHBOARDS, which the 分析 group below points at with
* `type: 'dashboard'` items; no `reports:` metadata exists yet, and a
Expand Down
44 changes: 24 additions & 20 deletions src/dashboards/legal.dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,14 @@ import type { Dashboard } from '@objectstack/spec/ui';
* inline map produces none. A filter added later with a plain-string label
* ships English on a zh-CN board with `pnpm lint:i18n-gate` still green.
*
* ⚠️ These option labels DUPLICATE `objects.<object>.fields.<field>.options.*`
* in the bundle, which is not ideal and is not avoidable here: the filter
* declares its own option list (the schema requires it for a `select` filter),
* and the two are separate authored strings that happen to say the same thing.
* If they drift, the bundle's copy is the one the grid and the record page use.
* ⚠️ The `category` filter's option labels DUPLICATE
* `objects.<object>.fields.<field>.options.*` in the bundle, which is not
* ideal and is not avoidable here: the filter declares its own option list
* (the schema requires it for a `select` filter), and the two are separate
* authored strings that happen to say the same thing. If they drift, the
* bundle's copy is the one the grid and the record page use. The `department`
* filter has no such copy since #109: its options are business-unit rows, and
* a row's name is data, shown as written.
*/
export const LegalDashboard: Dashboard = {
name: 'legal_workbench',
Expand All @@ -75,10 +78,10 @@ export const LegalDashboard: Dashboard = {
columns: 12,
gap: 4,

// Both are persisted `Field.select` columns on `clm_contract`, and every
// widget on this dashboard is bound to `contract_metrics` (base object
// `clm_contract`), so neither filter can reach a widget whose object lacks
// the column.
// Both are persisted columns on `clm_contract` (`category` a select,
// `department` a lookup), and every widget on this dashboard is bound to
// `contract_metrics` (base object `clm_contract`), so neither filter can
// reach a widget whose object lacks the column.
globalFilters: [
{
name: 'category',
Expand All @@ -100,23 +103,24 @@ export const LegalDashboard: Dashboard = {
{ value: 'other', label: { en: 'Other', 'zh-CN': '其他' } },
],
},
// `department` is a lookup to `sys_business_unit` since #109, so the
// filter's options are the department-kind units themselves, read at run
// time, and the value it applies is the unit's id, which is what the
// column holds. Retired departments stay in the list on purpose: their
// contracts are still on the board.
{
name: 'department',
field: 'department',
object: 'clm_contract',
label: { en: 'Requesting Department', 'zh-CN': '发起部门' },
type: 'select',
type: 'lookup',
scope: 'dashboard',
options: [
{ value: 'sales', label: { en: 'Sales', 'zh-CN': '销售' } },
{ value: 'procurement', label: { en: 'Procurement', 'zh-CN': '采购' } },
{ value: 'legal', label: { en: 'Legal', 'zh-CN': '法务' } },
{ value: 'finance', label: { en: 'Finance', 'zh-CN': '财务' } },
{ value: 'operations', label: { en: 'Operations', 'zh-CN': '运营' } },
{ value: 'people', label: { en: 'People / HR', 'zh-CN': '人力资源' } },
{ value: 'it', label: { en: 'IT', 'zh-CN': '信息技术' } },
{ value: 'other', label: { en: 'Other', 'zh-CN': '其他' } },
],
optionsFrom: {
object: 'sys_business_unit',
valueField: 'id',
labelField: 'name',
filter: { kind: 'department' },
},
},
],

Expand Down
50 changes: 32 additions & 18 deletions src/data/business-unit.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
import { SeedSchema } from '@objectstack/spec/data';

import { STRINGS } from './demo-locale.js';
import { SIGNING_ENTITY_PLAN } from './plan.js';
import { DEPARTMENT_PLAN, SIGNING_ENTITY_PLAN } from './plan.js';

/**
* The group's three legal entities — DESIGN.md §10's US parent with a European
* and an Asia-Pacific subsidiary — as rows of the platform's organisation
* object, `sys_business_unit`.
* and an Asia-Pacific subsidiary — and the seven departments that launch its
* contracts, as rows of the platform's organisation object,
* `sys_business_unit`.
*
* `clm_contract.our_entity` is a lookup to that object filtered to
* `kind: 'company'` (#20, ruling C), so a signing entity is DATA: a customer
* adds theirs in Setup → Business Units, and this demo plays the admin's part
* by seeding the fictional group's. The parent/child link is the platform's
* own `parent_business_unit_id`, so the org-chart view in Setup shows the
* group as a tree.
* by seeding the fictional group's. `clm_contract.department` is the same
* shape filtered to `kind: 'department'` (#109), so the departments are rows
* of the same object, under the US parent (`plan.ts` says why). The
* parent/child link is the platform's own `parent_business_unit_id`, so the
* org-chart view in Setup shows the group as one tree.
*
* ## Why `SeedSchema.parse` and not `defineSeed`
*
Expand All @@ -24,26 +27,37 @@ import { SIGNING_ENTITY_PLAN } from './plan.js';
* depends on the package that does. Every key below is a declared field of
* the installed `@objectstack/platform-objects` 18.1.0 definition (`name`,
* `code`, `kind`, `parent_business_unit_id`, `active`), read there rather
* than assumed.
* than assumed. `department` is one of that definition's `kind` options
* (`company · division · department · office · cost_center`).
*
* ## The key is `code`
*
* `code` is the platform's own natural key for a unit ("Short stable code …
* Unique within tenant") and it is locale-blind, so `demo-en` and `demo-zh`
* upsert the same three rows and every contract resolves its `our_entity`
* against the same codes in either language. The subsidiaries name their
* parent by code too; the loader writes a self-referencing dataset row by row,
* so the parent exists by the time a child names it.
* upsert the same ten rows and every contract resolves its `our_entity` and
* its `department` against the same codes in either language. The
* subsidiaries and the departments name their parent by code too; the loader
* writes a self-referencing dataset row by row, in this order, so the parent
* exists by the time a child names it — the companies come first.
*/
export const businessUnitSeed = SeedSchema.parse({
object: 'sys_business_unit',
mode: 'upsert',
externalId: 'code',
records: SIGNING_ENTITY_PLAN.map((entity, index) => ({
code: entity.code,
name: STRINGS.signingEntities[index]!,
kind: 'company',
parent_business_unit_id: entity.parentCode,
active: true,
})),
records: [
...SIGNING_ENTITY_PLAN.map((entity, index) => ({
code: entity.code,
name: STRINGS.signingEntities[index]!,
kind: 'company',
parent_business_unit_id: entity.parentCode,
active: true,
})),
...DEPARTMENT_PLAN.map((department, index) => ({
code: department.code,
name: STRINGS.departments[index]!,
kind: 'department',
parent_business_unit_id: department.parentCode,
active: true,
})),
],
});
3 changes: 3 additions & 0 deletions src/data/contract.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const contractSeed = defineSeed(Contract, {
// England and Wales → the APAC subsidiary. A code, resolved against the
// `sys_business_unit` rows `business-unit.seed.ts` upserts on `code`.
our_entity: SIGNING_ENTITY_PLAN[law.entityIndex]!.code,
// The requesting department follows the contract type (`plan.ts`): a
// code, resolved against the department-kind `sys_business_unit` rows
// `business-unit.seed.ts` upserts on `code`, as `our_entity` is.
department: type.department,
// `owner_id` names a user the way `legal_owner` on the next line does —
// a `lookup('sys_user')` resolved against `sys_user.name` by the two-boot
Expand Down
2 changes: 2 additions & 0 deletions src/data/demo-en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { DemoStrings } from '../strings.js';
import { APPROVAL_RULES, CLAUSES, CONTRACT_TYPES } from './catalog.js';
import { PARTIES } from './parties.js';
import {
DEPARTMENTS,
DEVIATION_JUSTIFICATIONS,
DEVIATION_TEXTS,
JURISDICTIONS,
Expand All @@ -28,6 +29,7 @@ import {
*/
export const EN: DemoStrings = {
signingEntities: SIGNING_ENTITIES,
departments: DEPARTMENTS,
contractTypes: CONTRACT_TYPES,
clauses: CLAUSES,
approvalRules: APPROVAL_RULES,
Expand Down
14 changes: 14 additions & 0 deletions src/data/demo-en/prose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ export const SIGNING_ENTITIES: readonly string[] & { length: 3 } = [
'Arcadia Asia Pacific Pte. Ltd.',
] as const;

/**
* The departments that launch contracts, in `DEPARTMENT_PLAN`'s order
* (`../plan.ts`). Each is a department-kind business unit under the US parent.
*/
export const DEPARTMENTS: readonly string[] & { length: 7 } = [
'Sales',
'Procurement',
'Legal',
'Finance',
'Operations',
'People & HR',
'IT',
] as const;

/** Distinguishes a second contract of the same type with the same counterparty. */
export const TITLE_QUALIFIERS: readonly string[] & { length: 8 } = [
'Renewal',
Expand Down
2 changes: 2 additions & 0 deletions src/data/demo-zh/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { DemoStrings } from '../strings.js';
import { APPROVAL_RULES, CLAUSES, CONTRACT_TYPES } from './catalog.js';
import { PARTIES } from './parties.js';
import {
DEPARTMENTS,
DEVIATION_JUSTIFICATIONS,
DEVIATION_TEXTS,
JURISDICTIONS,
Expand All @@ -27,6 +28,7 @@ import {
*/
export const ZH_CN: DemoStrings = {
signingEntities: SIGNING_ENTITIES,
departments: DEPARTMENTS,
contractTypes: CONTRACT_TYPES,
clauses: CLAUSES,
approvalRules: APPROVAL_RULES,
Expand Down
11 changes: 11 additions & 0 deletions src/data/demo-zh/prose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export const SIGNING_ENTITIES: readonly string[] & { length: 3 } = [
'阿卡迪亚亚太私人有限公司',
] as const;

/** 发起合同的七个部门,顺序同 `../plan.ts` 的 `DEPARTMENT_PLAN`;都是美国母公司之下 kind 为部门的业务单元。 */
export const DEPARTMENTS: readonly string[] & { length: 7 } = [
'销售部',
'采购部',
'法务部',
'财务部',
'运营部',
'人力资源部',
'信息技术部',
] as const;

/** 同一相对方、同一类型的第二份合同用它区分。 */
export const TITLE_QUALIFIERS: readonly string[] & { length: 8 } = [
'续签',
Expand Down
10 changes: 5 additions & 5 deletions src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import { obligationSeed, paymentPlanSeed } from './post-signature.seed.js';
* anyway:
*
* - **The configuration objects come first.** `clm_contract.contract_type`
* resolves against a type's `name`, `clm_contract.our_entity` against a
* business unit's `code` and `clm_deviation.clause` against a clause's
* `title`; a reference that resolves to nothing on a `required` column
* resolves against a type's `name`, `clm_contract.our_entity` and
* `clm_contract.department` against a business unit's `code`, and
* `clm_deviation.clause` against a clause's `title`; a reference that resolves to nothing on a `required` column
* takes the whole row with it.
* - **`clm_contract` comes before its five children**, all of which reference
* it by title through the one function in `keys.ts`.
Expand All @@ -53,8 +53,8 @@ import { obligationSeed, paymentPlanSeed } from './post-signature.seed.js';
*/
export const demoSeeds: Seed[] = [
// 1. Configuration first — every contract resolves its type here, its
// signing entity (a `sys_business_unit` of kind `company`), and every
// deviation its clause.
// signing entity (a `sys_business_unit` of kind `company`) and its
// department (one of kind `department`), and every deviation its clause.
businessUnitSeed,
contractTypeSeed,
clauseSeed,
Expand Down
Loading
Loading