@burdenoff/microfe-crewfoundry 2026.827.1 → 2026.830.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crewfoundry/CrewFoundryRoot.d.ts.map +1 -1
- package/dist/crewfoundry/CrewFoundryRoot.js +190 -185
- package/dist/crewfoundry/CrewFoundryRoot.js.map +1 -1
- package/dist/crewfoundry/components/forms/ConfirmDialog.d.ts.map +1 -1
- package/dist/crewfoundry/components/forms/ConfirmDialog.js +25 -24
- package/dist/crewfoundry/components/forms/ConfirmDialog.js.map +1 -1
- package/dist/crewfoundry/components/forms/FormModal.d.ts.map +1 -1
- package/dist/crewfoundry/components/forms/FormModal.js +103 -101
- package/dist/crewfoundry/components/forms/FormModal.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +207 -210
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +75 -78
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +101 -104
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +95 -98
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +101 -104
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
- package/dist/shared/hooks/useTr.d.ts +17 -0
- package/dist/shared/hooks/useTr.d.ts.map +1 -0
- package/dist/shared/hooks/useTr.js +14 -0
- package/dist/shared/hooks/useTr.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
|
|
2
2
|
import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useTr as n } from "../../shared/hooks/useTr.js";
|
|
4
|
+
import { getInitials as r } from "../components/viz/lib/format.js";
|
|
5
|
+
import { AvatarStack as i } from "../components/viz/AvatarStack.js";
|
|
5
6
|
import "../components/viz/index.js";
|
|
6
|
-
import { FormModal as
|
|
7
|
-
import { useMemo as
|
|
8
|
-
import { ArrowRight as
|
|
9
|
-
import { useNavigate as
|
|
10
|
-
import { jsx as
|
|
11
|
-
import { Badge as
|
|
12
|
-
import { usePagination as
|
|
13
|
-
import { useI18n as D } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
7
|
+
import { FormModal as a, useFormModal as o } from "../components/forms/FormModal.js";
|
|
8
|
+
import { useMemo as s, useState as c } from "react";
|
|
9
|
+
import { ArrowRight as l, FolderKanban as u, Plus as d, Users as f } from "lucide-react";
|
|
10
|
+
import { useNavigate as p } from "react-router";
|
|
11
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
12
|
+
import { Badge as g, Button as _, CardContent as v, CardDescription as y, CardHeader as b, CardTitle as x, GlassCard as S, GradientText as C, IllustratedEmptyState as w, PagePurpose as T, Pagination as E } from "@burdenoff/fe-libs/ui";
|
|
13
|
+
import { usePagination as D } from "@burdenoff/fe-libs/shared/hooks";
|
|
14
14
|
//#region src/crewfoundry/pages/TeamsTeamsPagePage.tsx
|
|
15
15
|
var O = [
|
|
16
16
|
{
|
|
@@ -50,261 +50,258 @@ var O = [
|
|
|
50
50
|
colSpan: 2
|
|
51
51
|
}
|
|
52
52
|
];
|
|
53
|
-
function k({ name: e, src: t, size:
|
|
54
|
-
return t ? /* @__PURE__ */
|
|
53
|
+
function k({ name: e, src: t, size: n = 32 }) {
|
|
54
|
+
return t ? /* @__PURE__ */ m("img", {
|
|
55
55
|
src: t,
|
|
56
56
|
alt: e,
|
|
57
|
-
width:
|
|
58
|
-
height:
|
|
57
|
+
width: n,
|
|
58
|
+
height: n,
|
|
59
59
|
loading: "lazy",
|
|
60
60
|
className: "shrink-0 rounded-full object-cover",
|
|
61
61
|
style: {
|
|
62
|
-
width:
|
|
63
|
-
height:
|
|
62
|
+
width: n,
|
|
63
|
+
height: n
|
|
64
64
|
}
|
|
65
|
-
}) : /* @__PURE__ */
|
|
65
|
+
}) : /* @__PURE__ */ m("span", {
|
|
66
66
|
"aria-hidden": "true",
|
|
67
67
|
className: "inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary",
|
|
68
68
|
style: {
|
|
69
|
-
width:
|
|
70
|
-
height:
|
|
71
|
-
fontSize: Math.round(
|
|
69
|
+
width: n,
|
|
70
|
+
height: n,
|
|
71
|
+
fontSize: Math.round(n * .36)
|
|
72
72
|
},
|
|
73
|
-
children:
|
|
73
|
+
children: r(e)
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
function A(e) {
|
|
77
77
|
return `${e.firstName} ${e.lastName}`.trim();
|
|
78
78
|
}
|
|
79
79
|
function j() {
|
|
80
|
-
let
|
|
81
|
-
let r = n(e);
|
|
82
|
-
return r === e ? t : r;
|
|
83
|
-
}, U = o(() => new Map(N.map((e) => [e.id, e])), [N]), W = o(() => {
|
|
80
|
+
let r = n(), j = p(), { teams: M, employees: N, projects: P } = e(), F = e((e) => e.addTeam), I = o(), { page: L, pageSize: R, setPage: z } = D({ initialPageSize: 12 }), [B, V] = c(null), H = s(() => new Map(N.map((e) => [e.id, e])), [N]), U = s(() => {
|
|
84
81
|
let e = /* @__PURE__ */ new Map();
|
|
85
82
|
for (let t of P) t.status === "active" && e.set(t.teamId, (e.get(t.teamId) ?? 0) + 1);
|
|
86
83
|
return e;
|
|
87
|
-
}, [P]),
|
|
84
|
+
}, [P]), W = (e) => e.members.map((e) => H.get(e)).filter((e) => e !== void 0), G = (e) => W(e).map((e) => ({
|
|
88
85
|
name: A(e),
|
|
89
86
|
avatarUrl: e.avatar,
|
|
90
87
|
role: e.position
|
|
91
|
-
})),
|
|
92
|
-
return /* @__PURE__ */
|
|
88
|
+
})), K = M.slice((L - 1) * R, (L - 1) * R + R), q = M.find((e) => e.id === B) ?? K[0] ?? M[0] ?? null, J = q ? W(q) : [], Y = q ? P.filter((e) => e.teamId === q.id && e.status === "active") : [], X = (e) => j(`../teams/team-detail-page?id=${encodeURIComponent(e.id)}`);
|
|
89
|
+
return /* @__PURE__ */ h("div", {
|
|
93
90
|
className: "space-y-6 p-6",
|
|
94
91
|
children: [
|
|
95
|
-
/* @__PURE__ */
|
|
92
|
+
/* @__PURE__ */ h("div", {
|
|
96
93
|
className: "flex items-start justify-between gap-4",
|
|
97
|
-
children: [/* @__PURE__ */
|
|
98
|
-
/* @__PURE__ */
|
|
94
|
+
children: [/* @__PURE__ */ h("div", { children: [
|
|
95
|
+
/* @__PURE__ */ m("h1", {
|
|
99
96
|
className: "text-2xl font-semibold",
|
|
100
|
-
children: /* @__PURE__ */
|
|
97
|
+
children: /* @__PURE__ */ m(C, { children: r("crewfoundry.teams.title", "Teams") })
|
|
101
98
|
}),
|
|
102
|
-
/* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ h("p", {
|
|
103
100
|
className: "text-sm text-text-secondary",
|
|
104
101
|
children: [
|
|
105
102
|
M.length,
|
|
106
103
|
" ",
|
|
107
|
-
|
|
104
|
+
r("crewfoundry.teams.summary.count", "teams across the organization")
|
|
108
105
|
]
|
|
109
106
|
}),
|
|
110
|
-
/* @__PURE__ */
|
|
107
|
+
/* @__PURE__ */ m(T, {
|
|
111
108
|
className: "mt-1",
|
|
112
109
|
children: "Manage your teams — create groups, assign members, and organize people around the work they do together."
|
|
113
110
|
})
|
|
114
|
-
] }), /* @__PURE__ */
|
|
111
|
+
] }), /* @__PURE__ */ h(_, {
|
|
115
112
|
size: "sm",
|
|
116
113
|
onClick: () => I.openModal(),
|
|
117
|
-
children: [/* @__PURE__ */
|
|
114
|
+
children: [/* @__PURE__ */ m(d, { className: "h-4 w-4 mr-1" }), r("crewfoundry.teams.actions.createTeam", "Create Team")]
|
|
118
115
|
})]
|
|
119
116
|
}),
|
|
120
|
-
M.length === 0 ? /* @__PURE__ */
|
|
117
|
+
M.length === 0 ? /* @__PURE__ */ m(w, {
|
|
121
118
|
illustration: "empty-people",
|
|
122
119
|
title: "No teams yet",
|
|
123
120
|
description: "Create a team to organize people around their work."
|
|
124
|
-
}) : /* @__PURE__ */
|
|
121
|
+
}) : /* @__PURE__ */ h("div", {
|
|
125
122
|
className: "grid gap-6 lg:grid-cols-3",
|
|
126
|
-
children: [/* @__PURE__ */
|
|
123
|
+
children: [/* @__PURE__ */ h("div", {
|
|
127
124
|
className: "space-y-4 lg:col-span-2",
|
|
128
|
-
children: [/* @__PURE__ */
|
|
125
|
+
children: [/* @__PURE__ */ m("div", {
|
|
129
126
|
className: "grid gap-4 sm:grid-cols-2",
|
|
130
|
-
children:
|
|
131
|
-
let n = e.leadId ?
|
|
132
|
-
return /* @__PURE__ */
|
|
133
|
-
treatment:
|
|
134
|
-
glow:
|
|
127
|
+
children: K.map((e) => {
|
|
128
|
+
let n = e.leadId ? H.get(e.leadId) : void 0, a = U.get(e.id) ?? 0, o = q?.id === e.id;
|
|
129
|
+
return /* @__PURE__ */ h(S, {
|
|
130
|
+
treatment: o ? "glass" : "plain",
|
|
131
|
+
glow: o,
|
|
135
132
|
role: "button",
|
|
136
133
|
tabIndex: 0,
|
|
137
|
-
"aria-pressed":
|
|
134
|
+
"aria-pressed": o,
|
|
138
135
|
"aria-label": `Select ${e.name} team`,
|
|
139
136
|
onClick: () => V(e.id),
|
|
140
137
|
onKeyDown: (t) => {
|
|
141
138
|
(t.key === "Enter" || t.key === " ") && (t.preventDefault(), V(e.id));
|
|
142
139
|
},
|
|
143
140
|
className: "cursor-pointer transition-shadow hover:shadow-md focus-visible:shadow-[var(--shadow-focus)] focus-visible:outline-none",
|
|
144
|
-
children: [/* @__PURE__ */
|
|
141
|
+
children: [/* @__PURE__ */ h(b, {
|
|
145
142
|
className: "pb-3",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
143
|
+
children: [/* @__PURE__ */ h("div", {
|
|
147
144
|
className: "flex items-start justify-between gap-2",
|
|
148
|
-
children: [/* @__PURE__ */
|
|
145
|
+
children: [/* @__PURE__ */ m(x, {
|
|
149
146
|
className: "text-base",
|
|
150
147
|
children: e.name
|
|
151
|
-
}), /* @__PURE__ */
|
|
148
|
+
}), /* @__PURE__ */ m(g, {
|
|
152
149
|
variant: e.status === "active" ? "default" : "secondary",
|
|
153
150
|
className: "text-[10px]",
|
|
154
151
|
children: t(e.status)
|
|
155
152
|
})]
|
|
156
|
-
}), /* @__PURE__ */
|
|
153
|
+
}), /* @__PURE__ */ h("div", {
|
|
157
154
|
className: "flex items-center gap-2 text-xs text-text-secondary",
|
|
158
155
|
children: [
|
|
159
|
-
/* @__PURE__ */
|
|
160
|
-
/* @__PURE__ */
|
|
156
|
+
/* @__PURE__ */ m("span", { children: r("crewfoundry.teams.card.ledBy", "Led by") }),
|
|
157
|
+
/* @__PURE__ */ m(k, {
|
|
161
158
|
name: n ? A(n) : e.leadName,
|
|
162
159
|
src: n?.avatar,
|
|
163
160
|
size: 20
|
|
164
161
|
}),
|
|
165
|
-
/* @__PURE__ */
|
|
162
|
+
/* @__PURE__ */ m("span", {
|
|
166
163
|
className: "font-medium text-text-primary",
|
|
167
164
|
children: n ? A(n) : e.leadName || "—"
|
|
168
165
|
})
|
|
169
166
|
]
|
|
170
167
|
})]
|
|
171
|
-
}), /* @__PURE__ */
|
|
168
|
+
}), /* @__PURE__ */ h(v, {
|
|
172
169
|
className: "space-y-3",
|
|
173
|
-
children: [/* @__PURE__ */
|
|
174
|
-
people:
|
|
170
|
+
children: [/* @__PURE__ */ m(i, {
|
|
171
|
+
people: G(e),
|
|
175
172
|
max: 5,
|
|
176
173
|
size: 30
|
|
177
|
-
}), /* @__PURE__ */
|
|
174
|
+
}), /* @__PURE__ */ h("div", {
|
|
178
175
|
className: "flex items-center gap-4 text-sm text-text-secondary",
|
|
179
|
-
children: [/* @__PURE__ */
|
|
176
|
+
children: [/* @__PURE__ */ h("span", {
|
|
180
177
|
className: "flex items-center gap-1",
|
|
181
178
|
children: [
|
|
182
|
-
/* @__PURE__ */
|
|
179
|
+
/* @__PURE__ */ m(f, { className: "h-3.5 w-3.5" }),
|
|
183
180
|
e.memberCount,
|
|
184
181
|
" ",
|
|
185
|
-
|
|
182
|
+
r("crewfoundry.teams.card.members", "Members")
|
|
186
183
|
]
|
|
187
|
-
}), /* @__PURE__ */
|
|
184
|
+
}), /* @__PURE__ */ h("span", {
|
|
188
185
|
className: "flex items-center gap-1",
|
|
189
186
|
children: [
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
-
|
|
187
|
+
/* @__PURE__ */ m(u, { className: "h-3.5 w-3.5" }),
|
|
188
|
+
a,
|
|
192
189
|
" ",
|
|
193
|
-
|
|
190
|
+
r("crewfoundry.teams.card.activeProjects", "Active Projects")
|
|
194
191
|
]
|
|
195
192
|
})]
|
|
196
193
|
})]
|
|
197
194
|
})]
|
|
198
195
|
}, e.id);
|
|
199
196
|
})
|
|
200
|
-
}), M.length > R && /* @__PURE__ */
|
|
197
|
+
}), M.length > R && /* @__PURE__ */ m(E, {
|
|
201
198
|
page: L,
|
|
202
199
|
pageSize: R,
|
|
203
200
|
totalItems: M.length,
|
|
204
201
|
onPageChange: z
|
|
205
202
|
})]
|
|
206
|
-
}), /* @__PURE__ */
|
|
203
|
+
}), /* @__PURE__ */ m("div", {
|
|
207
204
|
className: "lg:col-span-1",
|
|
208
|
-
children:
|
|
205
|
+
children: q ? /* @__PURE__ */ h(S, {
|
|
209
206
|
treatment: "glass",
|
|
210
207
|
className: "border-border-seam lg:sticky lg:top-6",
|
|
211
208
|
"aria-live": "polite",
|
|
212
|
-
children: [/* @__PURE__ */
|
|
209
|
+
children: [/* @__PURE__ */ h(b, {
|
|
213
210
|
className: "pb-3",
|
|
214
|
-
children: [/* @__PURE__ */
|
|
211
|
+
children: [/* @__PURE__ */ h("div", {
|
|
215
212
|
className: "flex items-start justify-between gap-2",
|
|
216
|
-
children: [/* @__PURE__ */
|
|
213
|
+
children: [/* @__PURE__ */ m(x, {
|
|
217
214
|
className: "text-lg",
|
|
218
|
-
children:
|
|
219
|
-
}), /* @__PURE__ */
|
|
220
|
-
variant:
|
|
215
|
+
children: q.name
|
|
216
|
+
}), /* @__PURE__ */ m(g, {
|
|
217
|
+
variant: q.status === "active" ? "default" : "secondary",
|
|
221
218
|
className: "text-[10px]",
|
|
222
|
-
children: t(
|
|
219
|
+
children: t(q.status)
|
|
223
220
|
})]
|
|
224
|
-
}), /* @__PURE__ */
|
|
225
|
-
}), /* @__PURE__ */
|
|
221
|
+
}), /* @__PURE__ */ m(y, { children: q.description })]
|
|
222
|
+
}), /* @__PURE__ */ h(v, {
|
|
226
223
|
className: "space-y-5",
|
|
227
224
|
children: [
|
|
228
|
-
/* @__PURE__ */
|
|
225
|
+
/* @__PURE__ */ h("section", { children: [/* @__PURE__ */ h("h3", {
|
|
229
226
|
className: "mb-2 text-sm font-semibold",
|
|
230
227
|
children: [
|
|
231
|
-
|
|
228
|
+
r("crewfoundry.teams.panel.members", "Members"),
|
|
232
229
|
" (",
|
|
233
|
-
|
|
230
|
+
J.length,
|
|
234
231
|
")"
|
|
235
232
|
]
|
|
236
|
-
}),
|
|
233
|
+
}), J.length === 0 ? /* @__PURE__ */ m("p", {
|
|
237
234
|
className: "text-xs text-text-secondary",
|
|
238
235
|
children: "No members have been added to this team yet."
|
|
239
|
-
}) : /* @__PURE__ */
|
|
236
|
+
}) : /* @__PURE__ */ m("ul", {
|
|
240
237
|
className: "space-y-2 max-h-72 overflow-y-auto pr-1",
|
|
241
|
-
children:
|
|
238
|
+
children: J.map((e) => /* @__PURE__ */ h("li", {
|
|
242
239
|
className: "flex items-center gap-2.5",
|
|
243
|
-
children: [/* @__PURE__ */
|
|
240
|
+
children: [/* @__PURE__ */ m(k, {
|
|
244
241
|
name: A(e),
|
|
245
242
|
src: e.avatar,
|
|
246
243
|
size: 32
|
|
247
|
-
}), /* @__PURE__ */
|
|
244
|
+
}), /* @__PURE__ */ h("div", {
|
|
248
245
|
className: "min-w-0",
|
|
249
|
-
children: [/* @__PURE__ */
|
|
246
|
+
children: [/* @__PURE__ */ h("p", {
|
|
250
247
|
className: "truncate text-sm font-medium text-text-primary",
|
|
251
|
-
children: [A(e), e.id ===
|
|
248
|
+
children: [A(e), e.id === q.leadId ? /* @__PURE__ */ m("span", {
|
|
252
249
|
className: "ml-1.5 text-[10px] font-medium text-action-primary-text",
|
|
253
250
|
children: "· Lead"
|
|
254
251
|
}) : null]
|
|
255
|
-
}), /* @__PURE__ */
|
|
252
|
+
}), /* @__PURE__ */ m("p", {
|
|
256
253
|
className: "truncate text-xs text-text-secondary",
|
|
257
254
|
children: e.position
|
|
258
255
|
})]
|
|
259
256
|
})]
|
|
260
257
|
}, e.id))
|
|
261
258
|
})] }),
|
|
262
|
-
/* @__PURE__ */
|
|
259
|
+
/* @__PURE__ */ h("section", { children: [/* @__PURE__ */ h("h3", {
|
|
263
260
|
className: "mb-2 text-sm font-semibold",
|
|
264
261
|
children: [
|
|
265
|
-
|
|
262
|
+
r("crewfoundry.teams.panel.activeProjects", "Active Projects"),
|
|
266
263
|
" (",
|
|
267
|
-
|
|
264
|
+
Y.length,
|
|
268
265
|
")"
|
|
269
266
|
]
|
|
270
|
-
}),
|
|
267
|
+
}), Y.length === 0 ? /* @__PURE__ */ m("p", {
|
|
271
268
|
className: "text-xs text-text-secondary",
|
|
272
269
|
children: "No active projects for this team right now."
|
|
273
|
-
}) : /* @__PURE__ */
|
|
270
|
+
}) : /* @__PURE__ */ m("ul", {
|
|
274
271
|
className: "space-y-3",
|
|
275
|
-
children:
|
|
272
|
+
children: Y.map((e) => /* @__PURE__ */ h("li", { children: [/* @__PURE__ */ h("div", {
|
|
276
273
|
className: "mb-1 flex items-center justify-between gap-2 text-xs",
|
|
277
|
-
children: [/* @__PURE__ */
|
|
274
|
+
children: [/* @__PURE__ */ m("span", {
|
|
278
275
|
className: "truncate font-medium text-text-primary",
|
|
279
276
|
children: e.name
|
|
280
|
-
}), /* @__PURE__ */
|
|
277
|
+
}), /* @__PURE__ */ h("span", {
|
|
281
278
|
className: "shrink-0 tabular-nums text-text-secondary",
|
|
282
279
|
children: [e.progress, "%"]
|
|
283
280
|
})]
|
|
284
|
-
}), /* @__PURE__ */
|
|
281
|
+
}), /* @__PURE__ */ m("div", {
|
|
285
282
|
className: "h-1.5 overflow-hidden rounded-full bg-bg-sunken",
|
|
286
|
-
children: /* @__PURE__ */
|
|
283
|
+
children: /* @__PURE__ */ m("div", {
|
|
287
284
|
className: "h-1.5 rounded-full bg-action-primary-bg",
|
|
288
285
|
style: { width: `${e.progress}%` }
|
|
289
286
|
})
|
|
290
287
|
})] }, e.id))
|
|
291
288
|
})] }),
|
|
292
|
-
/* @__PURE__ */
|
|
289
|
+
/* @__PURE__ */ h(_, {
|
|
293
290
|
variant: "outline",
|
|
294
291
|
size: "sm",
|
|
295
292
|
className: "w-full",
|
|
296
|
-
onClick: () =>
|
|
297
|
-
children: [
|
|
293
|
+
onClick: () => X(q),
|
|
294
|
+
children: [r("crewfoundry.teams.panel.openDetail", "Open full team detail"), /* @__PURE__ */ m(l, { className: "ml-1 h-4 w-4" })]
|
|
298
295
|
})
|
|
299
296
|
]
|
|
300
297
|
})]
|
|
301
298
|
}) : null
|
|
302
299
|
})]
|
|
303
300
|
}),
|
|
304
|
-
/* @__PURE__ */
|
|
301
|
+
/* @__PURE__ */ m(a, {
|
|
305
302
|
open: I.open,
|
|
306
303
|
onOpenChange: I.onOpenChange,
|
|
307
|
-
title:
|
|
304
|
+
title: r("crewfoundry.teams.actions.createTeam", "Create Team"),
|
|
308
305
|
description: "Organize people around the work they do together.",
|
|
309
306
|
fields: O,
|
|
310
307
|
submitLabel: "Create Team",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamsTeamsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/TeamsTeamsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { ArrowRight, FolderKanban, Plus, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useMemo, useState } from 'react';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { useCrewFoundryStore, type Employee, type Team } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, getInitials, type Person } from '../components/viz';\n\nconst TEAM_FIELDS: FieldConfig[] = [\n { name: 'name', label: 'Team Name', type: 'text', required: true },\n { name: 'department', label: 'Department', type: 'text', placeholder: 'e.g. Engineering' },\n { name: 'leadName', label: 'Team Lead', type: 'text', placeholder: 'Lead full name' },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Forming', value: 'forming' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', colSpan: 2 },\n];\n\nfunction InitialsAvatar({ name, src, size = 32 }: { name: string; src?: string; size?: number }) {\n if (src) {\n return (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n loading=\"lazy\"\n className=\"shrink-0 rounded-full object-cover\"\n style={{ width: size, height: size }}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nfunction employeeName(employee: Employee): string {\n return `${employee.firstName} ${employee.lastName}`.trim();\n}\n\nexport function TeamsTeamsPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { teams, employees, projects } = useCrewFoundryStore();\n const addTeam = useCrewFoundryStore((state) => state.addTeam);\n const createModal = useFormModal();\n const { page, pageSize, setPage } = usePagination({ initialPageSize: 12 });\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const employeeById = useMemo(\n () => new Map(employees.map((employee) => [employee.id, employee])),\n [employees]\n );\n\n const activeProjectsByTeam = useMemo(() => {\n const map = new Map<string, number>();\n for (const project of projects) {\n if (project.status === 'active') {\n map.set(project.teamId, (map.get(project.teamId) ?? 0) + 1);\n }\n }\n return map;\n }, [projects]);\n\n const membersOf = (team: Team): Employee[] =>\n team.members\n .map((memberId) => employeeById.get(memberId))\n .filter((employee): employee is Employee => employee !== undefined);\n\n const peopleOf = (team: Team): Person[] =>\n membersOf(team).map((employee) => ({\n name: employeeName(employee),\n avatarUrl: employee.avatar,\n role: employee.position,\n }));\n\n const pageTeams = teams.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize);\n const selectedTeam =\n teams.find((team) => team.id === selectedId) ?? pageTeams[0] ?? teams[0] ?? null;\n const selectedMembers = selectedTeam ? membersOf(selectedTeam) : [];\n const selectedActiveProjects = selectedTeam\n ? projects.filter(\n (project) => project.teamId === selectedTeam.id && project.status === 'active'\n )\n : [];\n\n const openTeam = (team: Team) =>\n navigate(`../teams/team-detail-page?id=${encodeURIComponent(team.id)}`);\n\n const handleCreate = async (values: FormValues) => {\n // CreateCrewTeamInput accepts only this field subset; the store spreads the\n // object straight into `input`, so we send exactly the accepted shape and\n // let the server response replace the optimistic row.\n const payload: Partial<Team> = {\n name: String(values.name ?? '').trim(),\n description: String(values.description ?? '').trim(),\n department: String(values.department ?? '').trim() || 'operations',\n leadName: String(values.leadName ?? '').trim(),\n status: (String(values.status ?? 'forming') as Team['status']) || 'forming',\n members: [],\n };\n await addTeam(payload as Team);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{t('crewfoundry.teams.title', 'Teams')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {teams.length} {t('crewfoundry.teams.summary.count', 'teams across the organization')}\n </p>\n <PagePurpose className=\"mt-1\">\n Manage your teams — create groups, assign members, and organize people around the work\n they do together.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => createModal.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('crewfoundry.teams.actions.createTeam', 'Create Team')}\n </Button>\n </div>\n\n {teams.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No teams yet\"\n description=\"Create a team to organize people around their work.\"\n />\n ) : (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Team cards */}\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pageTeams.map((team) => {\n const lead = team.leadId ? employeeById.get(team.leadId) : undefined;\n const activeCount = activeProjectsByTeam.get(team.id) ?? 0;\n const selected = selectedTeam?.id === team.id;\n return (\n <GlassCard\n key={team.id}\n treatment={selected ? 'glass' : 'plain'}\n glow={selected}\n role=\"button\"\n tabIndex={0}\n aria-pressed={selected}\n aria-label={`Select ${team.name} team`}\n onClick={() => setSelectedId(team.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setSelectedId(team.id);\n }\n }}\n className=\"cursor-pointer transition-shadow hover:shadow-md focus-visible:shadow-[var(--shadow-focus)] focus-visible:outline-none\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-base\">{team.name}</CardTitle>\n <Badge\n variant={team.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(team.status)}\n </Badge>\n </div>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <span>{t('crewfoundry.teams.card.ledBy', 'Led by')}</span>\n <InitialsAvatar\n name={lead ? employeeName(lead) : team.leadName}\n src={lead?.avatar}\n size={20}\n />\n <span className=\"font-medium text-text-primary\">\n {lead ? employeeName(lead) : team.leadName || '—'}\n </span>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <AvatarStack people={peopleOf(team)} max={5} size={30} />\n <div className=\"flex items-center gap-4 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" />\n {team.memberCount} {t('crewfoundry.teams.card.members', 'Members')}\n </span>\n <span className=\"flex items-center gap-1\">\n <FolderKanban className=\"h-3.5 w-3.5\" />\n {activeCount}{' '}\n {t('crewfoundry.teams.card.activeProjects', 'Active Projects')}\n </span>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {teams.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={teams.length}\n onPageChange={setPage}\n />\n )}\n </div>\n\n {/* Selected-team detail panel */}\n <div className=\"lg:col-span-1\">\n {selectedTeam ? (\n <GlassCard\n treatment=\"glass\"\n className=\"border-border-seam lg:sticky lg:top-6\"\n aria-live=\"polite\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg\">{selectedTeam.name}</CardTitle>\n <Badge\n variant={selectedTeam.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(selectedTeam.status)}\n </Badge>\n </div>\n <CardDescription>{selectedTeam.description}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {t('crewfoundry.teams.panel.members', 'Members')} ({selectedMembers.length})\n </h3>\n {selectedMembers.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No members have been added to this team yet.\n </p>\n ) : (\n <ul className=\"space-y-2 max-h-72 overflow-y-auto pr-1\">\n {selectedMembers.map((member) => (\n <li key={member.id} className=\"flex items-center gap-2.5\">\n <InitialsAvatar\n name={employeeName(member)}\n src={member.avatar}\n size={32}\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {employeeName(member)}\n {member.id === selectedTeam.leadId ? (\n <span className=\"ml-1.5 text-[10px] font-medium text-action-primary-text\">\n · Lead\n </span>\n ) : null}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {member.position}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {t('crewfoundry.teams.panel.activeProjects', 'Active Projects')} (\n {selectedActiveProjects.length})\n </h3>\n {selectedActiveProjects.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No active projects for this team right now.\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {selectedActiveProjects.map((project) => (\n <li key={project.id}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-xs\">\n <span className=\"truncate font-medium text-text-primary\">\n {project.name}\n </span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {project.progress}%\n </span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-1.5 rounded-full bg-action-primary-bg\"\n style={{ width: `${project.progress}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => openTeam(selectedTeam)}\n >\n {t('crewfoundry.teams.panel.openDetail', 'Open full team detail')}\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </CardContent>\n </GlassCard>\n ) : null}\n </div>\n </div>\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title={t('crewfoundry.teams.actions.createTeam', 'Create Team')}\n description=\"Organize people around the work they do together.\"\n fields={TEAM_FIELDS}\n submitLabel=\"Create Team\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA4BA,IAAM,IAA6B;CACjC;EAAE,MAAM;EAAQ,OAAO;EAAa,MAAM;EAAQ,UAAU;EAAM;CAClE;EAAE,MAAM;EAAc,OAAO;EAAc,MAAM;EAAQ,aAAa;EAAoB;CAC1F;EAAE,MAAM;EAAY,OAAO;EAAa,MAAM;EAAQ,aAAa;EAAkB;CACrF;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,SAAS,CACP;GAAE,OAAO;GAAU,OAAO;GAAU,EACpC;GAAE,OAAO;GAAW,OAAO;GAAW,CACvC;EACF;CACD;EAAE,MAAM;EAAe,OAAO;EAAe,MAAM;EAAY,SAAS;EAAG;CAC5E;AAED,SAAS,EAAe,EAAE,SAAM,QAAK,UAAO,MAAqD;AAc/F,QAbI,IAEA,kBAAC,OAAD;EACO;EACL,KAAK;EACL,OAAO;EACP,QAAQ;EACR,SAAQ;EACR,WAAU;EACV,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM;EACpC,CAAA,GAIJ,kBAAC,QAAD;EACE,eAAY;EACZ,WAAU;EACV,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM,UAAU,KAAK,MAAM,IAAO,IAAK;GAAE;YAEtE,EAAY,EAAK;EACb,CAAA;;AAIX,SAAS,EAAa,GAA4B;AAChD,QAAO,GAAG,EAAS,UAAU,GAAG,EAAS,WAAW,MAAM;;AAG5D,SAAgB,IAAqB;CACnC,IAAM,EAAE,GAAG,MAAc,GAAS,EAC5B,IAAW,GAAa,EACxB,EAAE,UAAO,cAAW,gBAAa,GAAqB,EACtD,IAAU,GAAqB,MAAU,EAAM,QAAQ,EACvD,IAAc,GAAc,EAC5B,EAAE,SAAM,aAAU,eAAY,EAAc,EAAE,iBAAiB,IAAI,CAAC,EACpE,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAC3D,KAAK,GAAa,MAAqB;EAC3C,IAAM,IAAQ,EAAU,EAAI;AAC5B,SAAO,MAAU,IAAM,IAAW;IAG9B,IAAe,QACb,IAAI,IAAI,EAAU,KAAK,MAAa,CAAC,EAAS,IAAI,EAAS,CAAC,CAAC,EACnE,CAAC,EAAU,CACZ,EAEK,IAAuB,QAAc;EACzC,IAAM,oBAAM,IAAI,KAAqB;AACrC,OAAK,IAAM,KAAW,EACpB,CAAI,EAAQ,WAAW,YACrB,EAAI,IAAI,EAAQ,SAAS,EAAI,IAAI,EAAQ,OAAO,IAAI,KAAK,EAAE;AAG/D,SAAO;IACN,CAAC,EAAS,CAAC,EAER,KAAa,MACjB,EAAK,QACF,KAAK,MAAa,EAAa,IAAI,EAAS,CAAC,CAC7C,QAAQ,MAAmC,MAAa,KAAA,EAAU,EAEjE,KAAY,MAChB,EAAU,EAAK,CAAC,KAAK,OAAc;EACjC,MAAM,EAAa,EAAS;EAC5B,WAAW,EAAS;EACpB,MAAM,EAAS;EAChB,EAAE,EAEC,IAAY,EAAM,OAAO,IAAO,KAAK,IAAW,IAAO,KAAK,IAAW,EAAS,EAChF,IACJ,EAAM,MAAM,MAAS,EAAK,OAAO,EAAW,IAAI,EAAU,MAAM,EAAM,MAAM,MACxE,IAAkB,IAAe,EAAU,EAAa,GAAG,EAAE,EAC7D,IAAyB,IAC3B,EAAS,QACN,MAAY,EAAQ,WAAW,EAAa,MAAM,EAAQ,WAAW,SACvE,GACD,EAAE,EAEA,KAAY,MAChB,EAAS,gCAAgC,mBAAmB,EAAK,GAAG,GAAG;AAiBzE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACZ,kBAAC,GAAD,EAAA,UAAe,EAAE,2BAA2B,QAAQ,EAAgB,CAAA;MACjE,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAM;OAAO;OAAE,EAAE,mCAAmC,gCAAgC;OACnF;;KACJ,kBAAC,GAAD;MAAa,WAAU;gBAAO;MAGhB,CAAA;KACV,EAAA,CAAA,EACN,kBAAC,GAAD;KAAQ,MAAK;KAAK,eAAe,EAAY,WAAW;eAAxD,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAChC,EAAE,wCAAwC,cAAc,CAClD;OACL;;GAEL,EAAM,WAAW,IAChB,kBAAC,GAAD;IACE,cAAa;IACb,OAAM;IACN,aAAY;IACZ,CAAA,GAEF,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAU,KAAK,MAAS;OACvB,IAAM,IAAO,EAAK,SAAS,EAAa,IAAI,EAAK,OAAO,GAAG,KAAA,GACrD,IAAc,EAAqB,IAAI,EAAK,GAAG,IAAI,GACnD,IAAW,GAAc,OAAO,EAAK;AAC3C,cACE,kBAAC,GAAD;QAEE,WAAW,IAAW,UAAU;QAChC,MAAM;QACN,MAAK;QACL,UAAU;QACV,gBAAc;QACd,cAAY,UAAU,EAAK,KAAK;QAChC,eAAe,EAAc,EAAK,GAAG;QACrC,YAAY,MAAM;AAChB,UAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,EAAc,EAAK,GAAG;;QAG1B,WAAU;kBAfZ,CAiBE,kBAAC,GAAD;SAAY,WAAU;mBAAtB,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WAAW,WAAU;qBAAa,EAAK;WAAiB,CAAA,EACxD,kBAAC,GAAD;WACE,SAAS,EAAK,WAAW,WAAW,YAAY;WAChD,WAAU;qBAET,EAAgB,EAAK,OAAO;WACvB,CAAA,CACJ;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,QAAD,EAAA,UAAO,EAAE,gCAAgC,SAAS,EAAQ,CAAA;WAC1D,kBAAC,GAAD;YACE,MAAM,IAAO,EAAa,EAAK,GAAG,EAAK;YACvC,KAAK,GAAM;YACX,MAAM;YACN,CAAA;WACF,kBAAC,QAAD;YAAM,WAAU;sBACb,IAAO,EAAa,EAAK,GAAG,EAAK,YAAY;YACzC,CAAA;WACH;YACK;YACb,kBAAC,GAAD;SAAa,WAAU;mBAAvB,CACE,kBAAC,GAAD;UAAa,QAAQ,EAAS,EAAK;UAAE,KAAK;UAAG,MAAM;UAAM,CAAA,EACzD,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;YAChC,EAAK;YAAY;YAAE,EAAE,kCAAkC,UAAU;YAC7D;cACP,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YACE,kBAAC,GAAD,EAAc,WAAU,eAAgB,CAAA;YACvC;YAAa;YACb,EAAE,yCAAyC,kBAAkB;YACzD;aACH;YACM;WACJ;UApDL,EAAK,GAoDA;QAEd;MACE,CAAA,EAEL,EAAM,SAAS,KACd,kBAAC,GAAD;MACQ;MACI;MACV,YAAY,EAAM;MAClB,cAAc;MACd,CAAA,CAEA;QAGN,kBAAC,OAAD;KAAK,WAAU;eACZ,IACC,kBAAC,GAAD;MACE,WAAU;MACV,WAAU;MACV,aAAU;gBAHZ,CAKE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAW,WAAU;mBAAW,EAAa;SAAiB,CAAA,EAC9D,kBAAC,GAAD;SACE,SAAS,EAAa,WAAW,WAAW,YAAY;SACxD,WAAU;mBAET,EAAgB,EAAa,OAAO;SAC/B,CAAA,CACJ;WACN,kBAAC,GAAD,EAAA,UAAkB,EAAa,aAA8B,CAAA,CAClD;UACb,kBAAC,GAAD;OAAa,WAAU;iBAAvB;QACE,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACG,EAAE,mCAAmC,UAAU;UAAC;UAAG,EAAgB;UAAO;UACxE;YACJ,EAAgB,WAAW,IAC1B,kBAAC,KAAD;SAAG,WAAU;mBAA8B;SAEvC,CAAA,GAEJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAgB,KAAK,MACpB,kBAAC,MAAD;UAAoB,WAAU;oBAA9B,CACE,kBAAC,GAAD;WACE,MAAM,EAAa,EAAO;WAC1B,KAAK,EAAO;WACZ,MAAM;WACN,CAAA,EACF,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,KAAD;YAAG,WAAU;sBAAb,CACG,EAAa,EAAO,EACpB,EAAO,OAAO,EAAa,SAC1B,kBAAC,QAAD;aAAM,WAAU;uBAA0D;aAEnE,CAAA,GACL,KACF;eACJ,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAO;YACN,CAAA,CACA;aACH;YAnBI,EAAO,GAmBX,CACL;SACC,CAAA,CAEC,EAAA,CAAA;QAEV,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACG,EAAE,0CAA0C,kBAAkB;UAAC;UAC/D,EAAuB;UAAO;UAC5B;YACJ,EAAuB,WAAW,IACjC,kBAAC,KAAD;SAAG,WAAU;mBAA8B;SAEvC,CAAA,GAEJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAuB,KAAK,MAC3B,kBAAC,MAAD,EAAA,UAAA,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD;WAAM,WAAU;qBACb,EAAQ;WACJ,CAAA,EACP,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACG,EAAQ,UAAS,IACb;aACH;aACN,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,OAAD;WACE,WAAU;WACV,OAAO,EAAE,OAAO,GAAG,EAAQ,SAAS,IAAI;WACxC,CAAA;UACE,CAAA,CACH,EAAA,EAfI,EAAQ,GAeZ,CACL;SACC,CAAA,CAEC,EAAA,CAAA;QAEV,kBAAC,GAAD;SACE,SAAQ;SACR,MAAK;SACL,WAAU;SACV,eAAe,EAAS,EAAa;mBAJvC,CAMG,EAAE,sCAAsC,wBAAwB,EACjE,kBAAC,GAAD,EAAY,WAAU,gBAAiB,CAAA,CAChC;;QACG;SACJ;UACV;KACA,CAAA,CACF;;GAGR,kBAAC,GAAD;IACE,MAAM,EAAY;IAClB,cAAc,EAAY;IAC1B,OAAO,EAAE,wCAAwC,cAAc;IAC/D,aAAY;IACZ,QAAQ;IACR,aAAY;IACZ,UAzOe,OAAO,MAAuB;AAYjD,WAAM,EARyB;MAC7B,MAAM,OAAO,EAAO,QAAQ,GAAG,CAAC,MAAM;MACtC,aAAa,OAAO,EAAO,eAAe,GAAG,CAAC,MAAM;MACpD,YAAY,OAAO,EAAO,cAAc,GAAG,CAAC,MAAM,IAAI;MACtD,UAAU,OAAO,EAAO,YAAY,GAAG,CAAC,MAAM;MAC9C,QAAS,OAAO,EAAO,UAAU,UAAU,IAAuB;MAClE,SAAS,EAAE;MACZ,CAC6B;;IA8N1B,CAAA;GACE"}
|
|
1
|
+
{"version":3,"file":"TeamsTeamsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/TeamsTeamsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { ArrowRight, FolderKanban, Plus, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useMemo, useState } from 'react';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { useCrewFoundryStore, type Employee, type Team } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, getInitials, type Person } from '../components/viz';\n\nconst TEAM_FIELDS: FieldConfig[] = [\n { name: 'name', label: 'Team Name', type: 'text', required: true },\n { name: 'department', label: 'Department', type: 'text', placeholder: 'e.g. Engineering' },\n { name: 'leadName', label: 'Team Lead', type: 'text', placeholder: 'Lead full name' },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Forming', value: 'forming' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', colSpan: 2 },\n];\n\nfunction InitialsAvatar({ name, src, size = 32 }: { name: string; src?: string; size?: number }) {\n if (src) {\n return (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n loading=\"lazy\"\n className=\"shrink-0 rounded-full object-cover\"\n style={{ width: size, height: size }}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nfunction employeeName(employee: Employee): string {\n return `${employee.firstName} ${employee.lastName}`.trim();\n}\n\nexport function TeamsTeamsPagePage() {\n const tr = useTr();\n const navigate = useNavigate();\n const { teams, employees, projects } = useCrewFoundryStore();\n const addTeam = useCrewFoundryStore((state) => state.addTeam);\n const createModal = useFormModal();\n const { page, pageSize, setPage } = usePagination({ initialPageSize: 12 });\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const employeeById = useMemo(\n () => new Map(employees.map((employee) => [employee.id, employee])),\n [employees]\n );\n\n const activeProjectsByTeam = useMemo(() => {\n const map = new Map<string, number>();\n for (const project of projects) {\n if (project.status === 'active') {\n map.set(project.teamId, (map.get(project.teamId) ?? 0) + 1);\n }\n }\n return map;\n }, [projects]);\n\n const membersOf = (team: Team): Employee[] =>\n team.members\n .map((memberId) => employeeById.get(memberId))\n .filter((employee): employee is Employee => employee !== undefined);\n\n const peopleOf = (team: Team): Person[] =>\n membersOf(team).map((employee) => ({\n name: employeeName(employee),\n avatarUrl: employee.avatar,\n role: employee.position,\n }));\n\n const pageTeams = teams.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize);\n const selectedTeam =\n teams.find((team) => team.id === selectedId) ?? pageTeams[0] ?? teams[0] ?? null;\n const selectedMembers = selectedTeam ? membersOf(selectedTeam) : [];\n const selectedActiveProjects = selectedTeam\n ? projects.filter(\n (project) => project.teamId === selectedTeam.id && project.status === 'active'\n )\n : [];\n\n const openTeam = (team: Team) =>\n navigate(`../teams/team-detail-page?id=${encodeURIComponent(team.id)}`);\n\n const handleCreate = async (values: FormValues) => {\n // CreateCrewTeamInput accepts only this field subset; the store spreads the\n // object straight into `input`, so we send exactly the accepted shape and\n // let the server response replace the optimistic row.\n const payload: Partial<Team> = {\n name: String(values.name ?? '').trim(),\n description: String(values.description ?? '').trim(),\n department: String(values.department ?? '').trim() || 'operations',\n leadName: String(values.leadName ?? '').trim(),\n status: (String(values.status ?? 'forming') as Team['status']) || 'forming',\n members: [],\n };\n await addTeam(payload as Team);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{tr('crewfoundry.teams.title', 'Teams')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {teams.length} {tr('crewfoundry.teams.summary.count', 'teams across the organization')}\n </p>\n <PagePurpose className=\"mt-1\">\n Manage your teams — create groups, assign members, and organize people around the work\n they do together.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => createModal.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {tr('crewfoundry.teams.actions.createTeam', 'Create Team')}\n </Button>\n </div>\n\n {teams.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No teams yet\"\n description=\"Create a team to organize people around their work.\"\n />\n ) : (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Team cards */}\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pageTeams.map((team) => {\n const lead = team.leadId ? employeeById.get(team.leadId) : undefined;\n const activeCount = activeProjectsByTeam.get(team.id) ?? 0;\n const selected = selectedTeam?.id === team.id;\n return (\n <GlassCard\n key={team.id}\n treatment={selected ? 'glass' : 'plain'}\n glow={selected}\n role=\"button\"\n tabIndex={0}\n aria-pressed={selected}\n aria-label={`Select ${team.name} team`}\n onClick={() => setSelectedId(team.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setSelectedId(team.id);\n }\n }}\n className=\"cursor-pointer transition-shadow hover:shadow-md focus-visible:shadow-[var(--shadow-focus)] focus-visible:outline-none\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-base\">{team.name}</CardTitle>\n <Badge\n variant={team.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(team.status)}\n </Badge>\n </div>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <span>{tr('crewfoundry.teams.card.ledBy', 'Led by')}</span>\n <InitialsAvatar\n name={lead ? employeeName(lead) : team.leadName}\n src={lead?.avatar}\n size={20}\n />\n <span className=\"font-medium text-text-primary\">\n {lead ? employeeName(lead) : team.leadName || '—'}\n </span>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <AvatarStack people={peopleOf(team)} max={5} size={30} />\n <div className=\"flex items-center gap-4 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" />\n {team.memberCount} {tr('crewfoundry.teams.card.members', 'Members')}\n </span>\n <span className=\"flex items-center gap-1\">\n <FolderKanban className=\"h-3.5 w-3.5\" />\n {activeCount}{' '}\n {tr('crewfoundry.teams.card.activeProjects', 'Active Projects')}\n </span>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {teams.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={teams.length}\n onPageChange={setPage}\n />\n )}\n </div>\n\n {/* Selected-team detail panel */}\n <div className=\"lg:col-span-1\">\n {selectedTeam ? (\n <GlassCard\n treatment=\"glass\"\n className=\"border-border-seam lg:sticky lg:top-6\"\n aria-live=\"polite\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg\">{selectedTeam.name}</CardTitle>\n <Badge\n variant={selectedTeam.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(selectedTeam.status)}\n </Badge>\n </div>\n <CardDescription>{selectedTeam.description}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {tr('crewfoundry.teams.panel.members', 'Members')} ({selectedMembers.length})\n </h3>\n {selectedMembers.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No members have been added to this team yet.\n </p>\n ) : (\n <ul className=\"space-y-2 max-h-72 overflow-y-auto pr-1\">\n {selectedMembers.map((member) => (\n <li key={member.id} className=\"flex items-center gap-2.5\">\n <InitialsAvatar\n name={employeeName(member)}\n src={member.avatar}\n size={32}\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {employeeName(member)}\n {member.id === selectedTeam.leadId ? (\n <span className=\"ml-1.5 text-[10px] font-medium text-action-primary-text\">\n · Lead\n </span>\n ) : null}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {member.position}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {tr('crewfoundry.teams.panel.activeProjects', 'Active Projects')} (\n {selectedActiveProjects.length})\n </h3>\n {selectedActiveProjects.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No active projects for this team right now.\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {selectedActiveProjects.map((project) => (\n <li key={project.id}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-xs\">\n <span className=\"truncate font-medium text-text-primary\">\n {project.name}\n </span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {project.progress}%\n </span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-1.5 rounded-full bg-action-primary-bg\"\n style={{ width: `${project.progress}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => openTeam(selectedTeam)}\n >\n {tr('crewfoundry.teams.panel.openDetail', 'Open full team detail')}\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </CardContent>\n </GlassCard>\n ) : null}\n </div>\n </div>\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title={tr('crewfoundry.teams.actions.createTeam', 'Create Team')}\n description=\"Organize people around the work they do together.\"\n fields={TEAM_FIELDS}\n submitLabel=\"Create Team\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTr — translation hook with fallback semantics.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `useI18n().t()` so that when a key has no translation (i.e. the
|
|
5
|
+
* provider returns the key itself), the caller's English fallback is used
|
|
6
|
+
* instead. This keeps consumer call sites consistent and avoids leaking
|
|
7
|
+
* untranslated keys into the UI.
|
|
8
|
+
*
|
|
9
|
+
* Supports optional interpolation params using `{{key}}` placeholders.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const tr = useTr();
|
|
13
|
+
* return <h1>{tr('crewfoundry.dashboard.title', 'Dashboard')}</h1>;
|
|
14
|
+
* return <p>{tr('crewfoundry.delete.confirm', 'Delete {{count}} items?', { count: 5 })}</p>;
|
|
15
|
+
*/
|
|
16
|
+
export declare function useTr(): (key: string, fallback: string, params?: Record<string, string | number>) => string;
|
|
17
|
+
//# sourceMappingURL=useTr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.d.ts","sourceRoot":"","sources":["../../../src/shared/hooks/useTr.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,IAAI,CACvB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KACrC,MAAM,CAcV"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback as e } from "react";
|
|
2
|
+
import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
3
|
+
//#region src/shared/hooks/useTr.ts
|
|
4
|
+
function n() {
|
|
5
|
+
let { t: n } = t();
|
|
6
|
+
return e((e, t, r) => {
|
|
7
|
+
let i = n(e), a = i === e ? t : i;
|
|
8
|
+
return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
|
|
9
|
+
}, [n]);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { n as useTr };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=useTr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.js","names":[],"sources":["../../../src/shared/hooks/useTr.ts"],"sourcesContent":["import { useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('crewfoundry.dashboard.title', 'Dashboard')}</h1>;\n * return <p>{tr('crewfoundry.delete.confirm', 'Delete {{count}} items?', { count: 5 })}</p>;\n */\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>\n) => string {\n const { t } = useI18n();\n return useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) => str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v)),\n raw\n );\n },\n [t]\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,GAAS;AACvB,QAAO,GACJ,GAAa,GAAkB,MAAqD;EACnF,IAAM,IAAa,EAAE,EAAI,EACnB,IAAM,MAAe,IAAM,IAAW;AAE5C,SADK,IACE,OAAO,QAAQ,EAAO,CAAC,QAC3B,GAAK,CAAC,GAAG,OAAO,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,IAAI,EAAE,OAAO,EAAE,CAAC,EAC5E,EACD,GAJmB;IAMtB,CAAC,EAAE,CACJ"}
|