api.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. import i18n from "../i18n";
  2. export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
  3. const getErrorMessage = async (response: Response, defaultMsg: string) => {
  4. try {
  5. const errorData = await response.json();
  6. if (errorData && errorData.detail) {
  7. if (Array.isArray(errorData.detail)) {
  8. // FastAPI/Pydantic validation error
  9. const firstError = errorData.detail[0];
  10. const errorType = firstError.type;
  11. const fieldName = firstError.loc[firstError.loc.length - 1];
  12. // Try to translate the error type using i18n
  13. // Pydantic types look like 'string_too_short', 'value_error.email'
  14. const translationKey = `errors.${errorType}`;
  15. const translatedMsg = i18n.global.t(translationKey, {
  16. ...firstError.ctx,
  17. defaultValue: firstError.msg
  18. });
  19. return `${fieldName}: ${translatedMsg}`;
  20. }
  21. return errorData.detail;
  22. }
  23. } catch (e) {}
  24. return defaultMsg;
  25. };
  26. export const getMaterials = async () => {
  27. const response = await fetch(`${API_BASE_URL}/materials?lang=${i18n.global.locale.value}`);
  28. if (!response.ok) {
  29. throw new Error('Failed to fetch materials');
  30. }
  31. return response.json();
  32. };
  33. export const getServices = async () => {
  34. const response = await fetch(`${API_BASE_URL}/services?lang=${i18n.global.locale.value}`);
  35. if (!response.ok) {
  36. throw new Error('Failed to fetch services');
  37. }
  38. return response.json();
  39. };
  40. export const uploadFilesToServer = async (data: FormData) => {
  41. const response = await fetch(`${API_BASE_URL}/files/upload`, {
  42. method: "POST",
  43. body: data,
  44. });
  45. if (!response.ok) throw new Error("Failed to upload files");
  46. return response.json();
  47. };
  48. export const submitOrder = async (orderData: FormData) => {
  49. const token = localStorage.getItem("token");
  50. const headers: Record<string, string> = {};
  51. if (token) {
  52. headers['Authorization'] = `Bearer ${token}`;
  53. }
  54. const response = await fetch(`${API_BASE_URL}/orders?lang=${i18n.global.locale.value}`, {
  55. method: 'POST',
  56. body: orderData,
  57. headers: headers,
  58. // Note: Do not set Content-Type header manually when using FormData
  59. // The browser will automatically set it with the correct boundary
  60. });
  61. if (!response.ok) {
  62. throw new Error(await getErrorMessage(response, 'Failed to submit order'));
  63. }
  64. return response.json();
  65. };
  66. export const getMyOrders = async (page = 1, size = 10) => {
  67. const token = localStorage.getItem("token");
  68. if (!token) return { orders: [], total: 0 };
  69. const query = new URLSearchParams({ page: page.toString(), size: size.toString() });
  70. const response = await fetch(`${API_BASE_URL}/orders/my?${query.toString()}&lang=${i18n.global.locale.value}`, {
  71. headers: {
  72. 'Authorization': `Bearer ${token}`
  73. }
  74. });
  75. if (!response.ok) {
  76. throw new Error('Failed to fetch orders');
  77. }
  78. return response.json();
  79. };
  80. export const registerUser = async (userData: any) => {
  81. const response = await fetch(`${API_BASE_URL}/auth/register?lang=${i18n.global.locale.value}`, {
  82. method: 'POST',
  83. headers: {
  84. 'Content-Type': 'application/json'
  85. },
  86. body: JSON.stringify(userData),
  87. });
  88. if (!response.ok) {
  89. throw new Error(await getErrorMessage(response, 'Failed to register'));
  90. }
  91. return response.json();
  92. };
  93. export const loginUser = async (userData: any) => {
  94. const response = await fetch(`${API_BASE_URL}/auth/login?lang=${i18n.global.locale.value}`, {
  95. method: 'POST',
  96. headers: {
  97. 'Content-Type': 'application/json'
  98. },
  99. body: JSON.stringify(userData),
  100. });
  101. if (!response.ok) {
  102. throw new Error(await getErrorMessage(response, 'Incorrect credentials'));
  103. }
  104. return response.json();
  105. };
  106. export const logoutUser = async () => {
  107. const token = localStorage.getItem("token");
  108. if (!token) return;
  109. await fetch(`${API_BASE_URL}/auth/logout`, {
  110. method: 'POST',
  111. headers: { 'Authorization': `Bearer ${token}` }
  112. });
  113. };
  114. export const socialLogin = async (socialData: any) => {
  115. const response = await fetch(`${API_BASE_URL}/auth/social-login?lang=${i18n.global.locale.value}`, {
  116. method: 'POST',
  117. headers: {
  118. 'Content-Type': 'application/json'
  119. },
  120. body: JSON.stringify(socialData),
  121. });
  122. if (!response.ok) {
  123. throw new Error(await getErrorMessage(response, 'Social login failed'));
  124. }
  125. return response.json();
  126. };
  127. export const getCurrentUser = async () => {
  128. const token = localStorage.getItem("token");
  129. if (!token) return null;
  130. const response = await fetch(`${API_BASE_URL}/auth/me?lang=${i18n.global.locale.value}`, {
  131. headers: {
  132. 'Authorization': `Bearer ${token}`
  133. }
  134. });
  135. if (!response.ok) {
  136. if (response.status === 401) {
  137. localStorage.removeItem("token");
  138. }
  139. return null;
  140. }
  141. return response.json();
  142. };
  143. export const updateProfile = async (userData: any) => {
  144. const token = localStorage.getItem("token");
  145. if (!token) throw new Error("No token found");
  146. const response = await fetch(`${API_BASE_URL}/auth/me?lang=${i18n.global.locale.value}`, {
  147. method: 'PUT',
  148. headers: {
  149. 'Content-Type': 'application/json',
  150. 'Authorization': `Bearer ${token}`
  151. },
  152. body: JSON.stringify(userData),
  153. });
  154. if (!response.ok) {
  155. throw new Error(await getErrorMessage(response, 'Failed to update profile'));
  156. }
  157. return response.json();
  158. };
  159. export const forgotPassword = async (email: string) => {
  160. const response = await fetch(`${API_BASE_URL}/auth/forgot-password?lang=${i18n.global.locale.value}`, {
  161. method: 'POST',
  162. headers: {
  163. 'Content-Type': 'application/json'
  164. },
  165. body: JSON.stringify({ email }),
  166. });
  167. if (!response.ok) {
  168. throw new Error(await getErrorMessage(response, 'Failed to request reset'));
  169. }
  170. return response.json();
  171. };
  172. export const resetPassword = async (data: any) => {
  173. const response = await fetch(`${API_BASE_URL}/auth/reset-password?lang=${i18n.global.locale.value}`, {
  174. method: 'POST',
  175. headers: {
  176. 'Content-Type': 'application/json'
  177. },
  178. body: JSON.stringify(data),
  179. });
  180. if (!response.ok) {
  181. throw new Error(await getErrorMessage(response, 'Failed to reset password'));
  182. }
  183. return response.json();
  184. };
  185. export const adminGetOrders = async (filters: { search?: string, status?: string, date_from?: string, date_to?: string } = {}) => {
  186. const token = localStorage.getItem("token");
  187. const query = new URLSearchParams();
  188. if (filters.search) query.append("search", filters.search);
  189. if (filters.status) query.append("status", filters.status);
  190. if (filters.date_from) query.append("date_from", filters.date_from);
  191. if (filters.date_to) query.append("date_to", filters.date_to);
  192. const response = await fetch(`${API_BASE_URL}/orders/admin/list?${query.toString()}&lang=${i18n.global.locale.value}`, {
  193. headers: { 'Authorization': `Bearer ${token}` }
  194. });
  195. if (!response.ok) throw new Error("Failed to fetch admin orders");
  196. return response.json();
  197. };
  198. export const adminUpdateOrder = async (orderId: number, data: any) => {
  199. const token = localStorage.getItem("token");
  200. const response = await fetch(`${API_BASE_URL}/orders/${orderId}?lang=${i18n.global.locale.value}`, {
  201. method: 'PATCH',
  202. headers: {
  203. 'Content-Type': 'application/json',
  204. 'Authorization': `Bearer ${token}`
  205. },
  206. body: JSON.stringify(data)
  207. });
  208. if (!response.ok) throw new Error("Failed to update order");
  209. return response.json();
  210. };
  211. export const adminDeleteOrder = async (orderId: number) => {
  212. const token = localStorage.getItem("token");
  213. const response = await fetch(`${API_BASE_URL}/orders/${orderId}/admin?lang=${i18n.global.locale.value}`, {
  214. method: 'DELETE',
  215. headers: { 'Authorization': `Bearer ${token}` }
  216. });
  217. if (!response.ok) throw new Error("Failed to delete order");
  218. return response.json();
  219. };
  220. export const getPriceEstimate = async (data: any) => {
  221. const response = await fetch(`${API_BASE_URL}/orders/estimate?lang=${i18n.global.locale.value}`, {
  222. method: 'POST',
  223. headers: { 'Content-Type': 'application/json' },
  224. body: JSON.stringify(data)
  225. });
  226. if (!response.ok) throw new Error("Failed to get estimate");
  227. return response.json();
  228. };
  229. export const adminGetMaterials = async () => {
  230. const token = localStorage.getItem("token");
  231. const response = await fetch(`${API_BASE_URL}/admin/materials?lang=${i18n.global.locale.value}`, {
  232. headers: { 'Authorization': `Bearer ${token}` }
  233. });
  234. if (!response.ok) throw new Error("Failed to fetch admin materials");
  235. return response.json();
  236. };
  237. export const adminCreateMaterial = async (data: any) => {
  238. const token = localStorage.getItem("token");
  239. const response = await fetch(`${API_BASE_URL}/admin/materials?lang=${i18n.global.locale.value}`, {
  240. method: 'POST',
  241. headers: {
  242. 'Content-Type': 'application/json',
  243. 'Authorization': `Bearer ${token}`
  244. },
  245. body: JSON.stringify(data)
  246. });
  247. if (!response.ok) throw new Error("Failed to create material");
  248. return response.json();
  249. };
  250. export const adminUpdateMaterial = async (id: number, data: any) => {
  251. const token = localStorage.getItem("token");
  252. const response = await fetch(`${API_BASE_URL}/admin/materials/${id}?lang=${i18n.global.locale.value}`, {
  253. method: 'PATCH',
  254. headers: {
  255. 'Content-Type': 'application/json',
  256. 'Authorization': `Bearer ${token}`
  257. },
  258. body: JSON.stringify(data)
  259. });
  260. if (!response.ok) throw new Error("Failed to update material");
  261. return response.json();
  262. };
  263. export const adminGetServices = async () => {
  264. const token = localStorage.getItem("token");
  265. const response = await fetch(`${API_BASE_URL}/admin/services?lang=${i18n.global.locale.value}`, {
  266. headers: { 'Authorization': `Bearer ${token}` }
  267. });
  268. if (!response.ok) throw new Error("Failed to fetch admin services");
  269. return response.json();
  270. };
  271. export const adminCreateService = async (data: any) => {
  272. const token = localStorage.getItem("token");
  273. const response = await fetch(`${API_BASE_URL}/admin/services?lang=${i18n.global.locale.value}`, {
  274. method: 'POST',
  275. headers: {
  276. 'Content-Type': 'application/json',
  277. 'Authorization': `Bearer ${token}`
  278. },
  279. body: JSON.stringify(data)
  280. });
  281. if (!response.ok) throw new Error("Failed to create service");
  282. return response.json();
  283. };
  284. export const adminUpdateService = async (id: number, data: any) => {
  285. const token = localStorage.getItem("token");
  286. const response = await fetch(`${API_BASE_URL}/admin/services/${id}?lang=${i18n.global.locale.value}`, {
  287. method: 'PATCH',
  288. headers: {
  289. 'Content-Type': 'application/json',
  290. 'Authorization': `Bearer ${token}`
  291. },
  292. body: JSON.stringify(data)
  293. });
  294. if (!response.ok) throw new Error("Failed to update service");
  295. return response.json();
  296. };
  297. export const adminDeleteMaterial = async (id: number) => {
  298. const token = localStorage.getItem("token");
  299. const response = await fetch(`${API_BASE_URL}/admin/materials/${id}?lang=${i18n.global.locale.value}`, {
  300. method: 'DELETE',
  301. headers: { 'Authorization': `Bearer ${token}` }
  302. });
  303. if (!response.ok) throw new Error("Failed to delete material");
  304. return response.json();
  305. };
  306. export const adminDeleteService = async (id: number) => {
  307. const token = localStorage.getItem("token");
  308. const response = await fetch(`${API_BASE_URL}/admin/services/${id}?lang=${i18n.global.locale.value}`, {
  309. method: 'DELETE',
  310. headers: { 'Authorization': `Bearer ${token}` }
  311. });
  312. if (!response.ok) throw new Error("Failed to delete service");
  313. return response.json();
  314. };
  315. export const adminUploadOrderPhoto = async (orderId: number, formData: FormData) => {
  316. const token = localStorage.getItem("token");
  317. const response = await fetch(`${API_BASE_URL}/admin/orders/${orderId}/photos?lang=${i18n.global.locale.value}`, {
  318. method: 'POST',
  319. headers: {
  320. 'Authorization': `Bearer ${token}`
  321. },
  322. body: formData
  323. });
  324. if (!response.ok) throw new Error("Failed to upload photo");
  325. return response.json();
  326. };
  327. export const adminDeletePhoto = async (photoId: number) => {
  328. const token = localStorage.getItem("token");
  329. const response = await fetch(`${API_BASE_URL}/admin/photos/${photoId}?lang=${i18n.global.locale.value}`, {
  330. method: 'DELETE',
  331. headers: { 'Authorization': `Bearer ${token}` }
  332. });
  333. if (!response.ok) throw new Error("Failed to delete photo");
  334. return response.json();
  335. };
  336. export const adminGetAllPhotos = async () => {
  337. const token = localStorage.getItem("token");
  338. const response = await fetch(`${API_BASE_URL}/admin/all-photos?lang=${i18n.global.locale.value}`, {
  339. headers: { 'Authorization': `Bearer ${token}` }
  340. });
  341. if (!response.ok) throw new Error("Failed to fetch all photos");
  342. return response.json();
  343. };
  344. export const adminAttachFile = async (orderId: number, formData: FormData) => {
  345. const token = localStorage.getItem("token");
  346. const response = await fetch(`${API_BASE_URL}/orders/${orderId}/attach-file?lang=${i18n.global.locale.value}`, {
  347. method: 'POST',
  348. headers: {
  349. 'Authorization': `Bearer ${token}`
  350. },
  351. body: formData
  352. });
  353. if (!response.ok) throw new Error("Failed to attach file");
  354. return response.json();
  355. };
  356. export const adminDeleteFile = async (orderId: number, fileId: number) => {
  357. const token = localStorage.getItem("token");
  358. const response = await fetch(`${API_BASE_URL}/orders/${orderId}/files/${fileId}?lang=${i18n.global.locale.value}`, {
  359. method: 'DELETE',
  360. headers: {
  361. 'Authorization': `Bearer ${token}`
  362. }
  363. });
  364. if (!response.ok) throw new Error("Failed to delete file");
  365. return response.json();
  366. };
  367. export const adminUpdatePhotoStatus = async (photoId: number, data: { is_public: boolean }) => {
  368. const token = localStorage.getItem("token");
  369. const response = await fetch(`${API_BASE_URL}/admin/photos/${photoId}?lang=${i18n.global.locale.value}`, {
  370. method: 'PATCH',
  371. headers: {
  372. 'Content-Type': 'application/json',
  373. 'Authorization': `Bearer ${token}`
  374. },
  375. body: JSON.stringify(data)
  376. });
  377. if (!response.ok) throw new Error("Failed to update photo status");
  378. return response.json();
  379. };
  380. export const getPortfolio = async () => {
  381. const response = await fetch(`${API_BASE_URL}/portfolio?lang=${i18n.global.locale.value}`);
  382. if (!response.ok) throw new Error("Failed to fetch portfolio");
  383. return response.json();
  384. };
  385. export const getOrderDetails = async (orderId: number) => {
  386. const response = await fetch(`${API_BASE_URL}/orders/${orderId}?lang=${i18n.global.locale.value}`);
  387. if (!response.ok) throw new Error("Failed to fetch order details");
  388. return response.json();
  389. };
  390. export const getOrderMessages = async (orderId: number, lang: string = i18n.global.locale.value) => {
  391. const token = localStorage.getItem("token");
  392. const response = await fetch(`${API_BASE_URL}/orders/${orderId}/messages?lang=${lang}`, {
  393. headers: { 'Authorization': `Bearer ${token}` }
  394. });
  395. if (!response.ok) throw new Error("Failed to fetch messages");
  396. return response.json();
  397. };
  398. export const sendOrderMessage = async (orderId: number, message: string, lang: string = i18n.global.locale.value) => {
  399. const token = localStorage.getItem("token");
  400. const response = await fetch(`${API_BASE_URL}/orders/${orderId}/messages?lang=${lang}`, {
  401. method: 'POST',
  402. headers: {
  403. 'Content-Type': 'application/json',
  404. 'Authorization': `Bearer ${token}`
  405. },
  406. body: JSON.stringify({ message })
  407. });
  408. if (response.status === 429) {
  409. const err = await response.json();
  410. throw new Error(err.detail || "Rate limit exceeded");
  411. }
  412. if (!response.ok) throw new Error("Failed to send message");
  413. return response.json();
  414. };
  415. export const authPing = async () => {
  416. const token = localStorage.getItem("token");
  417. if (!token) return null;
  418. try {
  419. const response = await fetch(`${API_BASE_URL}/auth/ping?lang=${i18n.global.locale.value}`, {
  420. method: "POST",
  421. headers: { "Authorization": `Bearer ${token}` }
  422. });
  423. if (response.ok) {
  424. return response.json();
  425. }
  426. } catch (e) {
  427. // silently fail
  428. }
  429. return null;
  430. };
  431. // Blog API
  432. export const getBlogPosts = async (publishedOnly: boolean = true) => {
  433. const response = await fetch(`${API_BASE_URL}/blog?published_only=${publishedOnly}&lang=${i18n.global.locale.value}`);
  434. if (!response.ok) throw new Error("Failed to fetch blog posts");
  435. return response.json();
  436. };
  437. export const getBlogPost = async (idOrSlug: string) => {
  438. const response = await fetch(`${API_BASE_URL}/blog/${idOrSlug}?lang=${i18n.global.locale.value}`);
  439. if (!response.ok) throw new Error("Failed to fetch blog post");
  440. return response.json();
  441. };
  442. export const adminCreatePost = async (data: any) => {
  443. const token = localStorage.getItem("token");
  444. const response = await fetch(`${API_BASE_URL}/blog?lang=${i18n.global.locale.value}`, {
  445. method: 'POST',
  446. headers: {
  447. 'Content-Type': 'application/json',
  448. 'Authorization': `Bearer ${token}`
  449. },
  450. body: JSON.stringify(data)
  451. });
  452. if (!response.ok) throw new Error("Failed to create blog post");
  453. return response.json();
  454. };
  455. export const adminUpdatePost = async (id: number, data: any) => {
  456. const token = localStorage.getItem("token");
  457. const response = await fetch(`${API_BASE_URL}/blog/${id}?lang=${i18n.global.locale.value}`, {
  458. method: 'PUT',
  459. headers: {
  460. 'Content-Type': 'application/json',
  461. 'Authorization': `Bearer ${token}`
  462. },
  463. body: JSON.stringify(data)
  464. });
  465. if (!response.ok) throw new Error("Failed to update blog post");
  466. return response.json();
  467. };
  468. export const adminDeletePost = async (id: number) => {
  469. const token = localStorage.getItem("token");
  470. const response = await fetch(`${API_BASE_URL}/blog/${id}?lang=${i18n.global.locale.value}`, {
  471. method: 'DELETE',
  472. headers: { 'Authorization': `Bearer ${token}` }
  473. });
  474. if (!response.ok) throw new Error("Failed to delete blog post");
  475. return response.json();
  476. };
  477. export const adminGetUsers = async (page = 1, size = 50, search = "") => {
  478. const token = localStorage.getItem("token");
  479. const query = new URLSearchParams({ page: page.toString(), size: size.toString(), search });
  480. const response = await fetch(`${API_BASE_URL}/auth/admin/users?${query.toString()}`, {
  481. headers: { 'Authorization': `Bearer ${token}` }
  482. });
  483. if (!response.ok) throw new Error("Failed to fetch users");
  484. return response.json();
  485. };
  486. export const adminCreateUser = async (data: any) => {
  487. const token = localStorage.getItem("token");
  488. const response = await fetch(`${API_BASE_URL}/auth/admin/users`, {
  489. method: 'POST',
  490. headers: {
  491. 'Content-Type': 'application/json',
  492. 'Authorization': `Bearer ${token}`
  493. },
  494. body: JSON.stringify(data)
  495. });
  496. if (!response.ok) {
  497. const err = await response.json();
  498. throw new Error(err.detail || "Failed to create user");
  499. }
  500. return response.json();
  501. };
  502. export const adminUpdateUser = async (userId: number, data: any) => {
  503. const token = localStorage.getItem("token");
  504. const response = await fetch(`${API_BASE_URL}/auth/users/${userId}/admin?lang=${i18n.global.locale.value}`, {
  505. method: 'PATCH',
  506. headers: {
  507. 'Content-Type': 'application/json',
  508. 'Authorization': `Bearer ${token}`
  509. },
  510. body: JSON.stringify(data)
  511. });
  512. if (!response.ok) throw new Error("Failed to update user");
  513. return response.json();
  514. };
  515. export const adminGetAuditLogs = async (page = 1, size = 50, action = "") => {
  516. const token = localStorage.getItem("token");
  517. const query = new URLSearchParams({ page: page.toString(), size: size.toString() });
  518. if (action) query.append("action", action);
  519. const response = await fetch(`${API_BASE_URL}/admin/audit-logs?${query.toString()}`, {
  520. headers: { 'Authorization': `Bearer ${token}` }
  521. });
  522. if (!response.ok) throw new Error("Failed to fetch audit logs");
  523. return response.json();
  524. };
  525. export const adminGetOrderItems = async (orderId: number) => {
  526. const token = localStorage.getItem("token");
  527. const response = await fetch(`${API_BASE_URL}/orders/${orderId}/items?lang=${i18n.global.locale.value}`, {
  528. headers: { 'Authorization': `Bearer ${token}` }
  529. });
  530. if (!response.ok) throw new Error("Failed to fetch order items");
  531. return response.json();
  532. };
  533. export const adminUpdateOrderItems = async (orderId: number, items: any[]) => {
  534. const token = localStorage.getItem("token");
  535. const response = await fetch(`${API_BASE_URL}/orders/${orderId}/items?lang=${i18n.global.locale.value}`, {
  536. method: 'PUT',
  537. headers: {
  538. 'Content-Type': 'application/json',
  539. 'Authorization': `Bearer ${token}`
  540. },
  541. body: JSON.stringify(items)
  542. });
  543. if (!response.ok) throw new Error("Failed to update order items");
  544. return response.json();
  545. };