NotificationsPage.tsx 12 KB

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