423 lines
24 KiB
JavaScript
423 lines
24 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { ArrowLeft, MapPin, User, Users, Calendar, DollarSign, CheckCircle, Edit3, Upload, Plus, Box, X, Eye, FileText, Shield, Save, Trash2, Key } from 'lucide-react';
|
|
|
|
function ReceptionistForm({ branchId }) {
|
|
const [receptionists, setReceptionists] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [editingId, setEditingId] = useState(null); // null for new, 'list' for list, ID for edit
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
password_confirmation: ''
|
|
});
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState('');
|
|
|
|
const fetchReceptionists = () => {
|
|
setLoading(true);
|
|
fetch(`/api/branches/${branchId}/receptionist`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
setReceptionists(Array.isArray(data) ? data : []);
|
|
setEditingId('list');
|
|
})
|
|
.finally(() => setLoading(false));
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchReceptionists();
|
|
}, [branchId]);
|
|
|
|
const handleEdit = (rep) => {
|
|
setEditingId(rep.id);
|
|
setFormData({ name: rep.name, email: rep.email, password: '', password_confirmation: '' });
|
|
setError('');
|
|
setSuccess('');
|
|
};
|
|
|
|
const handleAddNew = () => {
|
|
setEditingId(null);
|
|
setFormData({ name: '', email: '', password: '', password_confirmation: '' });
|
|
setError('');
|
|
setSuccess('');
|
|
};
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
setError('');
|
|
setSuccess('');
|
|
|
|
try {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
|
const res = await fetch(`/api/branches/${branchId}/receptionist`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken
|
|
},
|
|
body: JSON.stringify({ ...formData, id: editingId })
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
setSuccess(editingId ? 'Account updated successfully!' : 'Account created successfully!');
|
|
fetchReceptionists();
|
|
} else {
|
|
setError(data.message || 'Failed to save receptionist.');
|
|
}
|
|
} catch (err) {
|
|
setError('An error occurred. Please try again.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id) => {
|
|
if (!confirm('Are you sure you want to delete this receptionist account?')) return;
|
|
|
|
try {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
|
await fetch(`/api/branches/${branchId}/receptionist?receptionist_id=${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-TOKEN': csrfToken }
|
|
});
|
|
setSuccess('Receptionist account removed.');
|
|
fetchReceptionists();
|
|
} catch (err) {
|
|
setError('Failed to delete account.');
|
|
}
|
|
};
|
|
|
|
if (loading && receptionists.length === 0) return <div className="text-gray-400 font-medium italic">Loading account details...</div>;
|
|
|
|
if (editingId === 'list') {
|
|
return (
|
|
<div className="space-y-6">
|
|
{success && <div className="p-4 bg-emerald-50 text-emerald-600 rounded-xl text-sm border border-emerald-100 font-bold">{success}</div>}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{receptionists.map(rep => (
|
|
<div key={rep.id} className="p-6 bg-gray-50 rounded-2xl border border-gray-100 flex items-center justify-between group">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-red-500 shadow-sm">
|
|
<User size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-gray-900">{rep.name}</p>
|
|
<p className="text-xs text-gray-500 font-medium">{rep.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-all">
|
|
<button onClick={() => handleEdit(rep)} className="p-2 bg-white text-gray-400 hover:text-red-500 rounded-lg shadow-sm border border-gray-100 transition-all">
|
|
<Edit3 size={16} />
|
|
</button>
|
|
<button onClick={() => handleDelete(rep.id)} className="p-2 bg-white text-gray-400 hover:text-rose-500 rounded-lg shadow-sm border border-gray-100 transition-all">
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<button
|
|
onClick={handleAddNew}
|
|
className="p-6 border-2 border-dashed border-gray-200 rounded-2xl flex flex-col items-center justify-center gap-2 text-gray-400 hover:border-red-500 hover:text-red-500 transition-all bg-white/50"
|
|
>
|
|
<Plus size={24} />
|
|
<span className="text-xs font-bold uppercase tracking-widest">Add New Receptionist</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-2xl bg-white p-8 rounded-3xl border border-gray-100 shadow-sm animate-in slide-in-from-bottom-4 duration-500">
|
|
<div className="flex items-center justify-between mb-8">
|
|
<h4 className="text-lg font-bold text-gray-900">{editingId ? 'Edit Receptionist' : 'Add New Receptionist'}</h4>
|
|
<button onClick={() => setEditingId('list')} className="text-xs font-bold text-gray-400 hover:text-gray-900 uppercase tracking-widest flex items-center gap-2">
|
|
<ArrowLeft size={14} />
|
|
Back to List
|
|
</button>
|
|
</div>
|
|
|
|
{error && <div className="mb-6 p-4 bg-red-50 text-red-600 rounded-xl text-sm border border-red-100 font-bold">{error}</div>}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold text-gray-400 tracking-widest pl-1">Full Name</label>
|
|
<input
|
|
className="w-full px-5 py-3 bg-gray-50 border border-transparent rounded-xl focus:bg-white focus:border-red-500 transition-all outline-none font-semibold text-gray-900"
|
|
value={formData.name}
|
|
onChange={e => setFormData({...formData, name: e.target.value})}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold text-gray-400 tracking-widest pl-1">Email Address</label>
|
|
<input
|
|
type="email"
|
|
className="w-full px-5 py-3 bg-gray-50 border border-transparent rounded-xl focus:bg-white focus:border-red-500 transition-all outline-none font-semibold text-gray-900"
|
|
value={formData.email}
|
|
onChange={e => setFormData({...formData, email: e.target.value})}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-6 pt-4 border-t border-gray-50">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold text-gray-400 tracking-widest pl-1">Password</label>
|
|
<input
|
|
type="password"
|
|
placeholder={editingId ? "•••••••• (Leave blank to keep current)" : "Enter password"}
|
|
className="w-full px-5 py-3 bg-gray-50 border border-transparent rounded-xl focus:bg-white focus:border-red-500 transition-all outline-none font-semibold text-gray-900"
|
|
value={formData.password}
|
|
onChange={e => setFormData({...formData, password: e.target.value})}
|
|
required={!editingId}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold text-gray-400 tracking-widest pl-1">Confirm Password</label>
|
|
<input
|
|
type="password"
|
|
placeholder="Confirm your password"
|
|
className="w-full px-5 py-3 bg-gray-50 border border-transparent rounded-xl focus:bg-white focus:border-red-500 transition-all outline-none font-semibold text-gray-900"
|
|
value={formData.password_confirmation}
|
|
onChange={e => setFormData({...formData, password_confirmation: e.target.value})}
|
|
required={formData.password !== ''}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 pt-6">
|
|
<button
|
|
disabled={saving}
|
|
className="flex-1 flex items-center justify-center gap-2 px-6 py-3.5 bg-red-500 text-white rounded-xl font-bold hover:bg-red-600 transition-all shadow-lg shadow-red-200 disabled:opacity-50"
|
|
>
|
|
<Save size={18} />
|
|
{saving ? 'Saving...' : editingId ? 'Update Account' : 'Create Receptionist'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditingId('list')}
|
|
className="px-6 py-3.5 bg-gray-100 text-gray-500 rounded-xl font-bold hover:bg-gray-200 transition-all"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function View({ id }) {
|
|
const [branch, setBranch] = useState(null);
|
|
const [activeTab, setActiveTab] = useState('Info');
|
|
const [selectedDoc, setSelectedDoc] = useState(null);
|
|
|
|
useEffect(() => {
|
|
fetch(`/api/branches/${id}`)
|
|
.then(res => res.json())
|
|
.then(data => setBranch(data));
|
|
}, [id]);
|
|
|
|
if (!branch) return <div className="p-8 text-center text-gray-500 font-bold">Loading branch details...</div>;
|
|
|
|
return (
|
|
<>
|
|
<main className="p-8 max-w-[1600px] mx-auto space-y-8">
|
|
{/* Navigation & Title */}
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => window.location.href = '/owner/branches'}
|
|
className="w-8 h-8 bg-white border border-gray-100 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-900 shadow-sm transition-all"
|
|
>
|
|
<ArrowLeft size={16} />
|
|
</button>
|
|
<h2 className="text-3xl font-bold text-gray-900 tracking-tight">{branch.name}</h2>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex items-center gap-8 border-b border-gray-100 px-4">
|
|
{['Info', 'Documents', 'User'].map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab)}
|
|
className={`py-4 text-xs font-semibold tracking-wide transition-all border-b-2 ${
|
|
activeTab === tab
|
|
? 'border-red-500 text-red-500'
|
|
: 'border-transparent text-gray-400 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div className="bg-white rounded-[2rem] border border-gray-100 shadow-sm overflow-hidden p-10">
|
|
{activeTab === 'Info' ? (
|
|
<div className="space-y-10">
|
|
{/* ... existing info content ... */}
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xl font-bold text-gray-900">Branch Information</h3>
|
|
<button className="flex items-center gap-2 px-4 py-2.5 bg-gray-50 text-gray-500 rounded-xl text-xs font-semibold hover:bg-gray-100 transition-all border border-gray-100">
|
|
<Edit3 size={14} />
|
|
<span>Edit Details</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-8 gap-x-20">
|
|
{/* Left Column */}
|
|
<div className="space-y-8">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-10 h-10 bg-gray-50 rounded-xl flex items-center justify-center text-gray-400">
|
|
<MapPin size={18} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] uppercase font-semibold text-gray-400 tracking-widest mb-1 mt-1">Location</p>
|
|
<p className="text-base font-semibold text-gray-900">{branch.location}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-10 h-10 bg-gray-50 rounded-xl flex items-center justify-center text-gray-400">
|
|
<User size={18} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] uppercase font-semibold text-gray-400 tracking-widest mb-1 mt-1">Manager</p>
|
|
<p className="text-base font-semibold text-gray-900">{branch.manager_name}</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* Right Column */}
|
|
<div className="space-y-8">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-10 h-10 bg-gray-50 rounded-xl flex items-center justify-center text-gray-400">
|
|
<Calendar size={18} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] uppercase font-semibold text-gray-400 tracking-widest mb-1 mt-1">Operational Start Date</p>
|
|
<p className="text-base font-semibold text-gray-900">{branch.operational_start_date ? new Date(branch.operational_start_date).toLocaleDateString() : 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-10 h-10 bg-gray-50 rounded-xl flex items-center justify-center text-gray-400">
|
|
<CheckCircle size={18} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] uppercase font-semibold text-gray-400 tracking-widest mb-1 mt-1">Status</p>
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-emerald-50 text-emerald-600 text-[10px] font-bold uppercase tracking-wider rounded-full border border-emerald-100">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
|
{branch.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : activeTab === 'Documents' ? (
|
|
<div className="space-y-8">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xl font-bold text-gray-900">Branch Documents</h3>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{branch.documents.length > 0 ? branch.documents.map((doc) => (
|
|
<div
|
|
key={doc.id}
|
|
onClick={() => setSelectedDoc(doc)}
|
|
className="p-4 border border-gray-100 rounded-2xl flex items-center gap-4 hover:border-red-500/30 transition-all group cursor-pointer"
|
|
>
|
|
<div className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center text-gray-400 group-hover:bg-red-50 group-hover:text-red-500">
|
|
{doc.path.toLowerCase().endsWith('.pdf') ? <FileText size={20} /> : <Box size={20} />}
|
|
</div>
|
|
<div className="flex-1 overflow-hidden">
|
|
<p className="font-semibold text-gray-900 truncate">{doc.name}</p>
|
|
<p className="text-[10px] text-rose-500 font-bold uppercase tracking-wider">Expires: {doc.expiry_date ? new Date(doc.expiry_date).toLocaleDateString() : 'N/A'}</p>
|
|
</div>
|
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<Eye size={16} className="text-gray-400" />
|
|
</div>
|
|
</div>
|
|
)) : (
|
|
<div className="col-span-full py-12 text-center text-gray-400 font-medium bg-gray-50 rounded-2xl border-2 border-dashed border-gray-200">
|
|
No documents uploaded yet.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-8 animate-in fade-in duration-500">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-xl font-bold text-gray-900">Branch Receptionist</h3>
|
|
<p className="text-xs text-gray-400 font-medium mt-1 uppercase tracking-widest">Manage receptionist login credentials for this branch.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<ReceptionistForm branchId={branch.id} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
|
|
{/* Document Preview Modal */}
|
|
{selectedDoc && (
|
|
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
|
<div className="bg-white rounded-[2.5rem] w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl animate-in zoom-in-95 duration-200">
|
|
<div className="p-6 flex items-center justify-between border-b border-gray-100">
|
|
<div>
|
|
<h3 className="text-xl font-bold text-gray-900">{selectedDoc.name}</h3>
|
|
<p className="text-xs text-gray-400 font-medium">Document Preview</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setSelectedDoc(null)}
|
|
className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-gray-100 text-gray-400 hover:text-gray-900 transition-all"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 p-8 bg-gray-50/50 overflow-auto flex items-center justify-center min-h-[400px]">
|
|
{selectedDoc.path.toLowerCase().endsWith('.pdf') ? (
|
|
<iframe
|
|
src={`/storage/${selectedDoc.path}`}
|
|
className="w-full h-full min-h-[60vh] rounded-2xl border border-gray-100 shadow-sm"
|
|
title={selectedDoc.name}
|
|
/>
|
|
) : (
|
|
<img
|
|
src={`/storage/${selectedDoc.path}`}
|
|
alt={selectedDoc.name}
|
|
className="max-w-full max-h-full object-contain rounded-2xl shadow-lg border border-gray-100"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="p-6 border-t border-gray-100 flex justify-end gap-3">
|
|
<a
|
|
href={`/storage/${selectedDoc.path}`}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="px-6 py-2.5 bg-gray-100 text-gray-600 rounded-xl text-xs font-bold hover:bg-gray-200 transition-all"
|
|
>
|
|
Open in New Tab
|
|
</a>
|
|
<button
|
|
onClick={() => setSelectedDoc(null)}
|
|
className="px-6 py-2.5 bg-gray-900 text-white rounded-xl text-xs font-bold hover:bg-black transition-all shadow-lg shadow-gray-200"
|
|
>
|
|
Close Preview
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|