Notification updates

This commit is contained in:
Eric Gullickson
2025-12-21 19:56:52 -06:00
parent 144f1d5bb0
commit 719c80ecd8
80 changed files with 7552 additions and 678 deletions

View File

@@ -0,0 +1,440 @@
/**
* @ai-summary Admin Email Templates mobile screen for managing notification email templates
* @ai-context Mobile-optimized version with touch-friendly UI
*/
import React, { useState, useCallback } from 'react';
import { Navigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Edit, Visibility, Close, Send } from '@mui/icons-material';
import toast from 'react-hot-toast';
import { useAdminAccess } from '../../../core/auth/useAdminAccess';
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
import { adminApi } from '../api/admin.api';
import { EmailTemplate, UpdateEmailTemplateRequest } from '../types/admin.types';
const SAMPLE_VARIABLES: Record<string, string> = {
userName: 'John Doe',
vehicleName: '2024 Toyota Camry',
category: 'Routine Maintenance',
subtypes: 'Oil Change, Air Filter',
dueDate: '2025-01-15',
dueMileage: '50,000',
documentType: 'Insurance',
documentTitle: 'State Farm Policy',
expirationDate: '2025-02-28',
};
export const AdminEmailTemplatesMobileScreen: React.FC = () => {
const { loading: authLoading, isAdmin } = useAdminAccess();
const queryClient = useQueryClient();
// State
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
const [previewTemplate, setPreviewTemplate] = useState<EmailTemplate | null>(null);
const [editSubject, setEditSubject] = useState('');
const [editBody, setEditBody] = useState('');
const [editIsActive, setEditIsActive] = useState(true);
const [previewSubject, setPreviewSubject] = useState('');
const [previewBody, setPreviewBody] = useState('');
// Queries
const { data: templates, isLoading } = useQuery({
queryKey: ['admin', 'emailTemplates'],
queryFn: () => adminApi.emailTemplates.list(),
enabled: isAdmin,
});
// Mutations
const updateMutation = useMutation({
mutationFn: ({ key, data }: { key: string; data: UpdateEmailTemplateRequest }) =>
adminApi.emailTemplates.update(key, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'emailTemplates'] });
toast.success('Template updated');
handleCloseEdit();
},
onError: () => {
toast.error('Failed to update template');
},
});
const previewMutation = useMutation({
mutationFn: ({ key, variables }: { key: string; variables: Record<string, string> }) =>
adminApi.emailTemplates.preview(key, variables),
onSuccess: (data) => {
setPreviewSubject(data.subject);
setPreviewBody(data.body);
},
onError: () => {
toast.error('Failed to generate preview');
},
});
const sendTestMutation = useMutation({
mutationFn: (key: string) => adminApi.emailTemplates.sendTest(key),
onSuccess: (data) => {
if (data.error) {
toast.error(`Test email failed: ${data.error}`);
} else if (data.message) {
toast.success(data.message);
}
},
onError: () => {
toast.error('Failed to send test email');
},
});
// Handlers
const handleEditClick = useCallback((template: EmailTemplate) => {
setEditingTemplate(template);
setEditSubject(template.subject);
setEditBody(template.body);
setEditIsActive(template.isActive);
}, []);
const handlePreviewClick = useCallback((template: EmailTemplate) => {
setPreviewTemplate(template);
previewMutation.mutate({
key: template.templateKey,
variables: SAMPLE_VARIABLES,
});
}, [previewMutation]);
const handleSendTestClick = useCallback((template: EmailTemplate) => {
sendTestMutation.mutate(template.templateKey);
}, [sendTestMutation]);
const handleCloseEdit = useCallback(() => {
setEditingTemplate(null);
setEditSubject('');
setEditBody('');
setEditIsActive(true);
}, []);
const handleClosePreview = useCallback(() => {
setPreviewTemplate(null);
setPreviewSubject('');
setPreviewBody('');
}, []);
const handleSave = useCallback(() => {
if (!editingTemplate) return;
const data: UpdateEmailTemplateRequest = {
subject: editSubject !== editingTemplate.subject ? editSubject : undefined,
body: editBody !== editingTemplate.body ? editBody : undefined,
isActive: editIsActive !== editingTemplate.isActive ? editIsActive : undefined,
};
// Only update if there are changes
if (data.subject || data.body || data.isActive !== undefined) {
updateMutation.mutate({
key: editingTemplate.templateKey,
data,
});
} else {
handleCloseEdit();
}
}, [editingTemplate, editSubject, editBody, editIsActive, updateMutation, handleCloseEdit]);
// Auth loading
if (authLoading) {
return (
<MobileContainer>
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="text-slate-500 mb-2">Loading admin access...</div>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto" />
</div>
</div>
</MobileContainer>
);
}
// Not admin
if (!isAdmin) {
return <Navigate to="/garage/settings" replace />;
}
// Edit view
if (editingTemplate) {
return (
<MobileContainer>
<div className="space-y-4 pb-24 p-4">
{/* Header */}
<div className="flex items-center justify-between gap-3">
<div className="flex-1">
<h1 className="text-2xl font-bold text-slate-800">Edit Template</h1>
<p className="text-sm text-slate-500">{editingTemplate.name}</p>
</div>
<button
onClick={handleCloseEdit}
className="p-2 hover:bg-gray-100 rounded-full transition-colors min-h-[44px] min-w-[44px] flex items-center justify-center"
>
<Close />
</button>
</div>
{/* Edit Form */}
<GlassCard>
<div className="p-4 space-y-4">
{/* Active Toggle */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">Active</span>
<button
onClick={() => setEditIsActive(!editIsActive)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors min-h-[44px] min-w-[44px] ${
editIsActive ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
editIsActive ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* Subject */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Subject
</label>
<input
type="text"
value={editSubject}
onChange={(e) => setEditSubject(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[44px]"
placeholder="Email subject"
/>
<p className="text-xs text-slate-500 mt-1">
Use variables like {editingTemplate.variables.map((v) => `{{${v}}}`).join(', ')}
</p>
</div>
{/* Body */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Body
</label>
<textarea
value={editBody}
onChange={(e) => setEditBody(e.target.value)}
rows={12}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm"
placeholder="Email body"
/>
<p className="text-xs text-slate-500 mt-1">
Use variables like {editingTemplate.variables.map((v) => `{{${v}}}`).join(', ')}
</p>
</div>
{/* Available Variables */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-xs font-medium text-blue-900 mb-2">Available Variables</p>
<div className="flex flex-wrap gap-1">
{editingTemplate.variables.map((variable) => (
<span
key={variable}
className="inline-block px-2 py-1 bg-white border border-blue-300 rounded text-xs font-mono text-blue-700"
>
{`{{${variable}}}`}
</span>
))}
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2">
<button
onClick={handleCloseEdit}
className="flex-1 px-4 py-3 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors min-h-[44px]"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={updateMutation.isPending}
className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:bg-gray-300 min-h-[44px]"
>
{updateMutation.isPending ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</GlassCard>
</div>
</MobileContainer>
);
}
// Preview view
if (previewTemplate) {
return (
<MobileContainer>
<div className="space-y-4 pb-24 p-4">
{/* Header */}
<div className="flex items-center justify-between gap-3">
<div className="flex-1">
<h1 className="text-2xl font-bold text-slate-800">Preview</h1>
<p className="text-sm text-slate-500">{previewTemplate.name}</p>
</div>
<button
onClick={handleClosePreview}
className="p-2 hover:bg-gray-100 rounded-full transition-colors min-h-[44px] min-w-[44px] flex items-center justify-center"
>
<Close />
</button>
</div>
{/* Preview Content */}
<GlassCard>
<div className="p-4 space-y-4">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-xs text-blue-700">
This preview uses sample data to show how the template will appear.
</p>
</div>
{previewMutation.isPending ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto" />
<p className="text-slate-500 mt-2 text-sm">Generating preview...</p>
</div>
) : (
<>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Subject
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-lg min-h-[44px] flex items-center">
{previewSubject}
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Body
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-lg font-mono text-sm whitespace-pre-wrap">
{previewBody}
</div>
</div>
</>
)}
<button
onClick={handleClosePreview}
className="w-full px-4 py-3 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors min-h-[44px]"
>
Close
</button>
</div>
</GlassCard>
</div>
</MobileContainer>
);
}
// List view
return (
<MobileContainer>
<div className="space-y-4 pb-24 p-4">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-slate-800">Email Templates</h1>
<p className="text-sm text-slate-500">
{templates?.length || 0} notification templates
</p>
</div>
{/* Templates List */}
{isLoading ? (
<GlassCard>
<div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto" />
<p className="text-slate-500 mt-2 text-sm">Loading templates...</p>
</div>
</GlassCard>
) : (
<div className="space-y-3">
{templates?.map((template) => (
<GlassCard key={template.id}>
<div className="p-4">
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-semibold text-slate-800">{template.name}</h3>
{template.description && (
<p className="text-xs text-slate-500 mt-1">{template.description}</p>
)}
</div>
<span
className={`ml-2 px-2 py-1 rounded-full text-xs font-medium ${
template.isActive
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-600'
}`}
>
{template.isActive ? 'Active' : 'Inactive'}
</span>
</div>
{/* Subject */}
<div className="mb-3">
<p className="text-xs font-medium text-slate-600 mb-1">Subject</p>
<p className="text-sm text-slate-700 truncate">{template.subject}</p>
</div>
{/* Variables */}
<div className="mb-4">
<p className="text-xs font-medium text-slate-600 mb-1">Variables</p>
<div className="flex flex-wrap gap-1">
{template.variables.map((variable) => (
<span
key={variable}
className="inline-block px-2 py-1 bg-gray-100 border border-gray-300 rounded text-xs font-mono text-slate-600"
>
{`{{${variable}}}`}
</span>
))}
</div>
</div>
{/* Actions */}
<div className="flex gap-2 flex-wrap">
<button
onClick={() => handleEditClick(template)}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors min-h-[44px]"
>
<Edit fontSize="small" />
Edit
</button>
<button
onClick={() => handlePreviewClick(template)}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors min-h-[44px]"
>
<Visibility fontSize="small" />
Preview
</button>
<button
onClick={() => handleSendTestClick(template)}
disabled={sendTestMutation.isPending}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors disabled:bg-gray-300 disabled:text-gray-500 min-h-[44px]"
>
<Send fontSize="small" />
{sendTestMutation.isPending ? 'Sending...' : 'Send Test Email'}
</button>
</div>
</div>
</GlassCard>
))}
</div>
)}
</div>
</MobileContainer>
);
};
export default AdminEmailTemplatesMobileScreen;