aimeat 1.30.2 → 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 (192) hide show
  1. package/dist/locales/en.json +246 -1
  2. package/dist/locales/fi.json +246 -1
  3. package/dist/public/components/InboxLink.js +29 -0
  4. package/dist/public/components/offer-card-view.js +87 -0
  5. package/dist/public/css/views/companies.css +65 -0
  6. package/dist/public/css/views/inbox.css +153 -1
  7. package/dist/public/css/views/members.css +33 -0
  8. package/dist/public/css/views/my-company.css +315 -0
  9. package/dist/public/css/views/profile.css +22 -0
  10. package/dist/public/js/services/messages.js +21 -3
  11. package/dist/public/js/services/notebook.js +16 -6
  12. package/dist/public/llms-template.txt +7 -0
  13. package/dist/public/spa.html +14 -0
  14. package/dist/public/views/companies.js +190 -0
  15. package/dist/public/views/members.js +42 -0
  16. package/dist/public/views/my-company.js +664 -0
  17. package/dist/public/views/portfolio-themes.js +77 -0
  18. package/dist/public/views/profile/agents-tab.js +4 -1
  19. package/dist/public/views/profile/discover-tab.js +118 -0
  20. package/dist/public/views/profile/inbox-tab.js +663 -16
  21. package/dist/public/views/profile/landing-page.js +2 -1
  22. package/dist/public/views/profile/notebook-card.js +11 -3
  23. package/dist/public/views/profile/offers-tab.js +5 -24
  24. package/dist/public/views/profile/organisms/home.js +5 -3
  25. package/dist/public/views/profile/organisms/mindmap.js +14 -3
  26. package/dist/public/views/profile/organisms/workspace.js +5 -2
  27. package/dist/public/views/profile/organisms-tab.js +12 -4
  28. package/dist/public/views/profile.js +2 -0
  29. package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
  30. package/dist/src/cli/connect/mcp/tools/core.js +34 -0
  31. package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
  32. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +1 -0
  33. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -1
  34. package/dist/src/cli/connect/mcp/tools/dm-messages.js +23 -0
  35. package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -1
  36. package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
  37. package/dist/src/cli/connect/mcp/tools/organisms.js +31 -0
  38. package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
  39. package/dist/src/cli/connect/mcp/tools/workspaces.d.ts.map +1 -1
  40. package/dist/src/cli/connect/mcp/tools/workspaces.js +4 -0
  41. package/dist/src/cli/connect/mcp/tools/workspaces.js.map +1 -1
  42. package/dist/src/generated/api-types.d.ts +249 -0
  43. package/dist/src/generated/api-types.d.ts.map +1 -1
  44. package/dist/src/generated/prisma-postgres/edge.js +6 -3
  45. package/dist/src/generated/prisma-postgres/index-browser.js +3 -0
  46. package/dist/src/generated/prisma-postgres/index.d.ts +112 -1
  47. package/dist/src/generated/prisma-postgres/index.js +6 -3
  48. package/dist/src/generated/prisma-postgres/package.json +1 -1
  49. package/dist/src/generated/prisma-postgres/schema.prisma +4 -0
  50. package/dist/src/generated/prisma-postgres/wasm.js +6 -3
  51. package/dist/src/mcp/annotations.d.ts.map +1 -1
  52. package/dist/src/mcp/annotations.js +2 -0
  53. package/dist/src/mcp/annotations.js.map +1 -1
  54. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  55. package/dist/src/mcp/catalog/definitions.js +33 -1
  56. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  57. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  58. package/dist/src/mcp/catalog/scopes.js +1 -0
  59. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  60. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  61. package/dist/src/mcp/catalog/surfaces.js +7 -4
  62. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  63. package/dist/src/mcp/core.d.ts.map +1 -1
  64. package/dist/src/mcp/core.js +49 -0
  65. package/dist/src/mcp/core.js.map +1 -1
  66. package/dist/src/mcp/dm-messages.d.ts +2 -0
  67. package/dist/src/mcp/dm-messages.d.ts.map +1 -1
  68. package/dist/src/mcp/dm-messages.js +45 -1
  69. package/dist/src/mcp/dm-messages.js.map +1 -1
  70. package/dist/src/models/message-schemas.d.ts +140 -2
  71. package/dist/src/models/message-schemas.d.ts.map +1 -1
  72. package/dist/src/models/message-schemas.js +60 -3
  73. package/dist/src/models/message-schemas.js.map +1 -1
  74. package/dist/src/routes/bootstrap.d.ts.map +1 -1
  75. package/dist/src/routes/bootstrap.js +3 -0
  76. package/dist/src/routes/bootstrap.js.map +1 -1
  77. package/dist/src/routes/discover.d.ts +22 -0
  78. package/dist/src/routes/discover.d.ts.map +1 -0
  79. package/dist/src/routes/discover.js +123 -0
  80. package/dist/src/routes/discover.js.map +1 -0
  81. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  82. package/dist/src/routes/federation-sync.js +69 -2
  83. package/dist/src/routes/federation-sync.js.map +1 -1
  84. package/dist/src/routes/messages.d.ts.map +1 -1
  85. package/dist/src/routes/messages.js +102 -3
  86. package/dist/src/routes/messages.js.map +1 -1
  87. package/dist/src/routes/organisms.d.ts +3 -0
  88. package/dist/src/routes/organisms.d.ts.map +1 -1
  89. package/dist/src/routes/organisms.js +11 -32
  90. package/dist/src/routes/organisms.js.map +1 -1
  91. package/dist/src/routes/portal.d.ts.map +1 -1
  92. package/dist/src/routes/portal.js +3 -0
  93. package/dist/src/routes/portal.js.map +1 -1
  94. package/dist/src/routes/portfolio.d.ts.map +1 -1
  95. package/dist/src/routes/portfolio.js +30 -0
  96. package/dist/src/routes/portfolio.js.map +1 -1
  97. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  98. package/dist/src/server-bootstrap/routes-loader.js +2 -0
  99. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  100. package/dist/src/services/connect-tunnel.d.ts +3 -0
  101. package/dist/src/services/connect-tunnel.d.ts.map +1 -1
  102. package/dist/src/services/connect-tunnel.js +6 -0
  103. package/dist/src/services/connect-tunnel.js.map +1 -1
  104. package/dist/src/services/discovery/__tests__/discovery.test.d.ts +2 -0
  105. package/dist/src/services/discovery/__tests__/discovery.test.d.ts.map +1 -0
  106. package/dist/src/services/discovery/__tests__/discovery.test.js +178 -0
  107. package/dist/src/services/discovery/__tests__/discovery.test.js.map +1 -0
  108. package/dist/src/services/discovery/classify.d.ts +36 -0
  109. package/dist/src/services/discovery/classify.d.ts.map +1 -0
  110. package/dist/src/services/discovery/classify.js +61 -0
  111. package/dist/src/services/discovery/classify.js.map +1 -0
  112. package/dist/src/services/discovery/facets.d.ts +21 -0
  113. package/dist/src/services/discovery/facets.d.ts.map +1 -0
  114. package/dist/src/services/discovery/facets.js +0 -0
  115. package/dist/src/services/discovery/facets.js.map +1 -0
  116. package/dist/src/services/discovery/index.d.ts +22 -0
  117. package/dist/src/services/discovery/index.d.ts.map +1 -0
  118. package/dist/src/services/discovery/index.js +22 -0
  119. package/dist/src/services/discovery/index.js.map +1 -0
  120. package/dist/src/services/discovery/normalize.d.ts +25 -0
  121. package/dist/src/services/discovery/normalize.d.ts.map +1 -0
  122. package/dist/src/services/discovery/normalize.js +108 -0
  123. package/dist/src/services/discovery/normalize.js.map +1 -0
  124. package/dist/src/services/discovery/registry.d.ts +30 -0
  125. package/dist/src/services/discovery/registry.d.ts.map +1 -0
  126. package/dist/src/services/discovery/registry.js +31 -0
  127. package/dist/src/services/discovery/registry.js.map +1 -0
  128. package/dist/src/services/discovery/run.d.ts +23 -0
  129. package/dist/src/services/discovery/run.d.ts.map +1 -0
  130. package/dist/src/services/discovery/run.js +20 -0
  131. package/dist/src/services/discovery/run.js.map +1 -0
  132. package/dist/src/services/discovery/setup.d.ts +16 -0
  133. package/dist/src/services/discovery/setup.d.ts.map +1 -0
  134. package/dist/src/services/discovery/setup.js +12 -0
  135. package/dist/src/services/discovery/setup.js.map +1 -0
  136. package/dist/src/services/discovery/sources/memory-source.d.ts +24 -0
  137. package/dist/src/services/discovery/sources/memory-source.d.ts.map +1 -0
  138. package/dist/src/services/discovery/sources/memory-source.js +183 -0
  139. package/dist/src/services/discovery/sources/memory-source.js.map +1 -0
  140. package/dist/src/services/discovery/sources/table-sources.d.ts +27 -0
  141. package/dist/src/services/discovery/sources/table-sources.d.ts.map +1 -0
  142. package/dist/src/services/discovery/sources/table-sources.js +128 -0
  143. package/dist/src/services/discovery/sources/table-sources.js.map +1 -0
  144. package/dist/src/services/discovery/types.d.ts +117 -0
  145. package/dist/src/services/discovery/types.d.ts.map +1 -0
  146. package/dist/src/services/discovery/types.js +19 -0
  147. package/dist/src/services/discovery/types.js.map +1 -0
  148. package/dist/src/services/handbooks/agent.d.ts +1 -1
  149. package/dist/src/services/handbooks/agent.d.ts.map +1 -1
  150. package/dist/src/services/handbooks/agent.js +9 -3
  151. package/dist/src/services/handbooks/agent.js.map +1 -1
  152. package/dist/src/services/message-broadcast.d.ts +49 -0
  153. package/dist/src/services/message-broadcast.d.ts.map +1 -0
  154. package/dist/src/services/message-broadcast.js +116 -0
  155. package/dist/src/services/message-broadcast.js.map +1 -0
  156. package/dist/src/services/message-delivery.d.ts.map +1 -1
  157. package/dist/src/services/message-delivery.js +3 -0
  158. package/dist/src/services/message-delivery.js.map +1 -1
  159. package/dist/src/services/message-send.d.ts +9 -1
  160. package/dist/src/services/message-send.d.ts.map +1 -1
  161. package/dist/src/services/message-send.js +11 -5
  162. package/dist/src/services/message-send.js.map +1 -1
  163. package/dist/src/services/presence.d.ts +8 -0
  164. package/dist/src/services/presence.d.ts.map +1 -1
  165. package/dist/src/services/presence.js +38 -0
  166. package/dist/src/services/presence.js.map +1 -1
  167. package/dist/src/services/workspace-enrichment.d.ts +15 -5
  168. package/dist/src/services/workspace-enrichment.d.ts.map +1 -1
  169. package/dist/src/services/workspace-enrichment.js +52 -14
  170. package/dist/src/services/workspace-enrichment.js.map +1 -1
  171. package/dist/src/storage/interface.d.ts +49 -0
  172. package/dist/src/storage/interface.d.ts.map +1 -1
  173. package/dist/src/storage/providers/mongodb/index.d.ts +1 -0
  174. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  175. package/dist/src/storage/providers/mongodb/index.js +16 -0
  176. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  177. package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
  178. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  179. package/dist/src/storage/providers/sqlite/index.js +3 -0
  180. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  181. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +3 -0
  182. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
  183. package/dist/src/storage/providers/sqlite/repos/direct-message.js +15 -2
  184. package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
  185. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  186. package/dist/src/storage/providers/sqlite/schema.js +10 -0
  187. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  188. package/dist/src/storage/repositories/direct-message.repository.d.ts +3 -0
  189. package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
  190. package/package.json +1 -1
  191. package/prisma/schema.postgres.prisma +4 -0
  192. package/prisma/schema.prisma +4 -0
