@fayz-ai/plugin-workforce 0.3.14 → 0.3.15

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.
@@ -0,0 +1,2133 @@
1
+ import { WorkforceContextProvider, PeopleView, useWorkforceProvider, useWorkforceStore, useWorkforceConfig, formatCurrency } from './chunk-6GWD6GQK.js';
2
+ import { ModulePage, FilterRail, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MiniCalendar, toLocalISODate, Badge, Button, Skeleton, resolveRangePreset, compareCodes, PageToolbar, DateRangeControl, StatTiles, StatementTable, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Input, ModalFooter, toast, Popover, PopoverTrigger, PopoverContent, Checkbox, Avatar, AvatarFallback, DataTable, AvatarImage } from '@fayz-ai/ui';
3
+ import { useModuleNavigation, useModuleNavAccess, EntitlementGate, UpgradePrompt, parseViewId, ScheduleEditor, usePermissionOptional, adminNavigateTo, PersonLink } from '@fayz-ai/admin';
4
+ import React3 from 'react';
5
+ import { Send, CalendarDays, Plus, ExternalLink, HandCoins, AlertTriangle, Users, CalendarCheck, Unlock, Lock, Clock, LayoutTemplate, ListTree, UserPlus, UserMinus, CakeSlice, Plane, CalendarClock, ShieldAlert, Stethoscope, ClipboardList, Pencil, Eye, ChevronRight, Trash2, ImageOff, MapPin, ScanFace } from 'lucide-react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+ import { getSupabaseClientOptional } from '@fayz-ai/core';
8
+
9
+ function monthBounds(date) {
10
+ const d = /* @__PURE__ */ new Date(`${date}T12:00:00`);
11
+ const first = new Date(d.getFullYear(), d.getMonth(), 1);
12
+ const last = new Date(d.getFullYear(), d.getMonth() + 1, 0);
13
+ return { from: toLocalISODate(first), to: toLocalISODate(last) };
14
+ }
15
+ var STATUS_LABEL = {
16
+ draft: "Rascunho",
17
+ published: "Publicada",
18
+ closed: "Fechada"
19
+ };
20
+ var ASSIGNMENT_STATUS_LABEL = {
21
+ pending: "Pendente",
22
+ notified: "Avisado",
23
+ confirmed: "Confirmado",
24
+ declined: "Recusou",
25
+ realized: "Cumprido"
26
+ };
27
+ function AssignmentRow({ assignment }) {
28
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2", children: [
29
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
30
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium", children: assignment.personName ?? "Sem nome" }),
31
+ /* @__PURE__ */ jsxs("div", { className: "truncate text-xs text-muted-foreground", children: [
32
+ assignment.professionName ?? "Sem fun\xE7\xE3o",
33
+ assignment.shiftLabel ? ` \xB7 ${assignment.shiftLabel}` : "",
34
+ assignment.shiftStartTime ? ` \xB7 ${assignment.shiftStartTime}\u2013${assignment.shiftEndTime}` : ""
35
+ ] })
36
+ ] }),
37
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
38
+ assignment.role === "standby" && /* @__PURE__ */ jsx(Badge, { variant: "outline", children: "Reserva" }),
39
+ /* @__PURE__ */ jsx(Badge, { variant: assignment.status === "confirmed" ? "success" : assignment.status === "declined" ? "destructive" : "secondary", children: ASSIGNMENT_STATUS_LABEL[assignment.status] ?? assignment.status })
40
+ ] })
41
+ ] });
42
+ }
43
+ function SchedulesView() {
44
+ const provider = useWorkforceProvider();
45
+ const selectedDate = useWorkforceStore((s) => s.selectedDate);
46
+ const setSelectedDate = useWorkforceStore((s) => s.setSelectedDate);
47
+ const schedules = useWorkforceStore((s) => s.schedules);
48
+ const loading = useWorkforceStore((s) => s.schedulesLoading);
49
+ const fetchSchedules = useWorkforceStore((s) => s.fetchSchedules);
50
+ const templates = useWorkforceStore((s) => s.templates);
51
+ const fetchTemplates = useWorkforceStore((s) => s.fetchTemplates);
52
+ const staff = useWorkforceStore((s) => s.staff);
53
+ const fetchStaff = useWorkforceStore((s) => s.fetchStaff);
54
+ const [month, setMonth] = React3.useState(() => monthBounds(selectedDate));
55
+ const [templateId, setTemplateId] = React3.useState("");
56
+ const [busy, setBusy] = React3.useState(false);
57
+ const [personId, setPersonId] = React3.useState("");
58
+ React3.useEffect(() => {
59
+ void fetchSchedules(month.from, month.to);
60
+ }, [fetchSchedules, month.from, month.to]);
61
+ React3.useEffect(() => {
62
+ void fetchTemplates();
63
+ }, [fetchTemplates]);
64
+ React3.useEffect(() => {
65
+ void fetchStaff();
66
+ }, [fetchStaff]);
67
+ const day = schedules.find((s) => s.scheduleDate === selectedDate);
68
+ const markers = React3.useMemo(() => {
69
+ const out = {};
70
+ for (const s of schedules) {
71
+ out[s.scheduleDate] = [{
72
+ color: s.status === "published" ? "hsl(var(--success))" : "hsl(var(--warning))",
73
+ label: s.status === "published" ? "Escala publicada" : "Escala em rascunho"
74
+ }];
75
+ }
76
+ return out;
77
+ }, [schedules]);
78
+ async function createFromTemplate() {
79
+ if (!templateId) return;
80
+ setBusy(true);
81
+ try {
82
+ await provider.createSchedule({ scheduleDate: selectedDate, templateId });
83
+ await fetchSchedules(month.from, month.to);
84
+ toast.success("Escala criada a partir do molde.");
85
+ } catch (e) {
86
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel criar a escala.");
87
+ } finally {
88
+ setBusy(false);
89
+ }
90
+ }
91
+ async function publish() {
92
+ if (!day) return;
93
+ setBusy(true);
94
+ try {
95
+ await provider.publishSchedule(day.id);
96
+ await fetchSchedules(month.from, month.to);
97
+ toast.success("Escala publicada \u2014 os titulares foram espelhados na agenda.");
98
+ } catch (e) {
99
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel publicar a escala.");
100
+ } finally {
101
+ setBusy(false);
102
+ }
103
+ }
104
+ const titulares = day?.assignments.filter((a) => a.role === "titular") ?? [];
105
+ const pendentes = titulares.filter((a) => a.status === "pending" || a.status === "notified");
106
+ const rail = /* @__PURE__ */ jsx(
107
+ FilterRail,
108
+ {
109
+ before: /* @__PURE__ */ jsx(
110
+ MiniCalendar,
111
+ {
112
+ selectedDate: /* @__PURE__ */ new Date(`${selectedDate}T12:00:00`),
113
+ onDateSelect: (d) => setSelectedDate(toLocalISODate(d)),
114
+ onMonthChange: (d) => setMonth(monthBounds(toLocalISODate(d))),
115
+ markers
116
+ }
117
+ ),
118
+ sections: [
119
+ {
120
+ id: "fixed-hours",
121
+ title: "Hor\xE1rio fixo da pessoa",
122
+ content: /* @__PURE__ */ jsxs(Fragment, { children: [
123
+ /* @__PURE__ */ jsxs(Select, { value: personId, onValueChange: setPersonId, children: [
124
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Escolha quem" }) }),
125
+ /* @__PURE__ */ jsx(SelectContent, { children: staff.map((p) => /* @__PURE__ */ jsx(SelectItem, { value: p.id, children: p.name }, p.id)) })
126
+ ] }),
127
+ personId && /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(ScheduleEditor, { assigneeId: personId }) })
128
+ ] })
129
+ }
130
+ ]
131
+ }
132
+ );
133
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-6 p-6 lg:grid-cols-[260px_minmax(0,1fr)]", children: [
134
+ /* @__PURE__ */ jsx("aside", { className: "self-start rounded-xl border border-border bg-card p-4", children: rail }),
135
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-4", children: [
136
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
137
+ /* @__PURE__ */ jsxs("div", { children: [
138
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: (/* @__PURE__ */ new Date(`${selectedDate}T12:00:00`)).toLocaleDateString("pt-BR", {
139
+ weekday: "long",
140
+ day: "2-digit",
141
+ month: "long"
142
+ }) }),
143
+ day && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2 text-xs text-muted-foreground", children: [
144
+ /* @__PURE__ */ jsx(Badge, { variant: day.status === "published" ? "success" : "warning", children: STATUS_LABEL[day.status] ?? day.status }),
145
+ /* @__PURE__ */ jsxs("span", { children: [
146
+ titulares.length,
147
+ " titular(es) \xB7 ",
148
+ pendentes.length,
149
+ " pendente(s)"
150
+ ] })
151
+ ] })
152
+ ] }),
153
+ day && day.status === "draft" && /* @__PURE__ */ jsxs(Button, { onClick: publish, disabled: busy, children: [
154
+ /* @__PURE__ */ jsx(Send, { className: "mr-2 h-4 w-4", "aria-hidden": true }),
155
+ "Publicar escala"
156
+ ] })
157
+ ] }),
158
+ loading ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
159
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" }),
160
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" })
161
+ ] }) : day ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: day.assignments.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhuma vaga preenchida neste dia." }) : day.assignments.map((a) => /* @__PURE__ */ jsx(AssignmentRow, { assignment: a }, a.id)) }) : (
162
+ // O pré-requisito duro do V1, dito na tela em vez de escondido: sem
163
+ // molde não existe escala.
164
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-dashed border-border p-8 text-center", children: [
165
+ /* @__PURE__ */ jsx(CalendarDays, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground", "aria-hidden": true }),
166
+ /* @__PURE__ */ jsx("p", { className: "mb-4 text-sm text-muted-foreground", children: templates.length === 0 ? "Nenhum molde de escala cadastrado. Crie um em Templates para come\xE7ar." : "Ainda n\xE3o h\xE1 escala neste dia. Escolha um molde para mont\xE1-la." }),
167
+ templates.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-sm items-center gap-2", children: [
168
+ /* @__PURE__ */ jsxs(Select, { value: templateId, onValueChange: setTemplateId, children: [
169
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Molde de escala" }) }),
170
+ /* @__PURE__ */ jsx(SelectContent, { children: templates.map((t) => /* @__PURE__ */ jsx(SelectItem, { value: t.id, children: t.name }, t.id)) })
171
+ ] }),
172
+ /* @__PURE__ */ jsxs(Button, { onClick: createFromTemplate, disabled: !templateId || busy, children: [
173
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4", "aria-hidden": true }),
174
+ "Criar"
175
+ ] })
176
+ ] })
177
+ ] })
178
+ )
179
+ ] })
180
+ ] });
181
+ }
182
+ var WEEKDAYS = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "S\xE1b"];
183
+ var WEEKDAY_NAMES = ["Domingo", "Segunda", "Ter\xE7a", "Quarta", "Quinta", "Sexta", "S\xE1bado"];
184
+ function plural(n, one, many) {
185
+ return `${n} ${n === 1 ? one : many}`;
186
+ }
187
+ function seats(shift) {
188
+ return shift.requirements.reduce((sum, r) => sum + (r.requiredCount || 0), 0);
189
+ }
190
+ function ShiftCard({ shift, onChanged }) {
191
+ const provider = useWorkforceProvider();
192
+ const [profession, setProfession] = React3.useState("");
193
+ const [count, setCount] = React3.useState("1");
194
+ async function addRequirement() {
195
+ if (!profession.trim()) return;
196
+ try {
197
+ await provider.saveRequirement({
198
+ shiftId: shift.id,
199
+ professionName: profession.trim(),
200
+ requiredCount: Math.max(1, Number(count) || 1)
201
+ });
202
+ setProfession("");
203
+ setCount("1");
204
+ onChanged();
205
+ } catch (e) {
206
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel salvar a fun\xE7\xE3o.");
207
+ }
208
+ }
209
+ async function removeRequirement(id) {
210
+ try {
211
+ await provider.deleteRequirement(id);
212
+ onChanged();
213
+ } catch (e) {
214
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel remover a fun\xE7\xE3o.");
215
+ }
216
+ }
217
+ async function removeShift() {
218
+ try {
219
+ await provider.deleteShift(shift.id);
220
+ onChanged();
221
+ } catch (e) {
222
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel remover o turno.");
223
+ }
224
+ }
225
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border p-3", children: [
226
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
227
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
228
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: shift.label }),
229
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
230
+ shift.startTime,
231
+ "\u2013",
232
+ shift.endTime,
233
+ shift.crossesMidnight ? " \xB7 vira o dia" : ""
234
+ ] }),
235
+ /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: WEEKDAYS.map((d, i) => /* @__PURE__ */ jsx(
236
+ "span",
237
+ {
238
+ className: `inline-flex h-5 items-center justify-center rounded px-1.5 text-[10px] ${shift.weekdays.includes(i) ? "bg-primary/15 text-primary" : "bg-muted text-muted-foreground"}`,
239
+ children: d
240
+ },
241
+ i
242
+ )) })
243
+ ] }),
244
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: removeShift, "aria-label": "Remover turno", children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4", "aria-hidden": true }) })
245
+ ] }),
246
+ /* @__PURE__ */ jsx("div", { className: "mt-3 flex flex-wrap gap-2", children: shift.requirements.map((r) => /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "gap-1", children: [
247
+ r.professionName,
248
+ " (",
249
+ r.requiredCount,
250
+ ")",
251
+ /* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", onClick: () => removeRequirement(r.id), "aria-label": `Remover ${r.professionName}`, children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3", "aria-hidden": true }) })
252
+ ] }, r.id)) }),
253
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2", children: [
254
+ /* @__PURE__ */ jsx("div", { className: "w-full sm:w-56", children: /* @__PURE__ */ jsx(
255
+ Input,
256
+ {
257
+ value: profession,
258
+ onChange: (e) => setProfession(e.target.value),
259
+ placeholder: "Fun\xE7\xE3o (ex.: Gar\xE7om)",
260
+ className: "h-8"
261
+ }
262
+ ) }),
263
+ /* @__PURE__ */ jsx("div", { className: "w-20", children: /* @__PURE__ */ jsx(
264
+ Input,
265
+ {
266
+ value: count,
267
+ onChange: (e) => setCount(e.target.value),
268
+ type: "number",
269
+ min: 1,
270
+ className: "h-8",
271
+ "aria-label": "Vagas"
272
+ }
273
+ ) }),
274
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: addRequirement, children: "Adicionar" })
275
+ ] })
276
+ ] });
277
+ }
278
+ function NewShiftModal({ template, open, onOpenChange, onChanged }) {
279
+ const provider = useWorkforceProvider();
280
+ const [label, setLabel] = React3.useState("");
281
+ const [start, setStart] = React3.useState("11:00");
282
+ const [end, setEnd] = React3.useState("15:00");
283
+ const [weekdays, setWeekdays] = React3.useState([1, 2, 3, 4, 5]);
284
+ async function addShift() {
285
+ if (!label.trim()) return;
286
+ try {
287
+ await provider.saveShift({
288
+ templateId: template.id,
289
+ label: label.trim(),
290
+ startTime: start,
291
+ endTime: end,
292
+ weekdays,
293
+ sortOrder: template.shifts.length
294
+ });
295
+ onOpenChange(false);
296
+ setLabel("");
297
+ onChanged();
298
+ } catch (e) {
299
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel salvar o turno.");
300
+ }
301
+ }
302
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
303
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Novo turno" }) }),
304
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3", children: [
305
+ /* @__PURE__ */ jsx(Input, { value: label, onChange: (e) => setLabel(e.target.value), placeholder: "Nome do turno (ex.: Almo\xE7o)" }),
306
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
307
+ /* @__PURE__ */ jsx(Input, { type: "time", value: start, onChange: (e) => setStart(e.target.value), "aria-label": "In\xEDcio" }),
308
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "at\xE9" }),
309
+ /* @__PURE__ */ jsx(Input, { type: "time", value: end, onChange: (e) => setEnd(e.target.value), "aria-label": "Fim" })
310
+ ] }),
311
+ /* @__PURE__ */ jsxs("div", { children: [
312
+ /* @__PURE__ */ jsx("div", { className: "mb-1 text-xs text-muted-foreground", children: "Dias da semana" }),
313
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: WEEKDAYS.map((d, i) => /* @__PURE__ */ jsx(
314
+ Button,
315
+ {
316
+ variant: "bare",
317
+ size: "bare",
318
+ type: "button",
319
+ onClick: () => setWeekdays((w) => w.includes(i) ? w.filter((x) => x !== i) : [...w, i]),
320
+ className: `font-normal h-8 rounded px-2 text-xs ${weekdays.includes(i) ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`,
321
+ children: d
322
+ },
323
+ i
324
+ )) })
325
+ ] })
326
+ ] }),
327
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
328
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: "Cancelar" }),
329
+ /* @__PURE__ */ jsx(Button, { onClick: addShift, children: "Salvar turno" })
330
+ ] })
331
+ ] }) });
332
+ }
333
+ function TemplateBlock({ template, shifts, onChanged }) {
334
+ const provider = useWorkforceProvider();
335
+ const [open, setOpen] = React3.useState(false);
336
+ async function removeTemplate() {
337
+ try {
338
+ await provider.deleteTemplate(template.id);
339
+ onChanged();
340
+ } catch (e) {
341
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel remover o template.");
342
+ }
343
+ }
344
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border/60 bg-muted/20 p-3", children: [
345
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
346
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium", children: template.name }),
347
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
348
+ /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "secondary", onClick: () => setOpen(true), children: [
349
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1 h-4 w-4", "aria-hidden": true }),
350
+ " Turno"
351
+ ] }),
352
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: removeTemplate, "aria-label": `Remover ${template.name}`, children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4", "aria-hidden": true }) })
353
+ ] })
354
+ ] }),
355
+ shifts.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhum turno neste template ainda." }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: shifts.map((s) => /* @__PURE__ */ jsx(ShiftCard, { shift: s, onChanged }, s.id)) }),
356
+ /* @__PURE__ */ jsx(NewShiftModal, { template, open, onOpenChange: setOpen, onChanged })
357
+ ] });
358
+ }
359
+ function DayRow({ title, summary, empty, open, onToggle, children }) {
360
+ return /* @__PURE__ */ jsxs("div", { className: "border-b border-border last:border-b-0", children: [
361
+ /* @__PURE__ */ jsxs(
362
+ Button,
363
+ {
364
+ variant: "bare",
365
+ size: "bare",
366
+ type: "button",
367
+ onClick: onToggle,
368
+ "aria-expanded": open,
369
+ disabled: empty,
370
+ className: "flex w-full items-center justify-start gap-3 rounded-none px-3 py-2.5 text-left font-normal transition-colors hover:bg-muted/50 disabled:cursor-default disabled:opacity-100 disabled:hover:bg-transparent",
371
+ children: [
372
+ /* @__PURE__ */ jsx(
373
+ ChevronRight,
374
+ {
375
+ className: `h-4 w-4 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""} ${empty ? "opacity-0" : ""}`,
376
+ "aria-hidden": true
377
+ }
378
+ ),
379
+ /* @__PURE__ */ jsx("span", { className: `w-24 shrink-0 text-sm ${empty ? "text-muted-foreground" : "font-medium"}`, children: title }),
380
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: summary })
381
+ ]
382
+ }
383
+ ),
384
+ open && /* @__PURE__ */ jsx("div", { className: "space-y-3 px-3 pb-3", children })
385
+ ] });
386
+ }
387
+ function TemplatesView() {
388
+ const provider = useWorkforceProvider();
389
+ const templates = useWorkforceStore((s) => s.templates);
390
+ const loading = useWorkforceStore((s) => s.templatesLoading);
391
+ const fetchTemplates = useWorkforceStore((s) => s.fetchTemplates);
392
+ const [open, setOpen] = React3.useState(false);
393
+ const [name, setName] = React3.useState("");
394
+ const [openDay, setOpenDay] = React3.useState(null);
395
+ React3.useEffect(() => {
396
+ void fetchTemplates();
397
+ }, [fetchTemplates]);
398
+ const byDay = React3.useMemo(() => {
399
+ return WEEKDAY_NAMES.map((_, day) => {
400
+ const blocks = templates.map((t) => ({ template: t, shifts: t.shifts.filter((s) => s.weekdays.includes(day)) })).filter((b) => b.shifts.length > 0);
401
+ const shiftCount = blocks.reduce((n, b) => n + b.shifts.length, 0);
402
+ const seatCount = blocks.reduce((n, b) => n + b.shifts.reduce((m, s) => m + seats(s), 0), 0);
403
+ return { day, blocks, shiftCount, seatCount };
404
+ });
405
+ }, [templates]);
406
+ const withoutShifts = React3.useMemo(
407
+ () => templates.filter((t) => t.shifts.length === 0),
408
+ [templates]
409
+ );
410
+ async function createTemplate() {
411
+ if (!name.trim()) return;
412
+ try {
413
+ await provider.saveTemplate({ name: name.trim() });
414
+ setName("");
415
+ setOpen(false);
416
+ await fetchTemplates();
417
+ } catch (e) {
418
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel criar o template.");
419
+ }
420
+ }
421
+ const onChanged = React3.useCallback(() => {
422
+ void fetchTemplates();
423
+ }, [fetchTemplates]);
424
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 p-6", children: [
425
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
426
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Templates de escala" }),
427
+ /* @__PURE__ */ jsxs(Button, { onClick: () => setOpen(true), children: [
428
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4", "aria-hidden": true }),
429
+ " Novo template"
430
+ ] })
431
+ ] }),
432
+ loading ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
433
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }),
434
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })
435
+ ] }) : templates.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-dashed border-border p-8 text-center", children: [
436
+ /* @__PURE__ */ jsx(LayoutTemplate, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground", "aria-hidden": true }),
437
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhum template cadastrado." })
438
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
439
+ /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-xl border border-border", children: byDay.map(({ day, blocks, shiftCount, seatCount }) => /* @__PURE__ */ jsx(
440
+ DayRow,
441
+ {
442
+ title: WEEKDAY_NAMES[day],
443
+ summary: shiftCount === 0 ? "Ningu\xE9m escalado" : `${plural(shiftCount, "turno", "turnos")} \xB7 ${plural(seatCount, "vaga", "vagas")}`,
444
+ empty: shiftCount === 0,
445
+ open: openDay === day,
446
+ onToggle: () => setOpenDay((d) => d === day ? null : day),
447
+ children: blocks.map((b) => /* @__PURE__ */ jsx(TemplateBlock, { template: b.template, shifts: b.shifts, onChanged }, b.template.id))
448
+ },
449
+ day
450
+ )) }),
451
+ withoutShifts.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
452
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Sem turno \u2014 n\xE3o entram em dia nenhum at\xE9 ganharem o primeiro." }),
453
+ withoutShifts.map((t) => /* @__PURE__ */ jsx(TemplateBlock, { template: t, shifts: [], onChanged }, t.id))
454
+ ] })
455
+ ] }),
456
+ /* @__PURE__ */ jsx(Modal, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
457
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Novo template" }) }),
458
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsx(Input, { value: name, onChange: (e) => setName(e.target.value), placeholder: "Nome (ex.: Almo\xE7o Seg. a Sex.)" }) }),
459
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
460
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => setOpen(false), children: "Cancelar" }),
461
+ /* @__PURE__ */ jsx(Button, { onClick: createTemplate, children: "Criar" })
462
+ ] })
463
+ ] }) })
464
+ ] });
465
+ }
466
+ var KIND_LABEL = {
467
+ clock_in: "Entrada",
468
+ break_start: "Sa\xEDda p/ intervalo",
469
+ break_end: "Volta do intervalo",
470
+ clock_out: "Sa\xEDda"
471
+ };
472
+ function hhmm(iso) {
473
+ if (!iso) return "\u2014";
474
+ return new Date(iso).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
475
+ }
476
+ function hours(minutes) {
477
+ if (minutes == null) return "\u2014";
478
+ const sign = minutes < 0 ? "-" : "";
479
+ const abs = Math.abs(minutes);
480
+ return `${sign}${Math.floor(abs / 60)}h${String(abs % 60).padStart(2, "0")}`;
481
+ }
482
+ function iniciais(nome) {
483
+ const partes = (nome ?? "").trim().split(/\s+/).filter(Boolean);
484
+ if (!partes.length) return "\u2014";
485
+ return (partes[0][0] + (partes.length > 1 ? partes[partes.length - 1][0] : "")).toUpperCase();
486
+ }
487
+ var FotosContext = React3.createContext({});
488
+ function usePunchPhotoUrl(photo) {
489
+ const mapa = React3.useContext(FotosContext);
490
+ if (!photo) return null;
491
+ return mapa[`${photo.bucket}/${photo.path}`] ?? null;
492
+ }
493
+ function useFotosDaPagina(entries) {
494
+ const provider = useWorkforceProvider();
495
+ const [mapa, setMapa] = React3.useState({});
496
+ const chave = React3.useMemo(
497
+ () => entries.map((e) => e.photo ? `${e.photo.bucket}/${e.photo.path}` : "").filter(Boolean).sort().join("|"),
498
+ [entries]
499
+ );
500
+ React3.useEffect(() => {
501
+ const fotos = entries.map((e) => e.photo).filter(Boolean);
502
+ if (!fotos.length) {
503
+ setMapa({});
504
+ return;
505
+ }
506
+ let vivo = true;
507
+ provider.signPunchPhotos(fotos).then((m) => {
508
+ if (vivo) setMapa(m);
509
+ }).catch(() => {
510
+ if (vivo) setMapa({});
511
+ });
512
+ return () => {
513
+ vivo = false;
514
+ };
515
+ }, [provider, chave]);
516
+ return mapa;
517
+ }
518
+ function provaDe(entry) {
519
+ const e = entry.evidence ?? {};
520
+ const num = (v) => typeof v === "number" ? v : typeof v === "string" ? Number(v) : void 0;
521
+ return {
522
+ lat: num(e.lat),
523
+ lng: num(e.lng),
524
+ accuracy: num(e.accuracy_m),
525
+ faceScore: num(e.face_score),
526
+ // Houve captura E a imagem não atravessou. É o estado que a tela precisa
527
+ // distinguir de "não houve captura" — dizer "sem foto" nos dois casos
528
+ // esconderia 1.320 imagens que existem e não chegaram.
529
+ naoMigrada: !!e.photo_path && !entry.photo
530
+ };
531
+ }
532
+ function FotoDaBatida({ entry, className }) {
533
+ const url = usePunchPhotoUrl(entry.photo);
534
+ return /* @__PURE__ */ jsxs(Avatar, { className: className ?? "h-8 w-8 shrink-0", children: [
535
+ url && /* @__PURE__ */ jsx(AvatarImage, { src: url, alt: `Foto da batida de ${entry.personName ?? "quem bateu"}` }),
536
+ /* @__PURE__ */ jsx(AvatarFallback, { children: iniciais(entry.personName) })
537
+ ] });
538
+ }
539
+ function ProvaDoTotem({ entry }) {
540
+ const p = provaDe(entry);
541
+ const temAlgo = entry.photo || p.naoMigrada || p.lat != null || p.faceScore != null;
542
+ if (!temAlgo) return null;
543
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 pt-1", children: [
544
+ entry.photo ? /* @__PURE__ */ jsx(FotoDaBatida, { entry, className: "h-12 w-12 shrink-0 rounded-md" }) : p.naoMigrada ? /* @__PURE__ */ jsx(
545
+ "span",
546
+ {
547
+ className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-md border border-dashed border-border text-muted-foreground",
548
+ title: "O totem capturou a foto; a imagem ainda n\xE3o atravessou do V1",
549
+ children: /* @__PURE__ */ jsx(ImageOff, { className: "h-4 w-4", "aria-hidden": true })
550
+ }
551
+ ) : null,
552
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
553
+ p.lat != null && p.lng != null && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
554
+ /* @__PURE__ */ jsx(MapPin, { className: "h-3.5 w-3.5", "aria-hidden": true }),
555
+ p.lat.toFixed(5),
556
+ ", ",
557
+ p.lng.toFixed(5),
558
+ p.accuracy != null && /* @__PURE__ */ jsxs(Fragment, { children: [
559
+ " \xB7 \xB1 ",
560
+ Math.round(p.accuracy),
561
+ "m"
562
+ ] })
563
+ ] }),
564
+ p.faceScore != null && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
565
+ /* @__PURE__ */ jsx(ScanFace, { className: "h-3.5 w-3.5", "aria-hidden": true }),
566
+ "Match facial ",
567
+ (p.faceScore * 100).toFixed(1),
568
+ "%"
569
+ ] }),
570
+ p.naoMigrada && /* @__PURE__ */ jsx("span", { children: "Foto capturada no V1, ainda n\xE3o migrada" })
571
+ ] })
572
+ ] });
573
+ }
574
+ function DetalhesDoDia({ day, entries, onClose }) {
575
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (o) => {
576
+ if (!o) onClose();
577
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { children: [
578
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsxs(ModalTitle, { children: [
579
+ day.personName ?? "Sem nome",
580
+ " \xB7 ",
581
+ day.workDate
582
+ ] }) }),
583
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3", children: [
584
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-4 text-sm", children: [
585
+ /* @__PURE__ */ jsxs("span", { children: [
586
+ "Previsto ",
587
+ /* @__PURE__ */ jsx("b", { children: hours(day.plannedMinutes) })
588
+ ] }),
589
+ /* @__PURE__ */ jsxs("span", { children: [
590
+ "Realizado ",
591
+ /* @__PURE__ */ jsx("b", { children: hours(day.workedMinutes) })
592
+ ] }),
593
+ day.varianceMinutes != null && /* @__PURE__ */ jsxs("span", { children: [
594
+ "Saldo ",
595
+ /* @__PURE__ */ jsxs("b", { children: [
596
+ day.varianceMinutes >= 0 ? "+" : "",
597
+ hours(day.varianceMinutes)
598
+ ] })
599
+ ] }),
600
+ day.shiftLabel && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
601
+ "Turno ",
602
+ day.shiftLabel,
603
+ " ",
604
+ day.shiftStartTime,
605
+ "\u2013",
606
+ day.shiftEndTime
607
+ ] })
608
+ ] }),
609
+ /* @__PURE__ */ jsx("div", { className: "divide-y divide-border rounded-lg border border-border", children: entries.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-sm text-muted-foreground", children: "Nenhuma batida neste dia." }) : entries.map((e) => /* @__PURE__ */ jsxs("div", { className: "px-3 py-2 text-sm", children: [
610
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
611
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
612
+ /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": true }),
613
+ /* @__PURE__ */ jsxs("span", { className: e.voided ? "text-muted-foreground line-through" : "", children: [
614
+ KIND_LABEL[e.kind] ?? e.kind,
615
+ " \xB7 ",
616
+ hhmm(e.effectiveAt)
617
+ ] }),
618
+ e.adjusted && // O real fica à vista, riscado. Esconder o horário original
619
+ // transformaria o ajuste numa edição, que é justamente o que
620
+ // o modelo append-only existe para impedir.
621
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
622
+ "(real ",
623
+ /* @__PURE__ */ jsx("s", { children: hhmm(e.punchedAt) }),
624
+ ")"
625
+ ] })
626
+ ] }),
627
+ e.adjusted && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
628
+ e.adjustedByName ?? "algu\xE9m",
629
+ ": ",
630
+ e.adjustmentReason
631
+ ] })
632
+ ] }),
633
+ /* @__PURE__ */ jsx(ProvaDoTotem, { entry: e })
634
+ ] }, e.id)) })
635
+ ] }),
636
+ /* @__PURE__ */ jsx(ModalFooter, { children: /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Fechar" }) })
637
+ ] }) });
638
+ }
639
+ function AdjustDialog({ entry, onClose, onSaved }) {
640
+ const provider = useWorkforceProvider();
641
+ const [action, setAction] = React3.useState("correct");
642
+ const [time, setTime] = React3.useState(() => new Date(entry.effectiveAt).toTimeString().slice(0, 5));
643
+ const [reason, setReason] = React3.useState("");
644
+ const [busy, setBusy] = React3.useState(false);
645
+ async function save() {
646
+ if (!reason.trim()) {
647
+ toast.error("O motivo \xE9 obrigat\xF3rio.");
648
+ return;
649
+ }
650
+ setBusy(true);
651
+ try {
652
+ await provider.adjustTimeEntry({
653
+ timeEntryId: entry.id,
654
+ action,
655
+ effectiveAt: action === "void" ? void 0 : (/* @__PURE__ */ new Date(`${entry.workDate}T${time}:00`)).toISOString(),
656
+ reason: reason.trim()
657
+ });
658
+ toast.success("Ajuste registrado. O hor\xE1rio real foi preservado.");
659
+ onSaved();
660
+ onClose();
661
+ } catch (e) {
662
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel registrar o ajuste.");
663
+ } finally {
664
+ setBusy(false);
665
+ }
666
+ }
667
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (o) => {
668
+ if (!o) onClose();
669
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
670
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: "Ajustar batida" }) }),
671
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3", children: [
672
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
673
+ entry.personName ?? "Sem nome",
674
+ " \xB7 ",
675
+ KIND_LABEL[entry.kind] ?? entry.kind,
676
+ " \xB7",
677
+ " ",
678
+ "registrado \xE0s ",
679
+ hhmm(entry.punchedAt)
680
+ ] }),
681
+ /* @__PURE__ */ jsxs(Select, { value: action, onValueChange: (v) => setAction(v), children: [
682
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
683
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
684
+ /* @__PURE__ */ jsx(SelectItem, { value: "correct", children: "Corrigir o hor\xE1rio" }),
685
+ /* @__PURE__ */ jsx(SelectItem, { value: "insert", children: "Inserir a batida que faltou" }),
686
+ /* @__PURE__ */ jsx(SelectItem, { value: "void", children: "Anular esta batida" })
687
+ ] })
688
+ ] }),
689
+ action !== "void" && /* @__PURE__ */ jsx(Input, { type: "time", value: time, onChange: (e) => setTime(e.target.value), "aria-label": "Hor\xE1rio que passa a valer" }),
690
+ /* @__PURE__ */ jsx(
691
+ Input,
692
+ {
693
+ value: reason,
694
+ onChange: (e) => setReason(e.target.value),
695
+ placeholder: "Motivo (obrigat\xF3rio)"
696
+ }
697
+ )
698
+ ] }),
699
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
700
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onClose, children: "Cancelar" }),
701
+ /* @__PURE__ */ jsx(Button, { onClick: save, disabled: busy, children: "Registrar ajuste" })
702
+ ] })
703
+ ] }) });
704
+ }
705
+ function useAgora(ativo) {
706
+ const [agora, setAgora] = React3.useState(() => Date.now());
707
+ React3.useEffect(() => {
708
+ if (!ativo) return;
709
+ const id = setInterval(() => setAgora(Date.now()), 6e4);
710
+ return () => clearInterval(id);
711
+ }, [ativo]);
712
+ return agora;
713
+ }
714
+ var LIMITE_EM_CURSO_MIN = 16 * 60;
715
+ function Duracao({ day, agora }) {
716
+ if (day.reviewReason === "span_too_long") {
717
+ const dias = day.firstIn && day.lastOut ? Math.round((new Date(day.lastOut).getTime() - new Date(day.firstIn).getTime()) / 864e5) : 0;
718
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
719
+ /* @__PURE__ */ jsx("span", { className: "text-destructive", children: hours(day.workedMinutes) }),
720
+ /* @__PURE__ */ jsxs(
721
+ "span",
722
+ {
723
+ className: "block text-xs text-destructive",
724
+ title: "Fora da soma: acima de 16h num dia \xE9 sa\xEDda n\xE3o batida, n\xE3o jornada. Corrija pelo bot\xE3o Ajustar.",
725
+ children: [
726
+ "revisar \xB7 ",
727
+ dias > 0 ? `sa\xEDda ${dias}d depois` : "sa\xEDda no dia seguinte"
728
+ ]
729
+ }
730
+ )
731
+ ] });
732
+ }
733
+ if (!day.isOpen) {
734
+ return /* @__PURE__ */ jsx(Fragment, { children: hours(day.workedMinutes) });
735
+ }
736
+ const fechadas = day.workedMinutes ?? 0;
737
+ const correndo = day.openSince ? Math.max(0, Math.round((agora - new Date(day.openSince).getTime()) / 6e4)) : null;
738
+ const esquecido = correndo != null && correndo > LIMITE_EM_CURSO_MIN;
739
+ const correndoTexto = correndo == null ? "em aberto" : esquecido ? `sem sa\xEDda h\xE1 ${hours(correndo)}` : `${hours(correndo)} em curso`;
740
+ const cor = esquecido ? "text-destructive" : "text-muted-foreground";
741
+ const desde = day.openSince ? `Aberto desde ${new Date(day.openSince).toLocaleString("pt-BR")}` : void 0;
742
+ if (fechadas === 0) {
743
+ return /* @__PURE__ */ jsx("span", { className: cor, title: desde, children: correndoTexto });
744
+ }
745
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
746
+ hours(fechadas),
747
+ /* @__PURE__ */ jsxs("span", { className: `block text-xs ${cor}`, title: desde, children: [
748
+ "+ ",
749
+ correndoTexto
750
+ ] })
751
+ ] });
752
+ }
753
+ function SeloEscala({ day }) {
754
+ if (day.absent) return /* @__PURE__ */ jsx(Badge, { variant: "destructive", children: "Falta" });
755
+ if (day.outsideSchedule) return /* @__PURE__ */ jsx(Badge, { variant: "destructive", children: "Fora da escala" });
756
+ return /* @__PURE__ */ jsx(Badge, { variant: "success", children: "Na escala" });
757
+ }
758
+ function dayFacts(day, entries, hourlyRate) {
759
+ const value = hourlyRate != null && day.workedMinutes != null && !day.reviewReason ? day.workedMinutes / 60 * hourlyRate : null;
760
+ const alvo = entries.length ? entries[entries.length - 1] : null;
761
+ const travado = entries.some((e) => e.payableInvoiceId);
762
+ const retrato = entries.find((e) => e.kind === "clock_in" && e.photo) ?? entries.find((e) => e.photo) ?? null;
763
+ return { value, alvo, travado, retrato };
764
+ }
765
+ function tituloDoDia(workDate) {
766
+ const d = /* @__PURE__ */ new Date(`${workDate}T12:00:00`);
767
+ const hoje = /* @__PURE__ */ new Date();
768
+ const iso = (x) => `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}-${String(x.getDate()).padStart(2, "0")}`;
769
+ const ontem = new Date(hoje.getTime() - 864e5);
770
+ const extenso = d.toLocaleDateString("pt-BR", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
771
+ if (workDate === iso(hoje)) return `Hoje, ${extenso}`;
772
+ if (workDate === iso(ontem)) return `Ontem, ${extenso}`;
773
+ return extenso.charAt(0).toUpperCase() + extenso.slice(1);
774
+ }
775
+ function GrupoDoDia({ workDate, days, entriesByPerson, unitNames, agora, onAdjust, onDetails }) {
776
+ const { currency, hourlyRate, staffLookup } = useWorkforceConfig();
777
+ const pessoas = new Set(days.map((d) => d.personId)).size;
778
+ const abertos = days.filter((d) => d.isOpen).length;
779
+ const revisar = days.filter((d) => d.reviewReason === "span_too_long").length;
780
+ const minutos = days.reduce((sum, d) => sum + (d.reviewReason ? 0 : d.workedMinutes ?? 0), 0);
781
+ const valor = hourlyRate != null ? minutos / 60 * hourlyRate : null;
782
+ const columns = React3.useMemo(() => {
783
+ const head = (label) => () => /* @__PURE__ */ jsx("span", { className: "text-xs", children: label });
784
+ const facts = (day) => dayFacts(day, entriesByPerson.get(`${day.personId}|${day.workDate}`) ?? [], hourlyRate);
785
+ return [
786
+ {
787
+ id: "name",
788
+ header: head("Nome"),
789
+ cell: ({ row: { original: day } }) => {
790
+ const { retrato } = facts(day);
791
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
792
+ retrato ? /* @__PURE__ */ jsx(FotoDaBatida, { entry: retrato }) : /* @__PURE__ */ jsx(Avatar, { className: "h-8 w-8 shrink-0", children: /* @__PURE__ */ jsx(AvatarFallback, { children: iniciais(day.personName) }) }),
793
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
794
+ /* @__PURE__ */ jsx(
795
+ PersonLink,
796
+ {
797
+ personId: day.personId,
798
+ name: day.personName ?? "Sem nome",
799
+ kind: "staff",
800
+ lookup: staffLookup,
801
+ size: "sm",
802
+ className: "text-sm"
803
+ }
804
+ ),
805
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: "Profissional" })
806
+ ] })
807
+ ] });
808
+ }
809
+ },
810
+ {
811
+ id: "unit",
812
+ header: head("Unidade"),
813
+ cell: ({ row: { original: day } }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: day.unitId && unitNames.get(day.unitId) || "\u2014" })
814
+ },
815
+ { id: "in", header: head("Entrada"), cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: hhmm(row.original.firstIn) }) },
816
+ {
817
+ id: "out",
818
+ header: head("Sa\xEDda"),
819
+ cell: ({ row: { original: day } }) => /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: day.isOpen ? /* @__PURE__ */ jsx(Badge, { variant: "outline", children: "Em aberto" }) : hhmm(day.lastOut) })
820
+ },
821
+ { id: "duration", header: head("Dura\xE7\xE3o"), cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: /* @__PURE__ */ jsx(Duracao, { day: row.original, agora }) }) },
822
+ {
823
+ id: "value",
824
+ header: head("Valor"),
825
+ cell: ({ row: { original: day } }) => {
826
+ const { value } = facts(day);
827
+ return /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: value != null ? formatCurrency(value, currency) : "\u2014" });
828
+ }
829
+ },
830
+ { id: "schedule", header: head("Escala"), cell: ({ row }) => /* @__PURE__ */ jsx(SeloEscala, { day: row.original }) },
831
+ {
832
+ id: "actions",
833
+ header: head("A\xE7\xF5es"),
834
+ cell: ({ row: { original: day } }) => {
835
+ const { alvo, travado } = facts(day);
836
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
837
+ travado ? (
838
+ // A trava do banco, dita na tela: hora já paga não se corrige sem
839
+ // reabrir a competência.
840
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 px-2 text-xs text-muted-foreground", children: [
841
+ /* @__PURE__ */ jsx(Lock, { className: "h-3.5 w-3.5", "aria-hidden": true }),
842
+ " j\xE1 paga"
843
+ ] })
844
+ ) : alvo ? /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "ghost", onClick: () => onAdjust(alvo), children: [
845
+ /* @__PURE__ */ jsx(Pencil, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": true }),
846
+ " Ajustar"
847
+ ] }) : null,
848
+ /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "ghost", onClick: () => onDetails(day), children: [
849
+ /* @__PURE__ */ jsx(Eye, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": true }),
850
+ " Detalhes"
851
+ ] })
852
+ ] });
853
+ }
854
+ }
855
+ ];
856
+ }, [entriesByPerson, unitNames, agora, onAdjust, onDetails, currency, hourlyRate, staffLookup]);
857
+ return /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-xl border border-border", children: [
858
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4 border-l-2 border-l-primary bg-muted/40 px-4 py-3", children: [
859
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: tituloDoDia(workDate) }),
860
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
861
+ /* @__PURE__ */ jsx(Users, { className: "h-3.5 w-3.5", "aria-hidden": true }),
862
+ " ",
863
+ pessoas,
864
+ " pessoa",
865
+ pessoas === 1 ? "" : "s"
866
+ ] }),
867
+ abertos > 0 && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs font-medium text-warning", children: [
868
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5", "aria-hidden": true }),
869
+ " ",
870
+ abertos,
871
+ " em aberto"
872
+ ] }),
873
+ revisar > 0 && /* @__PURE__ */ jsxs(
874
+ "span",
875
+ {
876
+ className: "flex items-center gap-1 text-xs font-medium text-destructive",
877
+ title: "Dias acima de 16h \u2014 sa\xEDda n\xE3o batida. Fora do total at\xE9 serem ajustados.",
878
+ children: [
879
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5", "aria-hidden": true }),
880
+ " ",
881
+ revisar,
882
+ " a revisar"
883
+ ]
884
+ }
885
+ ),
886
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
887
+ /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5", "aria-hidden": true }),
888
+ " ",
889
+ hours(minutos)
890
+ ] }),
891
+ valor != null && valor > 0 && /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-success", children: formatCurrency(valor, currency) })
892
+ ] }),
893
+ /* @__PURE__ */ jsx(DataTable, { variant: "bare", columns, data: days, getRowId: (d) => `${d.personId}|${d.workDate}` })
894
+ ] });
895
+ }
896
+ function TimeEntriesView() {
897
+ const { currency, hourlyRate } = useWorkforceConfig();
898
+ const entries = useWorkforceStore((s) => s.entries);
899
+ const days = useWorkforceStore((s) => s.days);
900
+ const loading = useWorkforceStore((s) => s.timeLoading);
901
+ const fetchTime = useWorkforceStore((s) => s.fetchTime);
902
+ const [preset, setPreset] = React3.useState("last_7_days");
903
+ const [range, setRange] = React3.useState(() => resolveRangePreset("last_7_days"));
904
+ const from = range.from;
905
+ const to = range.to;
906
+ const [adjusting, setAdjusting] = React3.useState(null);
907
+ const [details, setDetails] = React3.useState(null);
908
+ React3.useEffect(() => {
909
+ void fetchTime(from, to);
910
+ }, [fetchTime, from, to]);
911
+ const temAberto = React3.useMemo(() => days.some((d) => d.isOpen), [days]);
912
+ const agora = useAgora(temAberto);
913
+ const entriesByPerson = React3.useMemo(() => {
914
+ const map = /* @__PURE__ */ new Map();
915
+ for (const e of entries) {
916
+ const key = `${e.personId}|${e.workDate}`;
917
+ const list = map.get(key) ?? [];
918
+ list.push(e);
919
+ map.set(key, list);
920
+ }
921
+ for (const list of map.values()) list.sort((a, b) => a.seq - b.seq);
922
+ return map;
923
+ }, [entries]);
924
+ const porDia = React3.useMemo(() => {
925
+ const map = /* @__PURE__ */ new Map();
926
+ for (const d of days) {
927
+ const list = map.get(d.workDate) ?? [];
928
+ list.push(d);
929
+ map.set(d.workDate, list);
930
+ }
931
+ for (const list of map.values()) {
932
+ list.sort((a, b) => (a.personName ?? "").localeCompare(b.personName ?? ""));
933
+ }
934
+ return [...map.entries()].sort(([a], [b]) => b.localeCompare(a));
935
+ }, [days]);
936
+ const DIAS_POR_PAGINA = 7;
937
+ const [pagina, setPagina] = React3.useState(0);
938
+ React3.useEffect(() => {
939
+ setPagina(0);
940
+ }, [from, to]);
941
+ const totalPaginas = Math.max(1, Math.ceil(porDia.length / DIAS_POR_PAGINA));
942
+ const paginaAtual = Math.min(pagina, totalPaginas - 1);
943
+ const diasVisiveis = React3.useMemo(
944
+ () => porDia.slice(paginaAtual * DIAS_POR_PAGINA, (paginaAtual + 1) * DIAS_POR_PAGINA),
945
+ [porDia, paginaAtual]
946
+ );
947
+ const entradasVisiveis = React3.useMemo(() => {
948
+ const chaves = /* @__PURE__ */ new Set();
949
+ for (const [, lista] of diasVisiveis) for (const d of lista) chaves.add(`${d.personId}|${d.workDate}`);
950
+ return entries.filter((e) => chaves.has(`${e.personId}|${e.workDate}`));
951
+ }, [diasVisiveis, entries]);
952
+ const fotos = useFotosDaPagina(entradasVisiveis);
953
+ const unitNames = React3.useMemo(() => {
954
+ const map = /* @__PURE__ */ new Map();
955
+ for (const d of days) if (d.unitId) map.set(d.unitId, d.unitName ?? "\u2014");
956
+ return map;
957
+ }, [days]);
958
+ const totais = React3.useMemo(() => {
959
+ const minutos = days.reduce((sum, d) => sum + (d.reviewReason ? 0 : d.workedMinutes ?? 0), 0);
960
+ return {
961
+ registros: days.length,
962
+ minutos,
963
+ abertos: days.filter((d) => d.isOpen).length,
964
+ revisar: days.filter((d) => d.reviewReason === "span_too_long").length,
965
+ valor: hourlyRate != null ? minutos / 60 * hourlyRate : null
966
+ };
967
+ }, [days, hourlyRate]);
968
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 p-6", children: [
969
+ /* @__PURE__ */ jsx(
970
+ PageToolbar,
971
+ {
972
+ range: /* @__PURE__ */ jsx(
973
+ DateRangeControl,
974
+ {
975
+ value: range,
976
+ onChange: setRange,
977
+ preset,
978
+ onPresetChange: setPreset
979
+ }
980
+ ),
981
+ onRefresh: () => void fetchTime(from, to)
982
+ }
983
+ ),
984
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
985
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Resultados" }),
986
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4 text-sm text-muted-foreground", children: [
987
+ /* @__PURE__ */ jsxs("span", { children: [
988
+ totais.registros,
989
+ " registro(s)"
990
+ ] }),
991
+ /* @__PURE__ */ jsxs("span", { children: [
992
+ "Total: ",
993
+ /* @__PURE__ */ jsx("b", { className: "text-foreground", children: hours(totais.minutos) }),
994
+ (totais.abertos > 0 || totais.revisar > 0) && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs", children: [
995
+ "(fora:",
996
+ " ",
997
+ [
998
+ totais.abertos > 0 ? `${totais.abertos} em aberto` : null,
999
+ totais.revisar > 0 ? `${totais.revisar} a revisar` : null
1000
+ ].filter(Boolean).join(" \xB7 "),
1001
+ ")"
1002
+ ] })
1003
+ ] }),
1004
+ /* @__PURE__ */ jsxs("span", { children: [
1005
+ "Valor: ",
1006
+ /* @__PURE__ */ jsx("b", { className: "text-foreground", children: totais.valor != null ? formatCurrency(totais.valor, currency) : "\u2014" })
1007
+ ] })
1008
+ ] })
1009
+ ] }),
1010
+ hourlyRate == null && // Honestidade em vez de um número inventado: o SQL da jornada não guarda
1011
+ // remuneração em lugar nenhum, então a coluna Valor só existe quando o
1012
+ // app diz quanto vale a hora.
1013
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
1014
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5", "aria-hidden": true }),
1015
+ "O valor da hora n\xE3o est\xE1 configurado \u2014 informe ",
1016
+ /* @__PURE__ */ jsx("code", { children: "hourlyRate" }),
1017
+ " no plugin para ver a coluna Valor."
1018
+ ] }),
1019
+ loading ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1020
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full" }),
1021
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full" })
1022
+ ] }) : porDia.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-dashed border-border p-8 text-center", children: [
1023
+ /* @__PURE__ */ jsx(Clock, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground", "aria-hidden": true }),
1024
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhum registro no per\xEDodo." })
1025
+ ] }) : /* @__PURE__ */ jsx(FotosContext.Provider, { value: fotos, children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1026
+ diasVisiveis.map(([workDate, list]) => /* @__PURE__ */ jsx(
1027
+ GrupoDoDia,
1028
+ {
1029
+ workDate,
1030
+ days: list,
1031
+ entriesByPerson,
1032
+ unitNames,
1033
+ agora,
1034
+ onAdjust: setAdjusting,
1035
+ onDetails: setDetails
1036
+ },
1037
+ workDate
1038
+ )),
1039
+ totalPaginas > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-xl border border-border px-4 py-3 text-sm", children: [
1040
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
1041
+ "Dias ",
1042
+ paginaAtual * DIAS_POR_PAGINA + 1,
1043
+ "\u2013",
1044
+ Math.min((paginaAtual + 1) * DIAS_POR_PAGINA, porDia.length),
1045
+ " de ",
1046
+ porDia.length
1047
+ ] }),
1048
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
1049
+ /* @__PURE__ */ jsx(
1050
+ Button,
1051
+ {
1052
+ size: "sm",
1053
+ variant: "outline",
1054
+ disabled: paginaAtual === 0,
1055
+ onClick: () => setPagina((n) => Math.max(0, n - 1)),
1056
+ children: "Mais recentes"
1057
+ }
1058
+ ),
1059
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground tabular-nums", children: [
1060
+ paginaAtual + 1,
1061
+ " / ",
1062
+ totalPaginas
1063
+ ] }),
1064
+ /* @__PURE__ */ jsx(
1065
+ Button,
1066
+ {
1067
+ size: "sm",
1068
+ variant: "outline",
1069
+ disabled: paginaAtual >= totalPaginas - 1,
1070
+ onClick: () => setPagina((n) => Math.min(totalPaginas - 1, n + 1)),
1071
+ children: "Mais antigos"
1072
+ }
1073
+ )
1074
+ ] })
1075
+ ] })
1076
+ ] }) }),
1077
+ adjusting && /* @__PURE__ */ jsx(
1078
+ AdjustDialog,
1079
+ {
1080
+ entry: adjusting,
1081
+ onClose: () => setAdjusting(null),
1082
+ onSaved: () => void fetchTime(from, to)
1083
+ }
1084
+ ),
1085
+ details && /* @__PURE__ */ jsx(
1086
+ DetalhesDoDia,
1087
+ {
1088
+ day: details,
1089
+ entries: entriesByPerson.get(`${details.personId}|${details.workDate}`) ?? [],
1090
+ onClose: () => setDetails(null)
1091
+ }
1092
+ )
1093
+ ] });
1094
+ }
1095
+ function monthLabel(iso) {
1096
+ return (/* @__PURE__ */ new Date(`${iso}T12:00:00`)).toLocaleDateString("pt-BR", { month: "long", year: "numeric" });
1097
+ }
1098
+ function lastMonths(count) {
1099
+ const out = [];
1100
+ const d = /* @__PURE__ */ new Date();
1101
+ d.setDate(1);
1102
+ for (let i = 0; i < count; i++) {
1103
+ out.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-01`);
1104
+ d.setMonth(d.getMonth() - 1);
1105
+ }
1106
+ return out;
1107
+ }
1108
+ function PeriodsView() {
1109
+ const provider = useWorkforceProvider();
1110
+ const periods = useWorkforceStore((s) => s.periods);
1111
+ const loading = useWorkforceStore((s) => s.periodsLoading);
1112
+ const fetchPeriods = useWorkforceStore((s) => s.fetchPeriods);
1113
+ const [dialog, setDialog] = React3.useState(null);
1114
+ const [text, setText] = React3.useState("");
1115
+ const [busy, setBusy] = React3.useState(false);
1116
+ React3.useEffect(() => {
1117
+ void fetchPeriods();
1118
+ }, [fetchPeriods]);
1119
+ const byMonth = React3.useMemo(() => {
1120
+ const map = /* @__PURE__ */ new Map();
1121
+ for (const p of periods) map.set(p.competenceMonth, p);
1122
+ return map;
1123
+ }, [periods]);
1124
+ async function confirm() {
1125
+ if (!dialog) return;
1126
+ if (dialog.mode === "reopen" && !text.trim()) {
1127
+ toast.error("O motivo da reabertura \xE9 obrigat\xF3rio.");
1128
+ return;
1129
+ }
1130
+ setBusy(true);
1131
+ try {
1132
+ if (dialog.mode === "close") await provider.closePeriod(dialog.month, text.trim() || void 0);
1133
+ else await provider.reopenPeriod(dialog.month, text.trim());
1134
+ await fetchPeriods();
1135
+ toast.success(dialog.mode === "close" ? "Compet\xEAncia fechada." : "Compet\xEAncia reaberta.");
1136
+ setDialog(null);
1137
+ setText("");
1138
+ } catch (e) {
1139
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel concluir.");
1140
+ } finally {
1141
+ setBusy(false);
1142
+ }
1143
+ }
1144
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 p-6", children: [
1145
+ /* @__PURE__ */ jsxs("div", { children: [
1146
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Fechamento de compet\xEAncia" }),
1147
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "M\xEAs fechado n\xE3o aceita batida nem ajuste com data dentro dele. A trava \xE9 do banco." })
1148
+ ] }),
1149
+ loading ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1150
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-14 w-full" }),
1151
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-14 w-full" })
1152
+ ] }) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border rounded-xl border border-border", children: lastMonths(12).map((month) => {
1153
+ const period = byMonth.get(month);
1154
+ const closed = period?.status === "closed";
1155
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 px-4 py-3", children: [
1156
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1157
+ /* @__PURE__ */ jsx(CalendarCheck, { className: "h-4 w-4 text-muted-foreground", "aria-hidden": true }),
1158
+ /* @__PURE__ */ jsxs("div", { children: [
1159
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium capitalize", children: monthLabel(month) }),
1160
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: closed ? `Fechada em ${period?.closedAt ? new Date(period.closedAt).toLocaleDateString("pt-BR") : "\u2014"}` : period?.reopenReason ? `Reaberta: ${period.reopenReason}` : "Aberta" })
1161
+ ] })
1162
+ ] }),
1163
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1164
+ /* @__PURE__ */ jsx(Badge, { variant: closed ? "success" : "secondary", children: closed ? "Fechada" : "Aberta" }),
1165
+ /* @__PURE__ */ jsx(
1166
+ Button,
1167
+ {
1168
+ size: "sm",
1169
+ variant: closed ? "ghost" : "secondary",
1170
+ onClick: () => {
1171
+ setText("");
1172
+ setDialog({ month, mode: closed ? "reopen" : "close" });
1173
+ },
1174
+ children: closed ? /* @__PURE__ */ jsxs(Fragment, { children: [
1175
+ /* @__PURE__ */ jsx(Unlock, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": true }),
1176
+ " Reabrir"
1177
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1178
+ /* @__PURE__ */ jsx(Lock, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": true }),
1179
+ " Fechar"
1180
+ ] })
1181
+ }
1182
+ )
1183
+ ] })
1184
+ ] }, month);
1185
+ }) }),
1186
+ dialog && /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (o) => {
1187
+ if (!o) setDialog(null);
1188
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
1189
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsxs(ModalTitle, { children: [
1190
+ dialog.mode === "close" ? "Fechar" : "Reabrir",
1191
+ " ",
1192
+ monthLabel(dialog.month)
1193
+ ] }) }),
1194
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsx(
1195
+ Input,
1196
+ {
1197
+ value: text,
1198
+ onChange: (e) => setText(e.target.value),
1199
+ placeholder: dialog.mode === "close" ? "Observa\xE7\xE3o (opcional)" : "Motivo da reabertura (obrigat\xF3rio)"
1200
+ }
1201
+ ) }),
1202
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
1203
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => setDialog(null), children: "Cancelar" }),
1204
+ /* @__PURE__ */ jsx(Button, { onClick: confirm, disabled: busy, children: "Confirmar" })
1205
+ ] })
1206
+ ] }) })
1207
+ ] });
1208
+ }
1209
+ var KEY = "workforce.payroll_account_codes";
1210
+ function message(e) {
1211
+ if (e instanceof Error) return e.message;
1212
+ if (typeof e === "object" && e && "message" in e) return String(e.message);
1213
+ return String(e);
1214
+ }
1215
+ function normalize(value) {
1216
+ if (!Array.isArray(value)) return null;
1217
+ const codes = value.map((v) => String(v).trim()).filter(Boolean);
1218
+ return codes.length ? codes : null;
1219
+ }
1220
+ function usePayrollAccounts() {
1221
+ const client = React3.useMemo(() => getSupabaseClientOptional(), []);
1222
+ const supported = !!client;
1223
+ const [codes, setCodes] = React3.useState(null);
1224
+ const [loading, setLoading] = React3.useState(supported);
1225
+ const [saving, setSaving] = React3.useState(false);
1226
+ const [error, setError] = React3.useState(null);
1227
+ React3.useEffect(() => {
1228
+ if (!client) return;
1229
+ let cancelled = false;
1230
+ setLoading(true);
1231
+ void client.rpc("tenant_settings").then(({ data, error: e }) => {
1232
+ if (cancelled || e) return;
1233
+ setCodes(normalize((data ?? {})[KEY]));
1234
+ }).catch(() => {
1235
+ }).finally(() => {
1236
+ if (!cancelled) setLoading(false);
1237
+ });
1238
+ return () => {
1239
+ cancelled = true;
1240
+ };
1241
+ }, [client]);
1242
+ const save = React3.useCallback(async (next) => {
1243
+ const value = next && next.length ? next : null;
1244
+ setCodes(value);
1245
+ if (!client) return;
1246
+ setError(null);
1247
+ setSaving(true);
1248
+ try {
1249
+ const { error: e } = await client.rpc("tenant_setting_set", { p_key: KEY, p_value: value });
1250
+ if (e) throw e;
1251
+ } catch (e) {
1252
+ setError(message(e));
1253
+ } finally {
1254
+ setSaving(false);
1255
+ }
1256
+ }, [client]);
1257
+ return { codes, save, loading, saving, error, supported };
1258
+ }
1259
+ function noEscopo(code, escopo) {
1260
+ if (!escopo) return true;
1261
+ return escopo.some((c) => code === c || code.startsWith(`${c}.`));
1262
+ }
1263
+ function cobertoPorAncestral(code, escopo) {
1264
+ return escopo.some((c) => c !== code && code.startsWith(`${c}.`));
1265
+ }
1266
+ function mesesEntre(de, ate) {
1267
+ const out = [];
1268
+ const fim = /* @__PURE__ */ new Date(`${ate.slice(0, 10)}T12:00:00`);
1269
+ const cursor = /* @__PURE__ */ new Date(`${de.slice(0, 7)}-01T12:00:00`);
1270
+ while (cursor <= fim && out.length < 120) {
1271
+ out.push(`${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}-01`);
1272
+ cursor.setMonth(cursor.getMonth() + 1);
1273
+ }
1274
+ return out;
1275
+ }
1276
+ var MESES = ["jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"];
1277
+ function monthLabel2(iso) {
1278
+ const [ano, mes] = iso.slice(0, 7).split("-");
1279
+ return `${MESES[Number(mes) - 1] ?? mes}/${ano.slice(2)}`;
1280
+ }
1281
+ var DEF = {
1282
+ codeKey: "statement_code",
1283
+ labelKey: "statement_name",
1284
+ periodKey: "competence_month",
1285
+ valueKey: "amount",
1286
+ sectionKey: "direction",
1287
+ sections: [
1288
+ {
1289
+ value: "payable",
1290
+ label: "Despesas",
1291
+ // A linha sem conta é uma linha de verdade. Escondê-la faria a soma da
1292
+ // árvore não fechar com o total do rodapé.
1293
+ unclassifiedLabel: "Sem classifica\xE7\xE3o"
1294
+ }
1295
+ ],
1296
+ lines: [
1297
+ { kind: "section", section: "payable" },
1298
+ { kind: "total", label: "TOTAL NO PER\xCDODO", of: { payable: 1 }, emphasis: "rule" }
1299
+ ],
1300
+ defaultDepth: 1
1301
+ };
1302
+ function PayrollCostsView() {
1303
+ const provider = useWorkforceProvider();
1304
+ const { currency } = useWorkforceConfig();
1305
+ const can = usePermissionOptional();
1306
+ const podeConfigurar = can("settings", "manage");
1307
+ const escopoStore = usePayrollAccounts();
1308
+ const [preset, setPreset] = React3.useState("this_month");
1309
+ const [range, setRange] = React3.useState(() => resolveRangePreset("this_month"));
1310
+ const from = range.from;
1311
+ const to = range.to;
1312
+ const [rows, setRows] = React3.useState(null);
1313
+ const [unavailable, setUnavailable] = React3.useState(false);
1314
+ const [loading, setLoading] = React3.useState(true);
1315
+ const [recarga, setRecarga] = React3.useState(0);
1316
+ React3.useEffect(() => {
1317
+ let cancelled = false;
1318
+ setLoading(true);
1319
+ const load = provider.getPayrollCosts;
1320
+ if (!load) {
1321
+ setUnavailable(true);
1322
+ setLoading(false);
1323
+ return;
1324
+ }
1325
+ load({ from, to }).then((r) => {
1326
+ if (cancelled) return;
1327
+ setRows(r);
1328
+ setUnavailable(false);
1329
+ }).catch(() => {
1330
+ if (!cancelled) setUnavailable(true);
1331
+ }).finally(() => {
1332
+ if (!cancelled) setLoading(false);
1333
+ });
1334
+ return () => {
1335
+ cancelled = true;
1336
+ };
1337
+ }, [provider, from, to, recarga]);
1338
+ const escopo = escopoStore.codes;
1339
+ const despesas = React3.useMemo(
1340
+ () => (rows ?? []).filter((r) => r.competenceMonth === null && r.chartAccountCode).map((r) => ({ code: r.chartAccountCode, name: r.chartAccountName })).sort((a, b) => compareCodes(a.code, b.code)),
1341
+ [rows]
1342
+ );
1343
+ const movimento = React3.useMemo(
1344
+ () => (rows ?? []).filter((r) => r.competenceMonth !== null && (r.chartAccountCode ? noEscopo(r.chartAccountCode, escopo) : !escopo)),
1345
+ [rows, escopo]
1346
+ );
1347
+ const analyticsRows = React3.useMemo(() => {
1348
+ const out = [];
1349
+ for (const a of despesas) {
1350
+ if (!noEscopo(a.code, escopo)) continue;
1351
+ out.push({
1352
+ direction: "payable",
1353
+ statement_code: a.code,
1354
+ statement_name: a.name,
1355
+ competence_month: null,
1356
+ amount: 0
1357
+ });
1358
+ }
1359
+ for (const r of movimento) {
1360
+ out.push({
1361
+ direction: "payable",
1362
+ statement_code: r.chartAccountCode ?? "",
1363
+ statement_name: r.chartAccountName,
1364
+ competence_month: r.competenceMonth,
1365
+ amount: r.total
1366
+ });
1367
+ }
1368
+ return out;
1369
+ }, [despesas, movimento, escopo]);
1370
+ const total = movimento.reduce((s, r) => s + r.total, 0);
1371
+ const meses = new Set(movimento.map((r) => r.competenceMonth)).size;
1372
+ const competencia = React3.useMemo(() => {
1373
+ const ms = mesesEntre(from, to);
1374
+ if (ms.length === 0) return null;
1375
+ return ms.length === 1 ? monthLabel2(ms[0]) : `${monthLabel2(ms[0])} a ${monthLabel2(ms[ms.length - 1])}`;
1376
+ }, [from, to]);
1377
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 p-6", children: [
1378
+ /* @__PURE__ */ jsx(
1379
+ PageToolbar,
1380
+ {
1381
+ range: /* @__PURE__ */ jsx(DateRangeControl, { value: range, onChange: setRange, preset, onPresetChange: setPreset }),
1382
+ onRefresh: () => setRecarga((n) => n + 1)
1383
+ }
1384
+ ),
1385
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [
1386
+ /* @__PURE__ */ jsxs("div", { children: [
1387
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Encargos e honor\xE1rios" }),
1388
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "O custo de pessoal como o Financeiro o registrou, na \xE1rvore do plano de contas. Esta tela s\xF3 l\xEA \u2014 o lan\xE7amento continua sendo l\xE1." }),
1389
+ competencia ? /* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [
1390
+ "Compet\xEAncia ",
1391
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: competencia }),
1392
+ " \u2014 o demonstrativo \xE9 mensal, e o per\xEDodo escolhido vira meses inteiros."
1393
+ ] }) : null
1394
+ ] }),
1395
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1396
+ unavailable ? null : /* @__PURE__ */ jsx(
1397
+ EscopoPopover,
1398
+ {
1399
+ contas: despesas,
1400
+ escopo,
1401
+ podeConfigurar,
1402
+ store: escopoStore
1403
+ }
1404
+ ),
1405
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: () => adminNavigateTo("/financial"), children: [
1406
+ /* @__PURE__ */ jsx(ExternalLink, { className: "mr-1 h-4 w-4", "aria-hidden": true }),
1407
+ " Abrir Financeiro"
1408
+ ] })
1409
+ ] })
1410
+ ] }),
1411
+ unavailable ? /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-dashed border-border p-8 text-center", children: [
1412
+ /* @__PURE__ */ jsx(HandCoins, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground", "aria-hidden": true }),
1413
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "O raz\xE3o do Financeiro n\xE3o est\xE1 dispon\xEDvel neste ambiente \u2014 instale o m\xF3dulo Financeiro para ver o custo de pessoal aqui." })
1414
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1415
+ !escopoStore.loading && !escopo ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-warning/40 bg-warning/5 p-4", children: [
1416
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0 text-warning", "aria-hidden": true }),
1417
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-sm", children: [
1418
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Nenhum ramo de pessoal escolhido." }),
1419
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground", children: [
1420
+ "O demonstrativo abaixo soma ",
1421
+ /* @__PURE__ */ jsx("strong", { children: "todas" }),
1422
+ " as despesas classificadas em conta cont\xE1bil \u2014 inclusive aluguel, imposto e equipamento. Escolha em",
1423
+ " ",
1424
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap font-medium", children: "Contas de pessoal" }),
1425
+ " os ramos do seu plano que s\xE3o gente, e o total passa a valer como custo de equipe."
1426
+ ] })
1427
+ ] })
1428
+ ] }) : null,
1429
+ /* @__PURE__ */ jsx(
1430
+ StatTiles,
1431
+ {
1432
+ loading,
1433
+ tiles: [
1434
+ { id: "total", label: "Total no per\xEDodo", value: formatCurrency(total, currency) },
1435
+ { id: "months", label: "Compet\xEAncias", value: String(meses) },
1436
+ {
1437
+ id: "avg",
1438
+ label: "M\xE9dia mensal",
1439
+ value: meses > 0 ? formatCurrency(total / meses, currency) : formatCurrency(0, currency)
1440
+ }
1441
+ ]
1442
+ }
1443
+ ),
1444
+ /* @__PURE__ */ jsx(
1445
+ StatementTable,
1446
+ {
1447
+ rows: analyticsRows,
1448
+ def: DEF,
1449
+ currency,
1450
+ formatPeriod: monthLabel2,
1451
+ accountLabel: "Conta",
1452
+ totalLabel: "Per\xEDodo",
1453
+ emptyMessage: "Nenhuma despesa classificada em conta cont\xE1bil no per\xEDodo.",
1454
+ loading
1455
+ }
1456
+ )
1457
+ ] })
1458
+ ] });
1459
+ }
1460
+ function EscopoPopover({
1461
+ contas,
1462
+ escopo,
1463
+ podeConfigurar,
1464
+ store
1465
+ }) {
1466
+ const marcados = React3.useMemo(() => new Set(escopo ?? []), [escopo]);
1467
+ const alterna = (code) => {
1468
+ const next = new Set(marcados);
1469
+ if (next.has(code)) next.delete(code);
1470
+ else {
1471
+ for (const c of [...next]) if (c.startsWith(`${code}.`)) next.delete(c);
1472
+ next.add(code);
1473
+ }
1474
+ void store.save([...next].sort(compareCodes));
1475
+ };
1476
+ const rotulo = escopo ? `${escopo.length} ${escopo.length === 1 ? "ramo" : "ramos"} de pessoal` : "Contas de pessoal";
1477
+ return /* @__PURE__ */ jsxs(Popover, { children: [
1478
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: escopo ? "secondary" : "default", children: [
1479
+ /* @__PURE__ */ jsx(ListTree, { className: "mr-1 h-4 w-4", "aria-hidden": true }),
1480
+ " ",
1481
+ rotulo
1482
+ ] }) }),
1483
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "w-[26rem] p-0", children: [
1484
+ /* @__PURE__ */ jsxs("div", { className: "border-b p-3", children: [
1485
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "O que \xE9 custo de pessoal" }),
1486
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Marque os ramos do seu plano de contas. Um ramo marcado traz toda a descend\xEAncia \u2014 inclusive contas criadas depois." })
1487
+ ] }),
1488
+ /* @__PURE__ */ jsx("div", { className: "max-h-80 overflow-y-auto py-1", children: contas.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-3 text-xs text-muted-foreground", children: "Nenhuma conta de despesa ativa no plano." }) : contas.map((c) => {
1489
+ const depth = c.code.split(".").length - 1;
1490
+ const herdado = cobertoPorAncestral(c.code, [...marcados]);
1491
+ const checked = marcados.has(c.code) || herdado;
1492
+ return /* @__PURE__ */ jsxs(
1493
+ "label",
1494
+ {
1495
+ className: "flex cursor-pointer items-center gap-2 px-3 py-1.5 text-sm hover:bg-muted/50",
1496
+ style: { paddingLeft: `${12 + depth * 14}px` },
1497
+ children: [
1498
+ /* @__PURE__ */ jsx(
1499
+ Checkbox,
1500
+ {
1501
+ checked,
1502
+ disabled: !podeConfigurar || store.saving || herdado,
1503
+ onChange: () => alterna(c.code)
1504
+ }
1505
+ ),
1506
+ /* @__PURE__ */ jsx("span", { className: "w-16 shrink-0 font-mono text-xs text-muted-foreground", children: c.code }),
1507
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: c.name })
1508
+ ]
1509
+ },
1510
+ c.code
1511
+ );
1512
+ }) }),
1513
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-t p-3", children: [
1514
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: !podeConfigurar ? "S\xF3 quem tem settings.manage pode mudar isto." : store.error ? store.error : store.saving ? "Salvando\u2026" : "Salva sozinho, para toda a casa." }),
1515
+ escopo && podeConfigurar ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => void store.save(null), children: "Limpar" }) : null
1516
+ ] })
1517
+ ] })
1518
+ ] });
1519
+ }
1520
+ var CONTRATO = {
1521
+ clt: "CLT",
1522
+ pj: "PJ",
1523
+ estagio: "Est\xE1gio",
1524
+ jovem_aprendiz: "Jovem Aprendiz",
1525
+ autonomo: "Aut\xF4nomo",
1526
+ socio: "S\xF3cio",
1527
+ temporario: "Tempor\xE1rio",
1528
+ intermitente: "Intermitente"
1529
+ };
1530
+ var REGIME = {
1531
+ mensalista: "Mensalista",
1532
+ horista: "Horista",
1533
+ diarista: "Diarista",
1534
+ escala_12x36: "Escala 12x36",
1535
+ escala_6x1: "Escala 6x1",
1536
+ escala_5x2: "Escala 5x2"
1537
+ };
1538
+ var PAGAMENTO = {
1539
+ mensal: "Mensal",
1540
+ quinzenal: "Quinzenal",
1541
+ semanal: "Semanal",
1542
+ diario: "Di\xE1rio",
1543
+ por_hora: "Por hora"
1544
+ };
1545
+ var FERIAS = {
1546
+ pendente: "Pendente",
1547
+ programada: "Programada",
1548
+ em_gozo: "Em gozo",
1549
+ concluida: "Conclu\xEDda",
1550
+ vencida: "Vencida"
1551
+ };
1552
+ var AFASTAMENTO = {
1553
+ falta_justificada: "Falta justificada",
1554
+ falta_injustificada: "Falta injustificada",
1555
+ atestado: "Atestado m\xE9dico",
1556
+ licenca_maternidade: "Licen\xE7a maternidade",
1557
+ licenca_paternidade: "Licen\xE7a paternidade",
1558
+ licenca_nao_remunerada: "Licen\xE7a n\xE3o remunerada",
1559
+ suspensao: "Suspens\xE3o",
1560
+ ferias: "F\xE9rias",
1561
+ afastamento_inss: "Afastamento INSS"
1562
+ };
1563
+ var PRIORIDADE = {
1564
+ baixa: "Baixa",
1565
+ normal: "Normal",
1566
+ alta: "Alta",
1567
+ urgente: "Urgente"
1568
+ };
1569
+ function dm(iso) {
1570
+ if (!iso) return "\u2014";
1571
+ const [a, m, d] = iso.slice(0, 10).split("-");
1572
+ return `${d}/${m}/${a}`;
1573
+ }
1574
+ function Cartao({ icon: Icon, label, value, alerta }) {
1575
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-border p-4", children: [
1576
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
1577
+ /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5", "aria-hidden": true }),
1578
+ " ",
1579
+ label
1580
+ ] }),
1581
+ /* @__PURE__ */ jsx("div", { className: `mt-1 text-2xl font-semibold tabular-nums ${alerta ? "text-destructive" : ""}`, children: value })
1582
+ ] });
1583
+ }
1584
+ function Painel({ overview, upcoming, onPessoa }) {
1585
+ const turnover = overview.headcount > 0 ? (overview.dismissals30d / overview.headcount * 100).toFixed(1) : null;
1586
+ const porEspecie = (k) => upcoming.filter((u) => u.kind === k).slice(0, 10);
1587
+ const listas = [
1588
+ { kind: "vacation", titulo: "F\xE9rias a vencer", vazio: "Nenhuma." },
1589
+ { kind: "probation", titulo: "Experi\xEAncia vencendo", vazio: "Nenhuma." },
1590
+ { kind: "pending", titulo: "Pend\xEAncias cr\xEDticas", vazio: "Nenhuma." }
1591
+ ];
1592
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1593
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-4", children: [
1594
+ /* @__PURE__ */ jsx(Cartao, { icon: Users, label: "Headcount ativo", value: overview.headcount }),
1595
+ /* @__PURE__ */ jsx(Cartao, { icon: UserPlus, label: "Admiss\xF5es (30d)", value: overview.admissions30d }),
1596
+ /* @__PURE__ */ jsx(Cartao, { icon: UserMinus, label: "Demiss\xF5es (30d)", value: overview.dismissals30d }),
1597
+ /* @__PURE__ */ jsx(Cartao, { icon: AlertTriangle, label: "Turnover (30d)", value: turnover != null ? `${turnover}%` : "\u2014" }),
1598
+ /* @__PURE__ */ jsx(Cartao, { icon: CakeSlice, label: "Aniversariantes do m\xEAs", value: overview.birthdaysThisMonth }),
1599
+ /* @__PURE__ */ jsx(
1600
+ Cartao,
1601
+ {
1602
+ icon: Plane,
1603
+ label: "F\xE9rias vencendo (60d)",
1604
+ value: overview.vacationsDue60d,
1605
+ alerta: overview.vacationsDue60d > 0
1606
+ }
1607
+ ),
1608
+ /* @__PURE__ */ jsx(
1609
+ Cartao,
1610
+ {
1611
+ icon: CalendarClock,
1612
+ label: "Experi\xEAncia vencendo",
1613
+ value: overview.probationEnding,
1614
+ alerta: overview.probationEnding > 0
1615
+ }
1616
+ ),
1617
+ /* @__PURE__ */ jsx(
1618
+ Cartao,
1619
+ {
1620
+ icon: ShieldAlert,
1621
+ label: "Pend\xEAncias cr\xEDticas",
1622
+ value: overview.criticalPending,
1623
+ alerta: overview.criticalPending > 0
1624
+ }
1625
+ )
1626
+ ] }),
1627
+ /* @__PURE__ */ jsx("div", { className: "grid gap-3 lg:grid-cols-3", children: listas.map(({ kind, titulo, vazio }) => {
1628
+ const itens = porEspecie(kind);
1629
+ return /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-xl border border-border", children: [
1630
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border bg-muted/40 px-4 py-2 text-sm font-medium", children: titulo }),
1631
+ itens.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-sm text-muted-foreground", children: vazio }) : /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border", children: itens.map((u, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 px-4 py-2 text-sm", children: [
1632
+ /* @__PURE__ */ jsx(
1633
+ Button,
1634
+ {
1635
+ variant: "bare",
1636
+ size: "bare",
1637
+ type: "button",
1638
+ onClick: () => onPessoa(u.personId),
1639
+ className: "truncate text-left underline decoration-dashed decoration-muted-foreground/40 underline-offset-2 hover:text-primary block font-normal",
1640
+ children: u.personName ?? "Sem nome"
1641
+ }
1642
+ ),
1643
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: dm(u.dueOn) })
1644
+ ] }, `${u.personId}|${u.dueOn}|${i}`)) })
1645
+ ] }, kind);
1646
+ }) })
1647
+ ] });
1648
+ }
1649
+ function CampoData({ label, value, onChange }) {
1650
+ return /* @__PURE__ */ jsxs("label", { className: "block", children: [
1651
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs text-muted-foreground", children: label }),
1652
+ /* @__PURE__ */ jsx(Input, { type: "date", value: value ?? "", onChange: (e) => onChange(e.target.value) })
1653
+ ] });
1654
+ }
1655
+ function Vinculo({ personId }) {
1656
+ const provider = useWorkforceProvider();
1657
+ const { currency } = useWorkforceConfig();
1658
+ const [form, setForm] = React3.useState({ personId });
1659
+ const [busy, setBusy] = React3.useState(false);
1660
+ const [carregando, setCarregando] = React3.useState(true);
1661
+ React3.useEffect(() => {
1662
+ let vivo = true;
1663
+ setCarregando(true);
1664
+ provider.getEmployment(personId).then((e) => {
1665
+ if (vivo) {
1666
+ setForm(e ?? { personId });
1667
+ setCarregando(false);
1668
+ }
1669
+ }).catch(() => {
1670
+ if (vivo) {
1671
+ setForm({ personId });
1672
+ setCarregando(false);
1673
+ }
1674
+ });
1675
+ return () => {
1676
+ vivo = false;
1677
+ };
1678
+ }, [provider, personId]);
1679
+ const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
1680
+ async function salvar() {
1681
+ setBusy(true);
1682
+ try {
1683
+ await provider.saveEmployment({ ...form, personId });
1684
+ toast.success("V\xEDnculo salvo.");
1685
+ } catch (e) {
1686
+ toast.error("N\xE3o deu para salvar", { description: e?.message });
1687
+ } finally {
1688
+ setBusy(false);
1689
+ }
1690
+ }
1691
+ if (carregando) return /* @__PURE__ */ jsx(Skeleton, { className: "h-48 w-full" });
1692
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-xl border border-border p-4", children: [
1693
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "V\xEDnculo" }),
1694
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-4", children: [
1695
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1696
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs text-muted-foreground", children: "Tipo de contrato" }),
1697
+ /* @__PURE__ */ jsxs(Select, { value: form.contractKind ?? "", onValueChange: (v) => set("contractKind", v), children: [
1698
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "\u2014" }) }),
1699
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(CONTRATO).map(([k, r]) => /* @__PURE__ */ jsx(SelectItem, { value: k, children: r }, k)) })
1700
+ ] })
1701
+ ] }),
1702
+ /* @__PURE__ */ jsx(CampoData, { label: "Admiss\xE3o", value: form.admissionDate, onChange: (v) => set("admissionDate", v) }),
1703
+ /* @__PURE__ */ jsx(CampoData, { label: "Demiss\xE3o", value: form.dismissalDate, onChange: (v) => set("dismissalDate", v) }),
1704
+ /* @__PURE__ */ jsx(CampoData, { label: "Fim da experi\xEAncia", value: form.probationFinalEnd, onChange: (v) => set("probationFinalEnd", v) }),
1705
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1706
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs text-muted-foreground", children: "Cargo" }),
1707
+ /* @__PURE__ */ jsx(Input, { value: form.jobTitle ?? "", onChange: (e) => set("jobTitle", e.target.value) })
1708
+ ] }),
1709
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1710
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs text-muted-foreground", children: "Setor" }),
1711
+ /* @__PURE__ */ jsx(Input, { value: form.department ?? "", onChange: (e) => set("department", e.target.value) })
1712
+ ] }),
1713
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1714
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs text-muted-foreground", children: "Regime de jornada" }),
1715
+ /* @__PURE__ */ jsxs(Select, { value: form.scheduleRegime ?? "", onValueChange: (v) => set("scheduleRegime", v), children: [
1716
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "\u2014" }) }),
1717
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(REGIME).map(([k, r]) => /* @__PURE__ */ jsx(SelectItem, { value: k, children: r }, k)) })
1718
+ ] })
1719
+ ] }),
1720
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1721
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs text-muted-foreground", children: "Horas semanais" }),
1722
+ /* @__PURE__ */ jsx(Input, { type: "number", value: form.weeklyHours ?? "", onChange: (e) => set("weeklyHours", e.target.value ? Number(e.target.value) : null) })
1723
+ ] }),
1724
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1725
+ /* @__PURE__ */ jsxs("span", { className: "mb-1 block text-xs text-muted-foreground", children: [
1726
+ "Sal\xE1rio base (",
1727
+ currency.symbol,
1728
+ ")"
1729
+ ] }),
1730
+ /* @__PURE__ */ jsx(Input, { type: "number", step: "0.01", value: form.baseSalary ?? "", onChange: (e) => set("baseSalary", e.target.value ? Number(e.target.value) : null) })
1731
+ ] }),
1732
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1733
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs text-muted-foreground", children: "Forma de pagamento" }),
1734
+ /* @__PURE__ */ jsxs(Select, { value: form.payFrequency ?? "", onValueChange: (v) => set("payFrequency", v), children: [
1735
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "\u2014" }) }),
1736
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(PAGAMENTO).map(([k, r]) => /* @__PURE__ */ jsx(SelectItem, { value: k, children: r }, k)) })
1737
+ ] })
1738
+ ] })
1739
+ ] }),
1740
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
1741
+ "Sal\xE1rio e aux\xEDlios s\xE3o ",
1742
+ /* @__PURE__ */ jsx("b", { children: "valores de refer\xEAncia" }),
1743
+ ": n\xE3o viram conta a pagar, comiss\xE3o nem desconto."
1744
+ ] }),
1745
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(Button, { onClick: salvar, disabled: busy, children: "Salvar v\xEDnculo" }) })
1746
+ ] });
1747
+ }
1748
+ function Lista({ titulo, icon: Icon, itens, vazio, onNovo, children }) {
1749
+ return /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-xl border border-border", children: [
1750
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border bg-muted/40 px-4 py-2", children: [
1751
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm font-medium", children: [
1752
+ /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground", "aria-hidden": true }),
1753
+ " ",
1754
+ titulo
1755
+ ] }),
1756
+ /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: onNovo, children: [
1757
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": true }),
1758
+ " Novo"
1759
+ ] })
1760
+ ] }),
1761
+ itens.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-sm text-muted-foreground", children: vazio }) : /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border", children })
1762
+ ] });
1763
+ }
1764
+ function Ficha({ personId, personName }) {
1765
+ const provider = useWorkforceProvider();
1766
+ const [ferias, setFerias] = React3.useState([]);
1767
+ const [faltas, setFaltas] = React3.useState([]);
1768
+ const [pendencias, setPendencias] = React3.useState([]);
1769
+ const [dialogo, setDialogo] = React3.useState(null);
1770
+ const recarregar = React3.useCallback(() => {
1771
+ provider.getVacations(personId).then(setFerias).catch(() => setFerias([]));
1772
+ provider.getAbsences(personId).then(setFaltas).catch(() => setFaltas([]));
1773
+ provider.getPendingItems(personId).then(setPendencias).catch(() => setPendencias([]));
1774
+ }, [provider, personId]);
1775
+ React3.useEffect(() => {
1776
+ recarregar();
1777
+ }, [recarregar]);
1778
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1779
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: personName ?? "Ficha" }),
1780
+ /* @__PURE__ */ jsx(Vinculo, { personId }),
1781
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-3 lg:grid-cols-3", children: [
1782
+ /* @__PURE__ */ jsx(
1783
+ Lista,
1784
+ {
1785
+ titulo: "F\xE9rias",
1786
+ icon: Plane,
1787
+ itens: ferias,
1788
+ vazio: "Nenhum registro.",
1789
+ onNovo: () => setDialogo("ferias"),
1790
+ children: ferias.map((v) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 px-4 py-2 text-sm", children: [
1791
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
1792
+ dm(v.accrualStart),
1793
+ " \u2013 ",
1794
+ dm(v.accrualEnd)
1795
+ ] }),
1796
+ /* @__PURE__ */ jsx(Badge, { variant: v.status === "concluida" ? "secondary" : v.status === "vencida" ? "destructive" : "outline", children: FERIAS[v.status] })
1797
+ ] }, v.id))
1798
+ }
1799
+ ),
1800
+ /* @__PURE__ */ jsx(
1801
+ Lista,
1802
+ {
1803
+ titulo: "Afastamentos",
1804
+ icon: Stethoscope,
1805
+ itens: faltas,
1806
+ vazio: "Nenhum registro.",
1807
+ onNovo: () => setDialogo("falta"),
1808
+ children: faltas.map((a) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 px-4 py-2 text-sm", children: [
1809
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: AFASTAMENTO[a.kind] }),
1810
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: [
1811
+ dm(a.startsOn),
1812
+ a.endsOn ? ` \u2013 ${dm(a.endsOn)}` : ""
1813
+ ] })
1814
+ ] }, a.id))
1815
+ }
1816
+ ),
1817
+ /* @__PURE__ */ jsx(
1818
+ Lista,
1819
+ {
1820
+ titulo: "Pend\xEAncias",
1821
+ icon: ClipboardList,
1822
+ itens: pendencias,
1823
+ vazio: "Nenhum registro.",
1824
+ onNovo: () => setDialogo("pendencia"),
1825
+ children: pendencias.map((p) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 px-4 py-2 text-sm", children: [
1826
+ /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 items-center gap-2", children: [
1827
+ /* @__PURE__ */ jsx(
1828
+ Checkbox,
1829
+ {
1830
+ checked: !!p.resolvedAt,
1831
+ onChange: (checked) => void provider.resolvePendingItem(p.id, checked).then(recarregar)
1832
+ }
1833
+ ),
1834
+ /* @__PURE__ */ jsx("span", { className: `truncate ${p.resolvedAt ? "text-muted-foreground line-through" : ""}`, children: p.title })
1835
+ ] }),
1836
+ !p.resolvedAt && (p.priority === "alta" || p.priority === "urgente") && /* @__PURE__ */ jsx(Badge, { variant: "destructive", children: PRIORIDADE[p.priority] })
1837
+ ] }, p.id))
1838
+ }
1839
+ )
1840
+ ] }),
1841
+ dialogo && /* @__PURE__ */ jsx(
1842
+ DialogoLancamento,
1843
+ {
1844
+ tipo: dialogo,
1845
+ personId,
1846
+ onClose: () => setDialogo(null),
1847
+ onSaved: () => {
1848
+ setDialogo(null);
1849
+ recarregar();
1850
+ }
1851
+ }
1852
+ )
1853
+ ] });
1854
+ }
1855
+ function DialogoLancamento({ tipo, personId, onClose, onSaved }) {
1856
+ const provider = useWorkforceProvider();
1857
+ const [busy, setBusy] = React3.useState(false);
1858
+ const [de, setDe] = React3.useState("");
1859
+ const [ate, setAte] = React3.useState("");
1860
+ const [kind, setKind] = React3.useState("atestado");
1861
+ const [titulo, setTitulo] = React3.useState("");
1862
+ const [prioridade, setPrioridade] = React3.useState("normal");
1863
+ const rotulo = tipo === "ferias" ? "Per\xEDodo de f\xE9rias" : tipo === "falta" ? "Afastamento" : "Pend\xEAncia";
1864
+ async function salvar() {
1865
+ setBusy(true);
1866
+ try {
1867
+ if (tipo === "ferias") {
1868
+ if (!de || !ate) {
1869
+ toast.error("O per\xEDodo aquisitivo \xE9 obrigat\xF3rio.");
1870
+ return;
1871
+ }
1872
+ await provider.saveVacation({ personId, accrualStart: de, accrualEnd: ate, status: "pendente" });
1873
+ } else if (tipo === "falta") {
1874
+ if (!de) {
1875
+ toast.error("A data de in\xEDcio \xE9 obrigat\xF3ria.");
1876
+ return;
1877
+ }
1878
+ await provider.saveAbsence({ personId, kind, startsOn: de, endsOn: ate || null, paid: true });
1879
+ } else {
1880
+ if (!titulo.trim()) {
1881
+ toast.error("A pend\xEAncia precisa de um t\xEDtulo.");
1882
+ return;
1883
+ }
1884
+ await provider.savePendingItem({ personId, title: titulo.trim(), priority: prioridade, dueOn: de || null });
1885
+ }
1886
+ toast.success(`${rotulo} lan\xE7ado.`);
1887
+ onSaved();
1888
+ } catch (e) {
1889
+ toast.error("N\xE3o deu para salvar", { description: e?.message });
1890
+ } finally {
1891
+ setBusy(false);
1892
+ }
1893
+ }
1894
+ return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (o) => {
1895
+ if (!o) onClose();
1896
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { children: [
1897
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsxs(ModalTitle, { children: [
1898
+ "Novo ",
1899
+ rotulo.toLowerCase()
1900
+ ] }) }),
1901
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3", children: [
1902
+ tipo === "falta" && /* @__PURE__ */ jsxs(Select, { value: kind, onValueChange: (v) => setKind(v), children: [
1903
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
1904
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(AFASTAMENTO).map(([k, r]) => /* @__PURE__ */ jsx(SelectItem, { value: k, children: r }, k)) })
1905
+ ] }),
1906
+ tipo === "pendencia" && /* @__PURE__ */ jsxs(Fragment, { children: [
1907
+ /* @__PURE__ */ jsx(Input, { value: titulo, onChange: (e) => setTitulo(e.target.value), placeholder: "O que falta (obrigat\xF3rio)" }),
1908
+ /* @__PURE__ */ jsxs(Select, { value: prioridade, onValueChange: (v) => setPrioridade(v), children: [
1909
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
1910
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(PRIORIDADE).map(([k, r]) => /* @__PURE__ */ jsx(SelectItem, { value: k, children: r }, k)) })
1911
+ ] })
1912
+ ] }),
1913
+ /* @__PURE__ */ jsx(
1914
+ CampoData,
1915
+ {
1916
+ label: tipo === "ferias" ? "In\xEDcio aquisitivo" : tipo === "falta" ? "In\xEDcio" : "Prazo",
1917
+ value: de,
1918
+ onChange: setDe
1919
+ }
1920
+ ),
1921
+ tipo !== "pendencia" && /* @__PURE__ */ jsx(CampoData, { label: tipo === "ferias" ? "Fim aquisitivo" : "Fim", value: ate, onChange: setAte })
1922
+ ] }),
1923
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
1924
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onClose, children: "Cancelar" }),
1925
+ /* @__PURE__ */ jsx(Button, { onClick: salvar, disabled: busy, children: "Salvar" })
1926
+ ] })
1927
+ ] }) });
1928
+ }
1929
+ function HrView() {
1930
+ const provider = useWorkforceProvider();
1931
+ const { staffLookup } = useWorkforceConfig();
1932
+ const [overview, setOverview] = React3.useState(null);
1933
+ const [upcoming, setUpcoming] = React3.useState([]);
1934
+ const [equipe, setEquipe] = React3.useState([]);
1935
+ const [loading, setLoading] = React3.useState(true);
1936
+ const [pessoa, setPessoa] = React3.useState(null);
1937
+ React3.useEffect(() => {
1938
+ let vivo = true;
1939
+ Promise.all([provider.getHrOverview(), provider.getHrUpcoming(), provider.getStaff()]).then(([o, u, e]) => {
1940
+ if (vivo) {
1941
+ setOverview(o);
1942
+ setUpcoming(u);
1943
+ setEquipe(e);
1944
+ setLoading(false);
1945
+ }
1946
+ }).catch(() => {
1947
+ if (vivo) {
1948
+ setOverview(null);
1949
+ setUpcoming([]);
1950
+ setEquipe([]);
1951
+ setLoading(false);
1952
+ }
1953
+ });
1954
+ return () => {
1955
+ vivo = false;
1956
+ };
1957
+ }, [provider]);
1958
+ const nomeDe = React3.useCallback(
1959
+ (id) => upcoming.find((u) => u.personId === id)?.personName ?? null,
1960
+ [upcoming]
1961
+ );
1962
+ if (loading) return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1963
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-28 w-full" }),
1964
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full" })
1965
+ ] });
1966
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1967
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1968
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "Abrir a ficha de" }),
1969
+ /* @__PURE__ */ jsxs(Select, { value: pessoa?.id ?? "", onValueChange: (id) => {
1970
+ const p = equipe.find((x) => x.id === id);
1971
+ setPessoa({ id, name: p?.name ?? nomeDe(id) });
1972
+ }, children: [
1973
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-72", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: equipe.length ? "Escolha algu\xE9m da equipe\u2026" : "Nenhuma pessoa cadastrada" }) }),
1974
+ /* @__PURE__ */ jsx(SelectContent, { children: equipe.map((p) => /* @__PURE__ */ jsx(SelectItem, { value: p.id, children: p.name ?? "Sem nome" }, p.id)) })
1975
+ ] })
1976
+ ] }),
1977
+ overview ? /* @__PURE__ */ jsx(Painel, { overview, upcoming, onPessoa: (id) => setPessoa({ id, name: nomeDe(id) }) }) : (
1978
+ // `null` aqui não é erro: `v_workforce_hr_overview` agrupa por tenant e
1979
+ // só devolve linha quando existe ao menos um VÍNCULO. Ninguém com
1980
+ // vínculo = nada para agrupar. Dizer "nenhuma ficha ainda" é a leitura
1981
+ // certa; um painel de zeros diria que o time tem zero pessoas.
1982
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-dashed border-border p-8 text-center", children: [
1983
+ /* @__PURE__ */ jsx(Users, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground", "aria-hidden": true }),
1984
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhuma ficha trabalhista ainda. Escolha algu\xE9m acima e preencha o v\xEDnculo \u2014 \xE9 dele que saem headcount, admiss\xF5es e experi\xEAncia vencendo." })
1985
+ ] })
1986
+ ),
1987
+ pessoa && /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-border p-4", children: [
1988
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [
1989
+ /* @__PURE__ */ jsx(PersonLink, { personId: pessoa.id, name: pessoa.name ?? "Pessoa", kind: "staff", lookup: staffLookup }),
1990
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPessoa(null), children: "Fechar" })
1991
+ ] }),
1992
+ /* @__PURE__ */ jsx(Ficha, { personId: pessoa.id, personName: pessoa.name })
1993
+ ] })
1994
+ ] });
1995
+ }
1996
+ function parseIntent(activeView) {
1997
+ const { base, id } = parseViewId(activeView);
1998
+ const m = base.match(/^(people)-(new|list|detail|edit)$/);
1999
+ if (m) return { view: m[1], mode: m[2], editId: id };
2000
+ return { view: base };
2001
+ }
2002
+ function buildNav(config, activeView, navigate) {
2003
+ const items = [];
2004
+ if (config.modules.schedules) {
2005
+ items.push({
2006
+ id: "schedules",
2007
+ label: config.labels.schedules,
2008
+ icon: "CalendarDays",
2009
+ active: activeView === "schedules",
2010
+ onClick: () => navigate("schedules")
2011
+ });
2012
+ }
2013
+ if (config.modules.templates) {
2014
+ items.push({
2015
+ id: "templates",
2016
+ label: config.labels.templates,
2017
+ icon: "LayoutTemplate",
2018
+ active: activeView === "templates",
2019
+ onClick: () => navigate("templates")
2020
+ });
2021
+ }
2022
+ if (config.modules.timeEntries) {
2023
+ items.push({
2024
+ id: "time-entries",
2025
+ label: config.labels.timeEntries,
2026
+ icon: "Clock",
2027
+ active: activeView === "time-entries",
2028
+ onClick: () => navigate("time-entries")
2029
+ });
2030
+ }
2031
+ if (config.modules.periods) {
2032
+ items.push({
2033
+ id: "periods",
2034
+ label: config.labels.periods,
2035
+ icon: "Lock",
2036
+ active: activeView === "periods",
2037
+ onClick: () => navigate("periods")
2038
+ });
2039
+ }
2040
+ if (config.modules.people) {
2041
+ items.push({
2042
+ id: "people",
2043
+ label: config.labels.people,
2044
+ icon: "UserCog",
2045
+ active: activeView.startsWith("people"),
2046
+ onClick: () => navigate("people")
2047
+ });
2048
+ }
2049
+ if (config.modules.hr) {
2050
+ items.push({
2051
+ id: "hr",
2052
+ label: config.labels.hr,
2053
+ icon: "BriefcaseBusiness",
2054
+ active: activeView === "hr",
2055
+ onClick: () => navigate("hr")
2056
+ });
2057
+ }
2058
+ if (config.modules.payrollCosts) {
2059
+ items.push({
2060
+ id: "payroll-costs",
2061
+ label: config.labels.payrollCosts,
2062
+ icon: "HandCoins",
2063
+ active: activeView === "payroll-costs",
2064
+ onClick: () => navigate("payroll-costs")
2065
+ });
2066
+ }
2067
+ for (const item of items) {
2068
+ const feature = config.navFeatureMap[item.id];
2069
+ if (feature) item.feature = feature;
2070
+ }
2071
+ return items;
2072
+ }
2073
+ function WorkforcePage({ config, provider, store, staffEntity }) {
2074
+ const { view, direction, navigate } = useModuleNavigation(
2075
+ "/workforce",
2076
+ {
2077
+ schedules: 0,
2078
+ templates: 0,
2079
+ "time-entries": 0,
2080
+ periods: 0,
2081
+ people: 0,
2082
+ "people-detail": 1,
2083
+ "people-edit": 2,
2084
+ "payroll-costs": 0
2085
+ },
2086
+ "schedules"
2087
+ );
2088
+ const intent = parseIntent(view);
2089
+ const nav = useModuleNavAccess(buildNav(config, view, navigate));
2090
+ function renderView() {
2091
+ const off = intent.view === "schedules" && !config.modules.schedules || intent.view === "templates" && !config.modules.templates || intent.view === "time-entries" && !config.modules.timeEntries || intent.view === "periods" && !config.modules.periods || intent.view === "people" && !config.modules.people || intent.view === "hr" && !config.modules.hr || intent.view === "payroll-costs" && !config.modules.payrollCosts;
2092
+ if (off) return /* @__PURE__ */ jsx(SchedulesView, {});
2093
+ switch (intent.view) {
2094
+ case "templates":
2095
+ return /* @__PURE__ */ jsx(TemplatesView, {});
2096
+ case "time-entries":
2097
+ return /* @__PURE__ */ jsx(TimeEntriesView, {});
2098
+ case "periods":
2099
+ return /* @__PURE__ */ jsx(PeriodsView, {});
2100
+ case "people":
2101
+ return /* @__PURE__ */ jsx(PeopleView, { entity: staffEntity, mode: intent.mode });
2102
+ case "hr":
2103
+ return /* @__PURE__ */ jsx(HrView, {});
2104
+ case "payroll-costs":
2105
+ return /* @__PURE__ */ jsx(PayrollCostsView, {});
2106
+ case "schedules":
2107
+ default:
2108
+ return /* @__PURE__ */ jsx(SchedulesView, {});
2109
+ }
2110
+ }
2111
+ function renderGatedView() {
2112
+ const feature = config.navFeatureMap[intent.view];
2113
+ if (!feature) return renderView();
2114
+ return /* @__PURE__ */ jsx(EntitlementGate, { feature, fallback: /* @__PURE__ */ jsx(UpgradePrompt, { feature }), children: renderView() });
2115
+ }
2116
+ return /* @__PURE__ */ jsx(WorkforceContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(
2117
+ ModulePage,
2118
+ {
2119
+ title: config.labels.pageTitle,
2120
+ subtitle: config.labels.pageSubtitle,
2121
+ nav,
2122
+ navVariant: config.moduleNav,
2123
+ showHeader: intent.view === "schedules",
2124
+ viewKey: view,
2125
+ direction,
2126
+ children: renderGatedView()
2127
+ }
2128
+ ) });
2129
+ }
2130
+
2131
+ export { WorkforcePage };
2132
+ //# sourceMappingURL=WorkforcePage-A64KM6IC.js.map
2133
+ //# sourceMappingURL=WorkforcePage-A64KM6IC.js.map