CustomerService.tsx
Golden-Hour-backend-layer-2/pages/CustomerService.tsx
import React, { useState } from 'react';
type ServiceView = 'Email' | 'Check-in' | 'Messages' | 'FAQ';
interface ArrivalDossier {
id: string;
guestName: string;
property: string;
arrivalDate: string;
source: 'Web Registry' | 'Manual' | 'Agency';
status: 'Syncing' | 'Identity Pending' | 'Verification' | 'Issued';
passportImg?: string;
accessCode?: string;
guestUrl: string;
}
interface EmailTemplate {
id: string;
title: string;
subject: string;
category: 'Transactional' | 'Logistical' | 'Emotional';
lastModified: string;
preview: string;
}
interface CustomerServiceProps {
view?: ServiceView;
}
const CustomerService: React.FC<CustomerServiceProps> = ({ view = 'Check-in' }) => {
const [selectedTemplate, setSelectedTemplate] = useState<EmailTemplate | null>(null);
const [isSyncing, setIsSyncing] = useState(false);
const [previewingPortal, setPreviewingPortal] = useState<ArrivalDossier | null>(null);
const [arrivals, setArrivals] = useState<ArrivalDossier[]>([
{
id: 'ARR-402',
guestName: 'Julianne Moore',
property: 'Villa Golden Hour',
arrivalDate: '2025-05-20',
source: 'Web Registry',
status: 'Issued',
accessCode: '882190',
guestUrl: 'https://atelier.gh/checkin/882190'
},
{
id: 'ARR-405',
guestName: 'Kenji Sato',
property: 'Azure Suite 4B',
arrivalDate: '2025-05-22',
source: 'Web Registry',
status: 'Identity Pending',
guestUrl: 'https://atelier.gh/checkin/991021'
}
]);
const viewMetadata: Record<ServiceView, { title: string; sub: string }> = {
'Email': { title: 'System Email Protocol', sub: 'Automated Communication Logs' },
'Check-in': { title: 'Check-in System Registry', sub: 'Identity Verification & Access Protocol' },
'Messages': { title: 'Concierge Communication', sub: 'Guest Engagement Ledger' },
'FAQ': { title: 'FAQ & Template Archive', sub: 'Knowledge & Support Repository' }
};
const syncWebRegistry = () => {
setIsSyncing(true);
setTimeout(() => {
const newArrival: ArrivalDossier = {
id: `ARR-${Math.floor(400 + Math.random() * 100)}`,
guestName: 'Elena Rodriguez',
property: 'Starlight Room',
arrivalDate: '2025-05-25',
source: 'Web Registry',
status: 'Syncing',
guestUrl: `https://atelier.gh/checkin/${Math.floor(100000 + Math.random() * 900000)}`
};
setArrivals(prev => [newArrival, ...prev]);
setIsSyncing(false);
setTimeout(() => {
setArrivals(prev => prev.map(a => a.id === newArrival.id ? { ...a, status: 'Identity Pending' } : a));
}, 2000);
}, 2500);
};
const issueAccessCode = (id: string) => {
setArrivals(prev => prev.map(a =>
a.id === id ? { ...a, status: 'Issued', accessCode: Math.floor(100000 + Math.random() * 900000).toString() } : a
));
};
const templates: EmailTemplate[] = [
{
id: 'T-001',
title: 'Booking Confirmation',
subject: 'Welcome to the Atelier — Reservation Confirmed',
category: 'Transactional',
lastModified: '12 May 2025',
preview: 'Cher Monsieur/Madame, we are delighted to confirm your upcoming stay at Villa Golden Hour. Your bespoke journey begins on...'
},
{
id: 'T-002',
title: 'Payment Confirmation',
subject: 'Receipt of Acquisition — Transaction Record',
category: 'Transactional',
lastModified: '14 May 2025',
preview: 'Payment of €2,500.00 has been successfully processed. Attached is your formal ledger for the services rendered...'
},
{
id: 'T-003',
title: 'Check-in Instruction',
subject: 'Arrival Protocols & Personal Access',
category: 'Logistical',
lastModified: '15 May 2025',
preview: 'Your keys will be available at the private vault. Our concierge, Pierre, will be standing by to assist with your luggage...'
}
];
const renderEmailView = () => (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-10 fade-in-section">
{templates.map((tpl) => (
<div key={tpl.id} className="group relative bg-white rounded-[2.5rem] border border-[#c5a059]/10 gallery-shadow p-10 hover:border-[#c5a059]/30 transition-all duration-700 flex flex-col h-full">
<div className="flex justify-between items-start mb-10">
<span className={`text-[7px] font-black uppercase tracking-widest px-3 py-1 rounded-full border ${
tpl.category === 'Transactional' ? 'bg-[#c5a059]/5 border-[#c5a059]/10 text-[#c5a059]' :
tpl.category === 'Logistical' ? 'bg-slate-50 border-slate-100 text-slate-400' : 'bg-rose-50 border-rose-100 text-rose-300'
}`}>
{tpl.category}
</span>
<span className="text-[7px] font-serif italic text-slate-300 uppercase tracking-widest">{tpl.id}</span>
</div>
<div className="flex-1 space-y-6">
<h3 className="font-serif italic text-3xl text-slate-900 leading-tight group-hover:text-[#c5a059] transition-all cursor-pointer" onClick={() => setSelectedTemplate(tpl)}>
{tpl.title}
</h3>
<div>
<p className="text-[8px] font-black uppercase tracking-widest text-slate-300 mb-1">Subject Line</p>
<p className="text-[10px] font-medium text-slate-500 uppercase tracking-wider">{tpl.subject}</p>
</div>
<div className="bg-[#fdfcf9] rounded-2xl p-6 border border-[#c5a059]/5 italic font-serif text-slate-400 text-sm leading-relaxed overflow-hidden line-clamp-3">
"{tpl.preview}"
</div>
</div>
<div className="mt-10 pt-10 border-t border-[#c5a059]/5 flex items-center justify-between">
<div className="text-left">
<p className="text-[7px] font-black uppercase text-slate-200 tracking-widest">Last Edition</p>
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">{tpl.lastModified}</p>
</div>
<div className="flex space-x-4">
<button className="w-8 h-8 rounded-full bg-white hairline-border flex items-center justify-center text-slate-300 hover:text-[#c5a059] transition-all" title="Modify Template">
<i className="fa-solid fa-pen-nib text-[10px]"></i>
</button>
<button className="w-8 h-8 rounded-full bg-white hairline-border flex items-center justify-center text-slate-300 hover:text-[#c5a059] transition-all" title="Test Send">
<i className="fa-solid fa-paper-plane text-[10px]"></i>
</button>
</div>
</div>
</div>
))}
</div>
);
const renderCheckInView = () => (
<div className="space-y-10 fade-in-section">
<div className="bg-white rounded-[2.5rem] border border-[#c5a059]/10 gallery-shadow overflow-hidden">
<table className="w-full text-left">
<thead className="bg-[#fdfcf9] border-b border-[#c5a059]/5 text-[7px] font-black uppercase tracking-[0.3em] text-slate-300 italic font-serif">
<tr>
<th className="px-10 py-6">Identity Dossier</th>
<th className="px-10 py-6">Maison Allocation</th>
<th className="px-10 py-6">Arrival Date</th>
<th className="px-10 py-6">Identity Progress</th>
<th className="px-10 py-6">Sanctuary Code</th>
<th className="px-10 py-6 text-center">Protocol Action</th>
</tr>
</thead>
<tbody className="divide-y divide-[#c5a059]/5">
{arrivals.map((dossier) => (
<tr key={dossier.id} className="hover:bg-[#fdfcf9] group transition-all duration-700">
<td className="px-10 py-8">
<div className="flex items-center">
<div className="w-12 h-12 rounded-full border border-[#c5a059]/10 p-0.5 mr-5 grayscale group-hover:grayscale-0 transition-all duration-1000">
<img src={`https://picsum.photos/seed/${dossier.id}/100/100`} className="w-full h-full object-cover rounded-full" alt="" />
</div>
<div>
<p className="font-serif italic text-lg text-slate-900 leading-none">{dossier.guestName}</p>
<p className="text-[7px] font-bold uppercase tracking-widest text-[#c5a059] mt-1.5">{dossier.source}</p>
</div>
</div>
</td>
<td className="px-10 py-8">
<span className="text-[11px] font-bold uppercase tracking-widest text-slate-500 font-serif italic">{dossier.property}</span>
</td>
<td className="px-10 py-8 serif-numeral text-base text-slate-400">{dossier.arrivalDate}</td>
<td className="px-10 py-8">
<div className="flex items-center space-x-3">
<div className={`flex items-center justify-center w-6 h-6 rounded-full border transition-all ${
dossier.status !== 'Syncing' ? 'bg-[#c5a059] border-[#c5a059] text-white shadow-sm' : 'border-[#c5a059]/20 text-slate-200'
}`} title="Web Data Synced">
<i className="fa-solid fa-check text-[8px]"></i>
</div>
<div className="w-8 h-[1px] bg-[#c5a059]/10"></div>
<div className={`flex items-center justify-center w-6 h-6 rounded-full border transition-all ${
['Verification', 'Issued'].includes(dossier.status) ? 'bg-[#c5a059] border-[#c5a059] text-white shadow-sm' : 'border-[#c5a059]/20 text-slate-200'
}`} title="Passport Uploaded">
<i className="fa-solid fa-passport text-[8px]"></i>
</div>
<div className="w-8 h-[1px] bg-[#c5a059]/10"></div>
<div className={`flex items-center justify-center w-6 h-6 rounded-full border transition-all ${
dossier.status === 'Issued' ? 'bg-[#c5a059] border-[#c5a059] text-white shadow-sm' : 'border-[#c5a059]/20 text-slate-200'
}`} title="Access Issued">
<i className="fa-solid fa-key text-[8px]"></i>
</div>
</div>
</td>
<td className="px-10 py-8">
{dossier.accessCode ? (
<div className="flex items-center space-x-3">
<span className="font-mono text-sm tracking-[0.2em] font-black bg-slate-900 text-[#c5a059] px-3 py-1 rounded-lg shadow-sm">
{dossier.accessCode}
</span>
<i className="fa-solid fa-circle-check text-emerald-500 text-[10px]"></i>
</div>
) : (
<button
onClick={() => issueAccessCode(dossier.id)}
className="text-[8px] font-black uppercase tracking-[0.2em] text-[#c5a059] hover:text-slate-900 transition-all border border-dashed border-[#c5a059]/30 px-4 py-2 rounded-xl"
>
Issue Digital Key
</button>
)}
</td>
<td className="px-10 py-8 text-center">
<div className="flex justify-center space-x-6">
<button onClick={() => setPreviewingPortal(dossier)} className="text-slate-200 hover:text-[#c5a059] transition-all" title="Preview Guest Portal">
<i className="fa-solid fa-eye text-xs"></i>
</button>
<button className="text-slate-200 hover:text-[#c5a059] transition-all" title="Copy Invitation URL">
<i className="fa-solid fa-link text-xs"></i>
</button>
<button className="text-slate-200 hover:text-rose-600 transition-all" title="Revoke Access">
<i className="fa-solid fa-ban text-xs"></i>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
<div className="bg-white rounded-[2.5rem] border border-[#c5a059]/10 gallery-shadow p-12">
<h4 className="font-serif italic text-2xl text-slate-900 mb-8">Statutory Logic Configuration</h4>
<div className="space-y-6">
<div className="flex items-center justify-between p-6 rounded-2xl bg-[#fdfcf9] border border-[#c5a059]/5">
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-800">Automatic Document Extraction</p>
<p className="text-[8px] font-serif italic text-slate-400 mt-1">Use AI to parse guest passports immediately</p>
</div>
<div className="w-10 h-5 bg-[#c5a059] rounded-full relative">
<div className="absolute right-1 top-1 w-3 h-3 bg-white rounded-full"></div>
</div>
</div>
<div className="flex items-center justify-between p-6 rounded-2xl bg-[#fdfcf9] border border-[#c5a059]/5">
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-800">Website Real-time Sync</p>
<p className="text-[8px] font-serif italic text-slate-400 mt-1">Polling Interval: Every 15 minutes</p>
</div>
<div className="w-10 h-5 bg-slate-200 rounded-full relative">
<div className="absolute left-1 top-1 w-3 h-3 bg-white rounded-full"></div>
</div>
</div>
</div>
</div>
<div className="bg-[#1a1a1a] rounded-[2.5rem] p-12 gallery-shadow text-white relative overflow-hidden">
<div className="absolute top-0 right-0 w-48 h-48 bg-[#c5a059]/10 rounded-full blur-3xl -mr-24 -mt-24"></div>
<h4 className="font-serif italic text-2xl mb-8 relative z-10">Atelier Link Blueprint</h4>
<div className="space-y-6 relative z-10">
<p className="text-slate-400 font-serif italic text-sm leading-relaxed">
The Guest Check-in portal allows direct document submission to our secure ledger.
Guests receive their unique Sanctuary code once identity is validated.
</p>
<div className="bg-white/5 rounded-2xl p-6 border border-white/10">
<p className="text-[8px] font-black uppercase tracking-[0.4em] text-[#c5a059] mb-3">System Operational State</p>
<div className="flex justify-between items-center">
<span className="text-stone-300 text-xs">Waiting for Guest Input</span>
<span className="serif-numeral text-xl">04 Dossiers</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
const renderMessagesView = () => (
<div className="bg-white rounded-[3rem] border border-[#c5a059]/10 gallery-shadow h-[600px] flex overflow-hidden fade-in-section">
<div className="w-80 border-r border-[#c5a059]/5 p-8 bg-[#fdfcf9]/50">
<h4 className="text-[10px] font-black uppercase tracking-[0.4em] text-slate-300 mb-8">Concierge Inbox</h4>
<div className="space-y-4">
{[1,2,3].map(i => (
<div key={i} className="p-4 rounded-2xl bg-white border border-[#c5a059]/10 shadow-sm cursor-pointer hover:border-[#c5a059] transition-all">
<p className="font-serif italic text-base text-slate-900">Julianne Moore</p>
<p className="text-[8px] text-slate-400 mt-1 truncate">I am arriving late, can you prepare the spa?</p>
</div>
))}
</div>
</div>
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center text-slate-300">
<i className="fa-solid fa-comments text-4xl mb-6 opacity-20"></i>
<p className="font-serif italic text-xl">Select a booking dossier to open secure channel</p>
</div>
</div>
);
const renderFAQView = () => (
<div className="space-y-10 fade-in-section">
<div className="bg-white rounded-[3rem] border border-[#c5a059]/10 gallery-shadow p-12">
<div className="divide-y divide-[#c5a059]/5">
{[
{ q: 'How is the access code generated?', a: 'Codes are cryptographically randomized upon identity verification.' },
{ q: 'What is the required passport resolution?', a: 'Standard HD scan is preferred for automated OCR validation.' },
{ q: 'Can I manually override a code?', a: 'Yes, admins can revoke and re-issue codes from the protocol action column.' }
].map((item, i) => (
<div key={i} className="py-8 group cursor-pointer border-b border-[#c5a059]/5 last:border-0">
<h4 className="font-serif italic text-xl text-slate-800 group-hover:text-[#c5a059] transition-colors">{item.q}</h4>
<p className="text-slate-400 text-sm mt-3 leading-relaxed">{item.a}</p>
</div>
))}
</div>
</div>
</div>
);
return (
<div className="relative pb-32">
<div className="space-y-16 fade-in-section">
<div className="flex flex-col md:flex-row md:items-end justify-between border-b border-[#c5a059]/10 pb-12 gap-8">
<div>
<h2 className="text-4xl font-serif italic text-slate-900 tracking-tight">
{viewMetadata[view].title}
</h2>
<p className="text-[10px] font-black uppercase tracking-[0.5em] text-[#c5a059] mt-3 font-serif italic">
{viewMetadata[view].sub}
</p>
</div>
<div className="flex items-center space-x-8">
{view === 'Check-in' && (
<button
onClick={syncWebRegistry}
disabled={isSyncing}
className={`px-8 py-3 rounded-full text-[9px] font-black uppercase tracking-[0.3em] transition-all flex items-center ${
isSyncing ? 'bg-slate-100 text-slate-300 cursor-not-allowed' : 'bg-black text-white hover:bg-[#c5a059] shadow-xl hover:scale-105'
}`}
>
{isSyncing ? (
<><i className="fa-solid fa-spinner animate-spin mr-3"></i> Harvesting Data</>
) : (
<><i className="fa-solid fa-cloud-arrow-down mr-3"></i> Sync Official Website</>
)}
</button>
)}
<div className="flex items-center text-slate-200">
<i className="fa-solid fa-user-shield text-[10px] mr-3"></i>
<span className="text-[8px] font-bold uppercase tracking-widest italic font-serif">Secure Session Active</span>
</div>
</div>
</div>
{view === 'Email' && renderEmailView()}
{view === 'Check-in' && renderCheckInView()}
{view === 'Messages' && renderMessagesView()}
{view === 'FAQ' && renderFAQView()}
</div>
{/* Guest Portal Preview Modal */}
{previewingPortal && (
<div className="fixed inset-0 z-[110] flex items-center justify-center p-8 bg-slate-900/70 backdrop-blur-xl fade-in-section">
<div className="bg-[#fdfcf9] rounded-[4rem] gallery-shadow w-full max-w-xl border border-[#c5a059]/20 flex flex-col relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-1.5 bg-[#c5a059]"></div>
<div className="p-16 text-center space-y-10">
<div className="mx-auto w-16 h-16 rounded-full border border-[#c5a059]/20 flex items-center justify-center">
<i className="fa-solid fa-hotel text-[#c5a059] text-xl"></i>
</div>
<div>
<h3 className="font-serif italic text-4xl text-slate-900">Welcome to the Atelier</h3>
<p className="text-[9px] font-bold uppercase tracking-[0.4em] text-slate-400 mt-3">Identity Registration Protocol</p>
</div>
<div className="space-y-6">
<div className="p-8 rounded-3xl bg-white border border-[#c5a059]/10 border-dashed text-center group cursor-pointer hover:border-[#c5a059] transition-all">
<i className="fa-solid fa-camera text-2xl text-slate-200 mb-4 group-hover:text-[#c5a059] transition-colors"></i>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Upload Passport or Identity Card</p>
<p className="text-[8px] font-serif italic text-slate-300 mt-2">Accepted formats: JPG, PNG, PDF</p>
</div>
<div className="bg-white border border-[#c5a059]/10 rounded-3xl p-8">
<div className="flex justify-between items-center text-[8px] font-bold uppercase tracking-widest text-slate-300 mb-6">
<span>Sanctuary Access</span>
<i className="fa-solid fa-lock text-[6px]"></i>
</div>
<div className="flex flex-col items-center">
{previewingPortal.accessCode ? (
<>
<p className="font-mono text-4xl font-black tracking-[0.5em] text-slate-900 mb-4">{previewingPortal.accessCode}</p>
<p className="text-[8px] font-serif italic text-emerald-500 uppercase tracking-widest">Protocol Verified — Welcome Home</p>
</>
) : (
<p className="text-[10px] font-serif italic text-slate-300">Code will be issued upon document verification</p>
)}
</div>
</div>
</div>
<button onClick={() => setPreviewingPortal(null)} className="w-full bg-black text-white py-5 rounded-full text-[10px] font-black uppercase tracking-[0.4em] hover:bg-[#c5a059] transition-all">
Return to Admin
</button>
</div>
</div>
</div>
)}
{/* Template Detailed Preview Modal */}
{selectedTemplate && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-8 bg-slate-900/60 backdrop-blur-md fade-in-section">
<div className="bg-white rounded-[3rem] gallery-shadow w-full max-w-2xl max-h-[90vh] overflow-hidden border border-[#c5a059]/20 flex flex-col">
<div className="p-10 border-b border-[#c5a059]/10 flex items-center justify-between bg-[#fdfcf9]">
<div>
<span className="text-[8px] font-bold uppercase tracking-[0.5em] text-[#c5a059] block mb-2 font-serif italic">Communication Blueprint</span>
<h3 className="font-serif italic text-3xl text-slate-900">{selectedTemplate.title}</h3>
</div>
<button onClick={() => setSelectedTemplate(null)} className="w-10 h-10 rounded-full border border-[#c5a059]/10 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-all">
<i className="fa-solid fa-xmark"></i>
</button>
</div>
<div className="flex-1 overflow-y-auto p-12 bg-[#fdfcf9]/50">
<div className="max-w-md mx-auto bg-white border border-[#c5a059]/10 rounded-xl p-10 shadow-sm space-y-8 font-serif">
<div className="text-center border-b border-[#c5a059]/5 pb-8 mb-8">
<span className="text-xl italic text-[#c5a059]">The Atelier</span>
</div>
<div className="space-y-6 text-slate-600 text-lg italic leading-relaxed">
<p>Cher Guest,</p>
<p>{selectedTemplate.preview} ... [Extended template content placeholder]</p>
<p>Your sanctuary is being prepared with meticulous care. We anticipate your arrival at the Golden Hour.</p>
</div>
<div className="pt-10 border-t border-[#c5a059]/5 text-right opacity-40">
<p className="text-[10px] uppercase tracking-widest font-sans font-bold">Un Regard de Provence</p>
</div>
</div>
</div>
<div className="p-8 bg-white border-t border-[#c5a059]/10 flex justify-between">
<button className="text-[8px] font-bold uppercase tracking-[0.2em] text-[#c5a059] hover:text-slate-900 transition-all">Download Stationery PDF</button>
<button onClick={() => setSelectedTemplate(null)} className="bg-black text-white px-8 py-3 rounded-full text-[8px] font-bold uppercase tracking-[0.3em] hover:bg-[#c5a059] transition-all">Archive Entry</button>
</div>
</div>
</div>
)}
<div className="fixed bottom-12 left-1/2 -translate-x-1/2 bg-white/90 backdrop-blur-md border border-[#c5a059]/20 px-10 py-5 rounded-full gallery-shadow flex items-center space-x-12 z-50 transition-all hover:border-[#c5a059]/40 group">
<button className="flex items-center space-x-3 text-slate-400 hover:text-slate-900 transition-all">
<i className="fa-solid fa-magnifying-glass text-[9px] text-[#c5a059]"></i>
<span className="text-[8px] font-bold uppercase tracking-[0.3em]">Query</span>
</button>
<div className="w-[0.5px] h-4 bg-[#c5a059]/20"></div>
<button className="flex items-center space-x-3 text-slate-400 hover:text-slate-900 transition-all">
<i className="fa-solid fa-gear text-[9px] text-[#c5a059]"></i>
<span className="text-[8px] font-bold uppercase tracking-[0.3em]">Configure</span>
</button>
</div>
</div>
);
};
export default CustomerService;
相关文章
App.tsx
App.tsx — typescript source code from the Golden-Hour-backend-layer-2 learning materials (Golden-Hour-backend-layer-2/App.tsx).
阅读文章 →Header.tsx
Header.tsx — typescript source code from the Golden-Hour-backend-layer-2 learning materials (Golden-Hour-backend-layer-2/components/Header.tsx).
阅读文章 →Sidebar.tsx
Sidebar.tsx — typescript source code from the Golden-Hour-backend-layer-2 learning materials (Golden-Hour-backend-layer-2/components/Sidebar.tsx).
阅读文章 →index.tsx
index.tsx — typescript source code from the Golden-Hour-backend-layer-2 learning materials (Golden-Hour-backend-layer-2/index.tsx).
阅读文章 →ContentManagement.tsx
ContentManagement.tsx — typescript source code from the Golden-Hour-backend-layer-2 learning materials (Golden-Hour-backend-layer-2/pages/ContentManagement.tsx).
阅读文章 →CustomerManagement.tsx
CustomerManagement.tsx — typescript source code from the Golden-Hour-backend-layer-2 learning materials (Golden-Hour-backend-layer-2/pages/CustomerManagement.tsx).
阅读文章 →