@@ -0,0 +1,664 @@
1
+ /**
2
+ * @file my-company.js
3
+ * @description "Yritykseni" (My Company) view — the Enterprise surface. An owner creates a company
4
+ * and lists their agents' EXISTING offerings for sale (picked, not typed; specs from
5
+ * agents.{name}.offers). Anyone can order a listed offering like profile>offers, fulfilled by the
6
+ * seller's agent: callable offerings invoke the seller's capability and return the result inline;
7
+ * task-runner offerings queue a task on the seller's agent. Every order is tracked: the buyer sees
8
+ * "My orders" (status + result), the company owner sees "Orders received". Calls the proprietary
9
+ * ee/ module's /v1/orgs + /v1/orders API; a Community node returns ENTERPRISE_REQUIRED.
10
+ * @structure MyCompanyView · OfferPicker (list your offerings to sell) · OfferingCard (specs + order)
11
+ * · OrdersList (orders received / my orders, with live status) · statusChip helper.
12
+ * @usage routed at /v1/my-company; header tab "Yritykseni".
13
+ * @version-history
14
+ * v0.1.0 — 2026-06-23 — initial (hand-typed)
15
+ * v0.2.0 — 2026-06-23 — pick existing offerings; order → result/inbox
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 %
19
+ */
20
+ import { h } from 'preact';
21
+ import { useState, useEffect } from 'preact/hooks';
22
+ import htm from 'htm';
23
+ import { t } from '/js/i18n.js';
24
+ import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '/js/api.js';
25
+ import { setOfferBilling } from '/js/services/offers.js';
26
+ import { useViewCSS } from '/components/useViewCSS.js';
27
+ import { DeliverableBody } from '/components/ImageDeliverable.js';
28
+ import { OfferCardView } from '/components/offer-card-view.js';
29
+ import { THEMES, buildPortfolioData } from '/views/portfolio-themes.js';
30
+
31
+ const html = htm.bind(h);
32
+
33
+ function statusChip(o) {
34
+ const s = o.kind === 'result' ? 'instant' : (o.status || 'queued');
35
+ const map = { queued: ['stQueued', 'queued'], active: ['stActive', 'active'], done: ['stDone', 'done'], failed: ['stFailed', 'failed'], instant: ['stInstant', 'done'] };
36
+ const [key, cls] = map[s] || ['stQueued', 'queued'];
37
+ return html`<span class="mc-status mc-status--${cls}">${t('myCompany.' + key)}</span>`;
38
+ }
39
+
40
+ /** One order row, in either the owner ("Orders received") or buyer ("My orders") list. */
41
+ function OrderRow({ o, view }) {
42
+ return html`
43
+ <li class="mc-order">
44
+ <div class="mc-order-head">
45
+ <span class="mc-order-title">${o.offerTitle}</span>
46
+ ${statusChip(o)}
47
+ </div>
48
+ <div class="mc-order-meta">
49
+ ${view === 'owner'
50
+ ? html`<span>${t('myCompany.orderedBy')}: ${o.buyerOwner}</span>`
51
+ : html`<span>${t('myCompany.fromCompany')}: ${o.orgSlug}</span>`}
52
+ <span>🤖 ${o.agentName}</span>
53
+ <span class="mc-mini">${new Date(o.createdAt).toLocaleString()}</span>
54
+ </div>
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>`}
57
+ ${o.kind === 'result' && o.result != null && html`
58
+ <div class="mc-result"><div class="mc-result-label">${t('myCompany.result')}</div><${DeliverableBody} value=${o.result} alt=${o.offerTitle} /></div>`}
59
+ </li>`;
60
+ }
61
+
62
+ function OrdersList({ orders, view, empty }) {
63
+ if (!orders || orders.length === 0) return html`<p class="mc-empty">${empty}</p>`;
64
+ return html`<ul class="mc-order-list">${orders.map(o => html`<${OrderRow} key=${o.id} o=${o} view=${view} />`)}</ul>`;
65
+ }
66
+
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 }) {
353
+ const offer = o.offer;
354
+ const isOwn = (o.agentOwner ?? orgOwner) === callerOwner; // I own the underlying agent/offering
355
+ const isAdmin = role === 'admin';
356
+ const [input, setInput] = useState('');
357
+ const [busy, setBusy] = useState(false);
358
+ const [result, setResult] = useState(null);
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];
368
+
369
+ async function order() {
370
+ setBusy(true); setErr(''); setResult(null);
371
+ try {
372
+ const res = await apiPost(`/v1/orgs/${encodeURIComponent(orgOwner)}/${encodeURIComponent(slug)}/use`,
373
+ { agentName: o.agentName, offerId: o.offerId, input: input.trim() || undefined });
374
+ setResult(res.data); setInput('');
375
+ if (onOrdered) await onOrdered();
376
+ } catch (e) { setErr(e.message || 'Order failed'); }
377
+ finally { setBusy(false); }
378
+ }
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
+
437
+ const actions = html`
438
+ ${manage}
439
+ <div class="mc-not-callable">${o.callable ? t('myCompany.callable') : t('myCompany.viaInbox')}</div>
440
+ <textarea class="input-field input-sm of-input" rows="2" value=${input}
441
+ placeholder=${t('myCompany.usePlaceholder')} onInput=${(e) => setInput(e.target.value)}></textarea>
442
+ <div class="flex-row-wrap">
443
+ <button class="btn-primary btn-sm" disabled=${busy} onClick=${order}>${t('myCompany.use')}</button>
444
+ </div>
445
+ ${err && html`<p class="mc-error">${err}</p>`}
446
+ ${result && html`
447
+ <div class="mc-result">
448
+ ${result.kind === 'result'
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} />`
450
+ : html`<div class="mc-result-msg">${t('myCompany.orderPlacedTask').replace('{agent}', o.agentName)}</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>`}
452
+ </div>`}
453
+ `;
454
+
455
+ return html`<li class="mc-cat-item"><${OfferCardView} entry=${{ agent: o.agentName, online: o.online }} offer=${offer} actions=${actions} /></li>`;
456
+ }
457
+
458
+ function OfferPicker({ slug, alreadyListed, onListed }) {
459
+ const [agents, setAgents] = useState(null); // [{ name, offers: [] }]
460
+ const [busy, setBusy] = useState('');
461
+ useEffect(() => {
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
+ });
469
+ }, []);
470
+ async function list(agentName, offerId) {
471
+ setBusy(agentName + '/' + offerId);
472
+ try { await apiPost(`/v1/orgs/${encodeURIComponent(slug)}/offerings`, { agentName, offerId }); await onListed(); }
473
+ catch (e) { alert(e.message || 'Failed'); }
474
+ finally { setBusy(''); }
475
+ }
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>`;
478
+ return html`
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>`;
502
+ }
503
+
504
+ export default function MyCompanyView() {
505
+ useViewCSS('/css/views/my-company.css');
506
+ const callerOwner = (typeof window !== 'undefined' && window.AIMEAT?.auth?.getSession?.()?.owner) || '';
507
+
508
+ const [state, setState] = useState('loading');
509
+ const [orgs, setOrgs] = useState([]);
510
+ const [selected, setSelected] = useState(null);
511
+ const [offerings, setOfferings] = useState([]);
512
+ const [ordersReceived, setOrdersReceived] = useState([]);
513
+ const [showPicker, setShowPicker] = useState(false);
514
+ const [subTab, setSubTab] = useState('catalog');
515
+ const [myOrders, setMyOrders] = useState(null);
516
+ const [showMyOrders, setShowMyOrders] = useState(false);
517
+ const [errMsg, setErrMsg] = useState('');
518
+ const [newSlug, setNewSlug] = useState('');
519
+ const [newName, setNewName] = useState('');
520
+ const [newBusinessId, setNewBusinessId] = useState('');
521
+ const [busy, setBusy] = useState(false);
522
+
523
+ async function loadOrgs() {
524
+ setState('loading');
525
+ try { const res = await apiGet('/v1/orgs'); setOrgs(res.data?.orgs ?? []); setState('ready'); }
526
+ catch (e) {
527
+ if (e.code === 'ENTERPRISE_REQUIRED') setState('enterprise');
528
+ else if (e.status === 401) setState('unauth');
529
+ else { setErrMsg(e.message || 'Failed to load'); setState('error'); }
530
+ }
531
+ }
532
+ useEffect(() => { loadOrgs(); }, []);
533
+
534
+ async function loadOrders(org) {
535
+ try { const r = await apiGet(`/v1/orgs/${encodeURIComponent(org.slug)}/orders`); setOrdersReceived(r.data?.orders ?? []); }
536
+ catch { setOrdersReceived([]); }
537
+ }
538
+ async function openOrg(org) {
539
+ setSelected(org); setOfferings([]); setOrdersReceived([]); setShowPicker(false); setSubTab('catalog');
540
+ try {
541
+ const res = await apiGet(`/v1/orgs/${encodeURIComponent(org.creatorOwner)}/${encodeURIComponent(org.slug)}/offerings`);
542
+ setOfferings(res.data?.offerings ?? []);
543
+ } catch (e) { setErrMsg(e.message || ''); }
544
+ loadOrders(org);
545
+ }
546
+ async function afterOrder() {
547
+ if (selected) { await loadOrders(selected); await loadOrgs(); }
548
+ if (showMyOrders) await loadMyOrders();
549
+ }
550
+ async function loadMyOrders() {
551
+ try { const r = await apiGet('/v1/orders'); setMyOrders(r.data?.orders ?? []); }
552
+ catch { setMyOrders([]); }
553
+ }
554
+ function toggleMyOrders() {
555
+ const next = !showMyOrders; setShowMyOrders(next);
556
+ if (next) loadMyOrders();
557
+ }
558
+
559
+ async function createOrg(e) {
560
+ e.preventDefault(); setErrMsg(''); setBusy(true);
561
+ try {
562
+ const res = await apiPost('/v1/orgs', { slug: newSlug.trim().toLowerCase(), name: newName.trim(), businessId: newBusinessId.trim() || undefined });
563
+ setNewSlug(''); setNewName(''); setNewBusinessId(''); await loadOrgs();
564
+ if (res.data?.org) await openOrg(res.data.org);
565
+ } catch (e) { setErrMsg(e.message || 'Create failed'); }
566
+ finally { setBusy(false); }
567
+ }
568
+
569
+ if (state === 'loading') return html`<div class="mc-container"><div class="mc-center"><div class="spinner"></div></div></div>`;
570
+ if (state === 'enterprise') return html`<div class="mc-container"><div class="mc-card mc-stub"><h1 class="mc-title">${t('myCompany.title')}</h1><p class="mc-stub-badge">${t('myCompany.enterpriseBadge')}</p><p class="mc-desc">${t('myCompany.enterpriseRequired')}</p></div></div>`;
571
+ if (state === 'unauth') return html`<div class="mc-container"><div class="mc-card"><h1 class="mc-title">${t('myCompany.title')}</h1><p class="mc-desc">${t('myCompany.loginRequired')}</p></div></div>`;
572
+ if (state === 'error') return html`<div class="mc-container"><div class="mc-card"><h1 class="mc-title">${t('myCompany.title')}</h1><p class="mc-error">${errMsg}</p></div></div>`;
573
+
574
+ const listedKeys = new Set(offerings.map(o => o.agentName + '/' + o.offerId));
575
+
576
+ return html`
577
+ <div class="mc-container">
578
+ <header class="mc-header">
579
+ <div class="mc-header-row">
580
+ <h1 class="mc-title">${t('myCompany.title')}</h1>
581
+ <button class="btn-outline btn-sm" onClick=${toggleMyOrders}>${showMyOrders ? t('myCompany.done') : t('myCompany.myOrders')}</button>
582
+ </div>
583
+ <p class="mc-desc">${t('myCompany.desc')}</p>
584
+ </header>
585
+ ${errMsg && html`<p class="mc-error">${errMsg}</p>`}
586
+
587
+ ${showMyOrders && html`
588
+ <div class="mc-card">
589
+ <h2 class="mc-col-title">${t('myCompany.myOrders')}</h2>
590
+ ${myOrders === null ? html`<div class="mc-center"><div class="spinner"></div></div>`
591
+ : html`<${OrdersList} orders=${myOrders} view="buyer" empty=${t('myCompany.noMyOrders')} />`}
592
+ </div>`}
593
+
594
+ <div class="mc-grid">
595
+ <aside class="mc-col">
596
+ <h2 class="mc-col-title">${t('myCompany.yourCompanies')}</h2>
597
+ ${orgs.length === 0 && html`<p class="mc-empty">${t('myCompany.noCompanies')}</p>`}
598
+ <ul class="mc-org-list">
599
+ ${orgs.map(org => html`<li key=${org.slug}>
600
+ <button class="mc-org-item ${selected?.slug === org.slug ? 'active' : ''}" onClick=${() => openOrg(org)}>
601
+ <span class="mc-org-name">${org.name}</span>
602
+ <span class="mc-org-meta">${org.goii} · ${(org.wallet?.balance ?? 0)} ${t('myCompany.morsels')}</span>
603
+ </button></li>`)}
604
+ </ul>
605
+ <form class="mc-form" onSubmit=${createOrg}>
606
+ <h3 class="mc-form-title">${t('myCompany.createCompany')}</h3>
607
+ <label class="mc-label">${t('myCompany.slug')}
608
+ <input class="mc-input" value=${newSlug} placeholder="overscale" onInput=${(e) => setNewSlug(e.target.value)} required /></label>
609
+ <label class="mc-label">${t('myCompany.name')}
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>
613
+ <button class="btn-primary" type="submit" disabled=${busy}>${t('myCompany.create')}</button>
614
+ </form>
615
+ </aside>
616
+
617
+ <section class="mc-col">
618
+ ${!selected && html`<p class="mc-empty">${t('myCompany.selectHint')}</p>`}
619
+ ${selected && html`
620
+ <div class="mc-detail-head">
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>
630
+ <span class="mc-wallet">${t('myCompany.wallet')}: <strong>${selected.wallet?.balance ?? 0}</strong> ${t('myCompany.morsels')}</span>
631
+ </div>
632
+
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>
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} />`}
660
+ `}
661
+ </section>
662
+ </div>
663
+ </div>`;
664
+ }