Product.svelte 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. <script>
  2. import { onMount } from 'svelte';
  3. import { browser } from '$app/environment';
  4. // Assets paths for static files
  5. const add_icon = '/assets/add_icon.svg';
  6. const save_icon = '/assets/save_icon.svg';
  7. const trash_icon = '/assets/trash_icon.svg';
  8. const edit_icon = '/assets/edit_icon.svg';
  9. const check_icon = '/assets/check_icon.svg';
  10. const cancel_icon = '/assets/cancel_icon.svg';
  11. let token = null;
  12. let company = null;
  13. const API = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, '');
  14. if (browser) {
  15. token = localStorage.getItem('token');
  16. company = Number(localStorage.getItem('company'));
  17. localStorage.setItem('typePage', 'false');
  18. }
  19. let products = [];
  20. let categories = [];
  21. let noCategory = false;
  22. let isAddingProduct = false;
  23. let editingProductId = null;
  24. let isEditingProduct = false;
  25. let productFormData = {
  26. name: '',
  27. category: '',
  28. price: 0,
  29. sendToKitchen: false
  30. };
  31. let associatedProduct = false;
  32. let isAddingCategory = false;
  33. let newCategoryName = '';
  34. let selectedCategoryFilter = '';
  35. $: filteredProducts = selectedCategoryFilter
  36. ? products.filter((p) => p.category === selectedCategoryFilter)
  37. : products;
  38. function resetProductForm() {
  39. productFormData = {
  40. name: '',
  41. description: '',
  42. category: categories[0]?.name ?? '',
  43. price: 0,
  44. sendToKitchen: false
  45. };
  46. editingProductId = null;
  47. isAddingProduct = false;
  48. isEditingProduct = false;
  49. }
  50. function handleProductSubmit(event) {
  51. event.preventDefault();
  52. if (!productFormData.name || !productFormData.category || productFormData.price <= 0) {
  53. console.error('Preencha todos os campos corretamente');
  54. return;
  55. }
  56. const categoryObj = categories.find((c) => c.name === productFormData.category);
  57. if (!categoryObj) {
  58. console.error('Categoria inválida');
  59. return;
  60. }
  61. const payload = {
  62. product_name: productFormData.name,
  63. product_price: Number(productFormData.price),
  64. category_id: categoryObj.id,
  65. company_id: company,
  66. product_is_kitchen: productFormData.sendToKitchen
  67. };
  68. const requestOptions = {
  69. method: 'POST',
  70. headers: {
  71. 'Content-Type': 'application/json',
  72. Authorization: `Bearer ${token}`
  73. },
  74. body: JSON.stringify(payload),
  75. redirect: 'follow'
  76. };
  77. fetch(`${API}/product/create`, requestOptions)
  78. .then((response) => response.json())
  79. .then((res) => {
  80. if (res.status === 'ok') {
  81. const productId = res.data?.product_id;
  82. if (!productId) {
  83. console.error('Produto criado, mas ID não retornado.');
  84. return;
  85. }
  86. // Chamada para adicionar a descrição
  87. const descPayload = {
  88. product_id: productId,
  89. company_id: company,
  90. description_text: productFormData.description
  91. };
  92. fetch(`${API}/description/create`, {
  93. method: 'POST',
  94. headers: {
  95. 'Content-Type': 'application/json',
  96. Authorization: `Bearer ${token}`
  97. },
  98. body: JSON.stringify(descPayload),
  99. redirect: 'follow'
  100. })
  101. .then((descRes) => descRes.json())
  102. .then((descResult) => {
  103. if (descResult.status === 'ok') {
  104. console.log('Descrição adicionada com sucesso!');
  105. } else {
  106. console.error('Erro ao adicionar descrição:', descResult.msg);
  107. }
  108. })
  109. .catch((error) => console.error('Erro na requisição de descrição:', error));
  110. const newProduct = {
  111. id: productId,
  112. ...productFormData
  113. };
  114. products = [...products, newProduct];
  115. //console.log('Produto criado com sucesso!');
  116. resetProductForm();
  117. fetchAllItems();
  118. } else {
  119. console.error('Erro ao criar produto:', res.msg);
  120. }
  121. })
  122. .catch((error) => {
  123. console.error('Erro na requisição:', error);
  124. });
  125. }
  126. function handleEditProduct(product) {
  127. productFormData = {
  128. name: product.name,
  129. category: product.category,
  130. price: product.price,
  131. sendToKitchen: product.sendToKitchen
  132. };
  133. editingProductId = product.id;
  134. isEditingProduct = true;
  135. }
  136. async function handleUpdateProduct(event) {
  137. event.preventDefault();
  138. token = localStorage.getItem('token');
  139. company = Number(localStorage.getItem('company'));
  140. const myHeaders = new Headers();
  141. myHeaders.append('Authorization', `Bearer ${token}`);
  142. myHeaders.append('Content-Type', 'application/json');
  143. const raw = JSON.stringify({
  144. update_product_id: editingProductId,
  145. product_name: productFormData.name,
  146. product_price: Number(productFormData.price),
  147. product_is_kitchen: productFormData.sendToKitchen,
  148. company_id: company
  149. });
  150. const requestOptions = {
  151. method: 'POST',
  152. headers: myHeaders,
  153. body: raw,
  154. redirect: 'follow'
  155. };
  156. try {
  157. const response = await fetch(
  158. `${API}/product/update`,
  159. requestOptions
  160. );
  161. const result = await response.json();
  162. if (result.status === 'ok' && productFormData.description != '') {
  163. // Atualizar a descrição após o produto
  164. const descPayload = {
  165. product_id: editingProductId,
  166. company_id: company,
  167. description_text: productFormData.description
  168. };
  169. try {
  170. const descRes = await fetch(`${API}/description/update`, {
  171. method: 'POST',
  172. headers: {
  173. 'Content-Type': 'application/json',
  174. Authorization: `Bearer ${token}`
  175. },
  176. body: JSON.stringify(descPayload),
  177. redirect: 'follow'
  178. });
  179. const descResult = await descRes.json();
  180. if (descResult.status === 'ok') {
  181. console.log('Descrição atualizada com sucesso!');
  182. } else {
  183. console.error('Erro ao atualizar descrição:', descResult.msg);
  184. }
  185. } catch (error) {
  186. console.error(error);
  187. }
  188. isEditingProduct = false;
  189. editingProductId = null;
  190. fetchAllItems(); // para recarregar lista
  191. } else {
  192. console.error('Erro ao atualizar produto:', result.msg || result);
  193. }
  194. isEditingProduct = false;
  195. } catch (error) {
  196. console.error('Erro na requisição de atualização:', error);
  197. }
  198. }
  199. function handleCategorySubmit(event) {
  200. event.preventDefault();
  201. if (!newCategoryName) {
  202. console.error('Digite o nome da categoria');
  203. return;
  204. }
  205. if (categories.some((c) => c.name === newCategoryName)) {
  206. console.error('Essa categoria já existe');
  207. return;
  208. }
  209. const requestOptions = {
  210. method: 'POST',
  211. headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
  212. body: JSON.stringify({
  213. category_name: newCategoryName,
  214. category_is_kitchen: false,
  215. company_id: company
  216. }),
  217. redirect: 'follow'
  218. };
  219. fetch(`${API}/category/create`, requestOptions)
  220. .then((response) => response.json())
  221. .then((res) => {
  222. if (res.status === 'ok') {
  223. categories = [...categories, { name: newCategoryName }];
  224. //console.log('Categoria criada com sucesso');
  225. newCategoryName = '';
  226. isAddingCategory = false;
  227. fetchAllItems();
  228. } else {
  229. console.error('Erro ao criar categoria:', res.msg);
  230. }
  231. })
  232. .catch((error) => {
  233. console.error('Erro na requisição:', error);
  234. });
  235. }
  236. function handleDeleteCategory(name) {
  237. const hasAssociatedProducts = products.some((p) => p.category === name);
  238. if (hasAssociatedProducts) {
  239. associatedProduct = true;
  240. return;
  241. }
  242. fetch(`${API}/category/delete`, {
  243. method: 'POST',
  244. headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
  245. body: JSON.stringify({
  246. category_name: name,
  247. company_id: company
  248. }),
  249. redirect: 'follow'
  250. })
  251. .then((response) => response.json())
  252. .then((res) => {
  253. if (res.status === 'ok') {
  254. categories = categories.filter((c) => c.name !== name);
  255. if (selectedCategoryFilter === name) selectedCategoryFilter = '';
  256. console.log('Categoria deletada com sucesso!');
  257. } else {
  258. console.error('Erro ao deletar categoria:', res.msg);
  259. }
  260. })
  261. .catch((error) => {
  262. console.error('Erro na requisição:', error);
  263. });
  264. }
  265. function handleDeleteProduct(productName) {
  266. const requestOptions = {
  267. method: 'POST',
  268. headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
  269. body: JSON.stringify({
  270. product_name: productName,
  271. company_id: company
  272. }),
  273. redirect: 'follow'
  274. };
  275. fetch(`${API}/product/delete`, requestOptions)
  276. .then((response) => response.json())
  277. .then((res) => {
  278. if (res.status === 'ok') {
  279. products = products.filter((p) => p.name !== productName);
  280. //console.log('Produto deletado com sucesso!');
  281. } else {
  282. console.error('Erro ao deletar produto:', res.msg);
  283. }
  284. })
  285. .catch((error) => {
  286. console.error('Erro na requisição:', error);
  287. });
  288. }
  289. function fetchAllItems() {
  290. const requestOptions = {
  291. method: 'POST',
  292. headers: {
  293. 'Content-Type': 'application/json',
  294. Authorization: `Bearer ${token}`
  295. },
  296. redirect: 'follow',
  297. body: JSON.stringify({ company_id: company })
  298. };
  299. fetch(`${API}/category/get`, requestOptions)
  300. .then((response) => response.json())
  301. .then((res) => {
  302. if (res.status === 'ok') {
  303. categories = res.data.map((categorie) => ({
  304. id: categorie.category_id,
  305. name: categorie.category_name
  306. }));
  307. return fetch(`${API}/product/get`, requestOptions);
  308. } else {
  309. throw new Error(`Erro ao carregar categorias: ${res.msg}`);
  310. }
  311. })
  312. .then((response) => response.json())
  313. .then((res) => {
  314. if (res.status === 'ok') {
  315. //console.log(res);
  316. products = res.data.map((item) => {
  317. const category = categories.find((c) => c.id === item.category_id);
  318. return {
  319. id: item.product_id,
  320. name: item.product_name,
  321. category: category?.name || 'Sem categoria',
  322. price: Number(item.product_price),
  323. sendToKitchen: item.product_is_kitchen
  324. };
  325. });
  326. } else {
  327. console.error('Erro ao carregar produtos:', res.msg);
  328. }
  329. })
  330. .catch((error) => {
  331. console.error('Erro ao buscar dados:', error);
  332. });
  333. }
  334. onMount(() => {
  335. fetchAllItems();
  336. });
  337. </script>
  338. <div class="container mx-auto">
  339. <div class="flex flex-col">
  340. <h1 class="mb-6 text-2xl font-bold">Gerenciar Produtos</h1>
  341. <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
  342. <!-- Categories -->
  343. <div class="overflow-hidden rounded-lg bg-gray-800 shadow-lg">
  344. <div class="flex items-center justify-between bg-gray-700 p-4">
  345. <h2 class="text-lg font-semibold">Categorias</h2>
  346. <button
  347. on:click={() => (isAddingCategory = true)}
  348. class="rounded-lg bg-emerald-600 p-1.5 hover:bg-emerald-700"
  349. >
  350. <img src={add_icon} alt="Adicionar" class="h-4 w-4" />
  351. </button>
  352. </div>
  353. {#if isAddingCategory}
  354. <form on:submit={handleCategorySubmit} class="space-y-4 p-4">
  355. <div>
  356. <p class="mb-1 block text-sm text-gray-400">Nome da Categoria</p>
  357. <input
  358. bind:value={newCategoryName}
  359. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  360. placeholder="Ex: Bebidas, Porções..."
  361. />
  362. </div>
  363. <div class="flex space-x-2">
  364. <button
  365. type="button"
  366. on:click={() => {
  367. isAddingCategory = false;
  368. newCategoryName = '';
  369. }}
  370. class="rounded-lg bg-gray-700 px-4 py-2 hover:bg-gray-600"
  371. >
  372. Cancelar
  373. </button>
  374. <button
  375. type="submit"
  376. class="flex items-center rounded-lg bg-emerald-600 px-4 py-2 hover:bg-emerald-700"
  377. >
  378. <img src={save_icon} alt="Salvar" class="mr-2 h-4 w-4" /> Salvar
  379. </button>
  380. </div>
  381. </form>
  382. {:else}
  383. <div class="divide-y divide-gray-700">
  384. {#if categories.length === 0}
  385. <div class="p-4 text-center text-gray-400">Nenhuma categoria cadastrada</div>
  386. {:else}
  387. {#each categories as category}
  388. <div
  389. class="flex cursor-pointer items-center justify-between p-4 hover:bg-gray-900"
  390. on:click={() => (selectedCategoryFilter = category.name)}
  391. >
  392. <span>{category.name}</span>
  393. <button
  394. on:click|preventDefault|stopPropagation={() =>
  395. handleDeleteCategory(category.name)}
  396. class="rounded-lg p-1.5 text-red-400 hover:bg-red-900/20"
  397. >
  398. <img src={trash_icon} alt="Deletar" class="h-4 w-4" />
  399. </button>
  400. </div>
  401. {/each}
  402. {#if selectedCategoryFilter}
  403. <div class="p-4">
  404. <button
  405. on:click={() => (selectedCategoryFilter = '')}
  406. class="w-full rounded-md bg-red-600 py-1 text-sm hover:bg-red-700"
  407. >
  408. Remover Filtro
  409. </button>
  410. </div>
  411. {/if}
  412. {#if associatedProduct}
  413. <div class="p-4">
  414. <p class="w-full rounded-md bg-yellow-600 p-1 text-sm">
  415. Delete os produtos associados a esta categoria
  416. </p>
  417. </div>
  418. {/if}
  419. {/if}
  420. </div>
  421. {/if}
  422. </div>
  423. <!-- Products -->
  424. <div class="overflow-hidden rounded-lg bg-gray-800 shadow-lg lg:col-span-2">
  425. <div class="flex items-center justify-between bg-gray-700 p-4">
  426. <h2 class="text-lg font-semibold">Produtos</h2>
  427. {#if noCategory}
  428. <div>
  429. <p class="w-full rounded-md bg-yellow-600 p-1 text-sm">Primeiro crie uma categoria</p>
  430. </div>
  431. {/if}
  432. <button
  433. on:click={() => {
  434. if (!categories.length) {
  435. noCategory = true;
  436. return;
  437. } else {
  438. noCategory = false;
  439. resetProductForm();
  440. isAddingProduct = true;
  441. }
  442. }}
  443. class="rounded-lg bg-emerald-600 p-1.5 hover:bg-emerald-700"
  444. >
  445. <img src={add_icon} alt="Adicionar" class="h-4 w-4" />
  446. </button>
  447. </div>
  448. {#if isAddingProduct}
  449. <form on:submit={handleProductSubmit} class="space-y-4 p-4">
  450. <div>
  451. <p class="mb-1 block text-sm text-gray-400">Nome do Produto</p>
  452. <input
  453. bind:value={productFormData.name}
  454. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  455. placeholder="Ex: Cerveja..."
  456. />
  457. </div>
  458. <div>
  459. <p class="mb-1 block text-sm text-gray-400">Descrição do Produto</p>
  460. <input
  461. bind:value={productFormData.description}
  462. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  463. placeholder="Descrição"
  464. />
  465. </div>
  466. <div>
  467. <p class="mb-1 block text-sm text-gray-400">Categoria</p>
  468. <select
  469. bind:value={productFormData.category}
  470. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  471. >
  472. {#each categories as category}
  473. <option value={category.name}>{category.name}</option>
  474. {/each}
  475. </select>
  476. </div>
  477. <div>
  478. <p class="mb-1 block text-sm text-gray-400">Preço (R$)</p>
  479. <input
  480. type="number"
  481. min="0"
  482. step="0.01"
  483. bind:value={productFormData.price}
  484. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  485. placeholder="0.00"
  486. />
  487. </div>
  488. <div class="flex items-center">
  489. <input
  490. id="sendToKitchen"
  491. type="checkbox"
  492. bind:checked={productFormData.sendToKitchen}
  493. class="h-4 w-4 rounded border border-gray-600 bg-gray-700"
  494. />
  495. <label for="sendToKitchen" class="ml-2 text-sm text-gray-300"
  496. >Enviar para a cozinha</label
  497. >
  498. </div>
  499. <div class="flex space-x-2">
  500. <button
  501. type="button"
  502. on:click={resetProductForm}
  503. class="rounded-lg bg-gray-700 px-4 py-2 hover:bg-gray-600"
  504. >
  505. Cancelar
  506. </button>
  507. <button
  508. type="submit"
  509. class="flex items-center rounded-lg bg-emerald-600 px-4 py-2 hover:bg-emerald-700"
  510. >
  511. <img src={save_icon} alt="Salvar" class="mr-2 h-4 w-4" /> Salvar
  512. </button>
  513. </div>
  514. </form>
  515. {:else if isEditingProduct}
  516. <form on:submit={handleUpdateProduct} class="space-y-4 p-4">
  517. <div>
  518. <p class="mb-1 block text-sm text-gray-400">Nome do Produto</p>
  519. <input
  520. bind:value={productFormData.name}
  521. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  522. placeholder="Ex: Cerveja..."
  523. />
  524. </div>
  525. <div>
  526. <p class="mb-1 block text-sm text-gray-400">Descrição do Produto</p>
  527. <input
  528. bind:value={productFormData.description}
  529. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  530. placeholder="Descrição"
  531. />
  532. </div>
  533. <div>
  534. <p class="mb-1 block text-sm text-gray-400">Categoria</p>
  535. <select
  536. bind:value={productFormData.category}
  537. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  538. >
  539. {#each categories as category}
  540. <option value={category.name}>{category.name}</option>
  541. {/each}
  542. </select>
  543. </div>
  544. <div>
  545. <p class="mb-1 block text-sm text-gray-400">Preço (R$)</p>
  546. <input
  547. type="number"
  548. min="0"
  549. step="0.01"
  550. bind:value={productFormData.price}
  551. class="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 focus:ring-emerald-500"
  552. placeholder="0.00"
  553. />
  554. </div>
  555. <div class="flex items-center">
  556. <input
  557. id="sendToKitchen"
  558. type="checkbox"
  559. bind:checked={productFormData.sendToKitchen}
  560. class="h-4 w-4 rounded border border-gray-600 bg-gray-700"
  561. />
  562. <label for="sendToKitchen" class="ml-2 text-sm text-gray-300"
  563. >Enviar para a cozinha</label
  564. >
  565. </div>
  566. <div class="flex space-x-2">
  567. <button
  568. type="button"
  569. on:click={resetProductForm}
  570. class="rounded-lg bg-gray-700 px-4 py-2 hover:bg-gray-600"
  571. >
  572. Cancelar
  573. </button>
  574. <button
  575. type="submit"
  576. class="flex items-center rounded-lg bg-emerald-600 px-4 py-2 hover:bg-emerald-700"
  577. >
  578. <img src={save_icon} alt="Salvar" class="mr-2 h-4 w-4" /> Salvar
  579. </button>
  580. </div>
  581. </form>
  582. {:else}
  583. <!-- Lista de Produtos -->
  584. {#if filteredProducts.length === 0}
  585. <p class="p-4 text-center text-gray-400">Nenhum produto encontrado</p>
  586. {:else}
  587. <div class="overflow-x-auto rounded-lg">
  588. <table
  589. class="min-w-full table-auto divide-y divide-gray-700 text-left text-sm text-white"
  590. >
  591. <thead class="bg-gray-800">
  592. <tr>
  593. <th class="px-6 py-3 font-semibold">Nome</th>
  594. <th class="px-6 py-3 font-semibold">Categoria</th>
  595. <th class="px-6 py-3 font-semibold">Preço</th>
  596. <th class="px-6 py-3 font-semibold">Cozinha</th>
  597. <th class="px-6 py-3 text-center font-semibold">Ações</th>
  598. </tr>
  599. </thead>
  600. <tbody class="divide-y divide-gray-700 bg-gray-800">
  601. {#each filteredProducts as product}
  602. <tr class="transition-colors hover:bg-gray-800">
  603. <td class="px-6 py-4">{product.name}</td>
  604. <td class="px-6 py-4 text-gray-400">{product.category}</td>
  605. <td class="px-6 py-4 text-gray-400">R$ {product.price.toFixed(2)}</td>
  606. <td class="px-6 py-4">
  607. <div class="mr-1 flex items-center justify-center">
  608. {#if product.sendToKitchen}
  609. <img class="h-4 w-4" src={check_icon} alt="check" />
  610. {:else}
  611. <img class="h-5 w-5" src={cancel_icon} alt="cancel" />
  612. {/if}
  613. </div>
  614. </td>
  615. <td class="px-6 py-4">
  616. <div class="flex justify-center gap-4">
  617. <button
  618. on:click={() => handleEditProduct(product)}
  619. class="rounded-lg text-blue-400 hover:bg-blue-900/20"
  620. >
  621. <img src={edit_icon} alt="Editar" class="h-4 w-4" />
  622. </button>
  623. <button
  624. on:click|stopPropagation={() => handleDeleteProduct(product.name)}
  625. class="rounded-lg text-red-400 hover:bg-red-900/20"
  626. >
  627. <img src={trash_icon} alt="Deletar" class="h-4 w-4" />
  628. </button>
  629. </div>
  630. </td>
  631. </tr>
  632. {/each}
  633. </tbody>
  634. </table>
  635. </div>
  636. {/if}
  637. {/if}
  638. </div>
  639. </div>
  640. </div>
  641. </div>