Healthcare management app: patients, appointments, consultations, lab, pharmacy inventory, billing, and reports at care.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/forms';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Figtree', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* QR create/show: flush mobile action bar to the physical screen bottom (mobile only) */
|
||||
@media (max-width: 1023px) {
|
||||
.mobile-action-bar {
|
||||
position: fixed;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.mobile-action-bar__toolbar {
|
||||
display: flex;
|
||||
height: 4rem;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
border-top: 1px solid rgb(226 232 240);
|
||||
background-color: #fff;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.mobile-action-bar__inset-fill {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
height: env(safe-area-inset-bottom, 0px);
|
||||
min-height: max(env(safe-area-inset-bottom, 0px), 20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra white bleed behind the bar for any remaining viewport gap on mobile */
|
||||
.qr-mobile-bottom-bleed {
|
||||
position: fixed;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
z-index: 48;
|
||||
pointer-events: none;
|
||||
background-color: #fff;
|
||||
height: calc(4rem + env(safe-area-inset-bottom, 0px) + 24px);
|
||||
min-height: calc(4rem + 24px);
|
||||
}
|
||||
|
||||
html.qr-mobile-page {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
html.qr-mobile-page body {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Payment QR landing: lock scroll and keep the amount sheet flush to the screen bottom */
|
||||
@media (max-width: 767px) {
|
||||
html.mini-payment-page,
|
||||
html.mini-payment-page body {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html.mini-payment-page body {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.mini-payment-sheet {
|
||||
position: fixed;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.mini-payment-bottom-bleed {
|
||||
position: fixed;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
z-index: 19;
|
||||
pointer-events: none;
|
||||
background-color: #fff;
|
||||
height: calc(12rem + env(safe-area-inset-bottom, 0px));
|
||||
min-height: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-stats-card {
|
||||
width: calc(66.666667% - 0.5rem);
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background-image: linear-gradient(to right, rgb(79 70 229), rgb(124 58 237));
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
transition: filter 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-primary-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary-lg {
|
||||
padding: 0.625rem 1.25rem;
|
||||
}
|
||||
|
||||
.btn-fab {
|
||||
display: inline-flex;
|
||||
height: 2.75rem;
|
||||
width: 2.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
background-image: linear-gradient(to right, rgb(79 70 229), rgb(124 58 237));
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
transition: filter 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-fab:hover {
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.mobile-stats-card {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import Alpine from 'alpinejs';
|
||||
import collapse from '@alpinejs/collapse';
|
||||
import { registerKioskFlow } from './kiosk-flow';
|
||||
|
||||
Alpine.plugin(collapse);
|
||||
document.addEventListener('alpine:init', () => registerKioskFlow(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();
|
||||
@@ -0,0 +1,52 @@
|
||||
@php
|
||||
$signedOut = (array) config('signed_out');
|
||||
$logo = (string) ($signedOut['logo'] ?? 'images/logo/ladillcare-logo.svg');
|
||||
$logoPath = public_path($logo);
|
||||
@endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Signed out — {{ $signedOut['title'] ?? config('app.name') }}</title>
|
||||
@include('partials.favicon')
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
|
||||
@vite(['resources/css/app.css'])
|
||||
<meta name="robots" content="noindex">
|
||||
</head>
|
||||
<body class="flex min-h-screen items-center justify-center bg-slate-100 px-4 py-10 font-sans antialiased">
|
||||
<div class="w-full max-w-md overflow-hidden rounded-3xl bg-white shadow-xl shadow-slate-200/70">
|
||||
<div class="h-1.5" style="background: linear-gradient(90deg, {{ $signedOut['gradient_from'] ?? '#4f46e5' }} 0%, {{ $signedOut['gradient_to'] ?? '#7c3aed' }} 100%);"></div>
|
||||
|
||||
<div class="px-6 pb-8 pt-7 text-center sm:px-8">
|
||||
<img src="{{ asset($logo) }}?v={{ @filemtime($logoPath) ?: '1' }}"
|
||||
alt="{{ $signedOut['title'] ?? config('app.name') }}"
|
||||
class="mx-auto h-7 w-auto">
|
||||
|
||||
<div @class([
|
||||
'mx-auto mt-6 flex h-14 w-14 items-center justify-center rounded-2xl',
|
||||
$signedOut['icon_wrapper_class'] ?? 'bg-indigo-50 text-indigo-700',
|
||||
])>
|
||||
<svg class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="mt-5 text-xl font-semibold text-slate-900">You’re signed out</h1>
|
||||
<p class="mt-2 text-sm leading-relaxed text-slate-500">
|
||||
{{ $signedOut['description'] ?? ('Your '.($signedOut['title'] ?? config('app.name')).' session has ended. Sign in again to continue.') }}
|
||||
</p>
|
||||
|
||||
<a href="{{ route('sso.connect') }}" class="btn-primary btn-primary-lg mt-7 w-full">
|
||||
Sign in again
|
||||
</a>
|
||||
|
||||
<a href="{{ 'https://'.config('app.platform_domain') }}"
|
||||
class="mt-4 inline-block text-sm font-medium text-slate-500 transition hover:text-slate-800">
|
||||
Go to ladill.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
@php
|
||||
$signedOut = (array) config('signed_out');
|
||||
$logo = (string) ($signedOut['logo'] ?? 'images/logo/ladillcare-logo.svg');
|
||||
$title = (string) ($signedOut['title'] ?? config('app.name'));
|
||||
$logoPath = public_path($logo);
|
||||
@endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign-in problem · {{ $title }}</title>
|
||||
@include('partials.favicon')
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body class="flex min-h-screen items-center justify-center bg-slate-50 font-sans text-slate-900 antialiased">
|
||||
<div class="mx-auto max-w-md px-6 text-center">
|
||||
<img src="{{ asset($logo) }}?v={{ @filemtime($logoPath) ?: '1' }}" alt="{{ $title }}" class="mx-auto h-8 w-auto">
|
||||
<h1 class="mt-6 text-xl font-semibold">We couldn't sign you in</h1>
|
||||
<p class="mt-2 text-sm text-slate-500">
|
||||
Sign-in didn't complete after a few tries. This is usually a temporary
|
||||
issue with the Ladill account service. Please wait a moment and try again.
|
||||
</p>
|
||||
@if ($reason !== '')
|
||||
<p class="mt-3 text-xs text-slate-400">Reference: {{ $reason }}</p>
|
||||
@endif
|
||||
<a href="{{ route('sso.connect', ['interactive' => 1]) }}" class="mt-6 inline-flex rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-indigo-700">Try again</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Signing out…</title>
|
||||
@include('partials.favicon')
|
||||
<meta name="robots" content="noindex">
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; display: flex; min-height: 100vh; align-items: center; justify-content: center; margin: 0; background: #f8fafc; color: #334155; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Signing you out of Ladill…</p>
|
||||
<iframe src="{{ $hubUrl }}" hidden width="0" height="0" title=""></iframe>
|
||||
<script>
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.origin !== @json($authOrigin)) return;
|
||||
if (event.data && event.data.type === 'ladill:logout:done' && event.data.return) {
|
||||
window.location.replace(event.data.return);
|
||||
}
|
||||
});
|
||||
setTimeout(function () { window.location.replace(@json($return)); }, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<x-app-layout title="Add branch">
|
||||
<div class="mx-auto max-w-lg">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Add branch</h1>
|
||||
<form method="POST" action="{{ route('care.branches.store') }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Code</label><input type="text" name="code" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Address</label><input type="text" name="address" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Phone</label><input type="tel" name="phone" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<button type="submit" class="btn-primary w-full">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,14 @@
|
||||
<x-app-layout title="Edit branch">
|
||||
<div class="mx-auto max-w-lg">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Edit branch</h1>
|
||||
<form method="POST" action="{{ route('care.branches.update', $branch) }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf @method('PUT')
|
||||
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" value="{{ old('name', $branch->name) }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Code</label><input type="text" name="code" value="{{ old('code', $branch->code) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Address</label><input type="text" name="address" value="{{ old('address', $branch->address) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Phone</label><input type="tel" name="phone" value="{{ old('phone', $branch->phone) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked(old('is_active', $branch->is_active))> Active</label>
|
||||
<button type="submit" class="btn-primary w-full">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,22 @@
|
||||
<x-app-layout title="Branches">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Branches</h1>
|
||||
@if (app(\App\Services\Care\CarePermissions::class)->can(auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user(), $organization) : null, 'admin.branches.manage'))
|
||||
<a href="{{ route('care.branches.create') }}" class="btn-primary">Add branch</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($branches as $branch)
|
||||
<div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{{ $branch->name }}</p>
|
||||
<p class="text-sm text-slate-500">{{ $branch->address ?? 'No address' }} · {{ $branch->departments_count }} department(s)</p>
|
||||
</div>
|
||||
<a href="{{ route('care.branches.edit', $branch) }}" class="text-sm text-slate-600 hover:text-slate-800">Edit</a>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-slate-500">No branches yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,26 @@
|
||||
<x-app-layout title="Add department">
|
||||
<div class="mx-auto max-w-lg">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Add department</h1>
|
||||
<form method="POST" action="{{ route('care.departments.store') }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Branch</label>
|
||||
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Type</label>
|
||||
<select name="type" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($types as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary w-full">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,19 @@
|
||||
<x-app-layout title="Edit department">
|
||||
<div class="mx-auto max-w-lg">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Edit department</h1>
|
||||
<form method="POST" action="{{ route('care.departments.update', $department) }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf @method('PUT')
|
||||
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" value="{{ old('name', $department->name) }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Type</label>
|
||||
<select name="type" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($types as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('type', $department->type) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked(old('is_active', $department->is_active))> Active</label>
|
||||
<button type="submit" class="btn-primary w-full">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,28 @@
|
||||
<x-app-layout title="Departments">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Departments</h1>
|
||||
<a href="{{ route('care.departments.create') }}" class="btn-primary">Add department</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Name</th><th class="px-4 py-3">Type</th><th class="px-4 py-3">Branch</th><th class="px-4 py-3"></th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($departments as $department)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium">{{ $department->name }}</td>
|
||||
<td class="px-4 py-3">{{ $types[$department->type] ?? $department->type }}</td>
|
||||
<td class="px-4 py-3">{{ $department->branch?->name }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ route('care.departments.edit', $department) }}" class="text-sky-600">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="px-4 py-6 text-center text-slate-500">No departments yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,28 @@
|
||||
<x-app-layout title="Add team member">
|
||||
<div class="mx-auto max-w-lg">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Add team member</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">Enter the Ladill user public ID (UUID) from their account profile.</p>
|
||||
<form method="POST" action="{{ route('care.members.store') }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
<div><label class="block text-sm font-medium">User public ID</label><input type="text" name="user_ref" required class="mt-1 w-full rounded-lg border-slate-300 font-mono text-sm"></div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Role</label>
|
||||
<select name="role" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($roles as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Branch (optional)</label>
|
||||
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All branches</option>
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary w-full">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,31 @@
|
||||
<x-app-layout title="Team members">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Team members</h1>
|
||||
<a href="{{ route('care.members.create') }}" class="btn-primary">Add member</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">User ID</th><th class="px-4 py-3">Role</th><th class="px-4 py-3">Branch</th><th class="px-4 py-3"></th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@foreach ($members as $member)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-mono text-xs">{{ $member->user_ref }}</td>
|
||||
<td class="px-4 py-3">{{ $roles[$member->role] ?? $member->role }}</td>
|
||||
<td class="px-4 py-3">{{ $member->branch?->name ?? 'All branches' }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
@if ($member->user_ref !== auth()->user()->public_id)
|
||||
<form method="POST" action="{{ route('care.members.destroy', $member) }}" class="inline" onsubmit="return confirm('Remove this member?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-600">Remove</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,56 @@
|
||||
@php
|
||||
$isWalkIn = $isWalkIn ?? false;
|
||||
@endphp
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Branch</label>
|
||||
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected(old('branch_id', $defaultBranch) == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Patient</label>
|
||||
<select name="patient_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">Select patient…</option>
|
||||
@foreach ($patients as $patient)
|
||||
<option value="{{ $patient->id }}" @selected(old('patient_id') == $patient->id)>{{ $patient->fullName() }} ({{ $patient->patient_number }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Practitioner</label>
|
||||
<select name="practitioner_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">Any / unassigned</option>
|
||||
@foreach ($practitioners as $practitioner)
|
||||
<option value="{{ $practitioner->id }}" @selected(old('practitioner_id') == $practitioner->id)>{{ $practitioner->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Department</label>
|
||||
<select name="department_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">—</option>
|
||||
@foreach ($departments as $department)
|
||||
<option value="{{ $department->id }}" @selected(old('department_id') == $department->id)>{{ $department->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@unless ($isWalkIn)
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Scheduled date & time</label>
|
||||
<input type="datetime-local" name="scheduled_at" value="{{ old('scheduled_at') }}" required
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
@endunless
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Reason for visit</label>
|
||||
<input type="text" name="reason" value="{{ old('reason') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Notes</label>
|
||||
<textarea name="notes" rows="3" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('notes') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<x-app-layout title="Book appointment">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Book appointment</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Schedule a future visit for a patient</p>
|
||||
|
||||
<form method="POST" action="{{ route('care.appointments.store') }}" class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
@include('care.appointments._form')
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button type="submit" class="btn-primary">Book appointment</button>
|
||||
<a href="{{ route('care.appointments.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,76 @@
|
||||
<x-app-layout title="Appointments">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Appointments</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Schedule and manage patient appointments</p>
|
||||
</div>
|
||||
@if (app(\App\Services\Care\CarePermissions::class)->can(auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user(), $organization) : null, 'appointments.manage'))
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('care.appointments.walk-in.create') }}" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Walk-in</a>
|
||||
<a href="{{ route('care.appointments.create') }}" class="btn-primary">Book appointment</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<input type="date" name="date" value="{{ request('date') }}" class="rounded-lg border-slate-300 text-sm">
|
||||
<select name="status" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All statuses</option>
|
||||
@foreach ($statuses as $value => $label)
|
||||
<option value="{{ $value }}" @selected(request('status') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="practitioner_id" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All practitioners</option>
|
||||
@foreach ($practitioners as $practitioner)
|
||||
<option value="{{ $practitioner->id }}" @selected((string) request('practitioner_id') === (string) $practitioner->id)>{{ $practitioner->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
@if (request()->hasAny(['date', 'status', 'practitioner_id']))
|
||||
<a href="{{ route('care.appointments.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Clear</a>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Patient</th>
|
||||
<th class="px-4 py-3">Scheduled</th>
|
||||
<th class="px-4 py-3">Practitioner</th>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($appointments as $appointment)
|
||||
<tr>
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-medium text-slate-900">{{ $appointment->patient->fullName() }}</p>
|
||||
<p class="text-xs text-slate-500">{{ $appointment->patient->patient_number }}</p>
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ $appointment->scheduled_at?->format('d M Y H:i') ?? '—' }}</td>
|
||||
<td class="px-4 py-3">{{ $appointment->practitioner?->name ?? '—' }}</td>
|
||||
<td class="px-4 py-3 capitalize">{{ str_replace('_', ' ', $appointment->type) }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700">
|
||||
{{ $statuses[$appointment->status] ?? $appointment->status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ route('care.appointments.show', $appointment) }}" class="text-sky-600 hover:text-sky-700">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-slate-500">No appointments found.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $appointments->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,66 @@
|
||||
<x-app-layout title="Appointment">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">{{ $statuses[$appointment->status] ?? $appointment->status }}</p>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $appointment->patient->fullName() }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
{{ $appointment->scheduled_at?->format('d M Y H:i') ?? 'Walk-in' }}
|
||||
@if ($appointment->practitioner) · {{ $appointment->practitioner->name }} @endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if ($canManage && $appointment->status === \App\Models\Appointment::STATUS_SCHEDULED)
|
||||
<form method="POST" action="{{ route('care.appointments.check-in', $appointment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn-primary">Check in</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ route('care.appointments.no-show', $appointment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg border border-amber-200 px-4 py-2 text-sm text-amber-700 hover:bg-amber-50">No show</button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($canConsult && in_array($appointment->status, [\App\Models\Appointment::STATUS_WAITING, \App\Models\Appointment::STATUS_CHECKED_IN], true))
|
||||
<form method="POST" action="{{ route('care.queue.start', $appointment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn-primary">Start consultation</button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($appointment->consultation)
|
||||
<a href="{{ route('care.consultations.show', $appointment->consultation) }}" class="rounded-lg border border-sky-200 px-4 py-2 text-sm text-sky-700 hover:bg-sky-50">View consultation</a>
|
||||
@endif
|
||||
@if ($canManage && ! in_array($appointment->status, [\App\Models\Appointment::STATUS_COMPLETED, \App\Models\Appointment::STATUS_CANCELLED], true))
|
||||
<form method="POST" action="{{ route('care.appointments.cancel', $appointment) }}" onsubmit="return confirm('Cancel this appointment?')">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg border border-red-200 px-4 py-2 text-sm text-red-600 hover:bg-red-50">Cancel</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-6 lg:grid-cols-2">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Details</h2>
|
||||
<dl class="mt-4 space-y-3 text-sm">
|
||||
<div><dt class="text-slate-500">Patient</dt><dd class="font-medium"><a href="{{ route('care.patients.show', $appointment->patient) }}" class="text-sky-600">{{ $appointment->patient->fullName() }}</a></dd></div>
|
||||
<div><dt class="text-slate-500">Branch</dt><dd class="font-medium">{{ $appointment->branch?->name ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Department</dt><dd class="font-medium">{{ $appointment->department?->name ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Type</dt><dd class="font-medium capitalize">{{ str_replace('_', ' ', $appointment->type) }}</dd></div>
|
||||
<div><dt class="text-slate-500">Reason</dt><dd class="font-medium">{{ $appointment->reason ?? '—' }}</dd></div>
|
||||
@if ($appointment->notes)
|
||||
<div><dt class="text-slate-500">Notes</dt><dd class="font-medium">{{ $appointment->notes }}</dd></div>
|
||||
@endif
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Timeline</h2>
|
||||
<dl class="mt-4 space-y-3 text-sm">
|
||||
<div><dt class="text-slate-500">Checked in</dt><dd class="font-medium">{{ $appointment->checked_in_at?->format('d M Y H:i') ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Waiting since</dt><dd class="font-medium">{{ $appointment->waiting_at?->format('d M Y H:i') ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Queue position</dt><dd class="font-medium">{{ $appointment->queue_position ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Started</dt><dd class="font-medium">{{ $appointment->started_at?->format('d M Y H:i') ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Completed</dt><dd class="font-medium">{{ $appointment->completed_at?->format('d M Y H:i') ?? '—' }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,13 @@
|
||||
<x-app-layout title="Walk-in">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Register walk-in</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Check in a patient and add them to the queue immediately</p>
|
||||
|
||||
<form method="POST" action="{{ route('care.appointments.walk-in.store') }}" class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
@include('care.appointments._form', ['isWalkIn' => true])
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button type="submit" class="btn-primary">Add to queue</button>
|
||||
<a href="{{ route('care.queue.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,41 @@
|
||||
<x-app-layout title="Audit log">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Audit log</h1>
|
||||
@if ($canExport)
|
||||
<a href="{{ route('care.audit.export', request()->query()) }}" class="btn-primary">Export CSV</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3">
|
||||
<input type="text" name="q" value="{{ request('q') }}" placeholder="Search…" class="rounded-lg border-slate-300 text-sm">
|
||||
<select name="action" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All actions</option>
|
||||
@foreach ($actions as $key => $label)
|
||||
<option value="{{ $key }}" @selected(request('action') === $key)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Time</th><th class="px-4 py-3">Action</th><th class="px-4 py-3">Actor</th><th class="px-4 py-3">Subject</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($logs as $log)
|
||||
<tr>
|
||||
<td class="px-4 py-3 whitespace-nowrap">{{ $log->created_at?->format('Y-m-d H:i') }}</td>
|
||||
<td class="px-4 py-3">{{ $actions[$log->action] ?? $log->action }}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs">{{ $log->actor_ref ?? '—' }}</td>
|
||||
<td class="px-4 py-3">{{ $log->subject_type ? class_basename($log->subject_type).' #'.$log->subject_id : '—' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="px-4 py-6 text-center text-slate-500">No audit entries yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $logs->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,40 @@
|
||||
@php $money = fn ($minor) => config('care.billing.currency').' '.number_format($minor / 100, 2); @endphp
|
||||
<x-app-layout title="Bills">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Bills & invoices</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Encounter billing and outstanding balances</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="GET" class="mt-4 flex gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<select name="status" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All statuses</option>
|
||||
@foreach ($statuses as $value => $label)
|
||||
<option value="{{ $value }}" @selected(request('status') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Invoice</th><th class="px-4 py-3">Patient</th><th class="px-4 py-3">Total</th><th class="px-4 py-3">Balance</th><th class="px-4 py-3">Status</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($bills as $bill)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-mono text-xs">{{ $bill->invoice_number }}</td>
|
||||
<td class="px-4 py-3">{{ $bill->patient->fullName() }}</td>
|
||||
<td class="px-4 py-3">{{ $money($bill->total_minor) }}</td>
|
||||
<td class="px-4 py-3 {{ $bill->balance_minor > 0 ? 'text-amber-700' : '' }}">{{ $money($bill->balance_minor) }}</td>
|
||||
<td class="px-4 py-3">{{ $statuses[$bill->status] ?? $bill->status }}</td>
|
||||
<td class="px-4 py-3 text-right"><a href="{{ route('care.bills.show', $bill) }}" class="text-sky-600">View</a></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No bills yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">{{ $bills->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,25 @@
|
||||
@php $money = fn ($minor) => config('care.billing.currency').' '.number_format($minor / 100, 2); @endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ $bill->invoice_number }}</title>
|
||||
<style>body{font-family:system-ui,sans-serif;max-width:720px;margin:2rem auto;color:#111}table{width:100%;border-collapse:collapse}th,td{padding:.5rem;text-align:left;border-bottom:1px solid #eee}h1{margin:0}</style>
|
||||
</head>
|
||||
<body onload="window.print()">
|
||||
<header>
|
||||
<h1>{{ $organization->name }}</h1>
|
||||
<p>Invoice {{ $bill->invoice_number }} · {{ $bill->created_at->format('d M Y') }}</p>
|
||||
<p>{{ $bill->patient->fullName() }} ({{ $bill->patient->patient_number }})</p>
|
||||
</header>
|
||||
<table class="mt-6">
|
||||
<thead><tr><th>Description</th><th>Qty</th><th>Amount</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach ($bill->lineItems as $item)
|
||||
<tr><td>{{ $item->description }}</td><td>{{ $item->quantity }}</td><td>{{ $money($item->total_minor) }}</td></tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="mt-4"><strong>Total:</strong> {{ $money($bill->total_minor) }} · <strong>Paid:</strong> {{ $money($bill->amount_paid_minor) }} · <strong>Balance:</strong> {{ $money($bill->balance_minor) }}</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,91 @@
|
||||
@php $money = fn ($minor) => config('care.billing.currency').' '.number_format($minor / 100, 2); @endphp
|
||||
<x-app-layout :title="$bill->invoice_number">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs uppercase text-slate-500">{{ $statuses[$bill->status] ?? $bill->status }}</p>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $bill->invoice_number }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $bill->patient->fullName() }} · {{ $bill->branch?->name }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('care.bills.print', $bill) }}" target="_blank" class="rounded-lg border border-slate-200 px-4 py-2 text-sm">Print</a>
|
||||
@if ($canManage && ! in_array($bill->status, [\App\Models\Bill::STATUS_PAID, \App\Models\Bill::STATUS_VOID]))
|
||||
<form method="POST" action="{{ route('care.bills.void', $bill) }}" onsubmit="return confirm('Void this bill?')">@csrf<button class="rounded-lg border border-red-200 px-4 py-2 text-sm text-red-600">Void</button></form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-6 lg:grid-cols-3">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6 lg:col-span-2">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Line items</h2>
|
||||
<table class="mt-4 min-w-full text-sm">
|
||||
<thead class="text-left text-xs uppercase text-slate-500"><tr><th class="py-2">Description</th><th>Qty</th><th>Unit</th><th>Total</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@foreach ($bill->lineItems as $item)
|
||||
<tr>
|
||||
<td class="py-2">{{ $item->description }} <span class="text-slate-400">({{ $lineTypes[$item->type] ?? $item->type }})</span></td>
|
||||
<td>{{ $item->quantity }}</td>
|
||||
<td>{{ $money($item->unit_price_minor) }}</td>
|
||||
<td>{{ $money($item->total_minor) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<dl class="mt-4 space-y-1 border-t border-slate-100 pt-4 text-sm">
|
||||
<div class="flex justify-between"><dt class="text-slate-500">Subtotal</dt><dd>{{ $money($bill->subtotal_minor) }}</dd></div>
|
||||
@if ($bill->discount_minor)<div class="flex justify-between"><dt class="text-slate-500">Discount</dt><dd>-{{ $money($bill->discount_minor) }}</dd></div>@endif
|
||||
@if ($bill->tax_minor)<div class="flex justify-between"><dt class="text-slate-500">Tax</dt><dd>{{ $money($bill->tax_minor) }}</dd></div>@endif
|
||||
<div class="flex justify-between font-semibold"><dt>Total</dt><dd>{{ $money($bill->total_minor) }}</dd></div>
|
||||
<div class="flex justify-between text-amber-700"><dt>Balance</dt><dd>{{ $money($bill->balance_minor) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if ($canManage && ! in_array($bill->status, [\App\Models\Bill::STATUS_PAID, \App\Models\Bill::STATUS_VOID]))
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Add line item</h2>
|
||||
<form method="POST" action="{{ route('care.bills.line-items.store', $bill) }}" class="mt-4 space-y-3">
|
||||
@csrf
|
||||
<select name="type" class="w-full rounded-lg border-slate-300 text-sm">@foreach ($lineTypes as $k => $v)<option value="{{ $k }}">{{ $v }}</option>@endforeach</select>
|
||||
<input type="text" name="description" required placeholder="Description" class="w-full rounded-lg border-slate-300 text-sm">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="number" name="quantity" value="1" min="1" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="number" name="unit_price_minor" placeholder="Price (minor)" min="0" class="rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary w-full">Add</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Discount & tax</h2>
|
||||
<form method="POST" action="{{ route('care.bills.adjustments', $bill) }}" class="mt-4 space-y-3">
|
||||
@csrf
|
||||
<input type="number" name="discount_minor" value="{{ $bill->discount_minor }}" min="0" placeholder="Discount (minor)" class="w-full rounded-lg border-slate-300 text-sm">
|
||||
<input type="number" name="tax_minor" value="{{ $bill->tax_minor }}" min="0" placeholder="Tax (minor)" class="w-full rounded-lg border-slate-300 text-sm">
|
||||
<button type="submit" class="btn-primary w-full">Update</button>
|
||||
</form>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($canPay && $bill->balance_minor > 0 && $bill->status !== \App\Models\Bill::STATUS_VOID)
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Record payment</h2>
|
||||
<form method="POST" action="{{ route('care.bills.payments.store', $bill) }}" class="mt-4 space-y-3">
|
||||
@csrf
|
||||
<input type="number" name="amount_minor" value="{{ $bill->balance_minor }}" min="1" required class="w-full rounded-lg border-slate-300 text-sm">
|
||||
<select name="method" class="w-full rounded-lg border-slate-300 text-sm">@foreach ($paymentMethods as $k => $v)<option value="{{ $k }}">{{ $v }}</option>@endforeach</select>
|
||||
<input type="text" name="reference" placeholder="Reference" class="w-full rounded-lg border-slate-300 text-sm">
|
||||
<button type="submit" class="btn-primary w-full">Record payment</button>
|
||||
</form>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Payments</h2>
|
||||
@forelse ($bill->payments as $payment)
|
||||
<p class="mt-2 text-sm">{{ $money($payment->amount_minor) }} · {{ $paymentMethods[$payment->method] ?? $payment->method }} · {{ $payment->paid_at->format('d M Y') }}</p>
|
||||
@empty
|
||||
<p class="mt-2 text-sm text-slate-400">No payments recorded.</p>
|
||||
@endforelse
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,232 @@
|
||||
<x-app-layout :title="'Consultation · '.$consultation->patient->fullName()">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium uppercase tracking-wide {{ $isCompleted ? 'text-emerald-600' : 'text-amber-600' }}">
|
||||
{{ $isCompleted ? 'Completed' : 'In progress' }}
|
||||
</p>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $consultation->patient->fullName() }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
{{ $consultation->patient->patient_number }}
|
||||
@if ($consultation->practitioner) · {{ $consultation->practitioner->name }} @endif
|
||||
</p>
|
||||
</div>
|
||||
@if ($canManage && ! $isCompleted)
|
||||
<form method="POST" action="{{ route('care.consultations.complete', $consultation) }}" onsubmit="return confirm('Complete this consultation?')">
|
||||
@csrf
|
||||
<button type="submit" class="btn-primary">Complete consultation</button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($canPrescribe)
|
||||
<a href="{{ route('care.prescriptions.create', $consultation) }}" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Prescribe</a>
|
||||
@endif
|
||||
@if ($canGenerateBill && $isCompleted && $consultation->visit)
|
||||
<form method="POST" action="{{ route('care.bills.generate', $consultation->visit) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg bg-slate-800 px-4 py-2 text-sm font-medium text-white hover:bg-slate-900">Generate bill</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($canRequestInvestigations && $investigationTypes->isNotEmpty())
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Request investigations</h2>
|
||||
<form method="POST" action="{{ route('care.lab.requests.store', $consultation) }}" class="mt-4 space-y-4">
|
||||
@csrf
|
||||
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($investigationTypes as $type)
|
||||
<label class="flex items-center gap-2 rounded-lg border border-slate-100 bg-slate-50 px-3 py-2 text-sm">
|
||||
<input type="checkbox" name="investigation_type_ids[]" value="{{ $type->id }}" class="rounded border-slate-300">
|
||||
<span>{{ $type->name }} <span class="text-slate-400">({{ config('care.investigation_categories')[$type->category] ?? $type->category }})</span></span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<input type="text" name="clinical_notes" placeholder="Clinical notes for lab" class="rounded-lg border-slate-300 text-sm">
|
||||
<select name="priority" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="routine">Routine</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Submit requests</button>
|
||||
</form>
|
||||
@if ($consultation->investigationRequests->isNotEmpty())
|
||||
<div class="mt-4 border-t border-slate-100 pt-4 text-sm">
|
||||
<p class="font-medium text-slate-700">Requested tests</p>
|
||||
@foreach ($consultation->investigationRequests as $req)
|
||||
<p class="mt-1">
|
||||
<a href="{{ route('care.lab.requests.show', $req) }}" class="text-sky-600">{{ $req->investigationType->name }}</a>
|
||||
· {{ config('care.investigation_statuses')[$req->status] ?? $req->status }}
|
||||
</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($consultation->prescriptions->isNotEmpty())
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Prescriptions</h2>
|
||||
<ul class="mt-4 space-y-2 text-sm">
|
||||
@foreach ($consultation->prescriptions as $rx)
|
||||
<li>
|
||||
<a href="{{ route('care.prescriptions.show', $rx) }}" class="text-sky-600 hover:text-sky-700">
|
||||
{{ $rx->created_at->format('d M Y H:i') }}
|
||||
</a>
|
||||
· {{ config('care.prescription_statuses')[$rx->status] ?? $rx->status }}
|
||||
· {{ $rx->items->pluck('name')->join(', ') }}
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if (! $isCompleted && ($canManage || $canVitals))
|
||||
<form method="POST" action="{{ route('care.consultations.update', $consultation) }}" enctype="multipart/form-data" class="mt-6 space-y-6">
|
||||
@csrf @method('PUT')
|
||||
|
||||
@if ($canVitals)
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Vital signs</h2>
|
||||
@php $latestVitals = $consultation->vitalSigns->last(); @endphp
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">BP systolic</label>
|
||||
<input type="number" name="vitals[bp_systolic]" value="{{ old('vitals.bp_systolic', $latestVitals?->bp_systolic) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">BP diastolic</label>
|
||||
<input type="number" name="vitals[bp_diastolic]" value="{{ old('vitals.bp_diastolic', $latestVitals?->bp_diastolic) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">Pulse</label>
|
||||
<input type="number" name="vitals[pulse]" value="{{ old('vitals.pulse', $latestVitals?->pulse) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">Temperature (°C)</label>
|
||||
<input type="number" step="0.1" name="vitals[temperature]" value="{{ old('vitals.temperature', $latestVitals?->temperature) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">Weight (kg)</label>
|
||||
<input type="number" step="0.01" name="vitals[weight_kg]" value="{{ old('vitals.weight_kg', $latestVitals?->weight_kg) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">Height (cm)</label>
|
||||
<input type="number" step="0.1" name="vitals[height_cm]" value="{{ old('vitals.height_cm', $latestVitals?->height_cm) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">SpO₂ (%)</label>
|
||||
<input type="number" name="vitals[spo2]" value="{{ old('vitals.spo2', $latestVitals?->spo2) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">Resp. rate</label>
|
||||
<input type="number" name="vitals[respiratory_rate]" value="{{ old('vitals.respiratory_rate', $latestVitals?->respiratory_rate) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($canManage)
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Clinical notes</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Symptoms</label>
|
||||
<textarea name="symptoms" rows="3" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('symptoms', $consultation->symptoms) }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Clinical notes</label>
|
||||
<textarea name="clinical_notes" rows="5" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('clinical_notes', $consultation->clinical_notes) }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Practitioner</label>
|
||||
<select name="practitioner_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">—</option>
|
||||
@foreach ($practitioners as $practitioner)
|
||||
<option value="{{ $practitioner->id }}" @selected(old('practitioner_id', $consultation->practitioner_id) == $practitioner->id)>{{ $practitioner->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6" x-data="{ diagnoses: {{ json_encode(old('diagnoses', $consultation->diagnoses->map(fn ($d) => ['code' => $d->code, 'description' => $d->description, 'is_primary' => $d->is_primary, 'notes' => $d->notes])->values()->all() ?: [['code' => '', 'description' => '', 'is_primary' => true, 'notes' => '']])) }} }">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Diagnoses</h2>
|
||||
<button type="button" @click="diagnoses.push({code:'',description:'',is_primary:false,notes:''})" class="text-sm text-sky-600 hover:text-sky-700">Add diagnosis</button>
|
||||
</div>
|
||||
<template x-for="(dx, index) in diagnoses" :key="index">
|
||||
<div class="mt-4 grid gap-3 rounded-xl border border-slate-100 bg-slate-50 p-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">ICD code</label>
|
||||
<input type="text" :name="'diagnoses['+index+'][code]'" x-model="dx.code" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500">Description</label>
|
||||
<input type="text" :name="'diagnoses['+index+'][description]'" x-model="dx.description" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="inline-flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" :name="'diagnoses['+index+'][is_primary]'" value="1" x-model="dx.is_primary" class="rounded border-slate-300">
|
||||
Primary diagnosis
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Documents</h2>
|
||||
<input type="file" name="documents[]" multiple accept=".pdf,.jpg,.jpeg,.png,.webp" class="mt-4 block w-full text-sm text-slate-500">
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
<a href="{{ route('care.queue.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Back to queue</a>
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
<div class="mt-6 space-y-6">
|
||||
@if ($consultation->vitalSigns->isNotEmpty())
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Vital signs</h2>
|
||||
@foreach ($consultation->vitalSigns as $vitals)
|
||||
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-4">
|
||||
<div><dt class="text-slate-500">BP</dt><dd class="font-medium">{{ $vitals->bp_systolic }}/{{ $vitals->bp_diastolic }}</dd></div>
|
||||
<div><dt class="text-slate-500">Pulse</dt><dd class="font-medium">{{ $vitals->pulse ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Temp</dt><dd class="font-medium">{{ $vitals->temperature ?? '—' }}°C</dd></div>
|
||||
<div><dt class="text-slate-500">SpO₂</dt><dd class="font-medium">{{ $vitals->spo2 ? $vitals->spo2.'%' : '—' }}</dd></div>
|
||||
</dl>
|
||||
@endforeach
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($consultation->symptoms || $consultation->clinical_notes)
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Clinical notes</h2>
|
||||
@if ($consultation->symptoms)
|
||||
<p class="mt-4 text-sm"><span class="font-medium text-slate-700">Symptoms:</span> {{ $consultation->symptoms }}</p>
|
||||
@endif
|
||||
@if ($consultation->clinical_notes)
|
||||
<p class="mt-2 whitespace-pre-wrap text-sm text-slate-600">{{ $consultation->clinical_notes }}</p>
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($consultation->diagnoses->isNotEmpty())
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Diagnoses</h2>
|
||||
<ul class="mt-4 space-y-2 text-sm">
|
||||
@foreach ($consultation->diagnoses as $diagnosis)
|
||||
<li>
|
||||
<span class="font-medium">{{ $diagnosis->description }}</span>
|
||||
@if ($diagnosis->code) <span class="text-slate-400">({{ $diagnosis->code }})</span> @endif
|
||||
@if ($diagnosis->is_primary) <span class="text-sky-600">primary</span> @endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,63 @@
|
||||
<x-app-layout title="Dashboard">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $organization->name }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">Healthcare management dashboard</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Patients today</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-slate-900">{{ $operational['patients_today'] }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Appointments today</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-slate-900">{{ $operational['appointments_today'] }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Open bills</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-slate-900">{{ $operational['open_bills'] }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Revenue today</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-emerald-700">{{ config('care.billing.currency') }} {{ number_format($operational['revenue_today_minor'] / 100, 2) }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Pending lab</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-slate-900">{{ $operational['pending_lab'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-4 sm:grid-cols-3">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Active branches</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-slate-900">{{ $stats['branches'] }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Team members</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-slate-900">{{ $stats['team_members'] }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">Departments</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-slate-900">{{ $stats['departments'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Branches</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($branches as $branch)
|
||||
<div class="flex items-center justify-between rounded-xl border border-slate-100 px-4 py-3">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{{ $branch->name }}</p>
|
||||
<p class="text-sm text-slate-500">{{ $branch->departments_count }} department(s) · {{ $branch->address ?? 'No address' }}</p>
|
||||
</div>
|
||||
@unless ($branch->is_active)
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">Inactive</span>
|
||||
@endunless
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-slate-500">No branches configured yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,48 @@
|
||||
@php $inputName = fn ($field) => $type ? "old_{$field}" : $field; @endphp
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Name</label>
|
||||
<input type="text" name="name" value="{{ old('name', $type->name ?? '') }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Code</label>
|
||||
<input type="text" name="code" value="{{ old('code', $type->code ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Category</label>
|
||||
<select name="category" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($categories as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('category', $type->category ?? '') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Unit</label>
|
||||
<input type="text" name="unit" value="{{ old('unit', $type->unit ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Reference low</label>
|
||||
<input type="number" step="any" name="reference_low" value="{{ old('reference_low', $type->reference_low ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Reference high</label>
|
||||
<input type="number" step="any" name="reference_high" value="{{ old('reference_high', $type->reference_high ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Reference text</label>
|
||||
<input type="text" name="reference_text" value="{{ old('reference_text', $type->reference_text ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm" placeholder="e.g. Negative, Normal">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Price (minor units)</label>
|
||||
<input type="number" name="price_minor" value="{{ old('price_minor', $type->price_minor ?? 0) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<label class="inline-flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $type->is_active ?? true)) class="rounded border-slate-300"> Active
|
||||
</label>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Description</label>
|
||||
<textarea name="description" rows="2" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('description', $type->description ?? '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-app-layout title="Add investigation">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Add investigation type</h1>
|
||||
<form method="POST" action="{{ route('care.lab.catalog.store') }}" class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
@include('care.lab.catalog._form', ['type' => null])
|
||||
<div class="mt-6"><button type="submit" class="btn-primary">Save</button></div>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-app-layout title="Edit investigation">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Edit {{ $type->name }}</h1>
|
||||
<form method="POST" action="{{ route('care.lab.catalog.update', $type) }}" class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf @method('PUT')
|
||||
@include('care.lab.catalog._form')
|
||||
<div class="mt-6"><button type="submit" class="btn-primary">Update</button></div>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,27 @@
|
||||
<x-app-layout title="Investigation catalog">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Investigation catalog</h1>
|
||||
<a href="{{ route('care.lab.catalog.create') }}" class="btn-primary">Add test</a>
|
||||
</div>
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Name</th><th class="px-4 py-3">Category</th><th class="px-4 py-3">Reference</th><th class="px-4 py-3">Price</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($types as $type)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium">{{ $type->name }}</td>
|
||||
<td class="px-4 py-3">{{ $categories[$type->category] ?? $type->category }}</td>
|
||||
<td class="px-4 py-3">{{ $type->reference_text ?? ($type->reference_low.'–'.$type->reference_high) }} {{ $type->unit }}</td>
|
||||
<td class="px-4 py-3">{{ number_format($type->price_minor / 100, 2) }}</td>
|
||||
<td class="px-4 py-3 text-right"><a href="{{ route('care.lab.catalog.edit', $type) }}" class="text-sky-600">Edit</a></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No tests in catalog.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">{{ $types->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,45 @@
|
||||
<x-app-layout title="Lab queue">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Lab work queue</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Pending samples and investigations in progress</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('care.lab.requests.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm">All requests</a>
|
||||
@if (app(\App\Services\Care\CarePermissions::class)->can(auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user(), $organization) : null, 'lab.manage'))
|
||||
<a href="{{ route('care.lab.catalog.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm">Catalog</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<select name="branch_id" class="rounded-lg border-slate-300 text-sm" onchange="this.form.submit()">
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected($branchId == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="status" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All active</option>
|
||||
@foreach ($statuses as $value => $label)
|
||||
@if (in_array($value, \App\Models\InvestigationRequest::activeStatuses()))
|
||||
<option value="{{ $value }}" @selected($status === $value)>{{ $label }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($queue as $item)
|
||||
<div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{{ $item->patient->fullName() }} — {{ $item->investigationType->name }}</p>
|
||||
<p class="text-xs text-slate-500">{{ $statuses[$item->status] ?? $item->status }} · {{ $item->priority }} · {{ $item->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
<a href="{{ route('care.lab.requests.show', $item) }}" class="text-sm text-sky-600">Open</a>
|
||||
</div>
|
||||
@empty
|
||||
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">Queue is empty.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,47 @@
|
||||
<x-app-layout title="Lab requests">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Investigation requests</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">All laboratory and diagnostic requests</p>
|
||||
</div>
|
||||
<a href="{{ route('care.lab.queue.index') }}" class="btn-primary">Lab queue</a>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<select name="status" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All statuses</option>
|
||||
@foreach ($statuses as $value => $label)
|
||||
<option value="{{ $value }}" @selected(request('status') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Patient</th>
|
||||
<th class="px-4 py-3">Test</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Requested</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($requests as $item)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium">{{ $item->patient->fullName() }}</td>
|
||||
<td class="px-4 py-3">{{ $item->investigationType->name }}</td>
|
||||
<td class="px-4 py-3">{{ $statuses[$item->status] ?? $item->status }}</td>
|
||||
<td class="px-4 py-3">{{ $item->created_at->format('d M Y') }}</td>
|
||||
<td class="px-4 py-3 text-right"><a href="{{ route('care.lab.requests.show', $item) }}" class="text-sky-600">View</a></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No requests found.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">{{ $requests->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,72 @@
|
||||
<x-app-layout :title="$investigation->investigationType->name">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium uppercase text-slate-500">{{ $statuses[$investigation->status] ?? $investigation->status }}</p>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $investigation->investigationType->name }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $investigation->patient->fullName() }} · {{ $investigation->patient->patient_number }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if ($canManage && $investigation->status === \App\Models\InvestigationRequest::STATUS_PENDING)
|
||||
<form method="POST" action="{{ route('care.lab.requests.collect-sample', $investigation) }}">@csrf<button class="btn-primary">Collect sample</button></form>
|
||||
@endif
|
||||
@if ($canManage && $investigation->status === \App\Models\InvestigationRequest::STATUS_SAMPLE_COLLECTED)
|
||||
<form method="POST" action="{{ route('care.lab.requests.start', $investigation) }}">@csrf<button class="btn-primary">Start processing</button></form>
|
||||
@endif
|
||||
@if ($canManage && $investigation->status === \App\Models\InvestigationRequest::STATUS_AWAITING_REVIEW)
|
||||
<form method="POST" action="{{ route('care.lab.requests.approve', $investigation) }}">@csrf<button class="btn-primary">Approve results</button></form>
|
||||
@endif
|
||||
@if ($canManage && $investigation->status === \App\Models\InvestigationRequest::STATUS_COMPLETED)
|
||||
<form method="POST" action="{{ route('care.lab.requests.deliver', $investigation) }}">@csrf<button class="btn-primary">Deliver to record</button></form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-6 lg:grid-cols-2">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6 text-sm">
|
||||
<h2 class="font-semibold uppercase text-xs text-slate-500">Request details</h2>
|
||||
<dl class="mt-4 space-y-2">
|
||||
<div><dt class="text-slate-500">Priority</dt><dd class="font-medium capitalize">{{ $investigation->priority }}</dd></div>
|
||||
<div><dt class="text-slate-500">Sample barcode</dt><dd class="font-medium font-mono">{{ $investigation->sample_barcode ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Clinical notes</dt><dd class="font-medium">{{ $investigation->clinical_notes ?? '—' }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@if ($canManage && in_array($investigation->status, [\App\Models\InvestigationRequest::STATUS_IN_PROGRESS, \App\Models\InvestigationRequest::STATUS_AWAITING_REVIEW], true))
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Enter results</h2>
|
||||
<form method="POST" action="{{ route('care.lab.requests.results', $investigation) }}" enctype="multipart/form-data" class="mt-4 space-y-4">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm text-slate-700">Value</label>
|
||||
<input type="text" name="value" class="mt-1 w-full rounded-lg border-slate-300 text-sm" placeholder="e.g. 5.2">
|
||||
<p class="mt-1 text-xs text-slate-400">Ref: {{ $investigation->investigationType->reference_text ?? ($investigation->investigationType->reference_low.'–'.$investigation->investigationType->reference_high) }} {{ $investigation->investigationType->unit }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-700">Summary</label>
|
||||
<textarea name="result_summary" rows="2" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-700">Interpretation</label>
|
||||
<textarea name="interpretation" rows="2" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></textarea>
|
||||
</div>
|
||||
<input type="file" name="attachments[]" multiple class="text-sm">
|
||||
<button type="submit" class="btn-primary">Save results</button>
|
||||
</form>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($investigation->result && ($canViewResults || $canManage))
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6 lg:col-span-2">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Results @if ($investigation->result->is_abnormal)<span class="text-red-600">· Abnormal</span>@endif</h2>
|
||||
@foreach ($investigation->result->values as $val)
|
||||
<p class="mt-3 text-sm">
|
||||
<span class="font-medium {{ $val->is_abnormal ? 'text-red-600' : '' }}">{{ $val->parameter }}: {{ $val->value }} {{ $val->unit }}</span>
|
||||
@if ($val->reference_text || $val->reference_low) <span class="text-slate-400">(ref: {{ $val->reference_text ?? $val->reference_low.'–'.$val->reference_high }})</span> @endif
|
||||
</p>
|
||||
@endforeach
|
||||
@if ($investigation->result->result_summary)<p class="mt-3 text-sm">{{ $investigation->result->result_summary }}</p>@endif
|
||||
@if ($investigation->result->interpretation)<p class="mt-2 text-sm text-slate-600">{{ $investigation->result->interpretation }}</p>@endif
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,62 @@
|
||||
<x-app-layout title="Set up Care">
|
||||
<div class="mx-auto max-w-xl">
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Welcome to Ladill Care</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">Set up your healthcare facility to get started.</p>
|
||||
|
||||
<form method="POST" action="{{ route('care.onboarding.store') }}" enctype="multipart/form-data" class="mt-8 space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Facility / organization name</label>
|
||||
<input type="text" name="organization_name" value="{{ old('organization_name', $user->name."'s Clinic") }}" required
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Facility type</label>
|
||||
<select name="facility_type" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($facilityTypes as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('facility_type', 'clinic') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Primary branch</label>
|
||||
<input type="text" name="branch_name" value="{{ old('branch_name', 'Main Branch') }}" required
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Branch address</label>
|
||||
<input type="text" name="branch_address" value="{{ old('branch_address') }}"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Branch phone</label>
|
||||
<input type="tel" name="branch_phone" value="{{ old('branch_phone') }}"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Logo (optional)</label>
|
||||
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||
class="mt-2 block w-full text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-sky-700">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Timezone</label>
|
||||
<select name="timezone" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($timezones as $tz)
|
||||
<option value="{{ $tz }}" @selected(old('timezone', config('app.timezone')) === $tz)>{{ $tz }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-primary-lg w-full">
|
||||
Complete setup
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,146 @@
|
||||
@php
|
||||
$patient = $patient ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Demographics</h2>
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">First name</label>
|
||||
<input type="text" name="first_name" value="{{ old('first_name', $patient?->first_name) }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Last name</label>
|
||||
<input type="text" name="last_name" value="{{ old('last_name', $patient?->last_name) }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Other names</label>
|
||||
<input type="text" name="other_names" value="{{ old('other_names', $patient?->other_names) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Gender</label>
|
||||
<select name="gender" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">—</option>
|
||||
@foreach ($genders as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('gender', $patient?->gender) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Date of birth</label>
|
||||
<input type="date" name="date_of_birth" value="{{ old('date_of_birth', $patient?->date_of_birth?->format('Y-m-d')) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Phone</label>
|
||||
<input type="tel" name="phone" value="{{ old('phone', $patient?->phone) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Email</label>
|
||||
<input type="email" name="email" value="{{ old('email', $patient?->email) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">National ID</label>
|
||||
<input type="text" name="national_id" value="{{ old('national_id', $patient?->national_id) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Branch</label>
|
||||
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">—</option>
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected(old('branch_id', $patient?->branch_id) == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Address</label>
|
||||
<input type="text" name="address" value="{{ old('address', $patient?->address) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">City</label>
|
||||
<input type="text" name="city" value="{{ old('city', $patient?->city) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Region</label>
|
||||
<input type="text" name="region" value="{{ old('region', $patient?->region) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700">Notes</label>
|
||||
<textarea name="notes" rows="3" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('notes', $patient?->notes) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Allergies</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
@for ($i = 0; $i < 3; $i++)
|
||||
@php $allergy = old("allergies.{$i}", $patient?->allergies[$i] ?? null); @endphp
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<input type="text" name="allergies[{{ $i }}][allergen]" value="{{ is_array($allergy) ? ($allergy['allergen'] ?? '') : ($allergy?->allergen ?? '') }}" placeholder="Allergen" class="rounded-lg border-slate-300 text-sm">
|
||||
<select name="allergies[{{ $i }}][severity]" class="rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($allergySeverities as $value => $label)
|
||||
<option value="{{ $value }}" @selected((is_array($allergy) ? ($allergy['severity'] ?? '') : ($allergy?->severity ?? '')) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="text" name="allergies[{{ $i }}][notes]" value="{{ is_array($allergy) ? ($allergy['notes'] ?? '') : ($allergy?->notes ?? '') }}" placeholder="Notes" class="rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Chronic conditions</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
@for ($i = 0; $i < 3; $i++)
|
||||
@php $condition = old("conditions.{$i}", $patient?->conditions[$i] ?? null); @endphp
|
||||
<div class="grid gap-3 sm:grid-cols-4">
|
||||
<input type="text" name="conditions[{{ $i }}][condition]" value="{{ is_array($condition) ? ($condition['condition'] ?? '') : ($condition?->condition ?? '') }}" placeholder="Condition" class="rounded-lg border-slate-300 text-sm sm:col-span-2">
|
||||
<input type="date" name="conditions[{{ $i }}][onset_date]" value="{{ is_array($condition) ? ($condition['onset_date'] ?? '') : ($condition?->onset_date?->format('Y-m-d') ?? '') }}" class="rounded-lg border-slate-300 text-sm">
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="conditions[{{ $i }}][is_chronic]" value="1" @checked(is_array($condition) ? ($condition['is_chronic'] ?? false) : ($condition?->is_chronic ?? false))> Chronic</label>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Family history</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
@for ($i = 0; $i < 2; $i++)
|
||||
@php $family = old("family_history.{$i}", $patient?->familyHistory[$i] ?? null); @endphp
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<input type="text" name="family_history[{{ $i }}][relation]" value="{{ is_array($family) ? ($family['relation'] ?? '') : ($family?->relation ?? '') }}" placeholder="Relation" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="text" name="family_history[{{ $i }}][condition]" value="{{ is_array($family) ? ($family['condition'] ?? '') : ($family?->condition ?? '') }}" placeholder="Condition" class="rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Emergency contact</h2>
|
||||
@php $contact = old('emergency_contacts.0', $patient?->emergencyContacts->first()); @endphp
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<input type="text" name="emergency_contacts[0][name]" value="{{ is_array($contact) ? ($contact['name'] ?? '') : ($contact?->name ?? '') }}" placeholder="Name" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="tel" name="emergency_contacts[0][phone]" value="{{ is_array($contact) ? ($contact['phone'] ?? '') : ($contact?->phone ?? '') }}" placeholder="Phone" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="text" name="emergency_contacts[0][relationship]" value="{{ is_array($contact) ? ($contact['relationship'] ?? '') : ($contact?->relationship ?? '') }}" placeholder="Relationship" class="rounded-lg border-slate-300 text-sm">
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="emergency_contacts[0][is_primary]" value="1" @checked(is_array($contact) ? ($contact['is_primary'] ?? true) : ($contact?->is_primary ?? true))> Primary contact</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Insurance</h2>
|
||||
@php $insurance = old('insurance.0', $patient?->insurancePolicies->first()); @endphp
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<input type="text" name="insurance[0][provider_name]" value="{{ is_array($insurance) ? ($insurance['provider_name'] ?? '') : ($insurance?->provider_name ?? '') }}" placeholder="Provider" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="text" name="insurance[0][policy_number]" value="{{ is_array($insurance) ? ($insurance['policy_number'] ?? '') : ($insurance?->policy_number ?? '') }}" placeholder="Policy number" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="text" name="insurance[0][coverage_type]" value="{{ is_array($insurance) ? ($insurance['coverage_type'] ?? '') : ($insurance?->coverage_type ?? '') }}" placeholder="Coverage type" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="date" name="insurance[0][expiry_date]" value="{{ is_array($insurance) ? ($insurance['expiry_date'] ?? '') : ($insurance?->expiry_date?->format('Y-m-d') ?? '') }}" class="rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Documents</h2>
|
||||
<input type="file" name="attachments[]" multiple accept=".pdf,.jpg,.jpeg,.png,.webp" class="mt-4 block w-full text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-sky-700">
|
||||
<p class="mt-2 text-xs text-slate-500">PDF or images, up to 10 MB each.</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<x-app-layout title="Register patient">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Register patient</h1>
|
||||
<form method="POST" action="{{ route('care.patients.store') }}" enctype="multipart/form-data" class="mt-6 space-y-6">
|
||||
@csrf
|
||||
@include('care.patients._form')
|
||||
<button type="submit" class="btn-primary btn-primary-lg">Register patient</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-app-layout title="Edit patient">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Edit patient</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $patient->patient_number }}</p>
|
||||
<form method="POST" action="{{ route('care.patients.update', $patient) }}" enctype="multipart/form-data" class="mt-6 space-y-6">
|
||||
@csrf @method('PUT')
|
||||
@include('care.patients._form', ['patient' => $patient])
|
||||
<button type="submit" class="btn-primary btn-primary-lg">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,62 @@
|
||||
<x-app-layout title="Patients">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Patients</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Search and manage patient records</p>
|
||||
</div>
|
||||
@if (app(\App\Services\Care\CarePermissions::class)->can(auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user(), $organization) : null, 'patients.manage'))
|
||||
<a href="{{ route('care.patients.create') }}" class="btn-primary">Register patient</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<input type="search" name="q" value="{{ request('q') }}" placeholder="Name, phone, patient ID, national ID…"
|
||||
class="min-w-[200px] flex-1 rounded-lg border-slate-300 text-sm">
|
||||
<input type="date" name="date_of_birth" value="{{ request('date_of_birth') }}"
|
||||
class="rounded-lg border-slate-300 text-sm" title="Date of birth">
|
||||
<button type="submit" class="btn-primary">Search</button>
|
||||
@if (request()->hasAny(['q', 'date_of_birth', 'phone', 'national_id', 'patient_number']))
|
||||
<a href="{{ route('care.patients.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Clear</a>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Patient</th>
|
||||
<th class="px-4 py-3">Patient ID</th>
|
||||
<th class="px-4 py-3">Phone</th>
|
||||
<th class="px-4 py-3">DOB</th>
|
||||
<th class="px-4 py-3">Branch</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($patients as $patient)
|
||||
<tr>
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-medium text-slate-900">{{ $patient->fullName() }}</p>
|
||||
@if ($patient->national_id)
|
||||
<p class="text-xs text-slate-500">NID: {{ $patient->national_id }}</p>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono text-xs">{{ $patient->patient_number }}</td>
|
||||
<td class="px-4 py-3">{{ $patient->phone ?? '—' }}</td>
|
||||
<td class="px-4 py-3">{{ $patient->date_of_birth?->format('Y-m-d') ?? '—' }}</td>
|
||||
<td class="px-4 py-3">{{ $patient->branch?->name ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ route('care.patients.show', $patient) }}" class="text-sky-600 hover:text-sky-700">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-slate-500">No patients found.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $patients->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,190 @@
|
||||
<x-app-layout :title="$patient->fullName()">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="font-mono text-xs text-sky-600">{{ $patient->patient_number }}</p>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $patient->fullName() }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
{{ $genders[$patient->gender] ?? '—' }}
|
||||
@if ($patient->date_of_birth)
|
||||
· {{ $patient->date_of_birth->format('d M Y') }}
|
||||
({{ $patient->date_of_birth->age }} yrs)
|
||||
@endif
|
||||
@if ($patient->phone) · {{ $patient->phone }} @endif
|
||||
</p>
|
||||
</div>
|
||||
@if ($canManage)
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('care.patients.edit', $patient) }}" class="btn-primary">Edit</a>
|
||||
<form method="POST" action="{{ route('care.patients.destroy', $patient) }}" onsubmit="return confirm('Archive this patient record?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="rounded-lg border border-red-200 px-4 py-2 text-sm text-red-600 hover:bg-red-50">Archive</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-6 lg:grid-cols-3">
|
||||
<div class="space-y-6 lg:col-span-2">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Personal information</h2>
|
||||
<dl class="mt-4 grid gap-3 sm:grid-cols-2 text-sm">
|
||||
<div><dt class="text-slate-500">National ID</dt><dd class="font-medium">{{ $patient->national_id ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Email</dt><dd class="font-medium">{{ $patient->email ?? '—' }}</dd></div>
|
||||
<div class="sm:col-span-2"><dt class="text-slate-500">Address</dt><dd class="font-medium">{{ collect([$patient->address, $patient->city, $patient->region])->filter()->implode(', ') ?: '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Branch</dt><dd class="font-medium">{{ $patient->branch?->name ?? '—' }}</dd></div>
|
||||
</dl>
|
||||
@if ($patient->notes)
|
||||
<p class="mt-4 text-sm text-slate-600">{{ $patient->notes }}</p>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Medical history</h2>
|
||||
<div class="mt-4 space-y-4 text-sm">
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800">Allergies</h3>
|
||||
@forelse ($patient->allergies as $allergy)
|
||||
<p class="mt-1 text-slate-600">{{ $allergy->allergen }} <span class="text-slate-400">({{ $allergySeverities[$allergy->severity] ?? $allergy->severity }})</span></p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">None recorded</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800">Conditions</h3>
|
||||
@forelse ($patient->conditions as $condition)
|
||||
<p class="mt-1 text-slate-600">{{ $condition->condition }}@if ($condition->is_chronic) <span class="text-amber-600">(chronic)</span>@endif</p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">None recorded</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800">Family history</h3>
|
||||
@forelse ($patient->familyHistory as $entry)
|
||||
<p class="mt-1 text-slate-600">{{ $entry->relation }}: {{ $entry->condition }}</p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">None recorded</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Visit & clinical history</h2>
|
||||
<div class="mt-4 space-y-4 text-sm">
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800">Recent visits</h3>
|
||||
@forelse ($visits as $visit)
|
||||
<p class="mt-1 text-slate-600">
|
||||
{{ $visit->checked_in_at?->format('d M Y H:i') ?? '—' }}
|
||||
· {{ config('care.visit_statuses')[$visit->status] ?? $visit->status }}
|
||||
@if ($visit->branch) · {{ $visit->branch->name }} @endif
|
||||
</p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">No visits yet</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800">Consultations</h3>
|
||||
@forelse ($consultations as $consultation)
|
||||
<p class="mt-1">
|
||||
<a href="{{ route('care.consultations.show', $consultation) }}" class="text-sky-600 hover:text-sky-700">
|
||||
{{ $consultation->started_at?->format('d M Y') ?? '—' }}
|
||||
@if ($consultation->practitioner) · {{ $consultation->practitioner->name }} @endif
|
||||
</a>
|
||||
@if ($consultation->diagnoses->isNotEmpty())
|
||||
<span class="text-slate-400">— {{ $consultation->diagnoses->first()->description }}</span>
|
||||
@endif
|
||||
</p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">No consultations yet</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800">Laboratory</h3>
|
||||
@forelse ($laboratory as $lab)
|
||||
<p class="mt-1">
|
||||
<a href="{{ route('care.lab.requests.show', $lab) }}" class="text-sky-600 hover:text-sky-700">
|
||||
{{ $lab->investigationType->name }}
|
||||
</a>
|
||||
· {{ $lab->completed_at?->format('d M Y') ?? '—' }}
|
||||
@if ($lab->result?->is_abnormal)
|
||||
<span class="text-red-600">abnormal</span>
|
||||
@endif
|
||||
</p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">No lab results yet</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800">Prescriptions</h3>
|
||||
@forelse ($prescriptions as $rx)
|
||||
<p class="mt-1">
|
||||
<a href="{{ route('care.prescriptions.show', $rx) }}" class="text-sky-600 hover:text-sky-700">
|
||||
{{ $rx->created_at->format('d M Y') }}
|
||||
</a>
|
||||
· {{ config('care.prescription_statuses')[$rx->status] ?? $rx->status }}
|
||||
· {{ $rx->items->pluck('name')->take(2)->join(', ') }}
|
||||
</p>
|
||||
@empty
|
||||
<p class="mt-1 text-slate-400">No prescriptions yet</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Emergency contacts</h2>
|
||||
@forelse ($patient->emergencyContacts as $contact)
|
||||
<div class="mt-3 text-sm">
|
||||
<p class="font-medium">{{ $contact->name }}@if ($contact->is_primary) <span class="text-sky-600">(primary)</span>@endif</p>
|
||||
<p class="text-slate-600">{{ $contact->phone }} · {{ $contact->relationship ?? '—' }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="mt-3 text-sm text-slate-400">None recorded</p>
|
||||
@endforelse
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Insurance</h2>
|
||||
@forelse ($patient->insurancePolicies as $policy)
|
||||
<div class="mt-3 text-sm">
|
||||
<p class="font-medium">{{ $policy->provider_name }}</p>
|
||||
<p class="text-slate-600">{{ $policy->policy_number ?? '—' }} · {{ $policy->coverage_type ?? '—' }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="mt-3 text-sm text-slate-400">None recorded</p>
|
||||
@endforelse
|
||||
</section>
|
||||
|
||||
@php $money = fn ($minor) => config('care.billing.currency').' '.number_format($minor / 100, 2); @endphp
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Billing</h2>
|
||||
<div class="mt-3 space-y-2 text-sm">
|
||||
@forelse ($invoices as $invoice)
|
||||
<div>
|
||||
<a href="{{ route('care.bills.show', $invoice) }}" class="font-medium text-sky-600 hover:text-sky-700">{{ $invoice->invoice_number }}</a>
|
||||
<p class="text-slate-600">{{ $invoice->created_at->format('d M Y') }} · {{ $money($invoice->total_minor) }} · {{ config('care.bill_statuses')[$invoice->status] ?? $invoice->status }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-slate-400">No invoices yet</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Documents</h2>
|
||||
<ul class="mt-3 space-y-2 text-sm">
|
||||
@forelse ($patient->attachments as $attachment)
|
||||
<li>
|
||||
<a href="{{ $attachment->url() }}" target="_blank" class="text-sky-600 hover:underline">{{ $attachment->original_name }}</a>
|
||||
</li>
|
||||
@empty
|
||||
<li class="text-slate-400">No documents uploaded</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="space-y-4">
|
||||
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" value="{{ old('name', $drug->name ?? '') }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Generic name</label><input type="text" name="generic_name" value="{{ old('generic_name', $drug->generic_name ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="block text-sm font-medium">SKU</label><input type="text" name="sku" value="{{ old('sku', $drug->sku ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Unit</label><input type="text" name="unit" value="{{ old('unit', $drug->unit ?? 'unit') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="block text-sm font-medium">Unit price (minor)</label><input type="number" name="unit_price_minor" value="{{ old('unit_price_minor', $drug->unit_price_minor ?? 0) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Reorder level</label><input type="number" name="reorder_level" value="{{ old('reorder_level', $drug->reorder_level ?? 10) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-app-layout title="Add drug">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Add drug</h1>
|
||||
<form method="POST" action="{{ route('care.pharmacy.drugs.store') }}" class="mt-6 max-w-xl space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
@include('care.pharmacy.drugs._form', ['drug' => null])
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-app-layout title="Edit drug">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Edit {{ $drug->name }}</h1>
|
||||
<form method="POST" action="{{ route('care.pharmacy.drugs.update', $drug) }}" class="mt-6 max-w-xl space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf @method('PUT')
|
||||
@include('care.pharmacy.drugs._form')
|
||||
<button type="submit" class="btn-primary">Update</button>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,32 @@
|
||||
@php $money = fn ($minor) => config('care.billing.currency').' '.number_format($minor / 100, 2); @endphp
|
||||
<x-app-layout title="Drug inventory">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Drug inventory</h1>
|
||||
@if ($canManage)<a href="{{ route('care.pharmacy.drugs.create') }}" class="btn-primary">Add drug</a>@endif
|
||||
</div>
|
||||
@if ($lowStock->isNotEmpty())
|
||||
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">{{ $lowStock->count() }} drug(s) below reorder level.</div>
|
||||
@endif
|
||||
@if ($expired->isNotEmpty())
|
||||
<div class="mt-4 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900">{{ $expired->count() }} expired batch(es) with stock on hand.</div>
|
||||
@endif
|
||||
<form method="GET" class="mt-4"><input type="search" name="q" value="{{ request('q') }}" placeholder="Search drugs…" class="rounded-lg border-slate-300 text-sm"></form>
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500"><tr><th class="px-4 py-3">Drug</th><th class="px-4 py-3">Stock</th><th class="px-4 py-3">Unit price</th><th></th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($drugs as $drug)
|
||||
<tr>
|
||||
<td class="px-4 py-3"><p class="font-medium">{{ $drug->name }}</p>@if($drug->generic_name)<p class="text-xs text-slate-500">{{ $drug->generic_name }}</p>@endif</td>
|
||||
<td class="px-4 py-3 {{ $drug->stockOnHand() <= $drug->reorder_level ? 'text-amber-700 font-medium' : '' }}">{{ $drug->stockOnHand() }} {{ $drug->unit }}</td>
|
||||
<td class="px-4 py-3">{{ $money($drug->unit_price_minor) }}</td>
|
||||
<td class="px-4 py-3 text-right"><a href="{{ route('care.pharmacy.drugs.show', $drug) }}" class="text-sky-600">View</a></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-slate-500">No drugs in inventory.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">{{ $drugs->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,36 @@
|
||||
@php $money = fn ($minor) => config('care.billing.currency').' '.number_format($minor / 100, 2); @endphp
|
||||
<x-app-layout :title="$drug->name">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $drug->name }}</h1>
|
||||
<p class="text-sm text-slate-500">Stock: {{ $drug->stockOnHand() }} {{ $drug->unit }} · {{ $money($drug->unit_price_minor) }}</p>
|
||||
</div>
|
||||
@if ($canManage)<a href="{{ route('care.pharmacy.drugs.edit', $drug) }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm">Edit</a>@endif
|
||||
</div>
|
||||
@if ($canManage)
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Receive stock</h2>
|
||||
<form method="POST" action="{{ route('care.pharmacy.drugs.batches.store', $drug) }}" class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
@csrf
|
||||
<input type="text" name="batch_number" required placeholder="Batch number" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="date" name="expiry_date" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="number" name="quantity_on_hand" required min="1" placeholder="Quantity" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="number" name="cost_minor" min="0" placeholder="Cost (minor)" class="rounded-lg border-slate-300 text-sm">
|
||||
<button type="submit" class="btn-primary sm:col-span-2">Receive batch</button>
|
||||
</form>
|
||||
</section>
|
||||
@endif
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Batches</h2>
|
||||
<div class="mt-4 space-y-2 text-sm">
|
||||
@forelse ($drug->batches as $batch)
|
||||
<div class="flex justify-between rounded-lg border border-slate-100 bg-slate-50 px-3 py-2 {{ $batch->isExpired() ? 'border-red-200 bg-red-50' : '' }}">
|
||||
<span>{{ $batch->batch_number }} · {{ $batch->quantity_on_hand }} on hand</span>
|
||||
<span class="text-slate-500">{{ $batch->expiry_date?->format('Y-m-d') ?? 'No expiry' }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-slate-400">No batches received.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,43 @@
|
||||
<x-app-layout :title="'Prescription · '.$consultation->patient->fullName()">
|
||||
<h1 class="text-xl font-semibold text-slate-900">New prescription</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $consultation->patient->fullName() }} ({{ $consultation->patient->patient_number }})</p>
|
||||
|
||||
<form method="POST" action="{{ route('care.prescriptions.store', $consultation) }}" class="mt-6 space-y-6" x-data="{ items: [{ is_procedure: false, name: '', dosage: '', frequency: '', duration: '', route: 'oral', quantity: '', instructions: '' }] }">
|
||||
@csrf
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Medications & procedures</h2>
|
||||
<button type="button" @click="items.push({ is_procedure: false, name: '', dosage: '', frequency: '', duration: '', route: 'oral', quantity: '', instructions: '' })" class="text-sm text-sky-600">Add line</button>
|
||||
</div>
|
||||
<template x-for="(item, index) in items" :key="index">
|
||||
<div class="mt-4 grid gap-3 rounded-xl border border-slate-100 bg-slate-50 p-4 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2 flex items-center gap-4">
|
||||
<label class="inline-flex items-center gap-2 text-sm"><input type="checkbox" :name="'items['+index+'][is_procedure]'" value="1" x-model="item.is_procedure" class="rounded border-slate-300"> Procedure</label>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<input type="text" :name="'items['+index+'][name]'" x-model="item.name" required placeholder="Medication or procedure name" class="w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<input type="text" :name="'items['+index+'][dosage]'" x-model="item.dosage" placeholder="Dosage" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="text" :name="'items['+index+'][frequency]'" x-model="item.frequency" placeholder="Frequency" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="text" :name="'items['+index+'][duration]'" x-model="item.duration" placeholder="Duration" class="rounded-lg border-slate-300 text-sm">
|
||||
<select :name="'items['+index+'][route]'" x-model="item.route" class="rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($routes as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="text" :name="'items['+index+'][quantity]'" x-model="item.quantity" placeholder="Quantity" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="text" :name="'items['+index+'][instructions]'" x-model="item.instructions" placeholder="Instructions" class="sm:col-span-2 rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<label class="block text-sm font-medium text-slate-700">Notes</label>
|
||||
<textarea name="notes" rows="2" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></textarea>
|
||||
<input type="hidden" name="activate" value="1">
|
||||
</section>
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Create prescription</button>
|
||||
<a href="{{ route('care.consultations.show', $consultation) }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,30 @@
|
||||
<x-app-layout title="Prescriptions">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Prescriptions</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">All prescriptions across the facility</p>
|
||||
</div>
|
||||
<a href="{{ route('care.prescriptions.queue') }}" class="btn-primary">Pharmacy queue</a>
|
||||
</div>
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Patient</th><th class="px-4 py-3">Items</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Date</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($prescriptions as $rx)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium">{{ $rx->patient->fullName() }}</td>
|
||||
<td class="px-4 py-3">{{ $rx->items->pluck('name')->join(', ') }}</td>
|
||||
<td class="px-4 py-3">{{ $statuses[$rx->status] ?? $rx->status }}</td>
|
||||
<td class="px-4 py-3">{{ $rx->created_at->format('d M Y') }}</td>
|
||||
<td class="px-4 py-3 text-right"><a href="{{ route('care.prescriptions.show', $rx) }}" class="text-sky-600">View</a></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No prescriptions.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">{{ $prescriptions->links() }}</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,59 @@
|
||||
<x-app-layout title="Pharmacy queue">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Pharmacy queue</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Active prescriptions awaiting dispensing</p>
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($queue as $rx)
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium text-slate-900">{{ $rx->patient->fullName() }}</p>
|
||||
<p class="text-xs text-slate-500">{{ $rx->patient->patient_number }} · {{ $rx->visit->branch?->name }}</p>
|
||||
<ul class="mt-2 space-y-1 text-sm text-slate-700">
|
||||
@foreach ($rx->items as $item)
|
||||
<li>{{ $item->is_procedure ? 'Procedure' : 'Med' }}: {{ $item->name }} {{ $item->dosage }} {{ $item->frequency }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@if ($canDispense)
|
||||
<form method="POST" action="{{ route('care.prescriptions.dispense', $rx) }}" class="mt-4 border-t border-slate-100 pt-4">
|
||||
@csrf
|
||||
@php $allocIndex = 0; @endphp
|
||||
@foreach ($rx->items as $item)
|
||||
@if (! $item->is_procedure)
|
||||
<div class="mt-2 grid gap-2 sm:grid-cols-[1fr_auto_auto] sm:items-end">
|
||||
<div>
|
||||
<label class="text-xs text-slate-500">Stock for {{ $item->name }}</label>
|
||||
<select name="allocations[{{ $allocIndex }}][drug_batch_id]" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">Skip stock deduction</option>
|
||||
@foreach ($drugs as $drug)
|
||||
@foreach ($drug->batches as $batch)
|
||||
<option value="{{ $batch->id }}">{{ $drug->name }} · {{ $batch->batch_number }} ({{ $batch->quantity_on_hand }} {{ $drug->unit }})</option>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="allocations[{{ $allocIndex }}][prescription_item_id]" value="{{ $item->id }}">
|
||||
<div>
|
||||
<label class="text-xs text-slate-500">Qty</label>
|
||||
<input type="number" name="allocations[{{ $allocIndex }}][quantity]" value="1" min="1" class="mt-1 w-20 rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
@php $allocIndex++; @endphp
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button type="submit" class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-700">Dispense</button>
|
||||
<a href="{{ route('care.prescriptions.show', $rx) }}" class="text-sm text-sky-600">View details</a>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@unless ($canDispense)
|
||||
<a href="{{ route('care.prescriptions.show', $rx) }}" class="text-sm text-sky-600">View</a>
|
||||
@endunless
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">No prescriptions in queue.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,40 @@
|
||||
<x-app-layout :title="'Prescription · '.$prescription->patient->fullName()">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs uppercase text-slate-500">{{ $statuses[$prescription->status] ?? $prescription->status }}</p>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $prescription->patient->fullName() }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $prescription->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if ($canManage && $prescription->status === \App\Models\Prescription::STATUS_DRAFT)
|
||||
<form method="POST" action="{{ route('care.prescriptions.activate', $prescription) }}">@csrf<button class="btn-primary">Activate</button></form>
|
||||
@endif
|
||||
@if ($canDispense && $prescription->status === \App\Models\Prescription::STATUS_ACTIVE)
|
||||
<form method="POST" action="{{ route('care.prescriptions.dispense', $prescription) }}">@csrf<button class="btn-primary">Dispense</button></form>
|
||||
@endif
|
||||
@if ($canManage && ! in_array($prescription->status, [\App\Models\Prescription::STATUS_DISPENSED, \App\Models\Prescription::STATUS_CANCELLED]))
|
||||
<form method="POST" action="{{ route('care.prescriptions.cancel', $prescription) }}" onsubmit="return confirm('Cancel prescription?')">@csrf<button class="rounded-lg border border-red-200 px-4 py-2 text-sm text-red-600">Cancel</button></form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase text-slate-500">Items</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
@foreach ($prescription->items as $item)
|
||||
<div class="rounded-xl border border-slate-100 bg-slate-50 p-4 text-sm">
|
||||
<p class="font-medium text-slate-900">{{ $item->name }} @if ($item->is_procedure)<span class="text-amber-600">(procedure)</span>@endif</p>
|
||||
<p class="mt-1 text-slate-600">
|
||||
@if ($item->dosage) {{ $item->dosage }} @endif
|
||||
@if ($item->frequency) · {{ $item->frequency }} @endif
|
||||
@if ($item->duration) · {{ $item->duration }} @endif
|
||||
@if ($item->route) · {{ $routes[$item->route] ?? $item->route }} @endif
|
||||
@if ($item->quantity) · Qty: {{ $item->quantity }} @endif
|
||||
</p>
|
||||
@if ($item->instructions)<p class="mt-1 text-slate-500">{{ $item->instructions }}</p>@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@if ($prescription->notes)<p class="mt-4 text-sm text-slate-600">{{ $prescription->notes }}</p>@endif
|
||||
</section>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,74 @@
|
||||
<x-app-layout title="Queue">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Patient queue</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Waiting patients and active consultations</p>
|
||||
</div>
|
||||
@if ($canManageQueue)
|
||||
<a href="{{ route('care.appointments.walk-in.create') }}" class="btn-primary">Walk-in</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<select name="branch_id" class="rounded-lg border-slate-300 text-sm" onchange="this.form.submit()">
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected($branchId == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="practitioner_id" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All practitioners</option>
|
||||
@foreach ($practitioners as $practitioner)
|
||||
<option value="{{ $practitioner->id }}" @selected($practitionerId == $practitioner->id)>{{ $practitioner->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 grid gap-6 lg:grid-cols-2">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Waiting ({{ $queue->count() }})</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($queue as $appointment)
|
||||
<div class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 p-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">
|
||||
@if ($appointment->queue_position)
|
||||
<span class="mr-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-sky-100 text-xs font-bold text-sky-700">{{ $appointment->queue_position }}</span>
|
||||
@endif
|
||||
{{ $appointment->patient->fullName() }}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">{{ $appointment->patient->patient_number }} · {{ $appointment->reason ?? '—' }}</p>
|
||||
</div>
|
||||
@if ($canConsult)
|
||||
<form method="POST" action="{{ route('care.queue.start', $appointment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">Start</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-slate-500">No patients waiting.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">In consultation ({{ $inConsultation->count() }})</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($inConsultation as $appointment)
|
||||
<div class="flex items-center justify-between rounded-xl border border-emerald-100 bg-emerald-50 p-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{{ $appointment->patient->fullName() }}</p>
|
||||
<p class="text-xs text-slate-500">{{ $appointment->practitioner?->name ?? 'Unassigned' }}</p>
|
||||
</div>
|
||||
@if ($appointment->consultation)
|
||||
<a href="{{ route('care.consultations.show', $appointment->consultation) }}" class="text-sm text-sky-600 hover:text-sky-700">Open</a>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-slate-500">No active consultations.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,12 @@
|
||||
<x-app-layout title="Reports">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Reports</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Operational and financial analytics</p>
|
||||
<div class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($reports as $key => $label)
|
||||
<a href="{{ route('care.reports.show', $key) }}" class="rounded-2xl border border-slate-200 bg-white p-6 hover:border-sky-200 hover:bg-sky-50/50">
|
||||
<h2 class="font-semibold text-slate-900">{{ $label }}</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">View and export</p>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,52 @@
|
||||
@php
|
||||
$money = fn ($minor) => config('care.billing.currency').' '.number_format($minor / 100, 2);
|
||||
$formatValue = function ($key, $value) use ($money) {
|
||||
if (str_ends_with($key, '_minor')) return $money((int) $value);
|
||||
return $value;
|
||||
};
|
||||
@endphp
|
||||
<x-app-layout :title="$label">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">{{ $label }}</h1>
|
||||
<p class="text-sm text-slate-500">{{ $from }} to {{ $to }}</p>
|
||||
</div>
|
||||
@if ($canExport)
|
||||
<a href="{{ route('care.reports.export', ['type' => $type, 'from' => $from, 'to' => $to, 'branch_id' => $branchId]) }}" class="btn-primary">Export CSV</a>
|
||||
@endif
|
||||
</div>
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<input type="date" name="from" value="{{ $from }}" class="rounded-lg border-slate-300 text-sm">
|
||||
<input type="date" name="to" value="{{ $to }}" class="rounded-lg border-slate-300 text-sm">
|
||||
<select name="branch_id" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All branches</option>
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected($branchId == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Apply</button>
|
||||
</form>
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@if ($type === 'clinical' && isset($data['diagnoses']))
|
||||
<table class="min-w-full text-sm">
|
||||
<thead><tr><th class="py-2 text-left">Diagnosis</th><th class="py-2 text-right">Count</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse ($data['diagnoses'] as $row)
|
||||
<tr class="border-t border-slate-50"><td class="py-2">{{ $row->description }}</td><td class="py-2 text-right">{{ $row->total }}</td></tr>
|
||||
@empty
|
||||
<tr><td colspan="2" class="py-4 text-slate-500">No diagnoses in period.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<dl class="grid gap-4 sm:grid-cols-2">
|
||||
@foreach ($data as $key => $value)
|
||||
<div class="rounded-xl border border-slate-100 bg-slate-50 p-4">
|
||||
<dt class="text-xs uppercase text-slate-500">{{ str_replace('_', ' ', $key) }}</dt>
|
||||
<dd class="mt-1 text-2xl font-semibold text-slate-900">{{ $formatValue($key, $value) }}</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
@endif
|
||||
</section>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,45 @@
|
||||
<x-app-layout title="Settings">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Facility settings</h1>
|
||||
|
||||
<form method="POST" action="{{ route('care.settings.update') }}" enctype="multipart/form-data" class="mt-6 space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Organization name</label>
|
||||
<input type="text" name="name" value="{{ old('name', $organization->name) }}" @disabled(! $canManage) required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Facility type</label>
|
||||
<select name="facility_type" @disabled(! $canManage) class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($facilityTypes as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('facility_type', data_get($organization->settings, 'facility_type', 'clinic')) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Timezone</label>
|
||||
<select name="timezone" @disabled(! $canManage) class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach (timezone_identifiers_list() as $tz)
|
||||
<option value="{{ $tz }}" @selected(old('timezone', $organization->timezone) === $tz)>{{ $tz }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if ($canManage)
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Logo</label>
|
||||
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/svg+xml" class="mt-2 block w-full text-sm">
|
||||
@if (\App\Support\OrganizationBranding::hasCustomLogo($organization))
|
||||
<label class="mt-2 flex items-center gap-2 text-sm"><input type="checkbox" name="remove_logo" value="1"> Remove current logo</label>
|
||||
@endif
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Save settings</button>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-sm text-slate-500">{{ $branchCount }} branch(es) configured.</p>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,60 @@
|
||||
@props(['title' => 'Ladill Care', 'heading' => null])
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{{ $title }} · Ladill Care</title>
|
||||
@include('partials.favicon')
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="h-full font-sans antialiased bg-slate-100 text-slate-900" x-data="{ sidebarOpen: false }">
|
||||
@include('partials.boot-splash')
|
||||
<div class="min-h-screen max-lg:min-h-dvh">
|
||||
<div x-show="sidebarOpen" x-cloak class="fixed inset-0 z-30 bg-black/50 lg:hidden" @click="sidebarOpen = false"></div>
|
||||
<aside x-data="{ ready: false }" x-init="requestAnimationFrame(() => ready = true)"
|
||||
class="fixed inset-y-0 left-0 z-40 w-64 -translate-x-full transform lg:translate-x-0"
|
||||
:class="{ '!translate-x-0': sidebarOpen, 'transition-transform': ready }">
|
||||
@include('partials.sidebar')
|
||||
</aside>
|
||||
<div class="flex min-h-screen flex-col min-w-0 lg:pl-64">
|
||||
@include('partials.topbar', ['heading' => $heading ?? $title])
|
||||
<main class="flex-1 p-4 pb-24 sm:p-6 lg:pb-6">
|
||||
@include('partials.flash')
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@auth
|
||||
@php
|
||||
$navUser = auth()->user();
|
||||
$navInitials = collect(explode(' ', trim((string) $navUser?->name)))
|
||||
->filter()->take(2)
|
||||
->map(fn ($part) => strtoupper(substr($part, 0, 1)))
|
||||
->implode('');
|
||||
$navAvatarUrl = $navUser && method_exists($navUser, 'avatarUrl')
|
||||
? $navUser->avatarUrl()
|
||||
: ($navUser?->avatar_url ?? null);
|
||||
@endphp
|
||||
@include('partials.mobile-bottom-nav', [
|
||||
'homeUrl' => route('care.dashboard'),
|
||||
'homeActive' => request()->routeIs('care.dashboard'),
|
||||
'searchUrl' => route('care.patients.index'),
|
||||
'searchActive' => request()->routeIs('care.patients.*'),
|
||||
'notificationsUrl' => route('care.dashboard'),
|
||||
'notificationsActive' => false,
|
||||
'unreadUrl' => route('care.dashboard'),
|
||||
'profileActive' => false,
|
||||
'profileName' => $navUser?->name ?? '',
|
||||
'profileSubtitle' => $navUser?->email ?? '',
|
||||
'profileMenuItems' => \App\Support\UserProfileMenu::items($navUser),
|
||||
'avatarUrl' => $navAvatarUrl,
|
||||
'initials' => $navInitials !== '' ? $navInitials : 'U',
|
||||
])
|
||||
@endauth
|
||||
@include('partials.wallet-topup-modal', ['openOnLoad' => (bool) session('topup_required')])
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
@props(['color' => 'slate'])
|
||||
@php
|
||||
$map = [
|
||||
'slate' => 'bg-slate-100 text-slate-700',
|
||||
'indigo' => 'bg-indigo-100 text-indigo-700',
|
||||
'green' => 'bg-green-100 text-green-700',
|
||||
'red' => 'bg-red-100 text-red-700',
|
||||
'amber' => 'bg-amber-100 text-amber-700',
|
||||
'blue' => 'bg-blue-100 text-blue-700',
|
||||
];
|
||||
$classes = $map[$color] ?? $map['slate'];
|
||||
@endphp
|
||||
<span {{ $attributes->merge(['class' => "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium $classes"]) }}>
|
||||
{{ $slot }}
|
||||
</span>
|
||||
@@ -0,0 +1,33 @@
|
||||
@props([
|
||||
'href' => null,
|
||||
'type' => 'button',
|
||||
'label' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$tag = $href ? 'a' : 'button';
|
||||
$ariaLabel = $label ?? trim(preg_replace('/\s+/', ' ', strip_tags((string) $slot)));
|
||||
@endphp
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($href) href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
aria-label="{{ $ariaLabel }}"
|
||||
title="{{ $ariaLabel }}"
|
||||
{{ $attributes->class(['btn-fab h-10 w-10 lg:hidden']) }}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
|
||||
</svg>
|
||||
</{{ $tag }}>
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($href) href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
{{ $attributes->class(['btn-primary hidden lg:inline-flex']) }}
|
||||
>
|
||||
<svg class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
|
||||
</svg>
|
||||
{{ $slot }}
|
||||
</{{ $tag }}>
|
||||
@@ -0,0 +1,37 @@
|
||||
@props([
|
||||
'name',
|
||||
'label' => null,
|
||||
'type' => 'text',
|
||||
'value' => null,
|
||||
'options' => [],
|
||||
'placeholder' => '',
|
||||
'required' => false,
|
||||
'rows' => 4,
|
||||
])
|
||||
@php
|
||||
$label = $label ?? \Illuminate\Support\Str::headline($name);
|
||||
$current = old($name, $value);
|
||||
$base = 'mt-1 block w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500';
|
||||
@endphp
|
||||
<div {{ $attributes->only('class') }}>
|
||||
<label for="{{ $name }}" class="block text-sm font-medium text-slate-700">
|
||||
{{ $label }} @if($required)<span class="text-red-500">*</span>@endif
|
||||
</label>
|
||||
|
||||
@if ($type === 'textarea')
|
||||
<textarea id="{{ $name }}" name="{{ $name }}" rows="{{ $rows }}" placeholder="{{ $placeholder }}"
|
||||
class="{{ $base }}">{{ $current }}</textarea>
|
||||
@elseif ($type === 'select')
|
||||
<select id="{{ $name }}" name="{{ $name }}" class="{{ $base }}">
|
||||
@foreach ($options as $optValue => $optLabel)
|
||||
<option value="{{ $optValue }}" @selected((string) $current === (string) $optValue)>{{ $optLabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input type="{{ $type }}" id="{{ $name }}" name="{{ $name }}" value="{{ $current }}"
|
||||
placeholder="{{ $placeholder }}" @if($type==='number') step="0.01" min="0" @endif
|
||||
class="{{ $base }}">
|
||||
@endif
|
||||
|
||||
@error($name)<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
@props([
|
||||
'name',
|
||||
'show' => false,
|
||||
'maxWidth' => '2xl'
|
||||
])
|
||||
|
||||
@php
|
||||
$maxWidth = [
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
][$maxWidth];
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
show: @js($show),
|
||||
focusables() {
|
||||
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||
return [...$el.querySelectorAll(selector)]
|
||||
.filter(el => ! el.hasAttribute('disabled'))
|
||||
},
|
||||
firstFocusable() { return this.focusables()[0] },
|
||||
lastFocusable() { return this.focusables().slice(-1)[0] },
|
||||
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
||||
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||
}"
|
||||
x-init="$watch('show', value => {
|
||||
if (value) {
|
||||
document.body.classList.add('overflow-y-hidden');
|
||||
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||
} else {
|
||||
setTimeout(() => { if (!show) document.body.classList.remove('overflow-y-hidden'); }, 200);
|
||||
}
|
||||
})"
|
||||
x-on:open-modal.window="String($event.detail) === @js($name) ? show = true : null"
|
||||
data-modal-name="{{ $name }}"
|
||||
x-on:close-modal.window="String($event.detail) === @js($name) ? show = false : null"
|
||||
x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false"
|
||||
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||
x-show="show"
|
||||
x-cloak
|
||||
x-transition:enter="transition-opacity ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center sm:justify-center"
|
||||
style="background: rgba(0,0,0,0.45); backdrop-filter: blur(3px);"
|
||||
>
|
||||
{{-- Backdrop click to close --}}
|
||||
<div class="absolute inset-0" x-on:click="show = false" data-close-modal="{{ $name }}"></div>
|
||||
|
||||
{{-- Panel: bottom-sheet on mobile, centered card on sm+ --}}
|
||||
<div
|
||||
x-show="show"
|
||||
class="relative z-10 w-full max-h-[90vh] overflow-y-auto rounded-t-3xl bg-white shadow-2xl sm:mb-6 sm:max-h-[85vh] sm:rounded-2xl {{ $maxWidth }} sm:border sm:border-gray-200"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
|
||||
x-transition:enter-end="translate-y-0 sm:opacity-100 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="translate-y-0 sm:opacity-100 sm:scale-100"
|
||||
x-transition:leave-end="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
|
||||
x-on:click.stop
|
||||
>
|
||||
{{-- Drag handle (mobile only) --}}
|
||||
<div class="flex justify-center pb-1 pt-3 sm:hidden">
|
||||
<div class="h-1 w-10 rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
|
||||
{{-- Close button (desktop only) --}}
|
||||
<button @click="show = false" data-close-modal="{{ $name }}" type="button"
|
||||
class="absolute right-3 top-3 z-10 hidden h-8 w-8 items-center justify-center rounded-full text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 sm:flex">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body style="margin:0;background:#f1f5f9;font-family:Arial,Helvetica,sans-serif;color:#0f172a;">
|
||||
<div style="max-width:560px;margin:0 auto;padding:32px 16px;">
|
||||
<div style="background:#ffffff;border-radius:16px;padding:28px;border:1px solid #e2e8f0;">
|
||||
<div style="white-space:pre-wrap;font-size:14px;line-height:1.6;color:#334155;">{{ $bodyText }}</div>
|
||||
@if (! empty($fromName))
|
||||
<p style="margin-top:24px;font-size:13px;color:#64748b;">— {{ $fromName }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<p style="margin-top:16px;text-align:center;font-size:11px;color:#94a3b8;">Sent via Ladill Frontdesk</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<x-app-layout title="Notifications">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-lg font-semibold text-slate-900">Notifications</h1>
|
||||
|
||||
<div class="mt-5 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
@forelse ($notifications as $notification)
|
||||
<a href="{{ $notification->data['url'] ?? '#' }}" class="flex items-start gap-3 border-b border-slate-50 px-5 py-4 last:border-0 hover:bg-slate-50 {{ $notification->read_at ? 'opacity-70' : '' }}">
|
||||
<span class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-slate-100 text-slate-500">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-slate-900">{{ $notification->data['title'] ?? 'Notification' }}</p>
|
||||
<p class="text-sm text-slate-600">{{ $notification->data['message'] ?? '' }}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">{{ $notification->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<p class="px-5 py-12 text-center text-sm text-slate-400">No notifications yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $notifications->links() }}</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,15 @@
|
||||
@props(['compact' => false])
|
||||
|
||||
<button type="button"
|
||||
@click="window.dispatchEvent(new CustomEvent('afia-open'))"
|
||||
@class([
|
||||
'inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-700 via-blue-600 to-emerald-400 text-sm font-semibold text-white shadow-sm transition hover:opacity-95',
|
||||
'h-9 w-9 px-0 lg:h-auto lg:w-auto lg:px-4 lg:py-2' => $compact,
|
||||
'px-3 py-2 lg:px-4' => ! $compact,
|
||||
])
|
||||
aria-label="Open Afia AI assistant">
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2.25c.414 0 .75.336.75.75a5.25 5.25 0 0 0 5.25 5.25.75.75 0 0 1 0 1.5A5.25 5.25 0 0 0 12.75 15a.75.75 0 0 1-1.5 0A5.25 5.25 0 0 0 6 9.75a.75.75 0 0 1 0-1.5A5.25 5.25 0 0 0 11.25 3a.75.75 0 0 1 .75-.75Zm6.75 11.25a.75.75 0 0 1 .75.75A2.25 2.25 0 0 0 21.75 16.5a.75.75 0 0 1 0 1.5A2.25 2.25 0 0 0 19.5 20.25a.75.75 0 0 1-1.5 0A2.25 2.25 0 0 0 15.75 18a.75.75 0 0 1 0-1.5A2.25 2.25 0 0 0 18 14.25a.75.75 0 0 1 .75-.75ZM5.25 14.25a.75.75 0 0 1 .75.75 3 3 0 0 0 3 3 .75.75 0 0 1 0 1.5 3 3 0 0 0-3 3 .75.75 0 0 1-1.5 0 3 3 0 0 0-3-3 .75.75 0 0 1 0-1.5 3 3 0 0 0 3-3 .75.75 0 0 1 .75-.75Z"/>
|
||||
</svg>
|
||||
<span @class(['sr-only lg:not-sr-only lg:inline' => $compact, 'hidden sm:inline' => ! $compact])>AI</span>
|
||||
</button>
|
||||
@@ -0,0 +1,106 @@
|
||||
@php
|
||||
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about visitor check-in, kiosks, hosts, badges, devices, or setting up your reception desk…";
|
||||
$afiaSuggestions = [
|
||||
'How do I set up a visitor kiosk?',
|
||||
'How do I check a visitor in?',
|
||||
'Where do I add reception desks?',
|
||||
'How do hosts approve visits?',
|
||||
];
|
||||
@endphp
|
||||
{{-- Afia — Ladill AI assistant slide-over. Opened via $dispatch('afia-open'). --}}
|
||||
<div x-data="afia({
|
||||
chatUrl: '{{ route('frontdesk.ai.chat') }}',
|
||||
csrf: '{{ csrf_token() }}',
|
||||
greeting: @js($afiaGreeting),
|
||||
suggestions: @js($afiaSuggestions),
|
||||
})" @afia-open.window="open = true; $nextTick(() => $refs.input && $refs.input.focus())">
|
||||
<div x-show="open" x-cloak @click="close()" class="fixed inset-0 z-50 bg-slate-900/20"></div>
|
||||
|
||||
<section x-show="open" x-cloak
|
||||
x-transition:enter="transition transform ease-out duration-200"
|
||||
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transition transform ease-in duration-150"
|
||||
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
||||
class="fixed inset-y-0 right-0 z-50 flex w-full max-w-md flex-col bg-white shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3.5">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="relative flex h-8 w-8 shrink-0 items-center justify-center">
|
||||
<svg viewBox="0 0 36 36" class="h-8 w-8" aria-hidden="true">
|
||||
<defs>
|
||||
<radialGradient id="afia-orb" cx="40%" cy="35%" r="60%">
|
||||
<stop offset="0%" stop-color="#a5b4fc"/>
|
||||
<stop offset="50%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#3730a3"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="18" cy="18" r="14" fill="url(#afia-orb)">
|
||||
<animate attributeName="r" values="14;14.8;14" dur="3s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="18" cy="18" r="11" fill="none" stroke="#c7d2fe" stroke-width=".6" opacity=".5">
|
||||
<animate attributeName="r" values="11;12.5;11" dur="4s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values=".5;.15;.5" dur="4s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle r="1.2" fill="#c7d2fe" opacity=".8">
|
||||
<animateMotion dur="3s" repeatCount="indefinite" path="M18,8 A10,10 0 1,1 17.99,8" rotate="auto"/>
|
||||
</circle>
|
||||
</svg>
|
||||
<span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2 border-white bg-emerald-400"></span>
|
||||
</span>
|
||||
<div class="leading-tight">
|
||||
<p class="text-sm font-semibold text-slate-900">Afia</p>
|
||||
<p class="text-[11px] text-slate-400">Frontdesk assistant</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<a href="{{ ladill_account_url('ai') }}"
|
||||
class="rounded-lg px-2 py-1 text-[11px] font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-700"
|
||||
title="AI usage history on your Ladill account">History</a>
|
||||
<button @click="close()" class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-600">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-4 py-4" x-ref="scroll">
|
||||
<div class="space-y-3">
|
||||
<template x-for="(m, i) in messages" :key="i">
|
||||
<div class="flex" :class="m.role === 'user' ? 'justify-end' : 'justify-start'">
|
||||
<div class="max-w-[85%] whitespace-pre-line rounded-2xl px-3.5 py-2.5 text-[13px] leading-relaxed"
|
||||
:class="m.role === 'user' ? 'bg-indigo-600 text-white rounded-br-md' : 'bg-slate-100 text-slate-700 rounded-bl-md'"
|
||||
x-html="m.text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\*\*([^*]+?)\*\*/g,'<strong>$1</strong>')"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="loading" class="flex justify-start">
|
||||
<div class="rounded-2xl rounded-bl-md bg-slate-100 px-4 py-3">
|
||||
<div class="flex gap-1">
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay:0ms"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay:150ms"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay:300ms"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="messages.length <= 1 && suggestions.length" class="mt-4 space-y-2">
|
||||
<template x-for="s in suggestions" :key="s">
|
||||
<button @click="useSuggestion(s)" :disabled="loading"
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-left text-[13px] font-medium text-slate-700 transition hover:border-indigo-200 hover:bg-indigo-50 disabled:opacity-50">
|
||||
<span class="text-indigo-400">→</span><span x-text="s"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 px-4 pb-4 pt-3">
|
||||
<form @submit.prevent="send()" class="flex items-end gap-2">
|
||||
<input type="text" x-ref="input" x-model="input" :disabled="loading" placeholder="Message Afia…"
|
||||
class="flex-1 rounded-xl border border-slate-200 bg-slate-50 px-4 py-2.5 text-sm text-slate-700 placeholder-slate-400 focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
<button type="submit" :disabled="loading || !input.trim()"
|
||||
class="btn-fab shrink-0 disabled:opacity-40">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
<p class="mt-2 text-center text-[10px] text-slate-400">Afia can make mistakes — verify important details.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
@php
|
||||
// Branded boot splash (Ladill Mail style). Self-icon from this app's subdomain.
|
||||
$sub = strtolower((string) ((explode('.', (string) (parse_url((string) config('app.url'), PHP_URL_HOST) ?: '')))[0] ?? ''));
|
||||
$icon = $sub !== '' && is_file(public_path("images/launcher-icons/{$sub}.svg")) ? "images/launcher-icons/{$sub}.svg" : null;
|
||||
$label = (string) config('app.name', 'Ladill');
|
||||
@endphp
|
||||
<div id="ladill-boot" role="status" aria-live="polite">
|
||||
<div class="lb-wrap">
|
||||
@if ($icon)
|
||||
<img class="lb-logo" src="{{ asset($icon) }}?v={{ @filemtime(public_path($icon)) ?: '1' }}" alt="">
|
||||
@endif
|
||||
<div class="lb-bar"><span></span></div>
|
||||
<div class="lb-title">Loading {{ $label }}…</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
#ladill-boot{position:fixed;inset:0;z-index:99999;background:#f8fafc;display:flex;align-items:center;justify-content:center;transition:opacity .35s ease;font-family:'Figtree',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
||||
#ladill-boot .lb-wrap{display:flex;flex-direction:column;align-items:center;gap:24px;width:260px}
|
||||
#ladill-boot .lb-logo{width:64px;height:64px;animation:lb-pulse 1.6s ease-in-out infinite}
|
||||
#ladill-boot .lb-bar{width:100%;height:4px;background:#e2e8f0;border-radius:999px;overflow:hidden}
|
||||
#ladill-boot .lb-bar>span{display:block;height:100%;width:8%;border-radius:999px;background:linear-gradient(90deg,#4f46e5,#7c3aed);animation:lb-fill 1.9s cubic-bezier(.4,0,.2,1) forwards}
|
||||
#ladill-boot .lb-title{font-size:14px;color:#64748b;font-weight:500}
|
||||
@keyframes lb-fill{0%{width:8%}55%{width:68%}100%{width:93%}}
|
||||
@keyframes lb-pulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(.93);opacity:.82}}
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
var el=document.getElementById('ladill-boot');
|
||||
if(!el)return;
|
||||
// Show once per browser session; suppress instantly on later navigations (no flash).
|
||||
try{
|
||||
if(sessionStorage.getItem('lb_booted')){el.parentNode.removeChild(el);return;}
|
||||
sessionStorage.setItem('lb_booted','1');
|
||||
}catch(e){}
|
||||
var start=Date.now(),MIN=550;
|
||||
function done(){
|
||||
var wait=Math.max(0,MIN-(Date.now()-start));
|
||||
setTimeout(function(){if(!el)return;el.style.opacity='0';setTimeout(function(){if(el&&el.parentNode)el.parentNode.removeChild(el);},400);},wait);
|
||||
}
|
||||
if(document.readyState==='complete')done();else window.addEventListener('load',done);
|
||||
setTimeout(done,8000); // safety net
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
@php
|
||||
$svgVer = @filemtime(public_path('favicon.svg')) ?: '1';
|
||||
$icoVer = @filemtime(public_path('favicon.ico')) ?: '1';
|
||||
@endphp
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}?v={{ $svgVer }}">
|
||||
<link rel="alternate icon" href="{{ asset('favicon.ico') }}?v={{ $icoVer }}" sizes="any">
|
||||
<link rel="shortcut icon" href="{{ asset('favicon.svg') }}?v={{ $svgVer }}">
|
||||
<link rel="apple-touch-icon" href="{{ asset('favicon.svg') }}?v={{ $svgVer }}">
|
||||
@@ -0,0 +1,34 @@
|
||||
@if (session('success'))
|
||||
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)" x-transition class="mb-4">
|
||||
<div class="rounded-lg bg-green-50 border border-green-200 p-4">
|
||||
<p class="text-sm text-green-800">{{ session('success') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 6000)" x-transition class="mb-4">
|
||||
<div class="rounded-lg bg-red-50 border border-red-200 p-4">
|
||||
<p class="text-sm text-red-800">{{ session('error') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('warning'))
|
||||
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 5000)" x-transition class="mb-4">
|
||||
<div class="rounded-lg bg-yellow-50 border border-yellow-200 p-4">
|
||||
<p class="text-sm text-yellow-800">{{ session('warning') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="mb-4 rounded-lg bg-red-50 border border-red-200 p-4">
|
||||
<p class="text-sm font-medium text-red-800">Please fix the following:</p>
|
||||
<ul class="mt-1 list-disc pl-5 text-sm text-red-700">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,53 @@
|
||||
@php
|
||||
// Shared Ladill app launcher — IDENTICAL across every app/service.
|
||||
// Driven by config/ladill_launcher.php (fully-extracted apps only) + icons
|
||||
// from public/images/launcher-icons/. Omits the current app (matched by
|
||||
// APP_URL host). To replicate elsewhere, copy this file + the config +
|
||||
// the launcher-icons folder verbatim.
|
||||
$sidebar = (bool) ($sidebar ?? false);
|
||||
$selfHost = parse_url((string) config('app.url'), PHP_URL_HOST);
|
||||
$launcherApps = array_values(array_filter(
|
||||
config('ladill_launcher.apps', []),
|
||||
fn (array $app) => parse_url($app['url'], PHP_URL_HOST) !== $selfHost
|
||||
));
|
||||
@endphp
|
||||
@if (! empty($launcherApps))
|
||||
<div class="relative" x-data="{ appsOpen: false }" @click.outside="appsOpen = false" @keydown.escape.window="appsOpen = false">
|
||||
<button type="button" @click="appsOpen = !appsOpen"
|
||||
@class([
|
||||
'inline-flex items-center justify-center rounded-xl border border-slate-200 text-slate-600 transition hover:bg-slate-50',
|
||||
'p-2' => ! $sidebar,
|
||||
'w-full gap-3 px-3 py-2 text-[13px] hover:text-slate-900' => $sidebar,
|
||||
])
|
||||
:class="appsOpen ? 'bg-slate-50 text-slate-900' : ''" aria-label="All apps">
|
||||
<svg @class(['shrink-0', 'h-5 w-5' => ! $sidebar, 'h-[18px] w-[18px]' => $sidebar]) viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M4.5 0.5H1.5C0.947715 0.5 0.5 0.947715 0.5 1.5V4.5C0.5 5.05228 0.947715 5.5 1.5 5.5H4.5C5.05228 5.5 5.5 5.05228 5.5 4.5V1.5C5.5 0.947715 5.05228 0.5 4.5 0.5Z"/>
|
||||
<path d="M12.5 0.5H9.5C8.94772 0.5 8.5 0.947715 8.5 1.5V4.5C8.5 5.05228 8.94772 5.5 9.5 5.5H12.5C13.0523 5.5 13.5 5.05228 13.5 4.5V1.5C13.5 0.947715 13.0523 0.5 12.5 0.5Z"/>
|
||||
<path d="M4.5 8.5H1.5C0.947715 8.5 0.5 8.94772 0.5 9.5V12.5C0.5 13.0523 0.947715 13.5 1.5 13.5H4.5C5.05228 13.5 5.5 13.0523 5.5 12.5V9.5C5.5 8.94772 5.05228 8.5 4.5 8.5Z"/>
|
||||
<path d="M12.5 8.5H9.5C8.94772 8.5 8.5 8.94772 8.5 9.5V12.5C8.5 13.0523 8.94772 13.5 9.5 13.5H12.5C13.0523 13.5 13.5 13.0523 13.5 12.5V9.5C13.5 8.94772 13.0523 8.5 12.5 8.5Z"/>
|
||||
</svg>
|
||||
@if ($sidebar)
|
||||
<span class="font-medium">All Ladill apps</span>
|
||||
@endif
|
||||
</button>
|
||||
<div x-show="appsOpen" x-cloak x-transition.origin.top.right
|
||||
@class([
|
||||
'absolute z-50 w-72 rounded-2xl border border-slate-200 bg-white p-3 shadow-xl',
|
||||
'right-0 mt-2' => ! $sidebar,
|
||||
'bottom-full left-0 mb-2' => $sidebar,
|
||||
])>
|
||||
<p class="px-1 pb-2 text-[10px] font-bold uppercase tracking-widest text-slate-400">All apps</p>
|
||||
<div class="grid grid-cols-3 gap-1">
|
||||
@foreach ($launcherApps as $app)
|
||||
<a href="{{ $app['url'] }}"
|
||||
class="flex flex-col items-center gap-1.5 rounded-xl px-2 py-3 text-center transition hover:bg-indigo-50">
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-100">
|
||||
<img src="{{ asset('images/launcher-icons/'.$app['icon']) }}?v={{ @filemtime(public_path('images/launcher-icons/'.$app['icon'])) ?: '1' }}" alt="" class="h-5 w-5 object-contain" width="20" height="20">
|
||||
</span>
|
||||
<span class="text-[11px] font-medium leading-tight text-slate-700">{{ $app['name'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,130 @@
|
||||
@php
|
||||
$showSearch = isset($searchUrl) && $searchUrl !== null && $searchUrl !== '#';
|
||||
$gridCols = match (true) {
|
||||
! empty($centerCompose) => $showSearch ? 'grid-cols-5' : 'grid-cols-4',
|
||||
default => $showSearch ? 'grid-cols-4' : 'grid-cols-3',
|
||||
};
|
||||
$avatarUrl = $avatarUrl ?? null;
|
||||
$initials = $initials ?? 'U';
|
||||
$notificationsUrl = $notificationsUrl ?? '#';
|
||||
$unreadUrl = $unreadUrl ?? null;
|
||||
$profileUrl = $profileUrl ?? '#';
|
||||
$profileName = trim((string) ($profileName ?? ''));
|
||||
$profileSubtitle = trim((string) ($profileSubtitle ?? ''));
|
||||
$profileMenuItems = $profileMenuItems ?? [];
|
||||
if ($profileMenuItems === [] && $profileUrl !== '#') {
|
||||
$profileMenuItems = [['type' => 'link', 'label' => 'Profile', 'href' => $profileUrl]];
|
||||
}
|
||||
$navActive = fn (bool $active) => $active ? 'text-indigo-600' : 'text-slate-600';
|
||||
@endphp
|
||||
<div x-data="{ profileOpen: false }" class="lg:hidden">
|
||||
<nav class="fixed inset-x-0 bottom-0 z-40 border-t border-slate-200 bg-white"
|
||||
style="padding-bottom: env(safe-area-inset-bottom, 0px)">
|
||||
<div class="grid h-16 {{ $gridCols }}">
|
||||
<a href="{{ $homeUrl }}"
|
||||
class="flex flex-col items-center justify-center gap-1 px-2 py-2 transition hover:text-slate-900 {{ $navActive($homeActive ?? false) }}">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||
<span class="text-[10px] font-medium">Home</span>
|
||||
</a>
|
||||
|
||||
@if ($showSearch)
|
||||
<a href="{{ $searchUrl }}"
|
||||
class="flex flex-col items-center justify-center gap-1 px-2 py-2 transition hover:text-slate-900 {{ $navActive($searchActive ?? false) }}">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
|
||||
<span class="text-[10px] font-medium">Search</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (!empty($centerCompose))
|
||||
<div class="flex items-center justify-center">
|
||||
<button type="button"
|
||||
@click="$dispatch('compose-open')"
|
||||
class="btn-fab -translate-y-1 ring-4 ring-white shadow-md"
|
||||
aria-label="Compose">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<a href="{{ $notificationsUrl }}"
|
||||
@if ($unreadUrl)
|
||||
x-data="{
|
||||
unread: 0,
|
||||
init() {
|
||||
fetch({{ \Illuminate\Support\Js::from($unreadUrl) }}, {
|
||||
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
}).then(r => r.json()).then(d => this.unread = d.unread_count || 0).catch(() => {});
|
||||
},
|
||||
}"
|
||||
@endif
|
||||
class="relative flex flex-col items-center justify-center gap-1 px-2 py-2 transition hover:text-slate-900 {{ $navActive($notificationsActive ?? false) }}">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
@if ($unreadUrl)
|
||||
<span x-show="unread > 0" x-cloak x-text="unread > 9 ? '9+' : unread"
|
||||
class="absolute right-3 top-1.5 inline-flex min-w-4 items-center justify-center rounded-full bg-rose-500 px-1 py-0.5 text-[9px] font-semibold text-white"></span>
|
||||
@endif
|
||||
<span class="text-[10px] font-medium">Notifications</span>
|
||||
</a>
|
||||
|
||||
<button type="button"
|
||||
@click="profileOpen = true"
|
||||
class="flex flex-col items-center justify-center gap-1 px-2 py-2 transition hover:text-slate-900 {{ $navActive($profileActive ?? false) }}"
|
||||
:class="profileOpen ? 'text-indigo-600' : ''"
|
||||
aria-label="Open profile menu">
|
||||
@if ($avatarUrl)
|
||||
<img src="{{ $avatarUrl }}" alt="" class="h-7 w-7 rounded-full object-cover ring-1 ring-slate-200">
|
||||
@else
|
||||
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full bg-slate-900 text-[10px] font-semibold text-white">{{ $initials }}</span>
|
||||
@endif
|
||||
<span class="text-[10px] font-medium">Profile</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{-- Profile menu bottom sheet (matches desktop avatar dropdown) --}}
|
||||
<div x-show="profileOpen" x-cloak class="fixed inset-0 z-50" @keydown.escape.window="profileOpen = false">
|
||||
<div x-show="profileOpen" x-transition.opacity.duration.200ms
|
||||
@click="profileOpen = false"
|
||||
class="absolute inset-0 bg-slate-900/40 backdrop-blur-[1px]"></div>
|
||||
|
||||
<div x-show="profileOpen"
|
||||
x-effect="profileOpen && window.dispatchEvent(new CustomEvent('wallet-balance-refresh'))"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="translate-y-full"
|
||||
x-transition:enter-end="translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="translate-y-0"
|
||||
x-transition:leave-end="translate-y-full"
|
||||
class="absolute inset-x-0 bottom-0 rounded-t-2xl border-t border-slate-200 bg-white shadow-2xl"
|
||||
style="padding-bottom: env(safe-area-inset-bottom, 0px)"
|
||||
@click.outside="profileOpen = false">
|
||||
<div class="flex justify-center pt-3">
|
||||
<span class="h-1 w-10 rounded-full bg-slate-200" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
@if ($profileName !== '' || $profileSubtitle !== '')
|
||||
<div class="flex items-center gap-3 border-b border-slate-100 px-5 py-4">
|
||||
@if ($avatarUrl)
|
||||
<img src="{{ $avatarUrl }}" alt="" class="h-12 w-12 rounded-full object-cover ring-1 ring-slate-200">
|
||||
@else
|
||||
<span class="inline-flex h-12 w-12 items-center justify-center rounded-full bg-slate-900 text-sm font-semibold text-white">{{ $initials }}</span>
|
||||
@endif
|
||||
<div class="min-w-0">
|
||||
@if ($profileName !== '')
|
||||
<p class="truncate text-sm font-semibold text-slate-900">{{ $profileName }}</p>
|
||||
@endif
|
||||
@if ($profileSubtitle !== '')
|
||||
<p class="truncate text-xs text-slate-500">{{ $profileSubtitle }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@include('partials.user-profile-menu', [
|
||||
'items' => $profileMenuItems,
|
||||
'variant' => 'sheet',
|
||||
'onNavigate' => 'profileOpen = false',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
@php
|
||||
$variant = $variant ?? 'primary';
|
||||
$type = $type ?? 'submit';
|
||||
$tag = ! empty($href) ? 'a' : 'button';
|
||||
$ariaLabel = $ariaLabel ?? $label ?? '';
|
||||
$desktopLabel = $desktopLabel ?? $label ?? '';
|
||||
$showDesktopIcon = $showDesktopIcon ?? ($variant === 'primary');
|
||||
|
||||
$mobileClass = match ($variant) {
|
||||
'primary' => 'btn-fab h-10 w-10 lg:hidden',
|
||||
'outline' => 'inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-700 shadow-sm transition hover:border-slate-300 hover:bg-slate-50 lg:hidden',
|
||||
'dark' => 'inline-flex h-10 w-10 items-center justify-center rounded-full bg-gray-900 text-white shadow-sm transition hover:bg-gray-800 lg:hidden',
|
||||
'indigo' => 'inline-flex h-10 w-10 items-center justify-center rounded-full bg-indigo-600 text-white shadow-sm transition hover:bg-indigo-700 lg:hidden',
|
||||
default => 'btn-fab h-10 w-10 lg:hidden',
|
||||
};
|
||||
|
||||
$desktopClass = match ($variant) {
|
||||
'primary' => 'btn-primary hidden items-center lg:inline-flex',
|
||||
'outline' => 'hidden items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 lg:inline-flex',
|
||||
'dark' => 'hidden items-center gap-1.5 rounded-lg bg-gray-900 px-3.5 py-2 text-sm font-medium text-white transition hover:bg-gray-800 lg:inline-flex',
|
||||
'indigo' => 'hidden items-center justify-center rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700 lg:inline-flex',
|
||||
default => 'btn-primary hidden items-center lg:inline-flex',
|
||||
};
|
||||
|
||||
$extraClass = $class ?? '';
|
||||
@endphp
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($tag === 'a') href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
aria-label="{{ $ariaLabel }}"
|
||||
title="{{ $ariaLabel }}"
|
||||
@if (! empty($attributes)) {!! $attributes !!} @endif
|
||||
class="{{ trim($mobileClass.' '.$extraClass) }}"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
|
||||
</svg>
|
||||
</{{ $tag }}>
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($tag === 'a') href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
@if (! empty($attributes)) {!! $attributes !!} @endif
|
||||
class="{{ trim($desktopClass.' '.$extraClass) }}"
|
||||
>
|
||||
@if ($showDesktopIcon)
|
||||
<svg class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
|
||||
</svg>
|
||||
@endif
|
||||
{{ $desktopLabel }}
|
||||
</{{ $tag }}>
|
||||
@@ -0,0 +1,27 @@
|
||||
@php
|
||||
$icon = $icon ?? 'arrow';
|
||||
$desktopClass = $desktopClass ?? 'text-xs font-medium text-indigo-600 hover:text-indigo-700';
|
||||
$label = $label ?? '';
|
||||
@endphp
|
||||
|
||||
<a href="{{ $href }}"
|
||||
aria-label="{{ $label }}"
|
||||
title="{{ $label }}"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-indigo-50 text-indigo-600 ring-1 ring-indigo-100 transition hover:bg-indigo-100 lg:h-auto lg:w-auto lg:rounded-none lg:bg-transparent lg:ring-0 lg:hover:bg-transparent">
|
||||
<span class="lg:hidden">
|
||||
@if ($icon === 'shield')
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"/>
|
||||
</svg>
|
||||
@elseif ($icon === 'calendar')
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"/>
|
||||
</svg>
|
||||
@else
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/>
|
||||
</svg>
|
||||
@endif
|
||||
</span>
|
||||
<span class="hidden lg:inline {{ $desktopClass }}">{{ $label }}</span>
|
||||
</a>
|
||||
@@ -0,0 +1,6 @@
|
||||
@php
|
||||
$title = $mobileTopbarTitle ?? 'Ladill';
|
||||
@endphp
|
||||
<div class="min-w-0 flex-1 lg:hidden">
|
||||
<h1 class="truncate text-base font-semibold text-slate-900">{{ $title }}</h1>
|
||||
</div>
|
||||
@@ -0,0 +1,97 @@
|
||||
{{-- In-app notification bell + dropdown (scoped to this app). --}}
|
||||
<div class="relative hidden lg:block"
|
||||
x-data="notificationDropdown({
|
||||
unreadUrl: {{ \Illuminate\Support\Js::from(route('notifications.unread')) }},
|
||||
markReadUrl: {{ \Illuminate\Support\Js::from(route('notifications.mark-read', ['id' => '__ID__'])) }},
|
||||
markAllReadUrl: {{ \Illuminate\Support\Js::from(route('notifications.mark-all-read')) }},
|
||||
indexUrl: {{ \Illuminate\Support\Js::from(route('notifications.index')) }},
|
||||
csrfToken: {{ \Illuminate\Support\Js::from(csrf_token()) }},
|
||||
})"
|
||||
@click.outside="open = false">
|
||||
<button type="button"
|
||||
@click="toggle()"
|
||||
class="relative inline-flex items-center justify-center rounded-full border border-slate-200 p-2.5 text-slate-600 transition hover:bg-slate-50"
|
||||
aria-label="Notifications">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.7" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/>
|
||||
</svg>
|
||||
<span x-show="unreadCount > 0"
|
||||
x-cloak
|
||||
x-text="unreadCount > 9 ? '9+' : unreadCount"
|
||||
class="absolute -right-1 -top-1 inline-flex min-w-5 items-center justify-center rounded-full bg-rose-500 px-1.5 py-0.5 text-[10px] font-semibold text-white"></span>
|
||||
</button>
|
||||
|
||||
<div x-show="open"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 z-50 mt-2 w-80 origin-top-right rounded-xl border border-slate-200 bg-white shadow-lg">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Notifications</h3>
|
||||
<button type="button"
|
||||
x-show="unreadCount > 0"
|
||||
@click="markAllRead()"
|
||||
class="text-xs font-medium text-indigo-600 hover:text-indigo-700">
|
||||
Mark all read
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="max-h-80 overflow-y-auto">
|
||||
<template x-if="loading">
|
||||
<div class="px-4 py-6 text-center">
|
||||
<svg class="mx-auto h-5 w-5 animate-spin text-slate-400" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && notifications.length === 0">
|
||||
<div class="px-4 py-8 text-center">
|
||||
<svg class="mx-auto h-10 w-10 text-slate-300" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-slate-500">No notifications yet</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && notifications.length > 0">
|
||||
<div class="divide-y divide-slate-100">
|
||||
<template x-for="notification in notifications" :key="notification.id">
|
||||
<a :href="notification.url || '#'"
|
||||
@click="markRead(notification.id)"
|
||||
class="flex items-start gap-3 px-4 py-3 transition hover:bg-slate-50">
|
||||
<span class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
|
||||
:class="getIconBg(notification.icon)">
|
||||
<template x-if="notification.icon === 'success'">
|
||||
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>
|
||||
</template>
|
||||
<template x-if="notification.icon === 'task'">
|
||||
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>
|
||||
</template>
|
||||
<template x-if="!['success','task'].includes(notification.icon)">
|
||||
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</template>
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-slate-900 truncate" x-text="notification.title"></p>
|
||||
<p class="text-xs text-slate-500 line-clamp-2" x-text="notification.message"></p>
|
||||
<p class="mt-1 text-[11px] text-slate-400" x-text="notification.created_at"></p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 px-4 py-2.5">
|
||||
<a :href="indexUrl" class="block text-center text-xs font-medium text-slate-600 hover:text-slate-900">
|
||||
View all notifications
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,108 @@
|
||||
<div class="flex h-full flex-col bg-white border-r border-slate-200 text-slate-700">
|
||||
<div class="flex h-16 shrink-0 items-center border-b border-slate-200 px-6">
|
||||
<a href="{{ route('care.dashboard') }}" class="flex items-center">
|
||||
<img src="{{ asset('images/logo/ladillcare-logo.svg') }}?v={{ @filemtime(public_path('images/logo/ladillcare-logo.svg')) ?: '1' }}" alt="Ladill Care" class="h-6 w-auto">
|
||||
</a>
|
||||
</div>
|
||||
@php
|
||||
$member = auth()->user()
|
||||
? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user())
|
||||
: null;
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
|
||||
$nav = [
|
||||
['name' => 'Dashboard', 'route' => route('care.dashboard'), 'active' => request()->routeIs('care.dashboard'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12 11.2 3.05c.44-.44 1.15-.44 1.59 0L21.75 12M4.5 9.75v10.5a.75.75 0 0 0 .75.75H9.75v-6a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75v6h4.5a.75.75 0 0 0 .75-.75V9.75" />'],
|
||||
];
|
||||
|
||||
if ($permissions->can($member, 'patients.view')) {
|
||||
$nav[] = ['name' => 'Patients', 'route' => route('care.patients.index'), 'active' => request()->routeIs('care.patients.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />'];
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'appointments.view')) {
|
||||
$nav[] = ['name' => 'Appointments', 'route' => route('care.appointments.index'), 'active' => request()->routeIs('care.appointments.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />'];
|
||||
$nav[] = ['name' => 'Queue', 'route' => route('care.queue.index'), 'active' => request()->routeIs('care.queue.*') || request()->routeIs('care.consultations.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.25 4.5h13.5a.75.75 0 0 1 .75.75v15.75a.75.75 0 0 1-.75.75H5.25a.75.75 0 0 1-.75-.75V5.25a.75.75 0 0 1 .75-.75Z" />'];
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'lab.view')) {
|
||||
$nav[] = ['name' => 'Laboratory', 'route' => route('care.lab.queue.index'), 'active' => request()->routeIs('care.lab.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714a2.25 2.25 0 0 0 .659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 0 1-1.59.659H9.06a2.25 2.25 0 0 1-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 0 1-2.25 2.25H7.25A2.25 2.25 0 0 1 5 17v-2.5" />'];
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'prescriptions.view')) {
|
||||
$nav[] = ['name' => 'Pharmacy queue', 'route' => route('care.prescriptions.queue'), 'active' => request()->routeIs('care.prescriptions.*') && ! request()->routeIs('care.pharmacy.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714a2.25 2.25 0 0 0 .659 1.591L19 14.5" />'];
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'pharmacy.view')) {
|
||||
$nav[] = ['name' => 'Inventory', 'route' => route('care.pharmacy.drugs.index'), 'active' => request()->routeIs('care.pharmacy.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5a1.125 1.125 0 0 0-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z" />'];
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'bills.view')) {
|
||||
$nav[] = ['name' => 'Billing', 'route' => route('care.bills.index'), 'active' => request()->routeIs('care.bills.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414-.336.75-.75.75h-.75m0 0H3.375c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />'];
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'reports.finance.view')) {
|
||||
$nav[] = ['name' => 'Reports', 'route' => route('care.reports.index'), 'active' => request()->routeIs('care.reports.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />'];
|
||||
}
|
||||
|
||||
$adminNav = [];
|
||||
if ($permissions->can($member, 'admin.branches.view')) {
|
||||
$adminNav[] = ['name' => 'Branches', 'route' => route('care.branches.index'), 'active' => request()->routeIs('care.branches.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m-1.5 3h1.5m3-9H15m-1.5 3H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21" />'];
|
||||
}
|
||||
if ($permissions->can($member, 'admin.departments.view')) {
|
||||
$adminNav[] = ['name' => 'Departments', 'route' => route('care.departments.index'), 'active' => request()->routeIs('care.departments.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />'];
|
||||
}
|
||||
if ($permissions->can($member, 'admin.members.view')) {
|
||||
$adminNav[] = ['name' => 'Team', 'route' => route('care.members.index'), 'active' => request()->routeIs('care.members.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />'];
|
||||
}
|
||||
if ($permissions->can($member, 'settings.view')) {
|
||||
$adminNav[] = ['name' => 'Settings', 'route' => route('care.settings'), 'active' => request()->routeIs('care.settings*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />'];
|
||||
}
|
||||
if ($permissions->can($member, 'audit.view')) {
|
||||
$adminNav[] = ['name' => 'Audit log', 'route' => route('care.audit.index'), 'active' => request()->routeIs('care.audit.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />'];
|
||||
}
|
||||
@endphp
|
||||
|
||||
<nav class="flex-1 space-y-1 overflow-y-auto px-3 py-4">
|
||||
@foreach ($nav as $item)
|
||||
<a href="{{ $item['route'] }}"
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium {{ $item['active'] ? 'bg-sky-50 text-sky-700' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
||||
<svg class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">{!! $item['icon'] !!}</svg>
|
||||
{{ $item['name'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
@if (count($adminNav))
|
||||
<p class="px-3 pt-4 pb-1 text-xs font-semibold uppercase tracking-wide text-slate-400">Administration</p>
|
||||
@foreach ($adminNav as $item)
|
||||
<a href="{{ $item['route'] }}"
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium {{ $item['active'] ? 'bg-sky-50 text-sky-700' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
||||
<svg class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">{!! $item['icon'] !!}</svg>
|
||||
{{ $item['name'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
@endif
|
||||
</nav>
|
||||
|
||||
<div class="shrink-0 border-t border-slate-100 px-3 py-3">
|
||||
<a href="{{ route('care.settings') }}"
|
||||
class="group flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-slate-600 transition hover:bg-slate-50 hover:text-slate-900 {{ request()->routeIs('care.settings*') ? 'bg-sky-50 text-sky-700 font-semibold' : '' }}">
|
||||
<svg class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.281Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
{{-- Account switcher (desktop) — only when the user belongs to more than one account. --}}
|
||||
@if (isset($accessibleAccounts) && $accessibleAccounts->count() > 1)
|
||||
<div x-data="{ accountSwitcherOpen: false }" @click.outside="accountSwitcherOpen = false" class="relative hidden lg:block">
|
||||
<button type="button" @click="accountSwitcherOpen = !accountSwitcherOpen"
|
||||
class="inline-flex items-center gap-1.5 rounded-full border border-slate-200 px-3 py-2 text-sm text-slate-700 transition hover:bg-slate-50">
|
||||
<span class="max-w-[140px] truncate">{{ $actingAccount->name ?? $actingAccount->email }}</span>
|
||||
<svg class="h-4 w-4 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m19 9-7 7-7-7"/></svg>
|
||||
</button>
|
||||
<div x-show="accountSwitcherOpen" x-cloak x-transition
|
||||
class="absolute right-0 z-30 mt-2 w-60 rounded-xl border border-slate-200 bg-white p-1 shadow-lg">
|
||||
<p class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-400">Switch account</p>
|
||||
@foreach ($accessibleAccounts as $acctOption)
|
||||
<form method="POST" action="{{ route('account.switch') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="account" value="{{ $acctOption->id }}">
|
||||
<button type="submit" class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-50 {{ $acctOption->id === $actingAccount->id ? 'font-semibold text-indigo-700' : 'text-slate-700' }}">
|
||||
<span class="truncate">{{ $acctOption->id === auth()->id() ? 'My account' : ($acctOption->name ?? $acctOption->email) }}</span>
|
||||
@if ($acctOption->id === $actingAccount->id)<span class="text-indigo-600">✓</span>@endif
|
||||
</button>
|
||||
</form>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,41 @@
|
||||
{{--
|
||||
Top-right header widgets:
|
||||
Mobile — Afia + launcher only (profile/notifications live in bottom nav).
|
||||
Desktop — Afia → notifications → launcher → divider → avatar dropdown.
|
||||
--}}
|
||||
@php
|
||||
$topbarUser = $user ?? auth()->user();
|
||||
$initials = collect(explode(' ', trim((string) $topbarUser?->name)))
|
||||
->filter()->take(2)->map(fn ($p) => strtoupper(substr($p, 0, 1)))->implode('');
|
||||
$avatarUrl = $topbarUser && method_exists($topbarUser, 'avatarUrl')
|
||||
? $topbarUser->avatarUrl()
|
||||
: ($topbarUser?->avatar_url ?? null);
|
||||
$showUserHeader = $showUser ?? true;
|
||||
@endphp
|
||||
|
||||
@include('partials.launcher')
|
||||
|
||||
@includeIf('partials.topbar-widgets-mid')
|
||||
|
||||
<div class="hidden h-8 w-px bg-slate-200 lg:block"></div>
|
||||
|
||||
<div class="relative hidden lg:block" x-data="{ profileOpen: false }" @click.outside="profileOpen = false" @keydown.escape.window="profileOpen = false">
|
||||
<button type="button" @click="profileOpen = !profileOpen"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-2 py-1.5 text-slate-700 transition hover:bg-slate-50">
|
||||
@if ($avatarUrl)
|
||||
<img src="{{ $avatarUrl }}" alt="Avatar" class="h-8 w-8 rounded-full object-cover ring-1 ring-slate-200">
|
||||
@else
|
||||
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-slate-900 text-xs font-semibold text-white">{{ $initials !== '' ? $initials : 'U' }}</span>
|
||||
@endif
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m19 9-7 7-7-7"/></svg>
|
||||
</button>
|
||||
|
||||
<div x-show="profileOpen" x-cloak x-transition
|
||||
class="absolute right-0 z-50 mt-2 w-64 rounded-xl border border-slate-200 bg-white shadow-lg">
|
||||
@include('partials.user-profile-menu', [
|
||||
'items' => \App\Support\UserProfileMenu::items($topbarUser),
|
||||
'user' => $topbarUser,
|
||||
'showUser' => $showUserHeader,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
@php
|
||||
$user = auth()->user();
|
||||
$initials = collect(explode(' ', trim((string) $user?->name)))
|
||||
->filter()->take(2)->map(fn ($p) => strtoupper(substr($p, 0, 1)))->implode('');
|
||||
@endphp
|
||||
<header class="flex items-center justify-between h-16 border-b border-slate-200 bg-white px-4 sm:px-6">
|
||||
<div class="flex flex-1 items-center gap-3 min-w-0">
|
||||
<button @click="sidebarOpen = !sidebarOpen" class="lg:hidden shrink-0 text-slate-500 hover:text-slate-700" aria-label="Menu">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||
</button>
|
||||
|
||||
@include('partials.mobile-topbar-title')
|
||||
|
||||
<h1 class="hidden text-sm font-semibold text-slate-700 lg:block">{{ $heading ?? 'Ladill Care' }}</h1>
|
||||
|
||||
@if (auth()->check() && app(\App\Services\Care\CarePermissions::class)->can(
|
||||
app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user()), 'patients.view'))
|
||||
<form method="GET" action="{{ route('care.patients.index') }}"
|
||||
class="relative hidden w-full max-w-md lg:block"
|
||||
x-data
|
||||
@keydown.window="if ($event.key === '/' && !['INPUT','TEXTAREA','SELECT'].includes($event.target.tagName)) { $event.preventDefault(); $refs.search.focus(); }">
|
||||
<svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
|
||||
<input x-ref="search" type="search" name="q" value="{{ request('q') }}"
|
||||
placeholder="Search patients…"
|
||||
class="w-full rounded-xl border border-slate-200 bg-slate-50 py-2 pl-9 pr-10 text-sm text-slate-700 placeholder-slate-400 transition focus:border-sky-300 focus:bg-white focus:ring-2 focus:ring-sky-100 focus:outline-none">
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
@includeIf('partials.topbar-widgets-prepend')
|
||||
@include('partials.topbar-desktop-widgets', ['user' => $user, 'showUser' => true])
|
||||
@includeIf('partials.topbar-widgets-append')
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,73 @@
|
||||
@props([
|
||||
'items' => [],
|
||||
'user' => null,
|
||||
'showUser' => false,
|
||||
'variant' => 'dropdown',
|
||||
'onNavigate' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$linkClass = match ($variant) {
|
||||
'dark' => 'block rounded-md px-3 py-2 text-sm text-slate-200 hover:bg-slate-800',
|
||||
'sheet' => 'block rounded-xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-slate-100',
|
||||
default => 'block rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-slate-100',
|
||||
};
|
||||
|
||||
$logoutClass = match ($variant) {
|
||||
'dark' => 'w-full rounded-md px-3 py-2 text-left text-sm text-rose-300 hover:bg-rose-950/40',
|
||||
'sheet' => 'w-full rounded-xl px-4 py-3 text-left text-sm font-medium text-rose-600 transition hover:bg-rose-50',
|
||||
default => 'w-full rounded-lg px-3 py-2 text-left text-sm text-rose-600 hover:bg-rose-50',
|
||||
};
|
||||
|
||||
$logoutWrapperClass = match ($variant) {
|
||||
'sheet' => 'border-t border-slate-100 pt-2',
|
||||
'dark' => '',
|
||||
default => '',
|
||||
};
|
||||
|
||||
$dividerClass = match ($variant) {
|
||||
'dark' => 'my-1 border-t border-slate-800',
|
||||
default => 'my-1 border-t border-slate-100',
|
||||
};
|
||||
|
||||
$containerClass = match ($variant) {
|
||||
'dark' => 'mt-2 rounded-lg border border-slate-800 bg-slate-900 p-1',
|
||||
'sheet' => 'space-y-1 p-2 pb-4',
|
||||
default => 'p-1',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->merge(['class' => $containerClass]) }}>
|
||||
@if ($showUser && $user)
|
||||
<div class="px-3 py-2">
|
||||
<p class="truncate text-sm font-semibold text-slate-900">{{ $user->name ?? 'Your account' }}</p>
|
||||
<p class="truncate text-xs text-slate-400">{{ $user->email }}</p>
|
||||
</div>
|
||||
<div class="{{ $dividerClass }}"></div>
|
||||
@endif
|
||||
|
||||
@foreach ($items as $item)
|
||||
@if (($item['type'] ?? 'link') === 'link')
|
||||
<a href="{{ $item['href'] }}"
|
||||
class="{{ $linkClass }}"
|
||||
@if ($onNavigate) @click="{{ $onNavigate }}" @endif>
|
||||
{{ $item['label'] }}
|
||||
</a>
|
||||
@elseif (($item['type'] ?? '') === 'wallet')
|
||||
@includeIf('partials.wallet-widget', [
|
||||
'onNavigate' => $onNavigate,
|
||||
'class' => $variant === 'sheet' ? 'mx-2' : 'mx-1',
|
||||
])
|
||||
@elseif (($item['type'] ?? '') === 'logout')
|
||||
@if ($variant !== 'sheet')
|
||||
<div class="{{ $dividerClass }}"></div>
|
||||
@endif
|
||||
<form method="POST" action="{{ $item['action'] }}" class="{{ $logoutWrapperClass }}">
|
||||
@csrf
|
||||
<button type="submit" class="{{ $logoutClass }}">
|
||||
{{ $item['label'] }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
@props(['openOnLoad' => false, 'suggested' => 10, 'min' => 1, 'returnUrl' => null])
|
||||
|
||||
<x-modal name="wallet-topup" :show="$openOnLoad" maxWidth="md">
|
||||
<div class="border-b border-slate-100 px-5 py-4 pr-12 sm:px-6 sm:pr-14">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Top up your wallet</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Add funds to your Ladill wallet to continue.</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('care.wallet') }}" method="GET" class="p-5 sm:p-6">
|
||||
@csrf
|
||||
<input type="hidden" name="return_url" value="{{ $returnUrl ?? url()->current() }}">
|
||||
|
||||
<label for="topup_amount" class="text-sm font-medium text-slate-700">Amount (GHS)</label>
|
||||
<input type="number" id="topup_amount" name="amount" min="{{ $min }}" max="5000" step="0.01" value="{{ $suggested }}" required
|
||||
class="mt-1.5 block w-full rounded-xl border border-slate-200 px-3 py-2.5 text-sm focus:border-indigo-400 focus:ring-1 focus:ring-indigo-400">
|
||||
<p class="mt-1.5 text-xs text-slate-500">You'll be taken to Paystack to pay securely; funds land in your Ladill wallet.</p>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-2 sm:flex-row sm:justify-end">
|
||||
<button type="button" @click="$dispatch('close-modal', 'wallet-topup')"
|
||||
class="hidden rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 sm:inline-flex">Cancel</button>
|
||||
<button type="submit" class="btn-primary w-full sm:w-auto">Add funds</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
@@ -0,0 +1,30 @@
|
||||
{{-- Wallet balance peek (links to the account wallet on account.ladill.com). --}}
|
||||
@php
|
||||
$onNavigate = $onNavigate ?? null;
|
||||
$class = trim((string) ($class ?? 'mx-1'));
|
||||
$balanceRoute = (string) config('billing.wallet_balance_route', 'wallet.balance');
|
||||
$balanceUrl = \Illuminate\Support\Facades\Route::has($balanceRoute) ? route($balanceRoute) : null;
|
||||
$walletUrl = \Illuminate\Support\Facades\Route::has('user.wallet.index')
|
||||
? route('user.wallet.index')
|
||||
: (function_exists('ladill_account_url') ? ladill_account_url('/wallet') : '#');
|
||||
@endphp
|
||||
@if ($balanceUrl)
|
||||
<a href="{{ $walletUrl }}"
|
||||
x-data="walletWidget({ url: {{ \Illuminate\Support\Js::from($balanceUrl) }} })" x-init="load()"
|
||||
@wallet-balance-refresh.window="load()"
|
||||
@if ($onNavigate) @click="{{ $onNavigate }}" @endif
|
||||
class="{{ $class }} flex items-center justify-between gap-3 rounded-xl border border-slate-100 bg-gradient-to-r from-indigo-50 to-slate-50 px-3 py-2.5 transition hover:border-indigo-200 hover:from-indigo-100/70">
|
||||
<span class="flex items-center gap-2.5 min-w-0">
|
||||
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-100 text-indigo-600">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.6" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 1 3 7.5m18 4.5v3.75A2.25 2.25 0 0 1 18.75 18H5.25A2.25 2.25 0 0 1 3 15.75V7.5m18 4.5h-3.75a1.5 1.5 0 0 0 0 3H21M3 7.5A2.25 2.25 0 0 1 5.25 5.25h11.25A2.25 2.25 0 0 1 18.75 7.5"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-[11px] font-medium text-slate-500">Wallet balance</span>
|
||||
<span class="block truncate text-sm font-semibold text-slate-900" x-text="display"></span>
|
||||
</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 shrink-0 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m9 5 7 7-7 7"/></svg>
|
||||
</a>
|
||||
@endif
|
||||
Reference in New Issue
Block a user