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.
- package/dist/locales/en.json +196 -2
- package/dist/locales/fi.json +196 -2
- package/dist/public/components/InboxLink.js +29 -0
- package/dist/public/css/views/companies.css +65 -0
- package/dist/public/css/views/inbox.css +100 -1
- package/dist/public/css/views/members.css +33 -0
- package/dist/public/css/views/my-company.css +52 -0
- package/dist/public/css/views/profile.css +22 -0
- package/dist/public/js/services/messages.js +16 -0
- package/dist/public/js/services/notebook.js +16 -6
- package/dist/public/llms-template.txt +7 -0
- package/dist/public/spa.html +8 -0
- package/dist/public/views/companies.js +190 -0
- package/dist/public/views/members.js +42 -0
- package/dist/public/views/my-company.js +440 -47
- package/dist/public/views/portfolio-themes.js +77 -0
- package/dist/public/views/profile/agents-tab.js +4 -1
- package/dist/public/views/profile/discover-tab.js +118 -0
- package/dist/public/views/profile/inbox-tab.js +506 -12
- package/dist/public/views/profile/landing-page.js +2 -1
- package/dist/public/views/profile/notebook-card.js +11 -3
- package/dist/public/views/profile/organisms/home.js +5 -3
- package/dist/public/views/profile/organisms/mindmap.js +14 -3
- package/dist/public/views/profile/organisms/workspace.js +5 -2
- package/dist/public/views/profile/organisms-tab.js +12 -4
- package/dist/public/views/profile.js +2 -0
- package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.js +34 -0
- package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +1 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.js +23 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js +31 -0
- package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.js +4 -0
- package/dist/src/cli/connect/mcp/tools/workspaces.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +211 -0
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +5 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +2 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +89 -1
- package/dist/src/generated/prisma-postgres/index.js +5 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +3 -0
- package/dist/src/generated/prisma-postgres/wasm.js +5 -3
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +1 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +18 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +6 -3
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/core.d.ts.map +1 -1
- package/dist/src/mcp/core.js +49 -0
- package/dist/src/mcp/core.js.map +1 -1
- package/dist/src/models/message-schemas.d.ts +56 -0
- package/dist/src/models/message-schemas.d.ts.map +1 -1
- package/dist/src/models/message-schemas.js +13 -0
- package/dist/src/models/message-schemas.js.map +1 -1
- package/dist/src/routes/bootstrap.d.ts.map +1 -1
- package/dist/src/routes/bootstrap.js +3 -0
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/discover.d.ts +22 -0
- package/dist/src/routes/discover.d.ts.map +1 -0
- package/dist/src/routes/discover.js +123 -0
- package/dist/src/routes/discover.js.map +1 -0
- package/dist/src/routes/federation-sync.d.ts.map +1 -1
- package/dist/src/routes/federation-sync.js +67 -2
- package/dist/src/routes/federation-sync.js.map +1 -1
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +86 -1
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/organisms.d.ts +3 -0
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +11 -32
- package/dist/src/routes/organisms.js.map +1 -1
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +2 -0
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/portfolio.d.ts.map +1 -1
- package/dist/src/routes/portfolio.js +30 -0
- package/dist/src/routes/portfolio.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +2 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/connect-tunnel.d.ts +3 -0
- package/dist/src/services/connect-tunnel.d.ts.map +1 -1
- package/dist/src/services/connect-tunnel.js +6 -0
- package/dist/src/services/connect-tunnel.js.map +1 -1
- package/dist/src/services/discovery/__tests__/discovery.test.d.ts +2 -0
- package/dist/src/services/discovery/__tests__/discovery.test.d.ts.map +1 -0
- package/dist/src/services/discovery/__tests__/discovery.test.js +178 -0
- package/dist/src/services/discovery/__tests__/discovery.test.js.map +1 -0
- package/dist/src/services/discovery/classify.d.ts +36 -0
- package/dist/src/services/discovery/classify.d.ts.map +1 -0
- package/dist/src/services/discovery/classify.js +61 -0
- package/dist/src/services/discovery/classify.js.map +1 -0
- package/dist/src/services/discovery/facets.d.ts +21 -0
- package/dist/src/services/discovery/facets.d.ts.map +1 -0
- package/dist/src/services/discovery/facets.js +0 -0
- package/dist/src/services/discovery/facets.js.map +1 -0
- package/dist/src/services/discovery/index.d.ts +22 -0
- package/dist/src/services/discovery/index.d.ts.map +1 -0
- package/dist/src/services/discovery/index.js +22 -0
- package/dist/src/services/discovery/index.js.map +1 -0
- package/dist/src/services/discovery/normalize.d.ts +25 -0
- package/dist/src/services/discovery/normalize.d.ts.map +1 -0
- package/dist/src/services/discovery/normalize.js +108 -0
- package/dist/src/services/discovery/normalize.js.map +1 -0
- package/dist/src/services/discovery/registry.d.ts +30 -0
- package/dist/src/services/discovery/registry.d.ts.map +1 -0
- package/dist/src/services/discovery/registry.js +31 -0
- package/dist/src/services/discovery/registry.js.map +1 -0
- package/dist/src/services/discovery/run.d.ts +23 -0
- package/dist/src/services/discovery/run.d.ts.map +1 -0
- package/dist/src/services/discovery/run.js +20 -0
- package/dist/src/services/discovery/run.js.map +1 -0
- package/dist/src/services/discovery/setup.d.ts +16 -0
- package/dist/src/services/discovery/setup.d.ts.map +1 -0
- package/dist/src/services/discovery/setup.js +12 -0
- package/dist/src/services/discovery/setup.js.map +1 -0
- package/dist/src/services/discovery/sources/memory-source.d.ts +24 -0
- package/dist/src/services/discovery/sources/memory-source.d.ts.map +1 -0
- package/dist/src/services/discovery/sources/memory-source.js +183 -0
- package/dist/src/services/discovery/sources/memory-source.js.map +1 -0
- package/dist/src/services/discovery/sources/table-sources.d.ts +27 -0
- package/dist/src/services/discovery/sources/table-sources.d.ts.map +1 -0
- package/dist/src/services/discovery/sources/table-sources.js +128 -0
- package/dist/src/services/discovery/sources/table-sources.js.map +1 -0
- package/dist/src/services/discovery/types.d.ts +117 -0
- package/dist/src/services/discovery/types.d.ts.map +1 -0
- package/dist/src/services/discovery/types.js +19 -0
- package/dist/src/services/discovery/types.js.map +1 -0
- package/dist/src/services/handbooks/agent.d.ts +1 -1
- package/dist/src/services/handbooks/agent.d.ts.map +1 -1
- package/dist/src/services/handbooks/agent.js +9 -3
- package/dist/src/services/handbooks/agent.js.map +1 -1
- package/dist/src/services/message-broadcast.d.ts +49 -0
- package/dist/src/services/message-broadcast.d.ts.map +1 -0
- package/dist/src/services/message-broadcast.js +116 -0
- package/dist/src/services/message-broadcast.js.map +1 -0
- package/dist/src/services/message-delivery.d.ts.map +1 -1
- package/dist/src/services/message-delivery.js +2 -0
- package/dist/src/services/message-delivery.js.map +1 -1
- package/dist/src/services/message-send.d.ts +6 -0
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +7 -5
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/presence.d.ts +8 -0
- package/dist/src/services/presence.d.ts.map +1 -1
- package/dist/src/services/presence.js +38 -0
- package/dist/src/services/presence.js.map +1 -1
- package/dist/src/services/workspace-enrichment.d.ts +15 -5
- package/dist/src/services/workspace-enrichment.d.ts.map +1 -1
- package/dist/src/services/workspace-enrichment.js +52 -14
- package/dist/src/services/workspace-enrichment.js.map +1 -1
- package/dist/src/storage/interface.d.ts +5 -0
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +1 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +13 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +3 -0
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +3 -0
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.js +13 -2
- package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +5 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts +3 -0
- package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +3 -0
- 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
|
-
/**
|
|
63
|
-
function
|
|
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 [
|
|
459
|
+
const [agents, setAgents] = useState(null); // [{ name, offers: [] }]
|
|
103
460
|
const [busy, setBusy] = useState('');
|
|
104
461
|
useEffect(() => {
|
|
105
|
-
apiGet('/v1/offers').then(
|
|
106
|
-
const
|
|
107
|
-
for (const a of (
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
112
|
-
setBusy(agentName + '/' +
|
|
113
|
-
try { await apiPost(`/v1/orgs/${encodeURIComponent(slug)}/offerings`, { agentName, offerId
|
|
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 (
|
|
118
|
-
if (
|
|
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
|
-
<
|
|
121
|
-
${
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
<
|
|
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-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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>
|