Product.svelte 21 KB

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