@fayz-ai/plugin-agenda 0.22.0 → 0.23.2
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/components/AttendanceRuler.d.ts.map +1 -1
- package/dist/components/AvailabilityHeaderControl.d.ts +6 -1
- package/dist/components/AvailabilityHeaderControl.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/data/turn-queue-types.d.ts +140 -0
- package/dist/data/turn-queue-types.d.ts.map +1 -1
- package/dist/data/turn-queue.d.ts.map +1 -1
- package/dist/index.js +2914 -311
- package/dist/index.js.map +1 -1
- package/dist/lib/turn-queue-plugin.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/dist/views/TurnQueuePage.d.ts +2 -4
- package/dist/views/TurnQueuePage.d.ts.map +1 -1
- package/dist/views/UnitTurnQueueWidget.d.ts.map +1 -1
- package/dist/views/turn-queue/TurnQueueConfigDialog.d.ts +9 -0
- package/dist/views/turn-queue/TurnQueueConfigDialog.d.ts.map +1 -0
- package/dist/views/turn-queue/TurnQueueReportPage.d.ts +6 -0
- package/dist/views/turn-queue/TurnQueueReportPage.d.ts.map +1 -0
- package/dist/views/turn-queue/turn-queue-ui.d.ts +29 -0
- package/dist/views/turn-queue/turn-queue-ui.d.ts.map +1 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createMockAgendaProvider, createSupabasePublicBookingProvider } from './chunk-A77PUFY5.js';
|
|
2
2
|
export { createMockAgendaProvider } from './chunk-A77PUFY5.js';
|
|
3
3
|
import React23, { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
|
|
4
|
-
import { tl, recordRailFields, RECORD_GROUP, activeFacet, connectionStatus, createConnectionStore, getActiveTenantId, createSafeDataProvider, todayLocal, getSupabaseClientOptional, registerTranslations, registerMessageDefaults,
|
|
5
|
-
import { createPluginContext, fayzConnectorOAuth, UNIT_DETAIL_ZONES, setScheduleBlockConfig, getScheduleBlockConfig, registerPersonActions, PluginSettingsPanel, adminNavigateTo, createArchetypeLookup, dedup, usePluginPrefs, PersonLink, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow,
|
|
4
|
+
import { tl, recordRailFields, RECORD_GROUP, activeFacet, connectionStatus, createConnectionStore, getActiveTenantId, createSafeDataProvider, todayLocal, getSupabaseClientOptional, registerTranslations, registerMessageDefaults, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER, getActiveUnitId, assertWrote, emitDataChanged, globalCache, usePluginRuntimeOptional, useTranslation, useActiveUnitId, emitDomainEvent, getCurrentLocale, localDateKey, useDataChanged, describeError, formatCurrency, resolveDataProvider } from '@fayz-ai/core';
|
|
5
|
+
import { createPluginContext, fayzConnectorOAuth, UNIT_DETAIL_ZONES, setScheduleBlockConfig, getScheduleBlockConfig, registerPersonActions, PluginSettingsPanel, adminNavigateTo, createArchetypeLookup, dedup, usePluginPrefs, PersonLink, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, usePermissionOptional, PageActions, useRightRailStore, useFieldRules, deriveEntityKey, openEntity, ScheduleEditor, useLimitGuard, useLookupCreate, CrudFormPage, CrudDetailPage, ContactPicker, PaymentStatusBadge as PaymentStatusBadge$1, RightRailPage, invalidateLimit } from '@fayz-ai/admin';
|
|
6
6
|
import FullCalendar from '@fullcalendar/react';
|
|
7
7
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
8
8
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
@@ -10,8 +10,8 @@ import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
|
|
|
10
10
|
import interactionPlugin from '@fullcalendar/interaction';
|
|
11
11
|
import listPlugin from '@fullcalendar/list';
|
|
12
12
|
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
|
|
13
|
-
import { CheckCircle, Stethoscope, RotateCcw, Check, CircleDashed, Layers, ArrowRight, Play,
|
|
14
|
-
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Skeleton, Switch, Input, Button, toast, FilterRail, AdaptiveFilter, FilterCheckRow, TooltipProvider, ModulePage,
|
|
13
|
+
import { CheckCircle, Stethoscope, RotateCcw, Check, CircleDashed, Layers, ArrowRight, Play, Radio, Monitor, BarChart3, SlidersHorizontal, Download, Loader2, LogIn, Coffee, Undo2, CheckCircle2, LogOut, AlertCircle, RefreshCw, CalendarPlus, X, Ban, Clock3, Plus, Settings, ChevronLeft, ChevronRight, ChevronDown, CircleDollarSign, CalendarDays, UserRound, Package, Clock, Mic, BellRing, UserPlus, History, ArrowUp, ArrowDown, Trash2, Sparkles, CalendarX, UserX, Search, CalendarOff, Pencil, User, MapPin, FileText, AlertTriangle, DollarSign, HandCoins, CheckSquare, Calendar, ExternalLink, Users, DoorOpen, Briefcase, CircleDot, MessageCircle, Phone, AlarmClock, ArrowLeft, Link2, Receipt, ClipboardCheck, MoreVertical, ArrowUpToLine, Info, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, PackageCheck, Maximize2 } from 'lucide-react';
|
|
14
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Skeleton, Switch, Input, Button, toast, FilterRail, AdaptiveFilter, FilterCheckRow, TooltipProvider, Badge, ModulePage, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, SubpageHeader, DatePicker, MultiSelect, Dropdown, DropdownTrigger, DropdownContent, DropdownLabel, DropdownSub, DropdownSubTrigger, DropdownSubContent, DropdownItem, DropdownSeparator, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, PersonAvatar, abbreviatePersonName, INLINE_FILTER_MAX, PersonMultiPicker, SegmentedControl, MiniCalendar, PageHeaderActions, ConfirmDialog, RailWorkspace, Tooltip, TooltipTrigger, TooltipContent, ModalTask, cn, Tabs, TabsList, TabsTrigger, TabsContent, createXlsxReport, PersonPicker, TimePicker, ModalTaskHost, SearchCombobox, ModalTaskHeader, normalizePhone, DotBadge } from '@fayz-ai/ui';
|
|
15
15
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
16
16
|
import { createPortal } from 'react-dom';
|
|
17
17
|
import { listAttendanceContracts, ensureAttendanceContracts, ContractSignPane, FormFillDialog, PersonFormsAlert } from '@fayz-ai/plugin-forms';
|
|
@@ -351,7 +351,7 @@ function resolveConfig(options) {
|
|
|
351
351
|
minAdvanceHours: options?.scheduleBlockDefaults?.minAdvanceHours ?? 0,
|
|
352
352
|
maxAdvanceDays: options?.scheduleBlockDefaults?.maxAdvanceDays ?? 60
|
|
353
353
|
},
|
|
354
|
-
defaultCalendarView: options?.defaultCalendarView ?? "
|
|
354
|
+
defaultCalendarView: options?.defaultCalendarView ?? "resourceTimeGridDay"
|
|
355
355
|
};
|
|
356
356
|
}
|
|
357
357
|
var ctx = createPluginContext("AgendaPage");
|
|
@@ -4218,7 +4218,7 @@ function bookingToEvent(booking, statusColors, visibleResourceIds = [], serviceL
|
|
|
4218
4218
|
// has no assignee. Availability treats it as busy for every professional, so
|
|
4219
4219
|
// resource views must render it in every visible lane instead of dropping it.
|
|
4220
4220
|
...booking.professionalId ? { resourceId: booking.professionalId } : visibleResourceIds.length > 0 ? { resourceIds: visibleResourceIds } : {},
|
|
4221
|
-
title: booking.clientName ?? "
|
|
4221
|
+
title: booking.clientName ?? "Sem cliente",
|
|
4222
4222
|
start: booking.startsAt,
|
|
4223
4223
|
end: booking.endsAt ?? void 0,
|
|
4224
4224
|
backgroundColor: kindColor ?? color + "30",
|
|
@@ -4424,7 +4424,7 @@ function CalendarView() {
|
|
|
4424
4424
|
const fcLocale = currentLocale === "pt-BR" ? ptBrLocale : void 0;
|
|
4425
4425
|
const calendarRef = useRef(null);
|
|
4426
4426
|
const prefs = usePluginPrefs("agenda", {
|
|
4427
|
-
calendarView: config.defaultCalendarView ?? "
|
|
4427
|
+
calendarView: config.defaultCalendarView ?? "resourceTimeGridDay",
|
|
4428
4428
|
// Como a SEMANA se agrupa: 'day' põe o dia em cima e as pessoas dentro
|
|
4429
4429
|
// dele; 'professional' inverte. 'auto' é o padrão e deixa a agenda decidir
|
|
4430
4430
|
// pelo tamanho da equipe — ver `weekGrouping` mais abaixo.
|
|
@@ -5366,6 +5366,7 @@ function CalendarView() {
|
|
|
5366
5366
|
] });
|
|
5367
5367
|
}, [nextProfessionalId]);
|
|
5368
5368
|
const eventContent = useCallback((arg) => {
|
|
5369
|
+
if (arg.event.display === "background") return null;
|
|
5369
5370
|
if (arg.event.extendedProps?.isLocationLabel) {
|
|
5370
5371
|
return /* @__PURE__ */ jsx("div", { className: "px-1.5 py-0.5 overflow-hidden", children: /* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium truncate", children: arg.event.title }) });
|
|
5371
5372
|
}
|
|
@@ -9290,7 +9291,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
9290
9291
|
setErro(null);
|
|
9291
9292
|
}
|
|
9292
9293
|
} catch (e) {
|
|
9293
|
-
if (vivo) setErro(e
|
|
9294
|
+
if (vivo) setErro(describeError(e, "N\xE3o foi poss\xEDvel ler a r\xE9gua."));
|
|
9294
9295
|
}
|
|
9295
9296
|
})();
|
|
9296
9297
|
return () => {
|
|
@@ -15805,6 +15806,1769 @@ END $function$;
|
|
|
15805
15806
|
REVOKE ALL ON FUNCTION public.agenda_turn_queue_configure(uuid, boolean, boolean, integer) FROM PUBLIC, anon;
|
|
15806
15807
|
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_configure(uuid, boolean, boolean, integer) TO authenticated, service_role;
|
|
15807
15808
|
`;
|
|
15809
|
+
var MIGRATION_021_A_VEZ_POR_AREA = `-- A lista da vez do V1 (BeautySoft, 20261114100000 + 20261114110000): uma fila
|
|
15810
|
+
-- por \xC1REA dentro da unidade, aus\xEAncia curta que guarda o lugar, a recep\xE7\xE3o
|
|
15811
|
+
-- conduzindo a fila (chamar, come\xE7ou, terminou, mover, tirar), servi\xE7o r\xE1pido
|
|
15812
|
+
-- que n\xE3o tira da vez e a fila recome\xE7ando todo dia.
|
|
15813
|
+
--
|
|
15814
|
+
-- \xC1reas, \xE1rea fixada e servi\xE7os r\xE1pidos s\xE3o da REDE (como no V1); o resto \xE9 da
|
|
15815
|
+
-- unidade e mora na linha de plg_agenda_turn_queues desde 016/020.
|
|
15816
|
+
-- Aditivo e idempotente: nada some, 016/017/020 continuam respondendo.
|
|
15817
|
+
|
|
15818
|
+
-- 1) Configura\xE7\xE3o da rede -------------------------------------------------------
|
|
15819
|
+
CREATE TABLE IF NOT EXISTS public.plg_agenda_turn_areas (
|
|
15820
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
15821
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
15822
|
+
key text NOT NULL CHECK (key ~ '^[a-z0-9_]{1,30}$'),
|
|
15823
|
+
name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 40),
|
|
15824
|
+
color text NOT NULL DEFAULT 'slate'
|
|
15825
|
+
CHECK (color IN ('amber', 'green', 'blue', 'pink', 'purple', 'orange', 'teal', 'slate')),
|
|
15826
|
+
keywords text[] NOT NULL DEFAULT '{}',
|
|
15827
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
15828
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
15829
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
15830
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
15831
|
+
UNIQUE (tenant_id, key)
|
|
15832
|
+
);
|
|
15833
|
+
|
|
15834
|
+
CREATE TABLE IF NOT EXISTS public.plg_agenda_turn_professional_areas (
|
|
15835
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
15836
|
+
professional_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
|
|
15837
|
+
area_key text NOT NULL,
|
|
15838
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
15839
|
+
PRIMARY KEY (tenant_id, professional_id)
|
|
15840
|
+
);
|
|
15841
|
+
|
|
15842
|
+
CREATE TABLE IF NOT EXISTS public.plg_agenda_turn_quick_services (
|
|
15843
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
15844
|
+
product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
|
|
15845
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
15846
|
+
PRIMARY KEY (tenant_id, product_id)
|
|
15847
|
+
);
|
|
15848
|
+
|
|
15849
|
+
ALTER TABLE public.plg_agenda_turn_areas ENABLE ROW LEVEL SECURITY;
|
|
15850
|
+
ALTER TABLE public.plg_agenda_turn_areas FORCE ROW LEVEL SECURITY;
|
|
15851
|
+
ALTER TABLE public.plg_agenda_turn_professional_areas ENABLE ROW LEVEL SECURITY;
|
|
15852
|
+
ALTER TABLE public.plg_agenda_turn_professional_areas FORCE ROW LEVEL SECURITY;
|
|
15853
|
+
ALTER TABLE public.plg_agenda_turn_quick_services ENABLE ROW LEVEL SECURITY;
|
|
15854
|
+
ALTER TABLE public.plg_agenda_turn_quick_services FORCE ROW LEVEL SECURITY;
|
|
15855
|
+
|
|
15856
|
+
DO $$ BEGIN
|
|
15857
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_areas' AND policyname = 'plg_agenda_turn_areas_read') THEN
|
|
15858
|
+
CREATE POLICY plg_agenda_turn_areas_read ON public.plg_agenda_turn_areas
|
|
15859
|
+
FOR SELECT TO authenticated USING (tenant_id = app.current_tenant_id());
|
|
15860
|
+
END IF;
|
|
15861
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_professional_areas' AND policyname = 'plg_agenda_turn_professional_areas_read') THEN
|
|
15862
|
+
CREATE POLICY plg_agenda_turn_professional_areas_read ON public.plg_agenda_turn_professional_areas
|
|
15863
|
+
FOR SELECT TO authenticated USING (tenant_id = app.current_tenant_id());
|
|
15864
|
+
END IF;
|
|
15865
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_quick_services' AND policyname = 'plg_agenda_turn_quick_services_read') THEN
|
|
15866
|
+
CREATE POLICY plg_agenda_turn_quick_services_read ON public.plg_agenda_turn_quick_services
|
|
15867
|
+
FOR SELECT TO authenticated USING (tenant_id = app.current_tenant_id());
|
|
15868
|
+
END IF;
|
|
15869
|
+
END $$;
|
|
15870
|
+
|
|
15871
|
+
REVOKE ALL ON public.plg_agenda_turn_areas, public.plg_agenda_turn_professional_areas, public.plg_agenda_turn_quick_services FROM PUBLIC, anon, authenticated;
|
|
15872
|
+
GRANT SELECT ON public.plg_agenda_turn_areas, public.plg_agenda_turn_professional_areas, public.plg_agenda_turn_quick_services TO authenticated;
|
|
15873
|
+
|
|
15874
|
+
-- 2) Unidade, entradas e trilha -------------------------------------------------
|
|
15875
|
+
ALTER TABLE public.plg_agenda_turn_queues
|
|
15876
|
+
ADD COLUMN IF NOT EXISTS follows_appointments boolean NOT NULL DEFAULT true,
|
|
15877
|
+
ADD COLUMN IF NOT EXISTS self_checkin boolean NOT NULL DEFAULT true;
|
|
15878
|
+
|
|
15879
|
+
ALTER TABLE public.plg_agenda_turn_entries
|
|
15880
|
+
ADD COLUMN IF NOT EXISTS area_key text,
|
|
15881
|
+
ADD COLUMN IF NOT EXISTS away_reason text,
|
|
15882
|
+
ADD COLUMN IF NOT EXISTS joined_at timestamptz;
|
|
15883
|
+
|
|
15884
|
+
ALTER TABLE public.plg_agenda_turn_events
|
|
15885
|
+
ADD COLUMN IF NOT EXISTS area_key text;
|
|
15886
|
+
|
|
15887
|
+
-- 'away' = saiu e guarda o lugar. 'unavailable' segue sendo "fora da fila".
|
|
15888
|
+
ALTER TABLE public.plg_agenda_turn_entries DROP CONSTRAINT IF EXISTS plg_agenda_turn_entries_status_check;
|
|
15889
|
+
ALTER TABLE public.plg_agenda_turn_entries ADD CONSTRAINT plg_agenda_turn_entries_status_check
|
|
15890
|
+
CHECK (status IN ('available', 'away', 'serving', 'unavailable'));
|
|
15891
|
+
|
|
15892
|
+
UPDATE public.plg_agenda_turn_entries
|
|
15893
|
+
SET joined_at = coalesce(available_at, state_changed_at)
|
|
15894
|
+
WHERE joined_at IS NULL AND status <> 'unavailable';
|
|
15895
|
+
|
|
15896
|
+
-- 3) Helpers ----------------------------------------------------------------------
|
|
15897
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__norm(p text)
|
|
15898
|
+
RETURNS text LANGUAGE sql IMMUTABLE SET search_path = '' AS $$
|
|
15899
|
+
SELECT translate(lower(coalesce(p, '')),
|
|
15900
|
+
'\xE1\xE0\xE2\xE3\xE4\xE9\xE8\xEA\xEB\xED\xEC\xEE\xEF\xF3\xF2\xF4\xF5\xF6\xFA\xF9\xFB\xFC\xE7\xF1', 'aaaaaeeeeiiiiooooouuuucn');
|
|
15901
|
+
$$;
|
|
15902
|
+
|
|
15903
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__area_defs(p_tenant uuid)
|
|
15904
|
+
RETURNS TABLE(key text, name text, color text, keywords text[], sort_order integer)
|
|
15905
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
15906
|
+
WITH cad AS (
|
|
15907
|
+
SELECT a.key, a.name, a.color, a.keywords, a.sort_order
|
|
15908
|
+
FROM public.plg_agenda_turn_areas a
|
|
15909
|
+
WHERE a.tenant_id = p_tenant AND a.is_active
|
|
15910
|
+
), padrao(key, name, color, keywords, sort_order) AS (
|
|
15911
|
+
VALUES
|
|
15912
|
+
('cabelo', 'Cabelo', 'amber', ARRAY['cabel', 'color', 'escov', 'penteado', 'barb'], 1),
|
|
15913
|
+
('unhas', 'Unhas', 'green', ARRAY['manicur', 'pedicur', 'unha'], 2),
|
|
15914
|
+
('estetica', 'Est\xE9tica', 'blue', ARRAY['estet', 'depil', 'sobrancel', 'massag', 'cili', 'maqui'], 3)
|
|
15915
|
+
)
|
|
15916
|
+
SELECT * FROM cad
|
|
15917
|
+
UNION ALL
|
|
15918
|
+
SELECT * FROM padrao WHERE NOT EXISTS (SELECT 1 FROM cad)
|
|
15919
|
+
UNION ALL
|
|
15920
|
+
SELECT 'geral', 'Outros', 'slate', ARRAY[]::text[], 99
|
|
15921
|
+
WHERE NOT EXISTS (SELECT 1 FROM cad WHERE cad.key = 'geral');
|
|
15922
|
+
$$;
|
|
15923
|
+
|
|
15924
|
+
-- The specialty arrives as a JSON array string ('["Cabelereiro","Colorista"]')
|
|
15925
|
+
-- from the V1 ETL, or as plain text typed in the V2 form.
|
|
15926
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__auto_area_of(p_tenant uuid, p_person uuid)
|
|
15927
|
+
RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
15928
|
+
DECLARE
|
|
15929
|
+
v_spec text;
|
|
15930
|
+
v_tokens text[];
|
|
15931
|
+
v_key text;
|
|
15932
|
+
BEGIN
|
|
15933
|
+
SELECT btrim(coalesce(nullif(p.metadata->'professional'->>'specialty', ''),
|
|
15934
|
+
nullif(p.metadata->'professional'->>'job_title', ''),
|
|
15935
|
+
nullif(p.custom_fields->>'profession', ''), ''))
|
|
15936
|
+
INTO v_spec FROM public.people p WHERE p.id = p_person AND p.tenant_id = p_tenant;
|
|
15937
|
+
IF coalesce(v_spec, '') = '' THEN RETURN 'geral'; END IF;
|
|
15938
|
+
BEGIN
|
|
15939
|
+
IF left(v_spec, 1) = '[' THEN
|
|
15940
|
+
SELECT array_agg(public.agenda_turn__norm(x)) INTO v_tokens FROM jsonb_array_elements_text(v_spec::jsonb) x;
|
|
15941
|
+
END IF;
|
|
15942
|
+
EXCEPTION WHEN others THEN v_tokens := NULL;
|
|
15943
|
+
END;
|
|
15944
|
+
IF v_tokens IS NULL THEN v_tokens := ARRAY[public.agenda_turn__norm(v_spec)]; END IF;
|
|
15945
|
+
|
|
15946
|
+
SELECT d.key INTO v_key FROM public.agenda_turn__area_defs(p_tenant) d
|
|
15947
|
+
WHERE EXISTS (SELECT 1 FROM unnest(d.keywords) pat, unnest(v_tokens) tok
|
|
15948
|
+
WHERE pat <> '' AND tok LIKE '%' || public.agenda_turn__norm(pat) || '%')
|
|
15949
|
+
ORDER BY d.sort_order, d.key LIMIT 1;
|
|
15950
|
+
RETURN coalesce(v_key, 'geral');
|
|
15951
|
+
END $$;
|
|
15952
|
+
|
|
15953
|
+
-- A pinned area wins over the specialty, as long as that area still exists.
|
|
15954
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__area_of(p_tenant uuid, p_person uuid)
|
|
15955
|
+
RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
15956
|
+
SELECT coalesce(
|
|
15957
|
+
(SELECT pa.area_key FROM public.plg_agenda_turn_professional_areas pa
|
|
15958
|
+
WHERE pa.tenant_id = p_tenant AND pa.professional_id = p_person
|
|
15959
|
+
AND EXISTS (SELECT 1 FROM public.agenda_turn__area_defs(p_tenant) d WHERE d.key = pa.area_key)),
|
|
15960
|
+
public.agenda_turn__auto_area_of(p_tenant, p_person));
|
|
15961
|
+
$$;
|
|
15962
|
+
|
|
15963
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__tz(p_tenant uuid, p_unit uuid)
|
|
15964
|
+
RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
15965
|
+
SELECT coalesce(
|
|
15966
|
+
(SELECT nullif(u.timezone, '') FROM public.v_units u WHERE u.id = p_unit),
|
|
15967
|
+
(SELECT nullif(t.settings->>'timezone', '') FROM public.tenants t WHERE t.id = p_tenant),
|
|
15968
|
+
'America/Sao_Paulo');
|
|
15969
|
+
$$;
|
|
15970
|
+
|
|
15971
|
+
-- Yesterday's queue does not survive the night: entries from a previous local
|
|
15972
|
+
-- day go out of the queue and leave a 'reset' in the trail.
|
|
15973
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__purge_stale(p_queue uuid, p_tz text)
|
|
15974
|
+
RETURNS void LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
15975
|
+
BEGIN
|
|
15976
|
+
WITH velhas AS (
|
|
15977
|
+
UPDATE public.plg_agenda_turn_entries e
|
|
15978
|
+
SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,
|
|
15979
|
+
state_changed_at = now(), updated_at = now()
|
|
15980
|
+
WHERE e.queue_id = p_queue AND e.status <> 'unavailable'
|
|
15981
|
+
AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE p_tz)::date
|
|
15982
|
+
< (now() AT TIME ZONE p_tz)::date
|
|
15983
|
+
RETURNING e.tenant_id, e.unit_id, e.professional_id, e.area_key, e.position
|
|
15984
|
+
)
|
|
15985
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, area_key, payload)
|
|
15986
|
+
SELECT v.tenant_id, p_queue, v.unit_id, v.professional_id, 'reset', 'automatic', v.area_key,
|
|
15987
|
+
jsonb_build_object('from_position', v.position)
|
|
15988
|
+
FROM velhas v;
|
|
15989
|
+
END $$;
|
|
15990
|
+
|
|
15991
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__renumber(p_queue uuid, p_area text)
|
|
15992
|
+
RETURNS void LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
15993
|
+
UPDATE public.plg_agenda_turn_entries e SET position = o.rn
|
|
15994
|
+
FROM (SELECT id, row_number() OVER (ORDER BY position, joined_at, id) AS rn
|
|
15995
|
+
FROM public.plg_agenda_turn_entries
|
|
15996
|
+
WHERE queue_id = p_queue AND area_key IS NOT DISTINCT FROM p_area
|
|
15997
|
+
AND status IN ('available', 'away')) o
|
|
15998
|
+
WHERE e.id = o.id AND e.position IS DISTINCT FROM o.rn;
|
|
15999
|
+
$$;
|
|
16000
|
+
|
|
16001
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__next_position(p_queue uuid, p_area text)
|
|
16002
|
+
RETURNS bigint LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16003
|
+
SELECT coalesce(max(position), 0) + 1 FROM public.plg_agenda_turn_entries
|
|
16004
|
+
WHERE queue_id = p_queue AND area_key IS NOT DISTINCT FROM p_area AND status IN ('available', 'away');
|
|
16005
|
+
$$;
|
|
16006
|
+
|
|
16007
|
+
-- Bookable people of this unit (or of the whole network), as the agenda shows them.
|
|
16008
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__in_unit(p_tenant uuid, p_unit uuid, p_person uuid)
|
|
16009
|
+
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16010
|
+
SELECT EXISTS (
|
|
16011
|
+
SELECT 1 FROM public.v_bookable_people b
|
|
16012
|
+
WHERE b.id = p_person AND b.tenant_id = p_tenant AND b.is_active AND b.is_bookable
|
|
16013
|
+
AND b.kind <> 'customer'
|
|
16014
|
+
AND (b.unit_ids IS NULL OR p_unit = ANY (b.unit_ids)));
|
|
16015
|
+
$$;
|
|
16016
|
+
|
|
16017
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__queue(p_tenant uuid, p_unit uuid)
|
|
16018
|
+
RETURNS public.plg_agenda_turn_queues LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16019
|
+
SELECT * FROM public.plg_agenda_turn_queues q
|
|
16020
|
+
WHERE q.tenant_id = p_tenant AND q.unit_id = p_unit AND q.queue_key = 'walk_in';
|
|
16021
|
+
$$;
|
|
16022
|
+
|
|
16023
|
+
-- 4) Quadro -------------------------------------------------------------------------
|
|
16024
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_board(p_unit_id uuid)
|
|
16025
|
+
RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16026
|
+
DECLARE
|
|
16027
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16028
|
+
v_queue public.plg_agenda_turn_queues;
|
|
16029
|
+
v_tz text;
|
|
16030
|
+
v_day date;
|
|
16031
|
+
v_lat numeric;
|
|
16032
|
+
v_lng numeric;
|
|
16033
|
+
v_result jsonb;
|
|
16034
|
+
BEGIN
|
|
16035
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16036
|
+
IF p_unit_id IS NULL THEN RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023'; END IF;
|
|
16037
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN
|
|
16038
|
+
RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';
|
|
16039
|
+
END IF;
|
|
16040
|
+
v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);
|
|
16041
|
+
v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);
|
|
16042
|
+
v_day := (now() AT TIME ZONE v_tz)::date;
|
|
16043
|
+
SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;
|
|
16044
|
+
|
|
16045
|
+
WITH ent AS (
|
|
16046
|
+
SELECT e.*, row_number() OVER (PARTITION BY e.area_key, (e.status IN ('available', 'away'))
|
|
16047
|
+
ORDER BY e.position, e.joined_at, e.id) AS rnk
|
|
16048
|
+
FROM public.plg_agenda_turn_entries e
|
|
16049
|
+
WHERE e.queue_id = v_queue.id AND e.status IN ('available', 'away', 'serving')
|
|
16050
|
+
AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = v_day
|
|
16051
|
+
), appts AS (
|
|
16052
|
+
SELECT a.assignee_id AS professional_id, count(*)::int AS n,
|
|
16053
|
+
min(a.starts_at) FILTER (WHERE a.starts_at >= now() AND a.status NOT IN ('completed', 'cancelled', 'no_show')) AS proximo
|
|
16054
|
+
FROM public.appointments a
|
|
16055
|
+
WHERE a.tenant_id = v_tenant AND a.assignee_id IS NOT NULL
|
|
16056
|
+
AND coalesce(a.unit_id, a.location_id) = p_unit_id
|
|
16057
|
+
AND (a.starts_at AT TIME ZONE v_tz)::date = v_day
|
|
16058
|
+
AND a.status <> 'cancelled'
|
|
16059
|
+
GROUP BY 1
|
|
16060
|
+
), roster AS (
|
|
16061
|
+
SELECT p.id, p.name, p.avatar_url, public.agenda_turn__area_of(v_tenant, p.id) AS area
|
|
16062
|
+
FROM public.people p
|
|
16063
|
+
WHERE p.tenant_id = v_tenant AND p.is_active
|
|
16064
|
+
AND (public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)
|
|
16065
|
+
OR EXISTS (SELECT 1 FROM appts t WHERE t.professional_id = p.id))
|
|
16066
|
+
), served AS (
|
|
16067
|
+
SELECT ev.professional_id,
|
|
16068
|
+
count(*) FILTER (WHERE ev.event_type IN ('started', 'advanced_to_service'))::int AS atendimentos
|
|
16069
|
+
FROM public.plg_agenda_turn_events ev
|
|
16070
|
+
WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day
|
|
16071
|
+
GROUP BY 1
|
|
16072
|
+
)
|
|
16073
|
+
SELECT jsonb_build_object(
|
|
16074
|
+
'unitId', p_unit_id,
|
|
16075
|
+
'today', v_day,
|
|
16076
|
+
'enabled', coalesce(v_queue.is_active, false),
|
|
16077
|
+
'selfCheckin', coalesce(v_queue.self_checkin, true),
|
|
16078
|
+
'auto', coalesce(v_queue.follows_appointments, true),
|
|
16079
|
+
'requireLocation', coalesce(v_queue.geofence_required, false),
|
|
16080
|
+
'locationConfigured', v_lat IS NOT NULL AND v_lng IS NOT NULL,
|
|
16081
|
+
'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color) ORDER BY d.sort_order, d.key)
|
|
16082
|
+
FROM public.agenda_turn__area_defs(v_tenant) d), '[]'::jsonb),
|
|
16083
|
+
'entries', coalesce((
|
|
16084
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
16085
|
+
'professionalId', e.professional_id, 'name', p.name, 'avatarUrl', p.avatar_url,
|
|
16086
|
+
'area', coalesce(e.area_key, 'geral'), 'status', e.status,
|
|
16087
|
+
'rank', CASE WHEN e.status IN ('available', 'away') THEN e.rnk END,
|
|
16088
|
+
'awayReason', e.away_reason, 'since', e.state_changed_at, 'joinedAt', e.joined_at,
|
|
16089
|
+
'appointmentId', e.appointment_id,
|
|
16090
|
+
'servedToday', coalesce(s.atendimentos, 0)
|
|
16091
|
+
) ORDER BY e.area_key, (e.status = 'serving'), e.rnk)
|
|
16092
|
+
FROM ent e JOIN public.people p ON p.id = e.professional_id
|
|
16093
|
+
LEFT JOIN served s ON s.professional_id = e.professional_id), '[]'::jsonb),
|
|
16094
|
+
'roster', coalesce((
|
|
16095
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
16096
|
+
'professionalId', r.id, 'name', r.name, 'avatarUrl', r.avatar_url, 'area', r.area,
|
|
16097
|
+
'appointmentsToday', coalesce(t.n, 0), 'nextAppointmentAt', t.proximo,
|
|
16098
|
+
'servedToday', coalesce(s.atendimentos, 0)) ORDER BY r.name)
|
|
16099
|
+
FROM roster r
|
|
16100
|
+
LEFT JOIN appts t ON t.professional_id = r.id
|
|
16101
|
+
LEFT JOIN served s ON s.professional_id = r.id
|
|
16102
|
+
WHERE NOT EXISTS (SELECT 1 FROM ent e WHERE e.professional_id = r.id)), '[]'::jsonb),
|
|
16103
|
+
'events', coalesce((
|
|
16104
|
+
SELECT jsonb_agg(x.item ORDER BY x.at DESC) FROM (
|
|
16105
|
+
SELECT jsonb_build_object('id', ev.id, 'professionalId', ev.professional_id, 'name', ep.name,
|
|
16106
|
+
'type', ev.event_type, 'origin', ev.origin, 'area', ev.area_key,
|
|
16107
|
+
'reason', coalesce(ev.reason, ev.payload->>'reason'), 'createdAt', ev.occurred_at) AS item,
|
|
16108
|
+
ev.occurred_at AS at
|
|
16109
|
+
FROM public.plg_agenda_turn_events ev
|
|
16110
|
+
JOIN public.people ep ON ep.id = ev.professional_id
|
|
16111
|
+
WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day
|
|
16112
|
+
ORDER BY ev.occurred_at DESC LIMIT 60) x), '[]'::jsonb)
|
|
16113
|
+
) INTO v_result;
|
|
16114
|
+
RETURN v_result;
|
|
16115
|
+
END $$;
|
|
16116
|
+
|
|
16117
|
+
-- 5) A\xE7\xF5es da recep\xE7\xE3o --------------------------------------------------------------
|
|
16118
|
+
-- join \u2192 create \xB7 leave \u2192 delete \xB7 away/back/move/start/finish/skip/call \u2192 edit
|
|
16119
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_act(
|
|
16120
|
+
p_unit_id uuid,
|
|
16121
|
+
p_professional_id uuid,
|
|
16122
|
+
p_action text,
|
|
16123
|
+
p_position integer DEFAULT NULL,
|
|
16124
|
+
p_reason text DEFAULT NULL
|
|
16125
|
+
) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
16126
|
+
DECLARE
|
|
16127
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16128
|
+
v_queue public.plg_agenda_turn_queues;
|
|
16129
|
+
v_entry public.plg_agenda_turn_entries;
|
|
16130
|
+
v_need text := CASE p_action WHEN 'join' THEN 'create' WHEN 'leave' THEN 'delete' ELSE 'edit' END;
|
|
16131
|
+
v_reason text := nullif(btrim(coalesce(p_reason, '')), '');
|
|
16132
|
+
v_area text;
|
|
16133
|
+
v_pos bigint;
|
|
16134
|
+
v_ids uuid[];
|
|
16135
|
+
v_users uuid[];
|
|
16136
|
+
BEGIN
|
|
16137
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16138
|
+
IF p_action NOT IN ('join', 'leave', 'away', 'back', 'move', 'start', 'finish', 'skip', 'call') THEN
|
|
16139
|
+
RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';
|
|
16140
|
+
END IF;
|
|
16141
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.' || v_need, p_unit_id) THEN
|
|
16142
|
+
RAISE EXCEPTION 'agenda.turn_queue.% is required', v_need USING ERRCODE = '42501';
|
|
16143
|
+
END IF;
|
|
16144
|
+
IF NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = p_professional_id AND p.tenant_id = v_tenant AND p.is_active) THEN
|
|
16145
|
+
RAISE EXCEPTION 'turn_queue_invalid_professional' USING ERRCODE = '22023';
|
|
16146
|
+
END IF;
|
|
16147
|
+
|
|
16148
|
+
SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
|
|
16149
|
+
WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;
|
|
16150
|
+
IF v_queue.id IS NULL OR NOT v_queue.is_active THEN
|
|
16151
|
+
RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023';
|
|
16152
|
+
END IF;
|
|
16153
|
+
PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));
|
|
16154
|
+
|
|
16155
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
16156
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = p_professional_id;
|
|
16157
|
+
IF v_entry.id IS NOT NULL AND v_entry.status = 'unavailable' THEN v_entry := NULL; END IF;
|
|
16158
|
+
|
|
16159
|
+
IF p_action = 'join' THEN
|
|
16160
|
+
IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16161
|
+
v_area := public.agenda_turn__area_of(v_tenant, p_professional_id);
|
|
16162
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_area);
|
|
16163
|
+
INSERT INTO public.plg_agenda_turn_entries
|
|
16164
|
+
(tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, appointment_id, away_reason, updated_at)
|
|
16165
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'available', v_pos, v_area, now(), now(), now(), NULL, NULL, now())
|
|
16166
|
+
ON CONFLICT (queue_id, professional_id) DO UPDATE SET
|
|
16167
|
+
status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),
|
|
16168
|
+
state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();
|
|
16169
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
16170
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'joined', 'frontdesk', auth.uid(), v_area, jsonb_build_object('to_position', v_pos));
|
|
16171
|
+
RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area, 'position', v_pos);
|
|
16172
|
+
END IF;
|
|
16173
|
+
|
|
16174
|
+
IF p_action = 'call' THEN
|
|
16175
|
+
SELECT array_agg(DISTINCT m.user_id) INTO v_users FROM app.memberships m
|
|
16176
|
+
WHERE m.tenant_id = v_tenant AND m.person_id = p_professional_id AND m.active;
|
|
16177
|
+
IF v_users IS NOT NULL THEN
|
|
16178
|
+
INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)
|
|
16179
|
+
SELECT v_tenant, u, 'Cliente para voc\xEA na recep\xE7\xE3o', coalesce(v_reason, 'Voc\xEA \xE9 o pr\xF3ximo da vez.'),
|
|
16180
|
+
'turn_queue_call', '#/agenda', 'person', p_professional_id
|
|
16181
|
+
FROM unnest(v_users) u;
|
|
16182
|
+
END IF;
|
|
16183
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)
|
|
16184
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'called', 'frontdesk', auth.uid(),
|
|
16185
|
+
coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, p_professional_id)), v_reason,
|
|
16186
|
+
jsonb_build_object('notified', v_users IS NOT NULL));
|
|
16187
|
+
RETURN jsonb_build_object('ok', true, 'changed', true, 'notified', v_users IS NOT NULL);
|
|
16188
|
+
END IF;
|
|
16189
|
+
|
|
16190
|
+
IF v_entry.id IS NULL THEN RAISE EXCEPTION 'turn_queue_not_in_queue' USING ERRCODE = '22023'; END IF;
|
|
16191
|
+
|
|
16192
|
+
IF p_action = 'leave' THEN
|
|
16193
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,
|
|
16194
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16195
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
16196
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
16197
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'left', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));
|
|
16198
|
+
|
|
16199
|
+
ELSIF p_action = 'away' THEN
|
|
16200
|
+
IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;
|
|
16201
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),
|
|
16202
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16203
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)
|
|
16204
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'away', 'frontdesk', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'),
|
|
16205
|
+
jsonb_build_object('from_position', v_entry.position));
|
|
16206
|
+
|
|
16207
|
+
ELSIF p_action = 'back' THEN
|
|
16208
|
+
IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16209
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,
|
|
16210
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16211
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
16212
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'back', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_entry.position));
|
|
16213
|
+
|
|
16214
|
+
ELSIF p_action = 'move' THEN
|
|
16215
|
+
IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;
|
|
16216
|
+
IF p_position IS NULL OR p_position < 1 THEN RAISE EXCEPTION 'turn_queue_invalid_position' USING ERRCODE = '22023'; END IF;
|
|
16217
|
+
SELECT array_agg(id ORDER BY position, joined_at, id) INTO v_ids FROM public.plg_agenda_turn_entries
|
|
16218
|
+
WHERE queue_id = v_queue.id AND area_key IS NOT DISTINCT FROM v_entry.area_key
|
|
16219
|
+
AND status IN ('available', 'away') AND id <> v_entry.id;
|
|
16220
|
+
v_ids := coalesce(v_ids, ARRAY[]::uuid[]);
|
|
16221
|
+
v_pos := least(p_position, coalesce(array_length(v_ids, 1), 0) + 1);
|
|
16222
|
+
v_ids := v_ids[1:v_pos - 1] || v_entry.id || v_ids[v_pos:];
|
|
16223
|
+
UPDATE public.plg_agenda_turn_entries e SET position = o.ord, updated_at = now()
|
|
16224
|
+
FROM unnest(v_ids) WITH ORDINALITY o(id, ord) WHERE e.id = o.id;
|
|
16225
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
16226
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'moved', 'frontdesk', auth.uid(), v_entry.area_key,
|
|
16227
|
+
jsonb_build_object('from_position', v_entry.position, 'to_position', v_pos));
|
|
16228
|
+
|
|
16229
|
+
ELSIF p_action = 'start' THEN
|
|
16230
|
+
IF v_entry.status = 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16231
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, previous_position = position,
|
|
16232
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16233
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
16234
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
16235
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'started', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));
|
|
16236
|
+
|
|
16237
|
+
ELSIF p_action = 'finish' THEN
|
|
16238
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);
|
|
16239
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL, appointment_id = NULL,
|
|
16240
|
+
position = v_pos, available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16241
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
16242
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
16243
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'finished', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));
|
|
16244
|
+
|
|
16245
|
+
ELSIF p_action = 'skip' THEN
|
|
16246
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)
|
|
16247
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'skipped', 'frontdesk', auth.uid(), v_entry.area_key, v_reason,
|
|
16248
|
+
jsonb_build_object('from_position', v_entry.position));
|
|
16249
|
+
END IF;
|
|
16250
|
+
|
|
16251
|
+
RETURN jsonb_build_object('ok', true, 'changed', true);
|
|
16252
|
+
END $$;
|
|
16253
|
+
|
|
16254
|
+
-- 6) O pr\xF3prio profissional ("Cheguei") -------------------------------------------
|
|
16255
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_state(p_unit_id uuid)
|
|
16256
|
+
RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16257
|
+
DECLARE
|
|
16258
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16259
|
+
v_person uuid;
|
|
16260
|
+
v_queue public.plg_agenda_turn_queues;
|
|
16261
|
+
v_entry public.plg_agenda_turn_entries;
|
|
16262
|
+
v_tz text;
|
|
16263
|
+
v_rank bigint;
|
|
16264
|
+
BEGIN
|
|
16265
|
+
IF v_tenant IS NULL OR auth.uid() IS NULL OR p_unit_id IS NULL THEN RETURN NULL; END IF;
|
|
16266
|
+
SELECT m.person_id INTO v_person FROM app.memberships m
|
|
16267
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;
|
|
16268
|
+
IF v_person IS NULL OR NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN RETURN NULL; END IF;
|
|
16269
|
+
v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);
|
|
16270
|
+
IF v_queue.id IS NULL OR NOT v_queue.is_active THEN RETURN NULL; END IF;
|
|
16271
|
+
v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);
|
|
16272
|
+
|
|
16273
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
16274
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable'
|
|
16275
|
+
AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;
|
|
16276
|
+
IF v_entry.id IS NOT NULL AND v_entry.status IN ('available', 'away') THEN
|
|
16277
|
+
SELECT count(*) + 1 INTO v_rank FROM public.plg_agenda_turn_entries o
|
|
16278
|
+
WHERE o.queue_id = v_queue.id AND o.area_key IS NOT DISTINCT FROM v_entry.area_key
|
|
16279
|
+
AND o.status IN ('available', 'away') AND o.id <> v_entry.id
|
|
16280
|
+
AND (o.position, o.joined_at, o.id) < (v_entry.position, v_entry.joined_at, v_entry.id);
|
|
16281
|
+
END IF;
|
|
16282
|
+
RETURN jsonb_build_object(
|
|
16283
|
+
'professionalId', v_person,
|
|
16284
|
+
'selfCheckin', v_queue.self_checkin,
|
|
16285
|
+
'requireLocation', v_queue.geofence_required,
|
|
16286
|
+
'inQueue', v_entry.id IS NOT NULL,
|
|
16287
|
+
'area', coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, v_person)),
|
|
16288
|
+
'status', v_entry.status,
|
|
16289
|
+
'awayReason', v_entry.away_reason,
|
|
16290
|
+
'rank', v_rank);
|
|
16291
|
+
END $$;
|
|
16292
|
+
|
|
16293
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_action(
|
|
16294
|
+
p_unit_id uuid,
|
|
16295
|
+
p_action text,
|
|
16296
|
+
p_latitude double precision DEFAULT NULL,
|
|
16297
|
+
p_longitude double precision DEFAULT NULL,
|
|
16298
|
+
p_accuracy_m double precision DEFAULT NULL,
|
|
16299
|
+
p_reason text DEFAULT NULL
|
|
16300
|
+
) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
16301
|
+
DECLARE
|
|
16302
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16303
|
+
v_person uuid;
|
|
16304
|
+
v_queue public.plg_agenda_turn_queues;
|
|
16305
|
+
v_entry public.plg_agenda_turn_entries;
|
|
16306
|
+
v_reason text := nullif(btrim(coalesce(p_reason, '')), '');
|
|
16307
|
+
v_lat numeric;
|
|
16308
|
+
v_lng numeric;
|
|
16309
|
+
v_distance double precision;
|
|
16310
|
+
v_area text;
|
|
16311
|
+
v_pos bigint;
|
|
16312
|
+
BEGIN
|
|
16313
|
+
IF v_tenant IS NULL OR auth.uid() IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16314
|
+
IF p_action NOT IN ('join', 'leave', 'away', 'back', 'finish') THEN
|
|
16315
|
+
RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';
|
|
16316
|
+
END IF;
|
|
16317
|
+
SELECT m.person_id INTO v_person FROM app.memberships m
|
|
16318
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;
|
|
16319
|
+
IF v_person IS NULL THEN RAISE EXCEPTION 'turn_queue_professional_not_linked' USING ERRCODE = '42501'; END IF;
|
|
16320
|
+
IF NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN
|
|
16321
|
+
RAISE EXCEPTION 'turn_queue_professional_outside_unit' USING ERRCODE = '42501';
|
|
16322
|
+
END IF;
|
|
16323
|
+
SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
|
|
16324
|
+
WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;
|
|
16325
|
+
IF v_queue.id IS NULL OR NOT v_queue.is_active THEN RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023'; END IF;
|
|
16326
|
+
IF NOT v_queue.self_checkin THEN RAISE EXCEPTION 'turn_queue_checkin_by_frontdesk' USING ERRCODE = '22023'; END IF;
|
|
16327
|
+
PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));
|
|
16328
|
+
|
|
16329
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
16330
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable';
|
|
16331
|
+
|
|
16332
|
+
IF p_action = 'join' THEN
|
|
16333
|
+
IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16334
|
+
IF v_queue.geofence_required THEN
|
|
16335
|
+
SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;
|
|
16336
|
+
IF v_lat IS NULL OR v_lng IS NULL THEN RAISE EXCEPTION 'unit_has_no_coordinates' USING ERRCODE = '22023'; END IF;
|
|
16337
|
+
IF p_latitude IS NULL OR p_longitude IS NULL THEN RAISE EXCEPTION 'location_required' USING ERRCODE = '22023'; END IF;
|
|
16338
|
+
v_distance := 6371000 * acos(least(1, greatest(-1,
|
|
16339
|
+
cos(radians(v_lat::double precision)) * cos(radians(p_latitude)) * cos(radians(p_longitude) - radians(v_lng::double precision))
|
|
16340
|
+
+ sin(radians(v_lat::double precision)) * sin(radians(p_latitude)))));
|
|
16341
|
+
IF v_distance > v_queue.geofence_radius_meters + least(coalesce(p_accuracy_m, 0), 100) THEN
|
|
16342
|
+
RAISE EXCEPTION 'outside_geofence' USING ERRCODE = '42501';
|
|
16343
|
+
END IF;
|
|
16344
|
+
END IF;
|
|
16345
|
+
v_area := public.agenda_turn__area_of(v_tenant, v_person);
|
|
16346
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_area);
|
|
16347
|
+
INSERT INTO public.plg_agenda_turn_entries
|
|
16348
|
+
(tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, updated_at)
|
|
16349
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'available', v_pos, v_area, now(), now(), now(), now())
|
|
16350
|
+
ON CONFLICT (queue_id, professional_id) DO UPDATE SET
|
|
16351
|
+
status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),
|
|
16352
|
+
state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();
|
|
16353
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key,
|
|
16354
|
+
inside_geofence, location_accuracy_meters, payload)
|
|
16355
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'joined', 'app', auth.uid(), v_area,
|
|
16356
|
+
CASE WHEN v_queue.geofence_required THEN true END, p_accuracy_m, jsonb_build_object('to_position', v_pos));
|
|
16357
|
+
RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area);
|
|
16358
|
+
END IF;
|
|
16359
|
+
|
|
16360
|
+
IF v_entry.id IS NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16361
|
+
|
|
16362
|
+
IF p_action = 'leave' THEN
|
|
16363
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,
|
|
16364
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16365
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
16366
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)
|
|
16367
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'left', 'app', auth.uid(), v_entry.area_key);
|
|
16368
|
+
ELSIF p_action = 'away' THEN
|
|
16369
|
+
IF v_entry.status <> 'available' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16370
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),
|
|
16371
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16372
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason)
|
|
16373
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'away', 'app', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'));
|
|
16374
|
+
ELSIF p_action = 'back' THEN
|
|
16375
|
+
IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16376
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,
|
|
16377
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16378
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)
|
|
16379
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'back', 'app', auth.uid(), v_entry.area_key);
|
|
16380
|
+
ELSIF p_action = 'finish' THEN
|
|
16381
|
+
IF v_entry.status <> 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
16382
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);
|
|
16383
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,
|
|
16384
|
+
available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16385
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
16386
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'finished', 'app', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));
|
|
16387
|
+
END IF;
|
|
16388
|
+
RETURN jsonb_build_object('ok', true, 'changed', true);
|
|
16389
|
+
END $$;
|
|
16390
|
+
|
|
16391
|
+
-- 7) Configurar ---------------------------------------------------------------------
|
|
16392
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_config(p_unit_id uuid)
|
|
16393
|
+
RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16394
|
+
DECLARE
|
|
16395
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16396
|
+
v_queue public.plg_agenda_turn_queues;
|
|
16397
|
+
BEGIN
|
|
16398
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16399
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN
|
|
16400
|
+
RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';
|
|
16401
|
+
END IF;
|
|
16402
|
+
v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);
|
|
16403
|
+
RETURN jsonb_build_object(
|
|
16404
|
+
'unit', jsonb_build_object('auto', coalesce(v_queue.follows_appointments, true),
|
|
16405
|
+
'selfCheckin', coalesce(v_queue.self_checkin, true),
|
|
16406
|
+
'requireLocation', coalesce(v_queue.geofence_required, false)),
|
|
16407
|
+
'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color, 'keywords', to_jsonb(d.keywords)) ORDER BY d.sort_order, d.key)
|
|
16408
|
+
FROM public.agenda_turn__area_defs(v_tenant) d WHERE d.key <> 'geral'), '[]'::jsonb),
|
|
16409
|
+
'professionals', coalesce((
|
|
16410
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
16411
|
+
'professionalId', p.id, 'name', p.name,
|
|
16412
|
+
'manualArea', pa.area_key,
|
|
16413
|
+
'autoArea', public.agenda_turn__auto_area_of(v_tenant, p.id)
|
|
16414
|
+
) ORDER BY p.name)
|
|
16415
|
+
FROM public.people p
|
|
16416
|
+
LEFT JOIN public.plg_agenda_turn_professional_areas pa ON pa.tenant_id = v_tenant AND pa.professional_id = p.id
|
|
16417
|
+
WHERE p.tenant_id = v_tenant AND p.is_active AND public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)), '[]'::jsonb),
|
|
16418
|
+
'quickServiceIds', coalesce((SELECT jsonb_agg(q.product_id) FROM public.plg_agenda_turn_quick_services q WHERE q.tenant_id = v_tenant), '[]'::jsonb),
|
|
16419
|
+
'services', coalesce((SELECT jsonb_agg(jsonb_build_object('id', s.id, 'name', s.name) ORDER BY s.name)
|
|
16420
|
+
FROM public.products s
|
|
16421
|
+
WHERE s.tenant_id = v_tenant AND s.kind = 'service' AND coalesce(s.is_active, true)), '[]'::jsonb)
|
|
16422
|
+
);
|
|
16423
|
+
END $$;
|
|
16424
|
+
|
|
16425
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_save_config(p_unit_id uuid, p_config jsonb)
|
|
16426
|
+
RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
16427
|
+
DECLARE
|
|
16428
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16429
|
+
v_area jsonb;
|
|
16430
|
+
v_i int := 0;
|
|
16431
|
+
BEGIN
|
|
16432
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16433
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.edit', p_unit_id) THEN
|
|
16434
|
+
RAISE EXCEPTION 'agenda.turn_queue.edit is required' USING ERRCODE = '42501';
|
|
16435
|
+
END IF;
|
|
16436
|
+
|
|
16437
|
+
IF jsonb_typeof(p_config->'unit') = 'object' THEN
|
|
16438
|
+
INSERT INTO public.plg_agenda_turn_queues AS q (tenant_id, unit_id, is_active, follows_appointments, self_checkin, geofence_required)
|
|
16439
|
+
VALUES (v_tenant, p_unit_id, false,
|
|
16440
|
+
coalesce((p_config->'unit'->>'auto')::boolean, true),
|
|
16441
|
+
coalesce((p_config->'unit'->>'selfCheckin')::boolean, true),
|
|
16442
|
+
coalesce((p_config->'unit'->>'requireLocation')::boolean, false))
|
|
16443
|
+
ON CONFLICT (tenant_id, unit_id, queue_key) DO UPDATE SET
|
|
16444
|
+
follows_appointments = coalesce((p_config->'unit'->>'auto')::boolean, q.follows_appointments),
|
|
16445
|
+
self_checkin = coalesce((p_config->'unit'->>'selfCheckin')::boolean, q.self_checkin),
|
|
16446
|
+
geofence_required = coalesce((p_config->'unit'->>'requireLocation')::boolean, q.geofence_required),
|
|
16447
|
+
updated_at = now();
|
|
16448
|
+
END IF;
|
|
16449
|
+
|
|
16450
|
+
-- The list sent replaces the network's list; a missing area is deactivated.
|
|
16451
|
+
IF jsonb_typeof(p_config->'areas') = 'array' THEN
|
|
16452
|
+
UPDATE public.plg_agenda_turn_areas SET is_active = false, updated_at = now()
|
|
16453
|
+
WHERE tenant_id = v_tenant AND key NOT IN (SELECT x->>'key' FROM jsonb_array_elements(p_config->'areas') x);
|
|
16454
|
+
FOR v_area IN SELECT * FROM jsonb_array_elements(p_config->'areas') LOOP
|
|
16455
|
+
v_i := v_i + 1;
|
|
16456
|
+
INSERT INTO public.plg_agenda_turn_areas (tenant_id, key, name, color, keywords, sort_order, is_active)
|
|
16457
|
+
VALUES (v_tenant, v_area->>'key', btrim(v_area->>'name'), coalesce(v_area->>'color', 'slate'),
|
|
16458
|
+
coalesce(ARRAY(SELECT btrim(k) FROM jsonb_array_elements_text(coalesce(v_area->'keywords', '[]'::jsonb)) k WHERE btrim(k) <> ''), ARRAY[]::text[]),
|
|
16459
|
+
v_i, true)
|
|
16460
|
+
ON CONFLICT (tenant_id, key) DO UPDATE SET
|
|
16461
|
+
name = excluded.name, color = excluded.color, keywords = excluded.keywords,
|
|
16462
|
+
sort_order = excluded.sort_order, is_active = true, updated_at = now();
|
|
16463
|
+
END LOOP;
|
|
16464
|
+
END IF;
|
|
16465
|
+
|
|
16466
|
+
-- {"<person uuid>": "<area key>" | null}; null = back to Autom\xE1tico.
|
|
16467
|
+
IF jsonb_typeof(p_config->'professionalAreas') = 'object' THEN
|
|
16468
|
+
DELETE FROM public.plg_agenda_turn_professional_areas pa
|
|
16469
|
+
WHERE pa.tenant_id = v_tenant
|
|
16470
|
+
AND pa.professional_id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'));
|
|
16471
|
+
INSERT INTO public.plg_agenda_turn_professional_areas (tenant_id, professional_id, area_key)
|
|
16472
|
+
SELECT v_tenant, p.id, p_config->'professionalAreas'->>p.id::text
|
|
16473
|
+
FROM public.people p
|
|
16474
|
+
WHERE p.tenant_id = v_tenant
|
|
16475
|
+
AND p.id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'))
|
|
16476
|
+
AND nullif(p_config->'professionalAreas'->>p.id::text, '') IS NOT NULL;
|
|
16477
|
+
END IF;
|
|
16478
|
+
|
|
16479
|
+
IF jsonb_typeof(p_config->'quickServiceIds') = 'array' THEN
|
|
16480
|
+
DELETE FROM public.plg_agenda_turn_quick_services WHERE tenant_id = v_tenant;
|
|
16481
|
+
INSERT INTO public.plg_agenda_turn_quick_services (tenant_id, product_id)
|
|
16482
|
+
SELECT v_tenant, s.id FROM public.products s
|
|
16483
|
+
WHERE s.tenant_id = v_tenant
|
|
16484
|
+
AND s.id::text IN (SELECT x #>> '{}' FROM jsonb_array_elements(p_config->'quickServiceIds') x)
|
|
16485
|
+
ON CONFLICT DO NOTHING;
|
|
16486
|
+
END IF;
|
|
16487
|
+
|
|
16488
|
+
RETURN jsonb_build_object('ok', true);
|
|
16489
|
+
END $$;
|
|
16490
|
+
|
|
16491
|
+
-- 8) A fila anda sozinha pelo atendimento ---------------------------------------
|
|
16492
|
+
-- Replaces 016's trigger body: start leaves the queue unless every service is
|
|
16493
|
+
-- quick; leaving in_progress (any status) puts the person back at the END.
|
|
16494
|
+
-- Never blocks the agenda.
|
|
16495
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_on_appointment_status()
|
|
16496
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
16497
|
+
DECLARE
|
|
16498
|
+
v_unit uuid := coalesce(NEW.unit_id, NEW.location_id);
|
|
16499
|
+
v_queue public.plg_agenda_turn_queues;
|
|
16500
|
+
v_entry public.plg_agenda_turn_entries;
|
|
16501
|
+
v_tz text;
|
|
16502
|
+
v_pos bigint;
|
|
16503
|
+
v_quick boolean;
|
|
16504
|
+
BEGIN
|
|
16505
|
+
IF NEW.assignee_id IS NULL OR v_unit IS NULL OR NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;
|
|
16506
|
+
IF NEW.status <> 'in_progress' AND OLD.status IS DISTINCT FROM 'in_progress' THEN RETURN NEW; END IF;
|
|
16507
|
+
|
|
16508
|
+
SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
|
|
16509
|
+
WHERE q.tenant_id = NEW.tenant_id AND q.unit_id = v_unit AND q.queue_key = 'walk_in'
|
|
16510
|
+
AND q.is_active AND q.follows_appointments
|
|
16511
|
+
FOR UPDATE;
|
|
16512
|
+
IF v_queue.id IS NULL THEN RETURN NEW; END IF;
|
|
16513
|
+
v_tz := public.agenda_turn__tz(NEW.tenant_id, v_unit);
|
|
16514
|
+
|
|
16515
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
16516
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = NEW.assignee_id AND e.status <> 'unavailable'
|
|
16517
|
+
AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;
|
|
16518
|
+
IF v_entry.id IS NULL THEN RETURN NEW; END IF;
|
|
16519
|
+
|
|
16520
|
+
IF NEW.status = 'in_progress' THEN
|
|
16521
|
+
IF v_entry.status = 'serving' THEN RETURN NEW; END IF;
|
|
16522
|
+
SELECT bool_and(q.product_id IS NOT NULL) INTO v_quick
|
|
16523
|
+
FROM public.appointment_items ai
|
|
16524
|
+
LEFT JOIN public.plg_agenda_turn_quick_services q ON q.tenant_id = NEW.tenant_id AND q.product_id = ai.service_id
|
|
16525
|
+
WHERE ai.booking_id = NEW.id;
|
|
16526
|
+
IF coalesce(v_quick, false) THEN
|
|
16527
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)
|
|
16528
|
+
VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'quick_service', 'automatic', NEW.id, v_entry.area_key);
|
|
16529
|
+
RETURN NEW;
|
|
16530
|
+
END IF;
|
|
16531
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, appointment_id = NEW.id,
|
|
16532
|
+
previous_position = position, state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16533
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
16534
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)
|
|
16535
|
+
VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'started', 'automatic', NEW.id, v_entry.area_key);
|
|
16536
|
+
ELSIF v_entry.status = 'serving' AND v_entry.appointment_id = NEW.id THEN
|
|
16537
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);
|
|
16538
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,
|
|
16539
|
+
available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
16540
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key, payload)
|
|
16541
|
+
VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'finished', 'automatic', NEW.id, v_entry.area_key,
|
|
16542
|
+
jsonb_build_object('status', NEW.status, 'to_position', v_pos));
|
|
16543
|
+
END IF;
|
|
16544
|
+
RETURN NEW;
|
|
16545
|
+
EXCEPTION WHEN others THEN
|
|
16546
|
+
RAISE WARNING 'agenda_turn_queue_on_appointment_status: %', SQLERRM;
|
|
16547
|
+
RETURN NEW;
|
|
16548
|
+
END $$;
|
|
16549
|
+
|
|
16550
|
+
-- 9) Relat\xF3rio: a justi\xE7a da fila no per\xEDodo --------------------------------------
|
|
16551
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_report(p_unit_ids uuid[], p_from date, p_to date)
|
|
16552
|
+
RETURNS TABLE(unit_id uuid, unit_name text, professional_id uuid, professional_name text, area text,
|
|
16553
|
+
entradas bigint, atendimentos bigint, servicos_rapidos bigint, chamados bigint,
|
|
16554
|
+
pulos bigint, ausencias bigint, minutos_ausente numeric)
|
|
16555
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16556
|
+
DECLARE
|
|
16557
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16558
|
+
BEGIN
|
|
16559
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16560
|
+
RETURN QUERY
|
|
16561
|
+
WITH units_ok AS MATERIALIZED (
|
|
16562
|
+
SELECT u.id, u.name FROM public.v_units u
|
|
16563
|
+
WHERE u.tenant_id = v_tenant AND (p_unit_ids IS NULL OR u.id = ANY (p_unit_ids))
|
|
16564
|
+
AND app.has_unit(u.id) AND app.has_permission('agenda.turn_queue.read', u.id)
|
|
16565
|
+
), ev AS MATERIALIZED (
|
|
16566
|
+
SELECT e.*, lead(e.occurred_at) OVER (PARTITION BY e.unit_id, e.professional_id ORDER BY e.occurred_at, e.id) AS proximo_em
|
|
16567
|
+
FROM public.plg_agenda_turn_events e JOIN units_ok u ON u.id = e.unit_id
|
|
16568
|
+
WHERE e.tenant_id = v_tenant
|
|
16569
|
+
AND (e.occurred_at AT TIME ZONE public.agenda_turn__tz(v_tenant, e.unit_id))::date BETWEEN p_from AND p_to
|
|
16570
|
+
)
|
|
16571
|
+
SELECT ev.unit_id, max(u.name), ev.professional_id, max(p.name), max(ev.area_key),
|
|
16572
|
+
count(*) FILTER (WHERE ev.event_type IN ('joined', 'available', 'override_available')),
|
|
16573
|
+
count(*) FILTER (WHERE ev.event_type IN ('started', 'advanced_to_service')),
|
|
16574
|
+
count(*) FILTER (WHERE ev.event_type = 'quick_service'),
|
|
16575
|
+
count(*) FILTER (WHERE ev.event_type = 'called'),
|
|
16576
|
+
count(*) FILTER (WHERE ev.event_type = 'skipped'),
|
|
16577
|
+
count(*) FILTER (WHERE ev.event_type = 'away'),
|
|
16578
|
+
round(coalesce(sum(extract(epoch FROM (ev.proximo_em - ev.occurred_at)) / 60)
|
|
16579
|
+
FILTER (WHERE ev.event_type = 'away' AND ev.proximo_em IS NOT NULL), 0)::numeric, 0)
|
|
16580
|
+
FROM ev
|
|
16581
|
+
JOIN units_ok u ON u.id = ev.unit_id
|
|
16582
|
+
JOIN public.people p ON p.id = ev.professional_id
|
|
16583
|
+
GROUP BY ev.unit_id, ev.professional_id
|
|
16584
|
+
ORDER BY 1, 7 DESC, 4;
|
|
16585
|
+
END $$;
|
|
16586
|
+
|
|
16587
|
+
-- 10) Grants e tempo real -----------------------------------------------------------
|
|
16588
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__norm(text) FROM PUBLIC, anon;
|
|
16589
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__area_defs(uuid) FROM PUBLIC, anon, authenticated;
|
|
16590
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__auto_area_of(uuid, uuid) FROM PUBLIC, anon, authenticated;
|
|
16591
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__area_of(uuid, uuid) FROM PUBLIC, anon, authenticated;
|
|
16592
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__tz(uuid, uuid) FROM PUBLIC, anon, authenticated;
|
|
16593
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__purge_stale(uuid, text) FROM PUBLIC, anon, authenticated;
|
|
16594
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__renumber(uuid, text) FROM PUBLIC, anon, authenticated;
|
|
16595
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__next_position(uuid, text) FROM PUBLIC, anon, authenticated;
|
|
16596
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__in_unit(uuid, uuid, uuid) FROM PUBLIC, anon, authenticated;
|
|
16597
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__queue(uuid, uuid) FROM PUBLIC, anon, authenticated;
|
|
16598
|
+
REVOKE ALL ON FUNCTION public.agenda_turn_queue_board(uuid) FROM PUBLIC, anon;
|
|
16599
|
+
REVOKE ALL ON FUNCTION public.agenda_turn_queue_act(uuid, uuid, text, integer, text) FROM PUBLIC, anon;
|
|
16600
|
+
REVOKE ALL ON FUNCTION public.agenda_turn_queue_my_state(uuid) FROM PUBLIC, anon;
|
|
16601
|
+
REVOKE ALL ON FUNCTION public.agenda_turn_queue_my_action(uuid, text, double precision, double precision, double precision, text) FROM PUBLIC, anon;
|
|
16602
|
+
REVOKE ALL ON FUNCTION public.agenda_turn_queue_config(uuid) FROM PUBLIC, anon;
|
|
16603
|
+
REVOKE ALL ON FUNCTION public.agenda_turn_queue_save_config(uuid, jsonb) FROM PUBLIC, anon;
|
|
16604
|
+
REVOKE ALL ON FUNCTION public.agenda_turn_queue_report(uuid[], date, date) FROM PUBLIC, anon;
|
|
16605
|
+
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_board(uuid) TO authenticated;
|
|
16606
|
+
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_act(uuid, uuid, text, integer, text) TO authenticated;
|
|
16607
|
+
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_my_state(uuid) TO authenticated;
|
|
16608
|
+
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_my_action(uuid, text, double precision, double precision, double precision, text) TO authenticated;
|
|
16609
|
+
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_config(uuid) TO authenticated;
|
|
16610
|
+
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_save_config(uuid, jsonb) TO authenticated;
|
|
16611
|
+
GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_report(uuid[], date, date) TO authenticated;
|
|
16612
|
+
|
|
16613
|
+
DO $$
|
|
16614
|
+
BEGIN
|
|
16615
|
+
IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'supabase_realtime') THEN
|
|
16616
|
+
IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND schemaname = 'public' AND tablename = 'plg_agenda_turn_entries') THEN
|
|
16617
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.plg_agenda_turn_entries;
|
|
16618
|
+
END IF;
|
|
16619
|
+
IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND schemaname = 'public' AND tablename = 'plg_agenda_turn_events') THEN
|
|
16620
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.plg_agenda_turn_events;
|
|
16621
|
+
END IF;
|
|
16622
|
+
END IF;
|
|
16623
|
+
END $$;
|
|
16624
|
+
`;
|
|
16625
|
+
var MIGRATION_022_A_CHAVE_DA_FILA_E_CONFIGURACAO_DA_UNIDADE = `-- A chave da fila \xE9 configura\xE7\xE3o da unidade, n\xE3o coluna.
|
|
16626
|
+
--
|
|
16627
|
+
-- 016/020/021 guardaram "ligada", "exigir loja", raio, "anda sozinha" e
|
|
16628
|
+
-- "check-in pelo profissional" como colunas de plg_agenda_turn_queues. O SDK j\xE1
|
|
16629
|
+
-- tem casa para isso (127/168): chave registrada em app.setting_keys com
|
|
16630
|
+
-- scope 'unit', valor em app.units.settings, lido em cascata
|
|
16631
|
+
-- unidade \u2192 casa \u2192 padr\xE3o. Um toggle novo vira uma linha de registro, n\xE3o uma
|
|
16632
|
+
-- migration de coluna, e a rede ganha "padr\xE3o da casa" sem 18 escritas.
|
|
16633
|
+
--
|
|
16634
|
+
-- plg_agenda_turn_queues continua: \xE9 a FILA (entidade, dona das entradas e da
|
|
16635
|
+
-- trilha). S\xF3 a configura\xE7\xE3o sai dela. As colunas antigas ficam paradas e s\xE3o
|
|
16636
|
+
-- removidas numa limpeza \xE0 parte (apps publicados ainda leem is_active).
|
|
16637
|
+
-- Idempotente: o espelho s\xF3 copia para unidade que ainda n\xE3o tem a chave.
|
|
16638
|
+
|
|
16639
|
+
SELECT app.seed_setting_keys($$[
|
|
16640
|
+
{ "key": "agenda.turn_queue.enabled", "type": "boolean", "default": false, "scope": "unit", "reader": "plugin:agenda",
|
|
16641
|
+
"description": "Usar a lista da vez nesta unidade (V1: companies.turn_queue_enabled)." },
|
|
16642
|
+
{ "key": "agenda.turn_queue.follows_appointments", "type": "boolean", "default": true, "scope": "unit", "reader": "plugin:agenda",
|
|
16643
|
+
"description": "A fila anda sozinha pelo atendimento: iniciar tira da vez, concluir devolve ao fim (V1: companies.turn_queue_auto)." },
|
|
16644
|
+
{ "key": "agenda.turn_queue.self_checkin", "type": "boolean", "default": true, "scope": "unit", "reader": "plugin:agenda",
|
|
16645
|
+
"description": "O profissional faz o pr\xF3prio check-in pelo bot\xE3o Cheguei (V1: companies.turn_queue_self_checkin)." },
|
|
16646
|
+
{ "key": "agenda.turn_queue.require_location", "type": "boolean", "default": false, "scope": "unit", "reader": "plugin:agenda",
|
|
16647
|
+
"description": "O Cheguei s\xF3 vale dentro do raio da loja (V1: companies.turn_queue_require_location)." },
|
|
16648
|
+
{ "key": "agenda.turn_queue.radius_m", "type": "integer", "default": 80, "scope": "unit", "reader": "plugin:agenda",
|
|
16649
|
+
"description": "Raio da loja em metros, de 10 a 1000 (V1: companies.turn_queue_radius_m)." }
|
|
16650
|
+
]$$::jsonb);
|
|
16651
|
+
|
|
16652
|
+
-- Leitura pela linha da fila: nula quando n\xE3o h\xE1 fila, para o chamador decidir.
|
|
16653
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__on(p_queue public.plg_agenda_turn_queues, p_key text)
|
|
16654
|
+
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16655
|
+
SELECT CASE WHEN p_queue.id IS NULL THEN NULL
|
|
16656
|
+
ELSE (app.unit_setting_value(p_queue.tenant_id, p_queue.unit_id, 'agenda.turn_queue.' || p_key) #>> '{}')::boolean END;
|
|
16657
|
+
$$;
|
|
16658
|
+
|
|
16659
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__radius(p_queue public.plg_agenda_turn_queues)
|
|
16660
|
+
RETURNS integer LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16661
|
+
SELECT coalesce((app.unit_setting_value(p_queue.tenant_id, p_queue.unit_id, 'agenda.turn_queue.radius_m') #>> '{}')::integer, 80);
|
|
16662
|
+
$$;
|
|
16663
|
+
|
|
16664
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__unit_cfg(p_tenant uuid, p_unit uuid)
|
|
16665
|
+
RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16666
|
+
SELECT jsonb_build_object(
|
|
16667
|
+
'enabled', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.enabled') #>> '{}')::boolean, false),
|
|
16668
|
+
'auto', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.follows_appointments') #>> '{}')::boolean, true),
|
|
16669
|
+
'selfCheckin', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.self_checkin') #>> '{}')::boolean, true),
|
|
16670
|
+
'requireLocation', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.require_location') #>> '{}')::boolean, false),
|
|
16671
|
+
'radiusM', coalesce((app.unit_setting_value(p_tenant, p_unit, 'agenda.turn_queue.radius_m') #>> '{}')::integer, 80));
|
|
16672
|
+
$$;
|
|
16673
|
+
|
|
16674
|
+
-- Grava s\xF3 a diferen\xE7a para o padr\xE3o da casa; igual ao padr\xE3o apaga a exce\xE7\xE3o.
|
|
16675
|
+
-- Compara pelo tenant da unidade, n\xE3o da sess\xE3o: dentro de migration n\xE3o h\xE1 sess\xE3o.
|
|
16676
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn__put(p_unit uuid, p_key text, p_value jsonb)
|
|
16677
|
+
RETURNS void LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
16678
|
+
UPDATE app.units u
|
|
16679
|
+
SET settings = app.jsonb_set_path(coalesce(u.settings, '{}'::jsonb), string_to_array('agenda.turn_queue.' || p_key, '.'),
|
|
16680
|
+
CASE WHEN app.unit_setting_value(u.tenant_id, NULL, 'agenda.turn_queue.' || p_key) IS NOT DISTINCT FROM p_value THEN NULL ELSE p_value END)
|
|
16681
|
+
WHERE u.id = p_unit AND p_value IS NOT NULL AND p_value <> 'null'::jsonb;
|
|
16682
|
+
$$;
|
|
16683
|
+
|
|
16684
|
+
-- Espelho \xFAnico do que as colunas diziam.
|
|
16685
|
+
DO $$
|
|
16686
|
+
DECLARE r record;
|
|
16687
|
+
BEGIN
|
|
16688
|
+
FOR r IN
|
|
16689
|
+
SELECT q.* FROM public.plg_agenda_turn_queues q JOIN app.units u ON u.id = q.unit_id
|
|
16690
|
+
WHERE q.queue_key = 'walk_in' AND u.settings #> '{agenda,turn_queue}' IS NULL
|
|
16691
|
+
LOOP
|
|
16692
|
+
PERFORM public.agenda_turn__put(r.unit_id, 'enabled', to_jsonb(r.is_active));
|
|
16693
|
+
PERFORM public.agenda_turn__put(r.unit_id, 'follows_appointments', to_jsonb(r.follows_appointments));
|
|
16694
|
+
PERFORM public.agenda_turn__put(r.unit_id, 'self_checkin', to_jsonb(r.self_checkin));
|
|
16695
|
+
PERFORM public.agenda_turn__put(r.unit_id, 'require_location', to_jsonb(r.geofence_required));
|
|
16696
|
+
PERFORM public.agenda_turn__put(r.unit_id, 'radius_m', to_jsonb(r.geofence_radius_meters));
|
|
16697
|
+
END LOOP;
|
|
16698
|
+
-- Re-pass what is already stored so values equal to the house default stop
|
|
16699
|
+
-- being exceptions (a first replay wrote them all).
|
|
16700
|
+
FOR r IN SELECT u.id, k.key, k.value FROM app.units u, jsonb_each(u.settings #> '{agenda,turn_queue}') k
|
|
16701
|
+
WHERE jsonb_typeof(u.settings #> '{agenda,turn_queue}') = 'object' LOOP
|
|
16702
|
+
PERFORM public.agenda_turn__put(r.id, r.key, r.value);
|
|
16703
|
+
END LOOP;
|
|
16704
|
+
END $$;
|
|
16705
|
+
|
|
16706
|
+
COMMENT ON COLUMN public.plg_agenda_turn_queues.is_active IS 'Superada por app.units.settings agenda.turn_queue.enabled (022). N\xE3o ler.';
|
|
16707
|
+
COMMENT ON COLUMN public.plg_agenda_turn_queues.geofence_required IS 'Superada por agenda.turn_queue.require_location (022). N\xE3o ler.';
|
|
16708
|
+
COMMENT ON COLUMN public.plg_agenda_turn_queues.geofence_radius_meters IS 'Superada por agenda.turn_queue.radius_m (022). N\xE3o ler.';
|
|
16709
|
+
COMMENT ON COLUMN public.plg_agenda_turn_queues.follows_appointments IS 'Superada por agenda.turn_queue.follows_appointments (022). N\xE3o ler.';
|
|
16710
|
+
COMMENT ON COLUMN public.plg_agenda_turn_queues.self_checkin IS 'Superada por agenda.turn_queue.self_checkin (022). N\xE3o ler.';
|
|
16711
|
+
|
|
16712
|
+
-- A aba Lista da vez da unidade (020): mesma assinatura, agora escrevendo a configura\xE7\xE3o.
|
|
16713
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_configure(
|
|
16714
|
+
p_unit_id uuid,
|
|
16715
|
+
p_enabled boolean,
|
|
16716
|
+
p_geofence_required boolean DEFAULT NULL,
|
|
16717
|
+
p_radius_meters integer DEFAULT NULL
|
|
16718
|
+
)
|
|
16719
|
+
RETURNS public.plg_agenda_turn_queues
|
|
16720
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
|
|
16721
|
+
DECLARE
|
|
16722
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16723
|
+
v_row public.plg_agenda_turn_queues;
|
|
16724
|
+
v_cfg jsonb;
|
|
16725
|
+
BEGIN
|
|
16726
|
+
IF v_tenant IS NULL OR auth.uid() IS NULL THEN
|
|
16727
|
+
RAISE EXCEPTION 'agenda_turn_queue_configure: no session or no active tenant' USING ERRCODE = '42501';
|
|
16728
|
+
END IF;
|
|
16729
|
+
IF NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant) THEN
|
|
16730
|
+
RAISE EXCEPTION 'agenda_turn_queue_configure: unit does not belong to this tenant' USING ERRCODE = '42501';
|
|
16731
|
+
END IF;
|
|
16732
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('settings.manage', p_unit_id) THEN
|
|
16733
|
+
RAISE EXCEPTION 'agenda_turn_queue_configure: settings.manage is required on this unit' USING ERRCODE = '42501';
|
|
16734
|
+
END IF;
|
|
16735
|
+
IF p_radius_meters IS NOT NULL AND (p_radius_meters < 10 OR p_radius_meters > 1000) THEN
|
|
16736
|
+
RAISE EXCEPTION 'agenda_turn_queue_configure: radius must be between 10 and 1000 meters' USING ERRCODE = '22023';
|
|
16737
|
+
END IF;
|
|
16738
|
+
|
|
16739
|
+
INSERT INTO public.plg_agenda_turn_queues (tenant_id, unit_id) VALUES (v_tenant, p_unit_id)
|
|
16740
|
+
ON CONFLICT (tenant_id, unit_id, queue_key) DO NOTHING;
|
|
16741
|
+
PERFORM public.agenda_turn__put(p_unit_id, 'enabled', to_jsonb(p_enabled));
|
|
16742
|
+
PERFORM public.agenda_turn__put(p_unit_id, 'require_location', to_jsonb(p_geofence_required));
|
|
16743
|
+
PERFORM public.agenda_turn__put(p_unit_id, 'radius_m', to_jsonb(p_radius_meters));
|
|
16744
|
+
|
|
16745
|
+
-- The return row carries the EFFECTIVE values, so the caller reads the same
|
|
16746
|
+
-- fields it always did.
|
|
16747
|
+
SELECT * INTO v_row FROM public.plg_agenda_turn_queues q
|
|
16748
|
+
WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in';
|
|
16749
|
+
v_cfg := public.agenda_turn__unit_cfg(v_tenant, p_unit_id);
|
|
16750
|
+
v_row.is_active := (v_cfg->>'enabled')::boolean;
|
|
16751
|
+
v_row.geofence_required := (v_cfg->>'requireLocation')::boolean;
|
|
16752
|
+
v_row.geofence_radius_meters := (v_cfg->>'radiusM')::integer;
|
|
16753
|
+
v_row.follows_appointments := (v_cfg->>'auto')::boolean;
|
|
16754
|
+
v_row.self_checkin := (v_cfg->>'selfCheckin')::boolean;
|
|
16755
|
+
RETURN v_row;
|
|
16756
|
+
END $function$;
|
|
16757
|
+
|
|
16758
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_config(p_unit_id uuid)
|
|
16759
|
+
RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
16760
|
+
DECLARE
|
|
16761
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16762
|
+
BEGIN
|
|
16763
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16764
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN
|
|
16765
|
+
RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';
|
|
16766
|
+
END IF;
|
|
16767
|
+
RETURN jsonb_build_object(
|
|
16768
|
+
'unit', public.agenda_turn__unit_cfg(v_tenant, p_unit_id),
|
|
16769
|
+
'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color, 'keywords', to_jsonb(d.keywords)) ORDER BY d.sort_order, d.key)
|
|
16770
|
+
FROM public.agenda_turn__area_defs(v_tenant) d WHERE d.key <> 'geral'), '[]'::jsonb),
|
|
16771
|
+
'professionals', coalesce((
|
|
16772
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
16773
|
+
'professionalId', p.id, 'name', p.name,
|
|
16774
|
+
'manualArea', pa.area_key,
|
|
16775
|
+
'autoArea', public.agenda_turn__auto_area_of(v_tenant, p.id)
|
|
16776
|
+
) ORDER BY p.name)
|
|
16777
|
+
FROM public.people p
|
|
16778
|
+
LEFT JOIN public.plg_agenda_turn_professional_areas pa ON pa.tenant_id = v_tenant AND pa.professional_id = p.id
|
|
16779
|
+
WHERE p.tenant_id = v_tenant AND p.is_active AND public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)), '[]'::jsonb),
|
|
16780
|
+
'quickServiceIds', coalesce((SELECT jsonb_agg(q.product_id) FROM public.plg_agenda_turn_quick_services q WHERE q.tenant_id = v_tenant), '[]'::jsonb),
|
|
16781
|
+
'services', coalesce((SELECT jsonb_agg(jsonb_build_object('id', s.id, 'name', s.name) ORDER BY s.name)
|
|
16782
|
+
FROM public.products s
|
|
16783
|
+
WHERE s.tenant_id = v_tenant AND s.kind = 'service' AND coalesce(s.is_active, true)), '[]'::jsonb)
|
|
16784
|
+
);
|
|
16785
|
+
END $$;
|
|
16786
|
+
|
|
16787
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_save_config(p_unit_id uuid, p_config jsonb)
|
|
16788
|
+
RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
16789
|
+
DECLARE
|
|
16790
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16791
|
+
v_area jsonb;
|
|
16792
|
+
v_i int := 0;
|
|
16793
|
+
BEGIN
|
|
16794
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
16795
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.edit', p_unit_id) THEN
|
|
16796
|
+
RAISE EXCEPTION 'agenda.turn_queue.edit is required' USING ERRCODE = '42501';
|
|
16797
|
+
END IF;
|
|
16798
|
+
IF NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant) THEN
|
|
16799
|
+
RAISE EXCEPTION 'unit does not belong to this tenant' USING ERRCODE = '42501';
|
|
16800
|
+
END IF;
|
|
16801
|
+
|
|
16802
|
+
IF jsonb_typeof(p_config->'unit') = 'object' THEN
|
|
16803
|
+
PERFORM public.agenda_turn__put(p_unit_id, 'follows_appointments', p_config->'unit'->'auto');
|
|
16804
|
+
PERFORM public.agenda_turn__put(p_unit_id, 'self_checkin', p_config->'unit'->'selfCheckin');
|
|
16805
|
+
PERFORM public.agenda_turn__put(p_unit_id, 'require_location', p_config->'unit'->'requireLocation');
|
|
16806
|
+
END IF;
|
|
16807
|
+
|
|
16808
|
+
-- The list sent replaces the network's list; a missing area is deactivated.
|
|
16809
|
+
IF jsonb_typeof(p_config->'areas') = 'array' THEN
|
|
16810
|
+
UPDATE public.plg_agenda_turn_areas SET is_active = false, updated_at = now()
|
|
16811
|
+
WHERE tenant_id = v_tenant AND key NOT IN (SELECT x->>'key' FROM jsonb_array_elements(p_config->'areas') x);
|
|
16812
|
+
FOR v_area IN SELECT * FROM jsonb_array_elements(p_config->'areas') LOOP
|
|
16813
|
+
v_i := v_i + 1;
|
|
16814
|
+
INSERT INTO public.plg_agenda_turn_areas (tenant_id, key, name, color, keywords, sort_order, is_active)
|
|
16815
|
+
VALUES (v_tenant, v_area->>'key', btrim(v_area->>'name'), coalesce(v_area->>'color', 'slate'),
|
|
16816
|
+
coalesce(ARRAY(SELECT btrim(k) FROM jsonb_array_elements_text(coalesce(v_area->'keywords', '[]'::jsonb)) k WHERE btrim(k) <> ''), ARRAY[]::text[]),
|
|
16817
|
+
v_i, true)
|
|
16818
|
+
ON CONFLICT (tenant_id, key) DO UPDATE SET
|
|
16819
|
+
name = excluded.name, color = excluded.color, keywords = excluded.keywords,
|
|
16820
|
+
sort_order = excluded.sort_order, is_active = true, updated_at = now();
|
|
16821
|
+
END LOOP;
|
|
16822
|
+
END IF;
|
|
16823
|
+
|
|
16824
|
+
IF jsonb_typeof(p_config->'professionalAreas') = 'object' THEN
|
|
16825
|
+
DELETE FROM public.plg_agenda_turn_professional_areas pa
|
|
16826
|
+
WHERE pa.tenant_id = v_tenant
|
|
16827
|
+
AND pa.professional_id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'));
|
|
16828
|
+
INSERT INTO public.plg_agenda_turn_professional_areas (tenant_id, professional_id, area_key)
|
|
16829
|
+
SELECT v_tenant, p.id, p_config->'professionalAreas'->>p.id::text
|
|
16830
|
+
FROM public.people p
|
|
16831
|
+
WHERE p.tenant_id = v_tenant
|
|
16832
|
+
AND p.id::text IN (SELECT jsonb_object_keys(p_config->'professionalAreas'))
|
|
16833
|
+
AND nullif(p_config->'professionalAreas'->>p.id::text, '') IS NOT NULL;
|
|
16834
|
+
END IF;
|
|
16835
|
+
|
|
16836
|
+
IF jsonb_typeof(p_config->'quickServiceIds') = 'array' THEN
|
|
16837
|
+
DELETE FROM public.plg_agenda_turn_quick_services WHERE tenant_id = v_tenant;
|
|
16838
|
+
INSERT INTO public.plg_agenda_turn_quick_services (tenant_id, product_id)
|
|
16839
|
+
SELECT v_tenant, s.id FROM public.products s
|
|
16840
|
+
WHERE s.tenant_id = v_tenant
|
|
16841
|
+
AND s.id::text IN (SELECT x #>> '{}' FROM jsonb_array_elements(p_config->'quickServiceIds') x)
|
|
16842
|
+
ON CONFLICT DO NOTHING;
|
|
16843
|
+
END IF;
|
|
16844
|
+
|
|
16845
|
+
RETURN jsonb_build_object('ok', true);
|
|
16846
|
+
END $$;
|
|
16847
|
+
|
|
16848
|
+
-- Fun\xE7\xF5es de 016/017/021 com a mesma l\xF3gica, lendo a configura\xE7\xE3o pela cascata.
|
|
16849
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_set_availability(
|
|
16850
|
+
p_available boolean,
|
|
16851
|
+
p_unit_id uuid,
|
|
16852
|
+
p_latitude numeric DEFAULT NULL,
|
|
16853
|
+
p_longitude numeric DEFAULT NULL,
|
|
16854
|
+
p_accuracy_meters numeric DEFAULT NULL,
|
|
16855
|
+
p_origin text DEFAULT 'app'
|
|
16856
|
+
) RETURNS jsonb
|
|
16857
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
16858
|
+
DECLARE
|
|
16859
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16860
|
+
v_actor uuid := auth.uid();
|
|
16861
|
+
v_person uuid;
|
|
16862
|
+
v_queue public.plg_agenda_turn_queues%ROWTYPE;
|
|
16863
|
+
v_lat numeric;
|
|
16864
|
+
v_lng numeric;
|
|
16865
|
+
v_distance numeric;
|
|
16866
|
+
v_position bigint;
|
|
16867
|
+
v_entry uuid;
|
|
16868
|
+
BEGIN
|
|
16869
|
+
IF v_tenant IS NULL THEN
|
|
16870
|
+
RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501';
|
|
16871
|
+
END IF;
|
|
16872
|
+
IF p_unit_id IS NULL THEN
|
|
16873
|
+
RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023';
|
|
16874
|
+
END IF;
|
|
16875
|
+
IF p_origin <> 'app' THEN
|
|
16876
|
+
RAISE EXCEPTION 'self_availability_origin_must_be_app' USING ERRCODE = '22023';
|
|
16877
|
+
END IF;
|
|
16878
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.edit', p_unit_id) THEN
|
|
16879
|
+
RAISE EXCEPTION 'agenda.turn_queue.edit is required' USING ERRCODE = '42501';
|
|
16880
|
+
END IF;
|
|
16881
|
+
|
|
16882
|
+
v_person := app.person_for_login(v_tenant, v_actor);
|
|
16883
|
+
IF v_person IS NULL THEN
|
|
16884
|
+
RAISE EXCEPTION 'sem_ficha_de_profissional' USING ERRCODE = '42501';
|
|
16885
|
+
END IF;
|
|
16886
|
+
|
|
16887
|
+
INSERT INTO public.plg_agenda_turn_queues (tenant_id, unit_id)
|
|
16888
|
+
VALUES (v_tenant, p_unit_id)
|
|
16889
|
+
ON CONFLICT (tenant_id, unit_id, queue_key) DO NOTHING;
|
|
16890
|
+
|
|
16891
|
+
SELECT * INTO v_queue
|
|
16892
|
+
FROM public.plg_agenda_turn_queues q
|
|
16893
|
+
WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id
|
|
16894
|
+
AND q.queue_key = 'walk_in' AND public.agenda_turn__on(q, 'enabled')
|
|
16895
|
+
FOR UPDATE;
|
|
16896
|
+
IF v_queue.id IS NULL THEN
|
|
16897
|
+
RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '42501';
|
|
16898
|
+
END IF;
|
|
16899
|
+
|
|
16900
|
+
IF p_available AND public.agenda_turn__on(v_queue, 'require_location') THEN
|
|
16901
|
+
SELECT u.latitude, u.longitude INTO v_lat, v_lng
|
|
16902
|
+
FROM public.v_units u
|
|
16903
|
+
WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;
|
|
16904
|
+
IF v_lat IS NULL OR v_lng IS NULL THEN
|
|
16905
|
+
RAISE EXCEPTION 'unit_has_no_coordinates' USING ERRCODE = '22023';
|
|
16906
|
+
END IF;
|
|
16907
|
+
IF p_latitude IS NULL OR p_longitude IS NULL THEN
|
|
16908
|
+
RAISE EXCEPTION 'location_required' USING ERRCODE = '22023';
|
|
16909
|
+
END IF;
|
|
16910
|
+
v_distance := 6371000 * acos(least(1, greatest(-1,
|
|
16911
|
+
cos(radians(v_lat::double precision)) * cos(radians(p_latitude::double precision))
|
|
16912
|
+
* cos(radians(p_longitude::double precision) - radians(v_lng::double precision))
|
|
16913
|
+
+ sin(radians(v_lat::double precision)) * sin(radians(p_latitude::double precision))
|
|
16914
|
+
)));
|
|
16915
|
+
IF v_distance > public.agenda_turn__radius(v_queue) THEN
|
|
16916
|
+
RAISE EXCEPTION 'outside_geofence' USING ERRCODE = '42501';
|
|
16917
|
+
END IF;
|
|
16918
|
+
END IF;
|
|
16919
|
+
|
|
16920
|
+
SELECT coalesce(max(e.position), 0) + 1 INTO v_position
|
|
16921
|
+
FROM public.plg_agenda_turn_entries e WHERE e.queue_id = v_queue.id;
|
|
16922
|
+
|
|
16923
|
+
INSERT INTO public.plg_agenda_turn_entries
|
|
16924
|
+
(tenant_id, queue_id, unit_id, professional_id, status, position, available_at, state_changed_at, appointment_id, updated_at)
|
|
16925
|
+
VALUES
|
|
16926
|
+
(v_tenant, v_queue.id, p_unit_id, v_person,
|
|
16927
|
+
CASE WHEN p_available THEN 'available' ELSE 'unavailable' END,
|
|
16928
|
+
CASE WHEN p_available THEN v_position ELSE 0 END,
|
|
16929
|
+
CASE WHEN p_available THEN now() ELSE NULL END,
|
|
16930
|
+
now(), NULL, now())
|
|
16931
|
+
ON CONFLICT (queue_id, professional_id) DO UPDATE SET
|
|
16932
|
+
status = EXCLUDED.status,
|
|
16933
|
+
position = CASE WHEN p_available THEN v_position ELSE public.plg_agenda_turn_entries.position END,
|
|
16934
|
+
available_at = EXCLUDED.available_at,
|
|
16935
|
+
state_changed_at = now(),
|
|
16936
|
+
appointment_id = NULL,
|
|
16937
|
+
updated_at = now()
|
|
16938
|
+
RETURNING id INTO v_entry;
|
|
16939
|
+
|
|
16940
|
+
INSERT INTO public.plg_agenda_turn_events
|
|
16941
|
+
(tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id,
|
|
16942
|
+
inside_geofence, location_accuracy_meters, payload)
|
|
16943
|
+
VALUES
|
|
16944
|
+
(v_tenant, v_queue.id, p_unit_id, v_person,
|
|
16945
|
+
CASE WHEN p_available THEN 'available' ELSE 'left' END,
|
|
16946
|
+
'app', v_actor, CASE WHEN p_available THEN true ELSE NULL END,
|
|
16947
|
+
CASE WHEN p_available THEN p_accuracy_meters ELSE NULL END,
|
|
16948
|
+
jsonb_build_object('entry_id', v_entry));
|
|
16949
|
+
|
|
16950
|
+
RETURN jsonb_build_object('ok', true, 'status', CASE WHEN p_available THEN 'available' ELSE 'unavailable' END);
|
|
16951
|
+
END $$;
|
|
16952
|
+
|
|
16953
|
+
|
|
16954
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_snapshot(p_unit_id uuid)
|
|
16955
|
+
RETURNS jsonb
|
|
16956
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' STABLE AS $$
|
|
16957
|
+
DECLARE
|
|
16958
|
+
v_tenant uuid := app.current_tenant_id();
|
|
16959
|
+
v_queue public.plg_agenda_turn_queues%ROWTYPE;
|
|
16960
|
+
v_person uuid;
|
|
16961
|
+
v_timezone text;
|
|
16962
|
+
v_day date;
|
|
16963
|
+
v_entries jsonb;
|
|
16964
|
+
v_events jsonb;
|
|
16965
|
+
v_professionals jsonb;
|
|
16966
|
+
BEGIN
|
|
16967
|
+
IF p_unit_id IS NULL THEN
|
|
16968
|
+
RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023';
|
|
16969
|
+
END IF;
|
|
16970
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN
|
|
16971
|
+
RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';
|
|
16972
|
+
END IF;
|
|
16973
|
+
|
|
16974
|
+
SELECT coalesce(t.settings->>'timezone', 'America/Sao_Paulo')
|
|
16975
|
+
INTO v_timezone
|
|
16976
|
+
FROM public.tenants t
|
|
16977
|
+
WHERE t.id = v_tenant;
|
|
16978
|
+
v_day := (now() AT TIME ZONE v_timezone)::date;
|
|
16979
|
+
v_person := app.person_for_login(v_tenant, auth.uid());
|
|
16980
|
+
|
|
16981
|
+
SELECT * INTO v_queue
|
|
16982
|
+
FROM public.plg_agenda_turn_queues q
|
|
16983
|
+
WHERE q.tenant_id = v_tenant
|
|
16984
|
+
AND q.unit_id = p_unit_id
|
|
16985
|
+
AND q.queue_key = 'walk_in'
|
|
16986
|
+
AND public.agenda_turn__on(q, 'enabled');
|
|
16987
|
+
|
|
16988
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object(
|
|
16989
|
+
'id', e.id, 'queue_id', e.queue_id, 'unit_id', e.unit_id,
|
|
16990
|
+
'professional_id', e.professional_id, 'professional_name', p.name,
|
|
16991
|
+
'professional_avatar_url', p.avatar_url, 'status', e.status, 'position', e.position,
|
|
16992
|
+
'state_changed_at', e.state_changed_at, 'available_at', e.available_at,
|
|
16993
|
+
'appointment_id', e.appointment_id
|
|
16994
|
+
) ORDER BY CASE e.status WHEN 'available' THEN 0 WHEN 'serving' THEN 1 ELSE 2 END, e.position), '[]'::jsonb)
|
|
16995
|
+
INTO v_entries
|
|
16996
|
+
FROM public.plg_agenda_turn_entries e
|
|
16997
|
+
JOIN public.people p ON p.id = e.professional_id AND p.tenant_id = e.tenant_id
|
|
16998
|
+
WHERE e.queue_id = v_queue.id;
|
|
16999
|
+
|
|
17000
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object(
|
|
17001
|
+
'id', ev.id, 'professional_id', ev.professional_id, 'professional_name', p.name,
|
|
17002
|
+
'event_type', ev.event_type, 'origin', ev.origin, 'reason', ev.reason,
|
|
17003
|
+
'occurred_at', ev.occurred_at
|
|
17004
|
+
) ORDER BY ev.occurred_at DESC), '[]'::jsonb)
|
|
17005
|
+
INTO v_events
|
|
17006
|
+
FROM (
|
|
17007
|
+
SELECT * FROM public.plg_agenda_turn_events
|
|
17008
|
+
WHERE queue_id = v_queue.id
|
|
17009
|
+
AND (occurred_at AT TIME ZONE v_timezone)::date = v_day
|
|
17010
|
+
ORDER BY occurred_at DESC LIMIT 50
|
|
17011
|
+
) ev
|
|
17012
|
+
JOIN public.people p ON p.id = ev.professional_id AND p.tenant_id = ev.tenant_id;
|
|
17013
|
+
|
|
17014
|
+
WITH scheduled AS (
|
|
17015
|
+
SELECT DISTINCT s.assignee_id AS professional_id
|
|
17016
|
+
FROM public.schedules s
|
|
17017
|
+
WHERE s.tenant_id = v_tenant
|
|
17018
|
+
AND s.kind = 'working_hours'
|
|
17019
|
+
AND s.assignee_id IS NOT NULL
|
|
17020
|
+
AND coalesce(s.unit_id, s.location_id) = p_unit_id
|
|
17021
|
+
AND (s.valid_from IS NULL OR s.valid_from <= v_day)
|
|
17022
|
+
AND (s.valid_to IS NULL OR v_day <= s.valid_to)
|
|
17023
|
+
AND (
|
|
17024
|
+
(s.specific_date = v_day AND s.is_active)
|
|
17025
|
+
OR (
|
|
17026
|
+
s.specific_date IS NULL
|
|
17027
|
+
AND s.is_active
|
|
17028
|
+
AND s.day_of_week = extract(dow FROM v_day)::smallint
|
|
17029
|
+
AND public.agenda_week_parity_active(s.valid_from, v_day, s.week_interval)
|
|
17030
|
+
AND NOT EXISTS (
|
|
17031
|
+
SELECT 1 FROM public.schedules day_override
|
|
17032
|
+
WHERE day_override.tenant_id = s.tenant_id
|
|
17033
|
+
AND day_override.assignee_id = s.assignee_id
|
|
17034
|
+
AND coalesce(day_override.unit_id, day_override.location_id) = p_unit_id
|
|
17035
|
+
AND day_override.specific_date = v_day
|
|
17036
|
+
)
|
|
17037
|
+
)
|
|
17038
|
+
)
|
|
17039
|
+
), booked AS (
|
|
17040
|
+
SELECT a.assignee_id AS professional_id,
|
|
17041
|
+
count(*)::integer AS appointment_count,
|
|
17042
|
+
min(a.starts_at) FILTER (WHERE a.starts_at >= now()) AS next_appointment_at
|
|
17043
|
+
FROM public.appointments a
|
|
17044
|
+
WHERE a.tenant_id = v_tenant
|
|
17045
|
+
AND a.assignee_id IS NOT NULL
|
|
17046
|
+
AND coalesce(a.unit_id, a.location_id) = p_unit_id
|
|
17047
|
+
AND (a.starts_at AT TIME ZONE v_timezone)::date = v_day
|
|
17048
|
+
AND coalesce(a.status, '') NOT IN ('cancelled', 'canceled')
|
|
17049
|
+
GROUP BY a.assignee_id
|
|
17050
|
+
), candidates AS (
|
|
17051
|
+
SELECT professional_id FROM scheduled
|
|
17052
|
+
UNION
|
|
17053
|
+
SELECT professional_id FROM booked
|
|
17054
|
+
UNION
|
|
17055
|
+
SELECT e.professional_id FROM public.plg_agenda_turn_entries e WHERE e.queue_id = v_queue.id
|
|
17056
|
+
)
|
|
17057
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object(
|
|
17058
|
+
'professional_id', p.id,
|
|
17059
|
+
'professional_name', p.name,
|
|
17060
|
+
'professional_avatar_url', p.avatar_url,
|
|
17061
|
+
'status', coalesce(e.status, 'unavailable'),
|
|
17062
|
+
'position', coalesce(e.position, 0),
|
|
17063
|
+
'last_checkin_at', last_checkin.occurred_at,
|
|
17064
|
+
'is_scheduled', scheduled.professional_id IS NOT NULL,
|
|
17065
|
+
'appointment_count', coalesce(booked.appointment_count, 0),
|
|
17066
|
+
'next_appointment_at', booked.next_appointment_at
|
|
17067
|
+
) ORDER BY
|
|
17068
|
+
CASE coalesce(e.status, 'unavailable') WHEN 'available' THEN 0 WHEN 'serving' THEN 1 ELSE 2 END,
|
|
17069
|
+
CASE WHEN booked.next_appointment_at IS NOT NULL THEN 0 ELSE 1 END,
|
|
17070
|
+
e.position NULLS LAST,
|
|
17071
|
+
p.name), '[]'::jsonb)
|
|
17072
|
+
INTO v_professionals
|
|
17073
|
+
FROM candidates c
|
|
17074
|
+
JOIN public.people p ON p.id = c.professional_id AND p.tenant_id = v_tenant AND p.is_active
|
|
17075
|
+
LEFT JOIN scheduled ON scheduled.professional_id = p.id
|
|
17076
|
+
LEFT JOIN booked ON booked.professional_id = p.id
|
|
17077
|
+
LEFT JOIN public.plg_agenda_turn_entries e ON e.queue_id = v_queue.id AND e.professional_id = p.id
|
|
17078
|
+
LEFT JOIN LATERAL (
|
|
17079
|
+
SELECT ev.occurred_at
|
|
17080
|
+
FROM public.plg_agenda_turn_events ev
|
|
17081
|
+
WHERE ev.queue_id = v_queue.id
|
|
17082
|
+
AND ev.professional_id = p.id
|
|
17083
|
+
AND ev.event_type IN ('available', 'override_available')
|
|
17084
|
+
AND (ev.occurred_at AT TIME ZONE v_timezone)::date = v_day
|
|
17085
|
+
ORDER BY ev.occurred_at DESC
|
|
17086
|
+
LIMIT 1
|
|
17087
|
+
) last_checkin ON true;
|
|
17088
|
+
|
|
17089
|
+
RETURN jsonb_build_object(
|
|
17090
|
+
'queue_id', v_queue.id,
|
|
17091
|
+
'unit_id', p_unit_id,
|
|
17092
|
+
'queue_name', coalesce(v_queue.name, 'Pr\xF3ximo da vez'),
|
|
17093
|
+
'current_professional_id', v_person,
|
|
17094
|
+
'date', v_day,
|
|
17095
|
+
'professionals', v_professionals,
|
|
17096
|
+
'entries', v_entries,
|
|
17097
|
+
'events', v_events
|
|
17098
|
+
);
|
|
17099
|
+
END $$;
|
|
17100
|
+
|
|
17101
|
+
|
|
17102
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_override(
|
|
17103
|
+
p_professional_id uuid,
|
|
17104
|
+
p_unit_id uuid,
|
|
17105
|
+
p_available boolean,
|
|
17106
|
+
p_reason text
|
|
17107
|
+
) RETURNS jsonb
|
|
17108
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
17109
|
+
DECLARE
|
|
17110
|
+
v_tenant uuid := app.current_tenant_id();
|
|
17111
|
+
v_queue public.plg_agenda_turn_queues%ROWTYPE;
|
|
17112
|
+
v_position bigint;
|
|
17113
|
+
BEGIN
|
|
17114
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.manage', p_unit_id) THEN
|
|
17115
|
+
RAISE EXCEPTION 'agenda.turn_queue.manage is required' USING ERRCODE = '42501';
|
|
17116
|
+
END IF;
|
|
17117
|
+
IF nullif(trim(p_reason), '') IS NULL THEN
|
|
17118
|
+
RAISE EXCEPTION 'override_reason_required' USING ERRCODE = '22023';
|
|
17119
|
+
END IF;
|
|
17120
|
+
SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
|
|
17121
|
+
WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' AND public.agenda_turn__on(q, 'enabled')
|
|
17122
|
+
FOR UPDATE;
|
|
17123
|
+
IF v_queue.id IS NULL THEN RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '42501'; END IF;
|
|
17124
|
+
SELECT coalesce(max(position), 0) + 1 INTO v_position FROM public.plg_agenda_turn_entries WHERE queue_id = v_queue.id;
|
|
17125
|
+
INSERT INTO public.plg_agenda_turn_entries
|
|
17126
|
+
(tenant_id, queue_id, unit_id, professional_id, status, position, available_at, state_changed_at, updated_at)
|
|
17127
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id,
|
|
17128
|
+
CASE WHEN p_available THEN 'available' ELSE 'unavailable' END,
|
|
17129
|
+
CASE WHEN p_available THEN v_position ELSE 0 END,
|
|
17130
|
+
CASE WHEN p_available THEN now() ELSE NULL END, now(), now())
|
|
17131
|
+
ON CONFLICT (queue_id, professional_id) DO UPDATE SET
|
|
17132
|
+
status = EXCLUDED.status, position = CASE WHEN p_available THEN v_position ELSE public.plg_agenda_turn_entries.position END,
|
|
17133
|
+
available_at = EXCLUDED.available_at, state_changed_at = now(), appointment_id = NULL, updated_at = now();
|
|
17134
|
+
INSERT INTO public.plg_agenda_turn_events
|
|
17135
|
+
(tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, reason)
|
|
17136
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id,
|
|
17137
|
+
CASE WHEN p_available THEN 'override_available' ELSE 'override_left' END,
|
|
17138
|
+
'frontdesk', auth.uid(), trim(p_reason));
|
|
17139
|
+
RETURN jsonb_build_object('ok', true);
|
|
17140
|
+
END $$;
|
|
17141
|
+
|
|
17142
|
+
|
|
17143
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_on_appointment_status()
|
|
17144
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
17145
|
+
DECLARE
|
|
17146
|
+
v_unit uuid := coalesce(NEW.unit_id, NEW.location_id);
|
|
17147
|
+
v_queue public.plg_agenda_turn_queues;
|
|
17148
|
+
v_entry public.plg_agenda_turn_entries;
|
|
17149
|
+
v_tz text;
|
|
17150
|
+
v_pos bigint;
|
|
17151
|
+
v_quick boolean;
|
|
17152
|
+
BEGIN
|
|
17153
|
+
IF NEW.assignee_id IS NULL OR v_unit IS NULL OR NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;
|
|
17154
|
+
IF NEW.status <> 'in_progress' AND OLD.status IS DISTINCT FROM 'in_progress' THEN RETURN NEW; END IF;
|
|
17155
|
+
|
|
17156
|
+
SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
|
|
17157
|
+
WHERE q.tenant_id = NEW.tenant_id AND q.unit_id = v_unit AND q.queue_key = 'walk_in'
|
|
17158
|
+
AND public.agenda_turn__on(q, 'enabled') AND public.agenda_turn__on(q, 'follows_appointments')
|
|
17159
|
+
FOR UPDATE;
|
|
17160
|
+
IF v_queue.id IS NULL THEN RETURN NEW; END IF;
|
|
17161
|
+
v_tz := public.agenda_turn__tz(NEW.tenant_id, v_unit);
|
|
17162
|
+
|
|
17163
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
17164
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = NEW.assignee_id AND e.status <> 'unavailable'
|
|
17165
|
+
AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;
|
|
17166
|
+
IF v_entry.id IS NULL THEN RETURN NEW; END IF;
|
|
17167
|
+
|
|
17168
|
+
IF NEW.status = 'in_progress' THEN
|
|
17169
|
+
IF v_entry.status = 'serving' THEN RETURN NEW; END IF;
|
|
17170
|
+
SELECT bool_and(q.product_id IS NOT NULL) INTO v_quick
|
|
17171
|
+
FROM public.appointment_items ai
|
|
17172
|
+
LEFT JOIN public.plg_agenda_turn_quick_services q ON q.tenant_id = NEW.tenant_id AND q.product_id = ai.service_id
|
|
17173
|
+
WHERE ai.booking_id = NEW.id;
|
|
17174
|
+
IF coalesce(v_quick, false) THEN
|
|
17175
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)
|
|
17176
|
+
VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'quick_service', 'automatic', NEW.id, v_entry.area_key);
|
|
17177
|
+
RETURN NEW;
|
|
17178
|
+
END IF;
|
|
17179
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, appointment_id = NEW.id,
|
|
17180
|
+
previous_position = position, state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17181
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
17182
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key)
|
|
17183
|
+
VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'started', 'automatic', NEW.id, v_entry.area_key);
|
|
17184
|
+
ELSIF v_entry.status = 'serving' AND v_entry.appointment_id = NEW.id THEN
|
|
17185
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);
|
|
17186
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,
|
|
17187
|
+
available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17188
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id, area_key, payload)
|
|
17189
|
+
VALUES (NEW.tenant_id, v_queue.id, v_unit, NEW.assignee_id, 'finished', 'automatic', NEW.id, v_entry.area_key,
|
|
17190
|
+
jsonb_build_object('status', NEW.status, 'to_position', v_pos));
|
|
17191
|
+
END IF;
|
|
17192
|
+
RETURN NEW;
|
|
17193
|
+
EXCEPTION WHEN others THEN
|
|
17194
|
+
RAISE WARNING 'agenda_turn_queue_on_appointment_status: %', SQLERRM;
|
|
17195
|
+
RETURN NEW;
|
|
17196
|
+
END $$;
|
|
17197
|
+
|
|
17198
|
+
|
|
17199
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_board(p_unit_id uuid)
|
|
17200
|
+
RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
17201
|
+
DECLARE
|
|
17202
|
+
v_tenant uuid := app.current_tenant_id();
|
|
17203
|
+
v_queue public.plg_agenda_turn_queues;
|
|
17204
|
+
v_tz text;
|
|
17205
|
+
v_day date;
|
|
17206
|
+
v_lat numeric;
|
|
17207
|
+
v_lng numeric;
|
|
17208
|
+
v_result jsonb;
|
|
17209
|
+
BEGIN
|
|
17210
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
17211
|
+
IF p_unit_id IS NULL THEN RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023'; END IF;
|
|
17212
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN
|
|
17213
|
+
RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';
|
|
17214
|
+
END IF;
|
|
17215
|
+
v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);
|
|
17216
|
+
v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);
|
|
17217
|
+
v_day := (now() AT TIME ZONE v_tz)::date;
|
|
17218
|
+
SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;
|
|
17219
|
+
|
|
17220
|
+
WITH ent AS (
|
|
17221
|
+
SELECT e.*, row_number() OVER (PARTITION BY e.area_key, (e.status IN ('available', 'away'))
|
|
17222
|
+
ORDER BY e.position, e.joined_at, e.id) AS rnk
|
|
17223
|
+
FROM public.plg_agenda_turn_entries e
|
|
17224
|
+
WHERE e.queue_id = v_queue.id AND e.status IN ('available', 'away', 'serving')
|
|
17225
|
+
AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = v_day
|
|
17226
|
+
), appts AS (
|
|
17227
|
+
SELECT a.assignee_id AS professional_id, count(*)::int AS n,
|
|
17228
|
+
min(a.starts_at) FILTER (WHERE a.starts_at >= now() AND a.status NOT IN ('completed', 'cancelled', 'no_show')) AS proximo
|
|
17229
|
+
FROM public.appointments a
|
|
17230
|
+
WHERE a.tenant_id = v_tenant AND a.assignee_id IS NOT NULL
|
|
17231
|
+
AND coalesce(a.unit_id, a.location_id) = p_unit_id
|
|
17232
|
+
AND (a.starts_at AT TIME ZONE v_tz)::date = v_day
|
|
17233
|
+
AND a.status <> 'cancelled'
|
|
17234
|
+
GROUP BY 1
|
|
17235
|
+
), roster AS (
|
|
17236
|
+
SELECT p.id, p.name, p.avatar_url, public.agenda_turn__area_of(v_tenant, p.id) AS area
|
|
17237
|
+
FROM public.people p
|
|
17238
|
+
WHERE p.tenant_id = v_tenant AND p.is_active
|
|
17239
|
+
AND (public.agenda_turn__in_unit(v_tenant, p_unit_id, p.id)
|
|
17240
|
+
OR EXISTS (SELECT 1 FROM appts t WHERE t.professional_id = p.id))
|
|
17241
|
+
), served AS (
|
|
17242
|
+
SELECT ev.professional_id,
|
|
17243
|
+
count(*) FILTER (WHERE ev.event_type IN ('started', 'advanced_to_service'))::int AS atendimentos
|
|
17244
|
+
FROM public.plg_agenda_turn_events ev
|
|
17245
|
+
WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day
|
|
17246
|
+
GROUP BY 1
|
|
17247
|
+
)
|
|
17248
|
+
SELECT jsonb_build_object(
|
|
17249
|
+
'unitId', p_unit_id,
|
|
17250
|
+
'today', v_day,
|
|
17251
|
+
'enabled', coalesce(public.agenda_turn__on(v_queue, 'enabled'), false),
|
|
17252
|
+
'selfCheckin', coalesce(public.agenda_turn__on(v_queue, 'self_checkin'), true),
|
|
17253
|
+
'auto', coalesce(public.agenda_turn__on(v_queue, 'follows_appointments'), true),
|
|
17254
|
+
'requireLocation', coalesce(public.agenda_turn__on(v_queue, 'require_location'), false),
|
|
17255
|
+
'locationConfigured', v_lat IS NOT NULL AND v_lng IS NOT NULL,
|
|
17256
|
+
'areas', coalesce((SELECT jsonb_agg(jsonb_build_object('key', d.key, 'name', d.name, 'color', d.color) ORDER BY d.sort_order, d.key)
|
|
17257
|
+
FROM public.agenda_turn__area_defs(v_tenant) d), '[]'::jsonb),
|
|
17258
|
+
'entries', coalesce((
|
|
17259
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
17260
|
+
'professionalId', e.professional_id, 'name', p.name, 'avatarUrl', p.avatar_url,
|
|
17261
|
+
'area', coalesce(e.area_key, 'geral'), 'status', e.status,
|
|
17262
|
+
'rank', CASE WHEN e.status IN ('available', 'away') THEN e.rnk END,
|
|
17263
|
+
'awayReason', e.away_reason, 'since', e.state_changed_at, 'joinedAt', e.joined_at,
|
|
17264
|
+
'appointmentId', e.appointment_id,
|
|
17265
|
+
'servedToday', coalesce(s.atendimentos, 0)
|
|
17266
|
+
) ORDER BY e.area_key, (e.status = 'serving'), e.rnk)
|
|
17267
|
+
FROM ent e JOIN public.people p ON p.id = e.professional_id
|
|
17268
|
+
LEFT JOIN served s ON s.professional_id = e.professional_id), '[]'::jsonb),
|
|
17269
|
+
'roster', coalesce((
|
|
17270
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
17271
|
+
'professionalId', r.id, 'name', r.name, 'avatarUrl', r.avatar_url, 'area', r.area,
|
|
17272
|
+
'appointmentsToday', coalesce(t.n, 0), 'nextAppointmentAt', t.proximo,
|
|
17273
|
+
'servedToday', coalesce(s.atendimentos, 0)) ORDER BY r.name)
|
|
17274
|
+
FROM roster r
|
|
17275
|
+
LEFT JOIN appts t ON t.professional_id = r.id
|
|
17276
|
+
LEFT JOIN served s ON s.professional_id = r.id
|
|
17277
|
+
WHERE NOT EXISTS (SELECT 1 FROM ent e WHERE e.professional_id = r.id)), '[]'::jsonb),
|
|
17278
|
+
'events', coalesce((
|
|
17279
|
+
SELECT jsonb_agg(x.item ORDER BY x.at DESC) FROM (
|
|
17280
|
+
SELECT jsonb_build_object('id', ev.id, 'professionalId', ev.professional_id, 'name', ep.name,
|
|
17281
|
+
'type', ev.event_type, 'origin', ev.origin, 'area', ev.area_key,
|
|
17282
|
+
'reason', coalesce(ev.reason, ev.payload->>'reason'), 'createdAt', ev.occurred_at) AS item,
|
|
17283
|
+
ev.occurred_at AS at
|
|
17284
|
+
FROM public.plg_agenda_turn_events ev
|
|
17285
|
+
JOIN public.people ep ON ep.id = ev.professional_id
|
|
17286
|
+
WHERE ev.queue_id = v_queue.id AND (ev.occurred_at AT TIME ZONE v_tz)::date = v_day
|
|
17287
|
+
ORDER BY ev.occurred_at DESC LIMIT 60) x), '[]'::jsonb)
|
|
17288
|
+
) INTO v_result;
|
|
17289
|
+
RETURN v_result;
|
|
17290
|
+
END $$;
|
|
17291
|
+
|
|
17292
|
+
|
|
17293
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_act(
|
|
17294
|
+
p_unit_id uuid,
|
|
17295
|
+
p_professional_id uuid,
|
|
17296
|
+
p_action text,
|
|
17297
|
+
p_position integer DEFAULT NULL,
|
|
17298
|
+
p_reason text DEFAULT NULL
|
|
17299
|
+
) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
17300
|
+
DECLARE
|
|
17301
|
+
v_tenant uuid := app.current_tenant_id();
|
|
17302
|
+
v_queue public.plg_agenda_turn_queues;
|
|
17303
|
+
v_entry public.plg_agenda_turn_entries;
|
|
17304
|
+
v_need text := CASE p_action WHEN 'join' THEN 'create' WHEN 'leave' THEN 'delete' ELSE 'edit' END;
|
|
17305
|
+
v_reason text := nullif(btrim(coalesce(p_reason, '')), '');
|
|
17306
|
+
v_area text;
|
|
17307
|
+
v_pos bigint;
|
|
17308
|
+
v_ids uuid[];
|
|
17309
|
+
v_users uuid[];
|
|
17310
|
+
BEGIN
|
|
17311
|
+
IF v_tenant IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
17312
|
+
IF p_action NOT IN ('join', 'leave', 'away', 'back', 'move', 'start', 'finish', 'skip', 'call') THEN
|
|
17313
|
+
RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';
|
|
17314
|
+
END IF;
|
|
17315
|
+
IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.' || v_need, p_unit_id) THEN
|
|
17316
|
+
RAISE EXCEPTION 'agenda.turn_queue.% is required', v_need USING ERRCODE = '42501';
|
|
17317
|
+
END IF;
|
|
17318
|
+
IF NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = p_professional_id AND p.tenant_id = v_tenant AND p.is_active) THEN
|
|
17319
|
+
RAISE EXCEPTION 'turn_queue_invalid_professional' USING ERRCODE = '22023';
|
|
17320
|
+
END IF;
|
|
17321
|
+
|
|
17322
|
+
SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
|
|
17323
|
+
WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;
|
|
17324
|
+
IF v_queue.id IS NULL OR NOT public.agenda_turn__on(v_queue, 'enabled') THEN
|
|
17325
|
+
RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023';
|
|
17326
|
+
END IF;
|
|
17327
|
+
PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));
|
|
17328
|
+
|
|
17329
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
17330
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = p_professional_id;
|
|
17331
|
+
IF v_entry.id IS NOT NULL AND v_entry.status = 'unavailable' THEN v_entry := NULL; END IF;
|
|
17332
|
+
|
|
17333
|
+
IF p_action = 'join' THEN
|
|
17334
|
+
IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17335
|
+
v_area := public.agenda_turn__area_of(v_tenant, p_professional_id);
|
|
17336
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_area);
|
|
17337
|
+
INSERT INTO public.plg_agenda_turn_entries
|
|
17338
|
+
(tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, appointment_id, away_reason, updated_at)
|
|
17339
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'available', v_pos, v_area, now(), now(), now(), NULL, NULL, now())
|
|
17340
|
+
ON CONFLICT (queue_id, professional_id) DO UPDATE SET
|
|
17341
|
+
status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),
|
|
17342
|
+
state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();
|
|
17343
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
17344
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'joined', 'frontdesk', auth.uid(), v_area, jsonb_build_object('to_position', v_pos));
|
|
17345
|
+
RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area, 'position', v_pos);
|
|
17346
|
+
END IF;
|
|
17347
|
+
|
|
17348
|
+
IF p_action = 'call' THEN
|
|
17349
|
+
SELECT array_agg(DISTINCT m.user_id) INTO v_users FROM app.memberships m
|
|
17350
|
+
WHERE m.tenant_id = v_tenant AND m.person_id = p_professional_id AND m.active;
|
|
17351
|
+
IF v_users IS NOT NULL THEN
|
|
17352
|
+
INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)
|
|
17353
|
+
SELECT v_tenant, u, 'Cliente para voc\xEA na recep\xE7\xE3o', coalesce(v_reason, 'Voc\xEA \xE9 o pr\xF3ximo da vez.'),
|
|
17354
|
+
'turn_queue_call', '#/agenda', 'person', p_professional_id
|
|
17355
|
+
FROM unnest(v_users) u;
|
|
17356
|
+
END IF;
|
|
17357
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)
|
|
17358
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'called', 'frontdesk', auth.uid(),
|
|
17359
|
+
coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, p_professional_id)), v_reason,
|
|
17360
|
+
jsonb_build_object('notified', v_users IS NOT NULL));
|
|
17361
|
+
RETURN jsonb_build_object('ok', true, 'changed', true, 'notified', v_users IS NOT NULL);
|
|
17362
|
+
END IF;
|
|
17363
|
+
|
|
17364
|
+
IF v_entry.id IS NULL THEN RAISE EXCEPTION 'turn_queue_not_in_queue' USING ERRCODE = '22023'; END IF;
|
|
17365
|
+
|
|
17366
|
+
IF p_action = 'leave' THEN
|
|
17367
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,
|
|
17368
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17369
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
17370
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
17371
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'left', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));
|
|
17372
|
+
|
|
17373
|
+
ELSIF p_action = 'away' THEN
|
|
17374
|
+
IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;
|
|
17375
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),
|
|
17376
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17377
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)
|
|
17378
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'away', 'frontdesk', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'),
|
|
17379
|
+
jsonb_build_object('from_position', v_entry.position));
|
|
17380
|
+
|
|
17381
|
+
ELSIF p_action = 'back' THEN
|
|
17382
|
+
IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17383
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,
|
|
17384
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17385
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
17386
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'back', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_entry.position));
|
|
17387
|
+
|
|
17388
|
+
ELSIF p_action = 'move' THEN
|
|
17389
|
+
IF v_entry.status = 'serving' THEN RAISE EXCEPTION 'turn_queue_in_service' USING ERRCODE = '22023'; END IF;
|
|
17390
|
+
IF p_position IS NULL OR p_position < 1 THEN RAISE EXCEPTION 'turn_queue_invalid_position' USING ERRCODE = '22023'; END IF;
|
|
17391
|
+
SELECT array_agg(id ORDER BY position, joined_at, id) INTO v_ids FROM public.plg_agenda_turn_entries
|
|
17392
|
+
WHERE queue_id = v_queue.id AND area_key IS NOT DISTINCT FROM v_entry.area_key
|
|
17393
|
+
AND status IN ('available', 'away') AND id <> v_entry.id;
|
|
17394
|
+
v_ids := coalesce(v_ids, ARRAY[]::uuid[]);
|
|
17395
|
+
v_pos := least(p_position, coalesce(array_length(v_ids, 1), 0) + 1);
|
|
17396
|
+
v_ids := v_ids[1:v_pos - 1] || v_entry.id || v_ids[v_pos:];
|
|
17397
|
+
UPDATE public.plg_agenda_turn_entries e SET position = o.ord, updated_at = now()
|
|
17398
|
+
FROM unnest(v_ids) WITH ORDINALITY o(id, ord) WHERE e.id = o.id;
|
|
17399
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
17400
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'moved', 'frontdesk', auth.uid(), v_entry.area_key,
|
|
17401
|
+
jsonb_build_object('from_position', v_entry.position, 'to_position', v_pos));
|
|
17402
|
+
|
|
17403
|
+
ELSIF p_action = 'start' THEN
|
|
17404
|
+
IF v_entry.status = 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17405
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'serving', away_reason = NULL, previous_position = position,
|
|
17406
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17407
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
17408
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
17409
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'started', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('from_position', v_entry.position));
|
|
17410
|
+
|
|
17411
|
+
ELSIF p_action = 'finish' THEN
|
|
17412
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);
|
|
17413
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL, appointment_id = NULL,
|
|
17414
|
+
position = v_pos, available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17415
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
17416
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
17417
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'finished', 'frontdesk', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));
|
|
17418
|
+
|
|
17419
|
+
ELSIF p_action = 'skip' THEN
|
|
17420
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason, payload)
|
|
17421
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id, 'skipped', 'frontdesk', auth.uid(), v_entry.area_key, v_reason,
|
|
17422
|
+
jsonb_build_object('from_position', v_entry.position));
|
|
17423
|
+
END IF;
|
|
17424
|
+
|
|
17425
|
+
RETURN jsonb_build_object('ok', true, 'changed', true);
|
|
17426
|
+
END $$;
|
|
17427
|
+
|
|
17428
|
+
|
|
17429
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_state(p_unit_id uuid)
|
|
17430
|
+
RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
17431
|
+
DECLARE
|
|
17432
|
+
v_tenant uuid := app.current_tenant_id();
|
|
17433
|
+
v_person uuid;
|
|
17434
|
+
v_queue public.plg_agenda_turn_queues;
|
|
17435
|
+
v_entry public.plg_agenda_turn_entries;
|
|
17436
|
+
v_tz text;
|
|
17437
|
+
v_rank bigint;
|
|
17438
|
+
BEGIN
|
|
17439
|
+
IF v_tenant IS NULL OR auth.uid() IS NULL OR p_unit_id IS NULL THEN RETURN NULL; END IF;
|
|
17440
|
+
SELECT m.person_id INTO v_person FROM app.memberships m
|
|
17441
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;
|
|
17442
|
+
IF v_person IS NULL OR NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN RETURN NULL; END IF;
|
|
17443
|
+
v_queue := public.agenda_turn__queue(v_tenant, p_unit_id);
|
|
17444
|
+
IF v_queue.id IS NULL OR NOT public.agenda_turn__on(v_queue, 'enabled') THEN RETURN NULL; END IF;
|
|
17445
|
+
v_tz := public.agenda_turn__tz(v_tenant, p_unit_id);
|
|
17446
|
+
|
|
17447
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
17448
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable'
|
|
17449
|
+
AND (coalesce(e.joined_at, e.available_at, e.state_changed_at) AT TIME ZONE v_tz)::date = (now() AT TIME ZONE v_tz)::date;
|
|
17450
|
+
IF v_entry.id IS NOT NULL AND v_entry.status IN ('available', 'away') THEN
|
|
17451
|
+
SELECT count(*) + 1 INTO v_rank FROM public.plg_agenda_turn_entries o
|
|
17452
|
+
WHERE o.queue_id = v_queue.id AND o.area_key IS NOT DISTINCT FROM v_entry.area_key
|
|
17453
|
+
AND o.status IN ('available', 'away') AND o.id <> v_entry.id
|
|
17454
|
+
AND (o.position, o.joined_at, o.id) < (v_entry.position, v_entry.joined_at, v_entry.id);
|
|
17455
|
+
END IF;
|
|
17456
|
+
RETURN jsonb_build_object(
|
|
17457
|
+
'professionalId', v_person,
|
|
17458
|
+
'selfCheckin', public.agenda_turn__on(v_queue, 'self_checkin'),
|
|
17459
|
+
'requireLocation', public.agenda_turn__on(v_queue, 'require_location'),
|
|
17460
|
+
'inQueue', v_entry.id IS NOT NULL,
|
|
17461
|
+
'area', coalesce(v_entry.area_key, public.agenda_turn__area_of(v_tenant, v_person)),
|
|
17462
|
+
'status', v_entry.status,
|
|
17463
|
+
'awayReason', v_entry.away_reason,
|
|
17464
|
+
'rank', v_rank);
|
|
17465
|
+
END $$;
|
|
17466
|
+
|
|
17467
|
+
|
|
17468
|
+
CREATE OR REPLACE FUNCTION public.agenda_turn_queue_my_action(
|
|
17469
|
+
p_unit_id uuid,
|
|
17470
|
+
p_action text,
|
|
17471
|
+
p_latitude double precision DEFAULT NULL,
|
|
17472
|
+
p_longitude double precision DEFAULT NULL,
|
|
17473
|
+
p_accuracy_m double precision DEFAULT NULL,
|
|
17474
|
+
p_reason text DEFAULT NULL
|
|
17475
|
+
) RETURNS jsonb LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
17476
|
+
DECLARE
|
|
17477
|
+
v_tenant uuid := app.current_tenant_id();
|
|
17478
|
+
v_person uuid;
|
|
17479
|
+
v_queue public.plg_agenda_turn_queues;
|
|
17480
|
+
v_entry public.plg_agenda_turn_entries;
|
|
17481
|
+
v_reason text := nullif(btrim(coalesce(p_reason, '')), '');
|
|
17482
|
+
v_lat numeric;
|
|
17483
|
+
v_lng numeric;
|
|
17484
|
+
v_distance double precision;
|
|
17485
|
+
v_area text;
|
|
17486
|
+
v_pos bigint;
|
|
17487
|
+
BEGIN
|
|
17488
|
+
IF v_tenant IS NULL OR auth.uid() IS NULL THEN RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501'; END IF;
|
|
17489
|
+
IF p_action NOT IN ('join', 'leave', 'away', 'back', 'finish') THEN
|
|
17490
|
+
RAISE EXCEPTION 'turn_queue_invalid_action:%', p_action USING ERRCODE = '22023';
|
|
17491
|
+
END IF;
|
|
17492
|
+
SELECT m.person_id INTO v_person FROM app.memberships m
|
|
17493
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.person_id IS NOT NULL LIMIT 1;
|
|
17494
|
+
IF v_person IS NULL THEN RAISE EXCEPTION 'turn_queue_professional_not_linked' USING ERRCODE = '42501'; END IF;
|
|
17495
|
+
IF NOT public.agenda_turn__in_unit(v_tenant, p_unit_id, v_person) THEN
|
|
17496
|
+
RAISE EXCEPTION 'turn_queue_professional_outside_unit' USING ERRCODE = '42501';
|
|
17497
|
+
END IF;
|
|
17498
|
+
SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
|
|
17499
|
+
WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' FOR UPDATE;
|
|
17500
|
+
IF v_queue.id IS NULL OR NOT public.agenda_turn__on(v_queue, 'enabled') THEN RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '22023'; END IF;
|
|
17501
|
+
IF NOT public.agenda_turn__on(v_queue, 'self_checkin') THEN RAISE EXCEPTION 'turn_queue_checkin_by_frontdesk' USING ERRCODE = '22023'; END IF;
|
|
17502
|
+
PERFORM public.agenda_turn__purge_stale(v_queue.id, public.agenda_turn__tz(v_tenant, p_unit_id));
|
|
17503
|
+
|
|
17504
|
+
SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
|
|
17505
|
+
WHERE e.queue_id = v_queue.id AND e.professional_id = v_person AND e.status <> 'unavailable';
|
|
17506
|
+
|
|
17507
|
+
IF p_action = 'join' THEN
|
|
17508
|
+
IF v_entry.id IS NOT NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17509
|
+
IF public.agenda_turn__on(v_queue, 'require_location') THEN
|
|
17510
|
+
SELECT u.latitude, u.longitude INTO v_lat, v_lng FROM public.v_units u WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;
|
|
17511
|
+
IF v_lat IS NULL OR v_lng IS NULL THEN RAISE EXCEPTION 'unit_has_no_coordinates' USING ERRCODE = '22023'; END IF;
|
|
17512
|
+
IF p_latitude IS NULL OR p_longitude IS NULL THEN RAISE EXCEPTION 'location_required' USING ERRCODE = '22023'; END IF;
|
|
17513
|
+
v_distance := 6371000 * acos(least(1, greatest(-1,
|
|
17514
|
+
cos(radians(v_lat::double precision)) * cos(radians(p_latitude)) * cos(radians(p_longitude) - radians(v_lng::double precision))
|
|
17515
|
+
+ sin(radians(v_lat::double precision)) * sin(radians(p_latitude)))));
|
|
17516
|
+
IF v_distance > public.agenda_turn__radius(v_queue) + least(coalesce(p_accuracy_m, 0), 100) THEN
|
|
17517
|
+
RAISE EXCEPTION 'outside_geofence' USING ERRCODE = '42501';
|
|
17518
|
+
END IF;
|
|
17519
|
+
END IF;
|
|
17520
|
+
v_area := public.agenda_turn__area_of(v_tenant, v_person);
|
|
17521
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_area);
|
|
17522
|
+
INSERT INTO public.plg_agenda_turn_entries
|
|
17523
|
+
(tenant_id, queue_id, unit_id, professional_id, status, position, area_key, joined_at, available_at, state_changed_at, updated_at)
|
|
17524
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'available', v_pos, v_area, now(), now(), now(), now())
|
|
17525
|
+
ON CONFLICT (queue_id, professional_id) DO UPDATE SET
|
|
17526
|
+
status = 'available', position = v_pos, area_key = v_area, joined_at = now(), available_at = now(),
|
|
17527
|
+
state_changed_at = now(), appointment_id = NULL, away_reason = NULL, updated_at = now();
|
|
17528
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key,
|
|
17529
|
+
inside_geofence, location_accuracy_meters, payload)
|
|
17530
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'joined', 'app', auth.uid(), v_area,
|
|
17531
|
+
CASE WHEN public.agenda_turn__on(v_queue, 'require_location') THEN true END, p_accuracy_m, jsonb_build_object('to_position', v_pos));
|
|
17532
|
+
RETURN jsonb_build_object('ok', true, 'changed', true, 'area', v_area);
|
|
17533
|
+
END IF;
|
|
17534
|
+
|
|
17535
|
+
IF v_entry.id IS NULL THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17536
|
+
|
|
17537
|
+
IF p_action = 'leave' THEN
|
|
17538
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', away_reason = NULL, appointment_id = NULL,
|
|
17539
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17540
|
+
PERFORM public.agenda_turn__renumber(v_queue.id, v_entry.area_key);
|
|
17541
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)
|
|
17542
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'left', 'app', auth.uid(), v_entry.area_key);
|
|
17543
|
+
ELSIF p_action = 'away' THEN
|
|
17544
|
+
IF v_entry.status <> 'available' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17545
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'away', away_reason = coalesce(v_reason, 'Ausente'),
|
|
17546
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17547
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, reason)
|
|
17548
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'away', 'app', auth.uid(), v_entry.area_key, coalesce(v_reason, 'Ausente'));
|
|
17549
|
+
ELSIF p_action = 'back' THEN
|
|
17550
|
+
IF v_entry.status <> 'away' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17551
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', away_reason = NULL,
|
|
17552
|
+
state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17553
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key)
|
|
17554
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'back', 'app', auth.uid(), v_entry.area_key);
|
|
17555
|
+
ELSIF p_action = 'finish' THEN
|
|
17556
|
+
IF v_entry.status <> 'serving' THEN RETURN jsonb_build_object('ok', true, 'changed', false); END IF;
|
|
17557
|
+
v_pos := public.agenda_turn__next_position(v_queue.id, v_entry.area_key);
|
|
17558
|
+
UPDATE public.plg_agenda_turn_entries SET status = 'available', appointment_id = NULL, position = v_pos,
|
|
17559
|
+
available_at = now(), state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
|
|
17560
|
+
INSERT INTO public.plg_agenda_turn_events (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, area_key, payload)
|
|
17561
|
+
VALUES (v_tenant, v_queue.id, p_unit_id, v_person, 'finished', 'app', auth.uid(), v_entry.area_key, jsonb_build_object('to_position', v_pos));
|
|
17562
|
+
END IF;
|
|
17563
|
+
RETURN jsonb_build_object('ok', true, 'changed', true);
|
|
17564
|
+
END $$;
|
|
17565
|
+
|
|
17566
|
+
|
|
17567
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__on(public.plg_agenda_turn_queues, text) FROM PUBLIC, anon, authenticated;
|
|
17568
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__radius(public.plg_agenda_turn_queues) FROM PUBLIC, anon, authenticated;
|
|
17569
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__unit_cfg(uuid, uuid) FROM PUBLIC, anon, authenticated;
|
|
17570
|
+
REVOKE ALL ON FUNCTION public.agenda_turn__put(uuid, text, jsonb) FROM PUBLIC, anon, authenticated;
|
|
17571
|
+
`;
|
|
15808
17572
|
var MIGRATIONS = [
|
|
15809
17573
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
15810
17574
|
{ id: "001_the_connector_definers_keep_a_pinned_path", sql: MIGRATION_001_THE_CONNECTOR_DEFINERS_KEEP_A_PINNED_PATH },
|
|
@@ -15826,7 +17590,9 @@ var MIGRATIONS = [
|
|
|
15826
17590
|
{ id: "017_a_equipe_do_dia_aparece", sql: MIGRATION_017_A_EQUIPE_DO_DIA_APARECE },
|
|
15827
17591
|
{ id: "018_o_agendamento_que_se_repete", sql: MIGRATION_018_O_AGENDAMENTO_QUE_SE_REPETE },
|
|
15828
17592
|
{ id: "019_a_serie_nasce_numa_viagem_so", sql: MIGRATION_019_A_SERIE_NASCE_NUMA_VIAGEM_SO },
|
|
15829
|
-
{ id: "020_a_lista_da_vez_se_liga_no_cadastro_da_unidade", sql: MIGRATION_020_A_LISTA_DA_VEZ_SE_LIGA_NO_CADASTRO_DA_UNIDADE }
|
|
17593
|
+
{ id: "020_a_lista_da_vez_se_liga_no_cadastro_da_unidade", sql: MIGRATION_020_A_LISTA_DA_VEZ_SE_LIGA_NO_CADASTRO_DA_UNIDADE },
|
|
17594
|
+
{ id: "021_a_vez_por_area", sql: MIGRATION_021_A_VEZ_POR_AREA },
|
|
17595
|
+
{ id: "022_a_chave_da_fila_e_configuracao_da_unidade", sql: MIGRATION_022_A_CHAVE_DA_FILA_E_CONFIGURACAO_DA_UNIDADE }
|
|
15830
17596
|
];
|
|
15831
17597
|
var channelSequence = 0;
|
|
15832
17598
|
function client() {
|
|
@@ -15892,6 +17658,66 @@ function createSupabaseTurnQueueProvider() {
|
|
|
15892
17658
|
if (error) throw error;
|
|
15893
17659
|
return data ?? { ok: false };
|
|
15894
17660
|
},
|
|
17661
|
+
async getBoard(unitId) {
|
|
17662
|
+
const { data, error } = await client().rpc("agenda_turn_queue_board", { p_unit_id: unitId });
|
|
17663
|
+
if (error) throw error;
|
|
17664
|
+
return data;
|
|
17665
|
+
},
|
|
17666
|
+
async act({ unitId, professionalId, action, position: position2, reason }) {
|
|
17667
|
+
const { data, error } = await client().rpc("agenda_turn_queue_act", {
|
|
17668
|
+
p_unit_id: unitId,
|
|
17669
|
+
p_professional_id: professionalId,
|
|
17670
|
+
p_action: action,
|
|
17671
|
+
p_position: position2 ?? null,
|
|
17672
|
+
p_reason: reason ?? null
|
|
17673
|
+
});
|
|
17674
|
+
if (error) throw error;
|
|
17675
|
+
return data ?? { ok: true };
|
|
17676
|
+
},
|
|
17677
|
+
async getConfig(unitId) {
|
|
17678
|
+
const { data, error } = await client().rpc("agenda_turn_queue_config", { p_unit_id: unitId });
|
|
17679
|
+
if (error) throw error;
|
|
17680
|
+
return data;
|
|
17681
|
+
},
|
|
17682
|
+
async saveConfig(unitId, patch) {
|
|
17683
|
+
const { error } = await client().rpc("agenda_turn_queue_save_config", { p_unit_id: unitId, p_config: patch });
|
|
17684
|
+
if (error) throw error;
|
|
17685
|
+
},
|
|
17686
|
+
async getMyState(unitId) {
|
|
17687
|
+
const { data, error } = await client().rpc("agenda_turn_queue_my_state", { p_unit_id: unitId });
|
|
17688
|
+
if (error) throw error;
|
|
17689
|
+
return data ?? null;
|
|
17690
|
+
},
|
|
17691
|
+
async myAction({ unitId, action, latitude, longitude, accuracyMeters, reason }) {
|
|
17692
|
+
const { data, error } = await client().rpc("agenda_turn_queue_my_action", {
|
|
17693
|
+
p_unit_id: unitId,
|
|
17694
|
+
p_action: action,
|
|
17695
|
+
p_latitude: latitude ?? null,
|
|
17696
|
+
p_longitude: longitude ?? null,
|
|
17697
|
+
p_accuracy_m: accuracyMeters ?? null,
|
|
17698
|
+
p_reason: reason ?? null
|
|
17699
|
+
});
|
|
17700
|
+
if (error) throw error;
|
|
17701
|
+
return data ?? { ok: true };
|
|
17702
|
+
},
|
|
17703
|
+
async getReport({ unitIds, from, to }) {
|
|
17704
|
+
const { data, error } = await client().rpc("agenda_turn_queue_report", { p_unit_ids: unitIds, p_from: from, p_to: to });
|
|
17705
|
+
if (error) throw error;
|
|
17706
|
+
return (data ?? []).map((row) => ({
|
|
17707
|
+
unitId: row.unit_id,
|
|
17708
|
+
unitName: row.unit_name ?? "",
|
|
17709
|
+
professionalId: row.professional_id,
|
|
17710
|
+
professionalName: row.professional_name ?? "Profissional",
|
|
17711
|
+
area: row.area ?? null,
|
|
17712
|
+
entradas: Number(row.entradas ?? 0),
|
|
17713
|
+
atendimentos: Number(row.atendimentos ?? 0),
|
|
17714
|
+
servicosRapidos: Number(row.servicos_rapidos ?? 0),
|
|
17715
|
+
chamados: Number(row.chamados ?? 0),
|
|
17716
|
+
pulos: Number(row.pulos ?? 0),
|
|
17717
|
+
ausencias: Number(row.ausencias ?? 0),
|
|
17718
|
+
minutosAusente: Number(row.minutos_ausente ?? 0)
|
|
17719
|
+
}));
|
|
17720
|
+
},
|
|
15895
17721
|
subscribe(onChange) {
|
|
15896
17722
|
const db = client();
|
|
15897
17723
|
const channelName = `agenda:turn-queue:${getActiveTenantId() ?? "active"}:${++channelSequence}`;
|
|
@@ -15938,56 +17764,195 @@ function createMockTurnQueueProvider() {
|
|
|
15938
17764
|
listeners2.forEach((listener) => listener());
|
|
15939
17765
|
return { ok: true, status: row.status };
|
|
15940
17766
|
},
|
|
17767
|
+
async getBoard(unitId) {
|
|
17768
|
+
return {
|
|
17769
|
+
unitId,
|
|
17770
|
+
today: now().slice(0, 10),
|
|
17771
|
+
enabled: true,
|
|
17772
|
+
selfCheckin: true,
|
|
17773
|
+
auto: true,
|
|
17774
|
+
requireLocation: false,
|
|
17775
|
+
locationConfigured: true,
|
|
17776
|
+
areas: [
|
|
17777
|
+
{ key: "cabelo", name: "Cabelo", color: "amber" },
|
|
17778
|
+
{ key: "unhas", name: "Unhas", color: "green" },
|
|
17779
|
+
{ key: "estetica", name: "Est\xE9tica", color: "blue" },
|
|
17780
|
+
{ key: "geral", name: "Outros", color: "slate" }
|
|
17781
|
+
],
|
|
17782
|
+
entries: entries.filter((entry) => entry.status !== "unavailable").map((entry, index) => ({
|
|
17783
|
+
professionalId: entry.professionalId,
|
|
17784
|
+
name: entry.professionalName,
|
|
17785
|
+
avatarUrl: null,
|
|
17786
|
+
area: "cabelo",
|
|
17787
|
+
status: entry.status === "serving" ? "serving" : "available",
|
|
17788
|
+
rank: entry.status === "serving" ? null : index + 1,
|
|
17789
|
+
awayReason: null,
|
|
17790
|
+
since: entry.stateChangedAt,
|
|
17791
|
+
joinedAt: entry.availableAt,
|
|
17792
|
+
appointmentId: null,
|
|
17793
|
+
servedToday: 0
|
|
17794
|
+
})),
|
|
17795
|
+
roster: [{ professionalId: "prof-3", name: "Bia Costa", avatarUrl: null, area: "unhas", appointmentsToday: 1, nextAppointmentAt: now(), servedToday: 0 }],
|
|
17796
|
+
events: []
|
|
17797
|
+
};
|
|
17798
|
+
},
|
|
17799
|
+
async act() {
|
|
17800
|
+
listeners2.forEach((listener) => listener());
|
|
17801
|
+
return { ok: true, changed: true };
|
|
17802
|
+
},
|
|
17803
|
+
async getConfig() {
|
|
17804
|
+
return {
|
|
17805
|
+
unit: { auto: true, selfCheckin: true, requireLocation: false },
|
|
17806
|
+
areas: [
|
|
17807
|
+
{ key: "cabelo", name: "Cabelo", color: "amber", keywords: ["cabel", "color", "escov", "penteado", "barb"] },
|
|
17808
|
+
{ key: "unhas", name: "Unhas", color: "green", keywords: ["manicur", "pedicur", "unha"] },
|
|
17809
|
+
{ key: "estetica", name: "Est\xE9tica", color: "blue", keywords: ["estet", "depil", "sobrancel", "massag", "cili", "maqui"] }
|
|
17810
|
+
],
|
|
17811
|
+
professionals: professionals.map((p) => ({ professionalId: p.professionalId, name: p.professionalName, manualArea: null, autoArea: "geral" })),
|
|
17812
|
+
quickServiceIds: [],
|
|
17813
|
+
services: [{ id: "svc-1", name: "Esmalta\xE7\xE3o" }, { id: "svc-2", name: "Corte" }]
|
|
17814
|
+
};
|
|
17815
|
+
},
|
|
17816
|
+
async saveConfig() {
|
|
17817
|
+
},
|
|
17818
|
+
async getMyState() {
|
|
17819
|
+
return null;
|
|
17820
|
+
},
|
|
17821
|
+
async myAction() {
|
|
17822
|
+
return { ok: true, changed: true };
|
|
17823
|
+
},
|
|
17824
|
+
async getReport() {
|
|
17825
|
+
return [];
|
|
17826
|
+
},
|
|
15941
17827
|
subscribe(onChange) {
|
|
15942
17828
|
listeners2.add(onChange);
|
|
15943
17829
|
return () => listeners2.delete(onChange);
|
|
15944
17830
|
}
|
|
15945
17831
|
};
|
|
15946
17832
|
}
|
|
17833
|
+
|
|
17834
|
+
// src/views/turn-queue/turn-queue-ui.ts
|
|
17835
|
+
var AWAY_REASONS = ["Banheiro", "Lanche", "Almo\xE7o", "Saiu da loja", "Servi\xE7o interno"];
|
|
17836
|
+
var AREA_COLORS = [
|
|
17837
|
+
{ value: "amber", label: "Amarelo" },
|
|
17838
|
+
{ value: "green", label: "Verde" },
|
|
17839
|
+
{ value: "blue", label: "Azul" },
|
|
17840
|
+
{ value: "pink", label: "Rosa" },
|
|
17841
|
+
{ value: "purple", label: "Roxo" },
|
|
17842
|
+
{ value: "orange", label: "Laranja" },
|
|
17843
|
+
{ value: "teal", label: "Verde-\xE1gua" },
|
|
17844
|
+
{ value: "slate", label: "Cinza" }
|
|
17845
|
+
];
|
|
17846
|
+
var AREA_TONE = {
|
|
17847
|
+
amber: { dot: "bg-amber-400", column: "bg-amber-50/70 border-amber-200/70 dark:bg-amber-500/5 dark:border-amber-500/20", stripe: "bg-amber-400", button: "bg-amber-400 text-amber-950 hover:bg-amber-500", text: "text-amber-600 dark:text-amber-400" },
|
|
17848
|
+
green: { dot: "bg-emerald-500", column: "bg-emerald-50/70 border-emerald-200/70 dark:bg-emerald-500/5 dark:border-emerald-500/20", stripe: "bg-emerald-500", button: "bg-emerald-500 text-white hover:bg-emerald-600", text: "text-emerald-600 dark:text-emerald-400" },
|
|
17849
|
+
blue: { dot: "bg-sky-500", column: "bg-sky-50/70 border-sky-200/70 dark:bg-sky-500/5 dark:border-sky-500/20", stripe: "bg-sky-500", button: "bg-sky-500 text-white hover:bg-sky-600", text: "text-sky-600 dark:text-sky-400" },
|
|
17850
|
+
pink: { dot: "bg-pink-500", column: "bg-pink-50/70 border-pink-200/70 dark:bg-pink-500/5 dark:border-pink-500/20", stripe: "bg-pink-500", button: "bg-pink-500 text-white hover:bg-pink-600", text: "text-pink-600 dark:text-pink-400" },
|
|
17851
|
+
purple: { dot: "bg-violet-500", column: "bg-violet-50/70 border-violet-200/70 dark:bg-violet-500/5 dark:border-violet-500/20", stripe: "bg-violet-500", button: "bg-violet-500 text-white hover:bg-violet-600", text: "text-violet-600 dark:text-violet-400" },
|
|
17852
|
+
orange: { dot: "bg-orange-500", column: "bg-orange-50/70 border-orange-200/70 dark:bg-orange-500/5 dark:border-orange-500/20", stripe: "bg-orange-500", button: "bg-orange-500 text-white hover:bg-orange-600", text: "text-orange-600 dark:text-orange-400" },
|
|
17853
|
+
teal: { dot: "bg-teal-500", column: "bg-teal-50/70 border-teal-200/70 dark:bg-teal-500/5 dark:border-teal-500/20", stripe: "bg-teal-500", button: "bg-teal-500 text-white hover:bg-teal-600", text: "text-teal-600 dark:text-teal-400" },
|
|
17854
|
+
slate: { dot: "bg-slate-400", column: "bg-slate-50/80 border-slate-200/80 dark:bg-slate-500/5 dark:border-slate-500/20", stripe: "bg-slate-400", button: "bg-slate-600 text-white hover:bg-slate-700", text: "text-slate-600 dark:text-slate-300" }
|
|
17855
|
+
};
|
|
17856
|
+
function toneOf(color) {
|
|
17857
|
+
return AREA_TONE[color in AREA_TONE ? color : "slate"];
|
|
17858
|
+
}
|
|
17859
|
+
function firstName(name) {
|
|
17860
|
+
return name.trim().split(/\s+/)[0] ?? name;
|
|
17861
|
+
}
|
|
17862
|
+
function minutesSince(iso) {
|
|
17863
|
+
if (!iso) return 0;
|
|
17864
|
+
return Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 6e4));
|
|
17865
|
+
}
|
|
17866
|
+
function sinceLabel(iso) {
|
|
17867
|
+
const m = minutesSince(iso);
|
|
17868
|
+
if (m < 1) return "agora";
|
|
17869
|
+
if (m < 60) return `${m} min`;
|
|
17870
|
+
return `${Math.floor(m / 60)}h${m % 60 ? ` ${String(m % 60).padStart(2, "0")}min` : ""}`;
|
|
17871
|
+
}
|
|
17872
|
+
function clock(iso) {
|
|
17873
|
+
return new Date(iso).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
17874
|
+
}
|
|
17875
|
+
function eventPhrase(event) {
|
|
17876
|
+
const by = event.origin === "frontdesk" ? " pela recep\xE7\xE3o" : "";
|
|
17877
|
+
const reason = event.reason ? event.reason : null;
|
|
17878
|
+
switch (event.type) {
|
|
17879
|
+
case "joined":
|
|
17880
|
+
case "available":
|
|
17881
|
+
case "override_available":
|
|
17882
|
+
return `entrou na fila${by}`;
|
|
17883
|
+
case "left":
|
|
17884
|
+
case "override_left":
|
|
17885
|
+
return `saiu da fila${by}`;
|
|
17886
|
+
case "away":
|
|
17887
|
+
return `saiu${by} (ausente)${reason ? ` \u2014 ${reason}` : ""}`;
|
|
17888
|
+
case "back":
|
|
17889
|
+
return `voltou${by}`;
|
|
17890
|
+
case "skipped":
|
|
17891
|
+
return `foi pulado${by}${reason ? ` \u2014 ausente (${reason})` : ""}`;
|
|
17892
|
+
case "called":
|
|
17893
|
+
return `foi chamado${by}`;
|
|
17894
|
+
case "started":
|
|
17895
|
+
case "advanced_to_service":
|
|
17896
|
+
return `come\xE7ou um atendimento${by}`;
|
|
17897
|
+
case "finished":
|
|
17898
|
+
case "service_completed":
|
|
17899
|
+
return `terminou e voltou para o fim${by}`;
|
|
17900
|
+
case "service_cancelled_restored":
|
|
17901
|
+
return "teve o atendimento cancelado e voltou ao lugar";
|
|
17902
|
+
case "moved":
|
|
17903
|
+
return `mudou de posi\xE7\xE3o${by}`;
|
|
17904
|
+
case "quick_service":
|
|
17905
|
+
return "fez um servi\xE7o r\xE1pido e continuou na vez";
|
|
17906
|
+
case "reset":
|
|
17907
|
+
return "saiu da fila na virada do dia";
|
|
17908
|
+
default:
|
|
17909
|
+
return "movimentou a fila";
|
|
17910
|
+
}
|
|
17911
|
+
}
|
|
17912
|
+
function turnErrorMessage(error) {
|
|
17913
|
+
const msg = String(error?.message ?? error ?? "");
|
|
17914
|
+
if (msg.includes("agenda.turn_queue.read")) return "Voc\xEA n\xE3o tem acesso \xE0 lista da vez.";
|
|
17915
|
+
if (msg.includes("agenda.turn_queue.")) return "Seu perfil n\xE3o pode fazer isso na lista da vez.";
|
|
17916
|
+
if (msg.includes("turn_queue_disabled")) return "A lista da vez est\xE1 desligada nesta unidade.";
|
|
17917
|
+
if (msg.includes("turn_queue_in_service")) return "Quem est\xE1 em atendimento n\xE3o pode sair nem mudar de lugar agora.";
|
|
17918
|
+
if (msg.includes("turn_queue_not_in_queue")) return "Esta pessoa n\xE3o est\xE1 mais na fila.";
|
|
17919
|
+
if (msg.includes("outside_geofence")) return "Voc\xEA est\xE1 fora do raio da loja. Quando chegar, aperte de novo.";
|
|
17920
|
+
if (msg.includes("unit_has_no_coordinates")) return "A loja ainda n\xE3o tem localiza\xE7\xE3o cadastrada.";
|
|
17921
|
+
if (msg.includes("location_required") || msg.includes("geolocation")) return "Precisamos da localiza\xE7\xE3o do celular para confirmar que voc\xEA est\xE1 na loja.";
|
|
17922
|
+
if (msg.includes("turn_queue_checkin_by_frontdesk")) return "Nesta unidade quem coloca na fila \xE9 a recep\xE7\xE3o.";
|
|
17923
|
+
if (msg.includes("turn_queue_professional_not_linked")) return "Seu acesso n\xE3o est\xE1 vinculado a um cadastro de profissional.";
|
|
17924
|
+
if (msg.includes("turn_queue_professional_outside_unit")) return "Seu cadastro de profissional n\xE3o \xE9 desta unidade.";
|
|
17925
|
+
return "N\xE3o foi poss\xEDvel concluir agora. Tente de novo.";
|
|
17926
|
+
}
|
|
15947
17927
|
function position() {
|
|
15948
17928
|
return new Promise((resolve, reject) => {
|
|
15949
17929
|
if (!navigator.geolocation) {
|
|
15950
17930
|
reject(new Error("geolocation_unavailable"));
|
|
15951
17931
|
return;
|
|
15952
17932
|
}
|
|
15953
|
-
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
|
15954
|
-
enableHighAccuracy: true,
|
|
15955
|
-
timeout: 12e3,
|
|
15956
|
-
maximumAge: 3e4
|
|
15957
|
-
});
|
|
17933
|
+
navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: true, timeout: 12e3, maximumAge: 3e4 });
|
|
15958
17934
|
});
|
|
15959
17935
|
}
|
|
15960
|
-
function failureMessage(error) {
|
|
15961
|
-
const message = String(error?.message ?? error ?? "");
|
|
15962
|
-
if (message.includes("outside_geofence")) {
|
|
15963
|
-
return "Sua localiza\xE7\xE3o n\xE3o permite dizer que est\xE1 dispon\xEDvel ainda. Quando chegar na loja, aperte novamente.";
|
|
15964
|
-
}
|
|
15965
|
-
if (message.includes("unit_has_no_coordinates")) {
|
|
15966
|
-
return "A unidade ainda n\xE3o tem localiza\xE7\xE3o configurada para validar sua disponibilidade.";
|
|
15967
|
-
}
|
|
15968
|
-
if (message.includes("sem_ficha_de_profissional")) {
|
|
15969
|
-
return "Seu acesso ainda n\xE3o est\xE1 vinculado a uma ficha de profissional.";
|
|
15970
|
-
}
|
|
15971
|
-
if (message.includes("unit_required")) return "Escolha a unidade antes de marcar disponibilidade.";
|
|
15972
|
-
return "N\xE3o foi poss\xEDvel validar sua disponibilidade agora.";
|
|
15973
|
-
}
|
|
15974
17936
|
function AvailabilityHeaderControl({ provider: provider2 }) {
|
|
15975
17937
|
const unitId = useActiveUnitId();
|
|
15976
|
-
const [
|
|
17938
|
+
const [me, setMe] = React23.useState(null);
|
|
15977
17939
|
const [busy, setBusy] = React23.useState(false);
|
|
15978
17940
|
const refresh = React23.useCallback(async () => {
|
|
15979
17941
|
if (!unitId) {
|
|
15980
|
-
|
|
17942
|
+
setMe(null);
|
|
15981
17943
|
return;
|
|
15982
17944
|
}
|
|
15983
17945
|
try {
|
|
15984
|
-
|
|
15985
|
-
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
17946
|
+
setMe(await provider2.getMyState(unitId));
|
|
17947
|
+
} catch {
|
|
17948
|
+
setMe(null);
|
|
17949
|
+
}
|
|
17950
|
+
try {
|
|
17951
|
+
const board = await provider2.getBoard(unitId);
|
|
17952
|
+
const leader = board.entries.filter((e) => e.status === "available").sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0))[0];
|
|
17953
|
+
publishTurnQueueLeader(unitId, board.enabled ? leader?.professionalId ?? null : null);
|
|
15989
17954
|
} catch {
|
|
15990
|
-
|
|
17955
|
+
publishTurnQueueLeader(unitId, null);
|
|
15991
17956
|
}
|
|
15992
17957
|
}, [provider2, unitId]);
|
|
15993
17958
|
React23.useEffect(() => {
|
|
@@ -15996,316 +17961,951 @@ function AvailabilityHeaderControl({ provider: provider2 }) {
|
|
|
15996
17961
|
void refresh();
|
|
15997
17962
|
});
|
|
15998
17963
|
}, [provider2, refresh]);
|
|
15999
|
-
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
return;
|
|
16003
|
-
}
|
|
17964
|
+
if (!unitId || !me || !me.selfCheckin) return null;
|
|
17965
|
+
async function run(action, reason) {
|
|
17966
|
+
if (!unitId || !me) return;
|
|
16004
17967
|
setBusy(true);
|
|
16005
17968
|
try {
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
|
|
17969
|
+
let geo = null;
|
|
17970
|
+
if (action === "join" && me.requireLocation) geo = await position();
|
|
17971
|
+
await provider2.myAction({
|
|
17972
|
+
unitId,
|
|
17973
|
+
action,
|
|
17974
|
+
reason,
|
|
17975
|
+
latitude: geo?.coords.latitude,
|
|
17976
|
+
longitude: geo?.coords.longitude,
|
|
17977
|
+
accuracyMeters: geo?.coords.accuracy
|
|
17978
|
+
});
|
|
17979
|
+
const said = {
|
|
17980
|
+
join: "Voc\xEA entrou no fim da fila.",
|
|
17981
|
+
leave: "Voc\xEA saiu da fila.",
|
|
17982
|
+
away: "Seu lugar fica guardado.",
|
|
17983
|
+
back: "Voc\xEA voltou para o seu lugar.",
|
|
17984
|
+
finish: "Voc\xEA voltou para o fim da fila."
|
|
17985
|
+
};
|
|
17986
|
+
toast.success(said[action]);
|
|
17987
|
+
await refresh();
|
|
17988
|
+
} catch (cause) {
|
|
17989
|
+
toast.error(turnErrorMessage(cause));
|
|
17990
|
+
} finally {
|
|
17991
|
+
setBusy(false);
|
|
17992
|
+
}
|
|
17993
|
+
}
|
|
17994
|
+
if (!me.inQueue) {
|
|
17995
|
+
return /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", className: "h-9 rounded-full", disabled: busy, onClick: () => {
|
|
17996
|
+
void run("join");
|
|
17997
|
+
}, children: [
|
|
17998
|
+
busy ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(LogIn, { className: "mr-1.5 h-4 w-4" }),
|
|
17999
|
+
"Cheguei"
|
|
18000
|
+
] });
|
|
18001
|
+
}
|
|
18002
|
+
const label = me.status === "serving" ? "Em atendimento" : me.status === "away" ? me.awayReason ?? "Ausente" : `Na vez \xB7 ${me.rank ?? "\u2013"}\xBA`;
|
|
18003
|
+
return /* @__PURE__ */ jsxs(Dropdown, { children: [
|
|
18004
|
+
/* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", className: "h-9 rounded-full", disabled: busy, children: [
|
|
18005
|
+
busy ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: `mr-2 h-2 w-2 rounded-full ${me.status === "away" ? "bg-destructive" : me.status === "serving" ? "bg-sky-500" : "bg-success"}` }),
|
|
18006
|
+
label
|
|
18007
|
+
] }) }),
|
|
18008
|
+
/* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-56", children: [
|
|
18009
|
+
/* @__PURE__ */ jsx(DropdownLabel, { children: "Lista da vez" }),
|
|
18010
|
+
me.status === "available" ? /* @__PURE__ */ jsxs(DropdownSub, { children: [
|
|
18011
|
+
/* @__PURE__ */ jsxs(DropdownSubTrigger, { children: [
|
|
18012
|
+
/* @__PURE__ */ jsx(Coffee, { className: "mr-2 h-4 w-4" }),
|
|
18013
|
+
"Sa\xED (guarda o lugar)"
|
|
18014
|
+
] }),
|
|
18015
|
+
/* @__PURE__ */ jsx(DropdownSubContent, { children: AWAY_REASONS.map((reason) => /* @__PURE__ */ jsx(DropdownItem, { onSelect: () => {
|
|
18016
|
+
void run("away", reason);
|
|
18017
|
+
}, children: reason }, reason)) })
|
|
18018
|
+
] }) : null,
|
|
18019
|
+
me.status === "away" ? /* @__PURE__ */ jsxs(DropdownItem, { onSelect: () => {
|
|
18020
|
+
void run("back");
|
|
18021
|
+
}, children: [
|
|
18022
|
+
/* @__PURE__ */ jsx(Undo2, { className: "mr-2 h-4 w-4" }),
|
|
18023
|
+
"Voltei"
|
|
18024
|
+
] }) : null,
|
|
18025
|
+
me.status === "serving" ? /* @__PURE__ */ jsxs(DropdownItem, { onSelect: () => {
|
|
18026
|
+
void run("finish");
|
|
18027
|
+
}, children: [
|
|
18028
|
+
/* @__PURE__ */ jsx(CheckCircle2, { className: "mr-2 h-4 w-4" }),
|
|
18029
|
+
"Terminei"
|
|
18030
|
+
] }) : null,
|
|
18031
|
+
/* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
18032
|
+
/* @__PURE__ */ jsxs(DropdownItem, { className: "text-destructive focus:text-destructive", onSelect: () => {
|
|
18033
|
+
void run("leave");
|
|
18034
|
+
}, children: [
|
|
18035
|
+
/* @__PURE__ */ jsx(LogOut, { className: "mr-2 h-4 w-4" }),
|
|
18036
|
+
"Sair da fila"
|
|
18037
|
+
] })
|
|
18038
|
+
] })
|
|
18039
|
+
] });
|
|
18040
|
+
}
|
|
18041
|
+
var QUICK_HINTS = ["esmalt", "piastra", "chapinha", "secagem", "lavagem", "avaliac", "retoque"];
|
|
18042
|
+
var AUTO = "__auto__";
|
|
18043
|
+
function norm(value) {
|
|
18044
|
+
return value.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
18045
|
+
}
|
|
18046
|
+
function slug(name, taken) {
|
|
18047
|
+
const base = norm(name).replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "").slice(0, 24) || "area";
|
|
18048
|
+
let key = base;
|
|
18049
|
+
for (let i = 2; taken.has(key) || key === "geral"; i++) key = `${base}_${i}`;
|
|
18050
|
+
return key;
|
|
18051
|
+
}
|
|
18052
|
+
function SwitchRow({ title, hint, checked, onChange }) {
|
|
18053
|
+
return /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-start justify-between gap-4 rounded-xl border border-border/70 p-4", children: [
|
|
18054
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
|
|
18055
|
+
/* @__PURE__ */ jsx("span", { className: "block text-sm font-medium text-foreground", children: title }),
|
|
18056
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-xs text-muted-foreground", children: hint })
|
|
18057
|
+
] }),
|
|
18058
|
+
/* @__PURE__ */ jsx(Switch, { checked, onCheckedChange: onChange })
|
|
18059
|
+
] });
|
|
18060
|
+
}
|
|
18061
|
+
function IconButton({ label, disabled, onClick, children, destructive }) {
|
|
18062
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
18063
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
18064
|
+
Button,
|
|
18065
|
+
{
|
|
18066
|
+
type: "button",
|
|
18067
|
+
variant: "ghost",
|
|
18068
|
+
size: "icon",
|
|
18069
|
+
"aria-label": label,
|
|
18070
|
+
disabled,
|
|
18071
|
+
onClick,
|
|
18072
|
+
className: cn("h-9 w-9 shrink-0", destructive && "text-destructive hover:text-destructive"),
|
|
18073
|
+
children
|
|
16023
18074
|
}
|
|
16024
|
-
}
|
|
16025
|
-
|
|
18075
|
+
) }),
|
|
18076
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: label })
|
|
18077
|
+
] });
|
|
18078
|
+
}
|
|
18079
|
+
function TurnQueueConfigDialog({ provider: provider2, unitId, onClose, onSaved }) {
|
|
18080
|
+
const [config, setConfig] = React23.useState(null);
|
|
18081
|
+
const [unit, setUnit] = React23.useState({ auto: true, selfCheckin: true, requireLocation: false });
|
|
18082
|
+
const [areas, setAreas] = React23.useState([]);
|
|
18083
|
+
const [pinned, setPinned] = React23.useState({});
|
|
18084
|
+
const [quick, setQuick] = React23.useState(/* @__PURE__ */ new Set());
|
|
18085
|
+
const [search, setSearch] = React23.useState("");
|
|
18086
|
+
const [saving, setSaving] = React23.useState(false);
|
|
18087
|
+
const [error, setError] = React23.useState(null);
|
|
18088
|
+
React23.useEffect(() => {
|
|
18089
|
+
let alive = true;
|
|
18090
|
+
provider2.getConfig(unitId).then((next) => {
|
|
18091
|
+
if (!alive) return;
|
|
18092
|
+
setConfig(next);
|
|
18093
|
+
setUnit(next.unit);
|
|
18094
|
+
setAreas(next.areas.map((area) => ({ ...area, keywordsText: area.keywords.join(", ") })));
|
|
18095
|
+
setPinned(Object.fromEntries(next.professionals.map((p) => [p.professionalId, p.manualArea])));
|
|
18096
|
+
setQuick(new Set(next.quickServiceIds));
|
|
18097
|
+
}).catch((cause) => {
|
|
18098
|
+
if (alive) setError(turnErrorMessage(cause));
|
|
18099
|
+
});
|
|
18100
|
+
return () => {
|
|
18101
|
+
alive = false;
|
|
18102
|
+
};
|
|
18103
|
+
}, [provider2, unitId]);
|
|
18104
|
+
const areaOptions = React23.useMemo(() => [
|
|
18105
|
+
...areas.filter((area) => area.name.trim()).map((area) => ({ key: area.key, name: area.name.trim() })),
|
|
18106
|
+
{ key: "geral", name: "Outros" }
|
|
18107
|
+
], [areas]);
|
|
18108
|
+
const areaName = (key) => areaOptions.find((a) => a.key === key)?.name ?? "Outros";
|
|
18109
|
+
const services = React23.useMemo(() => {
|
|
18110
|
+
const q = norm(search.trim());
|
|
18111
|
+
return (config?.services ?? []).filter((s) => !q || norm(s.name).includes(q));
|
|
18112
|
+
}, [config, search]);
|
|
18113
|
+
const moveArea = (index, delta) => setAreas((list) => {
|
|
18114
|
+
const next = [...list];
|
|
18115
|
+
const [row] = next.splice(index, 1);
|
|
18116
|
+
next.splice(index + delta, 0, row);
|
|
18117
|
+
return next;
|
|
18118
|
+
});
|
|
18119
|
+
const suggest = () => {
|
|
18120
|
+
const hits = (config?.services ?? []).filter((s) => QUICK_HINTS.some((hint) => norm(s.name).includes(hint)));
|
|
18121
|
+
setQuick((prev) => /* @__PURE__ */ new Set([...prev, ...hits.map((s) => s.id)]));
|
|
18122
|
+
toast.success(`${hits.length} servi\xE7o(s) marcado(s) pelo nome. Confira antes de salvar.`);
|
|
18123
|
+
};
|
|
18124
|
+
const blank = areas.find((area) => !area.name.trim());
|
|
18125
|
+
const save = async () => {
|
|
18126
|
+
if (blank) {
|
|
18127
|
+
setError("D\xEA um nome para cada \xE1rea (ou exclua a linha vazia).");
|
|
18128
|
+
return;
|
|
18129
|
+
}
|
|
18130
|
+
setSaving(true);
|
|
18131
|
+
setError(null);
|
|
18132
|
+
try {
|
|
18133
|
+
const initial = Object.fromEntries((config?.professionals ?? []).map((p) => [p.professionalId, p.manualArea]));
|
|
18134
|
+
const changedPins = Object.fromEntries(Object.entries(pinned).filter(([id, area]) => initial[id] !== area));
|
|
18135
|
+
await provider2.saveConfig(unitId, {
|
|
18136
|
+
unit,
|
|
18137
|
+
areas: areas.map(({ keywordsText, ...area }) => ({
|
|
18138
|
+
...area,
|
|
18139
|
+
name: area.name.trim(),
|
|
18140
|
+
keywords: keywordsText.split(",").map((k) => k.trim()).filter(Boolean)
|
|
18141
|
+
})),
|
|
18142
|
+
professionalAreas: changedPins,
|
|
18143
|
+
quickServiceIds: [...quick]
|
|
18144
|
+
});
|
|
18145
|
+
toast.success("Configura\xE7\xE3o da lista da vez salva.");
|
|
18146
|
+
onSaved();
|
|
18147
|
+
} catch (cause) {
|
|
18148
|
+
setError(turnErrorMessage(cause));
|
|
16026
18149
|
} finally {
|
|
16027
|
-
|
|
16028
|
-
}
|
|
16029
|
-
}
|
|
16030
|
-
const available = status === "available";
|
|
16031
|
-
return /* @__PURE__ */ jsx(PermissionGate, { feature: "agenda", action: "edit", children: /* @__PURE__ */ jsxs(
|
|
16032
|
-
"button",
|
|
16033
|
-
{
|
|
16034
|
-
type: "button",
|
|
16035
|
-
role: "switch",
|
|
16036
|
-
"aria-checked": available,
|
|
16037
|
-
className: "inline-flex min-h-11 items-center gap-2 rounded-full px-1.5 text-xs font-medium text-foreground transition-opacity disabled:cursor-not-allowed disabled:opacity-60 md:hidden",
|
|
16038
|
-
disabled: busy,
|
|
16039
|
-
onClick: () => {
|
|
16040
|
-
void toggle();
|
|
16041
|
-
},
|
|
16042
|
-
"aria-label": available ? "Sair da vez" : "Estou dispon\xEDvel",
|
|
16043
|
-
children: [
|
|
16044
|
-
/* @__PURE__ */ jsx("span", { className: "hidden min-[390px]:inline", children: "Dispon\xEDvel" }),
|
|
16045
|
-
/* @__PURE__ */ jsx(
|
|
16046
|
-
"span",
|
|
16047
|
-
{
|
|
16048
|
-
"aria-hidden": true,
|
|
16049
|
-
className: `relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${available ? "bg-success" : "bg-muted-foreground/35"}`,
|
|
16050
|
-
children: /* @__PURE__ */ jsx("span", { className: `inline-flex h-5 w-5 items-center justify-center rounded-full bg-white shadow-sm transition-transform ${available ? "translate-x-[1.375rem]" : "translate-x-0.5"}`, children: busy && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-muted-foreground" }) })
|
|
16051
|
-
}
|
|
16052
|
-
)
|
|
16053
|
-
]
|
|
18150
|
+
setSaving(false);
|
|
16054
18151
|
}
|
|
16055
|
-
|
|
16056
|
-
|
|
16057
|
-
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
18152
|
+
};
|
|
18153
|
+
return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (open) => {
|
|
18154
|
+
if (!open) onClose();
|
|
18155
|
+
}, children: /* @__PURE__ */ jsx(ModalContent, { size: "2xl", className: "flex max-h-[min(44rem,calc(100dvh-2rem))] flex-col", children: /* @__PURE__ */ jsxs(TooltipProvider, { delayDuration: 200, children: [
|
|
18156
|
+
/* @__PURE__ */ jsxs(ModalHeader, { children: [
|
|
18157
|
+
/* @__PURE__ */ jsx(ModalTitle, { children: "Configurar a lista da vez" }),
|
|
18158
|
+
/* @__PURE__ */ jsx(ModalDescription, { children: "Ligar a fila e o raio da loja ficam no cadastro da unidade, aba Lista da vez." })
|
|
18159
|
+
] }),
|
|
18160
|
+
!config ? /* @__PURE__ */ jsx(ModalBody, { className: "space-y-3", children: error ? /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18161
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full rounded-lg" }),
|
|
18162
|
+
[0, 1, 2].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-16 w-full rounded-xl" }, i))
|
|
18163
|
+
] }) }) : /* @__PURE__ */ jsxs(Tabs, { defaultValue: "unit", className: "flex min-h-0 flex-1 flex-col", children: [
|
|
18164
|
+
/* @__PURE__ */ jsx("div", { className: "px-6", children: /* @__PURE__ */ jsxs(TabsList, { className: "grid h-auto w-full grid-cols-2 gap-1 sm:grid-cols-4", children: [
|
|
18165
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "unit", children: "Unidade" }),
|
|
18166
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "areas", children: "\xC1reas" }),
|
|
18167
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "people", children: "Profissionais" }),
|
|
18168
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "quick", children: "Servi\xE7os r\xE1pidos" })
|
|
18169
|
+
] }) }),
|
|
18170
|
+
/* @__PURE__ */ jsxs(ModalBody, { className: "min-h-0 flex-1 overflow-y-auto", children: [
|
|
18171
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "unit", className: "mt-0 space-y-3", children: [
|
|
18172
|
+
/* @__PURE__ */ jsx(
|
|
18173
|
+
SwitchRow,
|
|
18174
|
+
{
|
|
18175
|
+
title: "A fila anda sozinha pelo atendimento",
|
|
18176
|
+
hint: "Ao iniciar o atendimento na agenda o profissional sai da vez; ao concluir, volta para o fim.",
|
|
18177
|
+
checked: unit.auto,
|
|
18178
|
+
onChange: (auto) => setUnit((u) => ({ ...u, auto }))
|
|
18179
|
+
}
|
|
18180
|
+
),
|
|
18181
|
+
/* @__PURE__ */ jsx(
|
|
18182
|
+
SwitchRow,
|
|
18183
|
+
{
|
|
18184
|
+
title: "O profissional faz o pr\xF3prio check-in",
|
|
18185
|
+
hint: 'Bot\xE3o "Cheguei" no topo da tela, para quem tem cadastro de profissional nesta unidade e est\xE1 logado nela.',
|
|
18186
|
+
checked: unit.selfCheckin,
|
|
18187
|
+
onChange: (selfCheckin) => setUnit((u) => ({ ...u, selfCheckin }))
|
|
18188
|
+
}
|
|
18189
|
+
),
|
|
18190
|
+
/* @__PURE__ */ jsx(
|
|
18191
|
+
SwitchRow,
|
|
18192
|
+
{
|
|
18193
|
+
title: "Exigir que o profissional esteja na loja",
|
|
18194
|
+
hint: 'O "Cheguei" s\xF3 vale dentro do raio da loja (pede a localiza\xE7\xE3o do celular). Desligado, estar logado na unidade basta.',
|
|
18195
|
+
checked: unit.requireLocation,
|
|
18196
|
+
onChange: (requireLocation) => setUnit((u) => ({ ...u, requireLocation }))
|
|
18197
|
+
}
|
|
18198
|
+
)
|
|
18199
|
+
] }),
|
|
18200
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "areas", className: "mt-0 space-y-3", children: [
|
|
18201
|
+
/* @__PURE__ */ jsx(NetworkNotice, {}),
|
|
18202
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: 'Uma fila por \xE1rea. O profissional cai na primeira \xE1rea (de cima para baixo) cuja palavra aparece na especialidade do cadastro. Quem n\xE3o casar com nenhuma fica em "Outros".' }),
|
|
18203
|
+
areas.map((area, index) => /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-xl border border-border/70 p-3", children: [
|
|
18204
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
18205
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-[12rem] flex-1 items-center gap-2", children: [
|
|
18206
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("h-2.5 w-2.5 shrink-0 rounded-full", toneOf(area.color).dot) }),
|
|
18207
|
+
/* @__PURE__ */ jsx(
|
|
18208
|
+
Input,
|
|
18209
|
+
{
|
|
18210
|
+
"aria-label": "Nome da \xE1rea",
|
|
18211
|
+
value: area.name,
|
|
18212
|
+
placeholder: "Nome da \xE1rea",
|
|
18213
|
+
onChange: (e) => setAreas((list) => list.map((a, i) => i === index ? { ...a, name: e.target.value } : a))
|
|
18214
|
+
}
|
|
18215
|
+
)
|
|
18216
|
+
] }),
|
|
18217
|
+
/* @__PURE__ */ jsxs(Select, { value: area.color, onValueChange: (color) => setAreas((list) => list.map((a, i) => i === index ? { ...a, color } : a)), children: [
|
|
18218
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "w-32 shrink-0", "aria-label": "Cor da \xE1rea", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
18219
|
+
/* @__PURE__ */ jsx(SelectContent, { children: AREA_COLORS.map((c) => /* @__PURE__ */ jsx(SelectItem, { value: c.value, children: c.label }, c.value)) })
|
|
18220
|
+
] }),
|
|
18221
|
+
/* @__PURE__ */ jsx(IconButton, { label: "Subir", disabled: index === 0, onClick: () => moveArea(index, -1), children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-4 w-4" }) }),
|
|
18222
|
+
/* @__PURE__ */ jsx(IconButton, { label: "Descer", disabled: index === areas.length - 1, onClick: () => moveArea(index, 1), children: /* @__PURE__ */ jsx(ArrowDown, { className: "h-4 w-4" }) }),
|
|
18223
|
+
/* @__PURE__ */ jsx(IconButton, { label: "Excluir \xE1rea", destructive: true, onClick: () => setAreas((list) => list.filter((_, i) => i !== index)), children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" }) })
|
|
18224
|
+
] }),
|
|
18225
|
+
/* @__PURE__ */ jsx(
|
|
18226
|
+
Input,
|
|
18227
|
+
{
|
|
18228
|
+
"aria-label": "Palavras da especialidade",
|
|
18229
|
+
value: area.keywordsText,
|
|
18230
|
+
placeholder: "Palavras da especialidade, separadas por v\xEDrgula",
|
|
18231
|
+
onChange: (e) => setAreas((list) => list.map((a, i) => i === index ? { ...a, keywordsText: e.target.value } : a))
|
|
18232
|
+
}
|
|
18233
|
+
)
|
|
18234
|
+
] }, area.key)),
|
|
18235
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-xl border border-dashed border-border/70 px-3 py-2.5 text-sm text-muted-foreground", children: [
|
|
18236
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-2.5 w-2.5 rounded-full bg-slate-400" }),
|
|
18237
|
+
"Outros \u2014 fixa, recebe quem n\xE3o casar com nenhuma \xE1rea"
|
|
18238
|
+
] }),
|
|
18239
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAreas((list) => [
|
|
18240
|
+
...list,
|
|
18241
|
+
{ key: slug(`area ${list.length + 1}`, new Set(list.map((a) => a.key))), name: "", color: "slate", keywords: [], keywordsText: "" }
|
|
18242
|
+
]), children: [
|
|
18243
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
|
|
18244
|
+
"Nova \xE1rea"
|
|
18245
|
+
] })
|
|
18246
|
+
] }),
|
|
18247
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "people", className: "mt-0 space-y-3", children: [
|
|
18248
|
+
/* @__PURE__ */ jsx(NetworkNotice, {}),
|
|
18249
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Autom\xE1tico segue a especialidade do cadastro. Escolha uma \xE1rea para fixar a pessoa nela." }),
|
|
18250
|
+
config.professionals.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhum profissional com agenda nesta unidade." }) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/60 rounded-xl border border-border/70", children: config.professionals.map((p) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2", children: [
|
|
18251
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-sm text-foreground", children: p.name }),
|
|
18252
|
+
/* @__PURE__ */ jsxs(
|
|
18253
|
+
Select,
|
|
18254
|
+
{
|
|
18255
|
+
value: pinned[p.professionalId] ?? AUTO,
|
|
18256
|
+
onValueChange: (value) => setPinned((m) => ({ ...m, [p.professionalId]: value === AUTO ? null : value })),
|
|
18257
|
+
children: [
|
|
18258
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "w-52 shrink-0", "aria-label": `\xC1rea de ${p.name}`, children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
18259
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
18260
|
+
/* @__PURE__ */ jsxs(SelectItem, { value: AUTO, children: [
|
|
18261
|
+
"Autom\xE1tico (",
|
|
18262
|
+
areaName(p.autoArea),
|
|
18263
|
+
")"
|
|
18264
|
+
] }),
|
|
18265
|
+
areaOptions.map((a) => /* @__PURE__ */ jsx(SelectItem, { value: a.key, children: a.name }, a.key))
|
|
18266
|
+
] })
|
|
18267
|
+
]
|
|
18268
|
+
}
|
|
18269
|
+
)
|
|
18270
|
+
] }, p.professionalId)) })
|
|
18271
|
+
] }),
|
|
18272
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "quick", className: "mt-0 space-y-3", children: [
|
|
18273
|
+
/* @__PURE__ */ jsx(NetworkNotice, {}),
|
|
18274
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Servi\xE7os marcados aqui n\xE3o tiram o profissional da vez (ex.: esmalta\xE7\xE3o). O atendimento acontece e ele continua no mesmo lugar." }),
|
|
18275
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
18276
|
+
/* @__PURE__ */ jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Buscar servi\xE7o", "aria-label": "Buscar servi\xE7o" }),
|
|
18277
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: suggest, className: "shrink-0", children: [
|
|
18278
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "mr-1.5 h-4 w-4" }),
|
|
18279
|
+
"Sugerir pelo nome"
|
|
18280
|
+
] })
|
|
18281
|
+
] }),
|
|
18282
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
18283
|
+
quick.size,
|
|
18284
|
+
" marcado(s)"
|
|
18285
|
+
] }),
|
|
18286
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-80 divide-y divide-border/50 overflow-y-auto rounded-xl border border-border/70", children: services.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-4 text-sm text-muted-foreground", children: "Nenhum servi\xE7o encontrado." }) : services.map((s) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-3 px-3 py-2 text-sm text-foreground", children: [
|
|
18287
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: quick.has(s.id), onChange: (on) => setQuick((prev) => {
|
|
18288
|
+
const next = new Set(prev);
|
|
18289
|
+
if (on) next.add(s.id);
|
|
18290
|
+
else next.delete(s.id);
|
|
18291
|
+
return next;
|
|
18292
|
+
}) }),
|
|
18293
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: s.name })
|
|
18294
|
+
] }, s.id)) })
|
|
18295
|
+
] })
|
|
18296
|
+
] })
|
|
18297
|
+
] }),
|
|
18298
|
+
/* @__PURE__ */ jsxs(ModalFooter, { className: "flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-end", children: [
|
|
18299
|
+
error && config ? /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive sm:mr-auto", children: error }) : null,
|
|
18300
|
+
blank && config ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground sm:mr-auto", children: "D\xEA um nome para a \xE1rea nova antes de salvar." }) : null,
|
|
18301
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Cancelar" }),
|
|
18302
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => {
|
|
18303
|
+
void save();
|
|
18304
|
+
}, disabled: !config || saving || !!blank, children: saving ? "Salvando\u2026" : "Salvar" })
|
|
18305
|
+
] })
|
|
18306
|
+
] }) }) });
|
|
16066
18307
|
}
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
};
|
|
16072
|
-
function time(iso) {
|
|
16073
|
-
return new Date(iso).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
18308
|
+
function NetworkNotice() {
|
|
18309
|
+
return /* @__PURE__ */ jsxs("p", { className: "flex items-start gap-2 rounded-lg bg-muted/60 px-3 py-2 text-xs text-muted-foreground", children: [
|
|
18310
|
+
/* @__PURE__ */ jsx(Info, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
|
|
18311
|
+
"Vale para todas as unidades da rede."
|
|
18312
|
+
] });
|
|
16074
18313
|
}
|
|
18314
|
+
var LEGEND = "Cliente sem prefer\xEAncia vai para o primeiro dispon\xEDvel da \xE1rea. Quem atende sai da vez e, ao terminar, volta para o fim. Sa\xEDda curta (banheiro, almo\xE7o\u2026) guarda o lugar.";
|
|
16075
18315
|
function localNow() {
|
|
16076
18316
|
const now = /* @__PURE__ */ new Date();
|
|
16077
18317
|
const pad = (value) => String(value).padStart(2, "0");
|
|
16078
18318
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:00`;
|
|
16079
18319
|
}
|
|
16080
|
-
function
|
|
16081
|
-
return
|
|
16082
|
-
|
|
16083
|
-
|
|
16084
|
-
|
|
16085
|
-
|
|
16086
|
-
|
|
16087
|
-
|
|
16088
|
-
|
|
18320
|
+
function readTvFlag() {
|
|
18321
|
+
return /[?&]tv=1\b/.test(window.location.hash);
|
|
18322
|
+
}
|
|
18323
|
+
function writeTvFlag(on) {
|
|
18324
|
+
const [path] = window.location.hash.split("?");
|
|
18325
|
+
window.history.replaceState(window.history.state, "", on ? `${path}?tv=1` : path);
|
|
18326
|
+
}
|
|
18327
|
+
function openBooking(unitId, professionalId) {
|
|
18328
|
+
window.location.hash = "/agenda";
|
|
18329
|
+
window.setTimeout(() => {
|
|
18330
|
+
window.dispatchEvent(new CustomEvent("agenda:open-booking", {
|
|
18331
|
+
detail: { prefill: { professionalId, locationId: unitId, startsAt: localNow() } }
|
|
18332
|
+
}));
|
|
18333
|
+
}, 150);
|
|
18334
|
+
}
|
|
18335
|
+
function groupByArea(board) {
|
|
18336
|
+
return board.areas.map((area) => {
|
|
18337
|
+
const mine = board.entries.filter((entry) => entry.area === area.key);
|
|
18338
|
+
return {
|
|
18339
|
+
area,
|
|
18340
|
+
queue: mine.filter((entry) => entry.status !== "serving").sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0)),
|
|
18341
|
+
serving: mine.filter((entry) => entry.status === "serving")
|
|
18342
|
+
};
|
|
18343
|
+
});
|
|
18344
|
+
}
|
|
18345
|
+
function BoardSkeleton() {
|
|
18346
|
+
return /* @__PURE__ */ jsx("div", { className: "grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(15rem,1fr))]", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-2xl border border-border/60 p-3", children: [
|
|
18347
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
18348
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-24" }),
|
|
18349
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-14 rounded-full" })
|
|
18350
|
+
] }),
|
|
18351
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full rounded-lg" }),
|
|
18352
|
+
[0, 1].map((j) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-border/50 bg-card p-3", children: [
|
|
18353
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3" }),
|
|
18354
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-9 rounded-full" }),
|
|
18355
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1.5", children: [
|
|
18356
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
|
|
18357
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-1/2" })
|
|
18358
|
+
] })
|
|
18359
|
+
] }, j))
|
|
18360
|
+
] }, i)) });
|
|
18361
|
+
}
|
|
18362
|
+
function IconAction({ label, children }) {
|
|
18363
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
18364
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children }),
|
|
18365
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: label })
|
|
18366
|
+
] });
|
|
18367
|
+
}
|
|
18368
|
+
function QueueCard({ entry, index, total, area, canEdit, canDelete, onAct }) {
|
|
18369
|
+
const tone = toneOf(area.color);
|
|
18370
|
+
const away = entry.status === "away";
|
|
18371
|
+
const isNext = !away && index === 0;
|
|
18372
|
+
const served = entry.servedToday;
|
|
18373
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm", children: [
|
|
18374
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("absolute inset-x-0 top-0 h-1", tone.stripe) }),
|
|
18375
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 pb-3 pt-4", children: [
|
|
18376
|
+
/* @__PURE__ */ jsx("span", { className: "w-4 shrink-0 text-center text-lg font-semibold tabular-nums text-muted-foreground", children: entry.rank ?? index + 1 }),
|
|
18377
|
+
/* @__PURE__ */ jsx(PersonAvatar, { name: entry.name, src: entry.avatarUrl, size: "md" }),
|
|
18378
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
18379
|
+
/* @__PURE__ */ jsx("p", { className: "break-words text-sm font-semibold leading-snug text-foreground", children: entry.name }),
|
|
18380
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: away ? /* @__PURE__ */ jsxs("span", { className: "font-medium text-destructive", children: [
|
|
18381
|
+
entry.awayReason ?? "Ausente",
|
|
18382
|
+
" \xB7 ",
|
|
18383
|
+
sinceLabel(entry.since)
|
|
18384
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18385
|
+
isNext ? /* @__PURE__ */ jsx("span", { className: cn("font-semibold", tone.text), children: "Pr\xF3ximo \xB7 " }) : null,
|
|
18386
|
+
"na fila ",
|
|
18387
|
+
sinceLabel(entry.since) === "agora" ? "agora" : `h\xE1 ${sinceLabel(entry.since)}`
|
|
18388
|
+
] }) }),
|
|
18389
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
18390
|
+
served,
|
|
18391
|
+
" ",
|
|
18392
|
+
served === 1 ? "atendimento" : "atendimentos",
|
|
18393
|
+
" da vez hoje"
|
|
16089
18394
|
] })
|
|
16090
18395
|
] }),
|
|
16091
|
-
|
|
16092
|
-
/* @__PURE__ */ jsx(
|
|
16093
|
-
/* @__PURE__ */
|
|
16094
|
-
|
|
16095
|
-
|
|
16096
|
-
|
|
16097
|
-
|
|
16098
|
-
|
|
16099
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex gap-3 p-3.5", children: [
|
|
16100
|
-
/* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
|
|
16101
|
-
/* @__PURE__ */ jsxs(Avatar, { className: "h-11 w-11", children: [
|
|
16102
|
-
/* @__PURE__ */ jsx(AvatarImage, { src: professional.professionalAvatarUrl ?? void 0 }),
|
|
16103
|
-
/* @__PURE__ */ jsx(AvatarFallback, { children: initials(professional.professionalName) })
|
|
16104
|
-
] }),
|
|
16105
|
-
/* @__PURE__ */ jsx("span", { className: `absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-card ${available ? "bg-success" : serving ? "bg-primary" : "bg-muted-foreground/50"}` })
|
|
16106
|
-
] }),
|
|
16107
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
16108
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
16109
|
-
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: professional.professionalName }),
|
|
16110
|
-
available && professional.position > 0 ? /* @__PURE__ */ jsxs("span", { className: "text-xs font-semibold tabular-nums text-primary", children: [
|
|
16111
|
-
"#",
|
|
16112
|
-
professional.position
|
|
16113
|
-
] }) : null
|
|
16114
|
-
] }),
|
|
16115
|
-
/* @__PURE__ */ jsx("p", { className: `mt-0.5 text-xs font-medium ${available ? "text-success" : serving ? "text-primary" : "text-muted-foreground"}`, children: available ? "Dispon\xEDvel" : serving ? "Em atendimento" : "Indispon\xEDvel" }),
|
|
16116
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground", children: [
|
|
16117
|
-
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
16118
|
-
/* @__PURE__ */ jsx(Clock3, { className: "h-3 w-3" }),
|
|
16119
|
-
professional.lastCheckinAt ? `Check-in ${time(professional.lastCheckinAt)}` : "Sem check-in hoje"
|
|
18396
|
+
canEdit || canDelete ? /* @__PURE__ */ jsxs(Dropdown, { children: [
|
|
18397
|
+
/* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", "aria-label": `A\xE7\xF5es de ${entry.name}`, children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" }) }) }),
|
|
18398
|
+
/* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-56", children: [
|
|
18399
|
+
/* @__PURE__ */ jsx(DropdownLabel, { children: entry.name }),
|
|
18400
|
+
canEdit ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18401
|
+
/* @__PURE__ */ jsxs(DropdownItem, { disabled: index === 0, onSelect: () => onAct(entry, "move", { position: 1 }), children: [
|
|
18402
|
+
/* @__PURE__ */ jsx(ArrowUpToLine, { className: "mr-2 h-4 w-4" }),
|
|
18403
|
+
"Mover para o topo"
|
|
16120
18404
|
] }),
|
|
16121
|
-
|
|
16122
|
-
/* @__PURE__ */ jsx(
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
18405
|
+
/* @__PURE__ */ jsxs(DropdownItem, { disabled: index === 0, onSelect: () => onAct(entry, "move", { position: index }), children: [
|
|
18406
|
+
/* @__PURE__ */ jsx(ArrowUp, { className: "mr-2 h-4 w-4" }),
|
|
18407
|
+
"Subir uma posi\xE7\xE3o"
|
|
18408
|
+
] }),
|
|
18409
|
+
/* @__PURE__ */ jsxs(DropdownItem, { disabled: index === total - 1, onSelect: () => onAct(entry, "move", { position: index + 2 }), children: [
|
|
18410
|
+
/* @__PURE__ */ jsx(ArrowDown, { className: "mr-2 h-4 w-4" }),
|
|
18411
|
+
"Descer uma posi\xE7\xE3o"
|
|
18412
|
+
] }),
|
|
18413
|
+
/* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
18414
|
+
away ? /* @__PURE__ */ jsxs(DropdownItem, { onSelect: () => onAct(entry, "back"), children: [
|
|
18415
|
+
/* @__PURE__ */ jsx(Undo2, { className: "mr-2 h-4 w-4" }),
|
|
18416
|
+
"Voltou"
|
|
18417
|
+
] }) : /* @__PURE__ */ jsxs(DropdownSub, { children: [
|
|
18418
|
+
/* @__PURE__ */ jsxs(DropdownSubTrigger, { children: [
|
|
18419
|
+
/* @__PURE__ */ jsx(Coffee, { className: "mr-2 h-4 w-4" }),
|
|
18420
|
+
"Saiu (guarda o lugar)"
|
|
18421
|
+
] }),
|
|
18422
|
+
/* @__PURE__ */ jsx(DropdownSubContent, { children: AWAY_REASONS.map((reason) => /* @__PURE__ */ jsx(DropdownItem, { onSelect: () => onAct(entry, "away", { reason }), children: reason }, reason)) })
|
|
18423
|
+
] }),
|
|
18424
|
+
/* @__PURE__ */ jsxs(DropdownItem, { onSelect: () => onAct(entry, "start"), children: [
|
|
18425
|
+
/* @__PURE__ */ jsx(Play, { className: "mr-2 h-4 w-4" }),
|
|
18426
|
+
"Come\xE7ou um atendimento"
|
|
18427
|
+
] })
|
|
18428
|
+
] }) : null,
|
|
18429
|
+
canDelete ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18430
|
+
/* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
18431
|
+
/* @__PURE__ */ jsxs(DropdownItem, { className: "text-destructive focus:text-destructive", onSelect: () => onAct(entry, "leave"), children: [
|
|
18432
|
+
/* @__PURE__ */ jsx(LogOut, { className: "mr-2 h-4 w-4" }),
|
|
18433
|
+
"Tirar da fila"
|
|
18434
|
+
] })
|
|
18435
|
+
] }) : null
|
|
16128
18436
|
] })
|
|
16129
|
-
] }
|
|
16130
|
-
|
|
16131
|
-
] })
|
|
18437
|
+
] }) : null
|
|
18438
|
+
] })
|
|
18439
|
+
] });
|
|
16132
18440
|
}
|
|
16133
|
-
function
|
|
16134
|
-
|
|
18441
|
+
function AreaColumn({ group, canEdit, canDelete, onAct, onCall }) {
|
|
18442
|
+
const tone = toneOf(group.area.color);
|
|
18443
|
+
const firstAvailable = group.queue.find((entry) => entry.status === "available");
|
|
18444
|
+
const inTurn = group.queue.filter((entry) => entry.status === "available").length;
|
|
18445
|
+
return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-col rounded-2xl border", tone.column), "aria-label": `\xC1rea ${group.area.name}`, children: [
|
|
18446
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-3 border-b border-border/40 p-3", children: [
|
|
18447
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
18448
|
+
/* @__PURE__ */ jsxs("h2", { className: "flex min-w-0 items-center gap-2 text-base font-semibold text-foreground", children: [
|
|
18449
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("h-2.5 w-2.5 shrink-0 rounded-full", tone.dot) }),
|
|
18450
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: group.area.name })
|
|
18451
|
+
] }),
|
|
18452
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "secondary", className: "shrink-0 tabular-nums", children: [
|
|
18453
|
+
inTurn,
|
|
18454
|
+
" na vez"
|
|
18455
|
+
] })
|
|
18456
|
+
] }),
|
|
18457
|
+
canEdit ? /* @__PURE__ */ jsxs(
|
|
18458
|
+
"button",
|
|
18459
|
+
{
|
|
18460
|
+
type: "button",
|
|
18461
|
+
disabled: !firstAvailable,
|
|
18462
|
+
onClick: () => onCall(group),
|
|
18463
|
+
className: cn(
|
|
18464
|
+
"inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-45",
|
|
18465
|
+
tone.button
|
|
18466
|
+
),
|
|
18467
|
+
children: [
|
|
18468
|
+
/* @__PURE__ */ jsx(BellRing, { className: "h-4 w-4 shrink-0" }),
|
|
18469
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: firstAvailable ? `Chamar o pr\xF3ximo: ${firstName(firstAvailable.name)}` : "Ningu\xE9m dispon\xEDvel" })
|
|
18470
|
+
]
|
|
18471
|
+
}
|
|
18472
|
+
) : null
|
|
18473
|
+
] }),
|
|
18474
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-2 p-2.5", children: group.queue.length === 0 ? /* @__PURE__ */ jsx("p", { className: "rounded-xl border border-dashed border-border/70 px-3 py-4 text-center text-sm text-muted-foreground", children: "Ningu\xE9m na fila desta \xE1rea." }) : group.queue.map((entry, index) => /* @__PURE__ */ jsx(
|
|
18475
|
+
QueueCard,
|
|
18476
|
+
{
|
|
18477
|
+
entry,
|
|
18478
|
+
index,
|
|
18479
|
+
total: group.queue.length,
|
|
18480
|
+
area: group.area,
|
|
18481
|
+
canEdit,
|
|
18482
|
+
canDelete,
|
|
18483
|
+
onAct
|
|
18484
|
+
},
|
|
18485
|
+
entry.professionalId
|
|
18486
|
+
)) }),
|
|
18487
|
+
group.serving.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 border-t border-border/40 p-2.5", children: [
|
|
18488
|
+
/* @__PURE__ */ jsx("p", { className: "px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Em atendimento" }),
|
|
18489
|
+
group.serving.map((entry) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 rounded-lg bg-card/80 px-2.5 py-2", children: [
|
|
18490
|
+
/* @__PURE__ */ jsxs("p", { className: "min-w-0 truncate text-sm text-foreground", children: [
|
|
18491
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: firstName(entry.name) }),
|
|
18492
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
18493
|
+
" \xB7 ",
|
|
18494
|
+
sinceLabel(entry.since)
|
|
18495
|
+
] })
|
|
18496
|
+
] }),
|
|
18497
|
+
canEdit ? /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", className: "h-8 shrink-0", onClick: () => onAct(entry, "finish"), children: [
|
|
18498
|
+
/* @__PURE__ */ jsx(CheckCircle2, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
18499
|
+
"Terminou"
|
|
18500
|
+
] }) : null
|
|
18501
|
+
] }, entry.professionalId))
|
|
18502
|
+
] }) : null
|
|
18503
|
+
] });
|
|
16135
18504
|
}
|
|
16136
|
-
function
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
/* @__PURE__ */
|
|
16140
|
-
|
|
16141
|
-
/* @__PURE__ */
|
|
16142
|
-
|
|
16143
|
-
|
|
18505
|
+
function OutOfQueue({ board, canCreate, onJoin }) {
|
|
18506
|
+
const groups = board.areas.map((area) => ({ area, members: board.roster.filter((member) => member.area === area.key) })).filter((group) => group.members.length > 0);
|
|
18507
|
+
return /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
|
|
18508
|
+
/* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-foreground", children: "Fora da fila" }),
|
|
18509
|
+
groups.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Toda a equipe da unidade est\xE1 na fila." }) : /* @__PURE__ */ jsx("div", { className: "grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(20rem,1fr))]", children: groups.map(({ area, members }) => /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-2 p-3", children: [
|
|
18510
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [
|
|
18511
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("h-2.5 w-2.5 rounded-full", toneOf(area.color).dot) }),
|
|
18512
|
+
area.name
|
|
18513
|
+
] }),
|
|
18514
|
+
members.map((member) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 py-1.5", children: [
|
|
18515
|
+
/* @__PURE__ */ jsx(PersonAvatar, { name: member.name, src: member.avatarUrl, size: "sm" }),
|
|
18516
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-[9rem] flex-1", children: [
|
|
18517
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium leading-snug text-foreground", children: member.name }),
|
|
18518
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: member.appointmentsToday === 0 ? "Sem agenda hoje" : `${member.appointmentsToday} ${member.appointmentsToday === 1 ? "hor\xE1rio" : "hor\xE1rios"} hoje${member.nextAppointmentAt ? ` \xB7 pr\xF3ximo ${clock(member.nextAppointmentAt)}` : ""}` })
|
|
18519
|
+
] }),
|
|
18520
|
+
canCreate ? /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", className: "h-8 shrink-0", onClick: () => onJoin(member), children: [
|
|
18521
|
+
/* @__PURE__ */ jsx(UserPlus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
18522
|
+
"Colocar na fila"
|
|
18523
|
+
] }) : null
|
|
18524
|
+
] }, member.professionalId))
|
|
18525
|
+
] }) }, area.key)) })
|
|
18526
|
+
] });
|
|
18527
|
+
}
|
|
18528
|
+
function TodayTrail({ board }) {
|
|
18529
|
+
const [open, setOpen] = React23.useState(false);
|
|
18530
|
+
return /* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
|
|
18531
|
+
/* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-2 h-8", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: [
|
|
18532
|
+
/* @__PURE__ */ jsx(History, { className: "mr-2 h-4 w-4" }),
|
|
18533
|
+
"Movimentos de hoje (",
|
|
18534
|
+
board.events.length,
|
|
18535
|
+
")"
|
|
18536
|
+
] }),
|
|
18537
|
+
open ? board.events.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhum movimento hoje." }) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "divide-y divide-border/60 p-0", children: board.events.map((event) => /* @__PURE__ */ jsxs("div", { className: "flex gap-3 px-4 py-2 text-sm", children: [
|
|
18538
|
+
/* @__PURE__ */ jsx("span", { className: "w-11 shrink-0 tabular-nums text-muted-foreground", children: clock(event.createdAt) }),
|
|
18539
|
+
/* @__PURE__ */ jsxs("p", { className: "min-w-0 text-foreground", children: [
|
|
18540
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: event.name }),
|
|
18541
|
+
" ",
|
|
18542
|
+
eventPhrase(event)
|
|
18543
|
+
] })
|
|
18544
|
+
] }, event.id)) }) }) : null
|
|
18545
|
+
] });
|
|
18546
|
+
}
|
|
18547
|
+
function TvBoard({ groups, onFullscreen }) {
|
|
18548
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
18549
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
18550
|
+
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold text-foreground", children: "Lista da vez" }),
|
|
18551
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onFullscreen, children: [
|
|
18552
|
+
/* @__PURE__ */ jsx(Maximize2, { className: "mr-2 h-4 w-4" }),
|
|
18553
|
+
"Tela cheia"
|
|
18554
|
+
] })
|
|
18555
|
+
] }),
|
|
18556
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-4 [grid-template-columns:repeat(auto-fill,minmax(14rem,1fr))]", children: groups.map((group) => {
|
|
18557
|
+
const tone = toneOf(group.area.color);
|
|
18558
|
+
const next = group.queue.find((entry) => entry.status === "available");
|
|
18559
|
+
return /* @__PURE__ */ jsxs("section", { className: cn("min-w-0 rounded-2xl border p-5", tone.column), children: [
|
|
18560
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-lg font-semibold text-foreground", children: [
|
|
18561
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("h-3 w-3 rounded-full", tone.dot) }),
|
|
18562
|
+
group.area.name
|
|
18563
|
+
] }),
|
|
18564
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Pr\xF3ximo da vez" }),
|
|
18565
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 break-words text-4xl font-bold leading-tight text-foreground", children: next ? next.name : "\u2014" }),
|
|
18566
|
+
/* @__PURE__ */ jsx("ol", { className: "mt-4 space-y-1.5", children: group.queue.map((entry, index) => /* @__PURE__ */ jsxs("li", { className: cn("flex items-baseline gap-3 text-2xl", entry.status === "away" ? "text-muted-foreground line-through decoration-1" : "text-foreground"), children: [
|
|
18567
|
+
/* @__PURE__ */ jsx("span", { className: "w-6 shrink-0 text-right tabular-nums text-muted-foreground", children: index + 1 }),
|
|
18568
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: entry.name })
|
|
18569
|
+
] }, entry.professionalId)) })
|
|
18570
|
+
] }, group.area.key);
|
|
18571
|
+
}) })
|
|
18572
|
+
] });
|
|
16144
18573
|
}
|
|
16145
|
-
function TurnQueuePage({ provider: provider2,
|
|
18574
|
+
function TurnQueuePage({ provider: provider2, title }) {
|
|
16146
18575
|
const unitId = useActiveUnitId();
|
|
16147
|
-
const
|
|
16148
|
-
const
|
|
16149
|
-
const
|
|
18576
|
+
const can = usePermissionOptional();
|
|
18577
|
+
const canEdit = can("agenda.turn_queue", "edit");
|
|
18578
|
+
const canCreate = can("agenda.turn_queue", "create");
|
|
18579
|
+
const canDelete = can("agenda.turn_queue", "delete");
|
|
18580
|
+
const [board, setBoard] = React23.useState(null);
|
|
16150
18581
|
const [error, setError] = React23.useState(null);
|
|
18582
|
+
const [tv, setTv] = React23.useState(readTvFlag);
|
|
18583
|
+
const [configOpen, setConfigOpen] = React23.useState(false);
|
|
18584
|
+
const [skipAsk, setSkipAsk] = React23.useState(null);
|
|
16151
18585
|
const [, tick] = React23.useState(0);
|
|
18586
|
+
const tvRef = React23.useRef(null);
|
|
16152
18587
|
const load = React23.useCallback(async () => {
|
|
16153
18588
|
if (!unitId) {
|
|
16154
|
-
|
|
18589
|
+
setBoard(null);
|
|
16155
18590
|
return;
|
|
16156
18591
|
}
|
|
16157
|
-
setLoading(true);
|
|
16158
18592
|
try {
|
|
16159
|
-
|
|
18593
|
+
const next = await provider2.getBoard(unitId);
|
|
18594
|
+
setBoard(next);
|
|
16160
18595
|
setError(null);
|
|
18596
|
+
const leader = next.entries.filter((e) => e.status === "available").sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0))[0];
|
|
18597
|
+
publishTurnQueueLeader(unitId, leader?.professionalId ?? null);
|
|
16161
18598
|
} catch (cause) {
|
|
16162
|
-
setError(
|
|
16163
|
-
} finally {
|
|
16164
|
-
setLoading(false);
|
|
18599
|
+
setError(turnErrorMessage(cause));
|
|
16165
18600
|
}
|
|
16166
18601
|
}, [provider2, unitId]);
|
|
16167
18602
|
React23.useEffect(() => {
|
|
18603
|
+
setBoard(null);
|
|
16168
18604
|
void load();
|
|
16169
18605
|
}, [load]);
|
|
16170
|
-
React23.useEffect(() => {
|
|
16171
|
-
if (!unitId) {
|
|
16172
|
-
setAgendaProfessionals([]);
|
|
16173
|
-
return;
|
|
16174
|
-
}
|
|
16175
|
-
let active = true;
|
|
16176
|
-
void agendaProvider.getProfessionals({ unitId }).then((rows2) => {
|
|
16177
|
-
if (active) setAgendaProfessionals(rows2);
|
|
16178
|
-
}).catch(() => {
|
|
16179
|
-
if (active) setAgendaProfessionals([]);
|
|
16180
|
-
});
|
|
16181
|
-
return () => {
|
|
16182
|
-
active = false;
|
|
16183
|
-
};
|
|
16184
|
-
}, [agendaProvider, unitId]);
|
|
16185
18606
|
React23.useEffect(() => provider2.subscribe(() => {
|
|
16186
18607
|
void load();
|
|
16187
18608
|
}), [provider2, load]);
|
|
16188
18609
|
React23.useEffect(() => {
|
|
16189
|
-
const timer = window.setInterval(() => tick((value) => value + 1),
|
|
18610
|
+
const timer = window.setInterval(() => tick((value) => value + 1), 3e4);
|
|
16190
18611
|
return () => window.clearInterval(timer);
|
|
16191
18612
|
}, []);
|
|
16192
|
-
const
|
|
16193
|
-
|
|
16194
|
-
|
|
16195
|
-
|
|
16196
|
-
|
|
16197
|
-
|
|
16198
|
-
|
|
16199
|
-
|
|
16200
|
-
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
|
|
16205
|
-
|
|
16206
|
-
|
|
16207
|
-
|
|
16208
|
-
|
|
16209
|
-
|
|
16210
|
-
|
|
18613
|
+
const groups = React23.useMemo(() => board ? groupByArea(board) : [], [board]);
|
|
18614
|
+
const act = React23.useCallback(async (entry, action, extra) => {
|
|
18615
|
+
if (!unitId) return;
|
|
18616
|
+
try {
|
|
18617
|
+
const result = await provider2.act({ unitId, professionalId: entry.professionalId, action, ...extra });
|
|
18618
|
+
const who = entry.name;
|
|
18619
|
+
if (action === "join") toast.success(`${who} entrou no fim da fila.`);
|
|
18620
|
+
else if (action === "leave") toast.success(`${who} saiu da fila.`);
|
|
18621
|
+
else if (action === "away") toast.success(`${who} saiu (${extra?.reason}). O lugar fica guardado.`);
|
|
18622
|
+
else if (action === "back") toast.success(`${who} voltou.`);
|
|
18623
|
+
else if (action === "start") toast.success(`${who} come\xE7ou um atendimento.`);
|
|
18624
|
+
else if (action === "finish") toast.success(`${who} voltou para o fim da fila.`);
|
|
18625
|
+
else if (action === "call") {
|
|
18626
|
+
toast.success(`${who} foi chamado(a).`);
|
|
18627
|
+
if (result.notified) {
|
|
18628
|
+
toast.success(`${who} foi avisado(a) no sino. Marque o encaixe na agenda com o profissional.`, {
|
|
18629
|
+
action: { label: "Abrir agenda", onClick: () => openBooking(unitId, entry.professionalId) }
|
|
18630
|
+
});
|
|
18631
|
+
}
|
|
18632
|
+
}
|
|
18633
|
+
await load();
|
|
18634
|
+
} catch (cause) {
|
|
18635
|
+
toast.error(turnErrorMessage(cause));
|
|
16211
18636
|
}
|
|
16212
|
-
|
|
16213
|
-
|
|
18637
|
+
}, [load, provider2, unitId]);
|
|
18638
|
+
const call = React23.useCallback((group) => {
|
|
18639
|
+
const next = group.queue.find((entry) => entry.status === "available");
|
|
18640
|
+
if (!next) return;
|
|
18641
|
+
const away = group.queue.slice(0, group.queue.indexOf(next)).find((entry) => entry.status === "away");
|
|
18642
|
+
if (away) {
|
|
18643
|
+
setSkipAsk({ away, next });
|
|
18644
|
+
return;
|
|
16214
18645
|
}
|
|
16215
|
-
|
|
16216
|
-
|
|
16217
|
-
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
18646
|
+
void act(next, "call");
|
|
18647
|
+
}, [act]);
|
|
18648
|
+
const confirmSkip = async () => {
|
|
18649
|
+
if (!skipAsk) return;
|
|
18650
|
+
const { away, next } = skipAsk;
|
|
18651
|
+
setSkipAsk(null);
|
|
16221
18652
|
if (!unitId) return;
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16226
|
-
|
|
16227
|
-
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
18653
|
+
try {
|
|
18654
|
+
await provider2.act({ unitId, professionalId: away.professionalId, action: "skip", reason: away.awayReason ?? void 0 });
|
|
18655
|
+
} catch (cause) {
|
|
18656
|
+
toast.error(turnErrorMessage(cause));
|
|
18657
|
+
return;
|
|
18658
|
+
}
|
|
18659
|
+
await act(next, "call");
|
|
18660
|
+
};
|
|
18661
|
+
const toggleTv = () => {
|
|
18662
|
+
setTv((value) => {
|
|
18663
|
+
writeTvFlag(!value);
|
|
18664
|
+
return !value;
|
|
18665
|
+
});
|
|
18666
|
+
};
|
|
18667
|
+
const header = /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2 lg:justify-end", children: [
|
|
18668
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "h-8 gap-1.5 rounded-full border-success/30 px-3 text-success", children: [
|
|
18669
|
+
/* @__PURE__ */ jsx(Radio, { className: "h-3.5 w-3.5" }),
|
|
18670
|
+
"Ao vivo"
|
|
18671
|
+
] }),
|
|
18672
|
+
/* @__PURE__ */ jsxs(Button, { variant: tv ? "secondary" : "outline", size: "sm", onClick: toggleTv, children: [
|
|
18673
|
+
/* @__PURE__ */ jsx(Monitor, { className: "mr-2 h-4 w-4" }),
|
|
18674
|
+
tv ? "Quadro" : "Modo TV"
|
|
18675
|
+
] }),
|
|
18676
|
+
can("agenda.turn_queue", "read") ? /* @__PURE__ */ jsx(IconAction, { label: "Relat\xF3rio da lista da vez", children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", "aria-label": "Relat\xF3rio da lista da vez", onClick: () => adminNavigateTo("/agenda/turn-queue/report"), children: [
|
|
18677
|
+
/* @__PURE__ */ jsx(BarChart3, { className: "h-4 w-4 sm:mr-2" }),
|
|
18678
|
+
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Relat\xF3rio" })
|
|
18679
|
+
] }) }) : null,
|
|
18680
|
+
canEdit ? /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setConfigOpen(true), children: [
|
|
18681
|
+
/* @__PURE__ */ jsx(SlidersHorizontal, { className: "mr-2 h-4 w-4" }),
|
|
18682
|
+
"Configurar"
|
|
18683
|
+
] }) : null
|
|
18684
|
+
] });
|
|
18685
|
+
let body;
|
|
18686
|
+
if (!unitId) {
|
|
18687
|
+
body = /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex min-h-64 flex-col items-center justify-center gap-3 text-center", children: [
|
|
18688
|
+
/* @__PURE__ */ jsx(MapPin, { className: "h-9 w-9 text-muted-foreground" }),
|
|
18689
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
18690
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: "Escolha uma unidade" }),
|
|
18691
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "A lista da vez sempre pertence a uma unidade." })
|
|
18692
|
+
] })
|
|
18693
|
+
] }) });
|
|
18694
|
+
} else if (error && !board) {
|
|
18695
|
+
body = /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-6 text-sm text-destructive", children: error }) });
|
|
18696
|
+
} else if (!board) {
|
|
18697
|
+
body = /* @__PURE__ */ jsx(BoardSkeleton, {});
|
|
18698
|
+
} else if (!board.enabled) {
|
|
18699
|
+
body = /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex min-h-52 flex-col items-center justify-center gap-3 text-center", children: [
|
|
18700
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: "A lista da vez est\xE1 desligada nesta unidade" }),
|
|
18701
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: "Ligue no cadastro da unidade, na aba Lista da vez, na unidade que vai usar a fila." }),
|
|
18702
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => adminNavigateTo("/settings/units"), children: "Abrir unidades" })
|
|
18703
|
+
] }) });
|
|
18704
|
+
} else if (tv) {
|
|
18705
|
+
body = /* @__PURE__ */ jsx("div", { ref: tvRef, className: "rounded-2xl bg-background p-1 [&:fullscreen]:overflow-auto [&:fullscreen]:p-8", children: /* @__PURE__ */ jsx(TvBoard, { groups, onFullscreen: () => {
|
|
18706
|
+
void tvRef.current?.requestFullscreen?.();
|
|
18707
|
+
} }) });
|
|
18708
|
+
} else {
|
|
18709
|
+
body = /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
18710
|
+
board.requireLocation && !board.locationConfigured ? /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm text-foreground", children: [
|
|
18711
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium", children: "Localiza\xE7\xE3o da loja pendente" }),
|
|
18712
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: 'O "Cheguei" exige estar na loja, mas a unidade ainda n\xE3o tem endere\xE7o com coordenada.' })
|
|
18713
|
+
] }) : null,
|
|
18714
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(15rem,1fr))]", children: groups.map((group) => /* @__PURE__ */ jsx(
|
|
18715
|
+
AreaColumn,
|
|
18716
|
+
{
|
|
18717
|
+
group,
|
|
18718
|
+
canEdit,
|
|
18719
|
+
canDelete,
|
|
18720
|
+
onAct: (entry, action, extra) => {
|
|
18721
|
+
void act(entry, action, extra);
|
|
18722
|
+
},
|
|
18723
|
+
onCall: call
|
|
18724
|
+
},
|
|
18725
|
+
group.area.key
|
|
18726
|
+
)) }),
|
|
18727
|
+
/* @__PURE__ */ jsx(OutOfQueue, { board, canCreate, onJoin: (member) => {
|
|
18728
|
+
void act(member, "join");
|
|
18729
|
+
} }),
|
|
18730
|
+
/* @__PURE__ */ jsx(TodayTrail, { board })
|
|
18731
|
+
] });
|
|
18732
|
+
}
|
|
18733
|
+
return /* @__PURE__ */ jsxs(ModulePage, { title, nav: [], navVariant: "sidebar", showHeader: false, children: [
|
|
18734
|
+
/* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-[96rem] space-y-4", children: [
|
|
18735
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between", children: [
|
|
18736
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-3xl text-xs leading-relaxed text-muted-foreground", children: LEGEND }),
|
|
18737
|
+
header
|
|
18738
|
+
] }),
|
|
18739
|
+
body
|
|
18740
|
+
] }) }),
|
|
18741
|
+
/* @__PURE__ */ jsx(Modal, { open: !!skipAsk, onOpenChange: (open) => {
|
|
18742
|
+
if (!open) setSkipAsk(null);
|
|
18743
|
+
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
|
|
18744
|
+
/* @__PURE__ */ jsxs(ModalHeader, { children: [
|
|
18745
|
+
/* @__PURE__ */ jsx(ModalTitle, { children: "Quem est\xE1 na vez saiu" }),
|
|
18746
|
+
skipAsk ? /* @__PURE__ */ jsxs(ModalDescription, { children: [
|
|
18747
|
+
skipAsk.away.name,
|
|
18748
|
+
" (",
|
|
18749
|
+
skipAsk.away.awayReason ?? "Ausente",
|
|
18750
|
+
", ",
|
|
18751
|
+
sinceLabel(skipAsk.away.since),
|
|
18752
|
+
") est\xE1 na frente.",
|
|
18753
|
+
" ",
|
|
18754
|
+
"Pedir para o cliente esperar guarda a vez; chamar ",
|
|
18755
|
+
firstName(skipAsk.next.name),
|
|
18756
|
+
" registra o pulo."
|
|
18757
|
+
] }) : null
|
|
18758
|
+
] }),
|
|
18759
|
+
/* @__PURE__ */ jsx(ModalBody, { className: "hidden" }),
|
|
18760
|
+
/* @__PURE__ */ jsxs(ModalFooter, { children: [
|
|
18761
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => setSkipAsk(null), children: "Esperar" }),
|
|
18762
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => {
|
|
18763
|
+
void confirmSkip();
|
|
18764
|
+
}, children: skipAsk ? `Chamar ${firstName(skipAsk.next.name)}` : "Chamar" })
|
|
18765
|
+
] })
|
|
18766
|
+
] }) }),
|
|
18767
|
+
configOpen && unitId ? /* @__PURE__ */ jsx(
|
|
18768
|
+
TurnQueueConfigDialog,
|
|
18769
|
+
{
|
|
18770
|
+
provider: provider2,
|
|
18771
|
+
unitId,
|
|
18772
|
+
onClose: () => setConfigOpen(false),
|
|
18773
|
+
onSaved: () => {
|
|
18774
|
+
setConfigOpen(false);
|
|
18775
|
+
void load();
|
|
16231
18776
|
}
|
|
16232
|
-
}
|
|
16233
|
-
|
|
16234
|
-
}
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
18777
|
+
}
|
|
18778
|
+
) : null
|
|
18779
|
+
] });
|
|
18780
|
+
}
|
|
18781
|
+
function ymd(d) {
|
|
18782
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
18783
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
18784
|
+
}
|
|
18785
|
+
var COLUMNS = [
|
|
18786
|
+
{ key: "entradas", label: "Entradas na fila" },
|
|
18787
|
+
{ key: "atendimentos", label: "Atendimentos da vez" },
|
|
18788
|
+
{ key: "servicosRapidos", label: "Servi\xE7os r\xE1pidos" },
|
|
18789
|
+
{ key: "chamados", label: "Chamadas" },
|
|
18790
|
+
{ key: "pulos", label: "Pulos" },
|
|
18791
|
+
{ key: "ausencias", label: "Aus\xEAncias" },
|
|
18792
|
+
{ key: "minutosAusente", label: "Minutos ausente" }
|
|
18793
|
+
];
|
|
18794
|
+
function Kpi({ label, value }) {
|
|
18795
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4", children: [
|
|
18796
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: label }),
|
|
18797
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-2xl font-semibold tabular-nums text-foreground", children: value })
|
|
18798
|
+
] }) });
|
|
18799
|
+
}
|
|
18800
|
+
function TurnQueueReportPage({ provider: provider2 }) {
|
|
18801
|
+
const activeUnit = useActiveUnitId();
|
|
18802
|
+
const today = /* @__PURE__ */ new Date();
|
|
18803
|
+
const [from, setFrom] = React23.useState(ymd(new Date(today.getFullYear(), today.getMonth(), 1)));
|
|
18804
|
+
const [to, setTo] = React23.useState(ymd(new Date(today.getFullYear(), today.getMonth() + 1, 0)));
|
|
18805
|
+
const [units, setUnits] = React23.useState([]);
|
|
18806
|
+
const [selected, setSelected] = React23.useState(activeUnit ? [activeUnit] : []);
|
|
18807
|
+
const [rows, setRows] = React23.useState(null);
|
|
18808
|
+
const [error, setError] = React23.useState(null);
|
|
18809
|
+
React23.useEffect(() => {
|
|
18810
|
+
const db = getSupabaseClientOptional();
|
|
18811
|
+
void db?.from("v_units").select("id, name").eq("is_active", true).order("name").then(({ data }) => setUnits(data ?? []));
|
|
18812
|
+
}, []);
|
|
18813
|
+
React23.useEffect(() => {
|
|
18814
|
+
let alive = true;
|
|
18815
|
+
setRows(null);
|
|
18816
|
+
provider2.getReport({ unitIds: selected.length ? selected : null, from, to }).then((next) => {
|
|
18817
|
+
if (alive) {
|
|
18818
|
+
setRows(next);
|
|
18819
|
+
setError(null);
|
|
18820
|
+
}
|
|
18821
|
+
}).catch((cause) => {
|
|
18822
|
+
if (alive) setError(turnErrorMessage(cause));
|
|
18823
|
+
});
|
|
18824
|
+
return () => {
|
|
18825
|
+
alive = false;
|
|
18826
|
+
};
|
|
18827
|
+
}, [provider2, selected, from, to]);
|
|
18828
|
+
const served = rows?.filter((r) => r.atendimentos > 0) ?? [];
|
|
18829
|
+
const total = rows?.reduce((s, r) => s + r.atendimentos, 0) ?? 0;
|
|
18830
|
+
const spread = rows && rows.length ? Math.max(...rows.map((r) => r.atendimentos)) - Math.min(...rows.map((r) => r.atendimentos)) : 0;
|
|
18831
|
+
const skips = rows?.reduce((s, r) => s + r.pulos, 0) ?? 0;
|
|
18832
|
+
const peak = Math.max(1, ...(rows ?? []).map((r) => r.atendimentos));
|
|
18833
|
+
const exportXlsx = async () => {
|
|
18834
|
+
if (!rows?.length) return;
|
|
18835
|
+
try {
|
|
18836
|
+
const report = await createXlsxReport({ title: "Lista da vez", subtitle: `De ${from} at\xE9 ${to}`, landscape: true });
|
|
18837
|
+
const sheet = report.sheet("Lista da vez", [
|
|
18838
|
+
{ header: "Profissional", width: 28 },
|
|
18839
|
+
{ header: "Unidade", width: 24 },
|
|
18840
|
+
...COLUMNS.map((c) => ({ header: c.label, type: "number", width: 14 }))
|
|
18841
|
+
]);
|
|
18842
|
+
sheet.header();
|
|
18843
|
+
sheet.table(rows.map((r) => [r.professionalName, r.unitName, ...COLUMNS.map((c) => Number(r[c.key]))]), { filter: true });
|
|
18844
|
+
await report.download(`lista-da-vez-${from}-${to}`);
|
|
18845
|
+
} catch (cause) {
|
|
18846
|
+
toast.error(String(cause?.message ?? "N\xE3o foi poss\xEDvel exportar."));
|
|
18847
|
+
}
|
|
18848
|
+
};
|
|
18849
|
+
return /* @__PURE__ */ jsx(ModulePage, { title: "Relat\xF3rio da lista da vez", nav: [], navVariant: "sidebar", showHeader: false, children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-6xl space-y-4", children: [
|
|
18850
|
+
/* @__PURE__ */ jsx(
|
|
18851
|
+
SubpageHeader,
|
|
18852
|
+
{
|
|
18853
|
+
title: "Relat\xF3rio da lista da vez",
|
|
18854
|
+
description: "Por profissional: entradas na fila, atendimentos da vez, servi\xE7os r\xE1pidos, chamadas, pulos e aus\xEAncias no per\xEDodo \u2014 a justi\xE7a da fila.",
|
|
18855
|
+
parentLabel: "Lista da vez",
|
|
18856
|
+
onBack: () => adminNavigateTo("/agenda/turn-queue"),
|
|
18857
|
+
actions: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", disabled: !rows?.length, onClick: () => {
|
|
18858
|
+
void exportXlsx();
|
|
18859
|
+
}, children: [
|
|
18860
|
+
/* @__PURE__ */ jsx(Download, { className: "mr-2 h-4 w-4" }),
|
|
18861
|
+
"Exportar"
|
|
18862
|
+
] })
|
|
18863
|
+
}
|
|
18864
|
+
),
|
|
18865
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "grid gap-3 p-4 sm:grid-cols-[repeat(2,minmax(0,11rem))_minmax(0,1fr)]", children: [
|
|
18866
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
18867
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "De" }),
|
|
18868
|
+
/* @__PURE__ */ jsx(DatePicker, { value: from, onChange: setFrom })
|
|
16258
18869
|
] }),
|
|
16259
|
-
|
|
16260
|
-
/* @__PURE__ */ jsx(
|
|
16261
|
-
/* @__PURE__ */ jsx(
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
|
|
16274
|
-
|
|
16275
|
-
|
|
16276
|
-
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
16280
|
-
|
|
16281
|
-
|
|
16282
|
-
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16292
|
-
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
|
|
16296
|
-
|
|
16297
|
-
|
|
16298
|
-
}) }),
|
|
16299
|
-
(snapshot?.events.length ?? 0) > 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4", children: [
|
|
16300
|
-
/* @__PURE__ */ jsx("h2", { className: "mb-3 text-sm font-semibold", children: "Movimentos de hoje" }),
|
|
16301
|
-
/* @__PURE__ */ jsx("div", { className: "grid gap-2", children: snapshot.events.slice(0, 12).map((event) => /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4 border-b border-border/60 py-2 text-sm last:border-0", children: [
|
|
16302
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
16303
|
-
/* @__PURE__ */ jsx("p", { className: "font-medium", children: event.professionalName }),
|
|
16304
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: event.reason ?? event.eventType.replace(/_/g, " ") })
|
|
16305
|
-
] }),
|
|
16306
|
-
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: new Date(event.occurredAt).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }) })
|
|
16307
|
-
] }, event.id)) })
|
|
16308
|
-
] }) })
|
|
18870
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
18871
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "At\xE9" }),
|
|
18872
|
+
/* @__PURE__ */ jsx(DatePicker, { value: to, onChange: setTo })
|
|
18873
|
+
] }),
|
|
18874
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
18875
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Unidades" }),
|
|
18876
|
+
/* @__PURE__ */ jsx(MultiSelect, { options: units.map((u) => ({ value: u.id, label: u.name })), value: selected, onChange: setSelected, allLabel: "Todas as unidades" })
|
|
18877
|
+
] })
|
|
18878
|
+
] }) }),
|
|
18879
|
+
error ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-6 text-sm text-destructive", children: error }) }) : !rows ? /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-4", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-20 rounded-xl" }, i)) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18880
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-4", children: [
|
|
18881
|
+
/* @__PURE__ */ jsx(Kpi, { label: "Atendimentos da vez", value: total }),
|
|
18882
|
+
/* @__PURE__ */ jsx(Kpi, { label: "Profissionais que atenderam", value: served.length }),
|
|
18883
|
+
/* @__PURE__ */ jsx(Kpi, { label: "Diferen\xE7a entre quem mais e quem menos atendeu", value: spread }),
|
|
18884
|
+
/* @__PURE__ */ jsx(Kpi, { label: "Pulos", value: skips })
|
|
18885
|
+
] }),
|
|
18886
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-3 p-4", children: [
|
|
18887
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [
|
|
18888
|
+
/* @__PURE__ */ jsx(BarChart3, { className: "h-4 w-4 text-muted-foreground" }),
|
|
18889
|
+
"Atendimentos da vez por profissional"
|
|
18890
|
+
] }),
|
|
18891
|
+
rows.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nenhum movimento da fila no per\xEDodo." }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: [...rows].sort((a, b) => b.atendimentos - a.atendimentos).map((r) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[minmax(0,10rem)_minmax(0,1fr)_2.5rem] items-center gap-3 text-sm", children: [
|
|
18892
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: r.professionalName }),
|
|
18893
|
+
/* @__PURE__ */ jsx("span", { className: "h-2.5 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx("span", { className: "block h-full rounded-full bg-primary", style: { width: `${r.atendimentos / peak * 100}%` } }) }),
|
|
18894
|
+
/* @__PURE__ */ jsx("span", { className: "text-right tabular-nums text-muted-foreground", children: r.atendimentos })
|
|
18895
|
+
] }, `${r.unitId}:${r.professionalId}`)) })
|
|
18896
|
+
] }) }),
|
|
18897
|
+
rows.length > 0 ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "overflow-x-auto p-0", children: /* @__PURE__ */ jsxs("table", { className: "w-full min-w-[56rem] text-sm", children: [
|
|
18898
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b border-border/70 text-left text-xs text-muted-foreground", children: [
|
|
18899
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-2.5 font-medium", children: "Profissional" }),
|
|
18900
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-2.5 font-medium", children: "Unidade" }),
|
|
18901
|
+
COLUMNS.map((c) => /* @__PURE__ */ jsx("th", { className: "px-3 py-2.5 text-right font-medium", children: c.label }, c.key))
|
|
18902
|
+
] }) }),
|
|
18903
|
+
/* @__PURE__ */ jsx("tbody", { className: "divide-y divide-border/50", children: rows.map((r) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
18904
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-2 text-foreground", children: r.professionalName }),
|
|
18905
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-2 text-muted-foreground", children: r.unitName }),
|
|
18906
|
+
COLUMNS.map((c) => /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-right tabular-nums text-foreground", children: Number(r[c.key]) }, c.key))
|
|
18907
|
+
] }, `${r.unitId}:${r.professionalId}`)) })
|
|
18908
|
+
] }) }) }) : null
|
|
16309
18909
|
] })
|
|
16310
18910
|
] }) });
|
|
16311
18911
|
}
|
|
@@ -16319,9 +18919,10 @@ function UnitTurnQueueWidget({ item }) {
|
|
|
16319
18919
|
if (!unitId) return;
|
|
16320
18920
|
let alive = true;
|
|
16321
18921
|
const supabase = getSupabaseClientOptional();
|
|
16322
|
-
void supabase?.
|
|
18922
|
+
void supabase?.rpc("agenda_turn_queue_config", { p_unit_id: unitId }).then(({ data }) => {
|
|
16323
18923
|
if (!alive) return;
|
|
16324
|
-
const
|
|
18924
|
+
const u = data?.unit;
|
|
18925
|
+
const next = { is_active: u?.enabled ?? false, geofence_required: u?.requireLocation ?? false, geofence_radius_meters: u?.radiusM ?? 80 };
|
|
16325
18926
|
setRow(next);
|
|
16326
18927
|
setRadius(String(next.geofence_radius_meters));
|
|
16327
18928
|
setLoading(false);
|
|
@@ -16413,14 +19014,11 @@ function createAgendaTurnQueuePlugin(options = {}) {
|
|
|
16413
19014
|
() => createSupabaseTurnQueueProvider(),
|
|
16414
19015
|
() => createMockTurnQueueProvider()
|
|
16415
19016
|
);
|
|
16416
|
-
const
|
|
16417
|
-
() => createSupabaseAgendaProvider(),
|
|
16418
|
-
() => createMockAgendaProvider()
|
|
16419
|
-
);
|
|
16420
|
-
const menu = options.labels?.menu ?? "Pr\xF3ximo da vez";
|
|
19017
|
+
const menu = options.labels?.menu ?? "Lista da vez";
|
|
16421
19018
|
const title = options.labels?.title ?? menu;
|
|
16422
|
-
const subtitle = options.labels?.subtitle ?? "
|
|
16423
|
-
const Page = (() => React23.createElement(TurnQueuePage, { provider: provider2,
|
|
19019
|
+
const subtitle = options.labels?.subtitle ?? "A fila justa do cliente sem prefer\xEAncia, por \xE1rea";
|
|
19020
|
+
const Page = (() => React23.createElement(TurnQueuePage, { provider: provider2, title, subtitle }));
|
|
19021
|
+
const ReportPage = (() => React23.createElement(TurnQueueReportPage, { provider: provider2 }));
|
|
16424
19022
|
const HeaderControl = (() => React23.createElement(AvailabilityHeaderControl, { provider: provider2 }));
|
|
16425
19023
|
return {
|
|
16426
19024
|
id: "agenda-turn-queue",
|
|
@@ -16438,7 +19036,7 @@ function createAgendaTurnQueuePlugin(options = {}) {
|
|
|
16438
19036
|
id: "agenda.turn_queue",
|
|
16439
19037
|
label: menu,
|
|
16440
19038
|
group: "Agenda",
|
|
16441
|
-
actions: ["read", "create", "edit", "manage"]
|
|
19039
|
+
actions: ["read", "create", "edit", "delete", "manage"]
|
|
16442
19040
|
}],
|
|
16443
19041
|
navigation: [{
|
|
16444
19042
|
section: options.navSection ?? "main",
|
|
@@ -16453,6 +19051,11 @@ function createAgendaTurnQueuePlugin(options = {}) {
|
|
|
16453
19051
|
component: Page,
|
|
16454
19052
|
permission: { feature: "agenda", action: "read" },
|
|
16455
19053
|
title
|
|
19054
|
+
}, {
|
|
19055
|
+
path: "/agenda/turn-queue/report",
|
|
19056
|
+
component: ReportPage,
|
|
19057
|
+
permission: { feature: "agenda", action: "read" },
|
|
19058
|
+
title: "Relat\xF3rio da lista da vez"
|
|
16456
19059
|
}],
|
|
16457
19060
|
// Lives on the add-on, not on agenda: the unit tab exists only while this
|
|
16458
19061
|
// module is enabled in settings/modules.
|