aimeat 1.31.0 โ†’ 1.32.0

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 (183) hide show
  1. package/dist/locales/en.json +196 -2
  2. package/dist/locales/fi.json +196 -2
  3. package/dist/public/components/InboxLink.js +29 -0
  4. package/dist/public/css/views/companies.css +65 -0
  5. package/dist/public/css/views/inbox.css +100 -1
  6. package/dist/public/css/views/members.css +33 -0
  7. package/dist/public/css/views/my-company.css +52 -0
  8. package/dist/public/css/views/profile.css +22 -0
  9. package/dist/public/js/services/messages.js +16 -0
  10. package/dist/public/js/services/notebook.js +16 -6
  11. package/dist/public/llms-template.txt +7 -0
  12. package/dist/public/spa.html +8 -0
  13. package/dist/public/views/companies.js +190 -0
  14. package/dist/public/views/members.js +42 -0
  15. package/dist/public/views/my-company.js +440 -47
  16. package/dist/public/views/portfolio-themes.js +77 -0
  17. package/dist/public/views/profile/agents-tab.js +4 -1
  18. package/dist/public/views/profile/discover-tab.js +118 -0
  19. package/dist/public/views/profile/inbox-tab.js +506 -12
  20. package/dist/public/views/profile/landing-page.js +2 -1
  21. package/dist/public/views/profile/notebook-card.js +11 -3
  22. package/dist/public/views/profile/organisms/home.js +5 -3
  23. package/dist/public/views/profile/organisms/mindmap.js +14 -3
  24. package/dist/public/views/profile/organisms/workspace.js +5 -2
  25. package/dist/public/views/profile/organisms-tab.js +12 -4
  26. package/dist/public/views/profile.js +2 -0
  27. package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
  28. package/dist/src/cli/connect/mcp/tools/core.js +34 -0
  29. package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
  30. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +1 -0
  31. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -1
  32. package/dist/src/cli/connect/mcp/tools/dm-messages.js +23 -0
  33. package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -1
  34. package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
  35. package/dist/src/cli/connect/mcp/tools/organisms.js +31 -0
  36. package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
  37. package/dist/src/cli/connect/mcp/tools/workspaces.d.ts.map +1 -1
  38. package/dist/src/cli/connect/mcp/tools/workspaces.js +4 -0
  39. package/dist/src/cli/connect/mcp/tools/workspaces.js.map +1 -1
  40. package/dist/src/generated/api-types.d.ts +211 -0
  41. package/dist/src/generated/api-types.d.ts.map +1 -1
  42. package/dist/src/generated/prisma-postgres/edge.js +5 -3
  43. package/dist/src/generated/prisma-postgres/index-browser.js +2 -0
  44. package/dist/src/generated/prisma-postgres/index.d.ts +89 -1
  45. package/dist/src/generated/prisma-postgres/index.js +5 -3
  46. package/dist/src/generated/prisma-postgres/package.json +1 -1
  47. package/dist/src/generated/prisma-postgres/schema.prisma +3 -0
  48. package/dist/src/generated/prisma-postgres/wasm.js +5 -3
  49. package/dist/src/mcp/annotations.d.ts.map +1 -1
  50. package/dist/src/mcp/annotations.js +1 -0
  51. package/dist/src/mcp/annotations.js.map +1 -1
  52. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  53. package/dist/src/mcp/catalog/definitions.js +18 -0
  54. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  55. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  56. package/dist/src/mcp/catalog/surfaces.js +6 -3
  57. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  58. package/dist/src/mcp/core.d.ts.map +1 -1
  59. package/dist/src/mcp/core.js +49 -0
  60. package/dist/src/mcp/core.js.map +1 -1
  61. package/dist/src/models/message-schemas.d.ts +56 -0
  62. package/dist/src/models/message-schemas.d.ts.map +1 -1
  63. package/dist/src/models/message-schemas.js +13 -0
  64. package/dist/src/models/message-schemas.js.map +1 -1
  65. package/dist/src/routes/bootstrap.d.ts.map +1 -1
  66. package/dist/src/routes/bootstrap.js +3 -0
  67. package/dist/src/routes/bootstrap.js.map +1 -1
  68. package/dist/src/routes/discover.d.ts +22 -0
  69. package/dist/src/routes/discover.d.ts.map +1 -0
  70. package/dist/src/routes/discover.js +123 -0
  71. package/dist/src/routes/discover.js.map +1 -0
  72. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  73. package/dist/src/routes/federation-sync.js +67 -2
  74. package/dist/src/routes/federation-sync.js.map +1 -1
  75. package/dist/src/routes/messages.d.ts.map +1 -1
  76. package/dist/src/routes/messages.js +86 -1
  77. package/dist/src/routes/messages.js.map +1 -1
  78. package/dist/src/routes/organisms.d.ts +3 -0
  79. package/dist/src/routes/organisms.d.ts.map +1 -1
  80. package/dist/src/routes/organisms.js +11 -32
  81. package/dist/src/routes/organisms.js.map +1 -1
  82. package/dist/src/routes/portal.d.ts.map +1 -1
  83. package/dist/src/routes/portal.js +2 -0
  84. package/dist/src/routes/portal.js.map +1 -1
  85. package/dist/src/routes/portfolio.d.ts.map +1 -1
  86. package/dist/src/routes/portfolio.js +30 -0
  87. package/dist/src/routes/portfolio.js.map +1 -1
  88. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  89. package/dist/src/server-bootstrap/routes-loader.js +2 -0
  90. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  91. package/dist/src/services/connect-tunnel.d.ts +3 -0
  92. package/dist/src/services/connect-tunnel.d.ts.map +1 -1
  93. package/dist/src/services/connect-tunnel.js +6 -0
  94. package/dist/src/services/connect-tunnel.js.map +1 -1
  95. package/dist/src/services/discovery/__tests__/discovery.test.d.ts +2 -0
  96. package/dist/src/services/discovery/__tests__/discovery.test.d.ts.map +1 -0
  97. package/dist/src/services/discovery/__tests__/discovery.test.js +178 -0
  98. package/dist/src/services/discovery/__tests__/discovery.test.js.map +1 -0
  99. package/dist/src/services/discovery/classify.d.ts +36 -0
  100. package/dist/src/services/discovery/classify.d.ts.map +1 -0
  101. package/dist/src/services/discovery/classify.js +61 -0
  102. package/dist/src/services/discovery/classify.js.map +1 -0
  103. package/dist/src/services/discovery/facets.d.ts +21 -0
  104. package/dist/src/services/discovery/facets.d.ts.map +1 -0
  105. package/dist/src/services/discovery/facets.js +0 -0
  106. package/dist/src/services/discovery/facets.js.map +1 -0
  107. package/dist/src/services/discovery/index.d.ts +22 -0
  108. package/dist/src/services/discovery/index.d.ts.map +1 -0
  109. package/dist/src/services/discovery/index.js +22 -0
  110. package/dist/src/services/discovery/index.js.map +1 -0
  111. package/dist/src/services/discovery/normalize.d.ts +25 -0
  112. package/dist/src/services/discovery/normalize.d.ts.map +1 -0
  113. package/dist/src/services/discovery/normalize.js +108 -0
  114. package/dist/src/services/discovery/normalize.js.map +1 -0
  115. package/dist/src/services/discovery/registry.d.ts +30 -0
  116. package/dist/src/services/discovery/registry.d.ts.map +1 -0
  117. package/dist/src/services/discovery/registry.js +31 -0
  118. package/dist/src/services/discovery/registry.js.map +1 -0
  119. package/dist/src/services/discovery/run.d.ts +23 -0
  120. package/dist/src/services/discovery/run.d.ts.map +1 -0
  121. package/dist/src/services/discovery/run.js +20 -0
  122. package/dist/src/services/discovery/run.js.map +1 -0
  123. package/dist/src/services/discovery/setup.d.ts +16 -0
  124. package/dist/src/services/discovery/setup.d.ts.map +1 -0
  125. package/dist/src/services/discovery/setup.js +12 -0
  126. package/dist/src/services/discovery/setup.js.map +1 -0
  127. package/dist/src/services/discovery/sources/memory-source.d.ts +24 -0
  128. package/dist/src/services/discovery/sources/memory-source.d.ts.map +1 -0
  129. package/dist/src/services/discovery/sources/memory-source.js +183 -0
  130. package/dist/src/services/discovery/sources/memory-source.js.map +1 -0
  131. package/dist/src/services/discovery/sources/table-sources.d.ts +27 -0
  132. package/dist/src/services/discovery/sources/table-sources.d.ts.map +1 -0
  133. package/dist/src/services/discovery/sources/table-sources.js +128 -0
  134. package/dist/src/services/discovery/sources/table-sources.js.map +1 -0
  135. package/dist/src/services/discovery/types.d.ts +117 -0
  136. package/dist/src/services/discovery/types.d.ts.map +1 -0
  137. package/dist/src/services/discovery/types.js +19 -0
  138. package/dist/src/services/discovery/types.js.map +1 -0
  139. package/dist/src/services/handbooks/agent.d.ts +1 -1
  140. package/dist/src/services/handbooks/agent.d.ts.map +1 -1
  141. package/dist/src/services/handbooks/agent.js +9 -3
  142. package/dist/src/services/handbooks/agent.js.map +1 -1
  143. package/dist/src/services/message-broadcast.d.ts +49 -0
  144. package/dist/src/services/message-broadcast.d.ts.map +1 -0
  145. package/dist/src/services/message-broadcast.js +116 -0
  146. package/dist/src/services/message-broadcast.js.map +1 -0
  147. package/dist/src/services/message-delivery.d.ts.map +1 -1
  148. package/dist/src/services/message-delivery.js +2 -0
  149. package/dist/src/services/message-delivery.js.map +1 -1
  150. package/dist/src/services/message-send.d.ts +6 -0
  151. package/dist/src/services/message-send.d.ts.map +1 -1
  152. package/dist/src/services/message-send.js +7 -5
  153. package/dist/src/services/message-send.js.map +1 -1
  154. package/dist/src/services/presence.d.ts +8 -0
  155. package/dist/src/services/presence.d.ts.map +1 -1
  156. package/dist/src/services/presence.js +38 -0
  157. package/dist/src/services/presence.js.map +1 -1
  158. package/dist/src/services/workspace-enrichment.d.ts +15 -5
  159. package/dist/src/services/workspace-enrichment.d.ts.map +1 -1
  160. package/dist/src/services/workspace-enrichment.js +52 -14
  161. package/dist/src/services/workspace-enrichment.js.map +1 -1
  162. package/dist/src/storage/interface.d.ts +5 -0
  163. package/dist/src/storage/interface.d.ts.map +1 -1
  164. package/dist/src/storage/providers/mongodb/index.d.ts +1 -0
  165. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  166. package/dist/src/storage/providers/mongodb/index.js +13 -0
  167. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  168. package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
  169. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  170. package/dist/src/storage/providers/sqlite/index.js +3 -0
  171. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  172. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +3 -0
  173. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
  174. package/dist/src/storage/providers/sqlite/repos/direct-message.js +13 -2
  175. package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
  176. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  177. package/dist/src/storage/providers/sqlite/schema.js +5 -0
  178. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  179. package/dist/src/storage/repositories/direct-message.repository.d.ts +3 -0
  180. package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
  181. package/package.json +1 -1
  182. package/prisma/schema.postgres.prisma +3 -0
  183. package/prisma/schema.prisma +3 -0
