2026-03-16 17:31:32 +05:30

484 lines
27 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('');
if (editingId === null || formData.password !== '') {
if (formData.password.length < 6) {
setError('Password must be at least 6 characters.');
setSaving(false);
return;
}
if (formData.password !== formData.password_confirmation) {
setError('Passwords do not match. Please make it proper.');
setSaving(false);
return;
}
}
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 {
if (data.errors) {
const firstError = Object.values(data.errors)[0][0];
setError(firstError);
} 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>
{/* Password Criteria Feedback */}
<div className="col-span-2 space-y-3 px-1">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full transition-colors ${formData.password.length >= 6 ? 'bg-emerald-500' : (formData.password.length > 0 ? 'bg-red-500' : 'bg-gray-300')}`}></div>
<span className={`text-[9px] font-black uppercase tracking-widest transition-colors ${
(formData.password.length >= 6 || (editingId && formData.password === ''))
? 'text-emerald-600'
: (formData.password.length > 0 ? 'text-red-500' : 'text-gray-400')}`}>
Min 6 characters
</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full transition-colors ${
(formData.password !== '' && formData.password === formData.password_confirmation)
? 'bg-emerald-500'
: (formData.password_confirmation !== '' ? 'bg-red-500' : 'bg-gray-300')}`}></div>
<span className={`text-[9px] font-black uppercase tracking-widest transition-colors ${
(formData.password !== '' && formData.password === formData.password_confirmation)
? 'text-emerald-600'
: (formData.password_confirmation !== '' ? 'text-red-500' : 'text-gray-400')}`}>
Passwords match
</span>
</div>
</div>
{/* Explicit Error Messages */}
<div className="space-y-1">
{formData.password.length > 0 && formData.password.length < 6 && (
<p className="text-[10px] text-red-500 font-bold uppercase tracking-widest flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<span className="w-1 h-1 rounded-full bg-red-500"></span>
Password is too short (min 6 characters)
</p>
)}
{formData.password_confirmation !== '' && formData.password !== formData.password_confirmation && (
<p className="text-[10px] text-red-500 font-bold uppercase tracking-widest flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<span className="w-1 h-1 rounded-full bg-red-500"></span>
Please make it proper (Passwords mismatch)
</p>
)}
</div>
</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>
)}
</>
);
}