Files
ladill-care/resources/js/app.js
T
isaaccladandCursor 2d06c92ddb
Deploy Ladill Care / deploy (push) Successful in 38s
Add Meet video visit scheduling for Care appointments.
Schedule and start video visits via the Meet service API, persist join links on appointments, and propagate the shared copy-button component.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 22:53:42 +00:00

175 lines
6.4 KiB
JavaScript

import Alpine from 'alpinejs';
import { registerLadillClipboard } from './ladill-clipboard';
import collapse from '@alpinejs/collapse';
Alpine.plugin(collapse);
registerLadillClipboard(Alpine);
// In-app notification bell + dropdown.
Alpine.data('notificationDropdown', (config = {}) => ({
open: false,
loading: false,
notifications: [],
unreadCount: 0,
unreadUrl: config.unreadUrl || '/notifications/unread',
markReadUrl: config.markReadUrl || '/notifications/__ID__/read',
markAllReadUrl: config.markAllReadUrl || '/notifications/mark-all-read',
indexUrl: config.indexUrl || '/notifications',
csrfToken: config.csrfToken || document.querySelector('meta[name="csrf-token"]')?.content || '',
init() {
this.fetchUnread();
setInterval(() => this.fetchUnread(), 60000);
},
async fetchUnread() {
try {
const res = await fetch(this.unreadUrl, {
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
});
const data = await res.json();
this.notifications = data.notifications || [];
this.unreadCount = data.unread_count || 0;
} catch (e) {
console.error('Failed to fetch notifications', e);
}
},
toggle() {
this.open = !this.open;
if (this.open) {
this.loading = true;
this.fetchUnread().finally(() => { this.loading = false; });
}
},
async markRead(id) {
const url = this.markReadUrl.replace('__ID__', id);
try {
await fetch(url, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': this.csrfToken, 'X-Requested-With': 'XMLHttpRequest' },
});
this.notifications = this.notifications.filter(n => n.id !== id);
this.unreadCount = Math.max(0, this.unreadCount - 1);
} catch (e) {
console.error('Failed to mark notification as read', e);
}
},
async markAllRead() {
try {
await fetch(this.markAllReadUrl, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': this.csrfToken, 'X-Requested-With': 'XMLHttpRequest' },
});
this.notifications = [];
this.unreadCount = 0;
} catch (e) {
console.error('Failed to mark all notifications as read', e);
}
},
getIconBg(icon) {
const map = { success: 'bg-green-50', task: 'bg-amber-50' };
return map[icon] || 'bg-slate-100';
},
getIconColor(icon) {
const map = { success: 'text-green-600', task: 'text-amber-600' };
return map[icon] || 'text-slate-500';
},
}));
// Afia — in-app AI assistant slide-over. Opened via $dispatch('afia-open').
Alpine.data('afia', (config = {}) => ({
open: false,
input: '',
loading: false,
messages: [
{ role: 'assistant', text: config.greeting || "Hi, I'm Afia 👋 How can I help?" },
],
suggestions: config.suggestions || [],
init() {
window.addEventListener('afia-open', () => {
this.open = true;
this.$nextTick(() => this.$refs.input && this.$refs.input.focus());
});
},
close() { this.open = false; },
useSuggestion(s) { this.input = s; this.send(); },
scrollDown() {
this.$nextTick(() => { const el = this.$refs.scroll; if (el) el.scrollTop = el.scrollHeight; });
},
async send() {
const text = this.input.trim();
if (!text || this.loading) return;
const history = this.messages.map((m) => ({ role: m.role, text: m.text }));
this.messages.push({ role: 'user', text });
this.input = '';
this.loading = true;
this.scrollDown();
try {
const res = await fetch(config.chatUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': config.csrf, 'Accept': 'application/json' },
body: JSON.stringify({ message: text, history }),
});
const data = await res.json();
if (! res.ok) {
this.messages.push({ role: 'assistant', text: data.message || data.reply || 'Sorry, I could not respond.' });
} else {
this.messages.push({ role: 'assistant', text: data.reply || data.message || 'Sorry, I could not respond.' });
}
} catch (e) {
this.messages.push({ role: 'assistant', text: 'Network error — please try again.' });
}
this.loading = false;
this.scrollDown();
},
}));
// Wallet balance peek for the avatar dropdown.
Alpine.data('walletWidget', (config = {}) => ({
display: '…',
async load() {
try {
const res = await fetch(config.url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } });
const data = await res.json();
this.display = data.available ? data.formatted : 'View wallet';
} catch (e) {
this.display = 'View wallet';
}
},
}));
// Deal line-items editor — products/services as quote lines; deal value = subtotal.
Alpine.data('dealForm', (initialLines = [], products = []) => ({
lines: Array.isArray(initialLines) ? initialLines : [],
products: Array.isArray(products) ? products : [],
selectedProduct: '',
addLine() {
this.lines.push({ product_id: '', description: '', quantity: 1, unit_price: '' });
},
removeLine(index) {
this.lines.splice(index, 1);
},
addProductLine() {
const p = this.products.find((x) => String(x.id) === String(this.selectedProduct));
if (!p) return;
this.lines.push({
product_id: p.id,
description: p.name || '',
quantity: 1,
unit_price: (Number(p.unit_price_minor || 0) / 100).toFixed(2),
});
this.selectedProduct = '';
},
lineTotal(line) {
return (parseFloat(line.quantity) || 0) * (parseFloat(line.unit_price) || 0);
},
get subtotal() {
return this.lines.reduce((sum, line) => sum + this.lineTotal(line), 0);
},
money(value) {
return (Number(value) || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
},
}));
window.Alpine = Alpine;
Alpine.start();