@@ -14,15 +14,19 @@
14
14
  * v0.1.0 โ€” 2026-06-23 โ€” initial (hand-typed)
15
15
  * v0.2.0 โ€” 2026-06-23 โ€” pick existing offerings; order โ†’ result/inbox
16
16
  * v0.2.1 โ€” 2026-06-23 โ€” order tracking (my orders + orders received) with live status
17
+ * v0.3.0 โ€” 2026-06-23 โ€” members & roles + customers + revenue-split receipt
18
+ * v0.4.0 โ€” 2026-06-23 โ€” company Settings tab (admin): name, Y-tunnus, description, commission %
17
19
  */
18
20
  import { h } from 'preact';
19
21
  import { useState, useEffect } from 'preact/hooks';
20
22
  import htm from 'htm';
21
23
  import { t } from '/js/i18n.js';
22
- import { apiGet, apiPost } from '/js/api.js';
24
+ import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '/js/api.js';
25
+ import { setOfferBilling } from '/js/services/offers.js';
23
26
  import { useViewCSS } from '/components/useViewCSS.js';
24
27
  import { DeliverableBody } from '/components/ImageDeliverable.js';
25
28
  import { OfferCardView } from '/components/offer-card-view.js';
29
+ import { THEMES, buildPortfolioData } from '/views/portfolio-themes.js';
26
30
 
27
31
  const html = htm.bind(h);
28
32
 
