NotificationsPage.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { useState, useEffect } from 'react';
  2. import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
  3. import { useNavigate } from 'react-router-dom';
  4. import { useTranslation } from 'react-i18next';
  5. import { Bell, CheckCircle2, Loader2, Mail, Save } from 'lucide-react';
  6. import { api } from '../api/client';
  7. import { useAuth } from '../contexts/AuthContext';
  8. import { useToast } from '../contexts/ToastContext';
  9. import { Button } from '../components/Button';
  10. import { Card, CardContent, CardHeader } from '../components/Card';
  11. import { useUiSettings } from '../hooks/useUiSettings';
  12. export function NotificationsPage() {
  13. const { t } = useTranslation();
  14. const { user } = useAuth();
  15. const { showToast } = useToast();
  16. const queryClient = useQueryClient();
  17. const navigate = useNavigate();
  18. const [notifyPrintStart, setNotifyPrintStart] = useState(true);
  19. const [notifyPrintComplete, setNotifyPrintComplete] = useState(true);
  20. const [notifyPrintFailed, setNotifyPrintFailed] = useState(true);
  21. const [notifyPrintStopped, setNotifyPrintStopped] = useState(true);
  22. const [isDirty, setIsDirty] = useState(false);
  23. // Check advanced auth status - redirect if disabled
  24. const { data: advancedAuthStatus, isLoading: isAdvancedAuthLoading } = useQuery({
  25. queryKey: ['advancedAuthStatus'],
  26. queryFn: api.getAdvancedAuthStatus,
  27. staleTime: 5 * 60 * 1000, // 5 minutes
  28. });
  29. const { data: settings, isLoading: isSettingsLoading } = useUiSettings({ staleTime: 5 * 60 * 1000 });
  30. // Fetch current preferences
  31. const { data: preferences, isLoading } = useQuery({
  32. queryKey: ['user-email-preferences'],
  33. queryFn: () => api.getUserEmailPreferences(),
  34. });
  35. // Redirect to settings if Advanced Auth is disabled
  36. useEffect(() => {
  37. if ((advancedAuthStatus && !advancedAuthStatus.advanced_auth_enabled) || (settings && !settings.user_notifications_enabled)) {
  38. navigate('/settings', { replace: true });
  39. }
  40. }, [advancedAuthStatus, settings, navigate]);
  41. // Populate form when preferences load
  42. useEffect(() => {
  43. if (preferences) {
  44. setNotifyPrintStart(preferences.notify_print_start);
  45. setNotifyPrintComplete(preferences.notify_print_complete);
  46. setNotifyPrintFailed(preferences.notify_print_failed);
  47. setNotifyPrintStopped(preferences.notify_print_stopped);
  48. setIsDirty(false);
  49. }
  50. }, [preferences]);
  51. // Save preferences
  52. const saveMutation = useMutation({
  53. mutationFn: () =>
  54. api.updateUserEmailPreferences({
  55. notify_print_start: notifyPrintStart,
  56. notify_print_complete: notifyPrintComplete,
  57. notify_print_failed: notifyPrintFailed,
  58. notify_print_stopped: notifyPrintStopped,
  59. }),
  60. onSuccess: () => {
  61. queryClient.invalidateQueries({ queryKey: ['user-email-preferences'] });
  62. setIsDirty(false);
  63. showToast(t('notifications.userEmail.saveSuccess'), 'success');
  64. },
  65. onError: (err: Error) => {
  66. showToast(err.message || t('notifications.userEmail.saveError'), 'error');
  67. },
  68. });
  69. const handleToggle = (
  70. setter: React.Dispatch<React.SetStateAction<boolean>>,
  71. value: boolean
  72. ) => {
  73. setter(!value);
  74. setIsDirty(true);
  75. };
  76. if (isLoading || isAdvancedAuthLoading || isSettingsLoading) {
  77. return (
  78. <div className="flex items-center justify-center h-64">
  79. <Loader2 className="w-8 h-8 animate-spin text-bambu-green" />
  80. </div>
  81. );
  82. }
  83. return (
  84. <div className="p-4 md:p-6 max-w-2xl mx-auto">
  85. <div className="flex items-center gap-3 mb-6">
  86. <Bell className="w-7 h-7 text-bambu-green" />
  87. <h1 className="text-2xl font-bold text-white">{t('notifications.userEmail.title')}</h1>
  88. </div>
  89. {/* Info card */}
  90. <Card className="mb-6 border-blue-300 bg-blue-50 dark:border-blue-500/30 dark:bg-blue-500/5">
  91. <CardContent className="py-4">
  92. <div className="flex items-start gap-3">
  93. <div className="w-10 h-10 rounded-full flex items-center justify-center bg-blue-100 dark:bg-blue-500/20 flex-shrink-0">
  94. <Mail className="w-5 h-5 text-blue-600 dark:text-blue-400" />
  95. </div>
  96. <div>
  97. <h3 className="text-white font-medium">{t('notifications.userEmail.emailNotifications')}</h3>
  98. <p className="text-sm text-bambu-gray mt-1">
  99. {t('notifications.userEmail.emailNotificationsDesc')}
  100. </p>
  101. {user?.email ? (
  102. <p className="text-sm text-blue-700 dark:text-blue-400 mt-2">
  103. {t('notifications.userEmail.sendingTo')}: <strong>{user.email}</strong>
  104. </p>
  105. ) : (
  106. <p className="text-sm text-yellow-700 dark:text-yellow-400 mt-2">
  107. {t('notifications.userEmail.noEmailWarning')}
  108. </p>
  109. )}
  110. </div>
  111. </div>
  112. </CardContent>
  113. </Card>
  114. {/* Preferences card */}
  115. <Card className="mb-6">
  116. <CardHeader>
  117. <h2 className="text-lg font-semibold text-white">{t('notifications.userEmail.printJobNotifications')}</h2>
  118. <p className="text-sm text-bambu-gray mt-1">{t('notifications.userEmail.printJobNotificationsDesc')}</p>
  119. </CardHeader>
  120. <CardContent className="space-y-4">
  121. {/* Print Job Starts */}
  122. <div className="flex items-center justify-between p-4 bg-bambu-dark rounded-lg">
  123. <div className="flex items-center gap-3">
  124. <div className={`w-10 h-10 rounded-full flex items-center justify-center ${notifyPrintStart ? 'bg-bambu-green/20' : 'bg-bambu-dark-tertiary'}`}>
  125. <CheckCircle2 className={`w-5 h-5 ${notifyPrintStart ? 'text-bambu-green' : 'text-bambu-gray'}`} />
  126. </div>
  127. <div>
  128. <p className="text-white font-medium">{t('notifications.userEmail.printJobStarts')}</p>
  129. <p className="text-sm text-bambu-gray">{t('notifications.userEmail.printJobStartsDesc')}</p>
  130. </div>
  131. </div>
  132. <button
  133. onClick={() => handleToggle(setNotifyPrintStart, notifyPrintStart)}
  134. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${
  135. notifyPrintStart ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
  136. }`}
  137. role="switch"
  138. aria-checked={notifyPrintStart}
  139. >
  140. <span
  141. className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
  142. notifyPrintStart ? 'translate-x-6' : 'translate-x-1'
  143. }`}
  144. />
  145. </button>
  146. </div>
  147. {/* Print Job Finishes */}
  148. <div className="flex items-center justify-between p-4 bg-bambu-dark rounded-lg">
  149. <div className="flex items-center gap-3">
  150. <div className={`w-10 h-10 rounded-full flex items-center justify-center ${notifyPrintComplete ? 'bg-bambu-green/20' : 'bg-bambu-dark-tertiary'}`}>
  151. <CheckCircle2 className={`w-5 h-5 ${notifyPrintComplete ? 'text-bambu-green' : 'text-bambu-gray'}`} />
  152. </div>
  153. <div>
  154. <p className="text-white font-medium">{t('notifications.userEmail.printJobFinishes')}</p>
  155. <p className="text-sm text-bambu-gray">{t('notifications.userEmail.printJobFinishesDesc')}</p>
  156. </div>
  157. </div>
  158. <button
  159. onClick={() => handleToggle(setNotifyPrintComplete, notifyPrintComplete)}
  160. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${
  161. notifyPrintComplete ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
  162. }`}
  163. role="switch"
  164. aria-checked={notifyPrintComplete}
  165. >
  166. <span
  167. className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
  168. notifyPrintComplete ? 'translate-x-6' : 'translate-x-1'
  169. }`}
  170. />
  171. </button>
  172. </div>
  173. {/* Print Errors */}
  174. <div className="flex items-center justify-between p-4 bg-bambu-dark rounded-lg">
  175. <div className="flex items-center gap-3">
  176. <div className={`w-10 h-10 rounded-full flex items-center justify-center ${notifyPrintFailed ? 'bg-bambu-green/20' : 'bg-bambu-dark-tertiary'}`}>
  177. <CheckCircle2 className={`w-5 h-5 ${notifyPrintFailed ? 'text-bambu-green' : 'text-bambu-gray'}`} />
  178. </div>
  179. <div>
  180. <p className="text-white font-medium">{t('notifications.userEmail.printErrors')}</p>
  181. <p className="text-sm text-bambu-gray">{t('notifications.userEmail.printErrorsDesc')}</p>
  182. </div>
  183. </div>
  184. <button
  185. onClick={() => handleToggle(setNotifyPrintFailed, notifyPrintFailed)}
  186. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${
  187. notifyPrintFailed ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
  188. }`}
  189. role="switch"
  190. aria-checked={notifyPrintFailed}
  191. >
  192. <span
  193. className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
  194. notifyPrintFailed ? 'translate-x-6' : 'translate-x-1'
  195. }`}
  196. />
  197. </button>
  198. </div>
  199. {/* Print Job Stops */}
  200. <div className="flex items-center justify-between p-4 bg-bambu-dark rounded-lg">
  201. <div className="flex items-center gap-3">
  202. <div className={`w-10 h-10 rounded-full flex items-center justify-center ${notifyPrintStopped ? 'bg-bambu-green/20' : 'bg-bambu-dark-tertiary'}`}>
  203. <CheckCircle2 className={`w-5 h-5 ${notifyPrintStopped ? 'text-bambu-green' : 'text-bambu-gray'}`} />
  204. </div>
  205. <div>
  206. <p className="text-white font-medium">{t('notifications.userEmail.printJobStops')}</p>
  207. <p className="text-sm text-bambu-gray">{t('notifications.userEmail.printJobStopsDesc')}</p>
  208. </div>
  209. </div>
  210. <button
  211. onClick={() => handleToggle(setNotifyPrintStopped, notifyPrintStopped)}
  212. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${
  213. notifyPrintStopped ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
  214. }`}
  215. role="switch"
  216. aria-checked={notifyPrintStopped}
  217. >
  218. <span
  219. className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
  220. notifyPrintStopped ? 'translate-x-6' : 'translate-x-1'
  221. }`}
  222. />
  223. </button>
  224. </div>
  225. </CardContent>
  226. </Card>
  227. {/* Save button */}
  228. <div className="flex justify-end">
  229. <Button
  230. onClick={() => saveMutation.mutate()}
  231. disabled={!isDirty || saveMutation.isPending || !user?.email}
  232. >
  233. {saveMutation.isPending ? (
  234. <>
  235. <Loader2 className="w-4 h-4 animate-spin" />
  236. {t('common.saving')}
  237. </>
  238. ) : (
  239. <>
  240. <Save className="w-4 h-4" />
  241. {t('common.save')}
  242. </>
  243. )}
  244. </Button>
  245. </div>
  246. </div>
  247. );
  248. }