@fayz-ai/plugin-forms 0.13.5 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{FormBuilder-AO6V2YOT.js → FormBuilder-N4R5SENC.js} +161 -15
- package/dist/FormBuilder-N4R5SENC.js.map +1 -0
- package/dist/chunk-2DFNG5UT.js +1661 -0
- package/dist/chunk-2DFNG5UT.js.map +1 -0
- package/dist/components/ContractSignPane.d.ts +12 -0
- package/dist/components/ContractSignPane.d.ts.map +1 -0
- package/dist/components/DocumentPreview.d.ts.map +1 -1
- package/dist/components/FieldConfigDialog.d.ts.map +1 -1
- package/dist/components/FieldPalette.d.ts.map +1 -1
- package/dist/components/FormFillDialog.d.ts +22 -2
- package/dist/components/FormFillDialog.d.ts.map +1 -1
- package/dist/components/FormRenderer.d.ts +9 -2
- package/dist/components/FormRenderer.d.ts.map +1 -1
- package/dist/components/PersonAnswerAlerts.d.ts +12 -0
- package/dist/components/PersonAnswerAlerts.d.ts.map +1 -0
- package/dist/components/PersonFormsAlert.d.ts +6 -1
- package/dist/components/PersonFormsAlert.d.ts.map +1 -1
- package/dist/components/fields/FieldTypeSettings.d.ts +11 -0
- package/dist/components/fields/FieldTypeSettings.d.ts.map +1 -0
- package/dist/components/fields/StencilCamera.d.ts +19 -0
- package/dist/components/fields/StencilCamera.d.ts.map +1 -0
- package/dist/components/fields/fields.d.ts +59 -0
- package/dist/components/fields/fields.d.ts.map +1 -0
- package/dist/components/fields/media.d.ts +18 -0
- package/dist/components/fields/media.d.ts.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +27 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +969 -134
- package/dist/index.js.map +1 -1
- package/dist/lib/attendance-contracts.d.ts +61 -0
- package/dist/lib/attendance-contracts.d.ts.map +1 -0
- package/dist/lib/categories.d.ts +1 -1
- package/dist/lib/categories.d.ts.map +1 -1
- package/dist/lib/contract-render.d.ts +57 -0
- package/dist/lib/contract-render.d.ts.map +1 -0
- package/dist/lib/print.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/types.d.ts +52 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CustomFormsSettingsTab.d.ts.map +1 -1
- package/dist/views/TemplateListView.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/FormBuilder-AO6V2YOT.js.map +0 -1
- package/dist/chunk-2AIKIPGU.js +0 -695
- package/dist/chunk-2AIKIPGU.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { FormRenderer, createOrUpdateFormTool, prefillFromPerson, printDocument, htmlToPlainText } from './chunk-
|
|
2
|
-
import
|
|
3
|
-
import { tl, createSafeDataProvider, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional,
|
|
1
|
+
import { isFieldEmpty, CustomFormsProvider, FormRenderer, sanitizeHtml, createOrUpdateFormTool, alertOf, answerAlerts, yesNoAnswer, prefillFromPerson, printDocument, htmlToPlainText, isMedia } from './chunk-2DFNG5UT.js';
|
|
2
|
+
import React12, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
|
3
|
+
import { tl, createSafeDataProvider, registerTranslations, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, getActiveTenantId, useTranslation, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
|
|
4
4
|
import { getFayzSupabaseClient, useOrganizationStore, PluginSettingsPanel, registerTimelineSource, registerTabCountProvider, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions, invalidateTabCounts, TimelineList } from '@fayz-ai/admin';
|
|
5
5
|
import { create } from 'zustand';
|
|
6
|
-
import { ClipboardList, CheckCircle2, Clock, Circle, FileText, FileSignature, FileCheck2, Activity, Image, Paperclip, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft,
|
|
7
|
-
import { Skeleton, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Badge, Switch, RowActions,
|
|
6
|
+
import { ClipboardList, QrCode, Printer, Check, CheckCircle2, Clock, Circle, X, Camera, FileText, FileSignature, FileCheck2, Activity, Stethoscope, Image, Paperclip, AlertTriangle, ChevronUp, ChevronDown, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft, EyeOff, Eye, Upload } from 'lucide-react';
|
|
7
|
+
import { Skeleton, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Button, Input, Badge, Switch, RowActions, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, ConfirmDialog } from '@fayz-ai/ui';
|
|
8
8
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
9
9
|
|
|
10
10
|
// src/data/tables.ts
|
|
@@ -205,6 +205,15 @@ function createMockFormsProvider() {
|
|
|
205
205
|
files.push(docFile);
|
|
206
206
|
return docFile;
|
|
207
207
|
},
|
|
208
|
+
async uploadMedia(blob) {
|
|
209
|
+
return { path: URL.createObjectURL(blob) };
|
|
210
|
+
},
|
|
211
|
+
async mediaUrl(path) {
|
|
212
|
+
return path;
|
|
213
|
+
},
|
|
214
|
+
async searchCatalog() {
|
|
215
|
+
return [];
|
|
216
|
+
},
|
|
208
217
|
async deleteFile(fileId) {
|
|
209
218
|
const idx = files.findIndex((f) => f.id === fileId);
|
|
210
219
|
if (idx !== -1) files.splice(idx, 1);
|
|
@@ -222,10 +231,13 @@ function getClient() {
|
|
|
222
231
|
if (!supabase) throw new Error("Supabase not initialized");
|
|
223
232
|
return supabase;
|
|
224
233
|
}
|
|
234
|
+
var MEDIA_BUCKET = "attachments";
|
|
235
|
+
var SIGNED_TTL_S = 3600;
|
|
236
|
+
var signedCache = /* @__PURE__ */ new Map();
|
|
225
237
|
var USAGE_FALLBACK_LIMIT = 5e3;
|
|
226
|
-
async function countTemplateUsageClientSide(
|
|
238
|
+
async function countTemplateUsageClientSide(db2, tenantId2) {
|
|
227
239
|
try {
|
|
228
|
-
const { data, error, count } = await
|
|
240
|
+
const { data, error, count } = await db2.from("v_documents").select("template_id, created_at", { count: "exact" }).eq("tenant_id", tenantId2).eq("is_active", true).not("template_id", "is", null).limit(USAGE_FALLBACK_LIMIT);
|
|
229
241
|
if (error) return {};
|
|
230
242
|
if (typeof count === "number" && count > USAGE_FALLBACK_LIMIT) return {};
|
|
231
243
|
const usage = {};
|
|
@@ -315,8 +327,8 @@ function createSupabaseFormsProvider() {
|
|
|
315
327
|
return {
|
|
316
328
|
// ── Templates ──────────────────────────────────────────
|
|
317
329
|
async getTemplates(query) {
|
|
318
|
-
const
|
|
319
|
-
let qb =
|
|
330
|
+
const db2 = getClient();
|
|
331
|
+
let qb = db2.from(T.templates).select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_current", true).eq("is_deleted", false).eq("is_active", true);
|
|
320
332
|
if (query.category) qb = qb.eq("category", query.category);
|
|
321
333
|
if (query.search) qb = qb.ilike("name", `%${query.search}%`);
|
|
322
334
|
const page = query.page ?? 1;
|
|
@@ -327,16 +339,16 @@ function createSupabaseFormsProvider() {
|
|
|
327
339
|
return { data: (data ?? []).map(mapTemplate), total: count ?? 0 };
|
|
328
340
|
},
|
|
329
341
|
async getTemplateById(id) {
|
|
330
|
-
const
|
|
331
|
-
const { data, error } = await
|
|
342
|
+
const db2 = getClient();
|
|
343
|
+
const { data, error } = await db2.from(T.templates).select("*").eq("id", id).eq("is_deleted", false).single();
|
|
332
344
|
if (error || !data) return null;
|
|
333
345
|
return mapTemplate(data);
|
|
334
346
|
},
|
|
335
347
|
async createTemplate(input) {
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
const { data, error } = await
|
|
339
|
-
tenant_id:
|
|
348
|
+
const db2 = getClient();
|
|
349
|
+
const tenantId2 = getTenantId();
|
|
350
|
+
const { data, error } = await db2.from(T.templates).insert({
|
|
351
|
+
tenant_id: tenantId2,
|
|
340
352
|
name: input.name,
|
|
341
353
|
description: input.description,
|
|
342
354
|
category: input.category,
|
|
@@ -349,15 +361,15 @@ function createSupabaseFormsProvider() {
|
|
|
349
361
|
return mapTemplate(data);
|
|
350
362
|
},
|
|
351
363
|
async updateTemplate(id, input) {
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const { count } = await
|
|
364
|
+
const db2 = getClient();
|
|
365
|
+
const tenantId2 = getTenantId();
|
|
366
|
+
const { count } = await db2.from(T.documents).select("document_id", { count: "exact", head: true }).eq("template_id", id);
|
|
355
367
|
const hasDocuments = (count ?? 0) > 0;
|
|
356
368
|
if (hasDocuments && input.schema) {
|
|
357
369
|
const existing = await this.getTemplateById(id);
|
|
358
370
|
if (!existing) throw new Error("Template not found");
|
|
359
|
-
const { data: newRow, error: insertError } = await
|
|
360
|
-
tenant_id:
|
|
371
|
+
const { data: newRow, error: insertError } = await db2.from(T.templates).insert({
|
|
372
|
+
tenant_id: tenantId2,
|
|
361
373
|
name: input.name ?? existing.name,
|
|
362
374
|
description: input.description ?? existing.description,
|
|
363
375
|
category: input.category ?? existing.category,
|
|
@@ -378,7 +390,7 @@ function createSupabaseFormsProvider() {
|
|
|
378
390
|
alert_when_missing: input.alertWhenMissing ?? existing.alertWhenMissing
|
|
379
391
|
}).select().single();
|
|
380
392
|
if (insertError) throw new Error(insertError.message);
|
|
381
|
-
await
|
|
393
|
+
await db2.from(T.templates).update({ is_current: false, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
382
394
|
return mapTemplate(newRow);
|
|
383
395
|
}
|
|
384
396
|
const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -393,13 +405,13 @@ function createSupabaseFormsProvider() {
|
|
|
393
405
|
if (input.requiredOnAttendance !== void 0) updates.required_on_attendance = input.requiredOnAttendance;
|
|
394
406
|
if (input.validityDays !== void 0) updates.validity_days = input.validityDays;
|
|
395
407
|
if (input.alertWhenMissing !== void 0) updates.alert_when_missing = input.alertWhenMissing;
|
|
396
|
-
const { data, error } = await
|
|
408
|
+
const { data, error } = await db2.from(T.templates).update(updates).eq("id", id).select().single();
|
|
397
409
|
if (error) throw new Error(error.message);
|
|
398
410
|
return mapTemplate(data);
|
|
399
411
|
},
|
|
400
412
|
async deleteTemplate(id) {
|
|
401
|
-
const
|
|
402
|
-
const { error } = await
|
|
413
|
+
const db2 = getClient();
|
|
414
|
+
const { error } = await db2.from(T.templates).update({ is_deleted: true, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
403
415
|
if (error) throw new Error(error.message);
|
|
404
416
|
},
|
|
405
417
|
// Uso por modelo. Caminho principal: `v_forms_template_usage`, que agrega no
|
|
@@ -411,10 +423,10 @@ function createSupabaseFormsProvider() {
|
|
|
411
423
|
// propósito — acima do teto o número seria mentira, e mentir sobre uma
|
|
412
424
|
// contagem é pior do que omiti-la, então ele desiste em silêncio.
|
|
413
425
|
async getTemplateUsage() {
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
if (!
|
|
417
|
-
const { data, error } = await
|
|
426
|
+
const db2 = getClient();
|
|
427
|
+
const tenantId2 = getTenantId();
|
|
428
|
+
if (!tenantId2) return {};
|
|
429
|
+
const { data, error } = await db2.from("v_forms_template_usage").select("template_id, document_count, last_document_at").eq("tenant_id", tenantId2);
|
|
418
430
|
if (!error) {
|
|
419
431
|
const usage = {};
|
|
420
432
|
for (const row of data ?? []) {
|
|
@@ -425,12 +437,12 @@ function createSupabaseFormsProvider() {
|
|
|
425
437
|
}
|
|
426
438
|
return usage;
|
|
427
439
|
}
|
|
428
|
-
return countTemplateUsageClientSide(
|
|
440
|
+
return countTemplateUsageClientSide(db2, tenantId2);
|
|
429
441
|
},
|
|
430
442
|
// ── Documents (public.documents + plg_forms_documents extension) ──
|
|
431
443
|
async getDocuments(query) {
|
|
432
|
-
const
|
|
433
|
-
let qb =
|
|
444
|
+
const db2 = getClient();
|
|
445
|
+
let qb = db2.from("v_documents").select("*", { count: "exact" }).eq("tenant_id", getTenantId()).eq("is_active", true);
|
|
434
446
|
if (query.personId) qb = qb.eq("person_id", query.personId);
|
|
435
447
|
if (query.templateId) qb = qb.eq("template_id", query.templateId);
|
|
436
448
|
if (query.status) {
|
|
@@ -456,8 +468,8 @@ function createSupabaseFormsProvider() {
|
|
|
456
468
|
* seriam seis idas para responder uma pergunta.
|
|
457
469
|
*/
|
|
458
470
|
async getDocumentCounts(query) {
|
|
459
|
-
const
|
|
460
|
-
let qb =
|
|
471
|
+
const db2 = getClient();
|
|
472
|
+
let qb = db2.from("v_documents").select("kind, template_category").eq("tenant_id", getTenantId()).eq("person_id", query.personId).eq("is_active", true).neq("status", "archived");
|
|
461
473
|
if (query.kinds?.length) qb = qb.in("kind", query.kinds);
|
|
462
474
|
const { data, error } = await qb;
|
|
463
475
|
if (error) return {};
|
|
@@ -469,16 +481,16 @@ function createSupabaseFormsProvider() {
|
|
|
469
481
|
return counts;
|
|
470
482
|
},
|
|
471
483
|
async getDocumentById(id) {
|
|
472
|
-
const
|
|
473
|
-
const { data, error } = await
|
|
484
|
+
const db2 = getClient();
|
|
485
|
+
const { data, error } = await db2.from("v_documents").select("*").eq("id", id).single();
|
|
474
486
|
if (error || !data) return null;
|
|
475
487
|
return mapDocument(data);
|
|
476
488
|
},
|
|
477
489
|
async createDocument(input) {
|
|
478
|
-
const
|
|
490
|
+
const tenantId2 = getTenantId();
|
|
479
491
|
const isForm = !!input.templateId;
|
|
480
492
|
const { data: coreDoc, error: coreError } = await getClient().from("documents").insert({
|
|
481
|
-
tenant_id:
|
|
493
|
+
tenant_id: tenantId2,
|
|
482
494
|
kind: input.kind ?? (isForm ? "form" : "attachment"),
|
|
483
495
|
person_id: input.personId,
|
|
484
496
|
title: input.title,
|
|
@@ -487,16 +499,20 @@ function createSupabaseFormsProvider() {
|
|
|
487
499
|
file_name: input.fileName,
|
|
488
500
|
file_size: input.fileSize,
|
|
489
501
|
mime_type: input.mimeType,
|
|
502
|
+
subject_type: input.subjectType,
|
|
503
|
+
subject_id: input.subjectId,
|
|
490
504
|
metadata: {}
|
|
491
505
|
}).select().single();
|
|
492
506
|
if (coreError) throw new Error(coreError.message);
|
|
493
507
|
if (isForm && input.templateId) {
|
|
494
|
-
const
|
|
495
|
-
const { error: extError } = await
|
|
508
|
+
const db2 = getClient();
|
|
509
|
+
const { error: extError } = await db2.from(T.documents).insert({
|
|
496
510
|
document_id: coreDoc.id,
|
|
497
|
-
tenant_id:
|
|
511
|
+
tenant_id: tenantId2,
|
|
498
512
|
template_id: input.templateId,
|
|
499
|
-
data: input.data ?? {}
|
|
513
|
+
data: input.data ?? {},
|
|
514
|
+
product_id: input.productId ?? null,
|
|
515
|
+
professional_id: input.professionalId ?? null
|
|
500
516
|
});
|
|
501
517
|
if (extError) {
|
|
502
518
|
await getClient().from("documents").delete().eq("id", coreDoc.id);
|
|
@@ -515,8 +531,8 @@ function createSupabaseFormsProvider() {
|
|
|
515
531
|
const { error: coreError } = await getClient().from("documents").update(coreUpdates).eq("id", id);
|
|
516
532
|
if (coreError) throw new Error(coreError.message);
|
|
517
533
|
if (input.data !== void 0) {
|
|
518
|
-
const
|
|
519
|
-
await
|
|
534
|
+
const db2 = getClient();
|
|
535
|
+
await db2.from(T.documents).update({ data: input.data, updated_at: now2 }).eq("document_id", id);
|
|
520
536
|
}
|
|
521
537
|
const doc = await this.getDocumentById(id);
|
|
522
538
|
return doc;
|
|
@@ -527,21 +543,21 @@ function createSupabaseFormsProvider() {
|
|
|
527
543
|
},
|
|
528
544
|
// ── Files ──────────────────────────────────────────────
|
|
529
545
|
async getDocumentFiles(documentId) {
|
|
530
|
-
const
|
|
531
|
-
const { data, error } = await
|
|
546
|
+
const db2 = getClient();
|
|
547
|
+
const { data, error } = await db2.from(T.documentFiles).select("*").eq("document_id", documentId).order("sort_order");
|
|
532
548
|
if (error) throw new Error(error.message);
|
|
533
549
|
return (data ?? []).map(mapFile);
|
|
534
550
|
},
|
|
535
551
|
async uploadFile(documentId, fieldKey, file) {
|
|
536
|
-
const
|
|
537
|
-
const
|
|
552
|
+
const db2 = getClient();
|
|
553
|
+
const tenantId2 = getTenantId();
|
|
538
554
|
const ext = file.name.split(".").pop() ?? "bin";
|
|
539
|
-
const storagePath = `${
|
|
540
|
-
const { error: uploadError } = await
|
|
555
|
+
const storagePath = `${tenantId2}/forms/${documentId}/${fieldKey}/${crypto.randomUUID()}.${ext}`;
|
|
556
|
+
const { error: uploadError } = await db2.storage.from("documents").upload(storagePath, file, { contentType: file.type });
|
|
541
557
|
if (uploadError) throw new Error(uploadError.message);
|
|
542
|
-
const { data: urlData } =
|
|
543
|
-
const { data, error } = await
|
|
544
|
-
tenant_id:
|
|
558
|
+
const { data: urlData } = db2.storage.from("documents").getPublicUrl(storagePath);
|
|
559
|
+
const { data, error } = await db2.from(T.documentFiles).insert({
|
|
560
|
+
tenant_id: tenantId2,
|
|
545
561
|
document_id: documentId,
|
|
546
562
|
field_key: fieldKey,
|
|
547
563
|
file_url: urlData.publicUrl,
|
|
@@ -552,9 +568,74 @@ function createSupabaseFormsProvider() {
|
|
|
552
568
|
if (error) throw new Error(error.message);
|
|
553
569
|
return mapFile(data);
|
|
554
570
|
},
|
|
571
|
+
async uploadMedia(blob, folder, fileName) {
|
|
572
|
+
const tenantId2 = getTenantId();
|
|
573
|
+
const ext = (fileName.split(".").pop() || "bin").toLowerCase();
|
|
574
|
+
const path = `${tenantId2}/forms/${folder}/${crypto.randomUUID()}.${ext}`;
|
|
575
|
+
const { error } = await getClient().storage.from(MEDIA_BUCKET).upload(path, blob, { contentType: blob.type || void 0, upsert: false });
|
|
576
|
+
if (error) throw new Error(error.message);
|
|
577
|
+
return { path };
|
|
578
|
+
},
|
|
579
|
+
async mediaUrl(path) {
|
|
580
|
+
const cached = signedCache.get(path);
|
|
581
|
+
if (cached && cached.until > Date.now()) return cached.url;
|
|
582
|
+
const { data, error } = await getClient().storage.from(MEDIA_BUCKET).createSignedUrl(path, SIGNED_TTL_S);
|
|
583
|
+
if (error || !data?.signedUrl) return null;
|
|
584
|
+
signedCache.set(path, { url: data.signedUrl, until: Date.now() + (SIGNED_TTL_S - 60) * 1e3 });
|
|
585
|
+
return data.signedUrl;
|
|
586
|
+
},
|
|
587
|
+
async searchCatalog(term) {
|
|
588
|
+
const db2 = getClient();
|
|
589
|
+
const tenantId2 = getTenantId();
|
|
590
|
+
const like = term.trim() ? `%${term.trim()}%` : null;
|
|
591
|
+
let sq = db2.from("products").select("id, name, price, category:categories(name)").eq("tenant_id", tenantId2).eq("kind", "service").eq("is_active", true).order("name").limit(500);
|
|
592
|
+
if (like) sq = sq.ilike("name", like);
|
|
593
|
+
let pq = db2.from("service_packages").select("id, name, price").eq("tenant_id", tenantId2).eq("is_active", true).order("name").limit(100);
|
|
594
|
+
if (like) pq = pq.ilike("name", like);
|
|
595
|
+
const [sv, pk] = await Promise.all([sq, pq]);
|
|
596
|
+
if (sv.error) throw new Error(sv.error.message);
|
|
597
|
+
return [
|
|
598
|
+
...(sv.data ?? []).map((r) => ({
|
|
599
|
+
kind: "service",
|
|
600
|
+
id: r.id,
|
|
601
|
+
name: r.name,
|
|
602
|
+
price: Number(r.price) || 0,
|
|
603
|
+
group: r.category?.name ?? null
|
|
604
|
+
})),
|
|
605
|
+
...(pk.data ?? []).map((r) => ({
|
|
606
|
+
kind: "package",
|
|
607
|
+
id: r.id,
|
|
608
|
+
name: r.name,
|
|
609
|
+
price: Number(r.price) || 0,
|
|
610
|
+
group: null
|
|
611
|
+
}))
|
|
612
|
+
];
|
|
613
|
+
},
|
|
614
|
+
async getLatestAnswers(personId, templateIds) {
|
|
615
|
+
if (!templateIds.length) return [];
|
|
616
|
+
const { data, error } = await getClient().from(T.documents).select("template_id, data, document:documents!inner(person_id, created_at, updated_at)").in("template_id", templateIds).eq("document.person_id", personId).order("created_at", { ascending: false, referencedTable: "documents" }).limit(50);
|
|
617
|
+
if (error) throw new Error(error.message);
|
|
618
|
+
const latest = /* @__PURE__ */ new Map();
|
|
619
|
+
for (const r of data ?? []) {
|
|
620
|
+
const at = r.document?.updated_at ?? r.document?.created_at ?? "";
|
|
621
|
+
const cur = latest.get(r.template_id);
|
|
622
|
+
if (!cur || at > cur.at) latest.set(r.template_id, { templateId: r.template_id, data: r.data ?? {}, at });
|
|
623
|
+
}
|
|
624
|
+
return [...latest.values()];
|
|
625
|
+
},
|
|
626
|
+
async getPackageServices(packageId) {
|
|
627
|
+
const { data, error } = await getClient().from("service_package_items").select("product_id, quantity, unit_price, product:products(name, price, kind)").eq("package_id", packageId).order("sort_order");
|
|
628
|
+
if (error) throw new Error(error.message);
|
|
629
|
+
return (data ?? []).filter((r) => r.product_id && (r.product?.kind ?? "service") === "service").map((r) => ({
|
|
630
|
+
serviceId: r.product_id,
|
|
631
|
+
name: r.product?.name ?? "",
|
|
632
|
+
unitPrice: Number(r.unit_price ?? r.product?.price) || 0,
|
|
633
|
+
quantity: Math.max(1, Number(r.quantity) || 1)
|
|
634
|
+
}));
|
|
635
|
+
},
|
|
555
636
|
async deleteFile(fileId) {
|
|
556
|
-
const
|
|
557
|
-
const { error } = await
|
|
637
|
+
const db2 = getClient();
|
|
638
|
+
const { error } = await db2.from(T.documentFiles).delete().eq("id", fileId);
|
|
558
639
|
if (error) throw new Error(error.message);
|
|
559
640
|
}
|
|
560
641
|
};
|
|
@@ -717,6 +798,8 @@ var en = {
|
|
|
717
798
|
"customForms.archiveTemplateConfirm": "Archive this form template? It leaves the list and the document picker, but is kept and existing documents are preserved.",
|
|
718
799
|
// Categories
|
|
719
800
|
"customForms.category.anamnesis": "Anamnesis",
|
|
801
|
+
"customForms.category.evaluation": "Evaluation",
|
|
802
|
+
"customForms.category.photos": "Photos",
|
|
720
803
|
"customForms.category.evolution": "Evolution Note",
|
|
721
804
|
"customForms.category.report": "Report",
|
|
722
805
|
"customForms.category.contract": "Contract",
|
|
@@ -764,6 +847,11 @@ var en = {
|
|
|
764
847
|
"customForms.fieldType.image": "Image / Drawing",
|
|
765
848
|
"customForms.fieldType.gallery": "Gallery",
|
|
766
849
|
"customForms.fieldType.budget": "Budget",
|
|
850
|
+
"customForms.fieldType.yes_no": "Yes or No",
|
|
851
|
+
"customForms.fieldType.yes_no_text": "Yes or No + Text",
|
|
852
|
+
"customForms.fieldType.stencil_photo": "Photo with stencil",
|
|
853
|
+
"customForms.fieldType.recommendation": "Recommendations",
|
|
854
|
+
"customForms.fieldType.signature": "Signature (drawn on screen)",
|
|
767
855
|
// Documents
|
|
768
856
|
"customForms.documents": "Documents",
|
|
769
857
|
"customForms.addDocument": "Add Document",
|
|
@@ -784,8 +872,8 @@ var en = {
|
|
|
784
872
|
"customForms.showContent": "Show content",
|
|
785
873
|
"customForms.hideContent": "Hide content",
|
|
786
874
|
"customForms.previewEmpty": "This record has no filled field.",
|
|
787
|
-
"customForms.previewMore": "See the other {count} fields",
|
|
788
|
-
"customForms.loadMore": "Load {count} more",
|
|
875
|
+
"customForms.previewMore": "See the other {{count}} fields",
|
|
876
|
+
"customForms.loadMore": "Load {{count}} more",
|
|
789
877
|
"customForms.loadingMore": "Loading\u2026",
|
|
790
878
|
"customForms.noRecords": "No records yet",
|
|
791
879
|
"customForms.noRecordsDescription": 'Anamnesis, evolution, report or contract \u2014 start with "New".',
|
|
@@ -799,7 +887,7 @@ var en = {
|
|
|
799
887
|
"customForms.deleteDocumentConfirm": "Delete this document?",
|
|
800
888
|
"customForms.archive": "Archive",
|
|
801
889
|
"customForms.archiveTitle": "Archive record",
|
|
802
|
-
"customForms.archiveDescription": "{name} leaves this record's list but stays stored \u2014 archiving does not delete.",
|
|
890
|
+
"customForms.archiveDescription": "{{name}} leaves this record's list but stays stored \u2014 archiving does not delete.",
|
|
803
891
|
"customForms.archiveDocumentConfirm": "Archive this document? It leaves the active list but is kept on file.",
|
|
804
892
|
"customForms.printPdf": "Print / PDF",
|
|
805
893
|
"customForms.usageCount": "{{count}} filled in",
|
|
@@ -847,6 +935,8 @@ var ptBR = {
|
|
|
847
935
|
"customForms.archiveTemplateConfirm": "Arquivar este modelo? Ele sai da lista e do seletor de documentos, mas fica guardado e os documentos existentes s\xE3o preservados.",
|
|
848
936
|
// Categories
|
|
849
937
|
"customForms.category.anamnesis": "Anamnese",
|
|
938
|
+
"customForms.category.evaluation": "Avalia\xE7\xE3o",
|
|
939
|
+
"customForms.category.photos": "Fotos",
|
|
850
940
|
"customForms.category.evolution": "Evolu\xE7\xE3o",
|
|
851
941
|
"customForms.category.report": "Laudo",
|
|
852
942
|
"customForms.category.contract": "Contrato",
|
|
@@ -894,6 +984,11 @@ var ptBR = {
|
|
|
894
984
|
"customForms.fieldType.image": "Imagem / Desenho",
|
|
895
985
|
"customForms.fieldType.gallery": "Galeria",
|
|
896
986
|
"customForms.fieldType.budget": "Or\xE7amento",
|
|
987
|
+
"customForms.fieldType.yes_no": "Sim ou N\xE3o",
|
|
988
|
+
"customForms.fieldType.yes_no_text": "Sim ou N\xE3o + Texto",
|
|
989
|
+
"customForms.fieldType.stencil_photo": "Foto com Gabarito",
|
|
990
|
+
"customForms.fieldType.recommendation": "Indica\xE7\xF5es",
|
|
991
|
+
"customForms.fieldType.signature": "Assinatura (escrita em tela)",
|
|
897
992
|
// Documents
|
|
898
993
|
"customForms.documents": "Documentos",
|
|
899
994
|
"customForms.addDocument": "Novo Documento",
|
|
@@ -917,8 +1012,8 @@ var ptBR = {
|
|
|
917
1012
|
"customForms.showContent": "Mostrar conte\xFAdo",
|
|
918
1013
|
"customForms.hideContent": "Ocultar conte\xFAdo",
|
|
919
1014
|
"customForms.previewEmpty": "Registro sem nenhum campo preenchido.",
|
|
920
|
-
"customForms.previewMore": "Ver os outros {count} campos",
|
|
921
|
-
"customForms.loadMore": "Carregar mais {count}",
|
|
1015
|
+
"customForms.previewMore": "Ver os outros {{count}} campos",
|
|
1016
|
+
"customForms.loadMore": "Carregar mais {{count}}",
|
|
922
1017
|
"customForms.loadingMore": "Carregando\u2026",
|
|
923
1018
|
"customForms.noRecords": "Nenhum registro",
|
|
924
1019
|
"customForms.noRecordsDescription": 'Anamnese, evolu\xE7\xE3o, laudo ou contrato \u2014 comece em "Novo".',
|
|
@@ -935,7 +1030,7 @@ var ptBR = {
|
|
|
935
1030
|
// O diálogo diz DE QUAL registro se trata: "Tem certeza?" sozinho é a
|
|
936
1031
|
// pergunta que ninguém consegue responder.
|
|
937
1032
|
"customForms.archiveTitle": "Arquivar registro",
|
|
938
|
-
"customForms.archiveDescription": "{name} sai da lista desta ficha, mas continua guardado \u2014 arquivar n\xE3o apaga.",
|
|
1033
|
+
"customForms.archiveDescription": "{{name}} sai da lista desta ficha, mas continua guardado \u2014 arquivar n\xE3o apaga.",
|
|
939
1034
|
"customForms.printPdf": "Imprimir / PDF",
|
|
940
1035
|
"customForms.usageCount": "{{count}} preenchidos",
|
|
941
1036
|
"customForms.usageLast": "\xFAltimo em {{date}}",
|
|
@@ -958,24 +1053,24 @@ var customFormsLocales = {
|
|
|
958
1053
|
en,
|
|
959
1054
|
"pt-BR": ptBR
|
|
960
1055
|
};
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
addDocument: "Add Document"
|
|
971
|
-
};
|
|
1056
|
+
var defaultLabels = () => ({
|
|
1057
|
+
pageTitle: tl("Forms", "Formul\xE1rios"),
|
|
1058
|
+
settingsLabel: tl("Forms & Documents", "Formul\xE1rios e documentos"),
|
|
1059
|
+
settingsSubtitle: tl("Create and manage custom forms for your business", "Crie e organize os formul\xE1rios da sua empresa"),
|
|
1060
|
+
templates: tl("Templates", "Modelos"),
|
|
1061
|
+
documents: tl("Documents", "Documentos"),
|
|
1062
|
+
newTemplate: tl("New Template", "Novo modelo"),
|
|
1063
|
+
addDocument: tl("Add Document", "Adicionar documento")
|
|
1064
|
+
});
|
|
972
1065
|
function resolveConfig(options) {
|
|
973
1066
|
return {
|
|
974
|
-
labels: { ...
|
|
1067
|
+
labels: { ...defaultLabels(), ...options?.labels }
|
|
975
1068
|
};
|
|
976
1069
|
}
|
|
977
1070
|
var CATEGORY_COLORS = {
|
|
978
1071
|
anamnesis: "bg-info-soft text-info-soft-foreground",
|
|
1072
|
+
evaluation: "bg-info-soft text-info-soft-foreground",
|
|
1073
|
+
photos: "bg-muted text-muted-foreground",
|
|
979
1074
|
evolution: "bg-success-soft text-success-soft-foreground",
|
|
980
1075
|
report: "bg-magic-soft text-magic-soft-foreground",
|
|
981
1076
|
contract: "bg-warning-soft text-warning-soft-foreground",
|
|
@@ -1223,7 +1318,7 @@ function CustomFormsSettingsTab({ config, provider, store }) {
|
|
|
1223
1318
|
window.location.hash = `#${ROUTE_BASE}/forms`;
|
|
1224
1319
|
store.getState().fetchTemplates();
|
|
1225
1320
|
}, [store]);
|
|
1226
|
-
|
|
1321
|
+
React12.useEffect(() => {
|
|
1227
1322
|
const sync = () => setBuilderTemplateId(readBuilderIdFromHash());
|
|
1228
1323
|
window.addEventListener("hashchange", sync);
|
|
1229
1324
|
return () => window.removeEventListener("hashchange", sync);
|
|
@@ -1254,11 +1349,11 @@ function BuilderWrapper({
|
|
|
1254
1349
|
config,
|
|
1255
1350
|
onBack
|
|
1256
1351
|
}) {
|
|
1257
|
-
const FormBuilderLazy =
|
|
1258
|
-
() => import('./FormBuilder-
|
|
1352
|
+
const FormBuilderLazy = React12.lazy(
|
|
1353
|
+
() => import('./FormBuilder-N4R5SENC.js').then((m) => ({ default: m.FormBuilder }))
|
|
1259
1354
|
);
|
|
1260
|
-
return /* @__PURE__ */ jsx(
|
|
1261
|
-
|
|
1355
|
+
return /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
|
|
1356
|
+
React12.Suspense,
|
|
1262
1357
|
{
|
|
1263
1358
|
fallback: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx("div", { className: "h-6 w-6 border-2 border-primary border-t-transparent animate-spin rounded-full" }) }),
|
|
1264
1359
|
children: /* @__PURE__ */ jsx(
|
|
@@ -1272,7 +1367,7 @@ function BuilderWrapper({
|
|
|
1272
1367
|
}
|
|
1273
1368
|
)
|
|
1274
1369
|
}
|
|
1275
|
-
);
|
|
1370
|
+
) });
|
|
1276
1371
|
}
|
|
1277
1372
|
var MAX_FIELDS = 8;
|
|
1278
1373
|
var MAX_CHARS = 280;
|
|
@@ -1287,6 +1382,35 @@ function asText(value) {
|
|
|
1287
1382
|
if (typeof value === "boolean") return value ? "Sim" : "N\xE3o";
|
|
1288
1383
|
return htmlToPlainText(String(value)) ?? String(value);
|
|
1289
1384
|
}
|
|
1385
|
+
function PreviewMedia({ items, provider }) {
|
|
1386
|
+
const [urls, setUrls] = useState([]);
|
|
1387
|
+
useEffect(() => {
|
|
1388
|
+
let alive = true;
|
|
1389
|
+
Promise.all(items.map((m) => Promise.resolve(provider.mediaUrl?.(m.path) ?? null).catch(() => null))).then((u) => {
|
|
1390
|
+
if (alive) setUrls(u);
|
|
1391
|
+
});
|
|
1392
|
+
return () => {
|
|
1393
|
+
alive = false;
|
|
1394
|
+
};
|
|
1395
|
+
}, [items.map((m) => m.path).join("|"), provider]);
|
|
1396
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 pt-1", children: items.map((m, i) => urls[i] ? m.type === "video" ? /* @__PURE__ */ jsx("video", { src: urls[i], className: "h-20 w-20 rounded-md border object-cover", preload: "metadata" }, m.path) : /* @__PURE__ */ jsx("img", { src: urls[i], alt: "", className: `h-20 rounded-md border object-cover ${m.type === "signature" ? "bg-white" : "w-16"}` }, m.path) : /* @__PURE__ */ jsx("div", { className: "h-20 w-16 animate-pulse rounded-md bg-muted" }, m.path)) });
|
|
1397
|
+
}
|
|
1398
|
+
function mediaOf(field, value) {
|
|
1399
|
+
if (field.type === "stencil_photo" || field.type === "signature") return isMedia(value) ? [value] : null;
|
|
1400
|
+
if (field.type === "gallery") return Array.isArray(value) ? value.filter(isMedia) : null;
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
function fieldText(field, value) {
|
|
1404
|
+
if (field.type === "yes_no" || field.type === "yes_no_text") {
|
|
1405
|
+
const a = yesNoAnswer(value);
|
|
1406
|
+
const extra = value && typeof value === "object" ? value.text?.trim() : "";
|
|
1407
|
+
return `${a === "sim" ? "Sim" : a === "nao" ? "N\xE3o" : ""}${extra ? ` \u2014 ${extra}` : ""}`;
|
|
1408
|
+
}
|
|
1409
|
+
if (field.type === "recommendation" && Array.isArray(value)) {
|
|
1410
|
+
return value.map((l) => `${l.quantity > 1 ? `${l.quantity}\xD7 ` : ""}${l.name}`).join(" \xB7 ");
|
|
1411
|
+
}
|
|
1412
|
+
return asText(value);
|
|
1413
|
+
}
|
|
1290
1414
|
function truncate(text) {
|
|
1291
1415
|
return text.length > MAX_CHARS ? `${text.slice(0, MAX_CHARS).trimEnd()}\u2026` : text;
|
|
1292
1416
|
}
|
|
@@ -1347,11 +1471,12 @@ function DocumentPreview({
|
|
|
1347
1471
|
const hidden = answered.length - shown.length;
|
|
1348
1472
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1349
1473
|
/* @__PURE__ */ jsx("dl", { className: "grid gap-x-6 gap-y-2 sm:grid-cols-2", children: shown.map((field) => {
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
1474
|
+
const media = mediaOf(field, doc.data[field.id]);
|
|
1475
|
+
const text = media ? "" : truncate(fieldText(field, doc.data[field.id]));
|
|
1476
|
+
const wide = field.type === "memo" || field.type === "richtext" || field.type === "gallery" || text.length > 60;
|
|
1352
1477
|
return /* @__PURE__ */ jsxs("div", { className: `min-w-0 ${wide ? "sm:col-span-2" : ""}`, children: [
|
|
1353
1478
|
/* @__PURE__ */ jsx("dt", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: htmlToPlainText(field.label) ?? field.label }),
|
|
1354
|
-
/* @__PURE__ */ jsx("dd", { className: "whitespace-pre-wrap text-sm leading-6 text-foreground", children: text })
|
|
1479
|
+
/* @__PURE__ */ jsx("dd", { className: "whitespace-pre-wrap text-sm leading-6 text-foreground", children: media ? /* @__PURE__ */ jsx(PreviewMedia, { items: media, provider }) : text })
|
|
1355
1480
|
] }, field.id);
|
|
1356
1481
|
}) }),
|
|
1357
1482
|
hidden > 0 && /* @__PURE__ */ jsx(
|
|
@@ -1367,6 +1492,8 @@ function DocumentPreview({
|
|
|
1367
1492
|
}
|
|
1368
1493
|
var CATEGORY_ORDER = [
|
|
1369
1494
|
"anamnesis",
|
|
1495
|
+
"evaluation",
|
|
1496
|
+
"photos",
|
|
1370
1497
|
"evolution",
|
|
1371
1498
|
"report",
|
|
1372
1499
|
"contract",
|
|
@@ -1374,6 +1501,8 @@ var CATEGORY_ORDER = [
|
|
|
1374
1501
|
];
|
|
1375
1502
|
var CATEGORY_GROUP_ORDER = {
|
|
1376
1503
|
anamnesis: 10,
|
|
1504
|
+
evaluation: 12,
|
|
1505
|
+
photos: 14,
|
|
1377
1506
|
evolution: 20,
|
|
1378
1507
|
report: 30,
|
|
1379
1508
|
contract: 40,
|
|
@@ -1381,6 +1510,8 @@ var CATEGORY_GROUP_ORDER = {
|
|
|
1381
1510
|
};
|
|
1382
1511
|
var CATEGORY_ICONS = {
|
|
1383
1512
|
anamnesis: ClipboardList,
|
|
1513
|
+
evaluation: Stethoscope,
|
|
1514
|
+
photos: Camera,
|
|
1384
1515
|
evolution: Activity,
|
|
1385
1516
|
report: FileCheck2,
|
|
1386
1517
|
contract: FileSignature,
|
|
@@ -1390,6 +1521,10 @@ function categoryLabel(category) {
|
|
|
1390
1521
|
switch (category) {
|
|
1391
1522
|
case "anamnesis":
|
|
1392
1523
|
return tl("Anamnesis", "Anamnese");
|
|
1524
|
+
case "evaluation":
|
|
1525
|
+
return tl("Evaluation", "Avalia\xE7\xE3o");
|
|
1526
|
+
case "photos":
|
|
1527
|
+
return tl("Photos", "Fotos");
|
|
1393
1528
|
case "evolution":
|
|
1394
1529
|
return tl("Evolution", "Evolu\xE7\xE3o");
|
|
1395
1530
|
case "report":
|
|
@@ -1405,6 +1540,8 @@ function normalizeCategory(value) {
|
|
|
1405
1540
|
}
|
|
1406
1541
|
var CATEGORY_TIMELINE_ICON = {
|
|
1407
1542
|
anamnesis: "ClipboardList",
|
|
1543
|
+
evaluation: "Stethoscope",
|
|
1544
|
+
photos: "Camera",
|
|
1408
1545
|
evolution: "Activity",
|
|
1409
1546
|
report: "FileCheck2",
|
|
1410
1547
|
contract: "FileSignature",
|
|
@@ -1412,6 +1549,8 @@ var CATEGORY_TIMELINE_ICON = {
|
|
|
1412
1549
|
};
|
|
1413
1550
|
var CATEGORY_TONE = {
|
|
1414
1551
|
anamnesis: "primary",
|
|
1552
|
+
evaluation: "primary",
|
|
1553
|
+
photos: "info",
|
|
1415
1554
|
evolution: "info",
|
|
1416
1555
|
report: "success",
|
|
1417
1556
|
contract: "warning",
|
|
@@ -1781,7 +1920,7 @@ function DocumentFormDialog({
|
|
|
1781
1920
|
provider.getTemplateById(existingDocument.templateId).then(setTemplate);
|
|
1782
1921
|
}
|
|
1783
1922
|
}, [existingDocument, template, provider]);
|
|
1784
|
-
const prefilled =
|
|
1923
|
+
const prefilled = React12.useRef(!!initialTemplate || !!existingDocument);
|
|
1785
1924
|
useEffect(() => {
|
|
1786
1925
|
if (prefilled.current || !template || existingDocument) return;
|
|
1787
1926
|
prefilled.current = true;
|
|
@@ -1813,7 +1952,7 @@ function DocumentFormDialog({
|
|
|
1813
1952
|
},
|
|
1814
1953
|
[template, existingDocument, data, personId, personName, store, onSaved, guardDocuments]
|
|
1815
1954
|
);
|
|
1816
|
-
const snapshot =
|
|
1955
|
+
const snapshot = React12.useRef(null);
|
|
1817
1956
|
useEffect(() => {
|
|
1818
1957
|
if (template && snapshot.current === null) {
|
|
1819
1958
|
snapshot.current = JSON.stringify(existingDocument?.data ?? {});
|
|
@@ -2063,7 +2202,7 @@ function AddDocumentDropdown({ personId, onSelect, optionFilter, label }) {
|
|
|
2063
2202
|
)
|
|
2064
2203
|
] }),
|
|
2065
2204
|
/* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-[min(60vh,20rem)] overflow-y-auto overscroll-contain p-1", children: [
|
|
2066
|
-
groups.map((group, gi) => /* @__PURE__ */ jsxs(
|
|
2205
|
+
groups.map((group, gi) => /* @__PURE__ */ jsxs(React12.Fragment, { children: [
|
|
2067
2206
|
gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
2068
2207
|
/* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
|
|
2069
2208
|
group.items.map((opt) => {
|
|
@@ -2278,6 +2417,91 @@ function FileDocumentViewer({ document: doc, onBack }) {
|
|
|
2278
2417
|
] }) })
|
|
2279
2418
|
] });
|
|
2280
2419
|
}
|
|
2420
|
+
var RANK = { danger: 3, warning: 2, info: 1 };
|
|
2421
|
+
var TONE = {
|
|
2422
|
+
danger: { wrap: "border-destructive/40 bg-destructive/5", chip: "bg-destructive text-destructive-foreground", dot: "bg-destructive" },
|
|
2423
|
+
warning: { wrap: "border-warning/50 bg-warning-soft", chip: "bg-warning text-warning-foreground", dot: "bg-warning" },
|
|
2424
|
+
info: { wrap: "border-info/40 bg-info-soft", chip: "bg-info text-white", dot: "bg-info" }
|
|
2425
|
+
};
|
|
2426
|
+
function PersonAnswerAlerts({ personId, provider }) {
|
|
2427
|
+
const [alerts, setAlerts] = React12.useState([]);
|
|
2428
|
+
const [open, setOpen] = React12.useState(false);
|
|
2429
|
+
React12.useEffect(() => {
|
|
2430
|
+
if (!personId || !provider.getLatestAnswers) return;
|
|
2431
|
+
let alive = true;
|
|
2432
|
+
(async () => {
|
|
2433
|
+
try {
|
|
2434
|
+
const { data: templates } = await provider.getTemplates({ pageSize: 200 });
|
|
2435
|
+
const withAlerts = templates.filter((t) => t.isActive && t.schema.fields.some((f) => alertOf(f)));
|
|
2436
|
+
if (!withAlerts.length) return;
|
|
2437
|
+
const answers = await provider.getLatestAnswers(personId, withAlerts.map((t) => t.id));
|
|
2438
|
+
const out = [];
|
|
2439
|
+
for (const a of answers) {
|
|
2440
|
+
const t = withAlerts.find((x) => x.id === a.templateId);
|
|
2441
|
+
if (!t) continue;
|
|
2442
|
+
for (const f of t.schema.fields) {
|
|
2443
|
+
const hit = answerAlerts(f, a.data[f.id]);
|
|
2444
|
+
if (!hit) continue;
|
|
2445
|
+
out.push({
|
|
2446
|
+
key: `${t.id}:${f.id}`,
|
|
2447
|
+
label: hit.message || f.label,
|
|
2448
|
+
severity: hit.severity ?? "danger",
|
|
2449
|
+
answer: yesNoAnswer(a.data[f.id]),
|
|
2450
|
+
templateName: t.name
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
out.sort((x, y) => RANK[y.severity] - RANK[x.severity]);
|
|
2455
|
+
if (alive) setAlerts(out);
|
|
2456
|
+
} catch {
|
|
2457
|
+
}
|
|
2458
|
+
})();
|
|
2459
|
+
return () => {
|
|
2460
|
+
alive = false;
|
|
2461
|
+
};
|
|
2462
|
+
}, [personId, provider]);
|
|
2463
|
+
if (!alerts.length) return null;
|
|
2464
|
+
const tone = TONE[alerts[0].severity];
|
|
2465
|
+
return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-lg border-2 ${tone.wrap}`, children: [
|
|
2466
|
+
/* @__PURE__ */ jsxs(
|
|
2467
|
+
"button",
|
|
2468
|
+
{
|
|
2469
|
+
type: "button",
|
|
2470
|
+
onClick: () => setOpen((o) => !o),
|
|
2471
|
+
"aria-expanded": open,
|
|
2472
|
+
className: "flex w-full items-center gap-2 px-3 py-2 text-left",
|
|
2473
|
+
children: [
|
|
2474
|
+
/* @__PURE__ */ jsx("span", { className: `flex h-7 w-7 shrink-0 items-center justify-center rounded-full ${tone.chip}`, children: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4" }) }),
|
|
2475
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
2476
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-sm font-semibold leading-tight", children: [
|
|
2477
|
+
alerts.length,
|
|
2478
|
+
" alerta",
|
|
2479
|
+
alerts.length > 1 ? "s" : "",
|
|
2480
|
+
" no hist\xF3rico do cliente"
|
|
2481
|
+
] }),
|
|
2482
|
+
/* @__PURE__ */ jsxs("span", { className: "block truncate text-xs text-muted-foreground", children: [
|
|
2483
|
+
alerts.slice(0, 3).map((a) => a.label).join(" \xB7 "),
|
|
2484
|
+
alerts.length > 3 ? "\u2026" : ""
|
|
2485
|
+
] })
|
|
2486
|
+
] }),
|
|
2487
|
+
open ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4 text-muted-foreground" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })
|
|
2488
|
+
]
|
|
2489
|
+
}
|
|
2490
|
+
),
|
|
2491
|
+
open && /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/40 border-t px-3", children: alerts.map((a) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2 py-2", children: [
|
|
2492
|
+
/* @__PURE__ */ jsx("span", { className: `mt-1.5 h-2 w-2 shrink-0 rounded-full ${TONE[a.severity].dot}` }),
|
|
2493
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
2494
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium leading-tight", children: a.label }),
|
|
2495
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-[11px] leading-tight text-muted-foreground", children: [
|
|
2496
|
+
"Respondeu ",
|
|
2497
|
+
/* @__PURE__ */ jsx("strong", { className: "uppercase", children: a.answer === "sim" ? "Sim" : "N\xE3o" }),
|
|
2498
|
+
" \xB7 ",
|
|
2499
|
+
a.templateName
|
|
2500
|
+
] })
|
|
2501
|
+
] })
|
|
2502
|
+
] }, a.key)) })
|
|
2503
|
+
] });
|
|
2504
|
+
}
|
|
2281
2505
|
function registerFormsTimelineSource() {
|
|
2282
2506
|
registerTimelineSource({
|
|
2283
2507
|
id: "documents",
|
|
@@ -2285,9 +2509,9 @@ function registerFormsTimelineSource() {
|
|
|
2285
2509
|
icon: "FileText",
|
|
2286
2510
|
tone: "primary",
|
|
2287
2511
|
async fetch(args) {
|
|
2288
|
-
const
|
|
2289
|
-
if (!
|
|
2290
|
-
let query =
|
|
2512
|
+
const db2 = getSupabaseClientOptional();
|
|
2513
|
+
if (!db2) return [];
|
|
2514
|
+
let query = db2.from("v_documents").select("id, title, kind, status, template_name, created_at").eq("tenant_id", args.tenantId).eq("person_id", args.personId).eq("is_active", true).order("created_at", { ascending: false }).limit(args.limit);
|
|
2291
2515
|
if (args.before) query = query.lt("created_at", args.before);
|
|
2292
2516
|
const { data, error } = await query;
|
|
2293
2517
|
if (error) return [];
|
|
@@ -2309,10 +2533,10 @@ function registerFormsTimelineSource() {
|
|
|
2309
2533
|
function registerFormsTabCounts() {
|
|
2310
2534
|
registerTabCountProvider({
|
|
2311
2535
|
id: "custom_forms:documents",
|
|
2312
|
-
async fetch({ tenantId, personId }) {
|
|
2313
|
-
const
|
|
2314
|
-
if (!
|
|
2315
|
-
const { data, error } = await
|
|
2536
|
+
async fetch({ tenantId: tenantId2, personId }) {
|
|
2537
|
+
const db2 = getSupabaseClientOptional();
|
|
2538
|
+
if (!db2) return {};
|
|
2539
|
+
const { data, error } = await db2.from("documents").select("kind").eq("tenant_id", tenantId2).eq("person_id", personId).eq("is_active", true).neq("status", "archived");
|
|
2316
2540
|
if (error) return {};
|
|
2317
2541
|
let records = 0;
|
|
2318
2542
|
let files = 0;
|
|
@@ -2407,10 +2631,10 @@ function buildFormsAnalyticsCards() {
|
|
|
2407
2631
|
}
|
|
2408
2632
|
];
|
|
2409
2633
|
}
|
|
2410
|
-
async function getPersonFormRequirements(
|
|
2634
|
+
async function getPersonFormRequirements(tenantId2, personId) {
|
|
2411
2635
|
const client = getFayzSupabaseClient();
|
|
2412
2636
|
const { data, error } = await client.rpc("forms_person_requirements", {
|
|
2413
|
-
p_tenant_id:
|
|
2637
|
+
p_tenant_id: tenantId2,
|
|
2414
2638
|
p_person_id: personId
|
|
2415
2639
|
});
|
|
2416
2640
|
if (error) throw error;
|
|
@@ -2452,17 +2676,17 @@ function Linha({ item }) {
|
|
|
2452
2676
|
)
|
|
2453
2677
|
] });
|
|
2454
2678
|
}
|
|
2455
|
-
function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, className }) {
|
|
2456
|
-
const
|
|
2457
|
-
const [rows, setRows] =
|
|
2458
|
-
|
|
2459
|
-
if (!personId || !
|
|
2679
|
+
function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, sendLink, className }) {
|
|
2680
|
+
const tenantId2 = useOrganizationStore((s) => s.currentOrg?.id);
|
|
2681
|
+
const [rows, setRows] = React12.useState(null);
|
|
2682
|
+
React12.useEffect(() => {
|
|
2683
|
+
if (!personId || !tenantId2) {
|
|
2460
2684
|
setRows([]);
|
|
2461
2685
|
return;
|
|
2462
2686
|
}
|
|
2463
2687
|
let cancelled = false;
|
|
2464
2688
|
setRows(null);
|
|
2465
|
-
getPersonFormRequirements(
|
|
2689
|
+
getPersonFormRequirements(tenantId2, personId).then((r) => {
|
|
2466
2690
|
if (!cancelled) setRows(r);
|
|
2467
2691
|
}).catch(() => {
|
|
2468
2692
|
if (!cancelled) setRows([]);
|
|
@@ -2470,8 +2694,8 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, cl
|
|
|
2470
2694
|
return () => {
|
|
2471
2695
|
cancelled = true;
|
|
2472
2696
|
};
|
|
2473
|
-
}, [personId,
|
|
2474
|
-
const itens =
|
|
2697
|
+
}, [personId, tenantId2, version]);
|
|
2698
|
+
const itens = React12.useMemo(() => [
|
|
2475
2699
|
// O QUE NÃO ALERTA NÃO INTERROMPE. Exigir um formulário põe a bola no
|
|
2476
2700
|
// checklist do atendimento; ALERTAR é a segunda decisão do V1
|
|
2477
2701
|
// (`alert_when_missing`, desligada por padrão) e é ela que autoriza a
|
|
@@ -2498,49 +2722,101 @@ function PersonFormsAlert({ personId, onFill, version = 0, extras, onPending, cl
|
|
|
2498
2722
|
const pendentes = itens.filter((i) => i.state !== "done");
|
|
2499
2723
|
const faltando = pendentes.length;
|
|
2500
2724
|
const chave = pendentes.map((i) => i.label).join("|");
|
|
2501
|
-
|
|
2725
|
+
React12.useEffect(() => {
|
|
2502
2726
|
onPending?.(chave ? chave.split("|") : []);
|
|
2503
2727
|
}, [chave]);
|
|
2504
2728
|
if (rows === null) return /* @__PURE__ */ jsx(Skeleton, { className: `h-12 w-full ${className ?? ""}` });
|
|
2505
2729
|
if (itens.length === 0) return null;
|
|
2506
2730
|
return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-lg border ${faltando ? "border-warning/40 bg-warning/5" : "border-success/30 bg-success/5"} ${className ?? ""}`, children: [
|
|
2507
|
-
/* @__PURE__ */ jsxs("
|
|
2731
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-inherit px-3 py-1.5 text-[11px] font-medium text-foreground", children: [
|
|
2508
2732
|
/* @__PURE__ */ jsx(ClipboardList, { className: `h-3.5 w-3.5 ${faltando ? "text-warning" : "text-success"}`, "aria-hidden": true }),
|
|
2509
|
-
faltando === 0 ? "Checagem do paciente \u2014 tudo em dia" : faltando === 1 ? "1 item pendente com o paciente" : `${faltando} itens pendentes com o paciente
|
|
2733
|
+
faltando === 0 ? "Checagem do paciente \u2014 tudo em dia" : faltando === 1 ? "1 item pendente com o paciente" : `${faltando} itens pendentes com o paciente`,
|
|
2734
|
+
sendLink && /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
2735
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
2736
|
+
"button",
|
|
2737
|
+
{
|
|
2738
|
+
type: "button",
|
|
2739
|
+
"aria-label": "Enviar link ao paciente pelo WhatsApp",
|
|
2740
|
+
"aria-disabled": sendLink === "soon",
|
|
2741
|
+
onClick: sendLink === "soon" ? void 0 : sendLink,
|
|
2742
|
+
className: `ml-auto inline-flex h-6 w-6 items-center justify-center rounded-md transition-colors ${sendLink === "soon" ? "cursor-not-allowed text-muted-foreground/60" : "text-foreground hover:bg-muted"}`,
|
|
2743
|
+
children: /* @__PURE__ */ jsx(QrCode, { className: "h-3.5 w-3.5", "aria-hidden": true })
|
|
2744
|
+
}
|
|
2745
|
+
) }),
|
|
2746
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "left", children: sendLink === "soon" ? "Enviar pelo WhatsApp (QR Code) \u2014 em breve" : "Enviar pelo WhatsApp (QR Code)" })
|
|
2747
|
+
] }) })
|
|
2510
2748
|
] }),
|
|
2511
2749
|
/* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/60", children: itens.map((i) => /* @__PURE__ */ jsx(Linha, { item: i }, i.id)) })
|
|
2512
2750
|
] });
|
|
2513
2751
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2752
|
+
function recommendationsOf(template, data) {
|
|
2753
|
+
const by = /* @__PURE__ */ new Map();
|
|
2754
|
+
for (const f of template.schema.fields) {
|
|
2755
|
+
if (f.type !== "recommendation" || !Array.isArray(data[f.id])) continue;
|
|
2756
|
+
for (const l of data[f.id]) {
|
|
2757
|
+
const cur = by.get(l.serviceId);
|
|
2758
|
+
by.set(l.serviceId, cur ? { ...cur, quantity: cur.quantity + l.quantity } : { ...l });
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return [...by.values()];
|
|
2520
2762
|
}
|
|
2521
|
-
function FormFillDialog({
|
|
2522
|
-
|
|
2763
|
+
function FormFillDialog({
|
|
2764
|
+
templateId,
|
|
2765
|
+
personId,
|
|
2766
|
+
personName,
|
|
2767
|
+
onSaved,
|
|
2768
|
+
onClose,
|
|
2769
|
+
frame = "modal",
|
|
2770
|
+
subject,
|
|
2771
|
+
productId,
|
|
2772
|
+
professionalId,
|
|
2773
|
+
recommendationShortcuts
|
|
2774
|
+
}) {
|
|
2775
|
+
const provider = React12.useMemo(() => createSafeDataProvider(
|
|
2523
2776
|
() => createSupabaseFormsProvider(),
|
|
2524
2777
|
() => createMockFormsProvider()
|
|
2525
2778
|
), []);
|
|
2526
|
-
const store =
|
|
2527
|
-
const config =
|
|
2528
|
-
const [template, setTemplate] =
|
|
2529
|
-
const [erro, setErro] =
|
|
2530
|
-
const [data, setData] =
|
|
2531
|
-
const [salvando, setSalvando] =
|
|
2779
|
+
const store = React12.useMemo(() => createCustomFormsStore(provider), [provider]);
|
|
2780
|
+
const config = React12.useMemo(() => resolveConfig(), []);
|
|
2781
|
+
const [template, setTemplate] = React12.useState(null);
|
|
2782
|
+
const [erro, setErro] = React12.useState(null);
|
|
2783
|
+
const [data, setData] = React12.useState({});
|
|
2784
|
+
const [salvando, setSalvando] = React12.useState(false);
|
|
2785
|
+
const [missing, setMissing] = React12.useState(/* @__PURE__ */ new Set());
|
|
2786
|
+
React12.useEffect(() => {
|
|
2787
|
+
if (!template || missing.size === 0) return;
|
|
2788
|
+
const still = new Set([...missing].filter((id) => {
|
|
2789
|
+
const f = template.schema.fields.find((x) => x.id === id);
|
|
2790
|
+
return f ? isFieldEmpty(f, data[id]) : false;
|
|
2791
|
+
}));
|
|
2792
|
+
if (still.size !== missing.size) setMissing(still);
|
|
2793
|
+
if (still.size === 0) setErro(null);
|
|
2794
|
+
}, [data, template, missing]);
|
|
2532
2795
|
async function salvar() {
|
|
2533
2796
|
if (!template) return;
|
|
2797
|
+
const faltando = template.schema.fields.filter((f) => f.required && isFieldEmpty(f, data[f.id]));
|
|
2798
|
+
if (faltando.length) {
|
|
2799
|
+
setMissing(new Set(faltando.map((f) => f.id)));
|
|
2800
|
+
const primeiro = [...faltando].sort((a, b) => a.row - b.row || a.col - b.col)[0];
|
|
2801
|
+
requestAnimationFrame(() => document.getElementById(`ff-field-${primeiro.id}`)?.scrollIntoView({ block: "center" }));
|
|
2802
|
+
setErro(faltando.length === 1 ? `Preencha o campo obrigat\xF3rio destacado: ${faltando[0].label}` : `Preencha os ${faltando.length} campos obrigat\xF3rios destacados.`);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
setErro(null);
|
|
2534
2806
|
setSalvando(true);
|
|
2535
2807
|
try {
|
|
2536
|
-
await store.getState().createDocument({
|
|
2808
|
+
const doc = await store.getState().createDocument({
|
|
2537
2809
|
templateId: template.id,
|
|
2538
2810
|
personId,
|
|
2539
2811
|
title: personName ? `${template.name} \u2014 ${personName}` : template.name,
|
|
2540
2812
|
data,
|
|
2541
|
-
status: "completed"
|
|
2813
|
+
status: "completed",
|
|
2814
|
+
subjectType: subject?.type,
|
|
2815
|
+
subjectId: subject?.id,
|
|
2816
|
+
productId: productId ?? void 0,
|
|
2817
|
+
professionalId: professionalId ?? void 0
|
|
2542
2818
|
});
|
|
2543
|
-
onSaved?.();
|
|
2819
|
+
await onSaved?.({ documentId: doc.id, template, data, recommendations: recommendationsOf(template, data) });
|
|
2544
2820
|
onClose();
|
|
2545
2821
|
} catch (e) {
|
|
2546
2822
|
setErro(e instanceof Error ? e.message : "N\xE3o foi poss\xEDvel salvar o formul\xE1rio.");
|
|
@@ -2548,7 +2824,7 @@ function FormFillDialog({ templateId, personId, personName, onSaved, onClose, fr
|
|
|
2548
2824
|
setSalvando(false);
|
|
2549
2825
|
}
|
|
2550
2826
|
}
|
|
2551
|
-
|
|
2827
|
+
React12.useEffect(() => {
|
|
2552
2828
|
let vivo = true;
|
|
2553
2829
|
(async () => {
|
|
2554
2830
|
try {
|
|
@@ -2563,13 +2839,25 @@ function FormFillDialog({ templateId, personId, personName, onSaved, onClose, fr
|
|
|
2563
2839
|
};
|
|
2564
2840
|
}, [templateId, store]);
|
|
2565
2841
|
const corpo = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2566
|
-
/* @__PURE__ */
|
|
2567
|
-
/*
|
|
2568
|
-
|
|
2569
|
-
/*
|
|
2570
|
-
/* @__PURE__ */ jsx("div", { className: "
|
|
2571
|
-
|
|
2572
|
-
|
|
2842
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: [
|
|
2843
|
+
erro && /* @__PURE__ */ jsx("p", { role: "alert", className: "mb-3 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: erro }),
|
|
2844
|
+
!template ? erro ? null : (
|
|
2845
|
+
/* Esqueleto com a FORMA do formulário: três campos e um rótulo. */
|
|
2846
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-4", "aria-busy": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
2847
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-24 animate-pulse rounded bg-muted/70" }),
|
|
2848
|
+
/* @__PURE__ */ jsx("div", { className: "h-9 w-full animate-pulse rounded-lg bg-muted" })
|
|
2849
|
+
] }, i)) })
|
|
2850
|
+
) : /* @__PURE__ */ jsx(CustomFormsProvider, { value: { config, provider, store }, children: /* @__PURE__ */ jsx(
|
|
2851
|
+
FormRenderer,
|
|
2852
|
+
{
|
|
2853
|
+
schema: template.schema,
|
|
2854
|
+
data,
|
|
2855
|
+
onChange: setData,
|
|
2856
|
+
missing,
|
|
2857
|
+
recommendationShortcuts
|
|
2858
|
+
}
|
|
2859
|
+
) })
|
|
2860
|
+
] }),
|
|
2573
2861
|
/* @__PURE__ */ jsx("div", { className: "border-t fayz-glass-divider px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5 shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
|
|
2574
2862
|
/* @__PURE__ */ jsx(
|
|
2575
2863
|
"button",
|
|
@@ -2601,6 +2889,544 @@ function FormFillDialog({ templateId, personId, personName, onSaved, onClose, fr
|
|
|
2601
2889
|
] }) });
|
|
2602
2890
|
}
|
|
2603
2891
|
|
|
2892
|
+
// src/lib/contract-render.ts
|
|
2893
|
+
var MONTHS = ["janeiro", "fevereiro", "mar\xE7o", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
|
|
2894
|
+
var brl = (n) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(n);
|
|
2895
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
2896
|
+
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2897
|
+
function dateBR(value) {
|
|
2898
|
+
if (!value) return "";
|
|
2899
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
2900
|
+
return m ? `${m[3]}/${m[2]}/${m[1]}` : value;
|
|
2901
|
+
}
|
|
2902
|
+
function itemsTable(items, withDiscount) {
|
|
2903
|
+
if (!items?.length) return "";
|
|
2904
|
+
const head = ["Item", "Qtd", "Valor Unit.", ...withDiscount ? ["Desconto"] : [], "Total"];
|
|
2905
|
+
const rows = items.map((i) => [
|
|
2906
|
+
esc(i.description),
|
|
2907
|
+
String(i.quantity),
|
|
2908
|
+
brl(i.unitPrice),
|
|
2909
|
+
...withDiscount ? [brl(i.discount ?? 0)] : [],
|
|
2910
|
+
brl(i.total)
|
|
2911
|
+
]);
|
|
2912
|
+
return `<table><thead><tr>${head.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
|
2913
|
+
}
|
|
2914
|
+
function renderContractTags(templateHtml, ctx) {
|
|
2915
|
+
const now2 = ctx.now ?? /* @__PURE__ */ new Date();
|
|
2916
|
+
const c = ctx.client ?? {};
|
|
2917
|
+
const u = ctx.unit ?? {};
|
|
2918
|
+
const services = ctx.services ?? [];
|
|
2919
|
+
const names = services.map((s) => esc(s.name)).join(", ");
|
|
2920
|
+
const map = {
|
|
2921
|
+
"Cliente.Nome": c.name ?? "",
|
|
2922
|
+
"Cliente.NomeSocial": c.socialName ?? "",
|
|
2923
|
+
"Cliente.CPF": c.cpf ?? "",
|
|
2924
|
+
"Cliente.RG": c.rg ?? "",
|
|
2925
|
+
"Cliente.Nascimento": dateBR(c.birthDate),
|
|
2926
|
+
"Cliente.Email": c.email ?? "",
|
|
2927
|
+
"Cliente.Endereco": c.street ?? "",
|
|
2928
|
+
"Cliente.Numero": c.number ?? "",
|
|
2929
|
+
"Cliente.Complemento": c.complement ?? "",
|
|
2930
|
+
"Cliente.Bairro": c.district ?? "",
|
|
2931
|
+
"Cliente.Cidade": c.city ?? "",
|
|
2932
|
+
"Cliente.Estado": c.state ?? "",
|
|
2933
|
+
"Cliente.Cep": c.postalCode ?? "",
|
|
2934
|
+
"Cliente.Telefone": c.phone ?? "",
|
|
2935
|
+
"Cliente.Celular": c.mobile ?? "",
|
|
2936
|
+
"Unidade.NomeFantasia": u.tradeName ?? "",
|
|
2937
|
+
"Unidade.RazaoSocial": u.legalName ?? "",
|
|
2938
|
+
"Unidade.CNPJ": u.cnpj ?? "",
|
|
2939
|
+
"Unidade.Endereco": u.street ?? "",
|
|
2940
|
+
"Unidade.Numero": u.number ?? "",
|
|
2941
|
+
"Unidade.Complemento": u.complement ?? "",
|
|
2942
|
+
"Unidade.Bairro": u.district ?? "",
|
|
2943
|
+
"Unidade.Cidade": u.city ?? "",
|
|
2944
|
+
"Unidade.Estado": u.state ?? "",
|
|
2945
|
+
"Unidade.Cep": u.postalCode ?? "",
|
|
2946
|
+
"Unidade.Telefone": u.phone ?? "",
|
|
2947
|
+
"Unidade.Celular": u.mobile ?? "",
|
|
2948
|
+
"Unidade.Email": u.email ?? "",
|
|
2949
|
+
"Profissional.Nome": ctx.professional?.name ?? "",
|
|
2950
|
+
"Servicos.Lista": names,
|
|
2951
|
+
"Servicos.Nome": names,
|
|
2952
|
+
"Servicos.PrecoTotal": brl(services.reduce((s, x) => s + x.price, 0)),
|
|
2953
|
+
"Servicos.TextoContrato": services.filter((s) => s.contractText).map((s) => s.contractText).join("<br/><br/>"),
|
|
2954
|
+
"FormasPagto.Lista": (ctx.paymentMethods ?? []).map(esc).join(", "),
|
|
2955
|
+
"Orcamento.Itens": itemsTable(ctx.approvedItems, true),
|
|
2956
|
+
"Orcamento.ItensAprovados": itemsTable(ctx.approvedItems, true),
|
|
2957
|
+
"Indicacoes.Itens": itemsTable(ctx.recommendedItems, false),
|
|
2958
|
+
"Indicacoes.Lista": (ctx.recommendedItems ?? []).map((i) => esc(i.description)).join(", "),
|
|
2959
|
+
"Sistema.Data": `${pad(now2.getDate())}/${pad(now2.getMonth() + 1)}/${now2.getFullYear()}`,
|
|
2960
|
+
"Sistema.DataExtenso": `${pad(now2.getDate())} de ${MONTHS[now2.getMonth()]} de ${now2.getFullYear()}`,
|
|
2961
|
+
"Sistema.Hora": `${pad(now2.getHours())}:${pad(now2.getMinutes())}`
|
|
2962
|
+
};
|
|
2963
|
+
const built = /* @__PURE__ */ new Set([
|
|
2964
|
+
"Orcamento.Itens",
|
|
2965
|
+
"Orcamento.ItensAprovados",
|
|
2966
|
+
"Indicacoes.Itens",
|
|
2967
|
+
"Servicos.TextoContrato",
|
|
2968
|
+
"Servicos.Lista",
|
|
2969
|
+
"Servicos.Nome",
|
|
2970
|
+
"Indicacoes.Lista",
|
|
2971
|
+
"FormasPagto.Lista"
|
|
2972
|
+
]);
|
|
2973
|
+
return templateHtml.replace(/\{\{\s*([A-Za-z]+\.[A-Za-z]+)\s*\}\}/g, (whole, key) => {
|
|
2974
|
+
if (!(key in map)) return whole;
|
|
2975
|
+
return built.has(key) ? map[key] : esc(map[key]);
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/lib/attendance-contracts.ts
|
|
2980
|
+
var db = () => {
|
|
2981
|
+
const c = getSupabaseClientOptional();
|
|
2982
|
+
if (!c) throw new Error("Supabase not initialized");
|
|
2983
|
+
return c;
|
|
2984
|
+
};
|
|
2985
|
+
var tenantId = () => getFormsTenantId() ?? getActiveTenantId() ?? "";
|
|
2986
|
+
var BUCKET = "attachments";
|
|
2987
|
+
function toLine(s) {
|
|
2988
|
+
const total = s.unitPrice * s.quantity - (s.discount ?? 0);
|
|
2989
|
+
return { description: s.name, quantity: s.quantity, unitPrice: s.unitPrice, discount: s.discount ?? 0, total };
|
|
2990
|
+
}
|
|
2991
|
+
function mapContract(row, templates, sessionTermId) {
|
|
2992
|
+
const m = row.metadata?.contract ?? {};
|
|
2993
|
+
const t = templates.get(m.templateId);
|
|
2994
|
+
return {
|
|
2995
|
+
id: row.id,
|
|
2996
|
+
templateId: m.templateId,
|
|
2997
|
+
templateName: t?.name ?? row.title ?? "Contrato",
|
|
2998
|
+
status: row.status === "signed" ? "signed" : "pending",
|
|
2999
|
+
html: m.html ?? "",
|
|
3000
|
+
serviceIds: m.serviceIds ?? [],
|
|
3001
|
+
isSessionTerm: !!sessionTermId && m.templateId === sessionTermId,
|
|
3002
|
+
requireSignature: t?.require_screen_signature ?? true,
|
|
3003
|
+
requireSelfie: t?.require_selfie ?? false,
|
|
3004
|
+
blocksFinish: t?.require_signature_for_execution ?? false,
|
|
3005
|
+
signedByName: m.signedByName ?? null,
|
|
3006
|
+
signedAt: m.signedAt ?? null,
|
|
3007
|
+
signaturePath: m.signaturePath ?? null,
|
|
3008
|
+
selfiePath: m.selfiePath ?? null
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
async function loadTemplates() {
|
|
3012
|
+
const { data, error } = await db().from("plg_forms_contract_templates").select("*").eq("tenant_id", tenantId());
|
|
3013
|
+
if (error) throw new Error(error.message);
|
|
3014
|
+
return new Map((data ?? []).map((t) => [t.id, t]));
|
|
3015
|
+
}
|
|
3016
|
+
async function sessionTermTemplateId() {
|
|
3017
|
+
const { data } = await db().from("tenants").select("settings").eq("id", tenantId()).maybeSingle();
|
|
3018
|
+
return data?.settings?.attendance_flow?.session_term_template_id ?? null;
|
|
3019
|
+
}
|
|
3020
|
+
async function loadContext(input) {
|
|
3021
|
+
const c = db();
|
|
3022
|
+
const [person, address, unit, pro] = await Promise.all([
|
|
3023
|
+
c.from("people").select("name, document_number, date_of_birth, email, phone, address, city, state, postal_code").eq("id", input.personId).maybeSingle(),
|
|
3024
|
+
c.from("addresses").select("street, number, complement, district, city, state, postal_code, is_default").eq("owner_type", "person").eq("owner_id", input.personId).order("is_default", { ascending: false }).limit(1).maybeSingle(),
|
|
3025
|
+
input.unitId ? c.from("v_units").select("name, legal_name, cnpj, address_street, address_number, address_complement, address_district, city, state, postal_code, phone, mobile_1, email").eq("id", input.unitId).maybeSingle() : Promise.resolve({ data: null }),
|
|
3026
|
+
input.professionalId ? c.from("people").select("name").eq("id", input.professionalId).maybeSingle() : Promise.resolve({ data: null })
|
|
3027
|
+
]);
|
|
3028
|
+
const p = person.data ?? {};
|
|
3029
|
+
const a = address.data ?? {};
|
|
3030
|
+
const u = unit.data ?? {};
|
|
3031
|
+
return {
|
|
3032
|
+
client: {
|
|
3033
|
+
name: p.name,
|
|
3034
|
+
cpf: p.document_number,
|
|
3035
|
+
birthDate: p.date_of_birth,
|
|
3036
|
+
email: p.email,
|
|
3037
|
+
phone: p.phone,
|
|
3038
|
+
mobile: p.phone,
|
|
3039
|
+
street: a.street ?? p.address,
|
|
3040
|
+
number: a.number,
|
|
3041
|
+
complement: a.complement,
|
|
3042
|
+
district: a.district,
|
|
3043
|
+
city: a.city ?? p.city,
|
|
3044
|
+
state: a.state ?? p.state,
|
|
3045
|
+
postalCode: a.postal_code ?? p.postal_code
|
|
3046
|
+
},
|
|
3047
|
+
unit: {
|
|
3048
|
+
tradeName: u.name,
|
|
3049
|
+
legalName: u.legal_name ?? u.name,
|
|
3050
|
+
cnpj: u.cnpj,
|
|
3051
|
+
street: u.address_street,
|
|
3052
|
+
number: u.address_number,
|
|
3053
|
+
complement: u.address_complement,
|
|
3054
|
+
district: u.address_district,
|
|
3055
|
+
city: u.city,
|
|
3056
|
+
state: u.state,
|
|
3057
|
+
postalCode: u.postal_code,
|
|
3058
|
+
phone: u.phone,
|
|
3059
|
+
mobile: u.mobile_1,
|
|
3060
|
+
email: u.email
|
|
3061
|
+
},
|
|
3062
|
+
professional: { name: pro.data?.name }
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
async function serviceTexts(ids) {
|
|
3066
|
+
if (!ids.length) return /* @__PURE__ */ new Map();
|
|
3067
|
+
const { data } = await db().from("products").select("id, metadata").in("id", ids);
|
|
3068
|
+
return new Map((data ?? []).map((r) => [r.id, r.metadata?.beauty?.contract_text ?? ""]));
|
|
3069
|
+
}
|
|
3070
|
+
async function listAttendanceContracts(bookingId) {
|
|
3071
|
+
const [templates, termId, rows] = await Promise.all([
|
|
3072
|
+
loadTemplates(),
|
|
3073
|
+
sessionTermTemplateId(),
|
|
3074
|
+
db().from("documents").select("id, title, status, metadata, created_at").eq("tenant_id", tenantId()).eq("kind", "contract").eq("subject_type", "appointment").eq("subject_id", bookingId).eq("is_active", true).order("created_at")
|
|
3075
|
+
]);
|
|
3076
|
+
if (rows.error) throw new Error(rows.error.message);
|
|
3077
|
+
return (rows.data ?? []).map((r) => mapContract(r, templates, termId));
|
|
3078
|
+
}
|
|
3079
|
+
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
3080
|
+
function ensureAttendanceContracts(input) {
|
|
3081
|
+
const prev = queue.get(input.bookingId) ?? Promise.resolve([]);
|
|
3082
|
+
const next = prev.catch(() => []).then(() => generate(input));
|
|
3083
|
+
queue.set(input.bookingId, next);
|
|
3084
|
+
next.finally(() => {
|
|
3085
|
+
if (queue.get(input.bookingId) === next) queue.delete(input.bookingId);
|
|
3086
|
+
}).catch(() => {
|
|
3087
|
+
});
|
|
3088
|
+
return next;
|
|
3089
|
+
}
|
|
3090
|
+
var queue = /* @__PURE__ */ new Map();
|
|
3091
|
+
async function generate(input) {
|
|
3092
|
+
const [templates, termId, existing] = await Promise.all([
|
|
3093
|
+
loadTemplates(),
|
|
3094
|
+
sessionTermTemplateId(),
|
|
3095
|
+
listAttendanceContracts(input.bookingId)
|
|
3096
|
+
]);
|
|
3097
|
+
const active = [...templates.values()].filter((t) => t.is_active);
|
|
3098
|
+
const wanted = [];
|
|
3099
|
+
if (input.contracted.length) {
|
|
3100
|
+
for (const t of active) {
|
|
3101
|
+
if (t.id === termId) continue;
|
|
3102
|
+
const allBilled = t.generate_on_quote_approval || t.generate_on_execution && t.execution_scope === "all_billed_services";
|
|
3103
|
+
const linked = t.generate_on_execution && (t.execution_scope ?? "linked_services") === "linked_services" ? input.contracted.filter((s) => t.metadata?.service_ids?.includes(s.serviceId)) : [];
|
|
3104
|
+
const services = allBilled ? input.contracted : linked;
|
|
3105
|
+
if (!services.length) continue;
|
|
3106
|
+
if (t.group_services || allBilled) wanted.push({ template: t, services });
|
|
3107
|
+
else for (const s of services) wanted.push({ template: t, services: [s] });
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
const term = termId ? templates.get(termId) : void 0;
|
|
3111
|
+
if (term?.is_active && input.sessionServices.length) wanted.push({ template: term, services: input.sessionServices });
|
|
3112
|
+
if (!wanted.length) return existing;
|
|
3113
|
+
const ctx = await loadContext(input);
|
|
3114
|
+
const texts = await serviceTexts([...new Set(wanted.flatMap((w) => w.services.map((s) => s.serviceId)))]);
|
|
3115
|
+
const c = db();
|
|
3116
|
+
for (const w of wanted) {
|
|
3117
|
+
const ids = [...new Set(w.services.map((s) => s.serviceId))].sort();
|
|
3118
|
+
const mine = existing.filter((e) => e.templateId === w.template.id);
|
|
3119
|
+
const perService = !w.template.group_services && w.template.id !== termId && !w.template.generate_on_quote_approval;
|
|
3120
|
+
const scope = perService ? mine.filter((e) => sameSet(e.serviceIds, ids)) : mine;
|
|
3121
|
+
if (scope.some((e) => e.status === "signed" && ids.every((id) => e.serviceIds.includes(id)))) continue;
|
|
3122
|
+
const html = renderContractTags(w.template.content ?? "", {
|
|
3123
|
+
...ctx,
|
|
3124
|
+
services: w.services.map((s) => ({ name: s.name, price: s.unitPrice * s.quantity, contractText: texts.get(s.serviceId) || void 0 })),
|
|
3125
|
+
approvedItems: input.contracted.map(toLine),
|
|
3126
|
+
recommendedItems: input.indicated.map(toLine)
|
|
3127
|
+
});
|
|
3128
|
+
const meta = { contract: { templateId: w.template.id, html, serviceIds: ids } };
|
|
3129
|
+
const pending = scope.find((e) => e.status === "pending");
|
|
3130
|
+
if (pending) {
|
|
3131
|
+
if (sameSet(pending.serviceIds, ids)) continue;
|
|
3132
|
+
const { error: error2 } = await c.from("documents").update({ metadata: meta, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", pending.id).eq("status", "pending");
|
|
3133
|
+
if (error2) throw new Error(error2.message);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
const { error } = await c.from("documents").insert({
|
|
3137
|
+
tenant_id: tenantId(),
|
|
3138
|
+
kind: "contract",
|
|
3139
|
+
person_id: input.personId,
|
|
3140
|
+
unit_id: input.unitId,
|
|
3141
|
+
title: w.template.name,
|
|
3142
|
+
status: "pending",
|
|
3143
|
+
subject_type: "appointment",
|
|
3144
|
+
subject_id: input.bookingId,
|
|
3145
|
+
metadata: meta
|
|
3146
|
+
});
|
|
3147
|
+
if (error) throw new Error(error.message);
|
|
3148
|
+
}
|
|
3149
|
+
return listAttendanceContracts(input.bookingId);
|
|
3150
|
+
}
|
|
3151
|
+
async function signAttendanceContract(contract, input) {
|
|
3152
|
+
const c = db();
|
|
3153
|
+
const up = async (blob, name) => {
|
|
3154
|
+
const path = `${tenantId()}/contracts/${contract.id}/${Date.now()}-${name}`;
|
|
3155
|
+
const { error: error2 } = await c.storage.from(BUCKET).upload(path, blob, { contentType: blob.type || void 0 });
|
|
3156
|
+
if (error2) throw new Error(error2.message);
|
|
3157
|
+
return path;
|
|
3158
|
+
};
|
|
3159
|
+
const signaturePath = input.signature ? await up(input.signature, "assinatura.png") : null;
|
|
3160
|
+
const selfiePath = input.selfie ? await up(input.selfie, "selfie.jpg") : null;
|
|
3161
|
+
const { data: row, error: readErr } = await c.from("documents").select("metadata").eq("id", contract.id).maybeSingle();
|
|
3162
|
+
if (readErr) throw new Error(readErr.message);
|
|
3163
|
+
const metadata = {
|
|
3164
|
+
...row?.metadata ?? {},
|
|
3165
|
+
contract: {
|
|
3166
|
+
...row?.metadata?.contract ?? {},
|
|
3167
|
+
signedByName: input.signerName.trim() || null,
|
|
3168
|
+
signedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3169
|
+
signaturePath,
|
|
3170
|
+
selfiePath
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
const { data, error } = await c.from("documents").update({ status: "signed", metadata, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", contract.id).eq("status", "pending").select("id");
|
|
3174
|
+
if (error) throw new Error(error.message);
|
|
3175
|
+
if (!data?.length) throw new Error("Este contrato j\xE1 foi assinado.");
|
|
3176
|
+
}
|
|
3177
|
+
async function contractMediaUrl(path) {
|
|
3178
|
+
if (!path) return null;
|
|
3179
|
+
const { data } = await db().storage.from(BUCKET).createSignedUrl(path, 3600);
|
|
3180
|
+
return data?.signedUrl ?? null;
|
|
3181
|
+
}
|
|
3182
|
+
var CONTRACT_TEXT_CLASS = [
|
|
3183
|
+
"space-y-2 text-sm leading-relaxed",
|
|
3184
|
+
"[&_b]:font-semibold [&_strong]:font-semibold [&_h1]:text-base [&_h1]:font-semibold [&_h2]:font-semibold [&_h3]:font-semibold",
|
|
3185
|
+
"[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5",
|
|
3186
|
+
"[&_table]:my-2 [&_table]:w-full [&_table]:text-xs [&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1",
|
|
3187
|
+
"[&_th]:border [&_th]:border-border [&_th]:bg-muted/50 [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:font-medium"
|
|
3188
|
+
].join(" ");
|
|
3189
|
+
function useSigned(path) {
|
|
3190
|
+
const [url, setUrl] = React12.useState(null);
|
|
3191
|
+
React12.useEffect(() => {
|
|
3192
|
+
let alive = true;
|
|
3193
|
+
contractMediaUrl(path).then((u) => {
|
|
3194
|
+
if (alive) setUrl(u);
|
|
3195
|
+
}).catch(() => {
|
|
3196
|
+
});
|
|
3197
|
+
return () => {
|
|
3198
|
+
alive = false;
|
|
3199
|
+
};
|
|
3200
|
+
}, [path]);
|
|
3201
|
+
return url;
|
|
3202
|
+
}
|
|
3203
|
+
function SignaturePad({ onChange }) {
|
|
3204
|
+
const ref = React12.useRef(null);
|
|
3205
|
+
const drawing = React12.useRef(false);
|
|
3206
|
+
React12.useEffect(() => {
|
|
3207
|
+
const c = ref.current;
|
|
3208
|
+
if (!c) return;
|
|
3209
|
+
const r = window.devicePixelRatio || 1;
|
|
3210
|
+
c.width = c.offsetWidth * r;
|
|
3211
|
+
c.height = c.offsetHeight * r;
|
|
3212
|
+
const ctx = c.getContext("2d");
|
|
3213
|
+
ctx.scale(r, r);
|
|
3214
|
+
ctx.lineWidth = 2;
|
|
3215
|
+
ctx.lineCap = "round";
|
|
3216
|
+
ctx.strokeStyle = "#111827";
|
|
3217
|
+
}, []);
|
|
3218
|
+
const pos = (e) => {
|
|
3219
|
+
const b = e.currentTarget.getBoundingClientRect();
|
|
3220
|
+
return [e.clientX - b.left, e.clientY - b.top];
|
|
3221
|
+
};
|
|
3222
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
3223
|
+
/* @__PURE__ */ jsx(
|
|
3224
|
+
"canvas",
|
|
3225
|
+
{
|
|
3226
|
+
ref,
|
|
3227
|
+
"aria-label": "\xC1rea de assinatura",
|
|
3228
|
+
className: "h-36 w-full touch-none rounded-md border bg-white",
|
|
3229
|
+
onPointerDown: (e) => {
|
|
3230
|
+
drawing.current = true;
|
|
3231
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
3232
|
+
const ctx = e.currentTarget.getContext("2d");
|
|
3233
|
+
const [x, y] = pos(e);
|
|
3234
|
+
ctx.beginPath();
|
|
3235
|
+
ctx.moveTo(x, y);
|
|
3236
|
+
},
|
|
3237
|
+
onPointerMove: (e) => {
|
|
3238
|
+
if (!drawing.current) return;
|
|
3239
|
+
const ctx = e.currentTarget.getContext("2d");
|
|
3240
|
+
const [x, y] = pos(e);
|
|
3241
|
+
ctx.lineTo(x, y);
|
|
3242
|
+
ctx.stroke();
|
|
3243
|
+
},
|
|
3244
|
+
onPointerUp: (e) => {
|
|
3245
|
+
drawing.current = false;
|
|
3246
|
+
e.currentTarget.toBlob((b) => onChange(b), "image/png");
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
),
|
|
3250
|
+
/* @__PURE__ */ jsx(
|
|
3251
|
+
"button",
|
|
3252
|
+
{
|
|
3253
|
+
type: "button",
|
|
3254
|
+
className: "text-xs text-muted-foreground hover:text-foreground",
|
|
3255
|
+
onClick: () => {
|
|
3256
|
+
const c = ref.current;
|
|
3257
|
+
if (c) {
|
|
3258
|
+
c.getContext("2d").clearRect(0, 0, c.width, c.height);
|
|
3259
|
+
onChange(null);
|
|
3260
|
+
}
|
|
3261
|
+
},
|
|
3262
|
+
children: "Limpar assinatura"
|
|
3263
|
+
}
|
|
3264
|
+
)
|
|
3265
|
+
] });
|
|
3266
|
+
}
|
|
3267
|
+
function SelfieCapture({ value, onChange }) {
|
|
3268
|
+
const video = React12.useRef(null);
|
|
3269
|
+
const stream = React12.useRef(null);
|
|
3270
|
+
const [on, setOn] = React12.useState(false);
|
|
3271
|
+
const [erro, setErro] = React12.useState(null);
|
|
3272
|
+
const preview = React12.useMemo(() => value ? URL.createObjectURL(value) : null, [value]);
|
|
3273
|
+
const stop = () => {
|
|
3274
|
+
stream.current?.getTracks().forEach((t) => t.stop());
|
|
3275
|
+
stream.current = null;
|
|
3276
|
+
setOn(false);
|
|
3277
|
+
};
|
|
3278
|
+
React12.useEffect(() => () => stop(), []);
|
|
3279
|
+
async function start() {
|
|
3280
|
+
setErro(null);
|
|
3281
|
+
try {
|
|
3282
|
+
stream.current = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" } });
|
|
3283
|
+
setOn(true);
|
|
3284
|
+
requestAnimationFrame(() => {
|
|
3285
|
+
if (video.current) {
|
|
3286
|
+
video.current.srcObject = stream.current;
|
|
3287
|
+
video.current.play().catch(() => {
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
});
|
|
3291
|
+
} catch {
|
|
3292
|
+
setErro("N\xE3o foi poss\xEDvel acessar a c\xE2mera.");
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
function capture() {
|
|
3296
|
+
const v = video.current;
|
|
3297
|
+
if (!v?.videoWidth) return;
|
|
3298
|
+
const c = document.createElement("canvas");
|
|
3299
|
+
c.width = v.videoWidth;
|
|
3300
|
+
c.height = v.videoHeight;
|
|
3301
|
+
c.getContext("2d").drawImage(v, 0, 0);
|
|
3302
|
+
c.toBlob((b) => {
|
|
3303
|
+
onChange(b);
|
|
3304
|
+
stop();
|
|
3305
|
+
}, "image/jpeg", 0.85);
|
|
3306
|
+
}
|
|
3307
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
3308
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Selfie" }),
|
|
3309
|
+
preview ? /* @__PURE__ */ jsxs("div", { className: "relative w-40", children: [
|
|
3310
|
+
/* @__PURE__ */ jsx("img", { src: preview, alt: "Selfie", className: "rounded-lg border" }),
|
|
3311
|
+
/* @__PURE__ */ jsx(
|
|
3312
|
+
"button",
|
|
3313
|
+
{
|
|
3314
|
+
type: "button",
|
|
3315
|
+
"aria-label": "Remover selfie",
|
|
3316
|
+
onClick: () => onChange(null),
|
|
3317
|
+
className: "absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-background/90",
|
|
3318
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
3319
|
+
}
|
|
3320
|
+
)
|
|
3321
|
+
] }) : on ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
3322
|
+
/* @__PURE__ */ jsx("video", { ref: video, playsInline: true, muted: true, className: "w-full max-w-[240px] rounded-lg border" }),
|
|
3323
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
3324
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: capture, children: [
|
|
3325
|
+
/* @__PURE__ */ jsx(Camera, { className: "mr-1 h-4 w-4" }),
|
|
3326
|
+
" Capturar"
|
|
3327
|
+
] }),
|
|
3328
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: stop, children: "Cancelar" })
|
|
3329
|
+
] })
|
|
3330
|
+
] }) : /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: start, children: [
|
|
3331
|
+
/* @__PURE__ */ jsx(Camera, { className: "mr-1 h-4 w-4" }),
|
|
3332
|
+
" Tirar selfie"
|
|
3333
|
+
] }),
|
|
3334
|
+
erro && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: erro })
|
|
3335
|
+
] });
|
|
3336
|
+
}
|
|
3337
|
+
function printContract(contract, signatureUrl) {
|
|
3338
|
+
const w = window.open("", "_blank");
|
|
3339
|
+
if (!w) return;
|
|
3340
|
+
const signed = contract.status === "signed" ? `<div style="margin-top:32px">${signatureUrl ? `<img src="${signatureUrl}" style="max-height:120px"/>` : ""}
|
|
3341
|
+
<p>Assinado ${contract.signedByName ? `por ${contract.signedByName.replace(/</g, "<")} ` : ""}em ${contract.signedAt ? new Date(contract.signedAt).toLocaleString("pt-BR") : ""}</p></div>` : "";
|
|
3342
|
+
w.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${contract.templateName.replace(/</g, "<")}</title>
|
|
3343
|
+
<style>body{font:14px/1.5 system-ui,sans-serif;max-width:760px;margin:32px auto;padding:0 16px;color:#111}
|
|
3344
|
+
table{border-collapse:collapse;width:100%;margin:8px 0}td,th{border:1px solid #ccc;padding:4px 6px;text-align:left}</style>
|
|
3345
|
+
</head><body>${sanitizeHtml(contract.html)}${signed}</body></html>`);
|
|
3346
|
+
w.document.close();
|
|
3347
|
+
w.focus();
|
|
3348
|
+
setTimeout(() => w.print(), 300);
|
|
3349
|
+
}
|
|
3350
|
+
function ContractSignPane({ contract, defaultSignerName, onSigned, onClose }) {
|
|
3351
|
+
const [signer, setSigner] = React12.useState(defaultSignerName ?? "");
|
|
3352
|
+
const [signature, setSignature] = React12.useState(null);
|
|
3353
|
+
const [selfie, setSelfie] = React12.useState(null);
|
|
3354
|
+
const [saving, setSaving] = React12.useState(false);
|
|
3355
|
+
const [erro, setErro] = React12.useState(null);
|
|
3356
|
+
const signatureUrl = useSigned(contract.signaturePath);
|
|
3357
|
+
const selfieUrl = useSigned(contract.selfiePath);
|
|
3358
|
+
const signed = contract.status === "signed";
|
|
3359
|
+
const html = React12.useMemo(() => sanitizeHtml(contract.html), [contract.html]);
|
|
3360
|
+
React12.useEffect(() => {
|
|
3361
|
+
setErro(null);
|
|
3362
|
+
}, [signature, selfie, signer]);
|
|
3363
|
+
const missing = contract.requireSignature && !signature ? "Desenhe a assinatura para confirmar." : contract.requireSelfie && !selfie ? "Tire a selfie para confirmar." : !signer.trim() ? "Informe o nome de quem assina." : null;
|
|
3364
|
+
async function sign() {
|
|
3365
|
+
if (missing) {
|
|
3366
|
+
setErro(missing);
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
setSaving(true);
|
|
3370
|
+
setErro(null);
|
|
3371
|
+
try {
|
|
3372
|
+
await signAttendanceContract(contract, { signerName: signer, signature, selfie });
|
|
3373
|
+
onSigned();
|
|
3374
|
+
} catch (e) {
|
|
3375
|
+
setErro(e instanceof Error ? e.message : "N\xE3o foi poss\xEDvel assinar o contrato.");
|
|
3376
|
+
} finally {
|
|
3377
|
+
setSaving(false);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3381
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
|
|
3382
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5", onClick: () => printContract(contract, signatureUrl), children: [
|
|
3383
|
+
/* @__PURE__ */ jsx(Printer, { className: "h-3.5 w-3.5" }),
|
|
3384
|
+
" Imprimir"
|
|
3385
|
+
] }) }),
|
|
3386
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-[55vh] overflow-y-auto rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx("div", { className: CONTRACT_TEXT_CLASS, dangerouslySetInnerHTML: { __html: html } }) }),
|
|
3387
|
+
signed ? /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
3388
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 rounded-lg border border-success/40 bg-success-soft px-3 py-2 text-sm text-success-soft-foreground", children: [
|
|
3389
|
+
/* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
|
|
3390
|
+
"Assinado em ",
|
|
3391
|
+
contract.signedAt ? new Date(contract.signedAt).toLocaleString("pt-BR") : "",
|
|
3392
|
+
contract.signedByName ? ` por ${contract.signedByName}` : ""
|
|
3393
|
+
] }),
|
|
3394
|
+
signatureUrl && /* @__PURE__ */ jsx("img", { src: signatureUrl, alt: "Assinatura", className: "max-h-32 rounded-md border bg-white p-2" }),
|
|
3395
|
+
selfieUrl && /* @__PURE__ */ jsx("img", { src: selfieUrl, alt: "Selfie", className: "max-h-40 rounded-lg border" })
|
|
3396
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
3397
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
3398
|
+
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium", htmlFor: "contract-signer", children: "Nome do Assinante" }),
|
|
3399
|
+
/* @__PURE__ */ jsx(Input, { id: "contract-signer", value: signer, onChange: (e) => setSigner(e.target.value), placeholder: "Nome completo" })
|
|
3400
|
+
] }),
|
|
3401
|
+
contract.requireSignature && /* @__PURE__ */ jsx(SignaturePad, { onChange: setSignature }),
|
|
3402
|
+
contract.requireSelfie && /* @__PURE__ */ jsx(SelfieCapture, { value: selfie, onChange: setSelfie })
|
|
3403
|
+
] }),
|
|
3404
|
+
erro && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: erro })
|
|
3405
|
+
] }),
|
|
3406
|
+
/* @__PURE__ */ jsx("div", { className: "shrink-0 border-t fayz-glass-divider px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
|
|
3407
|
+
/* @__PURE__ */ jsx(
|
|
3408
|
+
"button",
|
|
3409
|
+
{
|
|
3410
|
+
type: "button",
|
|
3411
|
+
onClick: onClose,
|
|
3412
|
+
className: "rounded-button px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground",
|
|
3413
|
+
children: signed ? "Fechar" : "Agora n\xE3o"
|
|
3414
|
+
}
|
|
3415
|
+
),
|
|
3416
|
+
!signed && /* @__PURE__ */ jsx(
|
|
3417
|
+
"button",
|
|
3418
|
+
{
|
|
3419
|
+
type: "button",
|
|
3420
|
+
onClick: sign,
|
|
3421
|
+
disabled: saving,
|
|
3422
|
+
className: "rounded-button bg-foreground px-3 py-1.5 text-xs font-semibold text-background disabled:opacity-40",
|
|
3423
|
+
children: saving ? "Assinando\u2026" : "Confirmar Assinatura"
|
|
3424
|
+
}
|
|
3425
|
+
)
|
|
3426
|
+
] }) })
|
|
3427
|
+
] });
|
|
3428
|
+
}
|
|
3429
|
+
|
|
2604
3430
|
// src/index.ts
|
|
2605
3431
|
function registerBuiltInProviders(formsProvider) {
|
|
2606
3432
|
registerDocumentTypeProvider({
|
|
@@ -2757,14 +3583,14 @@ function createCustomFormsPlugin(options) {
|
|
|
2757
3583
|
}
|
|
2758
3584
|
];
|
|
2759
3585
|
const registries = [...formRegistries, ...options?.settingsRegistries ?? []];
|
|
2760
|
-
const SettingsComponent = () =>
|
|
3586
|
+
const SettingsComponent = () => React12.createElement(PluginSettingsPanel, {
|
|
2761
3587
|
title: config.labels.settingsLabel,
|
|
2762
3588
|
customTabs: [
|
|
2763
3589
|
{
|
|
2764
3590
|
id: "forms",
|
|
2765
3591
|
label: config.labels.pageTitle,
|
|
2766
3592
|
icon: "FileText",
|
|
2767
|
-
content:
|
|
3593
|
+
content: React12.createElement(CustomFormsSettingsTab, { config, provider, store })
|
|
2768
3594
|
}
|
|
2769
3595
|
],
|
|
2770
3596
|
registries,
|
|
@@ -2772,10 +3598,12 @@ function createCustomFormsPlugin(options) {
|
|
|
2772
3598
|
hostPluginId: "custom_forms"
|
|
2773
3599
|
});
|
|
2774
3600
|
SettingsComponent.displayName = "CustomFormsSettingsPanel";
|
|
2775
|
-
const PersonDocumentsWidget2 = (props) =>
|
|
3601
|
+
const PersonDocumentsWidget2 = (props) => React12.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "records" });
|
|
2776
3602
|
PersonDocumentsWidget2.displayName = "PersonDocumentsWidget";
|
|
2777
|
-
const PersonFilesWidget = (props) =>
|
|
3603
|
+
const PersonFilesWidget = (props) => React12.createElement(PersonDocumentsWidget, { ...props, config, provider, store, surface: "files" });
|
|
2778
3604
|
PersonFilesWidget.displayName = "PersonFilesWidget";
|
|
3605
|
+
const PersonAnswerAlertsWidget = (props) => React12.createElement(PersonAnswerAlerts, { personId: props.item?.id, provider });
|
|
3606
|
+
PersonAnswerAlertsWidget.displayName = "PersonAnswerAlertsWidget";
|
|
2779
3607
|
return {
|
|
2780
3608
|
id: "custom_forms",
|
|
2781
3609
|
defaultAgentRole: "setup",
|
|
@@ -2820,6 +3648,13 @@ function createCustomFormsPlugin(options) {
|
|
|
2820
3648
|
zone: "person.detail.files",
|
|
2821
3649
|
component: PersonFilesWidget,
|
|
2822
3650
|
order: 0
|
|
3651
|
+
},
|
|
3652
|
+
{
|
|
3653
|
+
// Renders nothing unless a form marks an answer as an alert (V1 alert_config).
|
|
3654
|
+
id: "person-answer-alerts",
|
|
3655
|
+
zone: "person.detail.above-tabs",
|
|
3656
|
+
component: PersonAnswerAlertsWidget,
|
|
3657
|
+
order: -1
|
|
2823
3658
|
}
|
|
2824
3659
|
],
|
|
2825
3660
|
registries,
|
|
@@ -2886,6 +3721,6 @@ function createCustomFormsPlugin(options) {
|
|
|
2886
3721
|
};
|
|
2887
3722
|
}
|
|
2888
3723
|
|
|
2889
|
-
export { FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, getPersonFormRequirements, isOutstanding, registerDocumentTypeProvider };
|
|
3724
|
+
export { ContractSignPane, FormFillDialog, PersonFormsAlert, createCustomFormsPlugin, ensureAttendanceContracts, getPersonFormRequirements, isOutstanding, listAttendanceContracts, recommendationsOf, registerDocumentTypeProvider };
|
|
2890
3725
|
//# sourceMappingURL=index.js.map
|
|
2891
3726
|
//# sourceMappingURL=index.js.map
|