@@ -49,6 +53,7 @@ function OrderRow({ o, view }) {
49
53
  <span class="mc-mini">${new Date(o.createdAt).toLocaleString()}</span>
50
54
  </div>
51
55
  ${o.input && html`<div class="mc-order-input">${t('myCompany.orderedWhat')}: โ€œ${o.input}โ€</div>`}
56
+ ${o.charged > 0 && html`<div class="mc-order-receipt">๐Ÿงพ ${t('myCompany.receiptLabel')}: ${t('myCompany.charged').replace('{n}', o.charged)}${o.trackingCode ? html` ยท <span class="mc-mini">${o.trackingCode}</span>` : ''}</div>`}
52
57
  ${o.kind === 'result' && o.result != null && html`
53
58
  <div class="mc-result"><div class="mc-result-label">${t('myCompany.result')}</div><${DeliverableBody} value=${o.result} alt=${o.offerTitle} /></div>`}
54
59
  </li>`;
@@ -59,13 +64,307 @@ function OrdersList({ orders, view, empty }) {
59
64
  return html`<ul class="mc-order-list">${orders.map(o => html`<${OrderRow} key=${o.id} o=${o} view=${view} />`)}</ul>`;
60
65
  }
61
66
 
62
- /** A catalog entry: the SAME offer card format as profile>offers + an order box. */
63
- function OfferingCard({ o, orgOwner, slug, onOrdered }) {
67
+ /** Customers: who used the company's agents, which agents, how many times, total spend. */
68
+ function CustomersPanel({ slug }) {
69
+ const [customers, setCustomers] = useState(null);
70
+ useEffect(() => {
71
+ apiGet(`/v1/orgs/${encodeURIComponent(slug)}/customers`).then(r => setCustomers(r.data?.customers ?? [])).catch(() => setCustomers([]));
72
+ }, [slug]);
73
+ if (customers === null) return html`<div class="mc-center"><div class="spinner"></div></div>`;
74
+ if (!customers.length) return html`<p class="mc-empty">${t('myCompany.noCustomers')}</p>`;
75
+ return html`
76
+ <ul class="mc-cust-list">
77
+ ${customers.map(c => html`
78
+ <li class="mc-cust" key=${c.buyerOwner || c.buyerGhii}>
79
+ <div class="mc-cust-head">
80
+ <span class="mc-cust-name">${c.buyerOwner || c.buyerGhii}</span>
81
+ <span class="mc-cust-spend">${c.totalCharged} ${t('myCompany.morsels')}</span>
82
+ </div>
83
+ <div class="mc-cust-meta">${t('myCompany.orderCount')}: ${c.orderCount} ยท ${t('myCompany.lastOrder')}: ${new Date(c.lastOrderAt).toLocaleDateString()}</div>
84
+ <div class="mc-off-meta">${Object.entries(c.agents).map(([a, n]) => html`<span class="mc-chip" key=${a}>๐Ÿค– ${a} ร—${n}</span>`)}</div>
85
+ </li>`)}
86
+ </ul>`;
87
+ }
88
+
89
+ /** Members & roles: list members, invite by username, set role, remove (admins manage). */
90
+ function MembersPanel({ slug, creatorOwner, viewerRole }) {
91
+ const canManage = viewerRole === 'admin';
92
+ const [members, setMembers] = useState(null);
93
+ const [owner, setOwner] = useState('');
94
+ const [role, setRole] = useState('member');
95
+ const [busy, setBusy] = useState(false);
96
+ const [err, setErr] = useState('');
97
+ async function load() { try { const r = await apiGet(`/v1/orgs/${encodeURIComponent(slug)}/members`); setMembers(r.data?.members ?? []); } catch { setMembers([]); } }
98
+ useEffect(() => { load(); }, [slug]);
99
+ async function add(e) {
100
+ e.preventDefault(); setBusy(true); setErr('');
101
+ try { await apiPost(`/v1/orgs/${encodeURIComponent(slug)}/members`, { owner: owner.trim().toLowerCase(), role }); setOwner(''); await load(); }
102
+ catch (e) { setErr(e.message || 'Failed'); } finally { setBusy(false); }
103
+ }
104
+ async function changeRole(m, r) { try { await apiPatch(`/v1/orgs/${encodeURIComponent(slug)}/members/${encodeURIComponent(m.owner)}`, { role: r }); await load(); } catch (e) { setErr(e.message || ''); } }
105
+ async function remove(m) { try { await apiDelete(`/v1/orgs/${encodeURIComponent(slug)}/members/${encodeURIComponent(m.owner)}`); await load(); } catch (e) { setErr(e.message || ''); } }
106
+ const roleOpts = html`
107
+ <option value="admin">${t('myCompany.roleAdmin')}</option>
108
+ <option value="member">${t('myCompany.roleMember')}</option>
109
+ <option value="visitor">${t('myCompany.roleVisitor')}</option>`;
110
+ if (members === null) return html`<div class="mc-center"><div class="spinner"></div></div>`;
111
+ return html`
112
+ <ul class="mc-member-list">
113
+ ${members.map(m => {
114
+ const isCreator = m.owner === creatorOwner;
115
+ return html`
116
+ <li class="mc-member" key=${m.owner}>
117
+ <span class="mc-member-name">${m.owner}${isCreator ? html` <span class="mc-mini">(${t('myCompany.creator')})</span>` : ''}</span>
118
+ <div class="flex-row-wrap">
119
+ <select class="input-field input-sm" value=${m.role} disabled=${isCreator || !canManage} onChange=${(e) => changeRole(m, e.target.value)}>${roleOpts}</select>
120
+ ${canManage && !isCreator && html`<button class="btn-ghost btn-sm mc-remove" onClick=${() => remove(m)}>${t('myCompany.removeMember')}</button>`}
121
+ </div>
122
+ </li>`;
123
+ })}
124
+ </ul>
125
+ ${err && html`<p class="mc-error">${err}</p>`}
126
+ ${canManage && html`
127
+ <form class="mc-form" onSubmit=${add}>
128
+ <h3 class="mc-form-title">${t('myCompany.addMember')}</h3>
129
+ <div class="flex-row-wrap mc-manage-row">
130
+ <input class="input-field input-sm" placeholder=${t('myCompany.memberUsername')} value=${owner} onInput=${(e) => setOwner(e.target.value)} required />
131
+ <select class="input-field input-sm" value=${role} onChange=${(e) => setRole(e.target.value)}>${roleOpts}</select>
132
+ <button class="btn-primary btn-sm" type="submit" disabled=${busy}>${t('myCompany.addMemberBtn')}</button>
133
+ </div>
134
+ </form>`}`;
135
+ }
136
+
137
+ /** Company settings (admin only): logo, name, Y-tunnus, description, commission, directory visibility. */
138
+ function SettingsPanel({ org, onSaved }) {
139
+ const [name, setName] = useState(org.name || '');
140
+ const [businessId, setBusinessId] = useState(org.businessId || '');
141
+ const [description, setDescription] = useState(org.description || '');
142
+ const [commission, setCommission] = useState(org.commissionPercent ?? 20);
143
+ const [logo, setLogo] = useState(org.logo || '');
144
+ const [listed, setListed] = useState(org.listed !== false);
145
+ const [uploading, setUploading] = useState(false);
146
+ const [busy, setBusy] = useState(false);
147
+ const [err, setErr] = useState('');
148
+ const [ok, setOk] = useState(false);
149
+
150
+ // Upload the logo to public storage (under the admin's GAII), then store its public URL on the
151
+ // company. Reuses the generic /v1/storage endpoint โ€” the backend stays protocol-only.
152
+ async function uploadLogo(file) {
153
+ if (!file) return;
154
+ if (file.size > 1024 * 1024) { setErr(t('myCompany.logoTooBig')); return; }
155
+ setUploading(true); setErr(''); setOk(false);
156
+ try {
157
+ const b64 = await new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(String(r.result).split(',')[1]); r.onerror = rej; r.readAsDataURL(file); });
158
+ const ext = (file.type.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '');
159
+ const key = `org-${org.slug}-logo-${Date.now()}.${ext}`;
160
+ await apiPost('/v1/storage', { key, visibility: 'public', mime_type: file.type || 'image/png', data: b64 });
161
+ const ghii = window.AIMEAT?.auth?.getSession?.()?.ghii;
162
+ setLogo(`/v1/pub/${encodeURIComponent(ghii)}/${encodeURIComponent(key)}`);
163
+ } catch (e) { setErr(e.message || 'Upload failed'); }
164
+ finally { setUploading(false); }
165
+ }
166
+
167
+ async function save(e) {
168
+ e.preventDefault(); setBusy(true); setErr(''); setOk(false);
169
+ try {
170
+ const r = await apiPatch(`/v1/orgs/${encodeURIComponent(org.slug)}`, {
171
+ name: name.trim(), businessId: businessId.trim(), description: description.trim(),
172
+ commissionPercent: Number(commission), logo: logo || null, listed,
173
+ });
174
+ setOk(true);
175
+ if (onSaved) await onSaved(r.data?.org);
176
+ } catch (e) { setErr(e.message || 'Save failed'); }
177
+ finally { setBusy(false); }
178
+ }
179
+ return html`
180
+ <form class="mc-form mc-settings" onSubmit=${save}>
181
+ <div class="mc-label">${t('myCompany.logo')}
182
+ <div class="mc-logo-row">
183
+ ${logo
184
+ ? html`<img class="mc-logo-preview" src=${logo} alt=${t('myCompany.logo')} />`
185
+ : html`<div class="mc-logo-empty">${t('myCompany.noLogo')}</div>`}
186
+ <div class="flex-row-wrap">
187
+ <label class="btn-outline btn-sm mc-upload-btn">
188
+ ${uploading ? t('myCompany.uploading') : t('myCompany.uploadLogo')}
189
+ <input type="file" accept="image/*" class="mc-file-hidden" disabled=${uploading} onChange=${(e) => uploadLogo(e.target.files?.[0])} />
190
+ </label>
191
+ ${logo && html`<button type="button" class="btn-ghost btn-sm" onClick=${() => { setLogo(''); setOk(false); }}>${t('myCompany.removeLogo')}</button>`}
192
+ </div>
193
+ </div>
194
+ </div>
195
+ <label class="mc-label">${t('myCompany.name')}
196
+ <input class="mc-input" value=${name} onInput=${(e) => { setName(e.target.value); setOk(false); }} required /></label>
197
+ <label class="mc-label">${t('myCompany.businessId')}
198
+ <input class="mc-input" value=${businessId} placeholder="1234567-8" onInput=${(e) => { setBusinessId(e.target.value); setOk(false); }} /></label>
199
+ <span class="mc-mini">${t('myCompany.businessIdHint')}</span>
200
+ <label class="mc-label">${t('myCompany.description')}
201
+ <textarea class="mc-input" rows="3" value=${description} onInput=${(e) => { setDescription(e.target.value); setOk(false); }}></textarea></label>
202
+ <label class="mc-label">${t('myCompany.commission')}
203
+ <input class="mc-input mc-price-input" type="number" min="0" max="100" value=${commission} onInput=${(e) => { setCommission(e.target.value); setOk(false); }} /></label>
204
+ <span class="mc-mini">${t('myCompany.commissionHint')}</span>
205
+ <label class="mc-checkbox-row">
206
+ <input type="checkbox" checked=${listed} onChange=${(e) => { setListed(e.target.checked); setOk(false); }} />
207
+ <span>${t('myCompany.listedLabel')}</span>
208
+ </label>
209
+ <span class="mc-mini">${t('myCompany.listedHint')}</span>
210
+ ${err && html`<p class="mc-error">${err}</p>`}
211
+ ${ok && html`<p class="mc-saved">${t('myCompany.settingsSaved')}</p>`}
212
+ <button class="btn-primary" type="submit" disabled=${busy || uploading}>${t('myCompany.saveSettings')}</button>
213
+ </form>`;
214
+ }
215
+
216
+ /** Compose the prompt the user takes to their AI to generate the portfolio HTML (prompt-driven). */
217
+ function buildPortfolioPrompt(org, offerings, c) {
218
+ const origin = (typeof window !== 'undefined' && window.location.origin) || '';
219
+ const lines = [];
220
+ lines.push(`Build a single self-contained HTML file: a professional, responsive public portfolio / landing page for the company "${org.name}" on the AIMEAT platform.`);
221
+ lines.push('Requirements: ONE .html file, all CSS inline in a <style> tag, no external JS/CSS/font dependencies (system fonts only), mobile-friendly, accessible, tasteful.');
222
+ if (c.includeInfo) {
223
+ lines.push('', 'Company information to feature:', `- Name: ${org.name}`);
224
+ if (org.businessId) lines.push(`- Business ID (Y-tunnus): ${org.businessId}`);
225
+ if (org.description) lines.push(`- About: ${org.description}`);
226
+ }
227
+ if (c.useLogo && org.logo) lines.push(`- Logo image (use it in the header): ${origin}${org.logo}`);
228
+ if (c.includeOfferings && offerings.length) {
229
+ lines.push('', 'Services for sale (present them attractively as cards, include each price):');
230
+ for (const o of offerings) {
231
+ const price = o.offer?.price?.morsels ? `${o.offer.price.morsels} morsels` : 'contact for pricing';
232
+ lines.push(`- ${o.offer?.title} โ€” ${price} (fulfilled by agent ${o.agentName})${o.offer?.ask ? `: ${o.offer.ask}` : ''}`);
233
+ }
234
+ }
235
+ lines.push('', `Desired look & feel: ${(c.lookDescription || '').trim() || 'clean, modern, trustworthy, professional'}`);
236
+ lines.push('', 'Output ONLY the HTML, starting with <!DOCTYPE html>. No explanations, no markdown fences.');
237
+ return lines.join('\n');
238
+ }
239
+
240
+ /** Company portfolio builder (admin) โ€” prompt-driven, mirrors the profile portfolio with company focus. */
241
+ function PortfolioPanel({ org, offerings }) {
242
+ const [cfg, setCfg] = useState(undefined); // undefined=loading, null=none, object
243
+ const [includeOfferings, setIncludeOfferings] = useState(true);
244
+ const [includeInfo, setIncludeInfo] = useState(true);
245
+ const [useLogo, setUseLogo] = useState(!!org.logo);
246
+ const [look, setLook] = useState('');
247
+ const [themeId, setThemeId] = useState('minimal');
248
+ const [prompt, setPrompt] = useState('');
249
+ const [htmlIn, setHtmlIn] = useState('');
250
+ const [busy, setBusy] = useState(false);
251
+ const [err, setErr] = useState('');
252
+ const [msg, setMsg] = useState('');
253
+
254
+ useEffect(() => {
255
+ apiGet(`/v1/orgs/${encodeURIComponent(org.slug)}/portfolio/config`).then(r => {
256
+ const c = r.data?.config; setCfg(c || null);
257
+ if (c) { setIncludeOfferings(c.includeOfferings !== false); setIncludeInfo(c.includeInfo !== false); setUseLogo(c.useLogo !== false && !!org.logo); setLook(c.lookDescription || ''); if (c.themeId) setThemeId(c.themeId); }
258
+ }).catch(() => setCfg(null));
259
+ }, [org.slug]);
260
+
261
+ const choices = () => ({ includeOfferings, includeInfo, useLogo, lookDescription: look });
262
+ function renderTheme() {
263
+ const theme = THEMES.find(x => x.id === themeId) || THEMES[0];
264
+ return theme.render(buildPortfolioData(org, offerings, choices()));
265
+ }
266
+ function previewTheme() {
267
+ const url = URL.createObjectURL(new Blob([renderTheme()], { type: 'text/html' }));
268
+ window.open(url, '_blank');
269
+ setTimeout(() => URL.revokeObjectURL(url), 60000);
270
+ }
271
+ async function publishTheme() {
272
+ setBusy(true); setErr(''); setMsg('');
273
+ try {
274
+ const r = await apiPut(`/v1/orgs/${encodeURIComponent(org.slug)}/portfolio/publish`, { html: renderTheme() });
275
+ apiPut(`/v1/orgs/${encodeURIComponent(org.slug)}/portfolio/config`, { ...choices(), themeId }).catch(() => {});
276
+ setCfg(r.data?.portfolio); setMsg(t('myCompany.pfPublished'));
277
+ } catch (e) { setErr(e.message || 'Publish failed'); }
278
+ finally { setBusy(false); }
279
+ }
280
+
281
+ function generate() {
282
+ setPrompt(buildPortfolioPrompt(org, offerings, { includeOfferings, includeInfo, useLogo, lookDescription: look }));
283
+ apiPut(`/v1/orgs/${encodeURIComponent(org.slug)}/portfolio/config`, { includeOfferings, includeInfo, useLogo, lookDescription: look }).catch(() => {});
284
+ }
285
+ async function copyPrompt() { try { await navigator.clipboard.writeText(prompt); setMsg(t('myCompany.pfCopied')); } catch { /* clipboard blocked */ } }
286
+ async function publish() {
287
+ if (!htmlIn.trim()) { setErr(t('myCompany.pfNeedHtml')); return; }
288
+ setBusy(true); setErr(''); setMsg('');
289
+ try { const r = await apiPut(`/v1/orgs/${encodeURIComponent(org.slug)}/portfolio/publish`, { html: htmlIn }); setCfg(r.data?.portfolio); setMsg(t('myCompany.pfPublished')); setHtmlIn(''); }
290
+ catch (e) { setErr(e.message || 'Publish failed'); }
291
+ finally { setBusy(false); }
292
+ }
293
+ async function unpublish() {
294
+ setBusy(true); setErr('');
295
+ try { const r = await apiDelete(`/v1/orgs/${encodeURIComponent(org.slug)}/portfolio`); setCfg(r.data?.portfolio); }
296
+ catch (e) { setErr(e.message || ''); }
297
+ finally { setBusy(false); }
298
+ }
299
+
300
+ if (cfg === undefined) return html`<div class="mc-center"><div class="spinner"></div></div>`;
301
+ const published = cfg?.enabled && cfg?.publicUrl;
302
+
303
+ return html`
304
+ <div class="mc-portfolio">
305
+ <p class="section-desc">${t('myCompany.pfIntro')}</p>
306
+ ${published && html`
307
+ <div class="mc-pf-live">
308
+ <span class="mc-saved">${t('myCompany.pfLive')}</span>
309
+ <div class="flex-row-wrap">
310
+ <a class="btn-outline btn-sm" href=${cfg.publicUrl} target="_blank">${t('myCompany.pfVisit')}</a>
311
+ <button class="btn-ghost btn-sm mc-remove" disabled=${busy} onClick=${unpublish}>${t('myCompany.pfUnpublish')}</button>
312
+ </div>
313
+ ${cfg.publishedAt && html`<span class="mc-mini">${t('myCompany.pfLastPublished')}: ${new Date(cfg.publishedAt).toLocaleString()}${cfg.htmlSizeKb ? ` ยท ${cfg.htmlSizeKb} KB` : ''}</span>`}
314
+ </div>`}
315
+
316
+ <h3 class="mc-form-title">${t('myCompany.pfStep1')}</h3>
317
+ <label class="mc-checkbox-row"><input type="checkbox" checked=${includeInfo} onChange=${e => setIncludeInfo(e.target.checked)} /><span>${t('myCompany.pfIncludeInfo')}</span></label>
318
+ <label class="mc-checkbox-row"><input type="checkbox" checked=${includeOfferings} onChange=${e => setIncludeOfferings(e.target.checked)} /><span>${t('myCompany.pfIncludeOfferings')} (${offerings.length})</span></label>
319
+ <label class="mc-checkbox-row"><input type="checkbox" checked=${useLogo} disabled=${!org.logo} onChange=${e => setUseLogo(e.target.checked)} /><span>${t('myCompany.pfUseLogo')}${!org.logo ? ` โ€” ${t('myCompany.pfNoLogo')}` : ''}</span></label>
320
+
321
+ <h3 class="mc-form-title">${t('myCompany.pfThemeHeading')}</h3>
322
+ <p class="mc-mini">${t('myCompany.pfThemeHint')}</p>
323
+ <div class="mc-theme-row">
324
+ ${THEMES.map(th => html`<button type="button" key=${th.id} class="mc-theme-btn ${themeId === th.id ? 'active' : ''}" onClick=${() => setThemeId(th.id)}>${th.name}</button>`)}
325
+ </div>
326
+ <div class="flex-row-wrap">
327
+ <button type="button" class="btn-outline btn-sm" onClick=${previewTheme}>${t('myCompany.pfPreview')}</button>
328
+ <button type="button" class="btn-primary btn-sm" disabled=${busy} onClick=${publishTheme}>${t('myCompany.pfPublishTheme')}</button>
329
+ </div>
330
+
331
+ <h3 class="mc-form-title">${t('myCompany.pfOrAi')}</h3>
332
+ <label class="mc-label">${t('myCompany.pfLook')}
333
+ <textarea class="mc-input" rows="3" value=${look} placeholder=${t('myCompany.pfLookPlaceholder')} onInput=${e => setLook(e.target.value)}></textarea></label>
334
+ <button type="button" class="btn-outline btn-sm" onClick=${generate}>${t('myCompany.pfGenerate')}</button>
335
+
336
+ ${prompt && html`
337
+ <h3 class="mc-form-title">${t('myCompany.pfStep2')}</h3>
338
+ <p class="mc-mini">${t('myCompany.pfStep2Hint')}</p>
339
+ <textarea class="mc-input mc-pf-prompt" rows="8" readonly>${prompt}</textarea>
340
+ <button class="btn-outline btn-sm" onClick=${copyPrompt}>${t('myCompany.pfCopyPrompt')}</button>
341
+
342
+ <h3 class="mc-form-title">${t('myCompany.pfStep3')}</h3>
343
+ <textarea class="mc-input mc-pf-html" rows="6" value=${htmlIn} placeholder="<!DOCTYPE html>โ€ฆ" onInput=${e => setHtmlIn(e.target.value)}></textarea>
344
+ <div><button class="btn-primary btn-sm" disabled=${busy} onClick=${publish}>${t('myCompany.pfPublish')}</button></div>`}
345
+
346
+ ${err && html`<p class="mc-error">${err}</p>`}
347
+ ${msg && html`<p class="mc-saved">${msg}</p>`}
348
+ </div>`;
349
+ }
350
+
351
+ /** A catalog entry: the SAME offer card format as profile>offers + owner controls + an order box. */
352
+ function OfferingCard({ o, orgOwner, slug, onOrdered, onChanged, callerOwner, role }) {
64
353
  const offer = o.offer;
354
+ const isOwn = (o.agentOwner ?? orgOwner) === callerOwner; // I own the underlying agent/offering
355
+ const isAdmin = role === 'admin';
65
356
  const [input, setInput] = useState('');
66
357
  const [busy, setBusy] = useState(false);
67
358
  const [result, setResult] = useState(null);
68
359
  const [err, setErr] = useState('');
360
+ // Controls โ€” own offering: edit price/visibility; admin/own: remove from catalog.
361
+ const [editMode, setEditMode] = useState(false);
362
+ const [price, setPrice] = useState(offer.price?.morsels ?? 0);
363
+ const [vis, setVis] = useState(offer.visibility ?? 'private');
364
+ const [saving, setSaving] = useState(false);
365
+
366
+ const visLabel = (v) => t('myCompany.vis' + (v || 'private').charAt(0).toUpperCase() + (v || 'private').slice(1));
367
+ const ownerOf = (ghii) => String(ghii || '').split('@')[0];
69
368
 
70
369
  async function order() {
71
370
  setBusy(true); setErr(''); setResult(null);
@@ -78,7 +377,65 @@ function OfferingCard({ o, orgOwner, slug, onOrdered }) {
78
377
  finally { setBusy(false); }
79
378
  }
80
379
 
380
+ async function saveBilling() {
381
+ setSaving(true); setErr('');
382
+ try {
383
+ const p = Number(price) > 0 ? { morsels: Math.floor(Number(price)), unit: 'per-call' } : null;
384
+ await setOfferBilling(o.agentName, o.offerId, { price: p, visibility: vis });
385
+ // record who last edited the company's listing (surfaced as "last saved by")
386
+ await apiPatch(`/v1/orgs/${encodeURIComponent(slug)}/offerings`, { agentName: o.agentName, offerId: o.offerId }).catch(() => {});
387
+ setEditMode(false);
388
+ if (onChanged) await onChanged();
389
+ } catch (e) { setErr(e.message || 'Save failed'); }
390
+ finally { setSaving(false); }
391
+ }
392
+
393
+ async function removeFromCatalog() {
394
+ setSaving(true); setErr('');
395
+ try {
396
+ await apiDelete(`/v1/orgs/${encodeURIComponent(slug)}/offerings`, { agentName: o.agentName, offerId: o.offerId });
397
+ if (onChanged) await onChanged();
398
+ } catch (e) { setErr(e.message || 'Remove failed'); setSaving(false); }
399
+ }
400
+
401
+ const manage = editMode
402
+ ? html`
403
+ <div class="mc-manage">
404
+ <span class="of-label">${t('myCompany.manage')}</span>
405
+ <div class="flex-row-wrap mc-manage-row">
406
+ <select class="input-field input-sm" value=${vis} onChange=${(e) => setVis(e.target.value)}>
407
+ <option value="private">${t('myCompany.visPrivate')}</option>
408
+ <option value="unlisted">${t('myCompany.visUnlisted')}</option>
409
+ <option value="public">${t('myCompany.visPublic')}</option>
410
+ </select>
411
+ <input class="input-field input-sm mc-price-input" type="number" min="0" value=${price} onInput=${(e) => setPrice(e.target.value)} />
412
+ <span class="mc-mini">${t('myCompany.morsels')}</span>
413
+ <button class="btn-primary btn-sm" disabled=${saving} onClick=${saveBilling}>${t('myCompany.savePrice')}</button>
414
+ <button class="btn-ghost btn-sm" disabled=${saving} onClick=${() => { setEditMode(false); setPrice(offer.price?.morsels ?? 0); setVis(offer.visibility ?? 'private'); }}>${t('myCompany.cancel')}</button>
415
+ </div>
416
+ ${vis === 'private' && html`<div class="mc-mini mc-warn">${t('myCompany.privateHint')}</div>`}
417
+ </div>`
418
+ : html`
419
+ <div class="mc-manage">
420
+ <div class="mc-manage-head">
421
+ <span class="of-label">${t('myCompany.manage')}</span>
422
+ <div class="flex-row-wrap">
423
+ ${isOwn && html`<button class="btn-outline btn-sm" onClick=${() => setEditMode(true)}>${t('myCompany.edit')}</button>`}
424
+ ${(isAdmin || isOwn) && html`<button class="btn-ghost btn-sm mc-remove" disabled=${saving} onClick=${removeFromCatalog}>${t('myCompany.removeFromCatalog')}</button>`}
425
+ </div>
426
+ </div>
427
+ <div class="mc-config">
428
+ ${!isOwn && html`<span class="mc-mini">${t('myCompany.byMember')}: <b>${o.agentOwner}</b></span>`}
429
+ <span><b>${t('myCompany.visibilityLabel')}:</b> ${visLabel(offer.visibility)}</span>
430
+ <span><b>${t('myCompany.priceLabel')}:</b> ${offer.price?.morsels ? `${offer.price.morsels} ${t('myCompany.morsels')}` : t('myCompany.notForSale')}</span>
431
+ ${o.lastEditedBy
432
+ ? html`<span class="mc-mini">${t('myCompany.lastSavedBy')}: <b>${ownerOf(o.lastEditedBy)}</b>${o.lastEditedAt ? ` ยท ${new Date(o.lastEditedAt).toLocaleString()}` : ''}</span>`
433
+ : html`<span class="mc-mini">${t('myCompany.neverEdited')}</span>`}
434
+ </div>
435
+ </div>`;
436
+
81
437
  const actions = html`
438
+ ${manage}
82
439
  <div class="mc-not-callable">${o.callable ? t('myCompany.callable') : t('myCompany.viaInbox')}</div>
83
440
  <textarea class="input-field input-sm of-input" rows="2" value=${input}
84
441
  placeholder=${t('myCompany.usePlaceholder')} onInput=${(e) => setInput(e.target.value)}></textarea>
@@ -91,7 +448,7 @@ function OfferingCard({ o, orgOwner, slug, onOrdered }) {
91
448
  ${result.kind === 'result'
92
449
  ? html`<div class="mc-result-msg">${t('myCompany.orderPlacedResult')}</div><div class="mc-result-label">${t('myCompany.result')}</div><${DeliverableBody} value=${result.result} alt=${offer.title} format=${offer.deliverable?.format} />`
93
450
  : html`<div class="mc-result-msg">${t('myCompany.orderPlacedTask').replace('{agent}', o.agentName)}</div>`}
94
- ${result.receipt?.charged > 0 && html`<div class="mc-receipt">${t('myCompany.charged').replace('{n}', result.receipt.charged)} โ†’ ${t('myCompany.wallet')} +${result.receipt.toOrgWallet}</div>`}
451
+ ${result.receipt?.charged > 0 && html`<div class="mc-receipt">${t('myCompany.charged').replace('{n}', result.receipt.charged)} โ†’ ${t('myCompany.byMember')} +${result.receipt.memberCut ?? 0} ยท ${t('myCompany.wallet')} +${result.receipt.orgCut ?? result.receipt.toOrgWallet ?? 0}</div>`}
95
452
  </div>`}
96
453
  `;
97
454
 
@@ -99,43 +456,54 @@ function OfferingCard({ o, orgOwner, slug, onOrdered }) {
99
456
  }
100
457
 
101
458
  function OfferPicker({ slug, alreadyListed, onListed }) {
102
- const [myOffers, setMyOffers] = useState(null);
459
+ const [agents, setAgents] = useState(null); // [{ name, offers: [] }]
103
460
  const [busy, setBusy] = useState('');
104
461
  useEffect(() => {
105
- apiGet('/v1/offers').then(r => {
106
- const flat = [];
107
- for (const a of (r.data?.agents ?? [])) for (const off of (a.offers ?? [])) flat.push({ agentName: a.agent, offer: off });
108
- setMyOffers(flat);
109
- }).catch(() => setMyOffers([]));
462
+ Promise.all([apiGet('/v1/offers').catch(() => ({})), apiGet('/v1/agents').catch(() => ({}))]).then(([offR, agR]) => {
463
+ const offersByAgent = {};
464
+ for (const a of (offR.data?.agents ?? [])) offersByAgent[a.agent] = a.offers ?? [];
465
+ const names = (agR.data?.agents ?? agR.data ?? []).map(a => a.name || String(a.gaii || '').split('#')[0]).filter(Boolean);
466
+ for (const k of Object.keys(offersByAgent)) if (!names.includes(k)) names.push(k);
467
+ setAgents(names.map(name => ({ name, offers: offersByAgent[name] ?? [] })));
468
+ });
110
469
  }, []);
111
- async function list(agentName, offer) {
112
- setBusy(agentName + '/' + offer.id);
113
- try { await apiPost(`/v1/orgs/${encodeURIComponent(slug)}/offerings`, { agentName, offerId: offer.id }); await onListed(); }
470
+ async function list(agentName, offerId) {
471
+ setBusy(agentName + '/' + offerId);
472
+ try { await apiPost(`/v1/orgs/${encodeURIComponent(slug)}/offerings`, { agentName, offerId }); await onListed(); }
114
473
  catch (e) { alert(e.message || 'Failed'); }
115
474
  finally { setBusy(''); }
116
475
  }
117
- if (myOffers === null) return html`<div class="mc-center"><div class="spinner"></div></div>`;
118
- if (myOffers.length === 0) return html`<p class="mc-empty">${t('myCompany.noOffersToList')}</p>`;
476
+ if (agents === null) return html`<div class="mc-center"><div class="spinner"></div></div>`;
477
+ if (agents.length === 0) return html`<p class="mc-empty">${t('myCompany.noOffersToList')}</p>`;
119
478
  return html`
120
- <ul class="mc-pick-list">
121
- ${myOffers.map(({ agentName, offer }) => {
122
- const key = agentName + '/' + offer.id;
123
- const listed = alreadyListed.has(key);
124
- return html`
125
- <li class="mc-pick-item" key=${key}>
126
- <div>
127
- <div class="mc-off-title">${offer.title}</div>
128
- <div class="mc-off-meta"><span class="mc-chip">๐Ÿค– ${agentName}</span>${offer.price?.morsels ? html`<span class="mc-chip">${offer.price.morsels} ${t('myCompany.morsels')}</span>` : null}${offer.deliverable?.format ? html`<span class="mc-chip">๐Ÿ“ฆ ${offer.deliverable.format}</span>` : null}</div>
129
- </div>
130
- ${listed ? html`<span class="mc-listed">${t('myCompany.listed')}</span>`
131
- : html`<button class="btn-outline" disabled=${busy === key} onClick=${() => list(agentName, offer)}>${t('myCompany.putUpForSale')}</button>`}
132
- </li>`;
133
- })}
134
- </ul>`;
479
+ <div class="mc-pick-agents">
480
+ ${agents.map(ag => html`
481
+ <div class="mc-pick-agent" key=${ag.name}>
482
+ <div class="mc-pick-agent-name">๐Ÿค– ${ag.name}</div>
483
+ ${ag.offers.length === 0
484
+ ? html`<div class="mc-mini mc-pick-none">${t('myCompany.agentNoOfferings')}</div>`
485
+ : html`<ul class="mc-pick-list">
486
+ ${ag.offers.map(offer => {
487
+ const key = ag.name + '/' + offer.id;
488
+ const listed = alreadyListed.has(key);
489
+ return html`
490
+ <li class="mc-pick-item" key=${key}>
491
+ <div>
492
+ <div class="mc-off-title">${offer.title}</div>
493
+ <div class="mc-off-meta">${offer.price?.morsels ? html`<span class="mc-chip">${offer.price.morsels} ${t('myCompany.morsels')}</span>` : null}${offer.deliverable?.format ? html`<span class="mc-chip">๐Ÿ“ฆ ${offer.deliverable.format}</span>` : null}<span class="mc-chip">${offer.visibility || 'private'}</span></div>
494
+ </div>
495
+ ${listed ? html`<span class="mc-listed">${t('myCompany.listed')}</span>`
496
+ : html`<button class="btn-outline btn-sm" disabled=${busy === key} onClick=${() => list(ag.name, offer.id)}>${t('myCompany.putUpForSale')}</button>`}
497
+ </li>`;
498
+ })}
499
+ </ul>`}
500
+ </div>`)}
501
+ </div>`;
135
502
  }
136
503
 
137
504
  export default function MyCompanyView() {
138
505
  useViewCSS('/css/views/my-company.css');
506
+ const callerOwner = (typeof window !== 'undefined' && window.AIMEAT?.auth?.getSession?.()?.owner) || '';
139
507
 
140
508
  const [state, setState] = useState('loading');
141
509
  const [orgs, setOrgs] = useState([]);
@@ -143,11 +511,13 @@ export default function MyCompanyView() {
143
511
  const [offerings, setOfferings] = useState([]);
144
512
  const [ordersReceived, setOrdersReceived] = useState([]);
145
513
  const [showPicker, setShowPicker] = useState(false);
514
+ const [subTab, setSubTab] = useState('catalog');
146
515
  const [myOrders, setMyOrders] = useState(null);
147
516
  const [showMyOrders, setShowMyOrders] = useState(false);
148
517
  const [errMsg, setErrMsg] = useState('');
149
518
  const [newSlug, setNewSlug] = useState('');
150
519
  const [newName, setNewName] = useState('');
520
+ const [newBusinessId, setNewBusinessId] = useState('');
151
521
  const [busy, setBusy] = useState(false);
152
522
 
153
523
  async function loadOrgs() {
@@ -166,7 +536,7 @@ export default function MyCompanyView() {
166
536
  catch { setOrdersReceived([]); }
167
537
  }
168
538
  async function openOrg(org) {
169
- setSelected(org); setOfferings([]); setOrdersReceived([]); setShowPicker(false);
539
+ setSelected(org); setOfferings([]); setOrdersReceived([]); setShowPicker(false); setSubTab('catalog');
170
540
  try {
171
541
  const res = await apiGet(`/v1/orgs/${encodeURIComponent(org.creatorOwner)}/${encodeURIComponent(org.slug)}/offerings`);
172
542
  setOfferings(res.data?.offerings ?? []);
@@ -189,8 +559,8 @@ export default function MyCompanyView() {
189
559
  async function createOrg(e) {
190
560
  e.preventDefault(); setErrMsg(''); setBusy(true);
191
561
  try {
192
- const res = await apiPost('/v1/orgs', { slug: newSlug.trim().toLowerCase(), name: newName.trim() });
193
- setNewSlug(''); setNewName(''); await loadOrgs();
562
+ const res = await apiPost('/v1/orgs', { slug: newSlug.trim().toLowerCase(), name: newName.trim(), businessId: newBusinessId.trim() || undefined });
563
+ setNewSlug(''); setNewName(''); setNewBusinessId(''); await loadOrgs();
194
564
  if (res.data?.org) await openOrg(res.data.org);
195
565
  } catch (e) { setErrMsg(e.message || 'Create failed'); }
196
566
  finally { setBusy(false); }
@@ -238,6 +608,8 @@ export default function MyCompanyView() {
238
608
  <input class="mc-input" value=${newSlug} placeholder="overscale" onInput=${(e) => setNewSlug(e.target.value)} required /></label>
239
609
  <label class="mc-label">${t('myCompany.name')}
240
610
  <input class="mc-input" value=${newName} placeholder="Overscale Solutions Oy" onInput=${(e) => setNewName(e.target.value)} required /></label>
611
+ <label class="mc-label">${t('myCompany.businessIdOptional')}
612
+ <input class="mc-input" value=${newBusinessId} placeholder="1234567-8" onInput=${(e) => setNewBusinessId(e.target.value)} /></label>
241
613
  <button class="btn-primary" type="submit" disabled=${busy}>${t('myCompany.create')}</button>
242
614
  </form>
243
615
  </aside>
@@ -246,24 +618,45 @@ export default function MyCompanyView() {
246
618
  ${!selected && html`<p class="mc-empty">${t('myCompany.selectHint')}</p>`}
247
619
  ${selected && html`
248
620
  <div class="mc-detail-head">
249
- <h2 class="mc-col-title">${selected.name}</h2>
621
+ <div class="mc-head-id">
622
+ ${selected.logo && html`<img class="mc-head-logo" src=${selected.logo} alt=${selected.name} />`}
623
+ <div>
624
+ <h2 class="mc-col-title">${selected.name}${selected.listed === false ? html` <span class="mc-mini mc-warn">ยท ${t('myCompany.unlistedBadge')}</span>` : ''}</h2>
625
+ ${selected.businessId
626
+ ? html`<span class="mc-mini">${t('myCompany.businessId')}: ${selected.businessId}</span>`
627
+ : (selected.role === 'admin' ? html`<span class="mc-mini mc-warn">${t('myCompany.noBusinessId')}</span>` : null)}
628
+ </div>
629
+ </div>
250
630
  <span class="mc-wallet">${t('myCompany.wallet')}: <strong>${selected.wallet?.balance ?? 0}</strong> ${t('myCompany.morsels')}</span>
251
631
  </div>
252
632
 
253
- <div class="mc-section-head">
254
- <h3 class="mc-form-title">${t('myCompany.offerings')}</h3>
255
- <button class="btn-outline btn-sm" onClick=${() => setShowPicker(s => !s)}>${showPicker ? t('myCompany.done') : t('myCompany.addFromOffers')}</button>
256
- </div>
257
- ${showPicker && html`<div class="mc-picker"><p class="mc-pick-hint">${t('myCompany.pickHint')}</p><${OfferPicker} slug=${selected.slug} alreadyListed=${listedKeys} onListed=${() => openOrg(selected)} /></div>`}
258
- ${offerings.length === 0 && !showPicker && html`<p class="mc-empty">${t('myCompany.noOfferings')}</p>`}
259
- <ul class="mc-off-list">
260
- ${offerings.map(o => html`<${OfferingCard} key=${o.agentName + '/' + o.offerId} o=${o} orgOwner=${selected.creatorOwner} slug=${selected.slug} onOrdered=${afterOrder} />`)}
261
- </ul>
262
-
263
- <div class="mc-orders-received">
264
- <h3 class="mc-form-title">${t('myCompany.ordersReceived')}</h3>
265
- <${OrdersList} orders=${ordersReceived} view="owner" empty=${t('myCompany.noOrdersReceived')} />
633
+ <div class="mc-subtabs">
634
+ ${['catalog', ...(selected.role === 'admin' ? ['orders', 'customers', 'portfolio', 'settings'] : []), 'members'].map(id => html`
635
+ <button class="mc-subtab ${subTab === id ? 'active' : ''}" key=${id} onClick=${() => setSubTab(id)}>${t('myCompany.tab' + id.charAt(0).toUpperCase() + id.slice(1))}</button>`)}
636
+ <span class="mc-role-chip">${t('myCompany.role' + (selected.role || 'member').charAt(0).toUpperCase() + (selected.role || 'member').slice(1))}</span>
266
637
  </div>
638
+
639
+ ${subTab === 'catalog' && html`
640
+ <div class="mc-section-head">
641
+ <h3 class="mc-form-title">${t('myCompany.offerings')}</h3>
642
+ <button class="btn-outline btn-sm" onClick=${() => setShowPicker(s => !s)}>${showPicker ? t('myCompany.done') : t('myCompany.addFromOffers')}</button>
643
+ </div>
644
+ ${showPicker && html`<div class="mc-picker"><p class="mc-pick-hint">${t('myCompany.pickHint')}</p><${OfferPicker} slug=${selected.slug} alreadyListed=${listedKeys} onListed=${() => openOrg(selected)} /></div>`}
645
+ ${offerings.length === 0 && !showPicker && html`<p class="mc-empty">${t('myCompany.noOfferings')}</p>`}
646
+ <ul class="mc-off-list">
647
+ ${offerings.map(o => html`<${OfferingCard} key=${o.agentName + '/' + o.offerId} o=${o} orgOwner=${selected.creatorOwner} slug=${selected.slug} callerOwner=${callerOwner} role=${selected.role} onOrdered=${afterOrder} onChanged=${() => openOrg(selected)} />`)}
648
+ </ul>`}
649
+
650
+ ${subTab === 'orders' && html`<${OrdersList} orders=${ordersReceived} view="owner" empty=${t('myCompany.noOrdersReceived')} />`}
651
+ ${subTab === 'customers' && html`<${CustomersPanel} slug=${selected.slug} />`}
652
+ ${subTab === 'portfolio' && html`<${PortfolioPanel} org=${selected} offerings=${offerings} />`}
653
+ ${subTab === 'settings' && html`<${SettingsPanel} org=${selected} onSaved=${(updated) => {
654
+ // Update in place (no loadOrgs โ†’ no loading flash that would remount the panel and drop the "saved โœ“").
655
+ if (!updated) return;
656
+ setSelected(s => ({ ...s, ...updated }));
657
+ setOrgs(list => list.map(o => (o.slug === updated.slug && o.creatorOwner === updated.creatorOwner) ? { ...o, ...updated } : o));
658
+ }} />`}
659
+ ${subTab === 'members' && html`<${MembersPanel} slug=${selected.slug} creatorOwner=${selected.creatorOwner} viewerRole=${selected.role} />`}
267
660
  `}
268
661
  </section>
269
662
  </div>