Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica

- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind
- Frontend Homenest (versión alternativa)
- Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp)
- WACRM fork (Next.js 16 + Supabase)
- Hermes + Bridge + Skills (Qwen3.6 via Nan Builders)
- Scripts de migración y operación
- Documentación extensiva en docs/
This commit is contained in:
2026-07-20 07:44:23 +00:00
commit a718592291
699 changed files with 324602 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import type { Notification } from "@/types";
/**
* Count of unread notifications for the current user. Used by the
* sidebar to surface a badge on the Notifications nav entry.
*
* RLS on `notifications` already scopes every read to `auth.uid() =
* user_id`, so no explicit filter is needed here — same pattern as
* `useTotalUnread` for conversations.
*/
export function useUnreadNotifications(): number {
const [count, setCount] = useState(0);
useEffect(() => {
const supabase = createClient();
let cancelled = false;
(async () => {
// head:true skips fetching rows — we only need the `count`
// supabase-js returns alongside the (empty) response body.
const { count: unreadCount, error } = await supabase
.from("notifications")
.select("*", { count: "exact", head: true })
.is("read_at", null);
if (cancelled || error) return;
setCount(unreadCount ?? 0);
})();
const channel = supabase
.channel("notifications-unread-count")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "notifications" },
(payload) => {
if (payload.eventType === "INSERT") {
const row = payload.new as Notification;
if (!row.read_at) setCount((n) => n + 1);
} else if (payload.eventType === "UPDATE") {
// Updates here only ever set read_at (marking a notification
// read). Derive purely from the new row so we don't rely on
// payload.old columns, which require REPLICA IDENTITY FULL.
const newRow = payload.new as Notification;
if (newRow.read_at) setCount((n) => Math.max(0, n - 1));
} else if (payload.eventType === "DELETE") {
const oldRow = payload.old as Partial<Notification>;
if (!oldRow.read_at) setCount((n) => Math.max(0, n - 1));
}
},
)
.subscribe();
return () => {
cancelled = true;
supabase.removeChannel(channel);
};
}, []);
return count;
}