CommodityEditModal.svelte 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <script>
  2. import { createEventDispatcher, onDestroy } from 'svelte';
  3. // Control props
  4. export let visible = false;
  5. export let title = 'Editar Commodity';
  6. export let saveText = 'Salvar';
  7. export let cancelText = 'Cancelar';
  8. // Initial value
  9. // Expected shape:
  10. // {
  11. // tipo: 'graos_60kg',
  12. // descricao: '',
  13. // quantidade: number|string,
  14. // preco: number|string,
  15. // vencimentoPagamento: 'YYYY-MM-DD',
  16. // dataLimiteEntrega: 'YYYY-MM-DD',
  17. // cpr: boolean,
  18. // arquivos: File[] | Array<{ name: string, url?: string, type?: string, size?: number }>
  19. // }
  20. export let value = {};
  21. const dispatch = createEventDispatcher();
  22. const defaults = {
  23. tipo: 'graos_60kg',
  24. descricao: '',
  25. quantidade: '',
  26. preco: '',
  27. vencimentoPagamento: '',
  28. dataLimiteEntrega: '',
  29. cpr: false,
  30. arquivos: []
  31. };
  32. let local = { ...defaults };
  33. // Manage object URLs for previews
  34. let objectUrls = [];
  35. onDestroy(() => {
  36. objectUrls.forEach((u) => URL.revokeObjectURL(u));
  37. objectUrls = [];
  38. });
  39. // Sync when modal opens
  40. $: if (visible) {
  41. objectUrls.forEach((u) => URL.revokeObjectURL(u));
  42. objectUrls = [];
  43. local = {
  44. ...defaults,
  45. ...value,
  46. arquivos: value?.arquivos ? [...value.arquivos] : []
  47. };
  48. }
  49. function isFileLike(item) {
  50. return typeof File !== 'undefined' && item instanceof File;
  51. }
  52. function filePreview(item) {
  53. try {
  54. if (isFileLike(item)) {
  55. const url = URL.createObjectURL(item);
  56. objectUrls.push(url);
  57. return url;
  58. }
  59. if (item && item.url) return item.url;
  60. return null;
  61. } catch {
  62. return null;
  63. }
  64. }
  65. function formatSize(bytes) {
  66. if (!bytes && bytes !== 0) return '';
  67. const units = ['B', 'KB', 'MB', 'GB'];
  68. let i = 0;
  69. let size = bytes;
  70. while (size >= 1024 && i < units.length - 1) {
  71. size /= 1024;
  72. i++;
  73. }
  74. return `${size.toFixed(1)} ${units[i]}`;
  75. }
  76. function handleFilesChange(event) {
  77. const files = Array.from(event.currentTarget.files || []);
  78. local.arquivos = files;
  79. }
  80. function handleSave() {
  81. dispatch('save', { value: local });
  82. }
  83. function handleCancel() {
  84. dispatch('cancel');
  85. }
  86. </script>
  87. {#if visible}
  88. <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
  89. <div class="bg-white dark:bg-gray-800 w-full max-w-3xl rounded-lg shadow-lg">
  90. <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
  91. <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100">{title}</h3>
  92. <button
  93. class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
  94. on:click={handleCancel}
  95. aria-label="Fechar"
  96. >
  97. </button>
  98. </div>
  99. <div class="p-6 space-y-5 max-h-[75vh] overflow-y-auto">
  100. <!-- Tipo -->
  101. <div>
  102. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="tipo">Tipo</label>
  103. <select
  104. id="tipo"
  105. class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
  106. bind:value={local.tipo}
  107. >
  108. <option value="graos_60kg">Grãos em sacas de 60kg</option>
  109. </select>
  110. </div>
  111. <!-- Descrição, Quantidade, Preço -->
  112. <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  113. <div>
  114. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="descricao">Descrição</label>
  115. <input
  116. id="descricao"
  117. class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
  118. bind:value={local.descricao}
  119. placeholder="Descreva a commodity"
  120. />
  121. </div>
  122. <div>
  123. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="quantidade">Quantidade</label>
  124. <input
  125. id="quantidade"
  126. type="number"
  127. class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
  128. bind:value={local.quantidade}
  129. min="0"
  130. step="1"
  131. placeholder="0"
  132. />
  133. </div>
  134. <div>
  135. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="preco">Preço (R$)</label>
  136. <input
  137. id="preco"
  138. type="number"
  139. class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
  140. bind:value={local.preco}
  141. min="0"
  142. step="0.01"
  143. placeholder="0,00"
  144. />
  145. </div>
  146. </div>
  147. <!-- Datas -->
  148. <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  149. <div>
  150. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="vencimentoPagamento">Vencimento do Pagamento</label>
  151. <input
  152. id="vencimentoPagamento"
  153. type="date"
  154. class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
  155. bind:value={local.vencimentoPagamento}
  156. />
  157. </div>
  158. <div>
  159. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="dataLimiteEntrega">Data Limite da Entrega</label>
  160. <input
  161. id="dataLimiteEntrega"
  162. type="date"
  163. class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
  164. bind:value={local.dataLimiteEntrega}
  165. />
  166. </div>
  167. </div>
  168. <!-- CPR Checkbox -->
  169. <div class="flex items-center gap-2">
  170. <input
  171. id="cpr"
  172. type="checkbox"
  173. class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
  174. bind:checked={local.cpr}
  175. />
  176. <label for="cpr" class="text-sm text-gray-700 dark:text-gray-300">CPR</label>
  177. </div>
  178. <!-- Arquivos -->
  179. <div>
  180. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="arquivos">Anexos</label>
  181. <input
  182. id="arquivos"
  183. type="file"
  184. class="block w-full text-sm text-gray-700 dark:text-gray-200 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
  185. multiple
  186. on:change={handleFilesChange}
  187. />
  188. {#if local.arquivos && local.arquivos.length}
  189. <ul class="mt-3 space-y-2">
  190. {#each local.arquivos as item}
  191. <li class="flex items-center gap-3 p-2 rounded-md border border-gray-200 dark:border-gray-700">
  192. {#if (isFileLike(item) && item.type?.startsWith('image')) || (!isFileLike(item) && item.type?.startsWith?.('image'))}
  193. {#if filePreview(item)}
  194. <img src={filePreview(item)} alt={isFileLike(item) ? item.name : item.name} class="w-10 h-10 object-cover rounded" />
  195. {/if}
  196. {/if}
  197. <div class="flex-1 min-w-0">
  198. <div class="text-sm text-gray-800 dark:text-gray-100 truncate">{isFileLike(item) ? item.name : item.name}</div>
  199. <div class="text-xs text-gray-500 dark:text-gray-400">{isFileLike(item) ? formatSize(item.size) : (item.size ? formatSize(item.size) : (item.type || ''))}</div>
  200. </div>
  201. </li>
  202. {/each}
  203. </ul>
  204. {/if}
  205. </div>
  206. </div>
  207. <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
  208. <button
  209. class="px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
  210. on:click={handleCancel}
  211. >
  212. {cancelText}
  213. </button>
  214. <button
  215. class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white"
  216. on:click={handleSave}
  217. >
  218. {saveText}
  219. </button>
  220. </div>
  221. </div>
  222. </div>
  223. {/if}